本文已收錄GitHub,更有互聯網大廠面試真題,面試攻略,高效學習資料等java
從本質上,容器其實就是一種沙盒技術。就好像把應用隔離在一個盒子內,使其運行。由於有了盒子邊界的存在,應用於應用之間不會相互干擾。而且像集裝箱同樣,拿來就走,隨處運行。其實這就是 PaaS 的理想狀態。python
實現容器的核心,就是要生成限制應用運行時的邊界。咱們知道,編譯後的可執行代碼加上數據,叫作程序。而把程序運行起來後,就變成了進程,也就是所謂的應用。若是能在應用啓動時,給其加上一個邊界,這樣不就能實現期待的沙盒嗎?mysql
在 Linux 中,實現容器的邊界,主要有兩種技術 Cgroups 和 Namespace. Cgroups 用於對運行的容器進行資源的限制,Namespace 則會將容器隔離起來,實現邊界。linux
這樣看來,容器只是一種被限制的了特殊進程而已。git
在介紹 Namespace 前,先看一個實驗:github
# 使用 python3.6.8 的官方鏡像,創建了一個運行 django 的環境 # 進入該容器後,使用 ps 命令,查看運行的進程 root@8729260f784a:/src# ps -A PID TTY TIME CMD 1 ? 00:01:22 gunicorn 22 ? 00:01:20 gunicorn 23 ? 00:01:24 gunicorn 25 ? 00:01:30 gunicorn 27 ? 00:01:16 gunicorn 41 pts/0 00:00:00 bash 55 pts/0 00:00:00 ps
能夠看到,容器內 PID =1 的進程,是 gunicorn 啓動的 django 應用。熟悉 Linux 的同窗都知道,PID =1 的進程是系統啓動時的第一個進程,也稱 init 進程。其餘的進程,都是由它管理產生的。而此時,PID=1 確實是 django 進程。面試
接着,退出容器,在宿主機執行 ps 命令算法
# 環境爲 Centos7 [root@localhost ~]# ps -ef | grep gunicorn root 9623 8409 0 21:29 pts/0 00:00:00 grep --color=auto gunicorn root 30828 30804 0 May28 ? 00:01:22 /usr/local/bin/python /usr/local/bin/gunicorn -c gunicorn_config.py ctg.wsgi root 31171 30828 0 May28 ? 00:01:20 /usr/local/bin/python /usr/local/bin/gunicorn -c gunicorn_config.py ctg.wsgi root 31172 30828 0 May28 ? 00:01:24 /usr/local/bin/python /usr/local/bin/gunicorn -c gunicorn_config.py ctg.wsgi root 31174 30828 0 May28 ? 00:01:30 /usr/local/bin/python /usr/local/bin/gunicorn -c gunicorn_config.py ctg.wsgi root 31176 30828 0 May28 ? 00:01:16 /usr/local/bin/python /usr/local/bin/gunicorn -c gunicorn_config.py ctg.wsgi
若是以宿主機的視角,發現 django 進程 PID 變成了 30828. 這也就不難證實,在容器中,確實作了一些處理。把明明是 30828 的進程,變成了容器內的第一號進程,同時在容器還看不到宿主機的其餘進程。這也說明容器內的環境確實是被隔離了。sql
這種處理,其實就是 Linux 的 Namespace 機制。好比,上述將 PID 變成 1 的方法就是經過PID Namespace。在 Linux 中建立線程的方法是 clone, 在其中指定 CLONE_NEWPID 參數,這樣新建立的進程,就會看到一個全新的進程空間。而此時這個新的進程,也就變成了 PID=1 的進程。docker
int pid = clone(main_function, stack_size, CLONE_NEWPID | SIGCHLD, NULL);
在 Linux 相似於 PID Namespace 的參數還有不少,好比:
經過 Namespace 技術,咱們實現了容器和容器間,容器與宿主機之間的隔離。但這還不夠,想象這樣一種場景,宿主機上運行着兩個容器。雖然在容器間相互隔離,但以宿主機的視角來看的話,其實兩個容器就是兩個特殊的進程,而進程之間天然存在着競爭關係,天然就能夠將系統的資源吃光。固然,咱們不能容許這麼作的。
Cgroups 就是 Linux 內核中用來爲進程設置資源的一個技術。
Linux Cgroups 全稱是 Linux Control Group,主要的做用就是限制進程組使用的資源上限,包括 CPU,內存,磁盤,網絡帶寬。
還能夠對進程進行優先級設置,審計,掛起和恢復等操做。
在以前的版本中,可經過 libcgroup tools 來管理 cgroup, 在 RedHat7 後,已經改成經過 systemctl 來管理。
咱們知道,systemd 在 Linux 中的功能就是管理系統的資源。而爲了管理的方便,衍生出了一個叫 Unit 的概念,好比一個 unit 能夠有比較寬泛的定義,好比能夠表示抽象的服務,網絡的資源,設備,掛載的文件系統等。爲了更好的區分,Linux 將 Unit 的類型主要分爲 12 種。
在 Cgroup 中,主要使用的是 slice, scope and service 這三種類型。
如建立一個臨時 cgroup, 而後對其啓動的進程進行資源限制:
# 建立一個叫 toptest 的服務,在名爲 test 的 slice 中運行 [root@localhost ~]# systemd-run --unit=toptest --slice=test top -b Running as unit toptest.service.
如今 toptest 的服務已經運行在後臺了
# 經過 systemd-cgls 來查看 Cgroup 的信息 [root@localhost ~]# systemd-cgls ├─1 /usr/lib/systemd/systemd --switched-root --system --deserialize 22 ├─test.slice │ └─toptest.service │ └─6490 /usr/bin/top -b # 經過 systemctl status 查看服務的狀態 [root@localhost ~]# systemctl status toptest ● toptest.service - /usr/bin/top -b Loaded: loaded (/run/systemd/system/toptest.service; static; vendor preset: disabled) Drop-In: /run/systemd/system/toptest.service.d └─50-Description.conf, 50-ExecStart.conf, 50-Slice.conf Active: active (running) since Tue 2020-06-02 14:01:01 CST; 3min 50s ago Main PID: 6490 (top) CGroup: /test.slice/toptest.service └─6490 /usr/bin/top -b
如今對運行的 toptest 服務進行資源的限制。
# 先看下,沒有被限制前的 Cgroup 的信息, 6490 爲進程 PID [root@localhost ~]# cat /proc/6490/cgroup 11:pids:/test.slice 10:blkio:/test.slice 9:hugetlb:/ 8:cpuset:/ 7:memory:/test.slice 6:devices:/test.slice 5:net_prio,net_cls:/ 4:perf_event:/ 3:freezer:/ 2:cpuacct,cpu:/test.slice 1:name=systemd:/test.slice/toptest.service # 對其使用的 CPU 和 內存進行限制 systemctl set-property toptest.service CPUShares=600 MemoryLimit=500M # 再次查看 Cgroup 的信息,發如今 cpu 和 memory 追加了一些內容。 [root@localhost ~]# cat /proc/6490/cgroup 11:pids:/test.slice 10:blkio:/test.slice 9:hugetlb:/ 8:cpuset:/ 7:memory:/test.slice/toptest.service 6:devices:/test.slice 5:net_prio,net_cls:/ 4:perf_event:/ 3:freezer:/ 2:cpuacct,cpu:/test.slice/toptest.service 1:name=systemd:/test.slice/toptest.service
這時能夠在 /sys/fs/cgroup/memory/test.slice 和 /sys/fs/cgroup/cpu/test.slice 目錄下,多出了一個叫 toptest.service 的目錄。
在其目錄下 cat toptest.service/cpu.shares 能夠發現,裏面的 CPU 被限制了 600.
回到 Docker,其實 docker 和咱們上面作的操做基本一致,具體須要限制哪些資源就是在 docker run 裏指定:
$ docker run -it --cpu-period=100000 --cpu-quota=20000 ubuntu /bin/bash
關於 docker 具體的限制,能夠在 sys/fs/cgroup/cpu/docekr/ 等文件夾來查看。
如今咱們知道,容器技術的核心就是經過 Namespace 限制了容器看到的視野,經過 Cgroup限制了容器可訪問的資源。 但關於 Mount Namespace 還有一些特殊的地方,須要着重關注下。
Mount Namespace 特殊之處在於,除了在修改時須要進程對文件系統掛載點的認證,還須要顯式聲明須要掛載那些目錄。在 Linux 系統中,有一個叫 chroot 的命令,能夠改變進程的根目錄到指定的位置。而 Mount Namespace 正是基於 chroot 的基礎上發展出來的。
在容器內,應該看到徹底獨立的文件系統,並且不會受到宿主機以及其餘容器的影響。這個獨立的文件系統,就叫作容器鏡像。它還有一個更專業的名字叫 rootfs. rootfs 中包含了一個操做系統所須要的文件,配置和目錄,但並不包含系統內核。 由於在 Linux 中,文件和內核是分開存放的,操做系統只有在開啓啓動時纔會加載指定的內核。這也就意味着,全部的容器都會共享宿主機上操做系統的內核。
在 PaaS 時代,因爲雲端和本地的環境不一樣,應用打包的過程,一直是比較痛苦的過程。但有了 rootfs ,這個問題就被很好的解決了。由於在鏡像內,打包的不只僅是應用,還有所須要的依賴,都被封裝在一塊兒。這就解決了不管是在哪,應用均可以很好的運行的緣由。
不光這樣,rootfs 還解決了可重用性的問題,想象這個場景,你經過 rootfs 打包了一個包含 java 環境的 centos 鏡像,別人須要在容器內跑一個 apache 的服務,那麼他是否須要從頭開始搭建 java 環境呢?docker 在解決這個問題時,引入了一個叫層的概念,每次針對 rootfs 的修改,都只保存增量的內容,而不是 fork 一個新鏡像。
層級的想法,一樣來自於 Linux,一個叫 union file system (聯合文件系統)。它最主要的功能就是將不一樣位置的目錄聯合掛載到同一個目錄下。對應在 Docker 裏面,不一樣的環境則使用了不一樣的聯合文件系統。好比 centos7 下最新的版本使用的是 overlay2,而 Ubuntu 16.04 和 Docker CE 18.05 使用的是 AuFS.
能夠經過 docker info 來查詢使用的存儲驅動,我這裏的是 overlay2。
[root@localhost ~]# docker info Client: Debug Mode: false Server: Containers: 4 Running: 4 Paused: 0 Stopped: 0 Images: 4 Server Version: 19.03.8 Storage Driver: overlay2
接着咱們來了解下,Overlay2 的文件系統在 docker 中是如何使用的?
Overlay2
在 Linux 的主機上,OverlayFS 通常有兩個目錄,但在顯示時具體會顯示爲一個目錄。這兩個目錄被稱爲層,聯合在一塊兒的過程稱爲 union mount. 在其下層的目錄稱爲 lowerdir, 上層的目錄稱爲 upperdir. 二者聯合後,暴露出來的視圖稱爲 view. 聽起來有點抽象,先看下總體結構:
能夠看到,lowerdir 其實對應的就是鏡像層,upperdir 對應的就是容器器。而 merged 對應的就是二者聯合掛載以後的內容。並且咱們發現,當鏡像層和容器層擁有相同的文件時,會以容器層的文件爲準(最上層的文件爲準)。一般來講,overlay2 支持最多 128 lower 層。
下面實際看下容器層和鏡像具體的體現,我這臺 linux 主機上,運行着 4 個 container。
Docker 通常的存儲位置在 /var/lib/docker,先看下里面的結構:
[root@localhost docker]# ls -l /var/lib/docker total 16 drwx------. 2 root root 24 Mar 4 03:39 builder drwx--x--x. 4 root root 92 Mar 4 03:39 buildkit drwx------. 7 root root 4096 Jun 1 10:36 containers drwx------. 3 root root 22 Mar 4 03:39 image drwxr-x---. 3 root root 19 Mar 4 03:39 network drwx------. 69 root root 8192 Jun 1 15:01 overlay2 drwx------. 4 root root 32 Mar 4 03:39 plugins drwx------. 2 root root 6 Jun 1 15:00 runtimes drwx------. 2 root root 6 Mar 4 03:39 swarm drwx------. 2 root root 6 Jun 1 15:01 tmp drwx------. 2 root root 6 Mar 4 03:39 trust drwx------. 3 root root 45 May 18 10:28 volumes
須要着重關注的是 container, image, overlay2 這幾個文件夾。
以前提到,unionfs 的實現可能有多種,好比 overlay2,aufs,devicemapper 等。那麼天然在 image 文件夾下,就會存在多種驅動的文件夾,:
image/ └── overlay2 ├── distribution ├── imagedb │ ├── content │ └── metadata ├── layerdb │ ├── mounts │ ├── sha256 │ └── tmp └── repositories.json
這裏的 imagedb 和 layerdb, 就是存儲元數據的地方。以前咱們瞭解到,容器的文件系統構成就是經過 image 層 和 container 層聯合構成的,而每一個 image 多是由多個層構成。這就意味着,每一個層可能會被多個 image 引用。那麼之間是如何關聯的呢?答案就在 imagedb 這個文件下。
這裏我以 mysql 鏡像爲例:
# 查看 mysql 的鏡像 id [root@localhost docker]# docker image ls REPOSITORY TAG IMAGE ID CREATED SIZE ctg/mysql 5.7.29 84164b03fa2e 3 months ago 456MB # 進入到 imagedb/content/sha256 目錄, 能夠找到對應的鏡像 id [root@localhost docker]# ls -l image/overlay2/imagedb/content/sha256/ ... -rw-------. 1 root root 6995 Apr 27 02:45 84164b03fa2ecb33e8b4c1f2636ec3286e90786819faa4d1c103ae147824196a # 接着看下里面記錄的內容, 這裏截取有用的部分 cat image/overlay2/imagedb/content/sha256/84164b03fa2ecb33e8b4c1f2636ec3286e90786819faa4d1c103ae147824196a { ......... "os": "linux", "rootfs": { "type": "layers", "diff_ids": [ "sha256:f2cb0ecef392f2a630fa1205b874ab2e2aedf96de04d0b8838e4e728e28142da", "sha256:a9f6b7c7101b86ffaa53dc29638e577dabf5b24150577a59199d8554d7ce2921", "sha256:0c615b40cc37ed667e9cbaf33b726fe986d23e5b2588b7acbd9288c92b8716b6", "sha256:ad160f341db9317284bba805a3fe9112d868b272041933552df5ea14647ec54a", "sha256:1ea6ef84dc3af6506c26753e9e2cf7c0d6c1c743102b85ebd3ee5e357d7e9bc4", "sha256:6fce4d95d4af3777f3e3452e5d17612b7396a36bf0cb588ba2ae1b71d139bab9", "sha256:6de3946ea0137e75dcc43a3a081d10dda2fec0d065627a03800a99e4abe2ede4", "sha256:a35a4bacba4d5402b85ee6e898b95cc71462bc071078941cbe8c77a6ce2fca62", "sha256:1ff9500bdff4455fa89a808685622b64790c321da101d27c17b710f7be2e0e7e", "sha256:1cf663d0cb7a52a3a33a7c84ff5290b80966921ee8d3cb11592da332b4a9e016", "sha256:bcb387cbc5bcbc8b5c33fbfadbce4287522719db43d3e3a286da74492b7d6eca" ] } }
能夠看到 mysql 鏡像由 11 層組成,其中 f2cb 是最低層,bcb3 是最上層。
接着,咱們看下 layerdb 的內容:
[root@localhost docker]# ls -l image/overlay2/layerdb/ total 8 drwxr-xr-x. 6 root root 4096 May 13 13:38 mounts drwxr-xr-x. 39 root root 4096 Apr 27 02:51 sha256 drwxr-xr-x. 2 root root 6 Apr 27 02:51 tmp # 首先看下 sha256 目錄下的內容 [root@localhost docker]# ls -l image/overlay2/layerdb/sha256/ total 0 .... drwx------. 2 root root 71 Apr 27 02:45 bbb9cccab59a16cb6da78f8879e9d07a19e3a8d49010ab9c98a2c348fa116c87 drwx------. 2 root root 71 Apr 27 02:45 f2cb0ecef392f2a630fa1205b874ab2e2aedf96de04d0b8838e4e728e28142da ....
能夠發現,在這裏僅能找到最底層的層 ID,緣由在於層之間的關聯是經過 chainID 的方式保存的,簡單來講就是經過 sha256 算法後能計算出一層的容器 id.
好比這裏,最底層 id 是 f2cb0ecef392f2a630fa1205b874ab2e2aedf96de04d0b8838e4e728e28142da , 上一層 id 是 a9f6b7c7101b86ffaa53dc29638e577dabf5b24150577a59199d8554d7ce2921, 那麼對應在 sha256 目錄下的下一層 id 的計算方法就是:
[root@localhost docker]# echo -n "sha256:f2cb0ecef392f2a630fa1205b874ab2e2aedf96de04d0b8838e4e728e28142da sha256:a9f6b7c7101b86ffaa53dc29638e577dabf5b24150577a59199d8554d7ce2921" | sha256sum bbb9cccab59a16cb6da78f8879e9d07a19e3a8d49010ab9c98a2c348fa116c87 -
接着咱們能夠在 sha256 目錄下,找到 bbb9.. 這層的內容。
OK,如今咱們已經把鏡像和層關聯起來,但以前說過,image 目錄下存的都是元數據。真實的 rootfs 其實在另外一個地方 - /docker/overlay2 下。
# 經過查詢 cache-id,獲得就是真實的 rootfs 層 [root@localhost docker]# cat image/overlay2/layerdb/sha256/f2cb0ecef392f2a630fa1205b874ab2e2aedf96de04d0b8838e4e728e28142da/cache-id 2996b24990e75cbd304093139e665a45d96df8d7e49334527827dcff820dbf16[
進入到 /docker/overlay2 下查看:
[root@localhost docker]# ls -l overlay2/ total 4 ... drwx------. 3 root root 47 Apr 27 02:45 2996b24990e75cbd304093139e665a45d96df8d7e49334527827dcff820dbf16 ... drwx------. 2 root root 4096 May 13 13:38 l
這樣真實的 rootfs 層也被找到了。
這裏從新梳理下,咱們先是在 mage/overlay2/imagedb/content/sha256/ ,根據 image id 查看該 image 具備全部的層ID,而後根據最底層ID和上層ID經過 sha256 計算獲得,引用的上一層 ID, 依次類推,關聯全部的層。最後經過每一層的 cache-id,將元數據和真實的 rootfs 層數據對應起來了。
最後總結一下,rootfs 的構成。
每一個 rootfs 由鏡像層(lowerdir)和 容器層(upperdir)構成,其中鏡像層只能只讀,而容器層則能讀寫。並且鏡像層可有最多128層構成。
其實,rootfs 構成還有另一層,但因爲在進行提交或編譯時,不會把這層加進去,因此就沒把這層算在rootfs裏面,但實際上存在的。
在以前咱們查看 ls -l /var/lib/docker/overlay2/ 下鏡像層,會看到好幾個以 -init 結尾的目錄,並且數量剛好等於容器的數量。這層夾在鏡像層之上,容器層之下。是由 docker 內部單獨生成的一層,專門用於存放 etc/hosts、/etc/resolv.conf 等配置信息。存在的目的,是因爲用戶在容器啓動時,須要配置一些特定的值,好比 hostname,dns 等,但這些配置僅對當前容器有效,放到其餘環境下天然有別的配置,因此這層被單獨拿出來,在提交鏡像時,忽略這一層。
下面這張圖是 docker 官方中,截取下來的,基於上面咱們學習的內容,從新分析下 docker 和 傳統 VM 的區別:
遷移性和性能:
通常來講,運行着 CentOS 的 KVM,啓動後,在不作優化的前提下,須要佔用 100~200 M 內存。在加上用戶對宿主機的調用,須要經過虛擬化軟件攔截和處理,又是一層性能損耗,特別是對計算資源,網絡和磁盤I/O等。
隔離性:
傳統 VM:因爲是虛擬化出一套完整的操做系統,因此隔離性很是好。好比微軟的 Azure 平臺,就是在 Windows 服務器上,虛擬出大量的 Linux 虛擬機。
資源的限制:
傳統 VM:很是便於管理,控制資源的使用,依賴於虛擬的操做系統。
Docker:因爲 docker 內資源的限制經過 Cgroup 實現,而 Cgroup 有不少不完善的地方,好比
對 /proc 的處理問題。進入容器後,執行 top 命令,看到的信息和宿主機是同樣的,而不是配置後的容器的數據。(能夠經過 lxcfs 修正)。
在運行 java 程序時,給容器內設置的內存爲 4g,使用默認的 jvm 配置。而默認的 jvm 讀取的內存是宿主機(可能大於 4g),這樣就會出現 OOM 的狀況。
1. 容器是如何進行隔離的?
在建立新進程時,經過 Namespace 技術,如 PID namespaces 等,實現隔離性。讓運行後的容器僅能看到自己的內容。
好比,在容器運行時,會默認加上 PID, UTS, network, user, mount, IPC, cgroup 等 Namespace.
2. 容器是如何進行資源限制的?
經過 Linux Cgroup 技術,可爲每一個進程設定限制的 CPU,Memory 等資源,進而設置進程訪問資源的上限。
3. 簡述下 docker 的文件系統?
docker 的文件系統稱爲 rootfs,它的實現的想法來自與 Linux unionFS 。將不一樣的目錄,掛載到一塊兒,造成一個獨立的視圖。而且 docker 在此基礎上引入了層的概念,解決了可重用性的問題。
在具體實現上,rootfs 的存儲區分根據 linux 內核和 docker 自己的版本,分爲 overlay2 , overlay, aufs, devicemapper 等。rootfs(鏡像)其實就是多個層的疊加,當多層存在相同的文件時,上層的文件會將下層的文件覆蓋掉。
4. 容器的啓動過程?
5. 容器內運行多個應用的問題?
首先更正一個概念,咱們都說容器是一個單進程的應用,其實這裏的單進程不是指在容器中只容許着一個進程,而是指只有一個進程時可控的。在容器內固然可使用 ping,ssh 等進程,但這些進程時不受 docker 控制的。
容器內的主進程,也就是 pid =1 的進程,通常是經過 DockerFile 中 ENTRYPOINT 或者 CMD 指定的。若是在一個容器內若是存在着多個服務(進程),就可能出現主進程正常運行,可是子進程退出掛掉的問題,而對於 docker 來講,僅僅控制主進程,沒法對這種意外的狀況做出處理,也就會出現,容器明明正常運行,可是服務已經掛掉的狀況,這時編排系統就變得很是困難。並且多個服務,在也不容易進行排障和管理。
因此若是真的想要在容器內運行多個服務,通常會經過帶有 systemd 或者 supervisord 這類工具進行管理,或者經過 --init 方法。其實這些方法的本質就是讓多個服務的進程擁有同一個父進程。
但考慮到容器自己的設計,就是但願容器和服務可以同生命週期。因此這樣作,有點背道而馳的意味。
控制(回收和生命週期的管理)