This commit is contained in:
louzefeng
2024-07-11 05:50:32 +00:00
parent bf99793fd0
commit d3828a7aee
6071 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,344 @@
<audio id="audio" title="56 | 容器:大公司为保持创新,鼓励内部创业" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/d1/a4/d1d6bd823b83801b5dfcb824bfd0bba4.mp3"></audio>
上一章我们讲了虚拟化的原理。从一台物理机虚拟化出很多的虚拟机这种方式一定程度上实现了资源创建的灵活性。但是你同时会发现虚拟化的方式还是非常复杂的。这有点儿像你去成立子公司虽然说公司小但毕竟是一些独立的公司麻雀虽小五脏俱全因而就像上一章我们看到的那样CPU、内存、网络、硬盘全部需要虚拟化一个都不能偷懒。
那有没有一种更加灵活的方式既可以隔离出一部分资源专门用于某个进程又不需要费劲周折的虚拟化这么多的硬件呢毕竟最终我只想跑一个程序而不是要一整个Linux系统。这就像在一家大公司搞创新如果每一个创新项目都要成立一家子公司的话那简直太麻烦了。一般方式是在公司内部成立一个独立的组织分配独立的资源和人力先做一段时间的内部创业。如果真的做成功了再成立子公司也不迟。
在Linux操作系统中有一项新的技术称为容器它就可以做到这一点。
容器的英文叫ContainerContainer的另一个意思是“集装箱”。其实容器就像船上的不同的集装箱装着不同的货物有一定的隔离但是隔离性又没有那么好仅仅做简单的封装。当然封装也带来了好处一个是打包二是标准。
在没有集装箱的时代假设我们要将货物从A运到B中间要经过三个码头、换三次船。那么每次都要将货物卸下船来弄得乱七八糟然后还要再搬上船重新摆好。因此在没有集装箱的时候每次换船船员们都要在岸上待几天才能干完活。
有了尺寸全部都一样的集装箱以后,我们可以把所有的货物都打包在一起。每次换船的时候,把整个集装箱搬过去就行了,几个小时就能完成。船员换船时间大大缩短了。这是集装箱的“打包”和“标准”两大特点在生活中的应用。
其实容器的思想就是要变成软件交付的集装箱。那么容器如何对应用打包呢?
我们先来学习一下集装箱的打包过程。首先,我们得有个封闭的环境,将货物封装起来,让货物之间互不干扰,互相隔离,这样装货卸货才方便。
容器实现封闭的环境主要要靠两种技术,一种是看起来是隔离的技术,称为**namespace**命名空间。在每个namespace中的应用看到的都是不同的 IP地址、用户空间、进程ID等。另一种是用起来是隔离的技术称为**cgroup**(网络资源限制),即明明整台机器有很多的 CPU、内存但是一个应用只能用其中的一部分。
有了这两项技术,就相当于我们焊好了集装箱。接下来的问题就是,如何“将这些集装箱标准化”,在哪艘船上都能运输。这里就要用到镜像了。
所谓**镜像**Image就是在你焊好集装箱的那一刻将集装箱的状态保存下来。就像孙悟空说“定集装箱里的状态就被“定”在了那一刻然后这一刻的状态会被保存成一系列文件。无论在哪里运行这个镜像都能完整地还原当时的情况。
当程序员根据产品设计开发完毕之后,可以将代码连同运行环境打包成一个容器镜像。这个时候集装箱就焊好了。接下来,无论是在开发环境、测试环境,还是生产环境运行代码,都可以使用相同的镜像。就好像集装箱在开发、测试、生产这三个码头非常顺利地整体迁移,这样产品的发布和上线速度就加快了。
下面我们就来体验一下这个Linux上的容器技术
首先我们要安装一个目前最主流的容器技术的实现Docker。假设我们的操作系统是CentOS你可以参考[https://docs.docker.com/install/linux/docker-ce/centos/](https://docs.docker.com/install/linux/docker-ce/centos/)这个官方文档,进行安装。
第一步删除原有版本的Docker。
```
yum remove docker \
docker-client \
docker-client-latest \
docker-common \
docker-latest \
docker-latest-logrotate \
docker-logrotate \
docker-engine
```
第二步,安装依赖的包。
```
yum install -y yum-utils \
device-mapper-persistent-data \
lvm2
```
第三步安装Docker所属的库。
```
yum-config-manager \
--add-repo \
https://download.docker.com/linux/centos/docker-ce.repo
```
第四步安装Docker。
```
yum install docker-ce docker-ce-cli containerd.io
```
第五步启动Docker。
```
systemctl start docker
```
Docker安装好之后接下来我们就来运行一个容器。
就像上面我们讲过的,容器的运行需要一个镜像,这是我们集装箱封装的那个环境,在[https://hub.docker.com/](https://hub.docker.com/)上,你能找到你能想到的几乎所有环境。
最基础的环境就是操作系统。
咱们最初讲命令行的时候讲过每种操作系统的命令行不太一样就像黑话一样。有时候我们写一个脚本需要基于某种类型的操作系统例如Ubuntu或者centOS。但是Ubuntu或者centOS不同版本的命令也不一样需要有一个环境尝试一下命令是否正确。
最常见的做法是有几种类型的操作系统,就弄几台物理机。当然这样一般人可玩不起,但是有了虚拟机就好一些了。你可以在你的笔记本电脑上创建多台虚拟机,但是这个时候又会有另一个苦恼,那就是,虚拟机往往需要比较大的内存,一般一台笔记本电脑上无法启动多台虚拟机,所以做起实验来要经常切换虚拟机,非常麻烦。现在有了容器,好了,我们可以在一台虚拟机上创建任意的操作系统环境了。
比方说,你可以在[https://hub.docker.com/](https://hub.docker.com/)上搜索Ubuntu。点开之后找到Tags。镜像都有Tag这是镜像制作者自己任意指定的多用于表示这个镜像的版本号。
<img src="https://static001.geekbang.org/resource/image/16/fb/160b839adb2bd7390c16c4591204befb.png" alt="">
如果仔细看这些Tags我们会发现哪怕非常老版本的Ubuntu这里面都有例如14.04。如果我们突然需要一个基于Ubuntu 14.04的命令,那就不需要费劲去寻找、安装一个这么老的虚拟机,只要根据命令下载这个镜像就可以了。
```
# docker pull ubuntu:14.04
14.04: Pulling from library/ubuntu
a7344f52cb74: Pull complete
515c9bb51536: Pull complete
e1eabe0537eb: Pull complete
4701f1215c13: Pull complete
Digest: sha256:2f7c79927b346e436cc14c92bd4e5bd778c3bd7037f35bc639ac1589a7acfa90
Status: Downloaded newer image for ubuntu:14.04
```
下载完毕之后,我们可以通过下面的命令查看镜像。
```
# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
ubuntu 14.04 2c5e00d77a67 2 months ago 188MB
```
有了镜像,我们就可以通过下面的启动一个容器啦。
启动一个容器需要一个叫entrypoint的东西也就是入口。一个容器启动起来之后会从这个指令开始运行并且只有这个指令在运行容器才启动着。如果这个指令退出整个容器就退出了。
因为我们想尝试命令所以这里entrypoint要设置为bash。通过cat /etc/lsb-release我们可以看出这里面已经是一个老的Ubuntu 14.04的环境。
```
# docker run -it --entrypoint bash ubuntu:14.04
root@0e35f3f1fbc5:/# cat /etc/lsb-release
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=14.04
DISTRIB_CODENAME=trusty
DISTRIB_DESCRIPTION=&quot;Ubuntu 14.04.6 LTS&quot;
```
如果我们想尝试centOS 6也是没问题的。
```
# docker pull centos:6
6: Pulling from library/centos
ff50d722b382: Pull complete
Digest: sha256:dec8f471302de43f4cfcf82f56d99a5227b5ea1aa6d02fa56344986e1f4610e7
Status: Downloaded newer image for centos:6
# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
ubuntu 14.04 2c5e00d77a67 2 months ago 188MB
centos 6 d0957ffdf8a2 4 months ago 194MB
# docker run -it --entrypoint bash centos:6
[root@af4c8d598bdf /]# cat /etc/redhat-release
CentOS release 6.10 (Final)
```
除了可以如此简单地创建一个操作系统环境,容器还有一个很酷的功能,就是镜像里面带应用。这样的话,应用就可以像集装箱一样,到处迁移,启动即可提供服务。而不用像虚拟机那样,要先有一个操作系统的环境,然后再在里面安装应用。
我们举一个最简单的应用的例子也就是nginx。我们可以下载一个nginx的镜像运行起来里面就自带nginx了并且直接可以访问了。
```
# docker pull nginx
Using default tag: latest
latest: Pulling from library/nginx
fc7181108d40: Pull complete
d2e987ca2267: Pull complete
0b760b431b11: Pull complete
Digest: sha256:48cbeee0cb0a3b5e885e36222f969e0a2f41819a68e07aeb6631ca7cb356fed1
Status: Downloaded newer image for nginx:latest
# docker run -d -p 8080:80 nginx
73ff0c8bea6e169d1801afe807e909d4c84793962cba18dd022bfad9545ad488
# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
73ff0c8bea6e nginx &quot;nginx -g 'daemon of…&quot; 2 minutes ago Up 2 minutes 0.0.0.0:8080-&gt;80/tcp modest_payne
# curl http://localhost:8080
&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;head&gt;
&lt;title&gt;Welcome to nginx!&lt;/title&gt;
```
这次nginx镜像运行的方式和操作系统不太一样一个是-d因为它是一个应用不需要像操作系统那样有交互命令行而是以后台方式运行-d就是daemon的意思。
另外一个就是端口-p 8080:80。容器这么容易启动每台机器上可以启动N个nginx。大家都监听80端口不就冲突了吗所以我们要设置端口冒号后面的80是容器内部环境监听的端口冒号前面的8080是宿主机上监听的端口。
一旦容器启动起来之后通过docker ps就可以查看都有哪些容器正在运行。
接下来我们通过curl命令访问本机的8080端口可以打印出nginx的欢迎页面。
docker run一下应用就启动起来了是不是非常方便nginx是已经有人打包好的容器镜像放在公共的镜像仓库里面。如果是你自己开发的应用应该如何打包成为镜像呢
因为Java代码比较麻烦我们这里举一个简单的例子。假设你自己写的HTML的文件就是代码。
```
&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;head&gt;
&lt;title&gt;Welcome to nginx Test 7!&lt;/title&gt;
&lt;style&gt;
body {
width: 35em;
margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif;
}
&lt;/style&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;h1&gt;Test 7&lt;/h1&gt;
&lt;p&gt;If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.&lt;/p&gt;
&lt;p&gt;For online documentation and support please refer to
&lt;a href=&quot;http://nginx.org/&quot;&gt;nginx.org&lt;/a&gt;.&lt;br/&gt;
Commercial support is available at
&lt;a href=&quot;http://nginx.com/&quot;&gt;nginx.com&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Thank you for using nginx.&lt;/em&gt;&lt;/p&gt;
&lt;/body&gt;
&lt;/html&gt;
```
那我们如何将这些代码放到容器镜像里面呢要通过DockerfileDockerfile的格式应该包含下面的部分
- FROM 基础镜像
- RUN 运行过的所有命令
- COPY 拷贝到容器中的资源
- ENTRYPOINT 前台启动的命令或者脚本
按照上面说的格式可以有下面的Dockerfile。
```
FROM ubuntu:14.04
RUN echo &quot;deb http://archive.ubuntu.com/ubuntu trusty main restricted universe multiverse&quot; &gt; /etc/apt/sources.list
RUN echo &quot;deb http://archive.ubuntu.com/ubuntu trusty-updates main restricted universe multiverse&quot; &gt;&gt; /etc/apt/sources.list
RUN apt-get -y update
RUN apt-get -y install nginx
COPY test.html /usr/share/nginx/html/test.html
ENTRYPOINT nginx -g &quot;daemon off;&quot;
```
将代码、Dockerfile、脚本放在一个文件夹下以上面的Dockerfile为例子。
```
[nginx]# ls
Dockerfile test.html
```
现在我们编译这个Dockerfile。
```
docker build -f Dockerfile -t testnginx:1 .
```
编译过后,我们就有了一个新的镜像。
```
# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
testnginx 1 3b0e5da1a384 11 seconds ago 221MB
nginx latest f68d6e55e065 13 days ago 109MB
ubuntu 14.04 2c5e00d77a67 2 months ago 188MB
centos 6 d0957ffdf8a2 4 months ago 194MB
```
接下来,我们就可以运行这个新的镜像。
```
# docker run -d -p 8081:80 testnginx:1
f604f0e34bc263bc32ba683d97a1db2a65de42ab052da16df3c7811ad07f0dc3
# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
f604f0e34bc2 testnginx:1 &quot;/bin/sh -c 'nginx -…&quot; 2 seconds ago Up 2 seconds 0.0.0.0:8081-&gt;80/tcp youthful_torvalds
73ff0c8bea6e nginx &quot;nginx -g 'daemon of…&quot; 33 minutes ago Up 33 minutes 0.0.0.0:8080-&gt;80/tcp modest_payne
```
我们再来访问我们在nginx里面写的代码。
```
[root@deployer nginx]# curl http://localhost:8081/test.html
&lt;!DOCTYPE html&gt;
&lt;html&gt;
&lt;head&gt;
&lt;title&gt;Welcome to nginx Test 7!&lt;/title&gt;
```
看,我们的代码已经运行起来了。是不是很酷?
其实这种运行方式有更加酷的功能。
第一就是持续集成。
想象一下你写了一个程序然后把它打成了上面一样的镜像。你在本地一运行docker run就把他运行起来了。接下来你交给测试的就不是一个“程序包+配置+手册”了而是一个容器镜像了。测试小伙伴同样通过docker run也就运行起来了不存在“你这里跑的起来他那里跑不起来的情况”。测试完了再上生产交给运维的小伙伴也是这样一个镜像同样的运行同样的顺畅。这种模式使得软件的交付效率大大提高可以一天发布多次。
第二就是弹性伸缩。
想象一下你写了一个程序平时用的人少只需要10个副本就能够扛住了。突然有一天要做促销需要100个副本另外90台机器创建出来相对比较容易用任何一个云都可以做到但是里面90个副本的应用如何部署呢一个个上去手动部署吗有了容器就方便多了只要在每台机器上docker run一下就搞定了。
第三就是跨云迁移。
如果你不相信任何一个云怕被一个云绑定怕一个云挂了自己的应用也就挂了。那我们想一想该怎么办呢你只能手动在一个云上部署一份在另外一个云上也部署一份。有了容器了之后由于容器镜像对于云是中立的你在这个云上docker run就在这个云上提供服务等哪天想用另一个云了不用怕应用迁移不走只要在另外一个云上docker run一下就解决了。
到现在,是不是能够感受到容器的集装箱功能了,这就是看起来隔离的作用。
你可能会问多个容器运行在一台机器上不会相互影响吗如何限制CPU和内存的使用呢
Docker本身提供了这样的功能。Docker可以限制对于CPU的使用我们可以分几种的方式。
- Docker允许用户为每个容器设置一个数字代表容器的 CPU share默认情况下每个容器的 share 是 1024。这个数值是相对的本身并不能代表任何确定的意义。当主机上有多个容器运行时每个容器占用的 CPU 时间比例为它的 share 在总额中的比例。Docker为容器设置CPU share 的参数是 -c --cpu-shares。
- Docker提供了 --cpus 参数可以限定容器能使用的 CPU 核数。
- Docker可以通过 --cpuset 参数让容器只运行在某些核上
Docker会限制容器内存使用量下面是一些具体的参数。
- -m --memory容器能使用的最大内存大小。
- memory-swap容器能够使用的 swap 大小。
- memory-swappiness默认情况下主机可以把容器使用的匿名页swap出来你可以设置一个 0-100 之间的值,代表允许 swap 出来的比例。
- memory-reservation设置一个内存使用的 soft limit如果 docker 发现主机内存不足,会执行 OOM (Out of Memory)操作。这个值必须小于 --memory 设置的值。
- kernel-memory容器能够使用的 kernel memory 大小。
- oom-kill-disable是否运行 OOM (Out of Memory)的时候杀死容器。只有设置了 -m才可以把这个选项设置为 false否则容器会耗尽主机内存而且导致主机应用被杀死。
这就是用起来隔离的效果。
那这些看起来隔离和用起来隔离的技术,到内核里面是如何实现的呢?我们下一节仔细分析。
## 总结时刻
这里我们来总结一下这一节的内容。无论是容器还是虚拟机都依赖于内核中的技术虚拟机依赖的是KVM容器依赖的是namespace和cgroup对进程进行隔离。
为了运行Docker有一个daemon进程Docker Daemon用于接收命令行。
为了描述Docker里面运行的环境和应用有一个Dockerfile通过build命令称为容器镜像。容器镜像可以上传到镜像仓库也可以通过pull命令从镜像仓库中下载现成的容器镜像。
通过Docker run命令将容器镜像运行为容器通过namespace和cgroup进行隔离容器里面不包含内核是共享宿主机的内核的。对比虚拟机虚拟机在qemu进程里面是有客户机内核的应用运行在客户机的用户态。
<img src="https://static001.geekbang.org/resource/image/5a/c5/5a499cb50a1b214a39ddf19cbb63dcc5.jpg" alt="">
## 课堂练习
请你试着用Tomcat的容器镜像启动一个Java网站程序并进行访问。
欢迎留言和我分享你的疑惑和见解,也欢迎收藏本节内容,反复研读。你也可以把今天的内容分享给你的朋友,和他一起学习和进步。

View File

@@ -0,0 +1,579 @@
<audio id="audio" title="57 | Namespace技术内部创业公司应该独立运营" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/21/b2/213859bc0db6062bd7a77363d24dd4b2.mp3"></audio>
上一节我们讲了Docker的基本原理今天我们来看一下“看起来隔离的”技术namespace在内核里面是如何工作的。
既然容器是一种类似公司内部创业的技术,我们可以设想一下,如果一个创新项目要独立运营,应该成立哪些看起来独立的组织和部门呢?
首先是**用户管理**,咱们这个小分队应该有自己独立的用户和组管理体系,公司里面并不是任何人都知道我们在做什么。
其次是**项目管理**,咱们应该有自己独立的项目管理体系,不能按照大公司的来。
然后是**档案管理**,咱们这个创新项目的资料一定要保密,要不然创意让人家偷走了可不好。
最后就是**合作部**,咱们这个小分队还是要和公司其他部门或者其他公司合作的,所以需要一个外向的人来干这件事情。
对应到容器技术为了隔离不同类型的资源Linux内核里面实现了以下几种不同类型的namespace。
- UTS对应的宏为CLONE_NEWUTS表示不同的namespace可以配置不同的hostname。
- User对应的宏为CLONE_NEWUSER表示不同的namespace可以配置不同的用户和组。
- Mount对应的宏为CLONE_NEWNS表示不同的namespace的文件系统挂载点是隔离的
- PID对应的宏为CLONE_NEWPID表示不同的namespace有完全独立的pid也即一个namespace的进程和另一个namespace的进程pid可以是一样的但是代表不同的进程。
- Network对应的宏为CLONE_NEWNET表示不同的namespace有独立的网络协议栈。
还记得咱们启动的那个容器吗?
```
# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
f604f0e34bc2 testnginx:1 &quot;/bin/sh -c 'nginx -…&quot; 17 hours ago Up 17 hours 0.0.0.0:8081-&gt;80/tcp youthful_torvalds
```
我们可以看这个容器对应的entrypoint的pid。通过docker inspect命令可以看到进程号为58212。
```
[root@deployer ~]# docker inspect f604f0e34bc2
[
{
&quot;Id&quot;: &quot;f604f0e34bc263bc32ba683d97a1db2a65de42ab052da16df3c7811ad07f0dc3&quot;,
&quot;Created&quot;: &quot;2019-07-15T17:43:44.158300531Z&quot;,
&quot;Path&quot;: &quot;/bin/sh&quot;,
&quot;Args&quot;: [
&quot;-c&quot;,
&quot;nginx -g \&quot;daemon off;\&quot;&quot;
],
&quot;State&quot;: {
&quot;Status&quot;: &quot;running&quot;,
&quot;Running&quot;: true,
&quot;Pid&quot;: 58212,
&quot;ExitCode&quot;: 0,
&quot;StartedAt&quot;: &quot;2019-07-15T17:43:44.651756682Z&quot;,
&quot;FinishedAt&quot;: &quot;0001-01-01T00:00:00Z&quot;
},
......
&quot;Name&quot;: &quot;/youthful_torvalds&quot;,
&quot;RestartCount&quot;: 0,
&quot;Driver&quot;: &quot;overlay2&quot;,
&quot;Platform&quot;: &quot;linux&quot;,
&quot;HostConfig&quot;: {
&quot;NetworkMode&quot;: &quot;default&quot;,
&quot;PortBindings&quot;: {
&quot;80/tcp&quot;: [
{
&quot;HostIp&quot;: &quot;&quot;,
&quot;HostPort&quot;: &quot;8081&quot;
}
]
},
......
},
&quot;Config&quot;: {
&quot;Hostname&quot;: &quot;f604f0e34bc2&quot;,
&quot;ExposedPorts&quot;: {
&quot;80/tcp&quot;: {}
},
&quot;Image&quot;: &quot;testnginx:1&quot;,
&quot;Entrypoint&quot;: [
&quot;/bin/sh&quot;,
&quot;-c&quot;,
&quot;nginx -g \&quot;daemon off;\&quot;&quot;
],
},
&quot;NetworkSettings&quot;: {
&quot;Bridge&quot;: &quot;&quot;,
&quot;SandboxID&quot;: &quot;7fd3eb469578903b66687090e512958658ae28d17bce1a7cee2da3148d1dfad4&quot;,
&quot;Ports&quot;: {
&quot;80/tcp&quot;: [
{
&quot;HostIp&quot;: &quot;0.0.0.0&quot;,
&quot;HostPort&quot;: &quot;8081&quot;
}
]
},
&quot;Gateway&quot;: &quot;172.17.0.1&quot;,
&quot;IPAddress&quot;: &quot;172.17.0.3&quot;,
&quot;IPPrefixLen&quot;: 16,
&quot;MacAddress&quot;: &quot;02:42:ac:11:00:03&quot;,
&quot;Networks&quot;: {
&quot;bridge&quot;: {
&quot;NetworkID&quot;: &quot;c8eef1603afb399bf17af154be202fd1e543d3772cc83ef4a1ca3f97b8bd6eda&quot;,
&quot;EndpointID&quot;: &quot;8d9bb18ca57889112e758ede193d2cfb45cbf794c9d952819763c08f8545da46&quot;,
&quot;Gateway&quot;: &quot;172.17.0.1&quot;,
&quot;IPAddress&quot;: &quot;172.17.0.3&quot;,
&quot;IPPrefixLen&quot;: 16,
&quot;MacAddress&quot;: &quot;02:42:ac:11:00:03&quot;,
}
}
}
}
]
```
如果我们用ps查看机器上的nginx进程可以看到master和workerworker的父进程是master。
```
# ps -ef |grep nginx
root 58212 58195 0 01:43 ? 00:00:00 /bin/sh -c nginx -g &quot;daemon off;&quot;
root 58244 58212 0 01:43 ? 00:00:00 nginx: master process nginx -g daemon off;
33 58250 58244 0 01:43 ? 00:00:00 nginx: worker process
33 58251 58244 0 01:43 ? 00:00:05 nginx: worker process
33 58252 58244 0 01:43 ? 00:00:05 nginx: worker process
33 58253 58244 0 01:43 ? 00:00:05 nginx: worker process
```
在/proc/pid/ns里面我们能够看到这个进程所属于的6种namespace。我们拿出两个进程来应该可以看出来它们属于同一个namespace。
```
# ls -l /proc/58212/ns
lrwxrwxrwx 1 root root 0 Jul 16 19:19 ipc -&gt; ipc:[4026532278]
lrwxrwxrwx 1 root root 0 Jul 16 19:19 mnt -&gt; mnt:[4026532276]
lrwxrwxrwx 1 root root 0 Jul 16 01:43 net -&gt; net:[4026532281]
lrwxrwxrwx 1 root root 0 Jul 16 19:19 pid -&gt; pid:[4026532279]
lrwxrwxrwx 1 root root 0 Jul 16 19:19 user -&gt; user:[4026531837]
lrwxrwxrwx 1 root root 0 Jul 16 19:19 uts -&gt; uts:[4026532277]
# ls -l /proc/58253/ns
lrwxrwxrwx 1 33 tape 0 Jul 16 19:20 ipc -&gt; ipc:[4026532278]
lrwxrwxrwx 1 33 tape 0 Jul 16 19:20 mnt -&gt; mnt:[4026532276]
lrwxrwxrwx 1 33 tape 0 Jul 16 19:20 net -&gt; net:[4026532281]
lrwxrwxrwx 1 33 tape 0 Jul 16 19:20 pid -&gt; pid:[4026532279]
lrwxrwxrwx 1 33 tape 0 Jul 16 19:20 user -&gt; user:[4026531837]
lrwxrwxrwx 1 33 tape 0 Jul 16 19:20 uts -&gt; uts:[4026532277]
```
接下来我们来看如何操作namespace。这里我们重点关注pid和network。
操作namespace的常用指令**nsenter**可以用来运行一个进程进入指定的namespace。例如通过下面的命令我们可以运行/bin/bash并且进入nginx所在容器的namespace。
```
# nsenter --target 58212 --mount --uts --ipc --net --pid -- env --ignore-environment -- /bin/bash
root@f604f0e34bc2:/# 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
23: eth0@if24: &lt;BROADCAST,MULTICAST,UP,LOWER_UP&gt; mtu 1500 qdisc noqueue state UP group default
link/ether 02:42:ac:11:00:03 brd ff:ff:ff:ff:ff:ff
inet 172.17.0.3/16 brd 172.17.255.255 scope global eth0
valid_lft forever preferred_lft forever
```
另一个命令是**unshare**它会离开当前的namespace创建且加入新的namespace然后执行参数中指定的命令。
例如运行下面这行命令之后pid和net都进入了新的namespace。
```
unshare --mount --ipc --pid --net --mount-proc=/proc --fork /bin/bash
```
如果从shell上运行上面这行命令的话好像没有什么变化但是因为pid和net都进入了新的namespace所以我们查看进程列表和ip地址的时候应该会发现有所不同。
```
# ip addr
1: lo: &lt;LOOPBACK&gt; mtu 65536 qdisc noop state DOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
# ps aux
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.0 115568 2136 pts/0 S 22:55 0:00 /bin/bash
root 13 0.0 0.0 155360 1872 pts/0 R+ 22:55 0:00 ps aux
```
果真我们看不到宿主机上的IP地址和网卡了也看不到宿主机上的所有进程了。
另外我们还可以通过函数操作namespace。
第一个函数是**clone**也就是创建一个新的进程并把它放到新的namespace中。
```
int clone(int (*fn)(void *), void *child_stack, int flags, void *arg);
```
clone函数我们原来介绍过。这里面有一个参数flags原来我们没有注意它。其实它可以设置为CLONE_NEWUTS、CLONE_NEWUSER、CLONE_NEWNS、CLONE_NEWPID。CLONE_NEWNET会将clone出来的新进程放到新的namespace中。
第二个函数是**setns**用于将当前进程加入到已有的namespace中。
```
int setns(int fd, int nstype);
```
其中fd指向/proc/[pid]/ns/目录里相应namespace对应的文件表示要加入哪个namespace。nstype用来指定namespace的类型可以设置为CLONE_NEWUTS、CLONE_NEWUSER、CLONE_NEWNS、CLONE_NEWPID和CLONE_NEWNET。
第三个函数是**unshare**它可以使当前进程退出当前的namespace并加入到新创建的namespace。
```
int unshare(int flags);
```
其中flags用于指定一个或者多个上面的CLONE_NEWUTS、CLONE_NEWUSER、CLONE_NEWNS、CLONE_NEWPID和CLONE_NEWNET。
clone和unshare的区别是unshare是使当前进程加入新的namespaceclone是创建一个新的子进程然后让子进程加入新的namespace而当前进程保持不变。
这里我们尝试一下通过clone函数来进入一个namespace。
```
#define _GNU_SOURCE
#include &lt;sys/wait.h&gt;
#include &lt;sys/utsname.h&gt;
#include &lt;sched.h&gt;
#include &lt;string.h&gt;
#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &lt;unistd.h&gt;
#define STACK_SIZE (1024 * 1024)
static int childFunc(void *arg)
{
printf(&quot;In child process.\n&quot;);
execlp(&quot;bash&quot;, &quot;bash&quot;, (char *) NULL);
return 0;
}
int main(int argc, char *argv[])
{
char *stack;
char *stackTop;
pid_t pid;
stack = malloc(STACK_SIZE);
if (stack == NULL)
{
perror(&quot;malloc&quot;);
exit(1);
}
stackTop = stack + STACK_SIZE;
pid = clone(childFunc, stackTop, CLONE_NEWNS|CLONE_NEWPID|CLONE_NEWNET|SIGCHLD, NULL);
if (pid == -1)
{
perror(&quot;clone&quot;);
exit(1);
}
printf(&quot;clone() returned %ld\n&quot;, (long) pid);
sleep(1);
if (waitpid(pid, NULL, 0) == -1)
{
perror(&quot;waitpid&quot;);
exit(1);
}
printf(&quot;child has terminated\n&quot;);
exit(0);
}
```
在上面的代码中我们调用clone的时候给的参数是CLONE_NEWNS|CLONE_NEWPID|CLONE_NEWNET也就是说我们会进入一个新的pid、network以及mount的namespace。
如果我们编译运行它,可以得到下面的结果。
```
# echo $$
64267
# ps aux | grep bash | grep -v grep
root 64267 0.0 0.0 115572 2176 pts/0 Ss 16:53 0:00 -bash
# ./a.out
clone() returned 64360
In child process.
# echo $$
1
# ip addr
1: lo: &lt;LOOPBACK&gt; mtu 65536 qdisc noop state DOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
# exit
exit
child has terminated
# echo $$
64267
```
通过`echo $$`我们可以得到当前bash的进程号。一旦运行了上面的程序我们就会进入一个新的pid的namespace。
当我们再次`echo $$`的时候就会发现当前bash的进程号变成了1。上面的程序运行了一个新的bash它在一个独立的pid namespace里面自己是1号进程。如果运行ip addr可以看到宿主机的网卡都找不到了因为新的bash也在一个独立的network namespace里面等退出了再次echo $$的时候,就可以得到原来进程号。
clone系统调用我们在[进程的创建](https://time.geekbang.org/column/article/94064)那一节解析过当时我们没有看关于namespace的代码现在我们就来看一看namespace在内核做了哪些事情。
在内核里面clone会调用_do_fork-&gt;copy_process-&gt;copy_namespaces也就是说在创建子进程的时候有一个机会可以复制和设置namespace。
namespace是在哪里定义的呢在每一个进程的task_struct里面有一个指向namespace结构体的指针nsproxy。
```
struct task_struct {
......
/* Namespaces: */
struct nsproxy *nsproxy;
......
}
/*
* A structure to contain pointers to all per-process
* namespaces - fs (mount), uts, network, sysvipc, etc.
*
* The pid namespace is an exception -- it's accessed using
* task_active_pid_ns. The pid namespace here is the
* namespace that children will use.
*/
struct nsproxy {
atomic_t count;
struct uts_namespace *uts_ns;
struct ipc_namespace *ipc_ns;
struct mnt_namespace *mnt_ns;
struct pid_namespace *pid_ns_for_children;
struct net *net_ns;
struct cgroup_namespace *cgroup_ns;
};
```
我们可以看到在struct nsproxy结构里面有我们上面讲过的各种namespace。
在系统初始化的时候有一个默认的init_nsproxy。
```
struct nsproxy init_nsproxy = {
.count = ATOMIC_INIT(1),
.uts_ns = &amp;init_uts_ns,
#if defined(CONFIG_POSIX_MQUEUE) || defined(CONFIG_SYSVIPC)
.ipc_ns = &amp;init_ipc_ns,
#endif
.mnt_ns = NULL,
.pid_ns_for_children = &amp;init_pid_ns,
#ifdef CONFIG_NET
.net_ns = &amp;init_net,
#endif
#ifdef CONFIG_CGROUPS
.cgroup_ns = &amp;init_cgroup_ns,
#endif
};
```
下面我们来看copy_namespaces的实现。
```
/*
* called from clone. This now handles copy for nsproxy and all
* namespaces therein.
*/
int copy_namespaces(unsigned long flags, struct task_struct *tsk)
{
struct nsproxy *old_ns = tsk-&gt;nsproxy;
struct user_namespace *user_ns = task_cred_xxx(tsk, user_ns);
struct nsproxy *new_ns;
if (likely(!(flags &amp; (CLONE_NEWNS | CLONE_NEWUTS | CLONE_NEWIPC |
CLONE_NEWPID | CLONE_NEWNET |
CLONE_NEWCGROUP)))) {
get_nsproxy(old_ns);
return 0;
}
if (!ns_capable(user_ns, CAP_SYS_ADMIN))
return -EPERM;
......
new_ns = create_new_namespaces(flags, tsk, user_ns, tsk-&gt;fs);
tsk-&gt;nsproxy = new_ns;
return 0;
}
```
如果clone的参数里面没有CLONE_NEWNS | CLONE_NEWUTS | CLONE_NEWIPC | CLONE_NEWPID | CLONE_NEWNET | CLONE_NEWCGROUP就返回原来的namespace调用get_nsproxy。
接着我们调用create_new_namespaces。
```
/*
* Create new nsproxy and all of its the associated namespaces.
* Return the newly created nsproxy. Do not attach this to the task,
* leave it to the caller to do proper locking and attach it to task.
*/
static struct nsproxy *create_new_namespaces(unsigned long flags,
struct task_struct *tsk, struct user_namespace *user_ns,
struct fs_struct *new_fs)
{
struct nsproxy *new_nsp;
new_nsp = create_nsproxy();
......
new_nsp-&gt;mnt_ns = copy_mnt_ns(flags, tsk-&gt;nsproxy-&gt;mnt_ns, user_ns, new_fs);
......
new_nsp-&gt;uts_ns = copy_utsname(flags, user_ns, tsk-&gt;nsproxy-&gt;uts_ns);
......
new_nsp-&gt;ipc_ns = copy_ipcs(flags, user_ns, tsk-&gt;nsproxy-&gt;ipc_ns);
......
new_nsp-&gt;pid_ns_for_children =
copy_pid_ns(flags, user_ns, tsk-&gt;nsproxy-&gt;pid_ns_for_children);
......
new_nsp-&gt;cgroup_ns = copy_cgroup_ns(flags, user_ns,
tsk-&gt;nsproxy-&gt;cgroup_ns);
......
new_nsp-&gt;net_ns = copy_net_ns(flags, user_ns, tsk-&gt;nsproxy-&gt;net_ns);
......
return new_nsp;
......
}
```
在create_new_namespaces中我们可以看到对于各种namespace的复制。
我们来看copy_pid_ns对于pid namespace的复制。
```
struct pid_namespace *copy_pid_ns(unsigned long flags,
struct user_namespace *user_ns, struct pid_namespace *old_ns)
{
if (!(flags &amp; CLONE_NEWPID))
return get_pid_ns(old_ns);
if (task_active_pid_ns(current) != old_ns)
return ERR_PTR(-EINVAL);
return create_pid_namespace(user_ns, old_ns);
}
```
在copy_pid_ns中如果没有设置CLONE_NEWPID则返回老的pid namespace如果设置了就调用create_pid_namespace创建新的pid namespace.
我们再来看copy_net_ns对于network namespace的复制。
```
struct net *copy_net_ns(unsigned long flags,
struct user_namespace *user_ns, struct net *old_net)
{
struct ucounts *ucounts;
struct net *net;
int rv;
if (!(flags &amp; CLONE_NEWNET))
return get_net(old_net);
ucounts = inc_net_namespaces(user_ns);
......
net = net_alloc();
......
get_user_ns(user_ns);
net-&gt;ucounts = ucounts;
rv = setup_net(net, user_ns);
......
return net;
}
```
在这里面我们需要判断如果flags中不包含CLONE_NEWNET也就是不会创建一个新的network namespace则返回old_net否则需要新建一个network namespace。
然后copy_net_ns会调用net = net_alloc()分配一个新的struct net结构然后调用setup_net对新分配的net结构进行初始化之后调用list_add_tail_rcu将新建的network namespace添加到全局的network namespace列表net_namespace_list中。
我们来看一下setup_net的实现。
```
/*
* setup_net runs the initializers for the network namespace object.
*/
static __net_init int setup_net(struct net *net, struct user_namespace *user_ns)
{
/* Must be called with net_mutex held */
const struct pernet_operations *ops, *saved_ops;
LIST_HEAD(net_exit_list);
atomic_set(&amp;net-&gt;count, 1);
refcount_set(&amp;net-&gt;passive, 1);
net-&gt;dev_base_seq = 1;
net-&gt;user_ns = user_ns;
idr_init(&amp;net-&gt;netns_ids);
spin_lock_init(&amp;net-&gt;nsid_lock);
list_for_each_entry(ops, &amp;pernet_list, list) {
error = ops_init(ops, net);
......
}
......
}
```
在setup_net中这里面有一个循环list_for_each_entry对于pernet_list的每一项struct pernet_operations运行ops_init也就是调用pernet_operations的init函数。
这个pernet_list是怎么来的呢在网络设备初始化的时候我们要调用net_dev_init函数这里面有下面的代码。
```
register_pernet_device(&amp;loopback_net_ops)
int register_pernet_device(struct pernet_operations *ops)
{
int error;
mutex_lock(&amp;net_mutex);
error = register_pernet_operations(&amp;pernet_list, ops);
if (!error &amp;&amp; (first_device == &amp;pernet_list))
first_device = &amp;ops-&gt;list;
mutex_unlock(&amp;net_mutex);
return error;
}
struct pernet_operations __net_initdata loopback_net_ops = {
.init = loopback_net_init,
};
```
register_pernet_device函数注册了一个loopback_net_ops在这里面把init函数设置为loopback_net_init.
```
static __net_init int loopback_net_init(struct net *net)
{
struct net_device *dev;
dev = alloc_netdev(0, &quot;lo&quot;, NET_NAME_UNKNOWN, loopback_setup);
......
dev_net_set(dev, net);
err = register_netdev(dev);
......
net-&gt;loopback_dev = dev;
return 0;
......
}
```
在loopback_net_init函数中我们会创建并且注册一个名字为"lo"的struct net_device。注册完之后在这个namespace里面就会出现一个这样的网络设备称为loopback网络设备。
这就是为什么上面的实验中创建出的新的network namespace里面有一个lo网络设备。
## 总结时刻
这一节我们讲了namespace相关的技术有六种类型分别是UTS、User、Mount、Pid、Network和IPC。
还有两个常用的命令nsenter和unshare主要用于操作Namespace有三个常用的函数clone、setns和unshare。
在内核里面对于任何一个进程task_struct来讲里面都会有一个成员struct nsproxy用于保存namespace相关信息里面有 struct uts_namespace、struct ipc_namespace、struct mnt_namespace、struct pid_namespace、struct net *net_ns和struct cgroup_namespace *cgroup_ns。
创建namespace的时候我们在内核中会调用copy_namespaces调用顺序依次是copy_mnt_ns、copy_utsname、copy_ipcs、copy_pid_ns、copy_cgroup_ns和copy_net_ns来复制namespace。
<img src="https://static001.geekbang.org/resource/image/56/d7/56bb9502b58628ff3d1bee83b6f53cd7.png" alt="">
## 课堂练习
网络的Namespace有一个非常好的命令ip netns。请你研究一下这个命令并且创建一个容器用这个命令查看网络namespace。
欢迎留言和我分享你的疑惑和见解,也欢迎收藏本节内容,反复研读。你也可以把今天的内容分享给你的朋友,和他一起学习和进步。
<img src="https://static001.geekbang.org/resource/image/8c/37/8c0a95fa07a8b9a1abfd394479bdd637.jpg" alt="">

View File

@@ -0,0 +1,870 @@
<audio id="audio" title="58 | cgroup技术内部创业公司应该独立核算成本" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/71/d9/7187ee713149abd66837faa9173a30d9.mp3"></audio>
我们前面说了容器实现封闭的环境主要靠两种技术一种是“看起来是隔离”的技术Namespace另一种是用起来是隔离的技术cgroup。
上一节我们讲了“看起来隔离“的技术Namespace这一节我们就来看一下“用起来隔离“的技术cgroup。
cgroup全称是control group顾名思义它是用来做“控制”的。控制什么东西呢当然是资源的使用了。那它都能控制哪些资源的使用呢我们一起来看一看。
首先cgroup定义了下面的一系列子系统每个子系统用于控制某一类资源。
- CPU子系统主要限制进程的CPU使用率。
- cpuacct 子系统,可以统计 cgroup 中的进程的 CPU 使用报告。
- cpuset 子系统,可以为 cgroup 中的进程分配单独的 CPU 节点或者内存节点。
- memory 子系统,可以限制进程的 Memory 使用量。
- blkio 子系统,可以限制进程的块设备 IO。
- devices 子系统,可以控制进程能够访问某些设备。
- net_cls 子系统,可以标记 cgroups 中进程的网络数据包,然后可以使用 tc 模块traffic control对数据包进行控制。
- freezer 子系统,可以挂起或者恢复 cgroup 中的进程。
这么多子系统你可能要说了那我们不用都掌握吧没错这里面最常用的是对于CPU和内存的控制所以下面我们详细来说它。
在容器这一章的第一节我们讲了Docker有一些参数能够限制CPU和内存的使用如果把它落地到cgroup里面会如何限制呢
为了验证Docker的参数与cgroup的映射关系我们运行一个命令特殊的docker run命令这个命令比较长里面的参数都会映射为cgroup的某项配置然后我们运行docker ps可以看到这个容器的id为3dc0601189dd。
```
docker run -d --cpu-shares 513 --cpus 2 --cpuset-cpus 1,3 --memory 1024M --memory-swap 1234M --memory-swappiness 7 -p 8081:80 testnginx:1
# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
3dc0601189dd testnginx:1 &quot;/bin/sh -c 'nginx -…&quot; About a minute ago Up About a minute 0.0.0.0:8081-&gt;80/tcp boring_cohen
```
在Linux上为了操作cgroup有一个专门的cgroup文件系统我们运行mount命令可以查看。
```
# mount -t cgroup
cgroup on /sys/fs/cgroup/systemd type cgroup (rw,nosuid,nodev,noexec,relatime,xattr,release_agent=/usr/lib/systemd/systemd-cgroups-agent,name=systemd)
cgroup on /sys/fs/cgroup/net_cls,net_prio type cgroup (rw,nosuid,nodev,noexec,relatime,net_prio,net_cls)
cgroup on /sys/fs/cgroup/perf_event type cgroup (rw,nosuid,nodev,noexec,relatime,perf_event)
cgroup on /sys/fs/cgroup/devices type cgroup (rw,nosuid,nodev,noexec,relatime,devices)
cgroup on /sys/fs/cgroup/blkio type cgroup (rw,nosuid,nodev,noexec,relatime,blkio)
cgroup on /sys/fs/cgroup/cpu,cpuacct type cgroup (rw,nosuid,nodev,noexec,relatime,cpuacct,cpu)
cgroup on /sys/fs/cgroup/memory type cgroup (rw,nosuid,nodev,noexec,relatime,memory)
cgroup on /sys/fs/cgroup/cpuset type cgroup (rw,nosuid,nodev,noexec,relatime,cpuset)
cgroup on /sys/fs/cgroup/hugetlb type cgroup (rw,nosuid,nodev,noexec,relatime,hugetlb)
cgroup on /sys/fs/cgroup/freezer type cgroup (rw,nosuid,nodev,noexec,relatime,freezer)
cgroup on /sys/fs/cgroup/pids type cgroup (rw,nosuid,nodev,noexec,relatime,pids)
```
cgroup文件系统多挂载到/sys/fs/cgroup下通过上面的命令行我们可以看到我们可以用cgroup控制哪些资源。
对于CPU的控制我在这一章的第一节讲过Docker可以控制cpu-shares、cpus和cpuset。
我们在/sys/fs/cgroup/下面能看到下面的目录结构。
```
drwxr-xr-x 5 root root 0 May 30 17:00 blkio
lrwxrwxrwx 1 root root 11 May 30 17:00 cpu -&gt; cpu,cpuacct
lrwxrwxrwx 1 root root 11 May 30 17:00 cpuacct -&gt; cpu,cpuacct
drwxr-xr-x 5 root root 0 May 30 17:00 cpu,cpuacct
drwxr-xr-x 3 root root 0 May 30 17:00 cpuset
drwxr-xr-x 5 root root 0 May 30 17:00 devices
drwxr-xr-x 3 root root 0 May 30 17:00 freezer
drwxr-xr-x 3 root root 0 May 30 17:00 hugetlb
drwxr-xr-x 5 root root 0 May 30 17:00 memory
lrwxrwxrwx 1 root root 16 May 30 17:00 net_cls -&gt; net_cls,net_prio
drwxr-xr-x 3 root root 0 May 30 17:00 net_cls,net_prio
lrwxrwxrwx 1 root root 16 May 30 17:00 net_prio -&gt; net_cls,net_prio
drwxr-xr-x 3 root root 0 May 30 17:00 perf_event
drwxr-xr-x 5 root root 0 May 30 17:00 pids
drwxr-xr-x 5 root root 0 May 30 17:00 systemd
```
我们可以想象CPU的资源控制的配置文件应该在cpu,cpuacct这个文件夹下面。
```
# ls
cgroup.clone_children cpu.cfs_period_us notify_on_release
cgroup.event_control cpu.cfs_quota_us release_agent
cgroup.procs cpu.rt_period_us system.slice
cgroup.sane_behavior cpu.rt_runtime_us tasks
cpuacct.stat cpu.shares user.slice
cpuacct.usage cpu.stat
cpuacct.usage_percpu docker
```
果真这下面是对CPU的相关控制里面还有一个路径叫docker。我们进入这个路径。
```
]# ls
cgroup.clone_children
cgroup.event_control
cgroup.procs
cpuacct.stat
cpuacct.usage
cpuacct.usage_percpu
cpu.cfs_period_us
cpu.cfs_quota_us
cpu.rt_period_us
cpu.rt_runtime_us
cpu.shares
cpu.stat
3dc0601189dd218898f31f9526a6cfae83913763a4da59f95ec789c6e030ecfd
notify_on_release
tasks
```
这里面有个很长的id是我们创建的docker的id。
```
[3dc0601189dd218898f31f9526a6cfae83913763a4da59f95ec789c6e030ecfd]# ls
cgroup.clone_children cpuacct.usage_percpu cpu.shares
cgroup.event_control cpu.cfs_period_us cpu.stat
cgroup.procs cpu.cfs_quota_us notify_on_release
cpuacct.stat cpu.rt_period_us tasks
cpuacct.usage cpu.rt_runtime_us
```
在这里我们能看到cpu.shares还有一个重要的文件tasks。这里面是这个容器里所有进程的进程号也即所有这些进程都被这些CPU策略控制。
```
[3dc0601189dd218898f31f9526a6cfae83913763a4da59f95ec789c6e030ecfd]# cat tasks
39487
39520
39526
39527
39528
39529
```
如果我们查看cpu.shares里面就是我们设置的513。
```
[3dc0601189dd218898f31f9526a6cfae83913763a4da59f95ec789c6e030ecfd]# cat cpu.shares
513
```
另外我们还配置了cpus这个值其实是由cpu.cfs_period_us和cpu.cfs_quota_us共同决定的。cpu.cfs_period_us是运行周期cpu.cfs_quota_us是在周期内这些进程占用多少时间。我们设置了cpus为2代表的意思是在周期100000微秒的运行周期内这些进程要占用200000微秒的时间也即需要两个CPU同时运行一个整的周期。
```
[3dc0601189dd218898f31f9526a6cfae83913763a4da59f95ec789c6e030ecfd]# cat cpu.cfs_period_us
100000
[3dc0601189dd218898f31f9526a6cfae83913763a4da59f95ec789c6e030ecfd]# cat cpu.cfs_quota_us
200000
```
对于cpuset也即CPU绑核的参数在另外一个文件夹里面/sys/fs/cgroup/cpuset这里面同样有一个docker文件夹下面同样有docker id 也即3dc0601189dd218898f31f9526a6cfae83913763a4da59f95ec789c6e030ecfd文件夹这里面的cpuset.cpus就是配置的绑定到1、3两个核。
```
[3dc0601189dd218898f31f9526a6cfae83913763a4da59f95ec789c6e030ecfd]# cat cpuset.cpus
1,3
```
这一章的第一节我们还讲了Docker可以限制内存的使用量例如memory、memory-swap、memory-swappiness。这些在哪里控制呢
/sys/fs/cgroup/下面还有一个memory路径控制策略就是在这里面定义的。
```
[root@deployer memory]# ls
cgroup.clone_children memory.memsw.failcnt
cgroup.event_control memory.memsw.limit_in_bytes
cgroup.procs memory.memsw.max_usage_in_bytes
cgroup.sane_behavior memory.memsw.usage_in_bytes
docker memory.move_charge_at_immigrate
memory.failcnt memory.numa_stat
memory.force_empty memory.oom_control
memory.kmem.failcnt memory.pressure_level
memory.kmem.limit_in_bytes memory.soft_limit_in_bytes
memory.kmem.max_usage_in_bytes memory.stat
memory.kmem.slabinfo memory.swappiness
memory.kmem.tcp.failcnt memory.usage_in_bytes
memory.kmem.tcp.limit_in_bytes memory.use_hierarchy
memory.kmem.tcp.max_usage_in_bytes notify_on_release
memory.kmem.tcp.usage_in_bytes release_agent
memory.kmem.usage_in_bytes system.slice
memory.limit_in_bytes tasks
memory.max_usage_in_bytes user.slice
```
这里面全是对于memory的控制参数在这里面我们可看到了docker里面还有容器的id作为文件夹。
```
[docker]# ls
3dc0601189dd218898f31f9526a6cfae83913763a4da59f95ec789c6e030ecfd
cgroup.clone_children
cgroup.event_control
cgroup.procs
memory.failcnt
memory.force_empty
memory.kmem.failcnt
memory.kmem.limit_in_bytes
memory.kmem.max_usage_in_bytes
memory.kmem.slabinfo
memory.kmem.tcp.failcnt
memory.kmem.tcp.limit_in_bytes
memory.kmem.tcp.max_usage_in_bytes
memory.kmem.tcp.usage_in_bytes
memory.kmem.usage_in_bytes
memory.limit_in_bytes
memory.max_usage_in_bytes
memory.memsw.failcnt
memory.memsw.limit_in_bytes
memory.memsw.max_usage_in_bytes
memory.memsw.usage_in_bytes
memory.move_charge_at_immigrate
memory.numa_stat
memory.oom_control
memory.pressure_level
memory.soft_limit_in_bytes
memory.stat
memory.swappiness
memory.usage_in_bytes
memory.use_hierarchy
notify_on_release
tasks
[3dc0601189dd218898f31f9526a6cfae83913763a4da59f95ec789c6e030ecfd]# ls
cgroup.clone_children memory.memsw.failcnt
cgroup.event_control memory.memsw.limit_in_bytes
cgroup.procs memory.memsw.max_usage_in_bytes
memory.failcnt memory.memsw.usage_in_bytes
memory.force_empty memory.move_charge_at_immigrate
memory.kmem.failcnt memory.numa_stat
memory.kmem.limit_in_bytes memory.oom_control
memory.kmem.max_usage_in_bytes memory.pressure_level
memory.kmem.slabinfo memory.soft_limit_in_bytes
memory.kmem.tcp.failcnt memory.stat
memory.kmem.tcp.limit_in_bytes memory.swappiness
memory.kmem.tcp.max_usage_in_bytes memory.usage_in_bytes
memory.kmem.tcp.usage_in_bytes memory.use_hierarchy
memory.kmem.usage_in_bytes notify_on_release
memory.limit_in_bytes tasks
memory.max_usage_in_bytes
```
在docker id的文件夹下面有一个memory.limit_in_bytes里面配置的就是memory。
```
[3dc0601189dd218898f31f9526a6cfae83913763a4da59f95ec789c6e030ecfd]# cat memory.limit_in_bytes
1073741824
```
还有memory.swappiness里面配置的就是memory-swappiness。
```
[3dc0601189dd218898f31f9526a6cfae83913763a4da59f95ec789c6e030ecfd]# cat memory.swappiness
7
```
还有就是memory.memsw.limit_in_bytes里面配置的是memory-swap。
```
[3dc0601189dd218898f31f9526a6cfae83913763a4da59f95ec789c6e030ecfd]# cat memory.memsw.limit_in_bytes
1293942784
```
我们还可以看一下tasks文件的内容tasks里面是容器里面所有进程的进程号。
```
[3dc0601189dd218898f31f9526a6cfae83913763a4da59f95ec789c6e030ecfd]# cat tasks
39487
39520
39526
39527
39528
39529
```
至此我们看到了cgroup对于Docker资源的控制在用户态是如何表现的。我画了一张图总结一下。
<img src="https://static001.geekbang.org/resource/image/1c/0f/1c762a6283429ff3587a7fc370fc090f.png" alt="">
在内核中cgroup是如何实现的呢
首先在系统初始化的时候cgroup也会进行初始化在start_kernel中cgroup_init_early和cgroup_init都会进行初始化。
```
asmlinkage __visible void __init start_kernel(void)
{
......
cgroup_init_early();
......
cgroup_init();
......
}
```
在cgroup_init_early和cgroup_init中会有下面的循环。
```
for_each_subsys(ss, i) {
ss-&gt;id = i;
ss-&gt;name = cgroup_subsys_name[i];
......
cgroup_init_subsys(ss, true);
}
#define for_each_subsys(ss, ssid) \
for ((ssid) = 0; (ssid) &lt; CGROUP_SUBSYS_COUNT &amp;&amp; \
(((ss) = cgroup_subsys[ssid]) || true); (ssid)++)
```
for_each_subsys会在cgroup_subsys数组中进行循环。这个cgroup_subsys数组是如何形成的呢
```
#define SUBSYS(_x) [_x ## _cgrp_id] = &amp;_x ## _cgrp_subsys,
struct cgroup_subsys *cgroup_subsys[] = {
#include &lt;linux/cgroup_subsys.h&gt;
};
#undef SUBSYS
```
SUBSYS这个宏定义了这个cgroup_subsys数组数组中的项定义在cgroup_subsys.h头文件中。例如对于CPU和内存有下面的定义。
```
//cgroup_subsys.h
#if IS_ENABLED(CONFIG_CPUSETS)
SUBSYS(cpuset)
#endif
#if IS_ENABLED(CONFIG_CGROUP_SCHED)
SUBSYS(cpu)
#endif
#if IS_ENABLED(CONFIG_CGROUP_CPUACCT)
SUBSYS(cpuacct)
#endif
#if IS_ENABLED(CONFIG_MEMCG)
SUBSYS(memory)
#endif
```
根据SUBSYS的定义SUBSYS(cpu)其实是[cpu_cgrp_id] = &amp;cpu_cgrp_subsys而SUBSYS(memory)其实是[memory_cgrp_id] = &amp;memory_cgrp_subsys。
我们能够找到cpu_cgrp_subsys和memory_cgrp_subsys的定义。
```
cpuset_cgrp_subsys
struct cgroup_subsys cpuset_cgrp_subsys = {
.css_alloc = cpuset_css_alloc,
.css_online = cpuset_css_online,
.css_offline = cpuset_css_offline,
.css_free = cpuset_css_free,
.can_attach = cpuset_can_attach,
.cancel_attach = cpuset_cancel_attach,
.attach = cpuset_attach,
.post_attach = cpuset_post_attach,
.bind = cpuset_bind,
.fork = cpuset_fork,
.legacy_cftypes = files,
.early_init = true,
};
cpu_cgrp_subsys
struct cgroup_subsys cpu_cgrp_subsys = {
.css_alloc = cpu_cgroup_css_alloc,
.css_online = cpu_cgroup_css_online,
.css_released = cpu_cgroup_css_released,
.css_free = cpu_cgroup_css_free,
.fork = cpu_cgroup_fork,
.can_attach = cpu_cgroup_can_attach,
.attach = cpu_cgroup_attach,
.legacy_cftypes = cpu_files,
.early_init = true,
};
memory_cgrp_subsys
struct cgroup_subsys memory_cgrp_subsys = {
.css_alloc = mem_cgroup_css_alloc,
.css_online = mem_cgroup_css_online,
.css_offline = mem_cgroup_css_offline,
.css_released = mem_cgroup_css_released,
.css_free = mem_cgroup_css_free,
.css_reset = mem_cgroup_css_reset,
.can_attach = mem_cgroup_can_attach,
.cancel_attach = mem_cgroup_cancel_attach,
.post_attach = mem_cgroup_move_task,
.bind = mem_cgroup_bind,
.dfl_cftypes = memory_files,
.legacy_cftypes = mem_cgroup_legacy_files,
.early_init = 0,
};
```
在for_each_subsys的循环里面cgroup_subsys[]数组中的每一个cgroup_subsys都会调用cgroup_init_subsys对于cgroup_subsys对于初始化。
```
static void __init cgroup_init_subsys(struct cgroup_subsys *ss, bool early)
{
struct cgroup_subsys_state *css;
......
idr_init(&amp;ss-&gt;css_idr);
INIT_LIST_HEAD(&amp;ss-&gt;cfts);
/* Create the root cgroup state for this subsystem */
ss-&gt;root = &amp;cgrp_dfl_root;
css = ss-&gt;css_alloc(cgroup_css(&amp;cgrp_dfl_root.cgrp, ss));
......
init_and_link_css(css, ss, &amp;cgrp_dfl_root.cgrp);
......
css-&gt;id = cgroup_idr_alloc(&amp;ss-&gt;css_idr, css, 1, 2, GFP_KERNEL);
init_css_set.subsys[ss-&gt;id] = css;
......
BUG_ON(online_css(css));
......
}
```
cgroup_init_subsys里面会做两件事情一个是调用cgroup_subsys的css_alloc函数创建一个cgroup_subsys_state另外就是调用online_css也即调用cgroup_subsys的css_online函数激活这个cgroup。
对于CPU来讲css_alloc函数就是cpu_cgroup_css_alloc。这里面会调用 sched_create_group创建一个struct task_group。在这个结构中第一项就是cgroup_subsys_state也就是说task_group是cgroup_subsys_state的一个扩展最终返回的是指向cgroup_subsys_state结构的指针可以通过强制类型转换变为task_group。
```
struct task_group {
struct cgroup_subsys_state css;
#ifdef CONFIG_FAIR_GROUP_SCHED
/* schedulable entities of this group on each cpu */
struct sched_entity **se;
/* runqueue &quot;owned&quot; by this group on each cpu */
struct cfs_rq **cfs_rq;
unsigned long shares;
#ifdef CONFIG_SMP
atomic_long_t load_avg ____cacheline_aligned;
#endif
#endif
struct rcu_head rcu;
struct list_head list;
struct task_group *parent;
struct list_head siblings;
struct list_head children;
struct cfs_bandwidth cfs_bandwidth;
};
```
在task_group结构中有一个成员是sched_entity前面我们讲进程调度的时候遇到过它。它是调度的实体也即这一个task_group也是一个调度实体。
接下来online_css会被调用。对于CPU来讲online_css调用的是cpu_cgroup_css_online。它会调用sched_online_group-&gt;online_fair_sched_group。
```
void online_fair_sched_group(struct task_group *tg)
{
struct sched_entity *se;
struct rq *rq;
int i;
for_each_possible_cpu(i) {
rq = cpu_rq(i);
se = tg-&gt;se[i];
update_rq_clock(rq);
attach_entity_cfs_rq(se);
sync_throttle(tg, i);
}
}
```
在这里面对于每一个CPU取出每个CPU的运行队列rq也取出task_group的sched_entity然后通过attach_entity_cfs_rq将sched_entity添加到运行队列中。
对于内存来讲css_alloc函数就是mem_cgroup_css_alloc。这里面会调用 mem_cgroup_alloc创建一个struct mem_cgroup。在这个结构中第一项就是cgroup_subsys_state也就是说mem_cgroup是cgroup_subsys_state的一个扩展最终返回的是指向cgroup_subsys_state结构的指针我们可以通过强制类型转换变为mem_cgroup。
```
struct mem_cgroup {
struct cgroup_subsys_state css;
/* Private memcg ID. Used to ID objects that outlive the cgroup */
struct mem_cgroup_id id;
/* Accounted resources */
struct page_counter memory;
struct page_counter swap;
/* Legacy consumer-oriented counters */
struct page_counter memsw;
struct page_counter kmem;
struct page_counter tcpmem;
/* Normal memory consumption range */
unsigned long low;
unsigned long high;
/* Range enforcement for interrupt charges */
struct work_struct high_work;
unsigned long soft_limit;
......
int swappiness;
......
/*
* percpu counter.
*/
struct mem_cgroup_stat_cpu __percpu *stat;
int last_scanned_node;
/* List of events which userspace want to receive */
struct list_head event_list;
spinlock_t event_list_lock;
struct mem_cgroup_per_node *nodeinfo[0];
/* WARNING: nodeinfo must be the last member here */
};
```
在cgroup_init函数中cgroup的初始化还做了一件很重要的事情它会调用cgroup_init_cftypes(NULL, cgroup1_base_files)来初始化对于cgroup文件类型cftype的操作函数也就是将struct kernfs_ops *kf_ops设置为cgroup_kf_ops。
```
struct cftype cgroup1_base_files[] = {
......
{
.name = &quot;tasks&quot;,
.seq_start = cgroup_pidlist_start,
.seq_next = cgroup_pidlist_next,
.seq_stop = cgroup_pidlist_stop,
.seq_show = cgroup_pidlist_show,
.private = CGROUP_FILE_TASKS,
.write = cgroup_tasks_write,
},
}
static struct kernfs_ops cgroup_kf_ops = {
.atomic_write_len = PAGE_SIZE,
.open = cgroup_file_open,
.release = cgroup_file_release,
.write = cgroup_file_write,
.seq_start = cgroup_seqfile_start,
.seq_next = cgroup_seqfile_next,
.seq_stop = cgroup_seqfile_stop,
.seq_show = cgroup_seqfile_show,
};
```
在cgroup初始化完毕之后接下来就是创建一个cgroup的文件系统用于配置和操作cgroup。
cgroup是一种特殊的文件系统。它的定义如下
```
struct file_system_type cgroup_fs_type = {
.name = &quot;cgroup&quot;,
.mount = cgroup_mount,
.kill_sb = cgroup_kill_sb,
.fs_flags = FS_USERNS_MOUNT,
};
```
当我们mount这个cgroup文件系统的时候会调用cgroup_mount-&gt;cgroup1_mount。
```
struct dentry *cgroup1_mount(struct file_system_type *fs_type, int flags,
void *data, unsigned long magic,
struct cgroup_namespace *ns)
{
struct super_block *pinned_sb = NULL;
struct cgroup_sb_opts opts;
struct cgroup_root *root;
struct cgroup_subsys *ss;
struct dentry *dentry;
int i, ret;
bool new_root = false;
......
root = kzalloc(sizeof(*root), GFP_KERNEL);
new_root = true;
init_cgroup_root(root, &amp;opts);
ret = cgroup_setup_root(root, opts.subsys_mask, PERCPU_REF_INIT_DEAD);
......
dentry = cgroup_do_mount(&amp;cgroup_fs_type, flags, root,
CGROUP_SUPER_MAGIC, ns);
......
return dentry;
}
```
cgroup被组织成为树形结构因而有cgroup_root。init_cgroup_root会初始化这个cgroup_root。cgroup_root是cgroup的根它有一个成员kf_root是cgroup文件系统的根struct kernfs_root。kernfs_create_root就是用来创建这个kernfs_root结构的。
```
int cgroup_setup_root(struct cgroup_root *root, u16 ss_mask, int ref_flags)
{
LIST_HEAD(tmp_links);
struct cgroup *root_cgrp = &amp;root-&gt;cgrp;
struct kernfs_syscall_ops *kf_sops;
struct css_set *cset;
int i, ret;
root-&gt;kf_root = kernfs_create_root(kf_sops,
KERNFS_ROOT_CREATE_DEACTIVATED,
root_cgrp);
root_cgrp-&gt;kn = root-&gt;kf_root-&gt;kn;
ret = css_populate_dir(&amp;root_cgrp-&gt;self);
ret = rebind_subsystems(root, ss_mask);
......
list_add(&amp;root-&gt;root_list, &amp;cgroup_roots);
cgroup_root_count++;
......
kernfs_activate(root_cgrp-&gt;kn);
......
}
```
就像在普通文件系统上每一个文件都对应一个inode在cgroup文件系统上每个文件都对应一个struct kernfs_node结构当然kernfs_root作为文件系的根也对应一个kernfs_node结构。
接下来css_populate_dir会调用cgroup_addrm_files-&gt;cgroup_add_file-&gt;cgroup_add_file来创建整棵文件树并且为树中的每个文件创建对应的kernfs_node结构并将这个文件的操作函数设置为kf_ops也即指向cgroup_kf_ops 。
```
static int cgroup_add_file(struct cgroup_subsys_state *css, struct cgroup *cgrp,
struct cftype *cft)
{
char name[CGROUP_FILE_NAME_MAX];
struct kernfs_node *kn;
......
kn = __kernfs_create_file(cgrp-&gt;kn, cgroup_file_name(cgrp, cft, name),
cgroup_file_mode(cft), 0, cft-&gt;kf_ops, cft,
NULL, key);
......
}
struct kernfs_node *__kernfs_create_file(struct kernfs_node *parent,
const char *name,
umode_t mode, loff_t size,
const struct kernfs_ops *ops,
void *priv, const void *ns,
struct lock_class_key *key)
{
struct kernfs_node *kn;
unsigned flags;
int rc;
flags = KERNFS_FILE;
kn = kernfs_new_node(parent, name, (mode &amp; S_IALLUGO) | S_IFREG, flags);
kn-&gt;attr.ops = ops;
kn-&gt;attr.size = size;
kn-&gt;ns = ns;
kn-&gt;priv = priv;
......
rc = kernfs_add_one(kn);
......
return kn;
}
```
从cgroup_setup_root返回后接下来在cgroup1_mount中要做的一件事情是cgroup_do_mount调用kernfs_mount真的去mount这个文件系统返回一个普通的文件系统都认识的dentry。这种特殊的文件系统对应的文件操作函数为kernfs_file_fops。
```
const struct file_operations kernfs_file_fops = {
.read = kernfs_fop_read,
.write = kernfs_fop_write,
.llseek = generic_file_llseek,
.mmap = kernfs_fop_mmap,
.open = kernfs_fop_open,
.release = kernfs_fop_release,
.poll = kernfs_fop_poll,
.fsync = noop_fsync,
};
```
当我们要写入一个CGroup文件来设置参数的时候根据文件系统的操作kernfs_fop_write会被调用在这里面会调用kernfs_ops的write函数根据上面的定义为cgroup_file_write在这里会调用cftype的write函数。对于CPU和内存的write函数有以下不同的定义。
```
static struct cftype cpu_files[] = {
#ifdef CONFIG_FAIR_GROUP_SCHED
{
.name = &quot;shares&quot;,
.read_u64 = cpu_shares_read_u64,
.write_u64 = cpu_shares_write_u64,
},
#endif
#ifdef CONFIG_CFS_BANDWIDTH
{
.name = &quot;cfs_quota_us&quot;,
.read_s64 = cpu_cfs_quota_read_s64,
.write_s64 = cpu_cfs_quota_write_s64,
},
{
.name = &quot;cfs_period_us&quot;,
.read_u64 = cpu_cfs_period_read_u64,
.write_u64 = cpu_cfs_period_write_u64,
},
}
static struct cftype mem_cgroup_legacy_files[] = {
{
.name = &quot;usage_in_bytes&quot;,
.private = MEMFILE_PRIVATE(_MEM, RES_USAGE),
.read_u64 = mem_cgroup_read_u64,
},
{
.name = &quot;max_usage_in_bytes&quot;,
.private = MEMFILE_PRIVATE(_MEM, RES_MAX_USAGE),
.write = mem_cgroup_reset,
.read_u64 = mem_cgroup_read_u64,
},
{
.name = &quot;limit_in_bytes&quot;,
.private = MEMFILE_PRIVATE(_MEM, RES_LIMIT),
.write = mem_cgroup_write,
.read_u64 = mem_cgroup_read_u64,
},
{
.name = &quot;soft_limit_in_bytes&quot;,
.private = MEMFILE_PRIVATE(_MEM, RES_SOFT_LIMIT),
.write = mem_cgroup_write,
.read_u64 = mem_cgroup_read_u64,
},
}
```
如果设置的是cpu.shares则调用cpu_shares_write_u64。在这里面task_group的shares变量更新了并且更新了CPU队列上的调度实体。
```
int sched_group_set_shares(struct task_group *tg, unsigned long shares)
{
int i;
shares = clamp(shares, scale_load(MIN_SHARES), scale_load(MAX_SHARES));
tg-&gt;shares = shares;
for_each_possible_cpu(i) {
struct rq *rq = cpu_rq(i);
struct sched_entity *se = tg-&gt;se[i];
struct rq_flags rf;
update_rq_clock(rq);
for_each_sched_entity(se) {
update_load_avg(se, UPDATE_TG);
update_cfs_shares(se);
}
}
......
}
```
但是这个时候别忘了我们还没有将CPU的文件夹下面的tasks文件写入进程号呢。写入一个进程号到tasks文件里面按照cgroup1_base_files里面的定义我们应该调用cgroup_tasks_write。
接下来的调用链为cgroup_tasks_write-&gt;__cgroup_procs_write-&gt;cgroup_attach_task-&gt; cgroup_migrate-&gt;cgroup_migrate_execute。将这个进程和一个cgroup关联起来也即将这个进程迁移到这个cgroup下面。
```
static int cgroup_migrate_execute(struct cgroup_mgctx *mgctx)
{
struct cgroup_taskset *tset = &amp;mgctx-&gt;tset;
struct cgroup_subsys *ss;
struct task_struct *task, *tmp_task;
struct css_set *cset, *tmp_cset;
......
if (tset-&gt;nr_tasks) {
do_each_subsys_mask(ss, ssid, mgctx-&gt;ss_mask) {
if (ss-&gt;attach) {
tset-&gt;ssid = ssid;
ss-&gt;attach(tset);
}
} while_each_subsys_mask();
}
......
}
```
每一个cgroup子系统会调用相应的attach函数。而CPU调用的是cpu_cgroup_attach-&gt; sched_move_task-&gt; sched_change_group。
```
static void sched_change_group(struct task_struct *tsk, int type)
{
struct task_group *tg;
tg = container_of(task_css_check(tsk, cpu_cgrp_id, true),
struct task_group, css);
tg = autogroup_task_group(tsk, tg);
tsk-&gt;sched_task_group = tg;
#ifdef CONFIG_FAIR_GROUP_SCHED
if (tsk-&gt;sched_class-&gt;task_change_group)
tsk-&gt;sched_class-&gt;task_change_group(tsk, type);
else
#endif
set_task_rq(tsk, task_cpu(tsk));
}
```
在sched_change_group中设置这个进程以这个task_group的方式参与调度从而使得上面的cpu.shares起作用。
对于内存来讲写入内存的限制使用函数mem_cgroup_write-&gt;mem_cgroup_resize_limit来设置struct mem_cgroup的memory.limit成员。
在进程执行过程中申请内存的时候我们会调用handle_pte_fault-&gt;do_anonymous_page()-&gt;mem_cgroup_try_charge()。
```
int mem_cgroup_try_charge(struct page *page, struct mm_struct *mm,
gfp_t gfp_mask, struct mem_cgroup **memcgp,
bool compound)
{
struct mem_cgroup *memcg = NULL;
......
if (!memcg)
memcg = get_mem_cgroup_from_mm(mm);
ret = try_charge(memcg, gfp_mask, nr_pages);
......
}
```
在mem_cgroup_try_charge中先是调用get_mem_cgroup_from_mm获得这个进程对应的mem_cgroup结构然后在try_charge中根据mem_cgroup的限制看是否可以申请分配内存。
至此cgroup对于内存的限制才真正起作用。
## 总结时刻
内核中cgroup的工作机制我们在这里总结一下。
<img src="https://static001.geekbang.org/resource/image/c9/c4/c9cc56d20e6a4bac0f9657e6380a96c4.png" alt="">
第一步系统初始化的时候初始化cgroup的各个子系统的操作函数分配各个子系统的数据结构。
第二步mount cgroup文件系统创建文件系统的树形结构以及操作函数。
第三步写入cgroup文件设置cpu或者memory的相关参数这个时候文件系统的操作函数会调用到cgroup子系统的操作函数从而将参数设置到cgroup子系统的数据结构中。
第四步写入tasks文件将进程交给某个cgroup进行管理因为tasks文件也是一个cgroup文件统一会调用文件系统的操作函数进而调用cgroup子系统的操作函数将cgroup子系统的数据结构和进程关联起来。
第五步对于CPU来讲会修改scheduled entity放入相应的队列里面去从而下次调度的时候就起作用了。对于内存的cgroup设定只有在申请内存的时候才起作用。
## 课堂练习
这里我们用cgroup限制了CPU和内存如何限制网络呢给你一个提示tc请你研究一下。
欢迎留言和我分享你的疑惑和见解,也欢迎收藏本节内容,反复研读。你也可以把今天的内容分享给你的朋友,和他一起学习和进步。

View File

@@ -0,0 +1,112 @@
<audio id="audio" title="59 | 数据中心操作系统:上市敲钟" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/34/98/349889ca2fdd38573a4924b9e16d6198.mp3"></audio>
在这门课程里面我们说了内核态有很多的模块可以帮助我们管理硬件设备最重要的四种硬件资源是CPU、内存、存储和网络。
最初使用汇编语言的前辈在程序中需要指定使用的硬件资源例如指定使用哪个寄存器、放在内存的哪个位置、写入或者读取哪个串口等等。对于这些资源的使用需要程序员自己心里非常地清楚要不然一旦jump错了位置程序就无法运行。
为了将程序员从对硬件的直接操作中解放出来提升程序设计的效率于是我们有了操作系统这一层用来实现对于硬件资源的统一管理。某个程序应该使用哪个CPU、哪部分内存、哪部分硬盘只需要调用API就可以了这些都由操作系统自行分配和管理。
其实操作系统最重要的事情,就是调度。因此,在内核态就产生了这些模块:进程管理子系统、内存管理子系统、文件子系统、设备子系统和网络子系统。
这些模块通过统一的API也就是系统调用对上提供服务。基于这些API用户态有很多的工具可以帮我们使用好Linux操作系统比如用户管理、软件安装、软件运行、周期性进程、文件管理、网络管理和存储管理。
但是到目前为止,我们能管理的还是少数几台机器。当我们面临数据中心成千上万台机器的时候,仍然非常“痛苦”。如果我们运维数据中心依然像运维一台台物理机的前辈一样,天天关心哪个程序放在了哪台机器上,使用多少内存、多少硬盘,每台机器总共有多少内存、多少硬盘,还剩多少内存和硬盘,那头就大了。
因而对应到数据中心我们也需要一个调度器将运维人员从指定物理机或者虚拟机的痛苦中解放出来实现对于物理资源的统一管理这就是Kubernetes。
Kubernetes究竟有哪些功能可以解放运维人员呢为什么它能做数据中心的操作系统呢
我列了两个表格将操作系统的功能和模块与Kubernetes的功能和模块做了一个对比你可以看看。
<img src="https://static001.geekbang.org/resource/image/49/47/497c8c2c0cb193e0380ed1d7c82ac147.jpeg" alt="">
Kubernetes作为数据中心的操作系统还是主要管理数据中心里面的四种硬件资源CPU、内存、存储、网络。
对于CPU和内存这两种计算资源的管理我们可以通过Docker技术完成。它可以将CPU和内存资源通过namespace和cgroup从大的资源池里面隔离出来并通过镜像技术实现计算资源在数据中心里面的自由漂移。
就像我们上面说的一样没有操作系统的时候汇编程序员需要指定程序运行的CPU和内存物理地址。同理数据中心的管理员原来还需要指定程序运行的服务器及使用的CPU和内存。现在Kubernetes里面有一个调度器Scheduler你只需要告诉它你想运行10个4核8G的Java程序它会自动帮你选择空闲的、有足够资源的服务器去运行这些程序。
对于操作系统上的进程来讲有主线程做主要的工作还有其它线程做辅助的工作。对于数据中心里面的运行的程序来讲也会有一个主要提供服务的程序例如上面的Java程序也会有一些提供辅助功能的程序例如监控、环境预设值等。Kubernetes将多个Docker组装成一个Pod的概念。在一个Pod里面往往有一个Docker为主多个Docker为辅。
操作系统上的进程会在CPU上切换来切换去它使用的内存也会换入换出。在数据中心里面这些运行中的程序能不能在机器之间迁移呢能不能在一台服务器故障的时候选择其它的服务器运行呢反正我关心的是运行10个4核8G的Java程序又不在乎它在哪台上运行。
Kubernetes里面有Controller的概念可以控制Pod们的运行状态以及占用的资源。如果10个变9个了就选一台机器添加一个如果10个变11个了就随机删除一个。
操作系统上的进程有时候有亲和性的要求比如它可能希望在某一个CPU上运行不切换CPU从而提高运行效率。或者两个线程要求在一个CPU上从而可以使用Per CPU变量不加锁交互和协作比较方便。有的时候一个线程想避开另一个线程不要共用CPU以防相互干扰。Kubernetes的Scheduler也是有亲和性功能的你可以选择两个Pod永远运行在一台物理机上这样本地通信就非常方便了你也可以选择两个Pod永远不要运行在同一台物理机上这样一个挂了不影响另一个。
你可能会问Docker可以将CPU内存资源进行抽象在服务器之间迁移那数据应该怎么办呢如果数据放在每一台服务器上其实就像散落在汪洋大海里面用的时候根本找不到所以必须要有统一的存储。正像一台操作系统上多个进程之间要通过文件系统保存持久化的数据并且实现共享在数据中心里面也需要一个这样的基础设施。
统一的存储常常有三种形式,我们分别来看。
第一种方式是**对象存储**。
顾名思义这种方式是将文件作为一个完整对象的方式来保存。每一个文件对我们来说都应该有一个唯一标识这个对象的key而文件的内容就是value。对象可以分门别类地保存在一个叫作存储空间Bucket的地方有点儿像文件夹。
对于任何一个文件对象我们都可以通过HTTP RESTful API来远程获取对象。由于是简单的key-value模式当需要保存大容量数据的时候我们就比较容易根据唯一的key进行横向扩展所以对象存储能够容纳的数据量往往非常大。在数据中心里面保存文档、视频等是很好的方式当然缺点就是你没办法像操作文件一样操作它而是要将value当成整个的来对待。
第二种方式是**分布式文件系统**。
这种是最容易习惯的,因为使用它和使用本地的文件系统几乎没有什么区别,只不过是通过网络的方式访问远程的文件系统。多个容器能看到统一的文件系统,一个容器写入文件系统,另一个容器能够看到,可以实现共享。缺点是分布式文件系统的性能和规模是个矛盾,规模一大性能就难以保证,性能好则规模不会很大,所以不像对象存储一样能够保持海量的数据。
第三种方式是**分布式块存储**。
这就相当于云硬盘,也即存储虚拟化的方式,只不过将盘挂载给容器而不是虚拟机。块存储没有分布式文件系统这一层,一旦挂载到某一个容器,可以有本地的文件系统,这样做的缺点是,一般情况下,不同容器挂载的块存储都是不共享的,好处是在同样规模的情况下,性能相对分布式文件系统要好。如果为了解决一个容器从一台服务器迁移到另一台服务器,如何保持数据存储的问题,块存储是一个很好的选择。它不用解决多个容器共享数据的问题。
这三种形式对象存储使用HTTP进行访问当然任何容器都能访问到不需要Kubernetes去管理它。而分布式文件系统和分布式块存储就需要对接到Kubernetes让Kubernetes可以管理它们。如何对接呢Kubernetes提供Container Storage InterfaceCSI接口这是一个标准接口不同的存储可以实现这个接口来对接Kubernetes。是不是特别像设备驱动程序呀操作系统只要定义统一的接口不同的存储设备的驱动实现这些接口就能被操作系统使用了。
存储的问题解决了接下来是网络。因为不同的服务器上的Docker还是需要互相通信的。
Kubernetes有自己的网络模型里面是这样规定的。
1.IP-per-Pod每个 Pod 都拥有一个独立 IP 地址Pod 内所有容器共享一个网络命名空间。<br>
2.集群内所有 Pod 都在一个直接连通的扁平网络中,可通过 IP 直接访问。
- 所有容器之间无需 NAT 就可以直接互相访问。
- 所有 Node 和所有容器之间无需 NAT 就可以直接互相访问。
- 容器自己看到的 IP 跟其它容器看到的一样。
这其实是说里面的每一个Docker访问另一个Docker的时候都是感觉在一个扁平的网络里面。
要实现这样的网络模型有很多种方式例如Kubernetes自己提供Calico、Flannel。当然也可以对接Openvswitch这样的虚拟交换机也可以使用brctl这种传统的桥接模式也可以对接硬件交换机。
这又是一种类似驱动的模式和操作系统面临的问题是一样的。Kubernetes同样是提供统一的接口Container Network InterfaceCNI容器网络接口。无论你用哪种方式实现网络模型只要对接这个统一的接口Kubernetes就可以管理容器的网络。
至此Kubernetes作为数据中心的操作系统内核的问题解决了。
接下来是用户态的工作模式问题了。我们能不能像操作一台服务器那样操作数据中心呢?
使用操作系统需要安装一些软件于是我们需要yum之类的包管理系统使得软件的使用者和软件的编译者分隔开来软件的编译者需要知道这个软件需要安装哪些包包之间的依赖关系是什么软件安装到什么地方而软件的使用者仅仅需要yum install就可以了。Kubernetes就有这样一套包管理软件Helm你可以用它来很方便地安装、升级、扩容一些数据中心里面的常用软件例如数据库、缓存、消息队列。
使用操作系统,运行一个进程是最常见的需求。第一种进程是**交互式命令行**运行起来就是执行一个任务结束了马上返回结果。在Kubernetes里面有对应的概念叫作JobJob 负责批量处理短暂的一次性任务 (Short Lived One-off Tasks),即仅执行一次的任务,它保证批处理任务的一个或多个 Pod 成功结束。
第二种进程是**nohup长期运行**的进程。在Kubernetes里对应的概念是Deployment使用 Deployment 来创建 ReplicaSet。ReplicaSet 在后台创建 Pod。也即Doployment里面会声明我希望某个进程以N的Pod副本的形式运行并且长期运行一旦副本变少就会自动添加。
第三种进程是**系统服务**。在Kubernetes里面对应的概念是DaemonSet它保证在每个节点上都运行一个容器副本常用来部署一些集群的日志、监控或者其他系统管理应用。
第四种进程是**周期性进程**也即Crontab常常用来设置一些周期性的任务。在Kubernetes里面对应的概念是CronJob定时任务就类似于 Linux 系统的 Crontab在指定的时间周期运行指定的任务。
使用操作系统我们还需使用文件系统或者使用网络发送数据。虽然在Kubernetes里面有CSI和CNI来对接存储和网络但是在用户态不能让用户意识到后面具体设备而是应该有抽象的概念。
对于存储来讲Kubernetes有Volume的概念。Kubernetes Volume 的生命周期与 Pod 绑定在一起容器挂掉后Kubelet 再次重启容器时Volume 的数据依然还在,而 Pod 删除时Volume 才会真的被清理。数据是否丢失取决于具体的 Volume 类型。Volume的概念是对具体存储设备的抽象就像当我们使用ext4文件系统时不用管它是基于什么硬盘一样。
对于网络来讲Kubernetes有自己的DNS有Service的概念。Kubernetes Service是一个 Pod 的逻辑分组,这一组 Pod 能够被 Service 访问。每一个Service都一个名字Kubernetes会将Service的名字作为域名解析成为一个虚拟的Cluster IP然后通过负载均衡转发到后端的Pod。虽然Pod可能漂移IP会变但是Service会一直不变。
对应到Linux操作系统的iptablesKubernetes 在有个概念叫Network PolicyNetwork Policy 提供了基于策略的网络控制,用于隔离应用并减少攻击面。它使用标签选择器模拟传统的分段网络,并通过策略控制它们之间的流量以及来自外部的流量。
是不是很神奇有了Kubernetes我们就能像管理一台Linux服务器那样去管理数据中心了。
如果想深入了解Kubernetes这个数据中心的操作系统你可以订阅极客时间的专栏“[深入剖析Kubernetes](https://time.geekbang.org/column/article/114197)”。
## 总结时刻
下面,你可以对照着这个图,来总结一下这个数据中心操作系统的功能。
<img src="https://static001.geekbang.org/resource/image/1a/e5/1a8450f1fcda83b75c9ba301ebf9fbe5.jpg" alt="">
## 课堂练习
Kubernetes有一个简单的版本你可以按照官方文档用一台虚拟机安装一个试验一下。
欢迎留言和我分享你的疑惑和见解,也欢迎收藏本节内容,反复研读。你也可以把今天的内容分享给你的朋友,和他一起学习和进步。
<img src="https://static001.geekbang.org/resource/image/8c/37/8c0a95fa07a8b9a1abfd394479bdd637.jpg" alt="">