什麼是 docker

兩個基本的事實:java

  • 不管是虛擬機仍是容器,都是提供程序的運行時環境;
  • 開發者只關心程序運行的結果,不關心程序的運行過程。

對於運維來說,可能關注虛擬機和容器之間差別會更多些,由於涉及到平常維護、排障、監控、日誌等操做,但這不是重點。至少不是本篇文章的重點。node

那麼一個程序的運行須要哪些條件呢?總結下來有這麼幾個:python

  • 程序文件自己;
  • 程序的依賴;
  • 操做系統內核。

程序自己就不用提了,你的程序得是可以運行的。固然對於 java、python 這類自己沒法編譯成二進制的語言而言,你須要保證 java 虛擬機以及 python 解釋器的存在。linux

程序依賴

程序自身 OK 後,咱們須要考慮程序的依賴問題。爲了程序的開發儘量的簡單和快捷,開發者們會將通用且經常使用的功能作成各類程序庫,當開發者須要使用這些功能的時候,只須要引用或者調用這些庫就好了。c++

經過這樣的方式,確實極大的提高了開發的效率,同時也極大的下降了開發的難度,可是它同時也會爲程序產生依賴的問題。固然,每種語言自身會提供依賴的解決方案,好比 java 的 maven、python 的 pip 等。經過這些工具,你能夠保證你的程序依賴的庫文件你的程序均可以加載到。可是一旦你依賴的庫文件依賴於系統層面的庫(c 標準庫)時,maven、pip 這類的工具是沒法知曉這樣的問題的。而當操做系統缺乏這些的庫時,程序就會運行失敗。es6

你也許會遇到這樣的報錯:docker

# ./clocktest
./clocktest: error while loading shared libraries: libpthread_rt.so.1: cannot open shared object file: No such file or directory
複製代碼

這是典型的在操做系統上找不到依賴的共享 C 庫文件,這裏缺乏的是 libpthread_rt.so.1。固然你頗有可能不是這個庫,但錯誤信息是相似的。你甚至能夠在操做系統上對這種狀況進行模擬:vim

[root@localhost ~]# ldd /bin/man # 查看 man 命令依賴哪些系統庫
        linux-vdso.so.1 =>  (0x00007ffd121f8000)
        libmandb-2.6.3.so => /usr/lib64/man-db/libmandb-2.6.3.so (0x00007f077f4a8000)
        libman-2.6.3.so => /usr/lib64/man-db/libman-2.6.3.so (0x00007f077f288000)
        libgdbm.so.4 => /lib64/libgdbm.so.4 (0x00007f077f07f000)
        libpipeline.so.1 => /lib64/libpipeline.so.1 (0x00007f077ee72000)
        libc.so.6 => /lib64/libc.so.6 (0x00007f077eaa5000)
        libz.so.1 => /lib64/libz.so.1 (0x00007f077e88f000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f077f6ae000)
[root@localhost ~]# mv /lib64/libpipeline.so.1 /tmp/ # 將庫文件移走
[root@localhost ~]# man ls # 命令就運行失敗了
man: error while loading shared libraries: libpipeline.so.1: cannot open shared object file: No such file or directory
複製代碼

上面的示例中,咱們能夠經過 ldd 這個命令來查看一個二進制程序依賴哪些操做系統的庫文件。一旦它依賴的庫文件缺失,它就沒法運行。可是對於 java、python 這樣的語言來講,它的程序文件是沒法編譯成二進制的,它們的二進制程序只是 java 和 python。你就沒法經過這種方式來查看你的程序是否會有這種依賴。centos

若是運行你程序的操做系統庫文件齊全,你根本不會遇到這樣的狀況,頗有可能也不會知道你的程序會依賴系統庫文件。不過出現這樣的問題,只要找到這個庫所屬的 rpm 包,yum 安裝就行。固然,若是你的程序是 C 寫的,可能還會有頭文件的依賴。安全

第三方庫爲何會依賴系統庫?由於系統庫提供了不少相對於比較底層的功能,好比圖形接口等。你想造輪子都無法造(非 C 語言),由於內核都是 C 寫的。

庫文件也有版本,也會存在版本衝突的狀況。當你的操做系統兩個程序使用同一個庫的不一樣版本時,會在 yum 安裝時提示衝突。其實不經過 yum 安裝,運行時也會存在問題。

對於能夠將程序源代碼編譯成二進制的語言,好比 c、c++、go、rust 等,你能夠在編譯的時候將它依賴的動態庫文件編譯到程序二進制文件中,這樣就不會存在運行時庫文件找不到的問題。不過即便沒法將動態庫編譯到程序文件中,你也能夠經過 ldd 來查看它的依賴,而後提供就好。

內核

程序的依賴解決之後,就剩下內核了,程序的運行爲何依賴於內核呢?首先你得明白,內核是一個操做系統的絕對核心,它提供以下功能:

  • 進程管理;
  • 文件系統;
  • 驅動程序;
  • 網絡子系統;
  • 安全功能;
  • 內存管理。

直觀的講,用戶空間的進程,也就是你寫的程序,是沒法直接和硬件打交道的。這裏的硬件包括 cpu、內存、磁盤、網卡等,你的程序是沒法直接使用它們。不過你會發現你的程序使用內存、磁盤和網絡時不存在任何問題,這是由於你的程序在使用這些資源的時候會發起系統調用,由內核幫你完成。

內核是核心,可是光有內核還不行,你還須要和內核進行交互。而衆多的 Linux 發行版,包括 CentOS、Ubuntu、Debian 等就是提供和內核交互功能的。

程序自己、程序的依賴和內核共同構成了程序運行的最基本的因素。當咱們須要在操做系統上運行一個程序時,內核必定存在,所以咱們只須要提供程序文件和它的依賴就可讓它運行起來。docker 使用的就是這種思想,它經過鏡像來提供程序以及它的依賴。

相比於 Linux 發行版,docker 鏡像因爲只須要爲一個特定進程提供運行所需環境,所以它能夠作的很是小。鏡像越小下載越快,運行就越快,所以鏡像越小越好。而若是你能將鏡像作的越小,你對操做系統的理解就越深。

docker 和虛擬機

docker 和虛擬機之間差距巨大,二者之間的技術難度也不是同一個層次。虛擬機和 docker 其實都是宿主機上的一個進程,爲什麼差距這麼大?又或者說 docker 輕量,它輕量在哪呢?它們最大的區別在於虛擬機使用了本身的內核,這會形成一系列很是複雜的問題。

咱們先說說虛擬機重在哪,先看一個虛擬機(kvm)進程:

/usr/libexec/qemu-kvm -name k8s.master.01 -S -machine pc-i440fx-rhel7.0.0,accel=kvm,usb=off,dump-guest-core=off -cpu Broadwell-IBRS,+vme,+ds,+acpi,+ss,+ht,+tm,+pbe,+dtes64,+monitor,+ds_cpl,+vmx,+smx,+est,+tm2,+xtpr,+pdcm,+dca,+osxsave,+f16c,+rdrand,+arat,+tsc_adjust,+intel-pt,+stibp,+ssbd,+xsaveopt,+pdpe1gb,+abm -m 8096 -realtime mlock=off -smp 4,sockets=4,cores=1,threads=1 -uuid e43f9c9d-d295-4b95-9616-9e1dc5cd854d -no-user-config -nodefaults -chardev socket,id=charmonitor,path=/var/lib/libvirt/qemu/domain-1-k8s.master.01/monitor.sock,server,nowait -mon chardev=charmonitor,id=monitor,mode=control -rtc base=utc,driftfix=slew -global kvm-pit.lost_tick_policy=delay -no-hpet -no-shutdown -global PIIX4_PM.disable_s3=1 -global PIIX4_PM.disable_s4=1 -boot strict=on -device ich9-usb-ehci1,id=usb,bus=pci.0,addr=0x5.0x7 -device ich9-usb-uhci1,masterbus=usb.0,firstport=0,bus=pci.0,multifunction=on,addr=0x5 -device ich9-usb-uhci2,masterbus=usb.0,firstport=2,bus=pci.0,addr=0x5.0x1 -device ich9-usb-uhci3,masterbus=usb.0,firstport=4,bus=pci.0,addr=0x5.0x2 -device virtio-serial-pci,id=virtio-serial0,bus=pci.0,addr=0x6 -drive file=/home/k8s.master.01,format=qcow2,if=none,id=drive-virtio-disk0 -device virtio-blk-pci,scsi=off,bus=pci.0,addr=0x7,drive=drive-virtio-disk0,id=virtio-disk0,bootindex=1 -drive file=/opt/CentOS-7-x86_64-Minimal-1804.iso,format=raw,if=none,id=drive-ide0-0-1,readonly=on -device ide-cd,bus=ide.0,unit=1,drive=drive-ide0-0-1,id=ide0-0-1,bootindex=2 -netdev tap,fd=25,id=hostnet0,vhost=on,vhostfd=27 -device virtio-net-pci,netdev=hostnet0,id=net0,mac=52:54:00:1a:4c:0a,bus=pci.0,addr=0x3 -chardev pty,id=charserial0 -device isa-serial,chardev=charserial0,id=serial0 -chardev spicevmc,id=charchannel0,name=vdagent -device virtserialport,bus=virtio-serial0.0,nr=1,chardev=charchannel0,id=channel0,name=com.redhat.spice.0 -spice port=5900,addr=127.0.0.1,disable-ticketing,image-compression=off,seamless-migration=on -vga qxl -global qxl-vga.ram_size=67108864 -global qxl-vga.vram_size=67108864 -global qxl-vga.vgamem_mb=16 -global qxl-vga.max_outputs=1 -device intel-hda,id=sound0,bus=pci.0,addr=0x4 -device hda-duplex,id=sound0-codec0,bus=sound0.0,cad=0 -chardev spicevmc,id=charredir0,name=usbredir -device usb-redir,chardev=charredir0,id=redir0,bus=usb.0,port=1 -chardev spicevmc,id=charredir1,name=usbredir -device usb-redir,chardev=charredir1,id=redir1,bus=usb.0,port=2 -device virtio-balloon-pci,id=balloon0,bus=pci.0,addr=0x8 -msg timestamp=on
複製代碼

這就啓動了一臺虛擬機了,這些參數看起來很是底層,可讀性不好。至於這個進程裏面是怎麼實現內核以及各類用戶空間進程的,它們怎麼交互的,咱們根本看不到,都由虛擬機自行維護。整個過程很是抽象,難以理解。

同時,虛擬機使用的硬件是模擬的,內核也是本身的,所以虛擬機的任何操做都要比 docker 都多作一次。

咱們首先拿虛擬機內存來舉例。

虛擬機內存管理

有沒有想過一個問題,假如你的操做系統有 4G 內存,在不知道你的操做系統會運行哪些程序的狀況下,內核怎麼肯定哪些內存空間是給 A 進程,哪些內存空間是給 B 進程的呢?而一旦將某一段內存區域劃分給某個進程以後,它是否是就只能使用這麼多內存呢?全部進程都直接使用物理內存,隨着內存不斷的申請和釋放,勢必會產生很是多內存碎片,可用的內存只會愈來愈小。

爲了解決這樣的問題,進程不會直接使用物理內存,而是使用虛擬的線性內存,就是在進程和物理內存之間加一箇中間層。咱們以 32 位系統爲例,每一個進程都認爲本身有 4G 內存可用,其中的 1G 爲內核所使用。所以,在每一個進程看來,當前系統上只有本身和內核這兩個進程。

爲了實現這種機制,cpu 必需要將除了內核以外的內存劃分紅一個又一個的頁面(頁框),每個頁框都是一個固定大小的存儲單元,每個都是 4k。當任何一個進程啓動以後,假如它須要 10k 的空間,內核會在內存中找 3 個 4k 的頁面。而這三個頁面在內存中頗有多是不相鄰的,也就是說它們頗有多是不連續的,可是在每個進程看來,它是連續的。進程爲何會認爲是連續的呢?由於它看到的是內核爲它維持的內核數據結構,咱們在數據結構中規定了進程可以使用的空間是 3G,而且是連續的。

線性地址只是一箇中間層,數據最終仍是要存放到物理地址,CPU 中的有一個專門的設備 MMU 專門負責這種線性地址和物理內存之間的映射關係。

這樣會形成一個問題:虛擬機做爲操做系統中的一個進程,它使用的內存是線性內存。自己劃分給虛擬機的內存就是虛擬的,結果虛擬機中的進程使用線性內存會經過 MMU 映射到虛擬機的線性內存。最終在宿主機層面,線性內存纔會真正映射到物理內存。也就說虛擬機中的進程使用的內存會被映射兩次,性能可想而知。

早期會使用半虛擬化來解決這樣的問題,固然如今 CPU 已經支持將虛擬機中的線性內存直接映射到宿主機的物理內存上,一步到位。從這點能夠看出虛擬機的複雜性,而 docker 直接使用宿主機的內存,徹底沒有這樣的問題。

下面在介紹 namespace 時,你會對 docker 的這個過程更加理解。

虛擬機系統調用

虛擬機提供了包括內核在內的完整操做系統,但它自己只是宿主機的一個進程。當虛擬機中的進程想要和硬件打交道時,會發起系統調用,由內核幫它完成。好比當虛擬機中某個進程須要申請內存,該進程會發起系統調用,虛擬機的內核登場。可是虛擬機自己就是宿主機上的一個進程,虛擬機的內核根本沒法訪問內存,因而它只能發起一個系統調用,讓宿主機的內核幫忙申請。一個虛擬機內的進程想要申請內存都須要通過兩次系統調用,性能一樣十分低下。

早期一樣會使用半虛擬來解決這樣的問題,不過如今的 cpu 一樣支持讓虛擬機的內核直接和宿主機的硬件打交道。

兩者對比

上面只是簡單列出了虛擬機重在哪,與之對應的就是 docker 的輕。docker 的輕就輕在它就是操做系統的一個進程,它運行方式和進程如出一轍,徹底沒有很是抽象、難以理解的虛擬化過程。它的資源隔離以及限制都由內核完成(下面會講到),整個過程很是容易理解。

不少人喜歡將 docker 和虛擬機進行對比,我這裏也簡單列下:

對比點 虛擬機 docker
啓動速度 須要硬件自檢、內核引導、用戶空間初始化,慢 很是快
複雜度 雖然是一個進程,可是難以理解,很是複雜 就是一個進程
資源佔用 衆多內核進程產生額外消耗 無多餘消耗
隔離性

對比只是爲了看起來更直觀,可是它們本質上就不是同一類產品。就說最簡單的一點,你只要在本地安裝了 docker,想要什麼服務 docker run 一下就行,虛擬機不可能作到。docker 真正方便的是開發者,若是開發者不會使用的話,就挺吃虧。

安裝 docker

接下來咱們就重點講述 docker 的一些特色以及它的一些實現。首先咱們須要安裝它(這裏基於 centos7 和阿里雲 yum 源)。

yum install -y yum-utils device-mapper-persistent-data lvm2
yum-config-manager --add-repo https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
yum makecache fast
yum -y install docker-ce
複製代碼

安裝完成後啓動它並設置開機自啓:

systemctl enable --now docker
複製代碼

docker 只支持 Linux 平臺,雖然 Mac 和 Windows 都能安裝,但都是經過虛擬機來實現,並不是原生支持。主要仍是三者內核徹底不一致。

docker 鏡像

前面提到了,docker 本質上就是提供程序自己以及它的依賴,docker 經過鏡像將它們組織到一塊兒。鏡像是宿主機磁盤上的文件,裏面包含了應用的程序文件以及依賴。鏡像雖然表現的和一個程序文件同樣,可是它不是一個文件,它是一種分層結構。當你製做一個鏡像的時候,你必定會基於一個鏡像開始製做,而不是徹底從零開始。這也是鏡像的一個特色。

爲了方便鏡像的傳播,也爲了減小鏡像佔用的空間以及下載的時間,docker 對鏡像進行了分層。怎麼理解呢?好比咱們如今有個 java 程序,想要把它作成鏡像。想要將這個程序運行起來,那咱們鏡像中必需要有 java 環境,若是咱們還要提供 java 環境的話,製做鏡像就太麻煩了。咱們完成能夠在已經存在的、別人已經制做好的 jre 鏡像上,將咱們的程序加上去,這樣咱們的程序就能夠直接運行了。

在這個場景中,jre 鏡像是一個鏡像層,咱們新增的內容會在其上增長鏡像層。而咱們使用的 jre 鏡像,它也不可能只有一層,它在製做的時候可能也是在一個知足了 java 運行環境的鏡像上進行的。

咱們只需在 dockerhub 上面搜下 java,就能夠看到很是多的 java 鏡像。咱們隨便選一個官方的鏡像,pull 下來:

# docker pull openjdk:8-alpine 
8-alpine: Pulling from library/openjdk
e7c96db7181b: Extracting [=====================================>             ]  2.064MB/2.757MB
f910a506b6cb: Download complete 
c2274a1a0e27: Downloading [=>                                                 ]  2.669MB/70.73MB
複製代碼

pull 命令用來從拉取鏡像到本地,鏡像名由三部分組成:REGISTRY/IMAGE:TAG

  • REGISTRY:鏡像所在的倉庫,若是將其省略,那麼表示從 docker 官方倉庫 pull。咱們這裏就是從官方拉的;
  • IMAGE:鏡像名稱。這裏的名稱是 openjdk;
  • TAG:對鏡像的一種補充說明,通常會說明版本以及基於的發行版。咱們這裏是 8-alpine。

alpine 是一種發行版,它使用的不是標準 C 庫 glibc,而是 muslc。特色是很是小,性能會比 glibc 差一些?由於基於 alpine 製做的鏡像都很小,且它自帶包管理工具,不少流行應用經過它能夠直接下載安裝,製做鏡像十分方便,所以愈來愈多的鏡像基於它來製做。

從 pull 過程當中,能夠看到這個鏡像有三層。下載到本地以後能夠經過 docker images 列出當前機器上的全部的鏡像。你能夠經過下面的命令查看該鏡像的層數:

echo -e `docker inspect --format "{{ range .RootFS.Layers }}{{.}}\n{{ end }}" openjdk:8-alpine`
複製代碼

經過 docker history 能夠看到它的構建過程:

[root@localhost ~]# docker history openjdk:8-alpine
IMAGE               CREATED             CREATED BY                                      SIZE                COMMENT
a3562aa0b991        15 months ago       /bin/sh -c set -x  && apk add --no-cache   o…   99.3MB              
<missing>           15 months ago       /bin/sh -c #(nop) ENV JAVA_ALPINE_VERSION=8… 0B 
<missing>           15 months ago       /bin/sh -c #(nop) ENV JAVA_VERSION=8u212 0B 
<missing>           15 months ago       /bin/sh -c #(nop) ENV PATH=/usr/local/sbin:… 0B 
<missing>           15 months ago       /bin/sh -c #(nop) ENV JAVA_HOME=/usr/lib/jv… 0B 
<missing>           15 months ago       /bin/sh -c {   echo '#!/bin/sh';   echo 'set… 87B <missing> 15 months ago /bin/sh -c #(nop) ENV LANG=C.UTF-8 0B <missing> 15 months ago /bin/sh -c #(nop) CMD ["/bin/sh"] 0B <missing> 15 months ago /bin/sh -c #(nop) ADD file:a86aea1f3a7d68f6a… 5.53MB 複製代碼

當你 pull 一個鏡像時,docker 會對它的鏡像層進行校驗,若是該層本地已經存在就再也不下載,這樣能夠節約空間和時間。不過任何事情都是有利有弊。docker 的鏡像層都是隻讀的,無論運行起來成容器後怎麼寫都影響不到鏡像,這樣能夠確保你每一次運行鏡像的結果都是相同。

當同一個文件出如今多個層中時,你只能看到最上層的文件,下面全部層中的該文件都不可見。這種結構在共享鏡像的時候很是方便,由於你只須要下載本地沒有的層。可是它也會帶來性能的問題,當你在可寫層(鏡像運行成容器後會添加一層可寫層)修改一個底層的文件時,docker 必需要將文件從底層複製到頂層以後才能寫(copy-on-write 寫時複製),當鏡像層數越多,它找這個文件就會越慢。當這個文件越大,它複製也會越慢。這也就是爲何說鏡像越小越好、鏡像層越少越好的的緣由。

因爲鏡像層是隻讀的,容器運行時只會在鏡像層的頂層添加一層可寫層,所以同一個鏡像運行爲多個容器時,多個容器共享一樣鏡像層。當你在可寫層刪除鏡像層的文件時,docker 只是將其屏蔽,讓你不可見,而不會真正刪除。

關於可寫容器層以及寫時複製技術的實現,不一樣的存儲驅動的實現是不一樣的,docker 支持的存儲驅動有:

  • AUFS:18.06 版本以前的默認存儲驅動;
  • Btrfs:須要文件系統的支持,你宿主機得使用 btrfs 才行;
  • Device mapper:早期 centos/rhel 不支持 overlay2 時的首選;
  • Overlay2:docker 的首選,全部目前主流的發行版都支持;
  • ZFS:一樣須要宿主機文件系統支持;
  • VFS:測試用的。

在不一樣的場景,它們有不一樣的表現。可是,咱們通常不會在容器層寫數據,因此知道有這麼個東西就行。

經過 docker info 能夠看到當前使用的存儲驅動,當你的內核支持多個存儲驅動時,docker 會有一個優先的列表。

# docker info
...
 Storage Driver: overlay2
  Backing Filesystem: xfs
  Supports d_type: true
  Native Overlay Diff: true
複製代碼

centos7 上默認使用 overlay2,全部的鏡像層都保存在 /var/lib/docker/overlay2

ls /var/lib/docker/overlay2
複製代碼

可寫層的文件會直接寫入到宿主機的文件系統,由於可寫層會保存在 /var/lib/docker/containers

namespace

當你運行一個鏡像時,docker 會爲這個容器建立 namespace 以及 CGroup。namespace 提供了資源隔離能力,任何運行在容器內的進程看不到宿主機上運行的其餘進程,同時對它們影響很小。

namespace 是內核的功能,它用來隔離操做系統的各類資源。相比於虛擬機的資源隔離,namespace 輕量太多。也正是由於它和 CGroup 的存在,容器的使用才成爲了一種可能。

namespace 有 6 種:

Namespace 系統調用參數 隔離內容
UTS CLONE_NEWUTS 主機名與域名
IPC CLONE_NEWIPC 信號量、消息隊列和共享內存
PID CLONE_NEWPID 進程編號
Network CLONE_NEWNET 網絡設備、網絡棧、端口等等
Mount CLONE_NEWNS 掛載點(文件系統)
User CLONE_NEWUSER 用戶和用戶組

說到安全,namespace 的六項隔離看似全面,實際上依舊沒有徹底隔離 Linux 的資源,好比 SELinux、 Cgroups 以及 /sys、/proc/sys、/dev/sd* 等目錄下的資源。

這個先不談,咱們挑幾個 namespace 驗證一把。

pid namespace

pid namespace 用來隔離 pid,也就說相同的 pid 能夠出如今不一樣的 namespace 下。pid namespace 是一個樹狀結構,根 namespace 是全部 pid namespace 的父節點,全部其餘 namespace 是它的子節點。從根 namespace 能夠看到全部子 namespace 中的進程,反之不能夠。

也就是說,宿主機做爲 pid namespace 的根,能夠看到全部容器中的進程,還能夠經過信號的方式對其進行影響。而容器做爲一個 namespace,只能看到本身下面的。在容器中 pid 爲 1 的進程,在宿主機上只是一個普通的進程。

# docker run -it --name busybox --rm busybox /bin/sh
/ # ps
PID   USER     TIME  COMMAND
    1 root      0:00 /bin/sh
    6 root      0:00 ps
複製代碼
# docker inspect --format "{{.State.Pid}}" busybox
41881
複製代碼

由於 linux 中 pid 爲 1 的進程是全部進程的父進程,咱們能夠在容器中運行一個進程,而後查看這個進程在宿主機上的表現:

/ # cat &
/ # ps
PID   USER     TIME  COMMAND
    1 root      0:00 sh
   10 root      0:00 cat
   11 root      0:00 ps
[1]+  Stopped (tty input)        cat
複製代碼

stop 能夠不用理會。咱們在容器中運行了一個後臺進程,它的 pid 是 10。而後咱們回到宿主機上經過 grep 的方式來找這個進程:

# ps -ef|grep cat
root      41929  41881  0 10:20 pts/0    00:00:00 cat
root      41999  41934  0 10:20 pts/1    00:00:00 grep --color=auto cat
複製代碼

能夠看到它的 pid 是 41929,父進程是 41881,也就是容器的進程。經過這個你可能不必定確認它就是咱們要的 cat 進程,你能夠直接 kill 它,而後回到容器中查看。

user namespace

它能夠實現普通用戶的進程,在其餘 namespace 中運行的用戶是 root。

咱們已經知道,容器是一個進程,既然是進程,那就必定有運行它的用戶。默認狀況下,運行容器的用戶爲 root(和 docker 配置有關)。雖然容器提供了資源隔離性,可是使用 root 運行總歸存在安全隱患。咱們就能夠經過 user namespace 的方式讓其以非 root 用戶運行。

# docker run --rm -it --name busybox --user=99:99 busybox /bin/sh
複製代碼

將宿主機的 uid 和 gid 爲 99 的用戶(nobody)映射成了容器中 root 用戶,容器中的全部進程都只擁有 99 用戶的權限。

你能夠在宿主機上看到該容器的全部 namespace:

ll /proc/42534/ns/
total 0
lrwxrwxrwx 1 nobody nobody 0 Aug  7 10:55 ipc -> ipc:[4026532585]
lrwxrwxrwx 1 nobody nobody 0 Aug  7 10:55 mnt -> mnt:[4026532583]
lrwxrwxrwx 1 nobody nobody 0 Aug  7 10:46 net -> net:[4026532588]
lrwxrwxrwx 1 nobody nobody 0 Aug  7 10:55 pid -> pid:[4026532586]
lrwxrwxrwx 1 nobody nobody 0 Aug  7 10:55 user -> user:[4026531837]
lrwxrwxrwx 1 nobody nobody 0 Aug  7 10:55 uts -> uts:[4026532584]
複製代碼

中括號中的數字表示的是這個 namespace 的編號,若是兩個進程的編號相同,證實這兩個進程處於同一個 namespace 之下。

你能夠在當前容器中運行一個可以運行一段時間的命令(就像以前 cat 命令同樣),而後在宿主機上查看這個進程的用戶。

mnt namespace

在咱們運行一個容器以前,咱們先將當前宿主機上的掛載內容輸出到一個文件中:

mount > /tmp/run_before
複製代碼

運行容器,而後在另外一個會話中將掛載的內容輸出到另外一個文件中:

docker run --rm -it busybox
mount > /tmp/running
複製代碼

經過使用 vimdiff 進行比較,你會發現運行一個容器後,會多出兩行掛載:

overlay on /var/lib/docker/overlay2/1da1bb59b8cca7c2c2b681039fe2bd1fd39ad94badad81ee2da93e2aae80c34c/merged type overlay (rw,relatime,seclabel,lowerdir=/var/lib/docker/overlay2/l/3BX7LKVS3ZFSG43S3OZH4ZUJBR:/var/lib/docker/overlay2/l/XPK3YPEURTZO2ZNVMY6WUTGUYO,upperdir=/var/lib/docker/overlay2/1da1bb59b8cca7c2c2b681039fe2bd1fd39ad94badad81ee2da93e2aae80c34c/diff,workdir=/var/lib/docker/overlay2/1da1bb59b8cca7c2c2b681039fe2bd1fd39ad94badad81ee2da93e2aae80c34c/work)
proc on /run/docker/netns/8d6016c8a392 type proc (rw,nosuid,nodev,noexec,relatime)
複製代碼

一個是將宿主機的 /var/lib/docker/overlay2/1da1bb59b8cca7c2c2b681039fe2bd1fd39ad94badad81ee2da93e2aae80c34c/merged 掛載到了容器的根目錄,也就是說這個目錄就是可寫層。

另外一個是 /run/docker/netns/8d6016c8a392 做爲容器的 /proc,看起來和 network namespace 有關。

咱們能夠將當前容器退出後,讓它掛載一個 nfs 後從新運行:

docker run --rm -it --mount 'type=volume,source=nfsvolume,target=/app,volume-driver=local,volume-opt=type=nfs,volume-opt=device=:/opt/test,volume-opt=o=addr=10.0.0.13' busybox
複製代碼

這會將 10.0.0.13 nfs server 上的 /opt/test 目錄掛載到容器的 /app 目錄。

再次在宿主機上執行 mount 命令,你會發現除了容器的兩個掛載以外,還多了一個 nfs 掛載:

:/opt/test on /var/lib/docker/volumes/nfsvolume/_data type nfs (rw,relatime,vers=3,rsize=1048576,wsize=1048576,namlen=255,hard,proto=tcp,timeo=600,retrans=2,sec=sys,mountaddr=10.0.0.13,mountvers=3,mountproto=tcp,local_lock=none,addr=10.0.0.13)
複製代碼

從這點能夠看出,容器的掛載都是掛載到宿主機上,而後映射到容器中,只不過其餘進程不可見。這會給人一種是容器直接掛載的假象。所以你無論是在容器的 /app 目錄,仍是在宿主機的 /var/lib/docker/volumes/nfsvolume/_data 目錄,看到的內容都同樣。

net namespace

對於使用者來說,這個 namespace 是最直觀的。net namespace 提供了網絡層面的隔離,任何容器會有本身的網絡棧。從這一點上看,同一宿主機上容器之間的訪問就像物理機之間的訪問同樣。

容器啓動以後,docker 會爲該容器分配一個 ip 地址,這個地址你在宿主機上沒法看到,只能看到多出了一個網卡。相似這樣的:

16460: vethd9ea29c@if16459: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP group default 
    link/ether e2:43:d2:a1:3b:3a brd ff:ff:ff:ff:ff:ff link-netnsid 1
    inet6 fe80::e043:d2ff:fea1:3b3a/64 scope link 
       valid_lft forever preferred_lft forever
複製代碼

這個網卡是成對出現的,一個在宿主機上的 namespace,一個在容器的 namespace。經過這種方式,容器的 namespace 的流量就能夠經過宿主機出去了。想要驗證這個很簡單,只要對着這個網卡抓包就行。

從上面的輸出信息能夠看到,它的網卡序號是 16460,它的另外一對是 16459。

咱們能夠直接進入容器的 net namespace。先得到其 pid:

pid=`docker inspect --format "{{.State.Pid}}" busybox`
複製代碼

經過 nsenter 切換:

nsenter -n -t $pid
複製代碼

你如今可使用一切宿主機的網絡相關的命令,只不過它顯示的都是該容器中信息。包括 ip、netstat、tcpdump、iptables 等:

# ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
16459: eth0@if16460: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default 
    link/ether 02:42:ac:11:00:02 brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
       valid_lft forever preferred_lft forever

# ip r
default via 172.17.0.1 dev eth0 # 默認網關是 docker0
172.17.0.0/16 dev eth0 proto kernel scope link src 172.17.0.2 
複製代碼

經過 exit 命令退出當前的網絡名稱空間。

默認狀況下,docker 本地的網絡是 bridge 模式。docker0 會做爲橋設備,全部爲容器在宿主機上建立的網卡都會鏈接到 docker0 這個橋設備上。docker0 其實就至關於一個交換機,同時也是全部容器的網關。

你能夠經過 brctl 命令看到這一點;

[root@localhost ~]# brctl show
bridge name     bridge id               STP enabled     interfaces
docker0         8000.02423551e99b       no              veth3ebfdf3
                                                        vethba044e7
複製代碼

當我啓動兩個容器時,這兩個容器的對端網卡都被橋接到了 docker0 上。

cgroup

cgroup 也是內核的功能,經過它來限制進程資源使用。它能夠限制如下資源:

# ll /sys/fs/cgroup/
total 0
drwxr-xr-x 4 root root  0 Jun 23 10:38 blkio
lrwxrwxrwx 1 root root 11 Jun 23 10:38 cpu -> cpu,cpuacct
lrwxrwxrwx 1 root root 11 Jun 23 10:38 cpuacct -> cpu,cpuacct
drwxr-xr-x 4 root root  0 Jun 23 10:38 cpu,cpuacct
drwxr-xr-x 3 root root  0 Jun 23 10:38 cpuset
drwxr-xr-x 4 root root  0 Jun 23 10:38 devices
drwxr-xr-x 3 root root  0 Jun 23 10:38 freezer
drwxr-xr-x 3 root root  0 Jun 23 10:38 hugetlb
drwxr-xr-x 4 root root  0 Jun 23 10:38 memory
lrwxrwxrwx 1 root root 16 Jun 23 10:38 net_cls -> net_cls,net_prio
drwxr-xr-x 3 root root  0 Jun 23 10:38 net_cls,net_prio
lrwxrwxrwx 1 root root 16 Jun 23 10:38 net_prio -> net_cls,net_prio
drwxr-xr-x 3 root root  0 Jun 23 10:38 perf_event
drwxr-xr-x 4 root root  0 Jun 23 10:38 pids
drwxr-xr-x 4 root root  0 Jun 23 10:38 systemd
複製代碼

咱們能夠限制一個容器只能使用 10m 內存:

docker run --rm -it --name busybox -m 10m --mount 'type=tmpfs,dst=/tmp' busybox /bin/sh
複製代碼

你能夠在 CGroup 內存子系統中找到你容器的 pid:

cat /sys/fs/cgroup/memory/system.slice/docker-564f2957666b661bbd4947c945107efa6399e3954af7d4c2c5216521a0d5c5d7.scope/tasks 
43973
複製代碼

這裏的 564f2957666b661bbd4947c945107efa6399e3954af7d4c2c5216521a0d5c5d7 是容器的 id。接着能夠看到它的限制,單位是字節:

cat /sys/fs/cgroup/memory/system.slice/docker-564f2957666b661bbd4947c945107efa6399e3954af7d4c2c5216521a0d5c5d7.scope/memory.limit_in_bytes 
10485760
複製代碼

咱們可使用 dd 命令來模擬內存使用過大:

# dd if=/dev/urandom of=/tmp/xxx bs=2M count=10
Killed
複製代碼

由於是容器中的 dd 命令使用的內存過多,因此內核只殺掉了 dd 進程。而由於容器中 pid 爲 1 的進程(這裏是 sh)沒有被殺掉,因此容器運行正常。從這一點上能夠看出,容器中全部的進程都會受到到資源的限制,你能夠經過查看 CGroup 子系統來驗證這一說法。

這會形成一個問題:若是你容器中運行了 5 個進程,且限制容器只能使用 4G 內存。那麼只要這 5 個進程使用內存在 3.9G,那麼容器以及其中的進程都不會被幹掉,可是容器其實使用的內存已經遠遠超過了 4G,這也是爲何不建議一個容器中跑多個進程的緣由之一。

關於其餘的資源限制這裏就不演示了,咱們只要有這麼回事就行。

運行容器

docker 包裝了程序的自己以及它的依賴,可是它的運行依賴於 Linux 內核,由於它的全部鏡像都是基於 Linux 環境。雖然 Windows 和 Mac 上都提供了 docker 的安裝包,可是都是經過虛擬機的方式完成的,這一點須要注意。

運行容器經過 docker run 來完成,咱們可使用 docker run --help 來查看它支持哪些選項。它支持的選項很是多,而且不少是和資源隔離以及資源限制有關。

相關文章
相關標籤/搜索