Linux容器化原理笔记

一、容器

1. 从一台物理机虚拟化出很多虚拟机这种方式,一定程度上实现了资源创建的灵活性。但是同时会发现,虚拟化的方式还是非常复杂的, CPU、内存、网络、硬盘全部需要虚拟化,还有性能损失。那有没有一种更加灵活的方式,既可以隔离出一部分资源,专门用于某个进程,又不需要费劲周折的虚拟化这么多的硬件呢?毕竟最终只想跑一个程序,而不是要一整个Linux系统。

在Linux操作系统中,有一项新的技术称为容器,就可以做到这一点。容器实现封闭的环境主要要靠两种技术,一种是看起来隔离的技术,称为namespace(命名空间),在每个namespace中的应用看到的都是不同的IP地址、用户空间、进程ID等。另一种是用起来隔离的技术,称为cgroup(网络资源限制),即明明整台机器有很多的 CPU、内存,但是一个应用只能用其中的一部分。

有了这两项技术,就相当于焊好了互相隔离的集装箱。接下来的问题就是如何“将这些集装箱标准化”,在哪艘船上都能运输。这里就要用到镜像(Image),就是在焊好集装箱的那一刻,将集装箱的状态保存下来,集装箱里的状态就被“定”在了那一刻,然后这一刻的状态会被保存成一系列文件。无论在哪里运行这个镜像,都能完整地还原当时的情况。当程序开发完毕之后,可以将代码连同运行环境打包成一个容器镜像,接下来无论是在开发环境、测试环境,还是生产环境运行代码,都可以使用相同的镜像,这样产品的发布和上线速度就加快了。

对于目前主流的容器实现Docker安装,第一步是删除原有版本的 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

2. 容器的运行需要一个镜像,这是容器封装的一个环境,最基础的容器环境就是操作系统,可以在一台虚拟机上创建任意的操作系统环境,而不需要开几个虚拟机占据很多内存,例如需要开发和测试一个基于Ubuntu 14.04的命令,那就不需要费劲去寻找安装一个这么老版本的虚拟机,只要去dockerhub网站上搜一搜对应的镜像,然后根据命令下载这个镜像就可以了,如下所示:

# 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了,并且直接可以访问了,如下所示:

# 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的欢迎页面。

3. docker run一下,应用就启动起来了, nginx是已经有人打包好的容器镜像,放在公共的镜像仓库里面。如果是自己开发的应用,应该如何打包成为镜像呢?这里举一个简单的例子,假设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的格式应该包含下面的部分:

(1)FROM,基础镜像。

(2)RUN,运行过的所有命令。

(3)COPY,拷贝到容器中的资源。

(4)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;"

将HTML代码、Dockerfile、脚本都放在一个文件夹下,现在编译这个Dockerfile:

[nginx]# ls
Dockerfile  test.html

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

编译过后,就有了一个新的镜像叫testnginx1,如下所示:

# 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里面写的HTML代码:

[root@deployer nginx]# curl http://localhost:8081/test.html
<!DOCTYPE html>
<html>
  <head>
    <title>Welcome to nginx Test 7!</title>

4. docker的这种运行方式有以下几个优点:

(1)持续集成。例如写了一个程序,然后把它打成了上面一样的镜像,在本地运行docker run就把它运行起来了。接下来交给测试人员的就不是一个“程序包 + 配置 + 手册”了,而是一个容器镜像。测试小伙伴同样通过docker run也就运行起来了,不存在“你这里跑的起来,他那里跑不起来”的情况。测试完了再上生产,交给运维小伙伴也是这样一个镜像,同样的运行同样的顺畅。这种模式使得软件的交付效率大大提高,可以一天发布多次。

(2)弹性伸缩。例如写了一个程序平时用的人少,只需要10个副本就能够扛住了。突然有一天要做促销,需要100个副本,另外90台机器用云可以创建出来,但是里面90个副本的应用不可能一个个上去手动部署,有了容器就方便多了,只要在每台机器上docker run一下就部署完成了

(3)跨云迁移。如果不相信任何一个云,怕被一个云绑定,怕一个云挂了自己的应用也就挂了,由于容器镜像对于云是中立的,在这个云上docker run就在这个云上提供服务,等哪天想用另一个云了,不用怕应用迁移不走,只要在另外一个云上docker run一下就解决了

这就是容器的集装箱功能,就是namespace,即看起来隔离的作用。

既然多个容器运行在一台机器上,不会相互影响吗?如何限制CPU和内存的使用呢?Docker可以限制对于CPU的使用,可以分以下几种方式:

(1)Docker允许用户为每个容器设置一个数字,代表容器的CPU share,默认情况下每个容器的share是1024,这个数值是相对的,本身并不能代表任何确定的意义。当主机上有多个容器运行时,每个容器占用的CPU时间比例为它的share在总额中的比例。Docker为容器设置CPU share的参数是-c --cpu-shares。

(2)Docker提供了--cpus参数可以限定容器能使用的CPU核数。

(3)Docker可以通过--cpuset参数让容器只运行在某些核上。

Docker还能限制容器内存使用量,下面是一些具体的参数:

(1)-m/--memory:容器能使用的最大内存大小。

(2)-memory-swap:容器能够使用的swap大小。

(3)-memory-swappiness:默认情况下,主机可以把容器使用的匿名页swap出来,可以设置一个0-100之间的值,代表允许swap出来的比例。

(4)-memory-reservation:设置一个内存使用的soft limit,如果docker发现主机内存不足,会执行OOM(Out of Memory)操作,这个值必须小于--memory设置的值。

(5)-kernel-memory:容器能够使用的kernel memory大小。

(6)-oom-kill-disable:是否运行OOM的时候杀死容器。只有设置了-m才可以把这个选项设置为false,否则容器会耗尽主机内存,而且导致宿主机应用被杀死。

这就是cgroup,即用起来隔离的效果。

5. 来总结一下,无论是容器还是虚拟机都依赖于内核中的技术,虚拟机依赖的是KVM,容器依赖的是namespace和cgroup对进程进行隔离,如下图所示:

为了运行Docker,有一个daemon进程Docker Daemon用于接收命令行。为了描述Docker里面运行的环境和应用,有一个Dockerfile,通过build命令成为容器镜像。容器镜像可以上传到镜像仓库,也可以通过pull命令从镜像仓库中下载现成的容器镜像。通过docker run命令将容器镜像运行为容器,通过namespace和cgroup进行隔离,容器里面不包含内核,是共享宿主机的内核的,而虚拟机在qemu进程里面是有客户机内核的,应用运行在客户机的用户态。

二、namespace技术

6. 在容器技术中,为了隔离不同类型的资源,Linux内核里面实现了以下几种不同类型的namespace:

(1)UTS,对应的宏为CLONE_NEWUTS,表示不同namespace可以配置不同的hostname

(2)User,对应的宏为CLONE_NEWUSER,表示不同namespace可以配置不同的用户和组

(3)Mount,对应的宏为CLONE_NEWNS,表示不同namespace的文件系统挂载点是隔离的

(4)PID,对应的宏为 CLONE_NEWPID,表示不同namespace有完全独立的pid,即一个namespace的进程和另一个namespace的进程,pid可以是一样的,但是代表不同的进程

(5)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可以查看到机器上进程ID为58212的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(后面的号码相同),58253是58212的子进程,如下所示:

# 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]

7. 接下来看如何操作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地址现在只看到了loopback,进程也只看到了/bin/bash,如下所示:

# 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:(1)第一个函数是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中。

(2)第二个函数是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。

(3)第三个函数是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 $$的时候,就可以得到原来进程号。

8. clone系统调用在进程的创建部分解析过,当时没有看关于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网络设备

9. 上面讲了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。

三、cgroup技术

10. 前面说了容器实现封闭的环境主要靠两种技术,一种是“看起来是隔离”的技术Namespace,另一种是用起来是隔离的技术cgroup。上面讲了Namespace,这里就来看一下cgroup。cgroup全称是control group,顾名思义它是用来做“控制”的,控制的是资源的使用。首先cgroup定义了下面的一系列子系统,每个子系统用于控制某一类资源:

(1)CPU子系统,主要限制进程的CPU使用率。

(2)cpuacct子系统,可以统计cgroup中的进程的CPU使用报告。

(3)cpuset子系统,可以为cgroup中的进程分配单独的CPU节点或者内存节点。

(4)memory子系统,可以限制进程的Memory使用量。

(5)blkio子系统,可以限制进程的块设备IO。

(6)devices子系统,可以控制进程能够访问某些设备。

(7)net_cls子系统,可以标记cgroups中进程的网络数据包,然后可以使用tc模块(traffic control)对数据包进行控制。

(8)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,里面就是在前面docker run命令中设置的513,如下所示:

[3dc0601189dd218898f31f9526a6cfae83913763a4da59f95ec789c6e030ecfd]# cat cpu.shares
513

另外,还配置了cpus,这个值其实是由cpu.cfs_period_us和cpu.cfs_quota_us共同决定的。cpu.cfs_period_us是运行周期,cpu.cfs_quota_us是在周期内这些进程占用多少时间。前面docker run命令设置了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

11. 前面容器部分还讲了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,里面配置的就是前面docker run命令中的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资源的控制,在用户态是如何表现的,下面的图总结了一下:

12. 在内核中,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也是一个调度实体。

13. 接下来,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,
};

14. 在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);
    }
  }
......
}

15. 但是这个时候别忘了,还没有将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对于内存的限制才真正起作用

16. 内核中cgroup的工作机制,在这里总结一下,如下图所示:

(1)第一步,系统初始化时初始化cgroup的各个子系统的操作函数,分配各个子系统的数据结构。

(2)第二步,mount cgroup文件系统,创建文件系统的树形结构以及操作函数。

(3)第三步,写入cgroup文件,设置cpu或者memory的相关参数,这个时候文件系统的操作函数会调用到cgroup子系统的操作函数,从而将参数设置到cgroup子系统的数据结构中。

(4)第四步,写入tasks文件,将进程交给某个cgroup进行管理,因为tasks文件也是一个cgroup文件,统一会调用文件系统的操作函数,进而调用cgroup子系统的操作函数,将cgroup子系统的数据结构和进程关联起来。

(5)第五步,对于CPU来讲,会修改scheduled entity放入相应的队列里面去,从而下次调度的时候就起作用了。对于内存的cgroup设定,只有在申请内存的时候才起作用。

四、数据中心操作系统

17. 当面临数据中心成千上万台机器时,如果运维依然只是手动操控一台台物理机,天天关心哪个程序放在了哪台机器上,使用多少内存、多少硬盘,每台机器总共有多少内存、多少硬盘,还剩多少内存和硬盘,那头就大了。因而对应到数据中心,也需要一个调度器,将运维人员从指定物理机或者虚拟机的痛苦中解放出来,实现对于物理资源的统一管理,这就是 Kubernetes。下面是两个表格,将操作系统的功能和模块与Kubernetes做了一个对比:

Kubernetes作为数据中心的操作系统还是主要管理数据中心里的四种硬件资源:CPU、内存、存储、网络。对于CPU和内存这两种计算资源的管理,可以通过Docker技术完成,它可以将CPU和内存资源,通过namespace和cgroup从大的资源池里面隔离出来,并通过镜像技术,实现计算资源在数据中心里面的自由漂移。

没有操作系统的时候,汇编程序员需要指定程序运行的CP和内存物理地址,同理数据中心的管理员,原来还需要指定程序运行的服务器及使用的CPU和内存,现在Kubernetes里面有一个调度器Scheduler,只需要告诉你想运行10个4核8G的Java程序,它会自动选择空闲的、有足够资源的服务器,去运行这些程序

对于操作系统上的进程来说,有主线程做主要的工作,还有其它线程做辅助的工作。对于数据中心里运行的程序来说,也会有一个主要提供服务的程序,例如上面的Java程序也会有一些提供辅助功能的程序如监控、环境预设值等。Kubernetes将多个Docker组装成一个Pod的概念,在一个Pod里往往有一个Docker为主,多个Docker为辅

操作系统上的进程会在CPU上切换来切换去,它使用的内存也会换入换出。在数据中心里这些运行中的程序能不能在机器之间迁移呢?能不能在一台服务器故障时,选择其它的服务器运行呢? Kubernetes里面有Controller的概念,可以控制Pod们的运行状态以及占用的资源,如果10个变9个了,就选一台机器添加一个;如果10个变11 了,就随机删除一个。

操作系统上的进程有时候有亲和性的要求,比如它可能希望在某一个CPU上运行不切换CPU,从而提高运行效率。或者,两个线程要求在一个CPU上,从而可以使用Per CPU变量不加锁,交互和协作比较方便。有的时候,一个线程想避开另一个线程,不要共用CPU,以防相互干扰。Kubernetes的Scheduler也是有亲和性功能的,可以选择两个Pod永远运行在一台物理机上,这样本地通信就非常方便了;也可以选择两个Pod永远不要运行在同一台物理机上,这样一个挂了不影响另一个

18. 既然Docker可以将CPU内存资源进行抽象,在服务器之间迁移,那数据应该怎么办呢?如果数据放在每一台服务器上,就像散落在汪洋大海里面,用的时候根本找不到,所以必须要有统一的存储。正像一台操作系统上多个进程之间,要通过文件系统保存持久化的数据并且实现共享,在数据中心里面也需要一个这样的基础设施

统一的存储常常有三种形式,分别来看:

(1)对象存储。顾名思义,这种方式是将文件作为一个完整对象的方式来保存。每一个文件都应该有一个唯一标识这个对象的key,而文件的内容就是value。对象可以分门别类地保存在一个叫作存储空间(Bucket)的地方,有点像文件夹。对于任何一个文件对象,都可以通过HTTP RESTful API来远程获取对象。由于是简单的key-value模式,当需要保存大容量数据的时候,就比较容易根据唯一的key进行横向扩展,所以对象存储能够容纳的数据量往往非常大,在数据中心里面保存文档、视频等是很好的方式。当然缺点就是,没办法像操作文件一样操作它,而是要将value当成整个的来对待

(2)分布式文件系统。这种是最容易习惯的,因为使用它和使用本地文件系统几乎没什么区别,只不过是通过网络的方式访问远程的文件系统。多个容器能看到统一的文件系统,一个容器写入文件系统,另一个容器能够看到,可以实现共享。缺点是分布式文件系统的性能和规模是个矛盾,规模一大性能就难以保证,性能好则规模不会很大,所以不像对象存储一样能够保持海量的数据

(3)分布式块存储。这相当于云硬盘,即存储虚拟化的方式,只不过将盘挂载给容器而不是虚拟机。块存储没有分布式文件系统这一层,一旦挂载到某一个容器可以有本地的文件系统,这样做的缺点是,一般情况下不同容器挂载的块存储都是不共享的,好处是在同样规模的情况下,性能相对分布式文件系统要好。如果为了解决一个容器从一台服务器迁移到另一台服务器,如何保持数据存储的问题,块存储是一个很好的选择,它不用解决多个容器共享数据的问题。

这三种形式中,对象存储使用HTTP进行访问,当然任何容器都能访问到,不需要Kubernetes去管理它。而分布式文件系统和分布式块存储,就需要对接到Kubernetes,让Kubernetes可以管理它们,如何对接呢?Kubernetes提供Container Storage Interface(CSI)接口,这是一个标准接口,不同的存储可以实现这个接口来对接Kubernetes,很像操作系统中的设备驱动程序,操作系统只要定义统一的接口,不同存储设备的驱动实现这些接口,就能被操作系统使用了。

存储的问题解决了,接下来是网络,因为不同的服务器上的Docker还是需要互相通信的。Kubernetes有自己的网络模型,里面是这样规定的:

(1)IP-per-Pod,每个Pod都拥有一个独立IP地址,Pod内所有容器共享一个网络命名空间

(2)集群内所有Pod都在一个直接连通的扁平网络中,可通过IP直接访问,即所有容器之间无需NAT就可以直接互相访问;所有Node和所有容器之间无需NAT就可以直接互相访问容器自己看到的IP跟其它容器看到的一样

这其实是说,里面的每一个Docker访问另一个Docker时,都是感觉在一个扁平的网络里面。要实现这样的网络模型有很多种方式,例如Kubernetes自己提供Calico、Flannel,当然也可以对接Openvswitch这样的虚拟交换机,也可以使用brctl这种传统桥接模式,也可以对接硬件交换机,这又是一种类似驱动的模式,Kubernetes同样是提供统一的接口Container Network Interface(CNI,容器网络接口),无论用哪种方式实现网络模型,只要对接这个统一的接口Kubernetes就可以管理容器的网络。至此,Kubernetes作为数据中心的操作系统,内核的问题解决了。

19. 接下来是用户态的工作模式问题了,能不能像操作一台服务器那样操作数据中心呢?使用操作系统需要安装一些软件,于是需要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服务器那样,去管理数据中心了。

20. 下面这个图,总结了数据中心操作系统的功能,以及内核态与用户态中,操作系统与数据中心K8S提供功能抽象上的区别:

 

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值