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
- 我们删除掉所有的容器然后重新测试。
# 删除掉所有的容器
[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
- 启动一个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, 有时候在容器内部启动一些程序会没有权限。
- 宿主机查看其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