This commit is contained in:
louzefeng
2024-07-09 18:38:56 +00:00
parent 8bafaef34d
commit bf99793fd0
6071 changed files with 1017944 additions and 0 deletions

View File

@@ -0,0 +1,386 @@
<audio id="audio" title="01 | 认识容器:容器的基本操作和实现原理" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/yy/cf/yyf6b72216393ebyy9a3a74a912195cf.mp3"></audio>
你好我是程远。作为一名工程师我猜在过去的几年时间里你肯定用过或者听人提起过容器Container
说实话,容器这东西一点都不复杂,如果你只是想用的话,那跟着[Docker官网](https://docs.docker.com/get-started/)的说明,应该十来分钟就能搞定。
简单来说,它就是个小工具,可以把你想跑的程序,库文件啊,配置文件都一起“打包”。
然后,我们在任何一个计算机的节点上,都可以使用这个打好的包。有了容器,一个命令就能把你想跑的程序跑起来,做到了**一次打包,就可以到处使用。**
今天是咱们整个课程的第一讲,我想和你来聊聊容器背后的实现机制。
当然,空讲原理也没什么感觉,所以我还是会先带着你启动一个容器玩玩,然后咱们再一起来探讨容器里面的两大关键技术—— Namespace和Cgroups。基本上理解了这两个概念你就能彻底搞懂容器的核心原理了。
## 做个镜像
话不多说咱们就先动手玩一玩。启动容器的工具有很多在这里我们还是使用Docker这个最常用的容器管理工具。
如果你之前根本没用过Docker的话那我建议你先去[官网](https://docs.docker.com/)看看文档,一些基础的介绍我就不讲了,那些内容你随便在网上一搜就能找到。
安装完Docker之后咱们先来用下面的命令运行一个httpd服务。
```
# docker run -d centos/httpd:latest
```
这命令也很简单run的意思就是要启动一个容器 `-d` 参数里d是Daemon的首字母也就是让容器在后台运行。
最后一个参数 `centos/httpd:latest` 指定了具体要启动哪一个镜像比如这里咱们启动的是centos/httpd这个镜像的latest版本。
镜像是Docker公司的创举也是一个伟大的发明。你想想在没有容器之前你想安装httpd的话会怎么做是不是得运行一连串的命令甚至不同的系统上操作方法也不一样
但你看,有了容器之后,你只要运行一条命令就搞定了。其实所有的玄机都在这个镜像里面。
镜像这么神奇,那它到底是怎么一回事呢?其实,镜像就是一个特殊的文件系统,
**它提供了容器中程序执行需要的所有文件。**具体来说,就是应用程序想启动,需要三类文件:相关的程序可执行文件、库文件和配置文件,这三类文件都被容器打包做好了。
这样,在容器运行的时候就不再依赖宿主机上的文件操作系统类型和配置了,做到了想在哪个节点上运行,就可以在哪个节点上立刻运行。
那么我们怎么来做一个容器镜像呢?
刚才的例子里,我们用的 `centos/httpd:latest` 这个镜像是 **Docker镜像库**里直接提供的。当然我们也可以自己做一个提供httpd服务的容器镜像这里仍然可以用Docker这个工具来自定义镜像。
Docker为用户自己定义镜像提供了一个叫做Dockerfile的文件在这个Dockerfile文件里你可以设定自己镜像的创建步骤。
如果我们自己来做一个httpd的镜像也不难举个例子我们可以一起来写一个Dockerfile体会一下整个过程。用Dockerfile build image的 Dockerfile 和对应的目录我放在[这里](http://github.com/chengyli/training/tree/main/image/demo)了。
操作之前我们首先要理解这个Dockerfile做了什么其实它很简单只有下面这5行
```
# cat Dockerfile
FROM centos:8.1.1911
RUN yum install -y httpd
COPY file1 /var/www/html/
ADD file2.tar.gz /var/www/html/
CMD ["/sbin/httpd", "-D", "FOREGROUND"]
```
我们看下它做了哪几件事在一个centos的基准镜像上安装好httpd的包然后在httpd提供文件服务的配置目录下把需要对外提供的文件file1和file2拷贝过去最后指定容器启动以后需要自动启动的httpd服务。
有了这个镜像我们希望容器启动后就运行这个httpd服务让用户可以下载file1还有file2这两个文件。
我们具体来看这个Dockerfile的每一行第一个大写的词都是Dockerfile专门定义的指令也就是 `FROM``RUN``COPY``ADD``CMD`这些指令都很基础所以我们不做详细解释了你可以参考Dockerfile的[官方文档](https://docs.docker.com/engine/reference/builder/)。
我们写完这个Dockerfile之后想要让它变成一个镜像还需要执行一下 `docker build` 命令。
下面这个命令中 `-f ./Dockerfile` 指定Dockerfile文件`-t registry/httpd:v1` 指定了生成出来的镜像名,它的格式是"name:tag",这个镜像名也是后面启动容器需要用到的。
```
# docker build -t registry/httpd:v1 -f ./Dockerfile .
```
`docker build` 执行成功之后,我们再运行 `docker images` 这个命令,就可以看到生成的镜像了。
```
# docker images
REPOSITORY TAG IMAGEID CREATED SIZE
registry/httpd v1 c682fc3d4b9a 4 seconds ago 277MB
```
## 启动一个容器 (Container)
做完一个镜像之后,你就可以用这个镜像来启动一个容器了,我们刚才做的镜像名字是 `registry/httpd:v1`,那么还是用 `docker run` 这个命令来启动容器。
```
# docker run -d registry/httpd:v1
```
容器启动完成后,我们可以用 `docker ps` 命令来查看这个已经启动的容器:
```
# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
c5a9ff78d9c1 registry/httpd:v1 "/sbin/httpd -D FORE…" 2 seconds ago Up 2 seconds loving_jackson
```
在前面介绍Dockerfile的时候我们说过做这个镜像是用来提供HTTP服务的也就是让用户可以下载file1、file2这两个文件。
那怎样来验证我们建起来的容器是不是正常工作的呢?可以通过这两步来验证:
- 第一步我们可以进入容器的运行空间查看httpd服务是不是启动了配置文件是不是正确的。
- 第二步对于HTTP文件服务如果我们能用 `curl` 命令下载文件就可以证明这个容器提供了我们预期的httpd服务。
我们先来做第一步验证,我们可以运行 `docker exec` 这个命令进入容器的运行空间至于什么是容器的运行空间它的标准说法是容器的命名空间Namespace这个概念我们等会儿再做介绍。
进入容器运行空间之后我们怎么确认httpd的服务进程已经在容器里启动了呢
我们运行下面这个 `docker exec` 命令,也就是执行 `docker exec c5a9ff78d9c1 ps -ef` 可以看到httpd的服务进程正在容器的空间中运行。
```
# docker exec c5a9ff78d9c1 ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 01:59 ? 00:00:00 /sbin/httpd -D FOREGROUND
apache 6 1 0 01:59 ? 00:00:00 /sbin/httpd -D FOREGROUND
apache 7 1 0 01:59 ? 00:00:00 /sbin/httpd -D FOREGROUND
apache 8 1 0 01:59 ? 00:00:00 /sbin/httpd -D FOREGROUND
apache 9 1 0 01:59 ? 00:00:00 /sbin/httpd -D FOREGROUND
```
这里我解释一下,在这个 `docker exec` 后面紧跟着的ID表示容器的ID这个ID就是我们之前运行 `docker ps` 查看过那个容器容器的ID值是 `c5a9ff78d9c1` 。在这个ID值的后面就是我们要在容器空间里运行的 `ps -ef` 命令。
接下来我们再来确认一下httpd提供文件服务的目录中file1和file2文件是否存在。
我们同样可以用 `docker exec` 来查看一下容器的文件系统中httpd提供文件服务的目录 `/var/www/html` 是否有这两个文件。
很好我们可以看到file1、file2这两个文件也都放在指定目录中了。
```
# docker exec c5a9ff78d9c1 ls /var/www/html
file1
file2
```
到这里我们完成了第一步的验证进入到容器的运行空间里验证了httpd服务已经启动配置文件也是正确的。
那下面我们要做第二步的验证,用 `curl` 命令来验证是否可以从容器的httpd服务里下载到文件。
如果要访问httpd服务我们就需要知道这个容器的IP地址。容器的网络空间也是独立的有一个它自己的IP。我们还是可以用 `docker exec` 进入到容器的网络空间查看一下这个容器的IP。
运行下面的这条 `docker exec c5a9ff78d9c1 ip addr` 命令我们可以看到容器里网络接口eth0上配置的IP是 `172.17.0.2`
这个IP目前只能在容器的宿主机上访问在别的机器上目前是不能访问的。关于容器网络的知识我们会在后面的课程里介绍。
```
# docker exec c5a9ff78d9c1 ip addr
1: lo: &lt;LOOPBACK,UP,LOWER_UP&gt; 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
168: eth0@if169: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; 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
```
好了获取了httpd服务的IP地址之后我们随便下载一个文件试试比如选file2。
我们在宿主机上运行 `curl` 就可以下载这个文件了操作如下。很好文件下载成功了这证明了我们这个提供httpd服务的容器正常运行了。
```
# curl -L -O http://172.17.0.2/file2
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 6 100 6 0 0 1500 0 --:--:-- --:--:-- --:--:-- 1500
# ls
file2
```
上面的步骤完成之后,我们的第二步验证,用 `curl` 下载httpd服务提供的文件也成功了。
好了,我们刚才自己做了容器镜像,用这个镜像启动了容器,并且用 `docker exec` 命令检查了容器运行空间里的进程、文件和网络设置。
通过这上面的这些操作练习,估计你已经初步感知到,容器的文件系统是独立的,运行的进程环境是独立的,网络的设置也是独立的。但是它们和宿主机上的文件系统,进程环境以及网络感觉都已经分开了。
我想和你说,这个感觉没错,的确是这样。我们刚才启动的容器,已经从宿主机环境里被分隔出来了,就像下面这张图里的描述一样。
<img src="https://static001.geekbang.org/resource/image/4b/3a/4b67ff2f9070afbc2d0966464a67b83a.jpeg" alt="">
从用户使用的角度来看容器和一台独立的机器或者虚拟机没有什么太大的区别但是它和虚拟机相比却没有各种复杂的硬件虚拟层没有独立的Linux内核。
容器所有的进程调度,内存访问,文件的读写都直接跑在宿主机的内核之上,这是怎么做到的呢?
## 容器是什么
要回答这个问题,你可以先记住这两个术语 **Namespace和Cgroups**。如果有人问你Linux上的容器是什么最简单直接的回答就是Namesapce和Cgroups。Namespace和Cgroups可以让程序在一个资源可控的独立隔离环境中运行这个就是容器了。
我们现在已经发现容器的进程、网络还有文件系统都是独立的。那问题来了容器的独立运行环境到底是怎么创造的呢这就要提到Namespace这个概念了。所以接下来就先从我们已经有点感觉的Namespace开始分析。
### Namespace
接着前面的例子我们正好有了一个正在运行的容器那我们就拿这个运行的容器来看看Namespace到底是什么
在前面我们运行 `docker exec c5a9ff78d9c1 ps -ef`看到了5个httpd进程而且也只有这5个进程。
```
# docker exec c5a9ff78d9c1 ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 01:59 ? 00:00:00 /sbin/httpd -D FOREGROUND
apache 6 1 0 01:59 ? 00:00:00 /sbin/httpd -D FOREGROUND
apache 7 1 0 01:59 ? 00:00:00 /sbin/httpd -D FOREGROUND
apache 8 1 0 01:59 ? 00:00:00 /sbin/httpd -D FOREGROUND
apache 9 1 0 01:59 ? 00:00:00 /sbin/httpd -D FOREGROUND
```
如果我们不用 `docker exec`,直接在宿主机上运行 ps -ef就会看到很多进程。如果我们运行一下 `grep httpd` 同样可以看到这5个httpd的进程
```
# ps -ef | grep httpd
UID PID PPID C STIME TTY TIME CMD
root 20731 20684 0 18:59 ? 00:00:01 /sbin/httpd -D FOREGROUND
48 20787 20731 0 18:59 ? 00:00:00 /sbin/httpd -D FOREGROUND
48 20788 20731 0 18:59 ? 00:00:06 /sbin/httpd -D FOREGROUND
48 20789 20731 0 18:59 ? 00:00:05 /sbin/httpd -D FOREGROUND
48 20791 20731 0 18:59 ? 00:00:05 /sbin/httpd -D FOREGROUN
```
这两组输出结果到底有什么差别呢,你可以仔细做个对比,最大的不同就是**进程的PID不一样。**那为什么PID会不同呢或者说运行 `docker exec c5a9ff78d9c1 ps -ef``ps -ef` 实质的区别在哪里呢?
如果理解了PID为何不同我们就能搞清楚Linux Namespace的概念了为了方便后文的讲解我们先用下面这张图来梳理一下我们看到的PID。
<img src="https://static001.geekbang.org/resource/image/88/7a/888c00e0e8fe40edce3f1a9f6yye717a.jpeg" alt="">
Linux在创建容器的时候就会建出一个PID NamespacePID其实就是进程的编号。这个PID Namespace就是指每建立出一个Namespace就会单独对进程进行PID编号每个Namespace的PID编号都从1开始。
同时在这个PID Namespace中也只能看到Namespace中的进程而且看不到其他Namespace里的进程。
这也就是说如果有另外一个容器那么它也有自己的一个PID Namespace而这两个PID Namespace之间是不能看到对方的进程的这里就体现出了Namespace的作用**相互隔离**。
而在宿主机上的Host PID Namespace它是其他Namespace的父亲Namespace可以看到在这台机器上的所有进程不过进程PID编号不是Container PID Namespace里的编号了而是把所有在宿主机运行的进程放在一起再进行编号。
讲了PID Namespace之后我们了解到 **Namespace其实就是一种隔离机制主要目的是隔离运行在同一个宿主机上的容器让这些容器之间不能访问彼此的资源。**
这种隔离有两个作用:**第一是可以充分地利用系统的资源,也就是说在同一台宿主机上可以运行多个用户的容器;第二是保证了安全性,因为不同用户之间不能访问对方的资源。**
除了PID Namespace还有其他常见的Namespace类型比如我们之前运行了 `docker exec c5a9ff78d9c1 ip addr` 这个命令去查看容器内部的IP地址这里其实就是在查看Network Namespace。
在Network Namespace中都有一套独立的网络接口比如这里的loeth0还有独立的TCP/IP的协议栈配置。
```
# docker exec c5a9ff78d9c1 ip addr
1: lo: &lt;LOOPBACK,UP,LOWER_UP&gt; 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
168: eth0@if169: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; 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
```
我们还可以运行 `docker exec c5a9ff78d9c1 ls/` 查看容器中的根文件系统rootfs。然后你会发现它和宿主机上的根文件系统也是不一样的。**容器中的根文件系统,其实就是我们做的镜像。**
那容器自己的根文件系统完全独立于宿主机上的根文件系统,这一点是怎么做到的呢?其实这里依靠的是**Mount Namespace**Mount Namespace保证了每个容器都有自己独立的文件目录结构。
Namespace的类型还有很多我们查看"Linux Programmer's Manual"可以看到Linux中所有的Namespacecgroup/ipc/network/mount/pid/time/user/uts。
在这里呢,你需要记住的是 **Namespace 是Linux中实现容器的两大技术之一它最重要的作用是保证资源的隔离。**在后面的课程讲解到具体问题时我会不断地提到Namespace这个概念。
<img src="https://static001.geekbang.org/resource/image/5b/d2/5bbf4ac2fa9f81066732yy6f6202b8d2.jpg" alt="">
好了我们刚才说了Namespace这些Namespace尽管类型不同其实都是为了隔离容器资源**PID Namespace负责隔离不同容器的进程Network Namespace又负责管理网络环境的隔离Mount Namespace管理文件系统的隔离。**
正是通过这些Namespace我们才隔离出一个容器这里你也可以把它看作是一台“计算机”。
既然是一台“计算机”你肯定会问这个“计算机”有多少CPU有多少Memory啊那么Linux如何为这些“计算机”来定义CPU定义Memory的容量呢
### Cgroups
想要定义“计算机”各种容量大小,就涉及到支撑容器的第二个技术**Cgroups Control Groups**了。Cgroups可以对指定的进程做各种计算机资源的限制比如限制CPU的使用率内存使用量IO设备的流量等等。
Cgroups究竟有什么好处呢要知道在Cgroups出现之前任意一个进程都可以创建出成百上千个线程可以轻易地消耗完一台计算机的所有CPU资源和内存资源。
但是有了Cgroups这个技术以后我们就可以对一个进程或者一组进程的计算机资源的消耗进行限制了。
Cgroups通过不同的子系统限制了不同的资源每个子系统限制一种资源。每个子系统限制资源的方式都是类似的就是把相关的一组进程分配到一个控制组里然后通过**树结构**进行管理,每个控制组都设有自己的资源控制参数。
完整的Cgroups子系统的介绍你可以查看[Linux Programmer's Manual](https://man7.org/linux/man-pages/man7/cgroups.7.html) 中Cgroups的定义。
这里呢我们只需要了解几种比较常用的Cgroups子系统
- CPU子系统用来限制一个控制组一组进程你可以理解为一个容器里所有的进程可使用的最大CPU。
- memory子系统用来限制一个控制组最大的内存使用量。
- pids子系统用来限制一个控制组里最多可以运行多少个进程。
- cpuset子系统 这个子系统来限制一个控制组里的进程可以在哪几个物理CPU上运行。
因为memory子系统的限制参数最简单所以下面我们就用memory子系统为例一起看看Cgroups是怎么对一个容器做资源限制的。
对于启动的每个容器都会在Cgroups子系统下建立一个目录在Cgroups中这个目录也被称作控制组比如下图里的"docker-&lt;id1&gt;""docker-&lt;id2&gt;"等。然后我们设置这个控制组的参数,通过这个方式,来限制这个容器的内存资源。
<img src="https://static001.geekbang.org/resource/image/61/63/6193bba2757e5cc34bb023b13cac7663.jpeg" alt="">
还记得我们之前用Docker创建的那个容器吗在每个Cgroups子系统下对应这个容器就会有一个目录docker-**c5a9ff78d9c1……**这个容器的ID号容器中所有的进程都会储存在这个控制组中 cgroup.procs 这个参数里。
你看下面的这些进程号是不是很熟悉呢没错它们就是前面我们用ps看到的进程号。
我们实际看一下这个例子里的memory Cgroups它可以控制Memory的使用量。比如说我们将这个控制组Memory的最大用量设置为2GB。
具体操作是这样的我们把2* 1024 * 1024 * 1024 = 2147483648这个值写入memory Cgroup控制组中的memory.limit_in_bytes里**这样设置后cgroup.procs里面所有进程Memory使用量之和最大也不会超过2GB。**
```
# cd /sys/fs/cgroup/memory/system.slice/docker-c5a9ff78d9c1fedd52511e18fdbd26357250719fa0d128349547a50fad7c5de9.scope
# cat cgroup.procs
20731
20787
20788
20789
20791
# echo 2147483648 &gt; memory.limit_in_bytes
# cat memory.limit_in_bytes
2147483648
```
刚刚我们通过memory Cgroups定义了容器的memory可以使用的最大值。其他的子系统稍微复杂一些但用法也和memory类似我们在后面的课程中会结合具体的实例来详细解释其他的Cgroups。
这里我们还要提一下 **Cgroups有v1和v2两个版本**
Cgroups v1在Linux中很早就实现了各种子系统比较独立每个进程在各个Cgroups子系统中独立配置可以属于不同的group。
虽然这样比较灵活,但是也存在问题,会导致对**同一进程的资源协调比较困难**比如memory Cgroup与blkio Cgroup之间就不能协作。虽然v1有缺陷但是在主流的生产环境中大部分使用的还是v1。
Cgroups v2 做了设计改进,**解决了v1的问题使各个子系统可以协调统一地管理资源。**
不过Cgroups v2在生产环境的应用还很少因为该版本很多子系统的实现需要较新版本的Linux内核还有无论是主流的Linux发行版本还是容器云平台比如Kubernetes对v2的支持也刚刚起步。
所以啊我们在后面Cgroups的讲解里呢主要还是用 **Cgroups v1这个版本**在磁盘I/O的这一章中我们也会介绍一下Cgroups v2。
好了上面我们解读了Namespace和Cgroups两大技术它们是Linux下实现容器的两个基石后面课程中要讨论的容器相关问题或多或少都和Namespace或者Cgroups相关我们会结合具体问题做深入的分析。
目前呢,你只需要先记住这两个技术的作用,**Namespace帮助容器来实现各种计算资源的隔离Cgroups主要限制的是容器能够使用的某种资源量。**
## 重点总结
这一讲,我们对容器有了一个大致的认识,包括它的“形”,**一些基本的容器操作**;还有它的“神”,也就是**容器实现的原理**。
启动容器的基本操作是这样的首先用Dockerfile来建立一个容器的镜像然后再用这个镜像来启动一个容器。
那启动了容器之后,怎么检验它是不是正常工作了呢?
我们可以运行 `docker exec` 这个命令进入容器的运行空间,查看进程是否启动,检查配置文件是否正确,检验我们设置的服务是否能够正常提供。
我们用 `docker exec` 命令查看了容器的进程,网络和文件系统,就能体会到容器的文件系统、运行的进程环境和网络的设置都是独立的,所以从用户使用的角度看,容器和一台独立的机器或者虚拟机没有什么太大的区别。
最后我们一起学习了Namespace和Cgroups它们是Linux的两大技术用于实现容器的特性。
具体来说,**Namespace帮助容器实现各种计算资源的隔离Cgroups主要对容器使用某种资源量的多少做一个限制。**
所以我们在这里可以直接记住:**容器其实就是Namespace+Cgroups。**
## 思考题
用Dockerfile为你最熟悉的应用程序做个镜像然后用Docker命令启动这个容器。
欢迎在留言区分享你的疑惑和见解。你也可以把今天的内容分享给你的朋友,和他一起学习和进步。

View File

@@ -0,0 +1,127 @@
<audio id="audio" title="开篇词 | 一个态度两个步骤,成为容器实战高手" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/64/45/64247149e756b7836570yyc5a2acbc45.mp3"></audio>
你好,我是李程远,欢迎你加入我的极客时间专栏。从今天开始,我想和你聊一聊,怎么解决容器里的各种问题。
先来介绍一下我自己吧。我毕业于浙江大学计算机系第一份工作是开发基于Xen的Linux虚拟机接下来的十几年我的工作始终围绕着Linux系统。
在2013年我加入eBay从事云平台方面的工作最先接触的是OpenStack云平台。
一直到了2015年的时候我们的团队开始做Kubernetes要用Kubernetes来管理eBay整个云平台。我们需要迁移所有eBay的应用程序把它们从原来的物理机或者虚拟机迁移到容器的环境里。
在Kubernetes具体落地的过程中我们碰到了形形色色的容器问题。
首先,我们都知道,容器是一种轻量级的隔离技术。而轻量级隔离造成了一些**行为模式**的不同比如原来运行在虚拟机里的CPU监控程序移到容器之后再用原来的算法计算容器CPU使用率就不适用了。
然后呢,从**隔离程度**这个方面考虑CPU、memory、IO disk and network真的能做到精确隔离吗
其实还是有问题的,比如想让多个用户容器运行在一个节点上,我们就需要保证,每个容器的磁盘容量在一定的限额范围内,还需要合理分配磁盘读写性能。
第三个方面,就是**处理性能敏感的应用。**容器技术的引入,会带来新的开销,那么肯定会影响性能。
比如说原来运行在物理机上、有极高性能要求的程序在迁移到容器后我们还需要对容器网络做优化对Cgroup做优化。只有做了这样的优化我们才能保证迁移过来的程序当它们运行在容器里的时候性能差异控制在2%以内(当时做迁移的标准)。
另外如果涉及高内存使用的应用我们做迁移的时候还要考虑PageCache、Swap还有HugePage等等问题在叠加了Cgroup之后会带来新的变化。
综合来看,我们遇到的问题有的很简单,看一下源代码,写个测试代码验证一下,一两个小时就可以搞定。但有的问题却很复杂,我们需要尝试不同的测试,反复查看各种源代码,陆陆续续花费一两个月的时间解决。
通过5年的不断努力我和我的团队逐渐把eBay所有的业务都迁移到了容器中。现在我们的云平台上运行着百万个容器。
## 怎么理解容器的知识体系?
可以说,从我接触容器知识到能够得心应手地解决各种容器问题,这个过程还真是有点磕磕绊绊。
一开始,我被各种各样的问题所淹没,觉得容器的内容太复杂了,没有一个系统性的解决方法。我只能是见招拆招,一个个解决,就这样,随着我解决的问题越来越多,我也开始思考,是不是有一些规律性的东西。
容器问题虽然有很多类型,既有基本功能问题,也有性能问题,还有不少稳定性问题。但大部分问题,**最终都会归结到Linux操作系统上。**
比如容器里进程被OOM Kill了这个OOM Killer就是Linux里常见的内存保护机制容器的进程引起平均负载增高而平均负载也是在Linux里被反复讨论的概念还有容器使用的OverlayFS系统看上去和Linux常用的XFS、Ext4系统不同但是它也是Linux内核维护的一种文件系统。
我们都知道Linux操作系统不外乎是**进程管理、内存管理、文件系统、网络协议栈,再加上一些安全管理。**这样一梳理容器的问题就都可以投射到Linux操作系统这些模块上了是不是一下子感觉清晰了很多
当然了容器还有自己的特殊性Linux内核原来的特性加上Namespace和Cgroups会带来的变化。
所以我们在对应到每个模块上分析问题的时候还需要考虑到Namespace和Cgroups。这两个概念是容器技术的基石我们课程中要讨论的容器相关问题多少都会和Namespace或者Cgroups相关。
总之就是一句话,**我们可以结合Linux操作系统的主要模块把容器的知识结构系统地串联起来同时看到Namespace和Cgroups带来的特殊性。 **
<img src="https://static001.geekbang.org/resource/image/e0/21/e033032867a9cff7d399871c604ae921.jpeg" alt="">
## 怎么解决容器问题?
心中有了容器的知识体系,我们也就能在容器实践中解决具体的问题了。结合我自己这么多年的经历,我总结了一条经验,**解决容器问题需要一个态度+两个步骤。**
在解决容器问题的过程中,我们常见的误区就是浅尝辄止,不去挖掘问题的根本原因。我之前也碰到过这种情况,接下来我就拿一个具体的例子来说明。
有一次团队一位同学问我怎么让Kubernetes节点上的容器从内部触发自己的容器重启啊
我试了一下在容器中把第1号进程杀了然后容器退出Kubernetes自动地把容器带回来就能实现类似的自动重启功能了同事试了也可以认为问题解决了也挺开心的。我也没有多想以为自己找到方法了。
后来又有一个同事和我说,这样做没有效果啊。我这才发现问题没那么简单,是我想当然了。
所以我又花时间理了理Linux信号的基本知识trace了一下内核代码终于让我找到了真正的原因那就是对于发送给1号进程的信号内核会根据不同的类型、不同的注册状态采取不同的处理方式。
你看这是一个挺简单的问题就是kill一下容器里的1号进程。你或许也遇到过如果你也和我开始时的态度一样就很可能会错过找到真正答案的机会。这就是我说的解决容器问题时我们需要的一个态度不要浅尝辄止要刨根问底。
态度有了,那如果我们在线上碰到了更加复杂的问题,又该怎么解决呢?这就需要两个步骤了。
我们的第一步,就是**化繁为简,重现问题。**
想要做到这一点,倒推回去,还是需要我们对基本的概念足够了解。只有对每个模块的概念都很清晰,我们才能对复杂问题做拆分。
能够对问题做拆分是不是就够了呢?其实还不够,我自己有一个判断标准,就是还要能够写模拟程序,看是否可以用最简单的程序来重现问题。**如果我们能用简单的代码程序重现问题,那么问题也就解决了一半。**
接下来我们还需要进行第二步,就是想办法**把黑盒系统变成白盒系统。**
我在前面提到过容器的问题大多都会归结到Linux系统上。Linux系统从内核、库函数以及服务程序上看虽然都是开源的但是它运行在生产环境的时候几乎就是一个黑盒。
之所以说系统是黑盒一方面是因为这个系统太庞大太复杂了另一方面在实际运行的时候只有很少的log会记录运行的过程和参数。所以在出问题的时候我们无法知道问题对应的代码我们也不可能在生产环境中随心所欲地加debug log。
因此,我们就需要想点办法把它变成白盒,才能去排查和解决问题。具体怎么做呢?这里需要我们熟练地掌握调试工具,这样才能把某些函数变成“白盒”,从而找到复杂问题的根本原因,再对症下药。
这里我想提醒你的是我们熟练掌握工具有个重要前提就是从全局上去掌握Linux系统以及容器回归到底层原理去看问题。可以说你把基础概念吃透了练好了“内功心法”有了这个底子工具运用是水到渠成的事儿。
## 我是怎么设计这门课的?
讲到这里,估计你会有个问题,这“一个态度两个步骤”很好理解啊,我也了解到了,但是怎么才能真正地掌握这些知识、拥有解决问题的思路呢?
这就是我们这门课想要实现的目标了,那就是带你走进一个个具体的案例中,体验解决问题的全过程,在实战中习得知识和技能。
所以,在这门课程里,我会把零散的知识点体系化,按照类似操作系统的模块划分,为你讲述我所理解的容器。
我们将一起学习容器进程、容器内存、容器存储、 容器网络、容器安全这几部分内容。在每一节课中,我们都会解决一个实际问题或者研究一个现象。围绕这个问题,我会为你讲解相关的知识点,并带着你结合实际的操作做理解,最终解决问题或者解释现象。
我们要实现两个学习目标。
**第一系统掌握容器核心点Namespace和Cgroups。**
**第二理解Namespace和Cgroups对Linux原来模块的影响看看它们是如何影响一些传统操作系统的行为。**
比如Memory Cgroup对Pagecache和Swap空间有怎样的影响再比如在proc文件系统下我们的网络参数应用了Network Namespace之后需要如何重新设置等等。
当我们一起把容器知识的框架搭建起来,把里面的核心概念、底层逻辑掌握之后,你其实就可以解决容器的大部分问题了。但是,我知道,你一定还有个问题,那就是工具呢?不讲了吗?我真的可以水到渠成吗?
不要着急,这里我要做个特别说明,课程结束后,我会给你做一个专题加餐。目前,我是这么设计的,我选择了一个真实案例,就是在生产环境中容器网络延时不稳定的问题。
在这个案例中,我们会用到**perfftracebcc/ebpf这几个Linux调试工具**,了解它们的原理,熟悉它们在调试问题的不同阶段所发挥的作用,然后用这些工具一起来解决现实场景中复杂的容器问题。
为什么一定要把这个专题放到课程结束后呢?因为我需要给你留一段消化吸收的时间,这里我安排了一个月时间。
希望你能利用这一个月,把整个课程的内容复习一遍,把基本功打扎实,你才能在专题学习里彻底掌握这几个工具,遇到类似问题时也能有清晰的解决思路,这样这个专题的学习效率也才能更高。
之所以一定要这么安排,也是想跟你表达我的一个观点,就是工具很重要,但是工具不是最重要的。
所有学习,我们一定是先掌握知识体系,一定不能陷入唯工具论的思维框架里。我知道,这样的安排似乎只是我的一家之言,但这恰恰就是我想通过这门课交付给你的,因为这些真的是我自己的经验之谈,是我的受益点。这么学看似慢了,但其实只有这样,我们走的才是捷径。
<img src="https://static001.geekbang.org/resource/image/36/2a/36ee722764e0c3a7e1aba7999b26b52a.jpg" alt="">
好了,介绍完了课程设计和学习目标,还有一件事特别重要,我要特别提醒下。
在这个容器课程中,每一讲里都会有一些小例子,所以需要**你有一台安装有Linux的机器或者用VirtualBox安装一个虚拟机来跑Linux。Linux的版本建议是CentOS 8 或者是Ubuntu 20.04。**
希望你提前做好准备,这样在学习的过程中,你就能跟着我的讲解进行一些实际的操作,对容器知识也会有更加深刻的印象。
你还可以拉上身边的小伙伴,组团来学习这门课程,共同学习、互相鼓励的氛围会让你的学习体验更好。另外,有什么想法或者疑问,你都可以通过留言区和我交流、互动。
最后,我想和你说,**容器是一个很好的技术窗口,它可以帮助你在这个瞬息万变的计算机世界里看到后面那些“不变”的技术,只有掌握好那些“不变”的技术,你才可以更加从容地去接受技术的瞬息万变。**
我希望,这个专栏能帮你打开容器这扇窗,让你看到更精彩的风景,建立起你自己的容器知识体系。从今天开始,跟着我一起搞懂容器,提升实力,吃透原理,在技术之路上一起前进吧!