本文已收录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 方法。其实这些方法的本质就是让多个服务的进程拥有同一个父进程。
但考虑到容器自己的设计,就是但愿容器和服务可以同生命周期。因此这样作,有点背道而驰的意味。
控制(回收和生命周期的管理)