Docker終極指南:為什么Docker能做這么多事
本教程有三個目標:說明Docker解決的問題、說明它如何解決這個問題、以及說明它使用了哪些技術來解決這個問題,這不是一篇教你怎么運行安裝Docker的教程。
Docker是一個相對較新且發展非??焖俚捻椖?,可用來創建非常輕量的“虛擬機”。注意這里的引號非常重要,Docker創建的并非真正的虛擬機,而更像是打了激素的chroot,嗯,是大量的激素。
在我們繼續之前,我先說下,截至目前(2015年1月4日)為止,Docker只能在Linux上工作,暫不支持Windows或OSX(譯者注:不直接支持)。我稍后會講到Docker的架構,你會明白其中的原因。所以,如果想在非Linux平臺上使用Docker,你需要在虛擬機里運行Linux。
本教程有三個目標:說明Docker解決的問題、說明它如何解決這個問題、以及說明它使用了哪些技術來解決這個問題。這不是一篇教你怎么運行安裝Docker的教程,Docker此類教程已經有很多,包括Docker作者的在線互動教程(譯者注:作者很喜歡在一個句子里引用多個鏈接,下同)。本文***有一個步驟說明,目的是用一個明確的現實世界的例子來串聯文章中所有的理論,但不會太過詳細。
Docker能做什么?
Docker可以解決虛擬機能夠解決的問題,同時也能夠解決虛擬機由于資源要求過高而無法解決的問題。Docker能處理的事情包括:
- 隔離應用依賴
- 創建應用鏡像并進行復制
- 創建容易分發的即啟即用的應用
- 允許實例簡單、快速地擴展
- 測試應用并隨后銷毀它們
Docker背后的想法是創建軟件程序可移植的輕量容器,讓其可以在任何安裝了Docker的機器上運行,而不用關心底層操作系統,類似船舶使用的集裝箱,野心勃勃的他們成功了。
Docker究竟做了什么?
這一節我不會說明Docker使用了哪些技術來完成它的工作,或有什么具體的命令可用,這些放在了***一節,這里我將說明的是Docker提供的資源和抽象。
Docker兩個最重要的概念是鏡像和容器。除此之外,鏈接和數據卷也很重要。我們先從鏡像入手。
鏡像
Docker的鏡像類似虛擬機的快照,但更輕量,非常非常輕量(下節細說)。
創建Docker鏡像有幾種方式,多數是在一個現有鏡像基礎上創建新鏡像,因為幾乎你需要的任何東西都有了公共鏡像,包括所有主流Linux發行版,你應該不會找不到你需要的鏡像。不過,就算你想從頭構建一個鏡像,也有好幾種方法。
要創建一個鏡像,你可以拿一個鏡像,對它進行修改來創建它的子鏡像。實現的方式有兩種:在一個文件中指定一個基礎鏡像及需要完成的修改;或通過“運行”一個鏡像,對其進行修改并提交。不同方式各有優點,不過一般會使用文件來指定所做的變化。
鏡像擁有唯一ID,以及一個供人閱讀的名字和標簽對。鏡像可以命名為類似ubuntu:latest、ubuntu:precise、django:1.6、django:1.7等等。
容器
現在說容器了。你可以從鏡像中創建容器,這等同于從快照中創建虛擬機,不過更輕量。應用是由容器運行的。
舉個例子,你可以下載一個Ubuntu的鏡像(有個叫docker registry的鏡像公共倉庫),通過安裝Gunicorn和你的Django應用及其依賴完成對它的修改,然后從該鏡像中創建一個容器,在它啟動后運行你的應用。
容器與虛擬機一樣,是隔離的(有一點要注意,我稍后會討論到)。它們也擁有一個唯一ID和唯一的供人閱讀的名字。容器有必要對外暴露服務,因此Docker允許暴露容器的特定端口。
容器與虛擬機相比有兩個主要差異。***個是:它們被設計成運行單進程,無法很好地模擬一個完整的環境(如果那是你需要的,請看看LXC)。你可能會嘗試運行runit或supervisord實例來啟動多個進程,但(以我的愚見)這真的沒有必要。
單進程與多進程之爭非常精彩。你應該知道的是,Docker設計者極力推崇“一個容器一個進程的方式”,如果你要選擇在一個容器中運行多個進程,那唯一情況是:出于調試目的,運行類似ssh的東西來訪問運行中的容器,不過docker exec命令解決了這個問題。
容器和虛擬機的第二個巨大差異是:當你停止一個虛擬機時,可能除了一些臨時文件,沒有文件會被刪除;當你停止一個Docker容器,對初始狀態(創建容器所用的鏡像的狀態)做的所有變化都會丟失。這是使用Docker時必須做出的***思維變化之一:容器是短暫和一次性的。
數據卷
如果你的電子商務網站剛收到客戶支付的3萬元,內核崩潰了,所有數據庫變化都丟失了……對你或Docker來說都不是一件好事,不過不要擔心。Docker允許你定義數據卷——用于保存持久數據的空間。Docker強制你定義應用部分和數據部分,并要求你將它們分開。
卷是針對容器的,你可以使用同一個鏡像創建多個容器并定義不同的卷。卷保存在運行Docker的宿主文件系統上,你可以指定卷存放的目錄,或讓Docker保存在默認位置。保存在其他類型文件系統上的都不是一個卷,稍后再具體說。
鏈接
鏈接是Docker的另一個重要部分。
容器啟動時,將被分配一個隨機的私有IP,其它容器可以使用這個IP地址與其進行通訊。這點非常重要,原因有二:一是它提供了容器間相互通信的渠道,二是容器將共享一個本地網絡。我曾經碰到一個問題,在同一臺機器上為兩個客戶啟動兩個elasticsearch容器,但保留集群名稱為默認設置,結果這兩臺elasticsearch服務器立馬變成了一個自主集群。
要開啟容器間通訊,Docker允許你在創建一個新容器時引用其它現存容器,在你剛創建的容器里被引用的容器將獲得一個(你指定的)別名。我們就說,這兩個容器鏈接在了一起。
因此,如果DB容器已經在運行,我可以創建web服務器容器,并在創建時引用這個DB容器,給它一個別名,比如dbapp。在這個新建的web服務器容器里,我可以在任何時候使用主機名dbapp與DB容器進行通訊。
Docker更進一步,要求你聲明容器在被鏈接時要開放哪些端口給其他容器,否則將沒有端口可用。
Docker鏡像的可移植性
在創建鏡像時有一點要注意。Docker允許你在一個鏡像中指定卷和端口。從這個鏡像創建的容器繼承了這些設置。但是,Docker不允許你在鏡像上指定任何不可移植的內容。
例如,你可以在鏡像里定義卷,只要它們被保存在Docker使用的默認位置。這是因為如果你在宿主文件系統里指定了一個特定目錄來保存卷,其他使用這個鏡像的宿主無法保證這個目錄是存在的。
你可以定義要暴露的端口,但僅限那些在創建鏈接時暴露給其他容器的端口,你不能指定暴露給宿主的端口,因為你無從知曉使用那個鏡像的宿主有哪些端口可用。
你也不能在鏡像上定義鏈接。使用鏈接要求通過名字引用其他容器,但你無法預知每個使用那個鏡像的宿主如何命名容器。
鏡像必須完全可移植,Docker不允許例外。
以上就是主要的部分,創建鏡像、用它們創建容器,在需要時暴露端口和創造卷、通過鏈接將幾個容器連接在一起。不過,這一切如何能在不引起額外開銷條件下達成?
#p#
Docker如何完成它需要完成的東西?
兩個詞:cgroups和union文件系統。Docker使用cgroup來提供容器隔離,而union文件系統用于保存鏡像并使容器變得短暫。
Cgroups
這是Linux內核功能,它讓兩件事情變成可能:
- 限制Linux進程組的資源占用(內存、CPU)
- 為進程組制作 PID、UTS、IPC、網絡、用戶及裝載命名空間
這里的關鍵詞是命名空間。比如說,一個PID命名空間允許它里面的進程使用隔離的PID,并與主PID命名空間獨立開來,因此你可以在一個PID命名空間里擁有自己的PID為1的初始化進程。其他命名空間與此類似。然后你可以使用cgroup創建一個環境,進程可以在其中運行,并與操作系統的其他進程隔離開,但這里的關鍵點是這個環境上的進程使用的是已經加載和運行的內核,因此額外開銷與運行其他進程幾乎是一樣的。Chroot之于cgroup就好像我之于綠巨人(The Hulk)、貝恩(Bane)和毒液(Venom)的組合(譯者注:本文作者非常瘦弱,后三者都非常強壯)。
Union文件系統
Union文件系統允許通過union裝載變化的分層疊加。在union文件系統里,文件系統可以被裝載在其他文件系統之上,其結果就是一個變化的分層集合。每個裝載的文件系統表示前一個文件系統之后的變化集合,就像是一個diff。
當你下載一個鏡像,修改它,然后保存成新版本,你只是創建了加載在包裹基礎鏡像的初始層上的一個新的union文件系統。這使得Docker鏡像非常輕,比如:你的DB、Nginx和Syslog鏡像都可以共享同一個Ubuntu基礎,每一個鏡像保存的只是在基礎之上工作需要的變化。
截至2015年1月4日,Docker允許在union文件系統中使用aufs、btrfs或設備映射(device mapper)。
鏡像
我們來看一下postgresql的一個鏡像:
- [{
- "AppArmorProfile": "",
- "Args": [
- "postgres"
- ],
- "Config": {
- "AttachStderr": true,
- "AttachStdin": false,
- "AttachStdout": true,
- "Cmd": [
- "postgres"
- ],
- "CpuShares": 0,
- "Cpuset": "",
- "Domainname": "",
- "Entrypoint": [
- "/docker-entrypoint.sh"
- ],
- "Env": [
- "PATH=/usr/lib/postgresql/9.3/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
- "LANG=en_US.utf8",
- "PG_MAJOR=9.3",
- "PG_VERSION=9.3.5-1.pgdg70 1",
- "PGDATA=/var/lib/postgresql/data"
- ],
- "ExposedPorts": {
- "5432/tcp": {}
- },
- "Hostname": "6334a2022f21",
- "Image": "postgres",
- "MacAddress": "",
- "Memory": 0,
- "MemorySwap": 0,
- "NetworkDisabled": false,
- "OnBuild": null,
- "OpenStdin": false,
- "PortSpecs": null,
- "StdinOnce": false,
- "Tty": false,
- "User": "",
- "Volumes": {
- "/var/lib/postgresql/data": {}
- },
- "WorkingDir": ""
- },
- "Created": "2015-01-03T23:56:12.354896658Z",
- "Driver": "devicemapper",
- "ExecDriver": "native-0.2",
- "HostConfig": {
- "Binds": null,
- "CapAdd": null,
- "CapDrop": null,
- "ContainerIDFile": "",
- "Devices": null,
- "Dns": null,
- "DnsSearch": null,
- "ExtraHosts": null,
- "IpcMode": "",
- "Links": null,
- "LxcConf": null,
- "NetworkMode": "",
- "PortBindings": null,
- "Privileged": false,
- "PublishAllPorts": false,
- "RestartPolicy": {
- "MaximumRetryCount": 0,
- "Name": ""
- },
- "SecurityOpt": null,
- "VolumesFrom": [
- "bestwebappever.dev.db-data"
- ]
- },
- "HostnamePath": "/mnt/docker/containers/6334a2022f213f9534b45df33c64437081a38d50c7f462692b019185b8cbc6da/hostname",
- "HostsPath": "/mnt/docker/containers/6334a2022f213f9534b45df33c64437081a38d50c7f462692b019185b8cbc6da/hosts",
- "Id": "6334a2022f213f9534b45df33c64437081a38d50c7f462692b019185b8cbc6da",
- "Image": "aaab661c1e3e8da2d9fc6872986cbd7b9ec835dcd3886d37722f1133baa3d2db",
- "MountLabel": "",
- "Name": "/bestwebappever.dev.db",
- "NetworkSettings": {
- "Bridge": "docker0",
- "Gateway": "172.17.42.1",
- "IPAddress": "172.17.0.176",
- "IPPrefixLen": 16,
- "MacAddress": "02:42:ac:11:00:b0",
- "PortMapping": null,
- "Ports": {
- "5432/tcp": null
- }
- },
- "Path": "/docker-entrypoint.sh",
- "ProcessLabel": "",
- "ResolvConfPath": "/mnt/docker/containers/6334a2022f213f9534b45df33c64437081a38d50c7f462692b019185b8cbc6da/resolv.conf",
- "State": {
- "Error": "",
- "ExitCode": 0,
- "FinishedAt": "0001-01-01T00:00:00Z",
- "OOMKilled": false,
- "Paused": false,
- "Pid": 21654,
- "Restarting": false,
- "Running": true,
- "StartedAt": "2015-01-03T23:56:42.003405983Z"
- },
- "Volumes": {
- "/var/lib/postgresql/data": "/mnt/docker/vfs/dir/5ac73c52ca86600a82e61279346dac0cb3e173b067ba9b219ea044023ca67561",
- "postgresql_data": "/mnt/docker/vfs/dir/abace588b890e9f4adb604f633c280b9b5bed7d20285aac9cc81a84a2f556034"
- },
- "VolumesRW": {
- "/var/lib/postgresql/data": true,
- "postgresql_data": true
- }
- }
- ]
就是這樣,鏡像只是一個json,它指定了從該鏡像運行的容器的特性,union裝載點保存在哪里,要暴露什么端口等等。每個鏡像與一個union文件系統相關聯,每個Docker上的union文件系統都有一個上層,就像是計算機科技樹(不像其他樹有一大堆的家族)。如果它看起來有點嚇人或有些東西串不起來,不要擔心,這只是出于教學目的,你并不會直接處理這些文件。
容器
容器之所以是短暫的,是因為當你從鏡像上創建一個容器,Docker會創建一個空白的union文件系統加載在與該鏡像關聯的union文件系統之上。
由于union文件系統是空白的,這意味著沒有變化會被應用到鏡像的文件系統上,你創建的變化會得到體現,但是當容器停止,該容器的union文件系統會被丟棄,留下的是你啟動時的原始鏡像文件系統。除非你創建一個新的鏡像,或制作一個卷,你所做的變化在容器停止時都會消失。
卷所做的是在容器內指定一個目錄,以便在union文件系統之外保存它。
這是一個bestwebappever的容器:
- [{
- "AppArmorProfile": "",
- "Args": [],
- "Config": {
- "AttachStderr": true,
- "AttachStdin": false,
- "AttachStdout": true,
- "Cmd": [
- "/sbin/my_init"
- ],
- "CpuShares": 0,
- "Cpuset": "",
- "Domainname": "",
- "Entrypoint": null,
- "Env": [
- "DJANGO_CONFIGURATION=Local",
- "HOME=/root",
- "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
- "TALPOR_ENVIRONMENT=local",
- "TALPOR_DIR=/opt/bestwebappever"
- ],
- "ExposedPorts": {
- "80/tcp": {}
- },
- "Hostname": "44a87fdaf870",
- "Image": "talpor/bestwebappever:dev",
- "MacAddress": "",
- "Memory": 0,
- "MemorySwap": 0,
- "NetworkDisabled": false,
- "OnBuild": null,
- "OpenStdin": false,
- "PortSpecs": null,
- "StdinOnce": false,
- "Tty": false,
- "User": "",
- "Volumes": {
- "/opt/bestwebappever": {}
- },
- "WorkingDir": "/opt/bestwebappever"
- },
- "Created": "2015-01-03T23:56:15.378511619Z",
- "Driver": "devicemapper",
- "ExecDriver": "native-0.2",
- "HostConfig": {
- "Binds": [
- "/home/german/bestwebappever/:/opt/bestwebappever:rw"
- ],
- "CapAdd": null,
- "CapDrop": null,
- "ContainerIDFile": "",
- "Devices": null,
- "Dns": null,
- "DnsSearch": null,
- "ExtraHosts": null,
- "IpcMode": "",
- "Links": [
- "/bestwebappever.dev.db:/bestwebappever.dev.app/db",
- "/bestwebappever.dev.redis:/bestwebappever.dev.app/redis"
- ],
- "LxcConf": null,
- "NetworkMode": "",
- "PortBindings": {
- "80/tcp": [
- {
- "HostIp": "",
- "HostPort": "8887"
- }
- ]
- },
- "Privileged": false,
- "PublishAllPorts": false,
- "RestartPolicy": {
- "MaximumRetryCount": 0,
- "Name": ""
- },
- "SecurityOpt": null,
- "VolumesFrom": [
- "bestwebappever.dev.requirements-data"
- ]
- },
- "HostnamePath": "/mnt/docker/containers/44a87fdaf870281e86160e9e844b8987cfefd771448887675fed99460de491c4/hostname",
- "HostsPath": "/mnt/docker/containers/44a87fdaf870281e86160e9e844b8987cfefd771448887675fed99460de491c4/hosts",
- "Id": "44a87fdaf870281e86160e9e844b8987cfefd771448887675fed99460de491c4",
- "Image": "b84804fac17b61fe8f344359285186f1a63cd8c0017930897a078cd09d61bb60",
- "MountLabel": "",
- "Name": "/bestwebappever.dev.app",
- "NetworkSettings": {
- "Bridge": "docker0",
- "Gateway": "172.17.42.1",
- "IPAddress": "172.17.0.179",
- "IPPrefixLen": 16,
- "MacAddress": "02:42:ac:11:00:b3",
- "PortMapping": null,
- "Ports": {
- "80/tcp": [
- {
- "HostIp": "0.0.0.0",
- "HostPort": "8887"
- }
- ]
- }
- },
- "Path": "/sbin/my_init",
- "ProcessLabel": "",
- "ResolvConfPath": "/mnt/docker/containers/44a87fdaf870281e86160e9e844b8987cfefd771448887675fed99460de491c4/resolv.conf",
- "State": {
- "Error": "",
- "ExitCode": 0,
- "FinishedAt": "0001-01-01T00:00:00Z",
- "OOMKilled": false,
- "Paused": false,
- "Pid": 21796,
- "Restarting": false,
- "Running": true,
- "StartedAt": "2015-01-03T23:56:47.537259546Z"
- },
- "Volumes": {
- "/opt/bestwebappever": "/home/german/bestwebappever",
- "requirements_data": "/mnt/docker/vfs/dir/bc14bec26ca311d5ed9f2a83eebef872a879c9e2f1d932470e0fd853fe8be336"
- },
- "VolumesRW": {
- "/opt/bestwebappever": true,
- "requirements_data": true
- }
- }
- ]
基本上與鏡像相同,不過現在還指定了一些暴露給宿主的端口,也聲明了卷位于宿主的位置,容器狀態是從現在直到結束,等等。與前面一樣,如果它看起來讓人生畏,不要擔心,你不會直接處理這些json。
#p#
超級、無比簡單的步驟說明
***步,安裝Docker。
Docker命令工具需要root權限才能工作。你可以將你的用戶放入docker組來避免每次都要使用sudo。
第二步,使用以下命令從公共registry下載一個鏡像:
- $> docker pull ubuntu:latest
- ubuntu:latest: The image you are pulling has been verified
- 3b363fd9d7da: Pull complete
- .....<bunch of downloading-stuff output>.....
- 8eaa4ff06b53: Pull complete
- Status: Downloaded newer image for ubuntu:latest
- $>
這個公共registry上有你需要的幾乎所有東西的鏡像:Ubuntu、Fedora、Postgresql、MySQL、Jenkins、Elasticsearch、Redis等等。Docker開發人員在這個公共registry里維護著數個鏡像,不過你能從上面拉取大量來自用戶發布的自建鏡像。
也許你需要或想要一個私有的registry(用于開發應用之類的容器),你可以先看看這個?,F在,有好幾個方式可以設置你自己的私有registry。你也可以買一個。
第三步,列出你的鏡像:
- $> docker images
- REPOSITORY TAG IMAGE ID CREATED VIRTUAL SIZE
- ubuntu latest 8eaa4ff06b53 4 days ago 192.7 MB
第四步,從該鏡像上創建一個容器。
- $> docker run --rm -ti ubuntu /bin/bash
- root@4638a40c2fbb:/# ls
- bin boot dev etc home lib lib64 media mnt opt proc root......
- root@4638a40c2fbb:/# exit
上一條命令的簡要說明:
--rm:告訴Docker一旦運行的進程退出就刪除容器。這在進行測試時非常有用,可免除雜亂
-ti:告訴Docker分配一個偽終端并進入交互模式。這將進入到容器內,對于快速原型開發或嘗試很有用,但不要在生產容器中打開這些標志
ubuntu:這是容器立足的鏡像
/bin/bash:要運行的命令,因為我們以交互模式啟動,它將顯示一個容器的提示符
在運行run命令時,你可指定鏈接、卷、端口、窗口名稱(如果你沒提供,Docker將分配一個默認名稱)等等。
現在,我們在后臺運行一個容器:
- $> docker run -d ubuntu ping 8.8.8.8
- 31c68e9c09a0d632caae40debe13da3d6e612364198e2ef21f842762df4f987f
- $>
輸出的是分配的ID,因為是隨機的,你的將有所不同。我們來檢查一下容器是否起來了:
- $> docker ps
- CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
- 31c68e9c09a0 ubuntu:latest "ping 8.8.8.8" 2 minutes ago Up 2 minutes loving_mcclintock
就在那,它被自動分配了一個叫loving_mcclintock的名稱。我們看看容器里正在發生什么:
- $> docker exec -ti loving_mcclintock /bin/bash
- root@31c68e9c09a0:/# ps -aux|grep ping
- root 1 0.0 0.0 6504 636 ? Ss 20:46 0:00 ping 8.8.8.8
- root@31c68e9c09a0:/# exit
我們所做的是在容器里運行程序,這里的程序是/bin/bash。-ti標志與docker run的作用相同,將我們放置到容器的控制臺里。
結尾
差不多就是這樣了。有太多的東西可以講,但那超出了本文的范圍。
Docker的基本結構:
- https://docs.docker.com/introd ... cker/
- http://blog.docker.com/2014/03 ... iner/
與Docker相關的項目:
- Phusion Docker baseimage
- Shipyard
- DockerUI
- CoreOS
- Decking
- Docker-py
- Docker-map
- Docker-fabric