閱讀378 返回首頁    go 阿裏雲 go 技術社區[雲棲]


容器就像監獄,讓我們來構造一個監獄吧!(含代碼下載)

前言

 

容器,近年來最火的話題,在後端的開發中,容器的運用已經是主流技術了。今天,我們就來說說容器技術。首先,雖然目前Docker技術如此火爆,但其實容器技術本質上並不是什麼高大上的東西,總的來講,就是對目前的Linux底層的幾個API的封裝,然後圍繞著這幾個API開發出了一套周邊的環境。
 

目前來說,普遍討論關於容器的文章,一開始都會講到UTC隔離、PID隔離、IPC隔離、文件係統隔離、CGroups係統,今天這一篇,我們換一個視角,從以下幾個方麵來聊一下容器技術。

 

  • 第一部分,從容器和虛擬機說起,都說容器是非常輕量級的,那麼和虛擬機比起來,到底輕在哪裏?

  • 第二部分,通過構造一個監獄,來說明如何建立一個簡單的容器,會涉及到容器的各種技術,當然還有一些沒有涉及到的,我認為不影響理解。

  • 第三部分,通過代碼實戰一把,看看如何一步一步按照第二部分的說明啟動一個容器並運行自己的代碼,這一部分的全部代碼都在github上。

  • 最後,我會再說一下Docker技術,因為Docker從代碼來看,容器技術隻是他的一小部分,完整的Docker遠比單純的容器要複雜,我會簡單的說一下我對Docker的理解,包括Docker使用的其他技術點。

 

容器和虛擬機

 

要說容器,跑不了和虛擬機進行比較,虛擬機是比較古老的技術了,虛擬機的架構圖如下所示。

 

20170106103757538.jpg

 

虛擬機核心是什麼?是模擬硬件,讓虛擬機的操作係統以為自己跑在一個真實的物理機器上,用軟件模擬出來CPU、內存、硬盤、網卡,讓虛擬機裏麵的操作係統覺得自己是在操作真實的硬件,所以虛擬機裏麵的CPU啊,內存啊都是假的,都是軟件模擬出來的(現在有硬件虛擬化技術了,比純軟件模擬要高級一些,但操作係統不管這些),既然操作係統都騙過去了,當然跑在操作係統上的進程同樣也騙過去了唄,所以這些進程都完全感知不到底層硬件的區別,還以為自己很歡樂的跑在一台真實的物理機上了。

 

那麼容器又是什麼鬼?容器的架構圖如下(圖片來源於網絡):

20170106103808667.jpg

 

和虛擬機一個很明顯的區別就是容器其實並沒有模擬硬件,還是那個硬件,還是那個操作係統,隻不過是在操作係統上做了一點文章【這張圖中就是Docker Engine了】,讓進程以為自己運行在了一個全新的操作係統上,有一個很形象的詞來描述他就是軟禁!就是把進程軟禁在一個環境中,讓進程覺得自己很happy,其實一切盡在操作係統的掌控之中,其實虛擬機也是,虛擬機是把操作係統軟禁起來了,讓操作係統覺得很happy,容器是把進程軟禁起來,你看,一個是軟禁操作係統,一個是軟禁進程,這兩個明顯不是一個級別的東西,誰輕誰重不用說了吧。

 

既然容器和虛擬機的區別在於一個是通過模擬硬件來軟禁操作係統,一個是通過做做操作係統的手腳來軟禁進程那麼他們能達到的效果也是不一樣的。

 

  • 對於虛擬機來說,既然是模擬的硬件,那麼就可以在windows上裝linux虛擬機了,因為反正是模擬硬件嘛,虛擬機內部的操作係統也不知道外麵的宿主機是什麼係統。

  • 容器就不一樣了,因為是在操作係統上做文章,所以不可能在linux上裝windows了,並且還有一點就是,容器內的操作係統其實就是外麵宿主機的操作係統,兩者其實是一個,你在容器內用uname -a看到的內核版本和外麵看到的是一樣的。本質上容器就是一個進程,他和宿主機上任何其他進程沒什麼本質的區別。

 

建造容器監獄

 

如何來做一個容器呢?或者說容器是怎麼實現的呢?我們從幾個方麵來說一下容器的實現,一是最小係統,二是網絡係統,三是進程隔離技術,四是資源分配。最小係統告訴你軟禁進程所需要的那個舒適的監獄環境,網絡係統告訴你軟禁的進程如何和外界交互,進程隔離技術告訴你如果把進程關到這個舒適的監獄中去,資源分配告訴你監獄裏的進程如何給他分配資源讓他不能胡來。

 

建設基本監獄【最小係統打造】

 

要軟禁一個進程,當然需要有個監獄了,在說監獄之前,我們先看看操作係統的結構,一個完整的操作係統【Linux/Unix操作係統】分成三部分,如下圖所示【本圖也是網上找的,侵權馬上刪,這個圖是四個部分,包括一個boot參數部分,這不是重點】。

 

20170106103818205.jpg

 

首先是bootloader,這部分啟動部分是匯編代碼,CPU從一個固定位置讀取第一行匯編代碼開始運行,bootloader先會初始化CPU,內存,網卡(如果需要),然後這部分的主要作用是把操作係統的kernel代碼從硬盤加載到內存中,然後bootloader使命完成了,跳轉到kernel的main函數入口開始執行kernel代碼,kernel就是我們熟悉的Linux的內核代碼了,大家說的看內核代碼就是看的這個部分了,kernel代碼啟動以後,會重新初始化CPU,內存,網卡等設備,然後開始運行內核代碼,最後,啟動上帝進程(init),開始正常運行kernel,然後kernel會掛載文件係統

 

好了,到這裏,對進程來說都是無意義的,因為進程不關心這些,進程產生的時候這些工作已經做完了,進程能看到的就是這個文件係統了,對進程來說,內存空間,CPU核心數,網絡資源,文件係統是他唯一能看得見使用得到的東西,所以我們的監獄環境就是這麼幾項核心的東西了。

 

kernel和文件係統是可以分離的,比如我們熟悉的ubuntu操作係統,可能用的是3.18的Linux Kernel,再加上一個自己的文件係統,也可以用2.6的Kernel加上同樣的操作係統。每個Linux的發行版都是這樣的,底層的Kernel可能都是同一個,不同的隻是文件係統不同,所以,可以簡單的認為,Linux的各種發行版就是kernel內核加上一個獨特的文件係統,這個文件係統上有各種各樣的工具軟件。

 

既然是這樣,那麼我們要軟禁一個進程,最基礎的當然要給他一個文件係統啦,文件係統簡單的說就是一堆文件夾加上一堆文件組成的,我們先來生成一個文件係統,我之前是做嵌入式的,嵌入式的Linux係統生成文件係統一般用busybox,隻需要在在ubuntu上執行下麵的命令,就能生成一個文件係統:

 

 

apt-get install busybox-static
mkdir rootfs;cd rootfs
mkdir dev etc lib usr var proc tmp home root mnt sys
/bin/busybox --install -s bin

 

大概這麼幾步就製作完成了一個文件係統,也就是監獄的基本環境已經有了,記得把lib文件夾的內容拷過去。製作完了以後,文件係統就這樣了。

 

20170106103949192.jpg

 

還有一種方式,就是使用debootstap這個工具來做,也是幾行命令就做完了一個debian的文件係統了,裏麵連apt-get都有,Docker的基礎文件係統也是這個。

 

 

apt-get install qemu-user-static debootstrap binfmt-support
mkdir rootfs
debootstrap --foreign wheezy rootfs //wheezy是debian的版本
cp /usr/bin/qemu-arm-static rootfs/usr/bin/

 

完成以後,這個wheezy的文件係統就是一個標準的debian的文件係統了,裏麵的基本工具一應俱全。OK,基本的監獄環境已經搭建好了,進程住進去以後就跟在外麵一樣,啥都能幹,但就是跑不出來。

 

要測試這個環境,可以使用Linux的chroot命令,chroot ./rootfs就進入了這個製作好的文件係統了,你可以試試,看不到外麵的東西了哦。

 

打造探視係統【網絡係統】

 

剛剛隻建立了一個基本的監獄環境,對於現代的監獄,隻有個房子不能上網怎麼行?所以對於監獄環境,還需要建立一個網絡環境,好讓裏麵的進程們可以很方便的和監獄外的親友們聯係啊,不然誰願意一個人呆在裏麵啊。

 

如何來建立一個網絡呢?對於容器而言,很多地方是可配置的,這裏說可配置,其實意思就是可配置也可以不配置,對於網絡就是這樣,一般的容器技術,對網絡的支持有以下幾個方式。

 

  • 無網絡模式,就是不配置模式了,不給他網絡,隻有文件係統,適合單機版的程序。

  • 直接和宿主機使用同一套網絡,也是不配置模式,但是這個不配置是不進行網絡隔離,直接使用宿主機的網卡、ip、協議棧,這是最奔放的模式,各個容器如果啟動的是同一套程序,那麼需要配置不同的端口了,比如有3個容器,都是Redis程序,那麼需要啟動三個各不同的端口來提供服務,這樣各個容器沒有做到完全的隔離,但是這也有個好處,就是網絡的吞吐量比較高,不用進行轉發之類的操作。

  • 網橋模式,也是Docker默認使用的模式,我們安裝完Docker以後會多一個Docker0的網卡,其實這是一個網橋,一個網橋有兩個端口,兩個端口是兩個不同的網絡,可以對接兩塊網卡,從A網卡進去的數據會從B網卡出來,就像黑洞和白洞一樣,我們建立好網橋以後,在容器內建一塊虛擬網卡,把他和網橋對接,在容器外的宿主機上也建立一塊虛擬網卡,和網橋對接,這樣容器裏麵的進程就可以通過網橋這個探視係統和監獄外聯係了。

 

我們可以直接使用第二種不配置模式,直接使用宿主機的網絡,這也是最容易最方便的,但是我們在這裏說的時候稍微說一下第三種的網橋模式吧。

 

網橋最開始的作用主要是用來連接兩個不同的局域網的,更具體的應用,一般是用來連接兩個不同的mac層的局域網的,比如有線電視網和以太網,一般網橋隻做數據的過濾和轉發,也可以適當的做一些限流的工作,沒有路由器那麼複雜,實現起來也比較簡單,對高層協議透明,他能操作的都是mac報文,也就是在ip層以下的報文。

 

對於容器而言,使用網橋的方式是在宿主機上使用brctl命令建立一個網橋,作為容器和外界交互的渠道,也就是大家使用Docker的時候,用ifconfig命令看到的Docker0網卡,這實際上就是一個網橋,然後每啟動一個容器,就用brctl命令建立一對虛擬網卡,一塊給容器,一塊連到網橋上。這樣操作下來,容器中發給虛擬網卡的數據都會發給網橋,而網橋是宿主機上的,是能連接外網的,所以這樣來做到了容器內的進程能訪問外網。

 

容器的網絡我沒有深入研究,感覺不是特別複雜,最複雜的方式就是網橋的方式了,這些網絡配置都可以通過命令行來進行,但是Docker的源碼中是自己通過係統調用實現的,說實話我沒怎麼看明白,功力還是不夠啊。我使用的就是最最簡單的不隔離,和宿主機共用網卡,隻能通過端口來區分不同容器中的服務。

 

監禁皮卡丘【隔離進程】

 

好了,監獄已經建好了,探視係統也有了,得抓人了來軟禁了,把進程抓進來吧。我們以一個最最基本的進程/bin/bash為例,把這個進程抓進監獄吧。

 

說到抓進程,這時候就需要來聊聊容器的底層技術了,Linux提供幾項基礎技術來進行輕量級的係統隔離,這些個隔離技術組成了我們熟悉的Docker的基礎。本篇不會大段的描述這些技術,文章後麵我會給出一些參考鏈接,因為這類文章到處都可以找到,本篇隻是讓大家對容器本身有個了解。

 

下麵所說的所有基礎技術,其實就是一條係統調用,包括Docker的基礎技術,也是這麼一條係統調用(當然,Docker還有很多其他的,但是就容器來說,這條是核心的了)

 

 

clone(進程函數, 進程棧空間, CLONE_NEWUTS | CLONE_NEWPID | CLONE_NEWNS | CLONE_NEWNET |CLONE_NEWUSER | CLONE_NEWIPC , NULL

 

這是一條C語言的clone係統調用,實際上就是啟動一個新的進程,後麵的參數就是各種隔離了,包括UTS隔離、PID隔離、文件係統隔離、網絡隔離、用戶隔離、IPC通訊隔離。

 

在go語言中,沒有clone這個係統調用(不知道為什麼不做這個係統調用,可能是為了多平台的兼容吧),必須使用exec.Cmd這個對象來啟動進程,在linux環境下,可以設置Cmd的attr屬性,其中有個屬性叫CloneFlags,可以把上麵那些個隔離信息設置進去,這樣,啟動的進程就是我們需要的了,我們可以這麼來啟動這個進程。

 

 

cmd := exec.Command("./container", args...)

cmd.SysProcAttr = &syscall.SysProcAttr{

        Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS | syscall.CLONE_NEWNET,

    }

    cmd.Run()

 

這樣,通過這個cmd命令啟動的./container進程就是一個隔離進程了,也就是我們把這個進程給關起來了,他已經看不到其他東西了,是不是很簡單?但是你要是就直接這麼運行,還是看不到什麼特別的地方。

 

在這個之後,我們需要按照上麵所說的,把監獄先建立好,監獄的建立在./container中進行,建立監獄也比較簡單,基本上也是一堆係統調用,比如文件係統的軟禁,就像下麵的一樣。

 

 

syscall.Mount(rootfs, tmpMountPoint, "", syscall.MS_BIND, "")  //掛載根文件係統

syscall.Mount(rootfs+"/proc", tmpMountPoint+"/proc", "proc", 0, "");  //掛載proc文件夾

syscall.PivotRoot(tmpMountPoint, pivotDir)  //把進程軟禁到根文件係統中

 

關於上麵proc文件夾,做了特殊處理,在Linux中,proc文件夾的地位比較特殊,具體作用可以自行查文檔,簡單的說就是保存係統信息的文件夾。在這裏,devsys這兩個特殊文件夾也需要做特殊處理的,這裏沒有寫出來而已。

 

這些都做完了以後,就可以啟動真正需要執行的進程了,比如/bin/bash,或者你自己的程序,這樣啟動的/bin/bash或者你自己的程序就是在監獄中啟動的了,那麼他看到的所有東西都是監獄中的了,外麵宿主機的一切對他來說都是屏蔽的了,這樣,一個Docker的雛形就產生了。

 

這裏多說一下,通過clone係統調用啟動的進程,它自己看到自己的PID是1,也就是上帝進程了,這個上帝進程可以來造基礎監獄【文件係統】,打造放風係統【網絡係統】,然後再通過它來生成新的進程,這些進程出來就在監獄中了,我們使用Docker的時候,自己的服務實際上就是這些個在監獄中出生的進程【可能我的描述不太正確啊,我沒有仔細看Docker的源碼,我自己感覺是這樣的】。

 

至此,我們來總結一下,啟動一個最簡單的容器並運行你自己的進程,需要幾步。

 

  • 建立一個監獄【文件係統】,使用busybox或者debootstrap建立。

  • 建立一個放風係統【網絡係統】,使用網橋或者不隔離網絡,直接使用宿主機的網卡。

  • 抓一個皮卡丘【啟動上帝進程】並放到監獄中【掛載文件係統,初始化網絡】,配置Cloneflags的值,並通過exec.Cmd來進行上帝進程的啟動。

  • 讓皮卡丘生個孩子【啟動你自己的程序】,直接調用exec.Cmd的run方法啟動你自己的進程。

  • 完成。

 

通過上麵幾步,最簡容器就完成了,是不是很簡單?但是容器僅僅有這些是不夠的,我們還有三個隔離沒有講,這裏稍微提一下吧。

 

  • 一個是UTS隔離,主要是用來隔離hostname和域名的。

  • 一個是User隔離,這樣容器裏麵的用戶和宿主機用戶可以做映射,意思就是裏麵雖然看到的是root用戶,但是實際上並不是root,不能夠瞎搞係統,這樣容器的安全性會有保障。

  • 一個是IPC隔離,這個是進程通訊的隔離,這樣容器裏麵的進程和容器外麵的進程就不能進行進程間通訊了,保證了比較強的隔離性。

 

給犯人分配食物【資源配置】

 

我們知道,一般的監獄中的食物是定量的,畢竟不是每個監獄都可以吃自助餐的,容器也一樣,要是我們就啟個容器啥都不限製,裏麵要是有個牛逼的程序員寫的牛逼程序,瞬間就把你的內存和CPU給幹沒了。比如像下麵這個fork炸彈。【下麵程序請不要嚐試!!】

 

 

int main(){

    while(fork());

}

 

在容器技術中,Cgroups【control groups】就是幹這個事情的,cgroups負責給監獄設定資源,比如能用幾個cpu啊,cpu能給你多少百分比的使用量啊,內存能用多少啊,磁盤能用多少啊,磁盤的速度能給你多少啊,各種資源都可以從cgroups來進行配置,把這些東西配置給容器以後,就算容器裏麵運行一個fork炸彈也不怕了,反正影響不到外麵的宿主機,到這裏,容器已經越來越像虛擬機了。

 

cgroups是linux內核提供的API,雖然是API,但它的整個實現完美滿足了Linux兩大設計哲學之一:一切皆文件(還有一個哲學是通訊全管道),對API的調用實際上是操作文件。

 

我們以cpu的核心數看看如何來做一個cgroups的資源管理。假設我們的物理機是個8核的cpu,而我們剛剛啟動的容器我隻想讓他使用其中的兩個核,很簡單,我們用命令行直接操作sys/fs/cgroups文件夾下的文件來進行。這個配置我們可以在啟動的上帝進程中進行,也可以在容器外部進行,都是直接操作文件。

 

關於cgroups這個東西很複雜也很強大,其實在容器出來之前,好的運維工程師就已經把這個玩得很熘了。Docker也隻是把這些個文件操作封裝了一下,變成了docker的啟動和配置參數而已。

 

親自抓一次進程吧

 

好了,該說的都說了,我們來實戰一把,自己啟一個容器吧,並且啟動以後為了更直觀的看到效果,我們啟動一個ssh服務,打開22332端口,然後外麵就可以通過ssh連到容器內部了,這時候你愛幹什麼幹什麼了。

 

製作文件係統

 

文件係統製作我們直接使用debootstrap進行製作,在/root/目錄下建立一個rootfs的文件夾,然後使用debootstrap --foreign wheezy rootfs製作文件係統,製作完了以後,文件係統就是下麵這個樣子

 

20170106104002681.jpg

 

製作初始化腳本

 

初始化腳本就做兩件事情,一是啟動ssh服務,一是啟動一個shell,提前先把/etc/ssh/sshd_config中的端口改成23322。

 

 

#!/bin/bash

service ssh start

/bin/bash

 

然後把這個腳本放到製作的文件係統的root目錄下,加上執行權限。

 

啟動上帝進程

 

文件係統製作完成了,啟動腳本也做完了,我們看看我們這個容器的架構,架構很簡單,整個容器分為兩個獨立的進程,兩份獨立的代碼。

 

  • 一個是主進程【wocker.go】,這個進程本身就是一個http的服務,通過get方法接收參數,參數有rootfs的地址,容器的hostname,需要監禁的進程名稱(這裏就是我們的第二個進程【startContainer.go】),然後通過exec.Cmd這個包啟動這個進程。

  • 第二個進程啟動就是以隔離方式啟動的了,就是容器的上帝進程了,這個進程中進行文件係統掛載,hostname設置,權限係統的設定,然後啟動正式的服務進程(也就是我們的啟動腳本/root/start_container.sh

 

掛載文件係統

 

第二個進程是容器的上帝進程,在這裏進行文件係統的掛載,最重要的代碼如下

 

 

    syscall.Mount(rootfs, tmpMountPoint, "", syscall.MS_BIND, "") //掛載根文件係統

    syscall.Mount(procpath, tmpMountPointProc, "proc", 0, "")  //掛載proc文件夾,用來看係統信息的

    syscall.Mount(syspath, tmpMountPointSys, "sysfs", 0, "")    //掛載sys文件夾,用來做權限控製的

    syscall.Mount("udev", tmpMountPointDev, "devtmpfs", 0, "") //掛載dev,用來使用設備的

    syscall.PivotRoot(tmpMountPoint, pivotDir)//進入到文件係統中

 

具體代碼可以看github上的文件,這樣,根文件係統就掛載完了,已經進入了基本監獄中了。

 

啟動初始化腳本

 

文件係統掛載完了以後,然後啟動初始化腳本,這個就比較簡單了,一個exec.Cmd的Run方法調用就搞定了。

 

 

cmd := exec.Command("/root/start_container.sh")

 

這樣,ssh服務就在容器中啟動了,可以看到一行Starting OpenBSD Secure Shell server: sshd.的打印信息,容器啟動完成,這時候,我們可以通過ssh root@127.0.0.1 -p 23322這個命令登錄進我們的容器了,然後你就可以為所欲為了。

 

20170106104014610.jpg
 

上麵那個圖,我們看到登錄進來以後,hostname已經顯示為我們設定的hello了,這時這個會話已經在容器裏麵了,我們ps一下看看進程們。

 

20170106104034431.jpg

 

看到pid為1的進程了麼,那個就是啟動這個容器的上帝進程了。恩,到這裏,我們已經在容器中了,這裏啟動的任何東西都和我們知道的Docker中的進程沒什麼太大區別了。但在這裏,我缺失了權限的部分,大家可以自己加上去,主要是各種文件操作比較麻煩……

 

關於Docker的思考

 

Docker這門最近兩年非常火的技術,光從容器的角度來看的話,也不算什麼新的牛逼技術了,和虛擬機比起來還是要簡單不少,當然,Docker本身可完全不止容器技術本身,還有AUFS文件分層技術,還有etcd集群技術,最關鍵的是Docker通過自己的整個生態把容器包裹在裏麵了,提供了一整套的容器管理套件,這樣讓容器的使用變得異常簡單,所以Docker才能這麼流行吧。和虛擬機比起來,Docker的優點實在是太多了。

 

  • 首先,從易用性的角度來說,管理一個虛擬機的集群,有一整套軟件係統,比如openstack這種,光熟悉這個openstack就夠喝一壺的了,而且openstack的網絡管理異常複雜,哦,不對,是變態級的複雜,要把網絡調通不是那麼容易的事情。

  • 第二,從性能上來看看,我們剛剛說了容器的原理,所以實際上容器不管是對cpu的利用,還是內存的操作或者外部設備的操作,對一切硬件的操作實際上都是直接操作的,並沒有經過一個中間層進行過度,但是虛擬機就不一樣了,虛擬機是先操作假的硬件,然後假硬件再操作真硬件,利用率從理論上就會比容器的要差,雖然現在有硬件虛擬化的技術了能提升一部分性能,但從理論上來說性能還是沒有容器好,這部分我沒有實際測試過啊,隻是從理論上這麼覺得的,如果有不對的歡迎拍磚啊。

  • 第三,從部署的易用性上和啟動時間上,容器就完全可以秒了虛擬機了,這個不用多說吧,一個是啟動一台假電腦,一個是啟動一個進程。

 

那麼,Docker和虛擬機比起來,缺點在哪裏呢?我自己想了半天,除了資源隔離性沒有虛擬機好以外,我實在是想不出還有什麼缺點,因為cgroups的隔離技術隻能設定一個上限,比如在一台4核4G的機器上,你可能啟動兩個Docker,給他們的資源都是4核4G,如果有個Docker跑偏了,一個人就幹掉了4G內存,那麼另外一個Docker可能申請不到資源了。

 

而虛擬機就不存在這個問題,但是這也是個雙刃劍,Docker的這種做法可以更多的榨幹係統資源,而虛擬機的做法很可能在浪費係統資源。除了這個,我實在是想不出還有其他缺點。網上也有說權限管理沒有虛擬機好,但我覺得權限這東西,還是得靠人,靠軟件永遠靠不住。


最後,代碼都在github上,隻有非常非常簡單的三個文件【一個Container.go是容器類,一個wocker.go沒內容,一個startContainer.go啟動容器】,那個http服務留著沒寫,後麵寫http服務的時候在用一下。嗯,Docker確實是個好東西。

原文發布時間為:2017-01-06

本文來自雲棲社區合作夥伴DBAplus

最後更新:2017-05-13 08:43:48

  上一篇:go  巧用利器Powershell,讓數據庫自動化運維事半功倍
  下一篇:go  MySQL InnoDB事務結構體代碼變量全攻略(附源碼)