mirror of
https://github.com/cheetahlou/CategoryResourceRepost.git
synced 2025-11-17 06:33:48 +08:00
mod
This commit is contained in:
344
极客时间专栏/趣谈Linux操作系统/核心原理篇:第十部分 容器化/56 | 容器:大公司为保持创新,鼓励内部创业.md
Normal file
344
极客时间专栏/趣谈Linux操作系统/核心原理篇:第十部分 容器化/56 | 容器:大公司为保持创新,鼓励内部创业.md
Normal 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操作系统中,有一项新的技术,称为容器,它就可以做到这一点。
|
||||
|
||||
容器的英文叫Container,Container的另一个意思是“集装箱”。其实容器就像船上的不同的集装箱装着不同的货物,有一定的隔离,但是隔离性又没有那么好,仅仅做简单的封装。当然封装也带来了好处,一个是打包,二是标准。
|
||||
|
||||
在没有集装箱的时代,假设我们要将货物从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="Ubuntu 14.04.6 LTS"
|
||||
|
||||
```
|
||||
|
||||
如果我们想尝试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 "nginx -g 'daemon of…" 2 minutes ago Up 2 minutes 0.0.0.0:8080->80/tcp modest_payne
|
||||
|
||||
# curl http://localhost:8080
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Welcome to nginx!</title>
|
||||
|
||||
```
|
||||
|
||||
这次nginx镜像运行的方式和操作系统不太一样,一个是-d,因为它是一个应用,不需要像操作系统那样有交互命令行,而是以后台方式运行,-d就是daemon的意思。
|
||||
|
||||
另外一个就是端口-p 8080:80。容器这么容易启动,每台机器上可以启动N个nginx。大家都监听80端口,不就冲突了吗?所以我们要设置端口,冒号后面的80是容器内部环境监听的端口,冒号前面的8080是宿主机上监听的端口。
|
||||
|
||||
一旦容器启动起来之后,通过docker ps就可以查看都有哪些容器正在运行。
|
||||
|
||||
接下来,我们通过curl命令,访问本机的8080端口,可以打印出nginx的欢迎页面。
|
||||
|
||||
docker run一下,应用就启动起来了,是不是非常方便?nginx是已经有人打包好的容器镜像,放在公共的镜像仓库里面。如果是你自己开发的应用,应该如何打包成为镜像呢?
|
||||
|
||||
因为Java代码比较麻烦,我们这里举一个简单的例子。假设你自己写的HTML的文件就是代码。
|
||||
|
||||
```
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Welcome to nginx Test 7!</title>
|
||||
<style>
|
||||
body {
|
||||
width: 35em;
|
||||
margin: 0 auto;
|
||||
font-family: Tahoma, Verdana, Arial, sans-serif;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Test 7</h1>
|
||||
<p>If you see this page, the nginx web server is successfully installed and
|
||||
working. Further configuration is required.</p>
|
||||
<p>For online documentation and support please refer to
|
||||
<a href="http://nginx.org/">nginx.org</a>.<br/>
|
||||
Commercial support is available at
|
||||
<a href="http://nginx.com/">nginx.com</a>.</p>
|
||||
<p><em>Thank you for using nginx.</em></p>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
```
|
||||
|
||||
那我们如何将这些代码放到容器镜像里面呢?要通过Dockerfile,Dockerfile的格式应该包含下面的部分:
|
||||
|
||||
- FROM 基础镜像
|
||||
- RUN 运行过的所有命令
|
||||
- COPY 拷贝到容器中的资源
|
||||
- ENTRYPOINT 前台启动的命令或者脚本
|
||||
|
||||
按照上面说的格式,可以有下面的Dockerfile。
|
||||
|
||||
```
|
||||
FROM ubuntu:14.04
|
||||
RUN echo "deb http://archive.ubuntu.com/ubuntu trusty main restricted universe multiverse" > /etc/apt/sources.list
|
||||
RUN echo "deb http://archive.ubuntu.com/ubuntu trusty-updates main restricted universe multiverse" >> /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 "daemon off;"
|
||||
|
||||
```
|
||||
|
||||
将代码、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 "/bin/sh -c 'nginx -…" 2 seconds ago Up 2 seconds 0.0.0.0:8081->80/tcp youthful_torvalds
|
||||
73ff0c8bea6e nginx "nginx -g 'daemon of…" 33 minutes ago Up 33 minutes 0.0.0.0:8080->80/tcp modest_payne
|
||||
|
||||
```
|
||||
|
||||
我们再来访问我们在nginx里面写的代码。
|
||||
|
||||
```
|
||||
[root@deployer nginx]# curl http://localhost:8081/test.html
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Welcome to nginx Test 7!</title>
|
||||
|
||||
```
|
||||
|
||||
看,我们的代码已经运行起来了。是不是很酷?
|
||||
|
||||
其实这种运行方式有更加酷的功能。
|
||||
|
||||
第一就是持续集成。
|
||||
|
||||
想象一下,你写了一个程序,然后把它打成了上面一样的镜像。你在本地一运行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网站程序,并进行访问。
|
||||
|
||||
欢迎留言和我分享你的疑惑和见解,也欢迎收藏本节内容,反复研读。你也可以把今天的内容分享给你的朋友,和他一起学习和进步。
|
||||
@@ -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 "/bin/sh -c 'nginx -…" 17 hours ago Up 17 hours 0.0.0.0:8081->80/tcp youthful_torvalds
|
||||
|
||||
```
|
||||
|
||||
我们可以看这个容器对应的entrypoint的pid。通过docker inspect命令,可以看到,进程号为58212。
|
||||
|
||||
```
|
||||
[root@deployer ~]# docker inspect f604f0e34bc2
|
||||
[
|
||||
{
|
||||
"Id": "f604f0e34bc263bc32ba683d97a1db2a65de42ab052da16df3c7811ad07f0dc3",
|
||||
"Created": "2019-07-15T17:43:44.158300531Z",
|
||||
"Path": "/bin/sh",
|
||||
"Args": [
|
||||
"-c",
|
||||
"nginx -g \"daemon off;\""
|
||||
],
|
||||
"State": {
|
||||
"Status": "running",
|
||||
"Running": true,
|
||||
"Pid": 58212,
|
||||
"ExitCode": 0,
|
||||
"StartedAt": "2019-07-15T17:43:44.651756682Z",
|
||||
"FinishedAt": "0001-01-01T00:00:00Z"
|
||||
},
|
||||
......
|
||||
"Name": "/youthful_torvalds",
|
||||
"RestartCount": 0,
|
||||
"Driver": "overlay2",
|
||||
"Platform": "linux",
|
||||
"HostConfig": {
|
||||
"NetworkMode": "default",
|
||||
"PortBindings": {
|
||||
"80/tcp": [
|
||||
{
|
||||
"HostIp": "",
|
||||
"HostPort": "8081"
|
||||
}
|
||||
]
|
||||
},
|
||||
......
|
||||
},
|
||||
"Config": {
|
||||
"Hostname": "f604f0e34bc2",
|
||||
"ExposedPorts": {
|
||||
"80/tcp": {}
|
||||
},
|
||||
"Image": "testnginx:1",
|
||||
"Entrypoint": [
|
||||
"/bin/sh",
|
||||
"-c",
|
||||
"nginx -g \"daemon off;\""
|
||||
],
|
||||
},
|
||||
"NetworkSettings": {
|
||||
"Bridge": "",
|
||||
"SandboxID": "7fd3eb469578903b66687090e512958658ae28d17bce1a7cee2da3148d1dfad4",
|
||||
"Ports": {
|
||||
"80/tcp": [
|
||||
{
|
||||
"HostIp": "0.0.0.0",
|
||||
"HostPort": "8081"
|
||||
}
|
||||
]
|
||||
},
|
||||
"Gateway": "172.17.0.1",
|
||||
"IPAddress": "172.17.0.3",
|
||||
"IPPrefixLen": 16,
|
||||
"MacAddress": "02:42:ac:11:00:03",
|
||||
"Networks": {
|
||||
"bridge": {
|
||||
"NetworkID": "c8eef1603afb399bf17af154be202fd1e543d3772cc83ef4a1ca3f97b8bd6eda",
|
||||
"EndpointID": "8d9bb18ca57889112e758ede193d2cfb45cbf794c9d952819763c08f8545da46",
|
||||
"Gateway": "172.17.0.1",
|
||||
"IPAddress": "172.17.0.3",
|
||||
"IPPrefixLen": 16,
|
||||
"MacAddress": "02:42:ac:11:00:03",
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
```
|
||||
|
||||
如果我们用ps查看机器上的nginx进程,可以看到master和worker,worker的父进程是master。
|
||||
|
||||
```
|
||||
# ps -ef |grep nginx
|
||||
root 58212 58195 0 01:43 ? 00:00:00 /bin/sh -c nginx -g "daemon off;"
|
||||
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 -> ipc:[4026532278]
|
||||
lrwxrwxrwx 1 root root 0 Jul 16 19:19 mnt -> mnt:[4026532276]
|
||||
lrwxrwxrwx 1 root root 0 Jul 16 01:43 net -> net:[4026532281]
|
||||
lrwxrwxrwx 1 root root 0 Jul 16 19:19 pid -> pid:[4026532279]
|
||||
lrwxrwxrwx 1 root root 0 Jul 16 19:19 user -> user:[4026531837]
|
||||
lrwxrwxrwx 1 root root 0 Jul 16 19:19 uts -> uts:[4026532277]
|
||||
|
||||
# ls -l /proc/58253/ns
|
||||
lrwxrwxrwx 1 33 tape 0 Jul 16 19:20 ipc -> ipc:[4026532278]
|
||||
lrwxrwxrwx 1 33 tape 0 Jul 16 19:20 mnt -> mnt:[4026532276]
|
||||
lrwxrwxrwx 1 33 tape 0 Jul 16 19:20 net -> net:[4026532281]
|
||||
lrwxrwxrwx 1 33 tape 0 Jul 16 19:20 pid -> pid:[4026532279]
|
||||
lrwxrwxrwx 1 33 tape 0 Jul 16 19:20 user -> user:[4026531837]
|
||||
lrwxrwxrwx 1 33 tape 0 Jul 16 19:20 uts -> 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: <LOOPBACK,UP,LOWER_UP> 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: <BROADCAST,MULTICAST,UP,LOWER_UP> 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: <LOOPBACK> 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是使当前进程加入新的namespace;clone是创建一个新的子进程,然后让子进程加入新的namespace,而当前进程保持不变。
|
||||
|
||||
这里我们尝试一下,通过clone函数来进入一个namespace。
|
||||
|
||||
```
|
||||
#define _GNU_SOURCE
|
||||
#include <sys/wait.h>
|
||||
#include <sys/utsname.h>
|
||||
#include <sched.h>
|
||||
#include <string.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <unistd.h>
|
||||
#define STACK_SIZE (1024 * 1024)
|
||||
|
||||
static int childFunc(void *arg)
|
||||
{
|
||||
printf("In child process.\n");
|
||||
execlp("bash", "bash", (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("malloc");
|
||||
exit(1);
|
||||
}
|
||||
stackTop = stack + STACK_SIZE;
|
||||
|
||||
pid = clone(childFunc, stackTop, CLONE_NEWNS|CLONE_NEWPID|CLONE_NEWNET|SIGCHLD, NULL);
|
||||
if (pid == -1)
|
||||
{
|
||||
perror("clone");
|
||||
exit(1);
|
||||
}
|
||||
printf("clone() returned %ld\n", (long) pid);
|
||||
|
||||
sleep(1);
|
||||
|
||||
if (waitpid(pid, NULL, 0) == -1)
|
||||
{
|
||||
perror("waitpid");
|
||||
exit(1);
|
||||
}
|
||||
printf("child has terminated\n");
|
||||
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: <LOOPBACK> 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->copy_process->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 = &init_uts_ns,
|
||||
#if defined(CONFIG_POSIX_MQUEUE) || defined(CONFIG_SYSVIPC)
|
||||
.ipc_ns = &init_ipc_ns,
|
||||
#endif
|
||||
.mnt_ns = NULL,
|
||||
.pid_ns_for_children = &init_pid_ns,
|
||||
#ifdef CONFIG_NET
|
||||
.net_ns = &init_net,
|
||||
#endif
|
||||
#ifdef CONFIG_CGROUPS
|
||||
.cgroup_ns = &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->nsproxy;
|
||||
struct user_namespace *user_ns = task_cred_xxx(tsk, user_ns);
|
||||
struct nsproxy *new_ns;
|
||||
|
||||
if (likely(!(flags & (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->fs);
|
||||
|
||||
tsk->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->mnt_ns = copy_mnt_ns(flags, tsk->nsproxy->mnt_ns, user_ns, new_fs);
|
||||
......
|
||||
new_nsp->uts_ns = copy_utsname(flags, user_ns, tsk->nsproxy->uts_ns);
|
||||
......
|
||||
new_nsp->ipc_ns = copy_ipcs(flags, user_ns, tsk->nsproxy->ipc_ns);
|
||||
......
|
||||
new_nsp->pid_ns_for_children =
|
||||
copy_pid_ns(flags, user_ns, tsk->nsproxy->pid_ns_for_children);
|
||||
......
|
||||
new_nsp->cgroup_ns = copy_cgroup_ns(flags, user_ns,
|
||||
tsk->nsproxy->cgroup_ns);
|
||||
......
|
||||
new_nsp->net_ns = copy_net_ns(flags, user_ns, tsk->nsproxy->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 & 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 & CLONE_NEWNET))
|
||||
return get_net(old_net);
|
||||
|
||||
ucounts = inc_net_namespaces(user_ns);
|
||||
......
|
||||
net = net_alloc();
|
||||
......
|
||||
get_user_ns(user_ns);
|
||||
net->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(&net->count, 1);
|
||||
refcount_set(&net->passive, 1);
|
||||
net->dev_base_seq = 1;
|
||||
net->user_ns = user_ns;
|
||||
idr_init(&net->netns_ids);
|
||||
spin_lock_init(&net->nsid_lock);
|
||||
|
||||
list_for_each_entry(ops, &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(&loopback_net_ops)
|
||||
|
||||
int register_pernet_device(struct pernet_operations *ops)
|
||||
{
|
||||
int error;
|
||||
mutex_lock(&net_mutex);
|
||||
error = register_pernet_operations(&pernet_list, ops);
|
||||
if (!error && (first_device == &pernet_list))
|
||||
first_device = &ops->list;
|
||||
mutex_unlock(&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, "lo", NET_NAME_UNKNOWN, loopback_setup);
|
||||
......
|
||||
dev_net_set(dev, net);
|
||||
err = register_netdev(dev);
|
||||
......
|
||||
net->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="">
|
||||
@@ -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 "/bin/sh -c 'nginx -…" About a minute ago Up About a minute 0.0.0.0:8081->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 -> cpu,cpuacct
|
||||
lrwxrwxrwx 1 root root 11 May 30 17:00 cpuacct -> 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 -> 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 -> 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->id = i;
|
||||
ss->name = cgroup_subsys_name[i];
|
||||
......
|
||||
cgroup_init_subsys(ss, true);
|
||||
}
|
||||
|
||||
#define for_each_subsys(ss, ssid) \
|
||||
for ((ssid) = 0; (ssid) < CGROUP_SUBSYS_COUNT && \
|
||||
(((ss) = cgroup_subsys[ssid]) || true); (ssid)++)
|
||||
|
||||
```
|
||||
|
||||
for_each_subsys会在cgroup_subsys数组中进行循环。这个cgroup_subsys数组是如何形成的呢?
|
||||
|
||||
```
|
||||
#define SUBSYS(_x) [_x ## _cgrp_id] = &_x ## _cgrp_subsys,
|
||||
struct cgroup_subsys *cgroup_subsys[] = {
|
||||
#include <linux/cgroup_subsys.h>
|
||||
};
|
||||
#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] = &cpu_cgrp_subsys,而SUBSYS(memory)其实是[memory_cgrp_id] = &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(&ss->css_idr);
|
||||
INIT_LIST_HEAD(&ss->cfts);
|
||||
|
||||
/* Create the root cgroup state for this subsystem */
|
||||
ss->root = &cgrp_dfl_root;
|
||||
css = ss->css_alloc(cgroup_css(&cgrp_dfl_root.cgrp, ss));
|
||||
......
|
||||
init_and_link_css(css, ss, &cgrp_dfl_root.cgrp);
|
||||
......
|
||||
css->id = cgroup_idr_alloc(&ss->css_idr, css, 1, 2, GFP_KERNEL);
|
||||
init_css_set.subsys[ss->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 "owned" 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->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->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 = "tasks",
|
||||
.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 = "cgroup",
|
||||
.mount = cgroup_mount,
|
||||
.kill_sb = cgroup_kill_sb,
|
||||
.fs_flags = FS_USERNS_MOUNT,
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
当我们mount这个cgroup文件系统的时候,会调用cgroup_mount->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, &opts);
|
||||
|
||||
ret = cgroup_setup_root(root, opts.subsys_mask, PERCPU_REF_INIT_DEAD);
|
||||
......
|
||||
dentry = cgroup_do_mount(&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 = &root->cgrp;
|
||||
struct kernfs_syscall_ops *kf_sops;
|
||||
struct css_set *cset;
|
||||
int i, ret;
|
||||
|
||||
root->kf_root = kernfs_create_root(kf_sops,
|
||||
KERNFS_ROOT_CREATE_DEACTIVATED,
|
||||
root_cgrp);
|
||||
root_cgrp->kn = root->kf_root->kn;
|
||||
|
||||
ret = css_populate_dir(&root_cgrp->self);
|
||||
ret = rebind_subsystems(root, ss_mask);
|
||||
......
|
||||
list_add(&root->root_list, &cgroup_roots);
|
||||
cgroup_root_count++;
|
||||
......
|
||||
kernfs_activate(root_cgrp->kn);
|
||||
......
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
就像在普通文件系统上,每一个文件都对应一个inode,在cgroup文件系统上,每个文件都对应一个struct kernfs_node结构,当然kernfs_root作为文件系的根也对应一个kernfs_node结构。
|
||||
|
||||
接下来,css_populate_dir会调用cgroup_addrm_files->cgroup_add_file->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->kn, cgroup_file_name(cgrp, cft, name),
|
||||
cgroup_file_mode(cft), 0, cft->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 & S_IALLUGO) | S_IFREG, flags);
|
||||
|
||||
kn->attr.ops = ops;
|
||||
kn->attr.size = size;
|
||||
kn->ns = ns;
|
||||
kn->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 = "shares",
|
||||
.read_u64 = cpu_shares_read_u64,
|
||||
.write_u64 = cpu_shares_write_u64,
|
||||
},
|
||||
#endif
|
||||
#ifdef CONFIG_CFS_BANDWIDTH
|
||||
{
|
||||
.name = "cfs_quota_us",
|
||||
.read_s64 = cpu_cfs_quota_read_s64,
|
||||
.write_s64 = cpu_cfs_quota_write_s64,
|
||||
},
|
||||
{
|
||||
.name = "cfs_period_us",
|
||||
.read_u64 = cpu_cfs_period_read_u64,
|
||||
.write_u64 = cpu_cfs_period_write_u64,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
static struct cftype mem_cgroup_legacy_files[] = {
|
||||
{
|
||||
.name = "usage_in_bytes",
|
||||
.private = MEMFILE_PRIVATE(_MEM, RES_USAGE),
|
||||
.read_u64 = mem_cgroup_read_u64,
|
||||
},
|
||||
{
|
||||
.name = "max_usage_in_bytes",
|
||||
.private = MEMFILE_PRIVATE(_MEM, RES_MAX_USAGE),
|
||||
.write = mem_cgroup_reset,
|
||||
.read_u64 = mem_cgroup_read_u64,
|
||||
},
|
||||
{
|
||||
.name = "limit_in_bytes",
|
||||
.private = MEMFILE_PRIVATE(_MEM, RES_LIMIT),
|
||||
.write = mem_cgroup_write,
|
||||
.read_u64 = mem_cgroup_read_u64,
|
||||
},
|
||||
{
|
||||
.name = "soft_limit_in_bytes",
|
||||
.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->shares = shares;
|
||||
for_each_possible_cpu(i) {
|
||||
struct rq *rq = cpu_rq(i);
|
||||
struct sched_entity *se = tg->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->__cgroup_procs_write->cgroup_attach_task-> cgroup_migrate->cgroup_migrate_execute。将这个进程和一个cgroup关联起来,也即将这个进程迁移到这个cgroup下面。
|
||||
|
||||
```
|
||||
static int cgroup_migrate_execute(struct cgroup_mgctx *mgctx)
|
||||
{
|
||||
struct cgroup_taskset *tset = &mgctx->tset;
|
||||
struct cgroup_subsys *ss;
|
||||
struct task_struct *task, *tmp_task;
|
||||
struct css_set *cset, *tmp_cset;
|
||||
......
|
||||
if (tset->nr_tasks) {
|
||||
do_each_subsys_mask(ss, ssid, mgctx->ss_mask) {
|
||||
if (ss->attach) {
|
||||
tset->ssid = ssid;
|
||||
ss->attach(tset);
|
||||
}
|
||||
} while_each_subsys_mask();
|
||||
}
|
||||
......
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
每一个cgroup子系统会调用相应的attach函数。而CPU调用的是cpu_cgroup_attach-> sched_move_task-> 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->sched_task_group = tg;
|
||||
|
||||
#ifdef CONFIG_FAIR_GROUP_SCHED
|
||||
if (tsk->sched_class->task_change_group)
|
||||
tsk->sched_class->task_change_group(tsk, type);
|
||||
else
|
||||
#endif
|
||||
set_task_rq(tsk, task_cpu(tsk));
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
在sched_change_group中设置这个进程以这个task_group的方式参与调度,从而使得上面的cpu.shares起作用。
|
||||
|
||||
对于内存来讲,写入内存的限制使用函数mem_cgroup_write->mem_cgroup_resize_limit来设置struct mem_cgroup的memory.limit成员。
|
||||
|
||||
在进程执行过程中,申请内存的时候,我们会调用handle_pte_fault->do_anonymous_page()->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,请你研究一下。
|
||||
|
||||
欢迎留言和我分享你的疑惑和见解,也欢迎收藏本节内容,反复研读。你也可以把今天的内容分享给你的朋友,和他一起学习和进步。
|
||||
112
极客时间专栏/趣谈Linux操作系统/核心原理篇:第十部分 容器化/59 | 数据中心操作系统:上市敲钟.md
Normal file
112
极客时间专栏/趣谈Linux操作系统/核心原理篇:第十部分 容器化/59 | 数据中心操作系统:上市敲钟.md
Normal 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 Interface(CSI)接口,这是一个标准接口,不同的存储可以实现这个接口来对接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 Interface(CNI,容器网络接口)。无论你用哪种方式实现网络模型,只要对接这个统一的接口,Kubernetes就可以管理容器的网络。
|
||||
|
||||
至此,Kubernetes作为数据中心的操作系统,内核的问题解决了。
|
||||
|
||||
接下来是用户态的工作模式问题了。我们能不能像操作一台服务器那样操作数据中心呢?
|
||||
|
||||
使用操作系统,需要安装一些软件,于是,我们需要yum之类的包管理系统,使得软件的使用者和软件的编译者分隔开来,软件的编译者需要知道这个软件需要安装哪些包,包之间的依赖关系是什么,软件安装到什么地方,而软件的使用者仅仅需要yum install就可以了。Kubernetes就有这样一套包管理软件Helm,你可以用它来很方便地安装、升级、扩容一些数据中心里面的常用软件,例如数据库、缓存、消息队列。
|
||||
|
||||
使用操作系统,运行一个进程是最常见的需求。第一种进程是**交互式命令行**,运行起来就是执行一个任务,结束了马上返回结果。在Kubernetes里面有对应的概念叫作Job,Job 负责批量处理短暂的一次性任务 (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操作系统的iptables,Kubernetes 在有个概念叫Network Policy,Network 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="">
|
||||
Reference in New Issue
Block a user