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容器,對初始狀態(創建容器所用的鏡像的狀態)做的所有變化都會丟失。】(譯者注:該論述不正確,已與作者確認,感謝lostsnow指正)
容器是設計來運行一個應用的,而非一台機器。你可能會把容器當虛擬機用,但如我們所見,你將失去很多的靈活性,因為Docker提供了用於分離應用與數據的工具,使得你可以快捷地更新運行中的代碼/系統,而不影響數據。
數據卷
數據卷讓你可以不受容器生命周期影響進行數據持久化。它們表現為容器內的空間,但實際保存在容器之外,從而允許你在不影響數據的情況下銷毀、重建、修改、丟棄容器。Docker允許你定義應用部分和數據部分,並提供工具讓你可以將它們分開。使用Docker時必須做出的最大思維變化之一就是:容器應該是短暫和一次性的。
卷是針對容器的,你可以使用同一個鏡像創建多個容器並定義不同的卷。卷保存在運行Docker的宿主文件系統上,你可以指定卷存放的目錄,或讓Docker保存在默認位置。保存在其他類型文件系統上的都不是一個卷,稍後再具體說。
卷還可以用來在容器間共享數據,建議你閱讀卷的文檔做進一步了解。
鏈接
鏈接是Docker的另一個重要部分。
容器啟動時,將被分配一個隨機的私有IP,其它容器可以使用這個IP地址與其進行通訊。這點非常重要,原因有二:一是它提供了容器間相互通信的渠道,二是容器將共享一個本地網絡。我曾經碰到一個問題,在同一台機器上為兩個客戶啟動兩個elasticsearch容器,但保留集群名稱為默認設置,結果這兩台elasticsearch服務器立馬變成了一個自主集群。注:限制容器間通訊是可行的,請閱讀Docker的高級網絡文檔做進一步了解。
要開啟容器間通訊,Docker允許你在創建一個新容器時引用其它現存容器,在你剛創建的容器裡被引用的容器將獲得一個(你指定的)別名。我們就說,這兩個容器鏈接在了一起。
因此,如果DB容器已經在運行,我可以創建web服務器容器,並在創建時引用這個DB容器,給它一個別名,比如dbapp。在這個新建的web服務器容器裡,我可以在任何時候使用主機名dbapp與DB容器進行通訊。
Docker更進一步,要求你聲明容器在被鏈接時要開放哪些端口給其他容器,否則將沒有端口可用。
Docker鏡像的可移植性
在創建鏡像時有一點要注意。Docker允許你在一個鏡像中指定卷和端口。從這個鏡像創建的容器繼承了這些設置。但是,Docker不允許你在鏡像上指定任何不可移植的內容。
例如,你可以在鏡像裡定義卷,只要它們被保存在Docker使用的默認位置。這是因為如果你在宿主文件系統裡指定了一個特定目錄來保存卷,其他使用這個鏡像的宿主無法保證這個目錄是存在的。
你可以定義要公開的端口,但僅限那些在創建鏈接時公開給其他容器的端口,你不能指定公開給宿主的端口,因為你無從知曉使用那個鏡像的宿主有哪些端口可用。
你也不能在鏡像上定義鏈接。使用鏈接要求通過名字引用其他容器,但你無法預知每個使用那個鏡像的宿主如何命名容器。
鏡像必須完全可移植,Docker不允許例外。
以上就是主要的部分,創建鏡像、用它們創建容器、在需要時暴露端口和創造卷、通過鏈接將幾個容器連接在一起。不過,這一切如何能在不引起額外支出條件下達成?
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。
超級、無比簡單的步驟說明
第一步,安裝Docker。
Docker命令工具需要root權限才能工作。你可以將你的用戶放入docker組來避免每次都要使用sudo。
第二步,使用以下命令從公共registry下載一個鏡像:
$> docker pull ubuntu:latest
ubuntu:latest: The image you are pulling has been verified
3b363fd9d7da: Pull complete
..........
8eaa4ff06b53: Pull complete
Status: Downloaded newer image for ubuntu:latest
$>
這個公共registry上有你需要的幾乎所有東西的鏡像:Ubuntu、Fedora、Postgresql、MySQL、Jenkins、Elasticsearch、Redis等等。Docker開發人員在這個公共registry裡維護著數個鏡像,不過你能從上面拉取大量來自用戶發布的自建鏡像。
也許你需要或想要一個私有的registry(用於開發應用之類的容器),你可以先看看這個。現在,有好幾個方式可以設置你自己的私有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的大致內容就是這些,它包括了很多內容,但那超出了本文的范圍。
不過我會提供一些我認為非常重要或有趣的鏈接和延伸閱讀材料
Docker的基本結構:
https://docs.docker.com/introd ... cker/
http://blog.docker.com/2014/03 ... iner/
延伸閱讀:
Dockerfiles:允許你使用一個文本文件定義鏡像,這非常重要
我說過dockerfiles非常重要吧?
你真應該看看dockerfiles
docker build:你需要這個來構建你的dockerfiles
docker push/docker pull
docker create/docker run
docker rm/docker rmi
docker start/docker stop
docker exec
docker inspect
docker tag
Links
Volumes
有趣的鏈接:
ANNOUNCING DOCKER MACHINE, SWARM, AND COMPOSE FOR ORCHESTRATING DISTRIBUTED APPS
Docker at Shopify: How we built containers that power over 100,000 online shops
Why CoreOS is a game-changer in the data center and cloud
Docker Misconceptions
Microservices - Not a Free Lunch!
Feature Preview: Docker-Based Development Environments
How to compile Docker on Windows (感謝reddit的computermedic提供)
有用的項目和鏈接
Phusion Docker baseimage
Shipyard
DockerUI
CoreOS
Decking
Docker-py
Docker-map
Docker-fabric