容器,目前最火的話題了,在後端的開發中,容器的運用也已是主流技術了,今天,咱們就來講說容器技術,以前我對這一塊的瞭解不是不少,可是最近有些特殊緣由轉成運維工程師了,而公司的全線服務都是docker的,以一個開發人員的習慣,轉成運維之後,仍是想對這種東西總想深刻了解一下,因而看了很多相關資料而且看了一下docker的源代碼,發現這東西確實很厲害,和以前腦中的docker印象徹底不一樣,因而有了這篇文章。linux
先說結論,容器真的很好,很輕量級,功能又很重量級。git
首先,雖然目前docker技術如此火爆,可是其實容器技術本上並非什麼高大上的東西,總的來說,就是對目前的Linux底層的幾個API的封裝,而後圍繞着這幾個API開發出了一套周邊的環境。程序員
以前全部的講關於容器的文章,一開始就開始講UTC隔離,PID隔離,IPC隔離,文件系統隔離,CGroups系統,今天這一篇,咱們換一個視角,咱們從如下幾個方面來講一下容器技術。github
首先,咱們從容器和虛擬機提及,都說容器是很是輕量級的,那麼和虛擬機比起來,到底輕在什麼地方呢。golang
第二部分,咱們會經過一步一步的說明,經過構造一個監獄,來講明如何創建一個簡單的容器,會涉及到容器的各類技術,固然還有一些沒有涉及到的,我認爲不影響理解。redis
第三部分,咱們會經過代碼實戰一把,看看如何一步一步按照第二部分的說明啓動一個容器並運行本身的代碼,這一部分的所有代碼都在github上。docker
最後,我會再說一下docker技術,由於docker從代碼來看,容器技術只是他的一小部分,完整的docker遠比單純的容器要複雜,我會簡單的說一下我對docker的理解,包括docker使用的其餘技術點。shell
要說容器,跑不了和虛擬機進行比較,虛擬機是比較古老的技術了,虛擬機的架構圖以下所示。bootstrap
虛擬機核心是什麼?是模擬硬件,讓虛擬機的操做系統覺得本身跑在一個真實的物理機器上,用軟件模擬出來CPU,內存,硬盤,網卡
,讓虛擬機裏面的操做系統以爲本身是在操做真實的硬件,因此虛擬機裏面的CPU啊,內存啊都是假的,都是軟件模擬出來的(如今有硬件虛擬化技術了,比純軟件模擬要高級一些,但操做系統無論這些),既然操做系統都騙過去了,固然跑在操做系統上的進程一樣也騙過去了唄,因此這些進程都徹底感知不到底層硬件的區別,還覺得本身很歡樂的跑在一臺真實的物理機上了。ubuntu
那麼容器又是什麼鬼呢?容器的架構圖以下(這張圖網上找的,侵權立刻刪)
和虛擬機一個很明顯的區別就是容器其實並無模擬硬件
,仍是那個硬件,仍是那個操做系統,只不過是在操做系統上作了一點文章【這張圖中就是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文件夾的地位比較特殊,具體做用能夠自行查文檔,簡單的說就是保存系統信息的文件夾。在這裏,dev
和sys
這兩個特殊文件夾也須要作特殊處理的,這裏沒有寫出來而已。
這些都作完了之後,就能夠啓動真正須要執行的進程了,好比/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自己可徹底不止容器技術自己,還有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或者搜索西加加語言就行