面試官留步!聽我跟你侃會兒Docker原理
本文轉載自微信公眾號「sowhat1412」,作者sowhat1412。轉載本文請聯系sowhat1412公眾號。
開發人員開發完一個電商項目,該 Jar 項目包含 Redis、MySQL、ES、Haddop等若干組件。開發人員自測無誤后提交給測試進行預生產測試了。
測試:你的這個服務,我在進行單元測試跟數據核對的時候總是出現不知名的bug!你要不要來看下啊?
開發:你咋測試的?是按照操作文檔一步步來的么?
測試:絕對是按照文檔來的啊!
開發:你重啟了嗎?清緩存了嗎?代碼是最新版嗎?你用的是Chrome瀏覽器? 你是不是動啥東西了?
測試:這.. 這.. 這.. 我啥也沒干啊!
至此,開發跟測試之間的愛恨情仇正式開始!
1 Docker 簡介
1.1 Docker 由來
Docker 是基于 Go 語言開發的一個容器引擎,Docker是應用程序與系統之間的隔離層。通常應用程序對安裝的系統環境會有各種嚴格要求,當服務器很多時部署時系統環境的配置工作是非常繁瑣的。Docker讓應用程序不必再關心主機環境,各個應用安裝在Docker鏡像里,Docker引擎負責運行包裹了應用程序的docker鏡像。
Docker的理念是讓開發人員可以簡單地把應用程序及依賴裝載到容器中,然后輕松地部署到任何地方,Docker具有如下特性。
Docker容器是輕量級的虛擬技術,占用更少系統資源。
使用 Docker容器,不同團隊(如開發、測試,運維)之間更容易合作。
可以在任何地方部署 Docker 容器,比如在任何物理和虛擬機上,甚至在云上。
由于Docker容器非常輕量級,因此可擴展性很強。
1.2 Docker 基本組成
鏡像(image):
Docker 鏡像就好比是一個目標,可以通過這個目標來創建容器服務,可以簡單的理解為編程語言中的類。
容器(container):
Docker 利用容器技術,獨立運行一個或者一組應用,容器是通過鏡像來創建的,在容器中可執行啟動、停止、刪除等基本命令,最終服務運行或者項目運行就是在容器中的,可理解為是類的實例。
倉庫(repository):
倉庫就是存放鏡像的地方!倉庫分為公有倉庫和私有倉庫,類似Git。一般我們用的時候都是用國內docker鏡像來加速。
1.3 VM 跟 Docker
虛擬機:
傳統的虛擬機需要模擬整臺機器包括硬件,每臺虛擬機都需要有自己的操作系統,虛擬機一旦被開啟,預分配給他的資源將全部被占用。每一個虛擬機包括應用,必要的二進制和庫,以及一個完整的用戶操作系統。
Docker:
容器技術是和我們的宿主機共享硬件資源及操作系統可以實現資源的動態分配。容器包含應用和其所有的依賴包,但是與其他容器共享內核。容器在宿主機操作系統中,在用戶空間以分離的進程運行。
比對項 | Container(容器) | VM(虛擬機) |
---|---|---|
啟動速度 | 秒級 | 分鐘級 |
運行性能 | 接近原生 | 有所損失 |
磁盤占用 | MB | GB |
數量 | 成百上千 | 一般幾十臺 |
隔離性 | 進程級別 | 系統級別 |
操作系統 | 只支持Linux | 幾乎所有 |
封裝程度 | 只打包項目代碼和依賴關系 共享宿主機內核 |
完整的操作系統 |
1.4 Docker 跟 DevOps
DevOps 是一組過程、方法與系統的統稱,用于促進開發(應用程序/軟件工程)、技術運營和質量保障(QA)部門之間的溝通、協作與整合。
DevOps 是兩個傳統角色 Dev(Development) 和 Ops(Operations) 的結合,Dev 負責開發,Ops 負責部署上線,但 Ops 對 Dev 開發的應用缺少足夠的了解,而 Dev 來負責上線,很多服務軟件不知如何部署運行,二者中間有一道明顯的鴻溝,DevOps 就是為了彌補這道鴻溝。DevOps 要做的事,是偏 Ops 的;但是做這個事的人,是偏 Dev 的, 說白了就是要有一個了解 Dev 的人能把 Ops 的事干了。而Docker 是適合 DevOps 的。
1.5 Docker 跟 k8s
k8s 的全稱是 kubernetes,它是基于容器的集群管理平臺,是管理應用的全生命周期的一個工具,從創建應用、應用的部署、應用提供服務、擴容縮容應用、應用更新、都非常的方便,而且可以做到故障自愈,例如一個服務器掛了,可以自動將這個服務器上的服務調度到另外一個主機上進行運行,無需進行人工干涉。k8s 依托于Google自家的強大實踐應用,目前市場占有率已經超過Docker自帶的Swarm了。
如果你有很多 Docker 容器要啟動、維護、監控,那就上k8s吧!
1.6 hello world
docker run hello-world 的大致流程圖如下:
2 Docker 常見指令
官方文檔:
https://docs.docker.com/engine/reference/commandline/build/
3 Docker 運行原理
Docker 只提供一個運行環境,他跟 VM 不一樣,是不需要運行一個獨立的 OS,容器中的系統內核跟宿主機的內核是公用的。docker容器本質上是宿主機的進程。對 Docker 項目來說,它最核心的原理實際上就是為待創建的用戶進程做如下操作:
- 啟用 Linux Namespace 配置。
- 設置指定的 Cgroups 參數。
- 切換進程的根目錄(Change Root),優先使用 pivot_root 系統調用,如果系統不支持,才會使用 chroot。
3.1 namespace 進程隔離
Linux Namespaces 機制提供一種進程資源隔離方案。PID、IPC、Network 等系統資源不再是全局性的,而是屬于某個特定的Namespace。每個namespace下的資源對于其他namespace 下的資源都是透明,不可見的。系統中可以同時存在兩個進程號為0、1、2的進程,由于屬于不同的namespace,所以它們之間并不沖突。
PS:Linux 內核提拱了6種 namespace 隔離的系統調用,如下圖所示。
3.2 CGroup 分配資源
Docker 通過 Cgroup 來控制容器使用的資源配額,一旦超過這個配額就發出OOM。配額主要包括 CPU、內存、磁盤三大方面, 基本覆蓋了常見的資源配額和使用量控制。
Cgroup 是 Control Groups 的縮寫,是Linux 內核提供的一種可以限制、記錄、隔離進程組所使用的物理資源(如 CPU、內存、磁盤 IO 等等)的機制,被 LXC(Linux container)、Docker 等很多項目用于實現進程資源控制。Cgroup 本身是提供將進程進行分組化管理的功能和接口的基礎結構,I/O 或內存的分配控制等具體的資源管理是通過該功能來實現的,這些具體的資源 管理功能稱為 Cgroup 子系統。
3.3 chroot 跟 pivot_root 文件系統
chroot(change root file system)命令的功能是 改變進程的根目錄到指定的位置。比如我們現在有一個$HOME/test目錄,想要把它作為一個 /bin/bash 進程的根目錄。
首先,創建一個目錄和幾個文件夾HOME/test/{bin,lib64,lib}
把bash命令拷貝到test目錄對應的bin路徑下 cp -v /bin/{bash,ls} $HOME/test/bin
把bash命令需要的所有so文件,也拷貝到test目錄對應的lib路徑下
執行chroot命令,告訴操作系統,我們將使用HOME/test /bin/bash
被chroot的進程此時執行 ls / 返回的都是$HOME/test目錄下面的內容,Docker就是這樣實現容器根目錄的。為了能夠讓容器的這個根目錄看起來更真實,一般在容器的根目錄下掛載一個完整操作系統的文件系統,比如Ubuntu16.04的ISO。這樣在容器啟動之后,容器里執行ls /查看到的就是Ubuntu 16.04的所有目錄和文件。
而掛載在容器根目錄上、用來為容器進程提供隔離后執行環境的文件系統,就是所謂的容器鏡像。更專業的名字叫作:rootfs(根文件系統)。所以一個最常見的 rootfs 會包括如下所示的一些目錄和文件:
- $ ls /
- bin dev etc home lib lib64 mnt opt proc root run sbin sys tmp usr var
chroot 只改變當前進程的 /,pivot_root改變當前 mount namespace的 / 。pivot_root 可以認為是 chroot 的改良版。
3.4 一致性
由于 rootfs 里打包的不只是應用,而是整個操作系統的文件和目錄,也就意味著應用以及它運行所需要的所有依賴都被封裝在了一起。有了容器鏡像打包操作系統的能力,這個最基礎的依賴環境也終于變成了應用沙盒的一部分。這就賦予了容器所謂的一致性:
無論在本地、云端,還是在一臺任何地方的機器上,用戶只需要解壓打包好的容器鏡像,那么這個應用運行所需要的完整的執行環境就被重現出來了。
3.5 UnionFS 聯合文件系統
如何實現rootfs的高效可重復利用呢?Docker在鏡像的設計中引入了層(layer)的概念。也就是說用戶制作鏡像的每一步操作都會生成一個層,也就是一個增量rootfs。介紹分層前我們先說個重要知識點,聯合文件系統。
聯合文件系統(UnionFS)是一種分層、輕量級并且高性能的文件系統,它支持對文件系統的修改作為一次提交來一層層的疊加,同時可以將不同目錄掛載到同一個虛擬文件系統下。比如現在有水果fruits、蔬菜vegetables兩個目錄,其中水果中有蘋果和蕃茄,蔬菜有胡蘿卜和蕃茄:
- $ tree
- .
- ├── fruits
- │ ├── apple
- │ └── tomato
- └── vegetables
- ├── carrots
- └── tomato
然后使用聯合掛載的方式將這兩個目錄掛載到一個公共的目錄 mnt 上:
- $ mkdir mnt
- $ sudo mount -t aufs -o dirs=./fruits:./vegetables none ./mnt
這時再查看目錄 mnt 的內容,就能看到目錄 fruits 和 vegetables 下的文件被合并到了一起:
- $ tree ./mnt
- ./mnt
- ├── apple
- ├── carrots
- └── tomato
可以看到在 mnt 目錄下有三個文件,蘋果apple、胡蘿卜carrots和蕃茄tomato。水果和蔬菜的目錄被union到了 mnt 目錄下了。
- $ echo mnt > ./mnt/apple
- $ cat ./mnt/apple
- mnt
- $ cat ./fruits/apple
- mnt
可以看到./mnt/apple的內容改了,./fruits/apple的內容也改了。
- $ echo mnt_carrots > ./mnt/carrots
- $ cat ./vegetables/carrots
- old
- $ cat ./fruits/carrots
- mnt_carrots
./vegetables/carrots 并沒有變化,反而是 ./fruits/carrots 的目錄中出現了 carrots 文件,其內容是我們在 ./mnt/carrots 里的內容。
結論:
在mount aufs命令時候,沒有對 vegetables 跟 fruits 設置權限,默認命令行上第一個的目錄是可讀可寫的,后面的全都是只讀的。有重復的文件名,在mount命令行上,越往前的被操作的優先級越高。
3.6 layer 分層
說完聯合文件系統后我們再說下Docker中的分層,鏡像可以通過分層來進行繼承,基于基礎鏡像(沒有父鏡像)用戶可以制作各種具體的應用鏡像。不同 Docker 容器可以共享一些基礎的文件系統層,同時再加上自己獨有的改動層,大大提高了存儲的效率。
Docker 中使用一種叫 AUFS(Anothe rUnionFS)的聯合文件系統。AUFS 支持為每一個成員目錄設定不同的讀寫權限。
- rw 表示可寫可讀read-write。
- ro 表示read-only,如果你不指權限,那么除了第一個外,ro是默認值,對于ro分支,其永遠不會收到寫操作,也不會收到查找whiteout的操作。
- rr 表示 real-read-only,與read-only不同的是,rr 標記的是天生就是只讀的分支,這樣,AUFS可以提高性能,比如不再設置inotify來檢查文件變動通知。
當我們想修改ro層的文件時咋辦?因為ro是不允許修改的啊!Docker中一般ro層還帶個wh的能力。我們就需要對這個ro目錄里的文件作whiteout。AUFS的whiteout的實現是通過在上層的可寫的目錄下建立對應的whiteout隱藏文件來實現的。比如我們有三個目錄和文件如下所示:
- $ tree
- .
- ├── fruits
- │ ├── apple
- │ └── tomato
- ├── test #目錄為空
- └── vegetables
- ├── carrots
- └── tomato
執行如下:
- $ mkdir mnt
- $ mount -t aufs -o dirs=./test=rw:./fruits=ro:./vegetables=ro none ./mnt
- $ ls ./mnt/
- apple carrots tomato
在權限為 rw 的 test 目錄下建個 whiteout 的隱藏文件 .wh.apple,你就會發現 ./mnt/apple 這個文件就消失了,跟執行了 rm ./mnt/apple 是一樣的結果:
- $ touch ./test/.wh.apple
- $ ls ./mnt
- carrots tomato
對于AUFS來說鏡像的若干基礎層放置在/var/lib/docker/aufs/diff目錄下,然后通過查詢/sys/fs/aufs 查看被聯合掛載在一起的各個層的信息,多個基礎層最終被聯合掛載在/var/lib/docker/aufs/mnt里面,這里面存儲的就是一個成品。
Docker 目前支持的聯合文件系統包括 OverlayFS, AUFS, Btrfs, VFS, ZFS 和 Device Mapper。推薦使用 overlay2 存儲驅動,overlay2 是目前 Docker 默認的存儲驅動,以前則是 AUFS。
3.6.1 只讀層
我們以Ubuntu為例,當執行docker image inspect ubuntu:latest 會發現容器的rootfs最下面的四層,對應的正是ubuntu:latest鏡像的四層。它們的掛載方式都是只讀的(ro+wh),都以增量的方式分別包含了Ubuntu操作系統的一部分,四層聯合起來組成了一個成品。
3.6.2 可讀寫層
rootfs 最上層的操作權限為 rw, 在沒有寫入文件之前,這個目錄是空的。而一旦在容器里做了寫操作,你修改產生的內容就會以增量的方式出現在這個層中。如果你想刪除只讀層里的文件,咋辦呢?這個問題上面已經講解過了。
最上面這個可讀寫層就是專門用來存放修改 rootfs 后產生的增量,無論是增、刪、改,都發生在這里。而當我們使用完了這個被修改過的容器之后,還可以使用 docker commit 和 push 指令,保存這個被修改過的可讀寫層,并上傳到 Docker Hub上,供其他人使用。并且原先的只讀層里的內容則不會有任何變化,這就是增量 rootfs 的好處。
3.6.3 init 層
它是一個以-init結尾的層,夾在只讀層和讀寫層之間。Init層是Docker項目單獨生成的一個內部層,專門用來存放 /etc/hosts 等信息。
需要這樣一層的原因是這些文件本來屬于只讀的Ubuntu鏡像的一部分,但是用戶往往需要在啟動容器時寫入一些指定的值比如 hostname,那就需要在可讀寫層對它們進行修改??墒?,這些修改往往只對當前的容器有效,我們并不希望執行 docker commit 時,把 init 層的信息連同可讀寫層一起提交。
最后這6層被組合起來形成了一個完整的 Ubuntu 操作系統供容器使用。
4 Docker 網絡
由上面的 Docker 原理可知 Docker 使用了 Linux 的 Namespaces 技術來進行資源隔離,如 PID Namespace 隔離進程,Mount Namespace 隔離文件系統,Network Namespace 隔離網絡等。一個Network Namespace 提供了一份獨立的網絡環境(包括網卡、路由、Iptable規則)與其他的Network Namespace隔離,一個Docker容器一般會分配一個獨立的Network Namespace。
當你安裝Docker時,執行docker network ls會發現它會自動創建三個網絡。
- [root@server1 ~]$ docker network ls
- NETWORK ID NAME DRIVER SCOPE
- 0147b8d16c64 bridge bridge local
- 2da931af3f0b host host local
- 63d31338bcd9 none null local
我們在使用docker run創建Docker容器時,可以用 --net 選項指定容器的網絡模式,Docker可以有以下4種網絡模式:
網絡模式 | 使用注意 |
---|---|
host | 和宿主機共享網絡 |
none | 不配置網絡 |
bridge | docker默認,也可自創 |
container | 容器網絡連通,容器直接互聯,用的很少 |
4.1 Host 模式
等價于Vmware中的橋接模式,當啟動容器的時候用host模式,容器將不會虛擬出自己的網卡,配置自己的IP等,而是使用宿主機的IP和端口。但是容器的其他方面,如文件系統、進程列表等還是和宿主機隔離的。
4.2 Container 模式
Container 模式指定新創建的容器和已經存在的一個容器共享一個 Network Namespace,而不是和宿主機共享。新創建的容器不會創建自己的網卡,配置自己的IP,而是和一個指定的容器共享IP、端口范圍等。同樣,兩個容器除了網絡方面,其他的如文件系統、進程列表等還是隔離的。兩個容器的進程可以通過lo網卡設備通信。
4.3 None 模式
None 模式將容器放置在它自己的網絡棧中,并不進行任何配置。實際上,該模式關閉了容器的網絡功能,該模式下容器并不需要網絡(例如只需要寫磁盤卷的批處理任務)。
4.4 Bridge 模式
Bridge 模式是 Docker 默認的網絡設置,此模式會為每一個容器分配 Network Namespace、設置IP等。當Docker Server啟動時,會在主機上創建一個名為docker0的虛擬網橋,此主機上啟動的Docker容器會連接到這個虛擬網橋上。虛擬網橋的工作方式和物理交換機類似,主機上的所有容器就通過交換機連在了一個二層網絡中。

Docker 會從RFC1918所定義的私有IP網段中,選擇一個和宿主機不同的IP地址和子網分配給docker0,連接到 docker0 的容器從子網中選擇個未占用 IP 使用。一般 Docker 會用 172.17.0.0/16 這個網段,并將172.17.0.1/16 分配給 docker0 網橋(在主機上使用ifconfig命令是可以看到docker0的,可以認為它是網橋的管理接口,在宿主機上作為一塊虛擬網卡使用)。
網絡配置的過程大致3步:
在主機上創建一對虛擬網卡 veth pair 設備。veth設備總是成對出現的,它們組成了一個數據的通道,數據從一個設備進入,就會從另一個設備出來。因此veth設備常用來連接兩個網絡設備。
- Docker 將 veth pair 設備的一端放在新創建的容器中,并命名為eth0。另一端放在主機中,以veth65f9 這樣類似的名字命名,并將這個網絡設備加入到docker0網橋中,可以通過brctl show命令查看。
- 從 docker0 子網中分配一個IP給容器使用,并設置 docker0 的IP地址為容器的默認網關。
- Bridge 模式下容器的通信
容器訪問外部
假設主機網卡為eth0,IP地址10.10.101.105/24,網關10.10.101.254。從主機上一個IP為172.17.0.1/16 的容器中ping百度(180.76.3.151)。首先IP包從容器發往自己的默認網關 docker0,包到達docker0后,會查詢主機的路由表,發現包應該從主機的 eth0 發往主機的網關10.10.105.254/24。接著包會轉發給eth0,并從eth0發出去。這時Iptable規則就會起作用,將源地址換為 eth0 的地址。這樣,在外界看來,這個包就是從10.10.101.105上發出來的,Docker容器對外是不可見的。
外部訪問容器
創建容器并將容器的80端口映射到主機的80端口。當我們對主機 eth0 收到的目的端口為80的訪問時候,Iptable規則會進行DNAT轉換,將流量發往172.17.0.2:80,也就是我們上面創建的Docker容器。所以,外界只需訪問10.10.101.105:80就可以訪問到容器中的服務。
4.5 --link
容器創建后我們想通過容器名字來ping。此時需要用到--link,如下:
- docker run -d -P --name linux03 --link linux02 linux
- docker exec -it linux03 ping linux02 可ping通。
- docker exec -it linux02 ping linux03 不可ping通。
追本溯源 看下 linux03 的 /etc/hosts 會發現本質只是做了個host映射。
- 172.17.0.3 linux03 12ft4tesa # 跟Windows的host文件一樣,只是做了地址綁定
4.6 自建Bridge
我們之前直接啟動的命令 (默認是使用--net bridge,可省),這個bridge就是我們的docker0。下面倆是等價的。
- docker run -d -P --name linux01 LinuxSelf
- docker run -d -P --name linux01 --net bridge LinuxSelf
docker0默認不支持域名訪問 , 只能用 --link 打通連接。如果我們使用自定義的網絡時,docker底層已經幫我們維護好了對應關系,可以實現域名訪問。
- # --driver bridge 網絡模式定義為 :橋接
- # --subnet 192.168.0.0/16 定義子網 ,范圍為:192.168.0.2 ~ 192.168.255.255
- # --gateway 192.168.0.1 子網網關設為: 192.168.0.1
- docker network create --driver bridge --subnet 192.168.0.0/16 --gateway 192.168.0.1 mynet
接下來
- docker run -d -P --name linux-net-01 --net mynet LinuxSelf
- docker run -d -P --name linux-net-02 --net mynet LinuxSelf
- docker exec -it linux-net-01 ping linux-net-02的IP # 結果OK
- docker exec -it linux-net-01 ping linux-net-02 # 結果OK
5 可視化界面
5.1 Portainer
Portainer 是 Docker 的圖形化管理工具,提供狀態顯示面板、應用模板快速部署、容器鏡像網絡數據卷的基本操作(包括上傳下載鏡像,創建容器等操作)、事件日志顯示、容器控制臺操作、Swarm集群和服務等集中管理和操作、登錄用戶管理和控制等功能。功能十分全面,基本能滿足中小型單位對容器管理的全部需求。
5.2 DockerUI
DockerUI基于Docker API,提供等同Docker命令行的大部分功能,支持container管理,image管理。不過DockerUI一個致命的缺點就是 不支持多主機。
5.3 Shipyard
Shipyard 是一個集成管理docker容器、鏡像、Registries的系統,它可以簡化對橫跨多個主機的Docker容器集群進行管理. 通過Web用戶界面,你可以大致瀏覽相關信息,比如你的容器在使用多少處理器和內存資源、在運行哪些容器,還可以檢查所有集群上的事件日志。
6 Docker 學習指南
本來也想寫常見指令、Dockerfile、Docker Compose、Docker Swarm的,不過感覺還是拉閘吧,官方文檔他不香啊!推薦幾個學習指南。
官方文檔:https://docs.docker.com/engine/reference/commandline/build/
從入門到實踐:https://github.com/yeasy/docker_practice
在線教程:https://vuepress.mirror.docker-practice.com