docker原理简单剖析

1. 容器简介

1. 简介

百度词条的解释如下:

有效的将单个操作系统的资源划分到孤立的组中,以便更好的在孤立的组之间平衡有冲突的资源使用需求,这种技术就是容器技术。

docker 官网对容器的解释: https://docs.docker.com/get-started/

a container is a sandboxed process on your machine that is isolated from all other processes on the host machine. That isolation leverages kernel namespaces and cgroups, features that have been in Linux for a long time. Docker has worked to make these capabilities approachable and easy to use. To summarize, a container:

  • is a runnable instance of an image. You can create, start, stop, move, or delete a container using the DockerAPI or CLI.

  • can be run on local machines, virtual machines or deployed to the cloud.

  • is portable (can be run on any OS)

  • Containers are isolated from each other and run their own software, binaries, and configurations.

    通俗的理解容器技术就是一个装应用软件的箱子,箱子里面有软件运行所需的依赖库和配置。开发人员可以把这个箱子搬到任何机器上,且不影响里面软件的运行。

​ 总结起来一句话:容器是一种特殊的进程。

2. 和虚拟机区别

​ 容器:容器是一个不依赖于操作系统,运行应用程序的环境。通过Linux的Namespaces和Cgroups技术对应用程序进程进行隔离和限制的,Namespace的作用是隔离,它让应用进程只能看到该Namespace内的世界;而Cgroup 的作用是限制分配给进程的宿主机资源。但对于宿主机来说,这些被“隔离”了的进程跟其他进程并没有太大区别。说白了,容器就是跑在宿主机的一个进程,容器和容器之间是隔离的,但是宿主机能看到容器里面的相关进程。

​ 虚拟机:虚拟机是通过软件模拟的具有完整硬件系统功能的、运行在一个完全隔离环境中的完整计算机系统。在实体计算机中能够完成的工作在虚拟机中都能够实现。

总结:

容器 虚拟机(VM)
操作系统 与宿主机共享OS 宿主机OS上运行虚拟机OS
存储大小 镜像小,并与存储与传输 镜像庞大
运行性能 几乎无额外性能损失 操作系统额外的CPU、内存消耗
移植性 轻便,灵活,适应于linux 笨重,与虚拟化技术高度融合
硬件亲和性 面向软件开发者 面向硬件运维者
部署速度 快速,秒级 较慢,10s以上

3. 容器技术好处

好处: 开发/运维(devops)工程师中使用容器技术可以提升效率

更快速的应用交付和部署
更便捷的升级和扩缩容
更简单的系统运维
更高效的计算资源利用

坏处: 也有一些不好的地方, 比如

linux namespace 技术相对于虚拟机技术也有一些缺点,比如隔离不彻底。
1. 容器共用的宿主机的资源,也就是使用的是同一个操作系统的内核
2. 隔离不彻底,最典型的就是时间

4.主要产品:

docker、containerd、kata 各种各样的容器运行时产品层出不穷。

2. 容器主要实现docker

1. docker 架构图:

2. Docker容器之间隔离、和宿主机间共享

启动两个容器:

docker run -d -p 8080:8080 --name tomcat tomcat
docker run -d -p 80:80 --name nginx nginx

宿主机查看相关进程可以看到nginx和tomcat 相关进程:

[root@redisnode1 ~]# ps -ef | grep -e tomcat -e nginx|grep -v grep
root       5364   5346  0 22:04 ?        00:00:14 /usr/local/openjdk-11/bin/java -Djava.util.logging.config.file=/usr/local/tomcat/conf/logging.properties -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager -Djdk.tls.ephemeralDHKeySize=2048 -Djava.protocol.handler.pkgs=org.apache.catalina.webresources -Dorg.apache.catalina.security.SecurityListener.UMASK=0027 -Dignore.endorsed.dirs= -classpath /usr/local/tomcat/bin/bootstrap.jar:/usr/local/tomcat/bin/tomcat-juli.jar -Dcatalina.base=/usr/local/tomcat -Dcatalina.home=/usr/local/tomcat -Djava.io.tmpdir=/usr/local/tomcat/temp org.apache.catalina.startup.Bootstrap start
root      11015  10997  0 23:40 ?        00:00:00 nginx: master process nginx -g daemon off;
101       11080  11015  0 23:40 ?        00:00:00 nginx: worker process
101       11081  11015  0 23:40 ?        00:00:00 nginx: worker process
101       11082  11015  0 23:40 ?        00:00:00 nginx: worker process
101       11083  11015  0 23:40 ?        00:00:00 nginx: worker process

查看容器内进程

[root@redisnode1 ~]# docker top tomcat
UID                 PID                 PPID                C                   STIME               TTY                 TIME                CMD
root                5364                5346                0                   22:04               ?                   00:00:14            /usr/local/openjdk-11/bin/java -Djava.util.logging.config.file=/usr/local/tomcat/conf/logging.properties -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager -Djdk.tls.ephemeralDHKeySize=2048 -Djava.protocol.handler.pkgs=org.apache.catalina.webresources -Dorg.apache.catalina.security.SecurityListener.UMASK=0027 -Dignore.endorsed.dirs= -classpath /usr/local/tomcat/bin/bootstrap.jar:/usr/local/tomcat/bin/tomcat-juli.jar -Dcatalina.base=/usr/local/tomcat -Dcatalina.home=/usr/local/tomcat -Djava.io.tmpdir=/usr/local/tomcat/temp org.apache.catalina.startup.Bootstrap start
[root@redisnode1 ~]# docker top nginx
UID                 PID                 PPID                C                   STIME               TTY                 TIME                CMD
root                11015               10997               0                   23:40               ?                   00:00:00            nginx: master process nginx -g daemon off;
101                 11080               11015               0                   23:40               ?                   00:00:00            nginx: worker process
101                 11081               11015               0                   23:40               ?                   00:00:00            nginx: worker process
101                 11082               11015               0                   23:40               ?                   00:00:00            nginx: worker process
101                 11083               11015               0                   23:40               ?                   00:00:00            nginx: worker process

也可以通过pstree 进程查看:可以看到本地启动的docker 容器的相关进程

[root@redisnode1 ~]# pstree -a -l
systemd --switched-root --system --deserialize 22
...
  ├─dnsmasq --conf-file=/var/lib/libvirt/dnsmasq/default.conf --leasefile-ro --dhcp-script=/usr/libexec/libvirt_leaseshelper
  │   └─dnsmasq --conf-file=/var/lib/libvirt/dnsmasq/default.conf --leasefile-ro --dhcp-script=/usr/libexec/libvirt_leaseshelper
  ├─dockerd
  │   ├─docker-containe --config /var/run/docker/containerd/containerd.toml
  │   │   ├─docker-containe -namespace moby -workdir /var/lib/docker/containerd/daemon/io.containerd.runtime.v1.linux/moby/2f075447247d8177f7170e5f0d8d389d54c65811ae33c9be1fbafec2062c3141 -address /var/run/docker/containerd/docker-containerd.sock -containerd-binary /usr/bin/docker-containerd -runtime-root /var/run/docker/runtime-runc
  │   │   │   ├─nginx
  │   │   │   │   ├─nginx
  │   │   │   │   ├─nginx
  │   │   │   │   ├─nginx
  │   │   │   │   └─nginx
  │   │   │   └─9*[{docker-containe}]
  │   │   ├─docker-containe -namespace moby -workdir /var/lib/docker/containerd/daemon/io.containerd.runtime.v1.linux/moby/fb3b865a53c08e3d0d7e11d8efffbe68ccce12b29f41139fd07de27c0e178bb3 -address /var/run/docker/containerd/docker-containerd.sock -containerd-binary /usr/bin/docker-containerd -runtime-root /var/run/docker/runtime-runc
  │   │   │   ├─java -Djava.util.logging.config.file=/usr/local/tomcat/conf/logging.properties -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager -Djdk.tls.ephemeralDHKeySize=2048 -Djava.protocol.handler.pkgs=org.apache.catalina.webresources -Dorg.apache.catalina.security.SecurityListener.UMASK=0027 -Dignore.endorsed.dirs= -classpath /usr/local/tomcat/bin/bootstrap.jar:/usr/local/tomcat/bin/tomcat-juli.jar -Dcatalina.base=/usr/local/tomcat -Dcatalina.home=/usr/local/tomcat -Djava.io.tmpdir=/usr/local/tomcat/temp org.apache.catalina.startup.Bootstrap start
  │   │   │   │   └─33*[{java}]
  │   │   │   └─9*[{docker-containe}]
  │   │   └─16*[{docker-containe}]
  │   ├─docker-proxy -proto tcp -host-ip 0.0.0.0 -host-port 80 -container-ip 172.17.0.2 -container-port 80
  │   │   └─7*[{docker-proxy}]
  │   ├─docker-proxy -proto tcp -host-ip 0.0.0.0 -host-port 8080 -container-ip 172.17.0.3 -container-port 8080
  │   │   └─6*[{docker-proxy}]
  │   └─15*[{dockerd}]
  ├─gssproxy -D
  │   └─5*[{gssproxy}]

3. 容器核心技术namespace

​ Docker通过namespace实现了资源隔离。

​ namespace 是 Linux 内核用来隔离内核资源的方式。通过 namespace 可以让一些进程只能看到与自己相关的一部分资源,而另外一些进程也只能看到与它们自己相关的资源,这两拨进程根本就感觉不到对方的存在。具体的实现方式是把一个或多个进程的相关资源指定在同一个 namespace 中。

​ linux提供的namespace如下:

0. 查看系统的namespace 进程
[root@redisnode1 ns]# lsns
        NS TYPE  NPROCS   PID USER   COMMAND
4026531836 pid      154     1 root   /usr/lib/systemd/systemd --switched-root --system --deserialize 22
4026531837 user     160     1 root   /usr/lib/systemd/systemd --switched-root --system --deserialize 22
4026531838 uts      154     1 root   /usr/lib/systemd/systemd --switched-root --system --deserialize 22
4026531839 ipc      154     1 root   /usr/lib/systemd/systemd --switched-root --system --deserialize 22
4026531840 mnt      149     1 root   /usr/lib/systemd/systemd --switched-root --system --deserialize 22
4026531856 mnt        1    28 root   kdevtmpfs
4026531956 net      154     1 root   /usr/lib/systemd/systemd --switched-root --system --deserialize 22
4026532502 mnt        1   728 root   /usr/libexec/bluetooth/bluetoothd
4026532506 mnt        1  5364 root   /usr/local/openjdk-11/bin/java -Djava.util.logging.config.file=/usr/local/tomcat/conf/logging.properties -Djava.ut
4026532507 uts        1  5364 root   /usr/local/openjdk-11/bin/java -Djava.util.logging.config.file=/usr/local/tomcat/conf/logging.properties -Djava.ut
4026532508 ipc        1  5364 root   /usr/local/openjdk-11/bin/java -Djava.util.logging.config.file=/usr/local/tomcat/conf/logging.properties -Djava.ut
4026532509 pid        1  5364 root   /usr/local/openjdk-11/bin/java -Djava.util.logging.config.file=/usr/local/tomcat/conf/logging.properties -Djava.ut
4026532511 net        1  5364 root   /usr/local/openjdk-11/bin/java -Djava.util.logging.config.file=/usr/local/tomcat/conf/logging.properties -Djava.ut
4026532527 mnt        1   767 chrony /usr/sbin/chronyd
4026532569 mnt        1   792 root   /usr/sbin/NetworkManager --no-daemon
4026532583 mnt        5 11015 root   nginx: master process nginx -g daemon off
4026532584 uts        5 11015 root   nginx: master process nginx -g daemon off
4026532585 ipc        5 11015 root   nginx: master process nginx -g daemon off
4026532586 pid        5 11015 root   nginx: master process nginx -g daemon off
4026532588 net        5 11015 root   nginx: master process nginx -g daemon off
4026532641 mnt        1  1076 root   /usr/sbin/cupsd -f

可以看到针对tomcat 和 nginx 都有相对应的五种类型的ns。也就是docker 启动容器会创建新的namespace 实现。

1.查看一个进程所属的namespace: (5364 是上面tomcat 的进程)

系统中的每个进程都有/proc/[pid]/ns/这样一个目录,里面包含了这个进程所属namespace的信息

[root@redisnode1 ns]# ll /proc/5364/ns
total 0
lrwxrwxrwx. 1 root root 0 Mar  8 00:52 ipc -> ipc:[4026532508]
lrwxrwxrwx. 1 root root 0 Mar  8 00:52 mnt -> mnt:[4026532506]
lrwxrwxrwx. 1 root root 0 Mar  7 22:04 net -> net:[4026532511]
lrwxrwxrwx. 1 root root 0 Mar  8 00:52 pid -> pid:[4026532509]
lrwxrwxrwx. 1 root root 0 Mar  8 00:52 user -> user:[4026531837]
lrwxrwxrwx. 1 root root 0 Mar  8 00:52 uts -> uts:[4026532507]

​ ipc:[4026532508] 为例子分析,ipc是namespace的类型,4026532508是inode number,如果两个进程的ipc namespace的inode number一样,说明他们属于同一个namespace。当一个namespace中的所有进程都退出时,该namespace将会被销毁。

比如查看宿主机的dockerd 的ns和redis-server的ns,是同一个ns:都是systemd创建的ns

[root@redisnode1 ns]# ps -ef | grep dockerd | grep -v grep
root       3955      1  0 Mar07 ?        00:01:32 /usr/bin/dockerd
[root@redisnode1 ns]# ps -ef | grep redis-server| grep -v grep
root       1119      1  0 Mar07 ?        00:00:29 /usr/local/bin/redis-server *:6379
[root@redisnode1 ns]# ll /proc/3955/ns
total 0
lrwxrwxrwx. 1 root root 0 Mar  7 21:50 ipc -> ipc:[4026531839]
lrwxrwxrwx. 1 root root 0 Mar  7 21:50 mnt -> mnt:[4026531840]
lrwxrwxrwx. 1 root root 0 Mar  7 21:50 net -> net:[4026531956]
lrwxrwxrwx. 1 root root 0 Mar  7 21:50 pid -> pid:[4026531836]
lrwxrwxrwx. 1 root root 0 Mar  7 21:50 user -> user:[4026531837]
lrwxrwxrwx. 1 root root 0 Mar  7 21:50 uts -> uts:[4026531838]
[root@redisnode1 ns]# ll /proc/1119/ns
total 0
lrwxrwxrwx. 1 root root 0 Mar  7 08:02 ipc -> ipc:[4026531839]
lrwxrwxrwx. 1 root root 0 Mar  7 08:02 mnt -> mnt:[4026531840]
lrwxrwxrwx. 1 root root 0 Mar  7 08:02 net -> net:[4026531956]
lrwxrwxrwx. 1 root root 0 Mar  7 08:02 pid -> pid:[4026531836]
lrwxrwxrwx. 1 root root 0 Mar  7 08:02 user -> user:[4026531837]
lrwxrwxrwx. 1 root root 0 Mar  7 08:02 uts -> uts:[4026531838]
2. 跟namespace 相关的api

​ Linux 提供了多个 API 用来操作 namespace,它们是 clone()、setns() 和 unshare() 函数,为了确定隔离的到底是哪项 namespace,在使用这些 API 时,通常需要指定一些调用参数:CLONE_NEWIPC、CLONE_NEWNET、CLONE_NEWNS、CLONE_NEWPID、CLONE_NEWUSER、CLONE_NEWUTS 和 CLONE_NEWCGROUP。如果要同时隔离多个 namespace,可以使用 | (按位或)组合这些参数。

  • clone: 创建一个新的进程并把他放到新的namespace中
int clone(int (*child_func)(void *), void *child_stack, int flags, void *arg);

flags: 
    指定一个或者多个上面的CLONE_NEW*(当然也可以包含跟namespace无关的flags), 
    这样就会创建一个或多个新的不同类型的namespace, 
    并把新创建的子进程加入新创建的这些namespace中
  • setns: 将当前进程加入到已有的namespace中
int setns(int fd, int nstype);

fd: 
    指向/proc/[pid]/ns/目录里相应namespace对应的文件,
    表示要加入哪个namespace

nstype:
    指定namespace的类型(上面的任意一个CLONE_NEW*):
    1. 如果当前进程不能根据fd得到它的类型,如fd由其他进程创建,
    并通过UNIX domain socket传给当前进程,
    那么就需要通过nstype来指定fd指向的namespace的类型
    2. 如果进程能根据fd得到namespace类型,比如这个fd是由当前进程打开的,
    那么nstype设置为0即可
  • unshare: 使当前进程退出指定类型的namespace,并加入到新创建的namespace(相当于创建并加入新的namespace)
int unshare(int flags);

flags:
    指定一个或者多个上面的CLONE_NEW*,
    这样当前进程就退出了当前指定类型的namespace并加入到新创建的namespace

clone和unshare的区别:

clone和unshare的功能都是创建并加入新的namespace, 他们的区别是:

  • unshare是使当前进程加入新的namespace
  • clone是创建一个新的子进程,然后让子进程加入新的namespace,而当前进程保持不变
3. namespace 相关api 测试
基于busybox 创建一个简易版容器。busybox实现了一些Linux下常用的命令,如ls,hostname,date,ps,mount等等。
  • 准备container的根目录以及安装测试
mkdir chroot && cd chroot
wget https://busybox.net/downloads/binaries/1.21.1/busybox-x86_64    # 下载
# 创建new_root/bin目录,new_root将会是新容器的根目录,bin目录用来放busybox
# 由于/bin默认就在PATH中,所以里面放的程序都可以直接在shell里面执行,不需要带完整的路径
mkdir -p new_root/bin
chmod +x ./busybox-x86_64
# 将busybox-x86_64移到bin目录下,并重命名为busybox
mv busybox-x86_64 new_root/bin/busybox
# 运行ls试试,确保busybox能正常工作
./new_root/bin/busybox ls

# 安装busybox到bin目录,不安装的话每次执行ls命令都需要使用上面那种格式: busybox ls
# 安装之后就会创建一个ls到busybox的硬链接,这样执行ls的时候就不用再输入前面的busybox了
./new_root/bin/busybox --install ./new_root/bin/
# 运行下bin下面的ls,确保安装成功
./new_root/bin/ls -lh

# 使用chroot命令,切换根目录, 并且执行几个命令进行测试
[root@redisnode1 chroot]# sudo chroot ./new_root/ sh
/ # ls
bin
/ # pwd
/
/ # id
uid=0 gid=0 groups=0
/ # exit    # 退出sh 环境
[root@redisnode1 chroot]# 

# 由于下面我的centos7 是默认会找/usr/bin/xxx 目录,所以我将bin目录移动到 ./new_root/usr 目录下面,然后进行测试
# 其实看宿主机里面 /usr/bin 和 /bin 目录下面都是可执行文件,并且默认是一样的
[root@redisnode1 chroot]# mkdir -p ./new_root/usr
[root@redisnode1 chroot]# mv ./new_root/bin/ ./new_root/usr/
[root@redisnode1 chroot]# sudo chroot ./new_root/ sh
/ # ls
data      old_root  proc      usr
/ # pwd
/
/ # whoami    # 由于改变了根目录关系,所以找不到用户相关资源
whoami: unknown uid 0
/ # ps -ef
PID   USER     TIME   COMMAND
/ # exit
  • 创建容器并做相关配置
 # 新建/data目录用来在主机和容器之间共享数据
 mkdir -p /data && chown root:root /data

# 创建新的容器,指定所有namespace相关的参数,
# 这里--propagation private是为了让容器里的mount point都变成private的,
# 这是因为pivot_root命令需要原来根目录的挂载点为private,
# 只有我们需要在host和container之间共享挂载信息的时候,才需要使用shared或者slave类型
[root@redisnode1 chroot]# unshare --user --mount --ipc --pid --net --uts -r --fork --propagation private bash
# 修改主机名称
[root@redisnode1 chroot]# hostname container01
[root@redisnode1 chroot]# exec bash

# 创建old_root用于pivot_root命令,创建data目录用于绑定/data目录
[root@container01 chroot]# mkdir -p ./new_root/old_root/ ./new_root/data/
# 由于pivot_root命令要求老的根目录和新的根目录不能在同一个挂载点下, 所以这里利用bind mount,在原地创建一个新的挂载点
[root@container01 chroot]# mount --bind ./new_root/ ./new_root/
# 将/data目录绑定到new_root/data,这样pivot_root后,就能访问/data下的东西了
[root@container01 chroot]# mount --bind /data ./new_root/data

# 进入new_root目录,然后切换根目录
[root@container01 chroot]# cd new_root/
[root@container01 new_root]# pivot_root ./ ./old_root/

# shell提示符里显示的当前目录还是原来的目录,没有切换到‘/’下,这是因为当前运行的shell还是host里面的bash
[root@container01 new_root]# ls
data      old_root  proc      usr
[root@container01 new_root]# pwd
/root/chroot/new_root
# 没有信息是因为没有挂载/proc 目录,找不到文件
[root@container01 new_root]# ps -ef 
PID   USER     TIME   COMMAND

# 重新加载new_root下面的shell,这样contianer和host就没有关系了,从shell提示符中可以看出,当前目录已经变成了‘/’
# 由于没有/etc目录,也就没有相关的profile,于是shell的提示符里面只包含当前路径
[root@container01 new_root]# exec sh
# 设置PS1环境变量,让shell提示符好看点,这里直接写了root在提示符里面,是因为我们新的container里面没有账号相关的配置文件,虽然系统知道当
# 前账号的ID是0,但不知道账号的用户名是什么
/ # export PS1='root@$(hostname):$(pwd)# '

# 简单测试
root@container01:/# ls
data      old_root  proc      usr
root@container01:/# cd usr/
root@container01:/usr# ls
bin
root@container01:/usr# pwd
/usr
# 没有/etc目录,没有user相关的配置文件,所以不知道ID为0的用户名是什么
root@container01:/# whoami 
whoami: unknown uid 0

# mount命令依赖于/proc目录,所以这里mount操作失败, 下面创建/proc/目录, 然后进行挂载
root@container01:/# mount
mount: no /proc/mounts
root@container01:/# mkdir -p /proc
root@container01:/# mount -t proc none /proc
root@container01:/# ps -ef
PID   USER     TIME   COMMAND
    1 0          0:00 sh
  104 0          0:00 ps -ef
root@container01:/# mount
/dev/mapper/centos-root on /old_root type xfs (rw,seclabel,relatime,attr2,inode64,noquota)
devtmpfs on /old_root/dev type devtmpfs (rw,seclabel,nosuid,size=1913700k,nr_inodes=478425,mode=755)
tmpfs on /old_root/dev/shm type tmpfs (rw,seclabel,nosuid,nodev)
devpts on /old_root/dev/pts type devpts (rw,seclabel,nosuid,noexec,relatime,gid=5,mode=620,ptmxmode=000)
mqueue on /old_root/dev/mqueue type mqueue (rw,seclabel,relatime)
...

# 虚拟容器内部创建一个文件到/data 目录,实际该目录挂载到宿主机的/data 目录中; 然后宿主机可以到/data 目录看到该文件的存在
root@container01:/# cd /data/
root@container01:/data# pwd
/data
root@container01:/data# echo "123" >> 001
root@container01:/data# cat 001 
123

# 查看线程ID信息
root@container01:/# cd /
root@container01:/# echo $$
1
root@container01:/# ps -ef
PID   USER     TIME   COMMAND
    1 0          0:00 sh
  103 0          0:00 ps -ef
  • 汇总上面的脚本测试:

(1) 初始化

cd ~/chroot/ && mkdir -p /data && chown root:root /data

unshare --user --mount --ipc --pid --net --uts -r --fork --propagation private bash

(2) 挂载以及其他设置

hostname container01
exec bash

mkdir -p ./new_root/old_root/ ./new_root/data/
mount --bind ./new_root/ ./new_root/
mount --bind /data ./new_root/data
cd new_root/
pivot_root ./ ./old_root/

exec sh
export PS1='root@$(hostname):$(pwd)# '

mkdir -p /proc && mount -t proc none /proc
echo $$	

(3) 虚拟容器内部执行如下死循环占用cpu:

while : ; do : ; done &

​ 这里只是为了占用较多的CPU,在后面会用cgroup 对其进行限制。

补充:

(1)基于unshare 实现新的容器:(用 unshare -p 创建 PID Namespace 的时候必须加上 -f 选项。)

[root@redisnode1 chroot]# unshare --help

Usage:
 unshare [options] <program> [<argument>...]

Run a program with some namespaces unshared from the parent.

Options:
 -m, --mount               unshare mounts namespace
 -u, --uts                 unshare UTS namespace (hostname etc)
 -i, --ipc                 unshare System V IPC namespace
 -n, --net                 unshare network namespace
 -p, --pid                 unshare pid namespace
 -U, --user                unshare user namespace
 -f, --fork                fork before launching <program>
     --mount-proc[=<dir>]  mount proc filesystem first (implies --mount)
 -r, --map-root-user       map current user to root (implies --user)
     --propagation <slave|shared|private|unchanged>
                           modify mount propagation in mount namespace
 -s, --setgroups allow|deny  control the setgroups syscall in user namespaces

 -h, --help     display this help and exit
 -V, --version  output version information and exit

For more details see unshare(1).

unshare 然后测试:

unshare --user --mount --ipc --pid --net --uts -f -r sh    # unshare 创建新的ns并且执行sh
ps -ef|grep redis    # 内部查看进程,可以看到redis 相关进程(因为我们没有做目录的挂载,该namespace 还可以读到/proc 下面的信息。 需要改变/proc 目录的挂载到自己的内部)
sh-4.2# kill -9 1130    # 删除的时候报错找不到进程
sh: kill: (1130) - No such process

(2) unshare 指定usernamespace 的时候报错如下:

​ 是因为限制了namespace 的数量,修改限制即可。

[root@redisnode1 chroot]# unshare -U sh
unshare: unshare failed: Invalid argument
[root@redisnode1 chroot]# cat /proc/sys/user/max_user_namespaces
0
[root@redisnode1 chroot]# echo 2147483647 > /proc/sys/user/max_user_namespaces
[root@redisnode1 chroot]# cat /proc/sys/user/max_user_namespaces
2147483647
[root@redisnode1 chroot]# unshare -U sh
sh-4.2$ exit
exit

4. cgroup 技术

​ cgroup 全称是Control Group, cgroup和namespace类似,也是将进程进行分组,但它的目的和namespace不一样,namespace是为了隔离进程组之间的资源,而cgroup是为了对一组进程进行统一的资源监控和限制。

1. 什么是cgroup

​ 术语cgroup在不同的上下文中代表不同的意思,可以指整个Linux的cgroup技术,也可以指一个具体进程组。

​ cgroup是Linux下的一种将进程按组进行管理的机制,在用户层看来,cgroup技术就是把系统中的所有进程组织成一颗一颗独立的树,每棵树都包含系统的所有进程,树的每个节点是一个进程组,而每颗树又和一个或者多个subsystem关联,树的作用是将进程分组,而subsystem的作用就是对这些组进行操作。cgroup主要包括下面两部分:

  • subsystem

    ​ 一个subsystem就是一个内核模块,他被关联到一颗cgroup树之后,就会在树的每个节点(进程组)上做具体的操作。subsystem经常被称作"resource controller",因为它主要被用来调度或者限制每个进程组的资源。到目前为止,Linux支持12种subsystem,比如限制CPU的使用时间,限制使用的内存等。

    ​ 简单理解就是资源可以限制的类型。

  • hierarchy

​ 一个hierarchy可以理解为一棵cgroup树,树的每个节点就是一个进程组,每棵树都会与零到多个subsystem关联。

​ 简单理解就是将进程组和限制的资源类型进行绑定,达到资源限制的效果。

2. 查看当前系统支持的subsystem
[root@redisnode1 cpu]# cat /proc/cgroups
#subsys_name    hierarchy       num_cgroups     enabled
cpuset  4       4       1
cpu     7       93      1
cpuacct 7       93      1
memory  9       93      1
devices 5       93      1
freezer 8       4       1
net_cls 6       4       1
blkio   2       93      1
perf_event      10      4       1
hugetlb 11      4       1
pids    3       93      1
net_prio        6       4       1

​ 从左到右分别表示,subsystem的名字、subsystem所关联到的cgroup树的ID、subsystem所关联的cgroup树中进程组的个数,也即树上节点的个数、1表示开启,0表示没有被开启(可以通过设置内核的启动参数“cgroup_disable”来控制subsystem的开启)

3. 如何使用cgroup

​ 使用cgroup很简单,挂载这个文件系统就可以了。一般情况下都是挂载到/sys/fs/cgroup目录下。

# 挂载一颗和所有subsystem关联的cgroup树到/sys/fs/cgroup
mount -t cgroup xxx /sys/fs/cgroup

# 挂载一颗和cpuset subsystem关联的cgroup树到/sys/fs/cgroup/cpuset
mkdir /sys/fs/cgroup/cpuset
mount -t cgroup -o cpuset xxx /sys/fs/cgroup/cpuset

# 挂载一颗与cpu和cpuacct subsystem关联的cgroup树到/sys/fs/cgroup/cpu,cpuacct
mkdir /sys/fs/cgroup/cpu,cpuacct
mount -t cgroup -o cpu,cpuacct xxx /sys/fs/cgroup/cpu,cpuacct

# 挂载一棵cgroup树,但不关联任何subsystem,下面就是systemd所用到的方式
mkdir /sys/fs/cgroup/systemd
mount -t cgroup -o none,name=systemd xxx /sys/fs/cgroup/systemd

​ 创建并挂载好一颗cgroup树之后,就有了树的根节点,也即根cgroup,这时候就可以通过创建文件夹的方式创建子cgroup,然后再往每个子cgroup中添加进程以及资源限制就可以了。

在很多使用systemd的系统中,已经帮我们将各个subsystem和cgroup树关联并挂载好了, 查看如下:

[root@redisnode1 cpu]# mount -t cgroup
cgroup on /sys/fs/cgroup/systemd type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,xattr,release_agent=/usr/lib/systemd/systemd-cgroups-agent,name=systemd)
cgroup on /sys/fs/cgroup/blkio type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,blkio)
cgroup on /sys/fs/cgroup/pids type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,pids)
cgroup on /sys/fs/cgroup/cpuset type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,cpuset)
cgroup on /sys/fs/cgroup/devices type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,devices)
cgroup on /sys/fs/cgroup/net_cls,net_prio type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,net_prio,net_cls)
cgroup on /sys/fs/cgroup/cpu,cpuacct type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,cpuacct,cpu)
cgroup on /sys/fs/cgroup/freezer type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,freezer)
cgroup on /sys/fs/cgroup/memory type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,memory)
cgroup on /sys/fs/cgroup/perf_event type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,perf_event)
cgroup on /sys/fs/cgroup/hugetlb type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,hugetlb)
4. 查看当前进程属于哪个cgroup

​ 查看/proc/[proc]/cgroup 文件即可

[root@redisnode1 cpu]# cat /proc/1130/cgroup
11:hugetlb:/
10:perf_event:/
9:memory:/system.slice/redis_6379.service
8:freezer:/
7:cpuacct,cpu:/system.slice/redis_6379.service
6:net_prio,net_cls:/
5:devices:/system.slice/redis_6379.service
4:cpuset:/
3:pids:/system.slice/redis_6379.service
2:blkio:/system.slice/redis_6379.service
1:name=systemd:/system.slice/redis_6379.service
5. cgroup 测试

我们对上面使用unshare 创建的伪容器进行CPU限制:

[root@redisnode1 container]# mount -t cgroup     # 查看cgroup 可以限制的种类以及挂载的目录
cgroup on /sys/fs/cgroup/systemd type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,xattr,release_agent=/usr/lib/systemd/systemd-cgroups-agent,name=systemd)
cgroup on /sys/fs/cgroup/blkio type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,blkio)
cgroup on /sys/fs/cgroup/pids type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,pids)
cgroup on /sys/fs/cgroup/cpuset type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,cpuset)
cgroup on /sys/fs/cgroup/devices type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,devices)
cgroup on /sys/fs/cgroup/net_cls,net_prio type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,net_prio,net_cls)
cgroup on /sys/fs/cgroup/cpu,cpuacct type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,cpuacct,cpu)
cgroup on /sys/fs/cgroup/freezer type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,freezer)
cgroup on /sys/fs/cgroup/memory type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,memory)
cgroup on /sys/fs/cgroup/perf_event type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,perf_event)
cgroup on /sys/fs/cgroup/hugetlb type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,hugetlb)
[root@redisnode1 container]# cd /sys/fs/cgroup/cpu    # 进入到cpu 目录下面创建子组
# 创建子组,默认会帮我们创建一些相关的限制文件。tasks存放pid,一行一个; 
# cpu.cfs_period_us 可以理解为一个参考, 比如 100000 表示100 ms, 表示100%;
# cpu.cfs_quota_us 可以理解为相对参考的具体的数值, -1 表示无限制, 比如 20000 表示20 ms, 相对于上面就是最多使用20 % 的cpu
[root@redisnode1 cpu]# mkdir -p container    
[root@redisnode1 cpu]# cd container/
[root@redisnode1 container]# ls
cgroup.clone_children  cgroup.procs  cpuacct.usage         cpu.cfs_period_us  cpu.rt_period_us   cpu.shares  notify_on_release
cgroup.event_control   cpuacct.stat  cpuacct.usage_percpu  cpu.cfs_quota_us   cpu.rt_runtime_us  cpu.stat    tasks
[root@redisnode1 container]# cat ./cpu.cfs_quota_us # -1 表示无线
-1
[root@redisnode1 container]# cat ./cpu.cfs_period_us  # 100ms,度量单位
100000
[root@redisnode1 container]# echo 20000 > ./cpu.cfs_quota_us  # 表示占用20ms, 也就是最多20 %
[root@redisnode1 container]# cat ./cpu.cfs_quota_us 
20000
[root@redisnode1 container]# cat tasks 
[root@redisnode1 container]# echo 15863 > ./tasks  # 将进程ID加到task 中
[root@redisnode1 container]# cat tasks 
15863

再次到/proc/pid/cgroup 查看所属的组, 可以发现其cpu 属于container 组

删除该cpu 限制:直接删除资源限制挂载目录的文件夹即可:(需要等相关进程都停掉,资源释放出来才可以进行删除)

rmdir /sys/fs/cgroup/cpu/container

5. docker 对资源以及ns 查看验证

docker run 的时候可以指定cpu 和 memory 相关参数,docker run --help 查看支持的相关参数如下:

[root@redisnode1 cpu]# docker run --help

Usage:  docker run [OPTIONS] IMAGE [COMMAND] [ARG...]

Run a command in a new container

Options:
      --add-host list                  Add a custom host-to-IP mapping (host:ip)
  -a, --attach list                    Attach to STDIN, STDOUT or STDERR
      --blkio-weight uint16            Block IO (relative weight), between 10 and 1000, or 0 to disable (default 0)
      --blkio-weight-device list       Block IO weight (relative device weight) (default [])
      --cap-add list                   Add Linux capabilities
      --cap-drop list                  Drop Linux capabilities
      --cgroup-parent string           Optional parent cgroup for the container
      --cidfile string                 Write the container ID to the file
      --cpu-period int                 Limit CPU CFS (Completely Fair Scheduler) period
      --cpu-quota int                  Limit CPU CFS (Completely Fair Scheduler) quota
      --cpu-rt-period int              Limit CPU real-time period in microseconds
      --cpu-rt-runtime int             Limit CPU real-time runtime in microseconds
  -c, --cpu-shares int                 CPU shares (relative weight)
      --cpus decimal                   Number of CPUs
      --cpuset-cpus string             CPUs in which to allow execution (0-3, 0,1)
      --cpuset-mems string             MEMs in which to allow execution (0-3, 0,1)
  -d, --detach                         Run container in background and print container ID
      --detach-keys string             Override the key sequence for detaching a container
      --device list                    Add a host device to the container
      --device-cgroup-rule list        Add a rule to the cgroup allowed devices list
      --device-read-bps list           Limit read rate (bytes per second) from a device (default [])
      --device-read-iops list          Limit read rate (IO per second) from a device (default [])
      --device-write-bps list          Limit write rate (bytes per second) to a device (default [])
      --device-write-iops list         Limit write rate (IO per second) to a device (default [])
      --disable-content-trust          Skip image verification (default true)
      --dns list                       Set custom DNS servers
      --dns-option list                Set DNS options
      --dns-search list                Set custom DNS search domains
      --entrypoint string              Overwrite the default ENTRYPOINT of the image
  -e, --env list                       Set environment variables
      --env-file list                  Read in a file of environment variables
      --expose list                    Expose a port or a range of ports
      --group-add list                 Add additional groups to join
      --health-cmd string              Command to run to check health
      --health-interval duration       Time between running the check (ms|s|m|h) (default 0s)
      --health-retries int             Consecutive failures needed to report unhealthy
      --health-start-period duration   Start period for the container to initialize before starting health-retries countdown (ms|s|m|h) (default 0s)
      --health-timeout duration        Maximum time to allow one check to run (ms|s|m|h) (default 0s)
      --help                           Print usage
  -h, --hostname string                Container host name
      --init                           Run an init inside the container that forwards signals and reaps processes
  -i, --interactive                    Keep STDIN open even if not attached
      --ip string                      IPv4 address (e.g., 172.30.100.104)
      --ip6 string                     IPv6 address (e.g., 2001:db8::33)
      --ipc string                     IPC mode to use
      --isolation string               Container isolation technology
      --kernel-memory bytes            Kernel memory limit
  -l, --label list                     Set meta data on a container
      --label-file list                Read in a line delimited file of labels
      --link list                      Add link to another container
      --link-local-ip list             Container IPv4/IPv6 link-local addresses
      --log-driver string              Logging driver for the container
      --log-opt list                   Log driver options
      --mac-address string             Container MAC address (e.g., 92:d0:c6:0a:29:33)
  -m, --memory bytes                   Memory limit
      --memory-reservation bytes       Memory soft limit
      --memory-swap bytes              Swap limit equal to memory plus swap: '-1' to enable unlimited swap
      --memory-swappiness int          Tune container memory swappiness (0 to 100) (default -1)
      --mount mount                    Attach a filesystem mount to the container
      --name string                    Assign a name to the container
      --network string                 Connect a container to a network (default "default")
      --network-alias list             Add network-scoped alias for the container
      --no-healthcheck                 Disable any container-specified HEALTHCHECK
      --oom-kill-disable               Disable OOM Killer
      --oom-score-adj int              Tune host's OOM preferences (-1000 to 1000)
      --pid string                     PID namespace to use
      --pids-limit int                 Tune container pids limit (set -1 for unlimited)
      --privileged                     Give extended privileges to this container
  -p, --publish list                   Publish a container's port(s) to the host
  -P, --publish-all                    Publish all exposed ports to random ports
      --read-only                      Mount the container's root filesystem as read only
      --restart string                 Restart policy to apply when a container exits (default "no")
      --rm                             Automatically remove the container when it exits
      --runtime string                 Runtime to use for this container
      --security-opt list              Security Options
      --shm-size bytes                 Size of /dev/shm
      --sig-proxy                      Proxy received signals to the process (default true)
      --stop-signal string             Signal to stop a container (default "SIGTERM")
      --stop-timeout int               Timeout (in seconds) to stop a container
      --storage-opt list               Storage driver options for the container
      --sysctl map                     Sysctl options (default map[])
      --tmpfs list                     Mount a tmpfs directory
  -t, --tty                            Allocate a pseudo-TTY
      --ulimit ulimit                  Ulimit options (default [])
  -u, --user string                    Username or UID (format: <name|uid>[:<group|gid>])
      --userns string                  User namespace to use
      --uts string                     UTS namespace to use
  -v, --volume list                    Bind mount a volume
      --volume-driver string           Optional volume driver for the container
      --volumes-from list              Mount volumes from the specified container(s)
  -w, --workdir string                 Working directory inside the container      
  1. 我们删除掉所有的容器然后重新测试。
# 删除掉所有的容器
[root@redisnode1 cpu]# docker ps -a|awk '{print $1}'|grep -v CONTA|xargs docker stop | xargs docker rm
7bb84623f1b9
4c646652dc07
21d084aa9e6d
7caeb6830751
eac596f89268
c73718bdd907
2f075447247d
fb3b865a53c0
  1. 启动一个alpine 容器

指定cpu 和memory 相关的参数

docker run -it --cpu-period=100000 --cpu-quota=20000 -m 200M --privileged  centos:7 /usr/sbin/init

​ 加--privileged 参数是为了提权(docker 应用容器 获取宿主机root权限),执行/usr/sbin/init 进行初始化, 将容器的1 号进程设置为 init 进程,权限比较高,这样容器内部可以安装以及启动一个程序。不提权以及指定init的情况下,docker 容器默认的1 号进程是sh, 有时候在容器内部启动一些程序会没有权限。

  1. 宿主机查看其ns 和 cgroup
  • 查看ns
[root@redisnode1 chroot]# lsns
        NS TYPE  NPROCS   PID USER   COMMAND
4026531836 pid      159     1 root   /usr/lib/systemd/systemd --switched-root --system --deserialize 22
4026531837 user     166     1 root   /usr/lib/systemd/systemd --switched-root --system --deserialize 22
4026531838 uts      159     1 root   /usr/lib/systemd/systemd --switched-root --system --deserialize 22
4026531839 ipc      159     1 root   /usr/lib/systemd/systemd --switched-root --system --deserialize 22
4026531840 mnt      154     1 root   /usr/lib/systemd/systemd --switched-root --system --deserialize 22
4026531856 mnt        1    28 root   kdevtmpfs
4026531956 net      159     1 root   /usr/lib/systemd/systemd --switched-root --system --deserialize 22
4026532504 mnt        7 18928 root   /usr/sbin/init
4026532505 uts        7 18928 root   /usr/sbin/init
4026532506 ipc        7 18928 root   /usr/sbin/init
4026532507 pid        7 18928 root   /usr/sbin/init
4026532509 net        7 18928 root   /usr/sbin/init
4026532566 mnt        1   742 root   /usr/libexec/bluetooth/bluetoothd
4026532568 mnt        1   760 chrony /usr/sbin/chronyd
4026532633 mnt        1   806 root   /usr/sbin/NetworkManager --no-daemon
4026532641 mnt        1  1076 root   /usr/sbin/cupsd -f
[root@redisnode1 chroot]# cd /proc/18928
[root@redisnode1 18928]# ls
attr       clear_refs       cpuset   fd       limits     mem         net        oom_score      personality  schedstat  stack   syscall  wchan
autogroup  cmdline          cwd      fdinfo   loginuid   mountinfo   ns         oom_score_adj  projid_map   sessionid  stat    task
auxv       comm             environ  gid_map  map_files  mounts      numa_maps  pagemap        root         setgroups  statm   timers
cgroup     coredump_filter  exe      io       maps       mountstats  oom_adj    patch_state    sched        smaps      status  uid_map
[root@redisnode1 18928]# ls ns/
ipc  mnt  net  pid  user  uts
[root@redisnode1 18928]# ll ns/    # 可以看出来ns的user 和宿主机是同一个
total 0
lrwxrwxrwx. 1 root root 0 Mar  9 02:30 ipc -> ipc:[4026532506]
lrwxrwxrwx. 1 root root 0 Mar  9 02:30 mnt -> mnt:[4026532504]
lrwxrwxrwx. 1 root root 0 Mar  9 02:28 net -> net:[4026532509]
lrwxrwxrwx. 1 root root 0 Mar  9 02:30 pid -> pid:[4026532507]
lrwxrwxrwx. 1 root root 0 Mar  9 02:32 user -> user:[4026531837]
lrwxrwxrwx. 1 root root 0 Mar  9 02:30 uts -> uts:[4026532505]
  • 查看cgroup
    可以到进程下面查看所有的cgroup 信息,然后到cpu相关资源限制的子组中查看对应三个文件:tasks、cpu.cfs_period_us、cpu.cfs_quota_us

busybox: https://busybox.net/

namespace & cgroup 系列教程: https://segmentfault.com/a/1190000009732550

docker 官网: https://docs.docker.com/get-started/overview/

镜像文件存储:https://www.icode9.com/content-4-730441.html https://blog.csdn.net/haleycomet/article/details/52474524

posted @ 2022-03-17 12:26  QiaoZhi  阅读(622)  评论(0编辑  收藏  举报