'Docker容器實戰(五) - 特殊的進程'

"
  • 容器起於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所限定的資源、文件、設備、狀態,或者配置

而對於宿主機以及其他不相關的程序,它就完全看不到了。

所以容器,其實是一種特殊的進程而已。

總結

談到為“進程劃分一個獨立空間”的思想,相信你一定會聯想到虛擬機

你應該還看過一張虛擬機和容器的對比圖。

"
  • 容器起於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所限定的資源、文件、設備、狀態,或者配置

而對於宿主機以及其他不相關的程序,它就完全看不到了。

所以容器,其實是一種特殊的進程而已。

總結

談到為“進程劃分一個獨立空間”的思想,相信你一定會聯想到虛擬機

你應該還看過一張虛擬機和容器的對比圖。

Docker容器實戰(五) - 特殊的進程

左邊虛擬機的工作原理

名為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
"

相關推薦

推薦中...