歡迎光臨
每天分享高質量文章

手把手教你寫Docker

模擬Docker實現一個簡單的容器,不到 200行代碼(包括空行、註釋、異常處理),這並不是吹牛B。容器技術幾乎是Linux kernel內置的模塊,我們簡單呼叫一下API就能搞定很多事情。當然你要考慮各種商業因素、政治因素那就會成長為Docker這種量級的代碼量了。
盜用一下朋友圈裡的段子:小公司與大公司的區別就是,以殺豬為例,小公司是找到豬直接亂刀砍死。大公司要先做一套籠具抓豬,再做一套流程磨刀,再發明一套刀法(工程師通常會就刀法爭論很久)殺豬。抓豬的籠具除了能抓豬還能抓跳騷,磨刀的工具除了能磨柴刀,還能磨指甲刀。殺豬的流程除了能殺豬,也能殺雞。做完了之後你只敲一個殺豬的命令就行。你不知道豬在哪裡,因為這是另一個人負責的,代碼放在你不知道的某個目錄下;你也不知道刀在哪裡,因為目錄不可見,格式不可讀。刀法是啥你也不知道。這套系統理論上威力無比,一群人費了老大勁做出來,除了用柴刀殺豬沒乾過別的,殺雞從來沒測試過,殺跳騷代碼都不完整。但是公司里的所有人都覺得,殺豬就應該這樣。所以大家每天忙忙碌碌,豬快活的過了一年又一年。
所以這系列文章我主要介紹如何找到豬、怎麼持刀不傷到自己,如何發力能夠更凶狠;然後現場表演一下把一頭活蹦亂跳的豬捅死。


涉及到的技術

寫一個容器只需要兩個技術——Namespace和CGroup,而這兩個東西都是Linux kernel提供的,我們要做的就是——呼叫一下。無恥的盜用一下Brendan Gregg大神的圖。

這張圖中蘊含了一個經常被忽視的細節——容器是共享內核的,它們屬於多個行程同時運行在一個內核上,只不過是利用Namespace把它們隔離開,用CGroup限制可用資源。而虛擬機是共享“硬體”的,每個虛擬機都有自己獨立的操作系統。所以,虛擬機是可引導的、絕對安全的隔離技術;而容器是非常脆弱的,不安全的隔離技術。
Namespace是Linux內核提供的一種隔離技術,它提供了六種隔離空間:
Namespace 系統呼叫引數 說明
PID CLONE_NEW_PID 隔離行程編號
UTS CLONE_NEWUTS 主機名
IPC CLONE_NEWIPC 行程通訊相關的,信號量、訊息佇列、記憶體映射
Network CLONE_NEWNET 網絡協議棧、ARP表、路由表、NAT表
Mount CLONE_NEWNS 檔案系統掛載點
User CLONE_NEWUSER 用戶、組名
看的一臉懵逼對不對?沒關係,簡單的解釋一下。
學過操作系統原理的同學都知道(沒學過?你還敢在這個行業混?),在一個內核所有行程都共享操作系統定義的資源——主機名、域名、ARP表、路由表、NAT表;檔案系統、用戶和組、行程編號。以主機名為例,它是由操作系統定義在一塊記憶體空間中的,所以行程A能看到,行程B也能看到(如果有權限甚至可以修改)。Namespace提供了一種隔離技術,可以讓每個行程都定義“自己的主機名”。你可以理解為內核為每個行程都提供了一份當前主機名的備份,行程當然可以修改這份資料,但是這個修改只能作用於自己,其他行程感知不到——因為它不再是“全域性”的。
經常有人問是不是所有應用都可以做容器化?理解Namespace就很容易回答這個問題。容器技術本質上還是共享內核,所以任何需要修改內核的應用都不可以被容器化。比如LVS、OpenvSwtich這些需要加載內核模塊的應用都沒有辦法做成容器。
Hello world

呼叫Namespace非常簡單,只需要一個API(沒錯,一個,只要一個)——clone。


它會創建一個新的執行緒(內核不會太區分執行緒和行程),第一個引數指定了執行緒的代碼入口,第二個引數是執行緒棧,第三個引數是標誌位,第四個引數是代碼入口的引數指標。
我們上面所羅列的Namespace引數就是通過第三個引數——標誌位傳遞的。

我們先測試一下UTS(主機名)是否能正常工作,因為子行程不涉及到遞迴呼叫所以定義1024位元組的stack大小應該足夠了。main方法里的os.waitpid(pid, 0)是必須的,否則子行程會因為父行程終止而提前退出。
child_func是子行程的入口,這段代碼里我們呼叫sethostname修改主機然後再執行hostname驗證修改是否生效了。
libc是我封裝好的系統呼叫,非常簡單。

小試牛刀一下:

首先在父行程中輸出自己的行程編號和子行程的編號,然後在子行程中輸出自己的行程編號和父行程的編號。在子行程中我們呼叫sethostname修改了主機名並且通過hostname驗證了呼叫結果。但是這個修改並沒有波及到內核,最後我們在shell中呼叫hostname驗證了這一結果。


要有Shell

上面只是執行一次修改hostname的動作,動作有點小,不夠過癮。我們希望能夠在獨立的Namespace中拿到一個shell。


只需要更改兩行代碼。父行程裡面增加NEW_PID、NEW_IPC的標誌位,子行程里呼叫execle執行bash,通過最後一個引數指定了環境變數PS1,這個表示提示符。

再次執行,我們發現shell已經變化了。通過hostname驗證我們已經“在容器裡面”了。鍵入exit,退出容器。

是不是已經無法掩蓋自己內心的興奮了。別急,還有更興奮的,我們進行第三步——分離檔案系統。


徹底分離

如果你在上一部的shell中輸入一些top、ps、ls命令會發現幾乎和“Host”環境中一摸一樣。這是因為我們還沒有做最重要的一部——分離檔案系統。
Docker提供的有Ubuntu、CentOS的鏡像,其實這些並不是嚴格意義上的鏡像,它們準確的叫法應該是——根檔案系統(root filesystem)。
容器是共享內核的,所以無論是Ubuntu、CentOS它們裡面都使用Host的內核,如果你在Docker中通過uname查看會發現無論什麼鏡像它們的內核版本都和Host一摸一樣。所以,不同“操作系統”Docker鏡像其實就是不同的根檔案系統。
很多人用BusyBox的rootfs做演示,作為一個風騷的男人怎麼怎麼可能如此俗套。所以我用CentOS 7作為演示。
真正的原因切換容器中的根目錄,後續的代碼執行會使用新的根檔案系統,而後續的代碼是依賴Python運行環境的。所以我們需要一個帶Python的rootfs,CentOS 7剛好滿足這個。如果我們用C或者Golang就不會有這個限制了。
你可以通過CentOS提供的Dockerfile找到相關的rootfs的下載,比如:https://github.com/CentOS/sig-cloud-instance-images/tree/79db851f4016c283fb3d30f924031f5a866d51a1/docker。

把下載到的檔案解壓到/tmp目錄下。

分離檔案系統分為三個步驟,首先我們建立容器裡面的/proc檔案系統,很多Linux命令都是讀取這個檔案系統下的內容(比如top中顯示的行程串列);其次我們要把現在的用戶和容器裡面的用戶做映射,否則會提示權限不足;最後我們要通過pivot_root 函式把“切換”根檔案系統。

不要忘記修改main方法,為標誌位增加三個引數,映射用戶。

再次執行。

和CentOS 7一摸一樣,你甚至可以用yum命令,當然由於我們現在還沒有實現網絡功能所以yum會告訴你無法訪問網絡。
再多執行幾個添加檔案、刪除檔案看看?你會發現無論做什麼動作最終的資料都會被牢牢地固定在/tmp/rootfs下,也就是說——在容器裡面我們是沒有辦法訪問host的檔案的。
完整代碼:https://github.com/fireflyc/mini-docker。
本文轉載自公眾號:寫程式的康德。

Kubernetes零基礎進階培訓

本次培訓內容包括:容器原理、Docker架構及工作原理、Docker網絡與儲存方案、Harbor、Kubernetes架構、組件、核心機制、插件、核心模塊、Kubernetes網絡與儲存、監控、日誌、二次開發以及實踐經驗等,點擊瞭解具體培訓內容

4月20日正式上課,點擊閱讀原文鏈接即可報名。
赞(0)

分享創造快樂