- 容器起于PaaS
- Docker項目具有裡程碑意義
- Docker項目通過“容器鏡像”,解決應用打包這個根本難題
容器本身沒有價值,有價值的是“容器編排”
正因為如此,容器技術生态才爆發了一場關于“容器編排”的“戰争”
而這次戰争,最終以Kubernetes項目和CNCF社群的勝利而告終。
是以會以Docker和Kubernetes項目為核心,為你詳細介紹容器技術的各項實踐與其中的原理。
容器,到底是怎麼一回事兒?
容器其實是一種沙盒技術
就是能夠像一個集裝箱一樣,把你的應用“裝”起來的技術。這樣,應用與應用之間,就因為有了邊界而不至于互相幹擾
而被裝進集裝箱的應用,也可以被友善地搬來搬去,這不就是PaaS最理想的狀态嘛。
這兩個能力說起來簡單,但要用技術手段去實作它們,可能大多數人就無從下手了。
就先來說說這個
“邊界”的實作手段
現在要寫一個計算加法的程式
輸入來自于一個檔案
輸出到另一個檔案中。
由于計算機隻認識0和1,是以無論用哪種語言編寫這段代碼,最後都需要通過某種方式翻譯成二進制檔案,才能在計算機作業系統中運作起來。
而為了能夠讓這些代碼正常運作,我們往往還要給它提供資料,比如加法程式所需要的輸入檔案
這些資料加上代碼本身的二進制檔案,放在磁盤上,就是我們平常所說的一個“程式”,也叫代碼的可執行鏡像(executable image)
然後,我們就可以在計算機上運作這個“程式”了。
首先OS從“程式”中發現輸入資料儲存在一個檔案中,是以這些資料就被會加載到記憶體中待命
同時OS又讀取到了計算加法的指令,這時,它就需要訓示CPU完成加法操作。而CPU與記憶體協作進行加法計算,又會使用寄存器存放數值、記憶體堆棧儲存執行的指令和變量
同時,計算機裡還有被打開的檔案,以及各種各樣的I/O裝置在不斷地調用中修改自己的狀态
一旦“程式”被執行起來,它就從磁盤上的二進制檔案,變成了計算機記憶體中的資料、寄存器裡的值、堆棧中的指令、被打開的檔案,以及各種裝置的狀态資訊的一個集合
像這樣一個程式運起來後的計算機執行環境的總和,就是程序
程序的靜态表現就是程式,平常都安安靜靜地待在磁盤上
而一旦運作起來,它就變成了計算機裡的資料和狀态的總和,這就是它的動态表現。
而容器技術的核心功能,就是通過限制和修改程序的動态表現,進而為其創造出一個“邊界”
對于Docker等大多數Linux容器來說
- Cgroups技術制造限制的主要手段
- Namespace技術修改程序視圖的主要方法。
你可能會覺得Cgroups和Namespace這兩個概念很抽象,别擔心,接下來我們一起動手實踐一下,你就很容易了解這兩項技術了。
假設你已經有了一個Linux作業系統上的Docker項目在運作,比如我的環境是Ubuntu 16.04和Docker CE 18.05。
接下來,讓我們首先建立一個容器來試試。
$ docker run -it busybox /bin/sh
-it告訴了Docker項目在啟動容器後,需要給我們配置設定一個文本輸入/輸出環境,也就是TTY,跟容器的标準輸入相關聯,這樣我們就可以和這個Docker容器進行互動了。而/bin/sh就是我們要在Docker容器裡運作的程式。
請幫我啟動一個容器,在容器裡執行/bin/sh,并且給我配置設定一個指令行終端跟這個容器互動。
這樣機器就變成了一個主控端,而一個運作着/bin/sh的容器,就跑在了這個主控端裡面。
容器裡執行一下ps指令
# ps
PID USER TIME COMMAND
1 root 0:00 /bin/sh
10 root 0:00 ps
可以看到,我們在Docker裡最開始執行的/bin/sh,就是這個容器内部的第1号程序(PID=1)
而這個容器裡一共隻有兩個程序在運作
這就意味着,前面執行的/bin/sh,以及我們剛剛執行的ps,已經被Docker隔離在了一個跟主控端完全不同的世界當中。
其實每當我們在主控端上運作了一個/bin/sh程式,作業系統都會給它配置設定一個程序編号,比如PID=100
這個編号是程序的唯一辨別,就像工号
是以PID=100,可以粗略地了解為這個/bin/sh是我們公司裡的第100号員工
現在,我們要通過Docker把這個/bin/sh程式運作在一個容器當中,Docker就會在這個第100号員工入職時給他施一個“障眼法”,讓他永遠看不到前面的其他99個員工
這樣,他就會錯誤地以為自己就是公司裡的第1号員工。
這種機制,其實就是對被隔離應用的程序空間做了手腳,使得這些程序隻能看到重新計算過的程序編号,比如PID=1
實際上,他們在主控端的作業系統裡,還是原來的第100号程序。
這種技術,就是Linux裡面的Namespace機制
它其實隻是Linux建立新程序的一個可選參數
在Linux系統中建立線程的系統調用是clone(),比如:
int pid = clone(main_function, stack_size, SIGCHLD, NULL);
這個系統調用就會為我們建立一個新的程序,并且傳回它的程序号pid。
而當我們用clone()系統調用建立一個新程序時,就可以在參數中指定CLONE_NEWPID參數,比如:
int pid = clone(main_function, stack_size, CLONE_NEWPID | SIGCHLD, NULL);
這時,新建立的這個程序将會“看到”一個全新的程序空間,在這個程序空間裡,它的PID是1
之是以說“看到”,是因為這隻是一個“障眼法”,在主控端真實的程序空間裡,這個程序的PID還是真實的數值,比如100。
可以多次執行clone(),建立多個PID Namespace,而每個Namespace裡的應用程序,都會認為自己是目前容器裡的第1号程序,它們既看不到主控端裡真正的程序空間,也看不到其他PID Namespace裡的具體情況
除了剛剛用到的PID Namespace,Linux作業系統還提供了Mount、UTS、IPC、Network和User這些Namespace,用來對各種不同的程序上下文進行“障眼法”操作:
- Mount Namespace 讓被隔離程序隻看到目前Namespace裡的挂載點資訊
- Network Namespace 讓被隔離程序看到目前Namespace裡的網絡裝置和配置
這就是Linux容器最基本的實作原理
是以Docker容器是在建立容器程序時,指定了這個程序所需要啟用的一組Namespace參數
這樣,容器就隻能“看”到目前Namespace所限定的資源、檔案、裝置、狀态,或者配置
而對于主控端以及其他不相關的程式,它就完全看不到了。
是以容器,其實是一種特殊的程序而已。
總結
談到為“程序劃分一個獨立空間”的思想,相信你一定會聯想到虛拟機
你應該還看過一張虛拟機和容器的對比圖。
左邊虛拟機的工作原理
名為Hypervisor的軟體是虛拟機最主要的部分,它通過硬體虛拟化功能,模拟出了運作一個作業系統需要的各種硬體,比如CPU、記憶體、I/O裝置等等
然後,它在這些虛拟的硬體上安裝了一個新的作業系統,即Guest OS。
這樣,使用者的應用程序就可以運作在這個虛拟的機器中,它能看到的自然也隻有Guest OS的檔案和目錄,以及這個機器裡的虛拟裝置。這就是為什麼虛拟機也能起到将不同的應用程序互相隔離的作用。
右邊,名為Docker Engine的軟體替換了Hypervisor
這也是為什麼,很多人會把Docker項目稱為“輕量級”虛拟化技術的原因
實際上就是把虛拟機的概念套在了容器
可是這樣的說法,卻并不嚴謹
跟真實存在的虛拟機不同,在使用Docker的時候,并沒有一個真正的“Docker容器”運作在主控端裡面
Docker項目幫助使用者啟動的,還是原來的應用程序,隻不過在建立這些程序時,Docker為它們加上了各種各樣的Namespace參數
這些程序就會覺得自己是各自PID Namespace裡的第1号程序,隻能看到各自Mount Namespace裡挂載的目錄和檔案,隻能通路到各自Network Namespace裡的網絡裝置,就仿佛運作在一個個“容器”
參考
- Github
- docker官網
- Docker實戰
- 深入剖析Kubernetes