1. 程式人生 > >百度微服務架構師隨手筆記:教你如何手寫Docker

百度微服務架構師隨手筆記:教你如何手寫Docker

names lar 朋友 -i oot 封裝 路由 必須 spa

模擬Docker實現一個簡單的容器,不到 200行代碼(包括空行、註釋、異常處理),這並不是吹牛B。容器技術幾乎是Linux kernel內置的模塊,我們簡單調用一下API就能搞定很多事情。當然你要考慮各種商業因素、政治因素那就會成長為Docker這種量級的代碼量了。

盜用一下朋友圈裏的段子:小公司與大公司的區別就是,以殺豬為例,小公司是找到豬直接亂刀砍死。大公司要先做一套籠具抓豬,再做一套流程磨刀,再發明一套刀法(工程師通常會就刀法爭論很久)殺豬。抓豬的籠具除了能抓豬還能抓跳騷,磨刀的工具除了能磨柴刀,還能磨指甲刀。殺豬的流程除了能殺豬,也能殺雞。做完了之後你只敲一個殺豬的命令就行。你不知道豬在哪裏,因為這是另一個人負責的,代碼放在你不知道的某個目錄下;你也不知道刀在哪裏,因為目錄不可見,格式不可讀。刀法是啥你也不知道。這套系統理論上威力無比,一群人費了老大勁做出來,除了用柴刀殺豬沒幹過別的,殺雞從來沒測試過,殺跳騷代碼都不完整。但是公司裏的所有人都覺得,殺豬就應該這樣。所以大家每天忙忙碌碌,豬快活的過了一年又一年。

所以這系列文章我主要介紹如何找到豬、怎麽持刀不傷到自己,如何發力能夠更兇狠;然後現場表演一下把一頭活蹦亂跳的豬捅死。

涉及到的技術

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

技術分享圖片

這張圖中蘊含了一個經常被忽視的細節——容器是共享內核的,它們屬於多個進程同時運行在一個內核上,只不過是利用Namespace把它們隔離開,用CGroup限制可用資源。而虛擬機是共享“硬件”的,每個虛擬機都有自己獨立的操作系統。所以,虛擬機是可引導的、絕對安全的隔離技術;而容器是非常脆弱的,不安全的隔離技術。

Namespace是Linux內核提供的一種隔離技術,它提供了六種隔離空間:

技術分享圖片

看的一臉懵逼對不對?沒關系,簡單的解釋一下。

學過操作系統原理的同學都知道(沒學過?你還敢在這個行業混?),在一個內核所有進程都共享操作系統定義的資源——主機名、域名、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- ... ocker

技術分享圖片

把下載到的文件解壓到/tmp目錄下。

技術分享圖片

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

技術分享圖片

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

技術分享圖片

再次執行。

技術分享圖片

和CentOS 7一摸一樣,你甚至可以用yum命令,當然由於我們現在還沒有實現網絡功能所以yum會告訴你無法訪問網絡。

再多執行幾個添加文件、刪除文件看看?你會發現無論做什麽動作最終的數據都會被牢牢地固定在/tmp/rootfs下,也就是說——在容器裏面我們是沒有辦法訪問host的文件的。

完整代碼:https://github.com/fireflyc/mini-docker。

推薦一個交流學習群:685167672 裏面會分享一些資深架構師錄制的視頻錄像:有Spring,MyBatis,Netty源碼分析,高並發、高性能、分布式、微服務架構的原理,JVM性能優化這些成為架構師必備的知識體系。還能領取免費的學習資源,目前受益良多:

百度微服務架構師隨手筆記:教你如何手寫Docker