後端的輪子(四)--- 容器

吳YH堅發表於2016-08-11

容器,目前最火的話題了,在後端的開發中,容器的運用也已經是主流技術了,今天,我們就來說說容器技術,之前我對這一塊的瞭解不是很多,但是最近有些特殊原因轉成運維工程師了,而公司的全線服務都是docker的,以一個開發人員的習慣,轉成運維以後,還是想對這種東西總想深入瞭解一下,於是看了不少相關資料並且看了一下docker的原始碼,發現這東西確實很厲害,和之前腦中的docker印象完全不同,於是有了這篇文章。

先說結論,容器真的很好,很輕量級,功能又很重量級。

前言

首先,雖然目前docker技術如此火爆,但是其實容器技術本上並不是什麼高大上的東西,總的來講,就是對目前的Linux底層的幾個API的封裝,然後圍繞著這幾個API開發出了一套周邊的環境。

之前所有的講關於容器的文章,一開始就開始講UTC隔離,PID隔離,IPC隔離,檔案系統隔離,CGroups系統,今天這一篇,我們換一個視角,我們從以下幾個方面來說一下容器技術。

  • 首先,我們從容器和虛擬機器說起,都說容器是非常輕量級的,那麼和虛擬機器比起來,到底輕在什麼地方呢。
  • 第二部分,我們會通過一步一步的說明,通過構造一個監獄,來說明如何建立一個簡單的容器,會涉及到容器的各種技術,當然還有一些沒有涉及到的,我認為不影響理解。
  • 第三部分,我們會通過程式碼實戰一把,看看如何一步一步按照第二部分的說明啟動一個容器並執行自己的程式碼,這一部分的全部程式碼都在github上。
  • 最後,我會再說一下docker技術,因為docker從程式碼來看,容器技術只是他的一小部分,完整的docker遠比單純的容器要複雜,我會簡單的說一下我對docker的理解,包括docker使用的其他技術點。

容器和虛擬機器

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

虛擬機器核心是什麼?是模擬硬體,讓虛擬機器的作業系統以為自己跑在一個真實的物理機器上,用軟體模擬出來CPU,記憶體,硬碟,網路卡,讓虛擬機器裡面的作業系統覺得自己是在操作真實的硬體,所以虛擬機器裡面的CPU啊,記憶體啊都是假的,都是軟體模擬出來的(現在有硬體虛擬化技術了,比純軟體模擬要高階一些,但作業系統不管這些),既然作業系統都騙過去了,當然跑在作業系統上的程式同樣也騙過去了唄,所以這些程式都完全感知不到底層硬體的區別,還以為自己很歡樂的跑在一臺真實的物理機上了。

那麼容器又是什麼鬼呢?容器的架構圖如下(這張圖網上找的,侵權馬上刪)

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

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

  • 對於虛擬機器來說,既然是模擬的硬體,那麼就可以在windows上裝linux虛擬機器了,因為反正是模擬硬體嘛,虛擬機器內部的作業系統也不知道外面的宿主機是什麼系統。
  • 容器就不一樣了,因為是在作業系統上做文章,所以不可能在linux上裝windows了,並且還有一點就是,容器內的作業系統其實就是外面宿主機的作業系統,兩者其實是一個,你在容器內用uname -a看到的核心版本和外面看到的是一樣的。本質上容器就是一個程式,他和宿主機上任何其他程式沒什麼本質的區別。

建造容器監獄

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

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

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

首先是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資料夾的內容拷過去。製作完了以後,檔案系統就這樣了。

還有一種方式,就是使用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製作檔案系統,製作完了以後,檔案系統就是下面這個樣子

製作初始化指令碼

初始化指令碼就做兩件事情,一是啟動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這個命令登入進我們的容器了,然後你就可以為所欲為了。

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

看到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確實是個好東西。


如果你覺得不錯,歡迎轉發給更多人看到,也歡迎關注我的公眾號,主要聊聊搜尋,推薦,廣告技術,還有瞎扯。。文章會在這裡首先發出來:)掃描或者搜尋微訊號XJJ267或者搜尋西加加語言就行

相關文章