共计 2389 个字符,预计需要花费 6 分钟才能阅读完成。
为什么是容器
如果问你现在最热门的服务器端技术什么?想必很多人会不假思索的说是容器!
容器技术实际上并不是一个新鲜的名词,现在大家一提到容器马上想到的就是 Docker,但是容器这个词并不是 Docker 公司发明的,早期的 Pass 项目如 CloudFoundry,其底层就是基于 namespace 和 cgroups 的容器技术。
Docker 在当时实际上是个小弟,并没有引起大家的注意,但是现在 Docker 已经成为容器事实上的标准,是什么让 Docker 发展成现在这样的程度呢?
这个功能就是 Docker 镜像。
早期的 Pass 平台最为人诟病的一个软肋就是应用的打包部署问题,一个应用包在本地部署的好好的,接入 Pass 平台后却问题重重,而 Docker 镜像解决了这个根本性问题。
容器到底是怎么回事
前面我们说了容器实际上很早以前就有的技术,主要用到的是 Linux 的 namespace,cgroups 和 rootfs。
我们经常说沙盒或者集装箱,他们里面装的是货物,那容器这个沙盒装的又是什么呢?是进程!
我们把进程装进一个沙盒 (容器) 里面,给他制造边界,和盒子外面的世界隔离,所以我们会说容器实际上就是加了围墙的一个进程。
Namespace
为进程制造边界就需要用到 namespace 技术,我们先运行一个 docker 进程看一下:
docker run -it busybox /bin/sh
# -it 是提供一个 tty 的输入输出环境
# -d 后台运行程序
# -v 挂载外部存储
# -p 端口映射
# -e 参数变量
# busybox 轻量级的容器镜像
我们执行 ps 命令,可以看到有趣的现象,PID 为 1 的就是我们的启动进程。
而实际上在宿主机上面会启动一个进程,其 PID 在这里是 22035。
这就是 PID namespace 实现的障眼法,它在 Linux 进程启动的时候(clone 函数),会添加 CLONE_NEWPID 的参数,进程就会看到一个新的命名空间,所以进程 ID 就会变成 1,实际上进程在宿主机上面还是 22035。
除了 PID namespace 之外,还有很多的 namespace,比如 Network、Mount、User 等,通过这些 namespace 对进程进行网络、储存、文件等进行限制,使这个进程看不到宿主机的真实情况,装在了盒子里,这就是容器的核心原理了。
Cgroups
我们通过 Linux 的命名空间为新创建的进程隔离了文件系统、网络、宿主机器上的其他进程,但是命名空间并不能够为我们提供物理资源上的隔离,比如 CPU 或者内存。在同一台机器上可能运行着多个对彼此以及宿主机器一无所知的『容器』,但这些容器却共同占用了宿主机器的物理资源。
如果其中的某一个容器正在执行 CPU 密集型的任务,那么它就会影响其他容器的任务执行效率,导致多个容器相互影响并且抢占资源。如何对多个容器的资源使用进行限制就成了解决进程虚拟资源隔离之后的主要问题,而 Control Group(简称 cgroups)就能隔离宿主机器上的物理资源,例如 CPU、内存、磁盘 I/O 和网络带宽。cgroups 介绍、应用实例及原理描述
容器是一个单进程的模型
通过 namespace 和 cgroups 的学习我们知道了容器就是一个启用了多个 namespace 的应用进程,而这个进程能够使用的资源受到 cgroups 的限制。这里面有个很重要的概念:容器是一个单进程的模型。
由于容器本质上面是一个进程,即 PID= 1 的进程,他是后续其他进程的父进程,这就意味着在一个容器内,你没有办法同时运行两个应用,除非找到一个公共的 PID= 1 的父进程,并使用像 systemd 或者 supervisor 这样的软件替代 PID= 1 的进程来做为容器的启动进程。
但是我们还是希望容器和应用是同生命周期的,因为如果容器是好的,而里面的进程却已经挂了,这样处理起来就会非常麻烦了。
通过上面对容器原理的了解,我们能不能分析出容器和虚拟机的区别?
- 虚拟机需要 hypervisor 层,在上面创建虚拟机是一个完整的 OS
- 容器是 Linux 上的一个进程
- 虚拟机的 OS 资源消耗比容器大的多
- 容器使用的是宿主机上相同的内核
- 容器隔离不了时间等资源
镜像
前面说了 Docker 能够成为容器现在的事实标准,主要是因为 Docker 创新了镜像这个东西,那么镜像在 Linux 系统里面是怎么存在的呢?
我们 Docker 的工作目录是 /app/docker/docker/,其中有个 overlay2 子目录,它就是我们的镜像目录。
我们在这个目录下有三个目录和一个 l 的目录,如下:
我们可以进入其中一个目录,并查看该目录下 diff 子目录的内容:
到这里我们可以知道,镜像是由多个层组织并定义的,这些层本质上是文件,这些文件是只读的,每层具体的文件存放在层标识符下的 diff 目录下。
所以我们在制作镜像的时候就需要理解层的概念,提高镜像制作的效率和重复使用性。
以我们最常使用的 Dockerfile 制作镜像举例:
FROM node:8.16.0
RUN cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
RUN echo "Asia/Shanghai" >> /etc/timezone
RUN date -R
RUN mkdir -p /opt/app/
WORKDIR /opt/app
RUN rm -rf /opt/app/node_modules/
COPY package.json ./
RUN npm install --registry=https://registry.npm.taobao.org
Dockerfile 中的每一条命令在最终生成的镜像中都会产生一层。Docker 为了提高镜像分发效率,给镜像赋予了复用层的能力,在拉取,推送,build 不同镜像时,不同镜像中内容相同的层可以被复用从而节省大量操作。
所以我们应该尽量把不变的命令放到 Dockerfile 的上层,这样会显著提高镜像的使用效率。