容器是特殊的进程
容器只是运行在宿主机上的一种特殊的进程,使用的还是同一个宿主机的操作系统内核。
注意:
- 不能在 Windows 宿主机上运行 Linux 容器
- 不能在低版本的 Linux 宿主机上运行高版本的 Linux 容器
用 docker 演示启动一个 nginx 服务,操作如下:
- 准备一台机器,配置如下:
机器名 | IP | 系统 | 内核 | 配置 |
---|---|---|---|---|
centos | 192.168.56.103 | centos 7.5.1804 | linux 3.10.0 | 2 核 / 4G 内存 |
安装如下常用工具:
[root@centos ~]# yum install -y vim wget tree
- 安装 docker-1.13.1
[root@centos ~]# yum install -y docker-1.13.1-75.git8633870.el7.centos.x86_64
[root@centos ~]# systemctl start docker
[root@centos ~]# systemctl enable docker
- 运行一个 nginx 容器
[root@centos ~]# docker run -d --name nginx nginx:1.12.2
- 查看宿主机进程
[root@centos ~]# ps -ef | grep nginx
root 2756 2741 0 17:44 ? 00:00:00 nginx: master process nginx -g daemon off;
101 2778 2756 0 17:44 ? 00:00:00 nginx: worker process
- 最后记得关闭 nginx 容器
[root@centos ~]# docker container rm -f nginx
容器是一种沙盒技术
容器技术的核心功能,就是通过约束和修改进程的动态表现,从而为其创造出一个“边界”。
对于 Docker 项目来说,它最核心的原理实际上就是为待创建的用户进程:
- 启用 Linux Namespace 配置
- 设置 Linux Control Group 参数
- 切换进程的根目录(change root file system)
Linux Namesapce
Namespace 技术实际上修改了应用进程看待整个计算机“视图”,即它的“视线”被操作系统做了限制,只能“看到”某些指定的内容。
用 docker 执行 ping 命令,演示 PID Namespace,操作如下:
- 运行一个 busybox 容器(busybox 是一个软件工具箱,里面集成了些常用的命令以及工具),并执行 ping 命令
[root@centos ~]# docker run -d --name busybox busybox:1.29.3 ping baidu.com
- 查看 busybox 容器内的进程,ping 进程的 PID 为 1
[root@centos ~]# docker exec busybox ps
PID USER TIME COMMAND
1 root 0:00 ping baidu.com
- 查看宿主机进程,同一个 ping 进程的 PID 却为 1744
[root@centos ~]# ps -ef | grep ping
root 1744 1728 0 15:53 ? 00:00:00 ping baidu.com
- 最后记得关闭 busybox 容器
[root@centos ~]# docker container rm -f busybox
这种技术,就是 Linux 里面的 Namespace 机制。而 Namespace 的使用方式也非常有意思:它其实只是 Linux 创建新进程的一个可选参数。
我们知道,在 Linux 系统中创建线程的系统调用是 clone(),比如:
int pid = clone(main_function, stack_size, SIGCHLD, NULL);
这个系统调用就会为我们创建一个新的进程,并且返回它的进程号 PID。
而当我们用 clone() 系统调用创建一个新进程时,就可以在参数中指定 CLONE_NEWPID 参数,比如:
int pid = clone(main_function, stack_size, CLONE_NEWPID | SIGCHLD, NULL);
这时,新创建的这个进程将会“看到”一个全新的进程空间,在这个进程空间里,它的 PID 是 1,之所有说“看到”,是因为这只是一个“障眼法”,在宿主机真实的进程空间里,这个进程的 PID 还是真实的数值,比如:1744。
而除了刚刚用到的 PID Namespace,Linux 操作系统还提供了 Mount、UTS、IPC、Network 和 User 这些 Namespace。用来对各种不同的进程上下文进程“障眼法”操作,比如:
- Mount Namespace 用于让被隔离进程只看到当前 Namespace 里的挂载点信息
- Network Namespace 用于让被隔离进程只看到当前 Namespace 里的网络设备和配置
Linux Control Group
Linux CGroup 的主要作用就是限制进程组能够使用的资源上限,包括 CPU、内存、磁盘、网络带宽等等。
在 Linux 中,CGroup 给用户暴露出来的操作接口是文件系统,即它以文件和目录的方式组织在操作系统的 /sys/fs/cgroup 路径下。可以通过 mount 指令把它们展示出来,比如:
[root@centos ~]# 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/hugetlb type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,hugetlb)
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/pids type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,pids)
cgroup on /sys/fs/cgroup/blkio type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,blkio)
cgroup on /sys/fs/cgroup/cpuset type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,cpuset)
cgroup on /sys/fs/cgroup/cpu,cpuacct type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,cpuacct,cpu)
cgroup on /sys/fs/cgroup/perf_event type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,perf_event)
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)
用 docker 执行 while 循环,演示 CPU CGroup,操作如下:
- 运行一个 busybox 容器,并限制只允许使用 20% 的 CPU,while 循环可以模拟跑满 CPU,操作如下:
[root@centos ~]# docker run -d --cpu-period=100000 --cpu-quota=20000 --name busybox busybox:1.29.3 /bin/sh -c "while : ; do : ; done"
52f0ea4715b26f56bb27b46aedaaa326c24040afe520f840e18ace3f7bf99e19
- 查看宿主机 top
[root@centos ~]# top
top - 17:36:55 up 1:53, 1 user, load average: 0.01, 0.06, 0.16
Tasks: 99 total, 2 running, 97 sleeping, 0 stopped, 0 zombie
%Cpu0 : 0.3 us, 0.3 sy, 0.0 ni, 99.3 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
%Cpu1 : 7.3 us, 13.0 sy, 0.0 ni, 79.7 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
KiB Mem : 3881016 total, 3492996 free, 146688 used, 241332 buff/cache
KiB Swap: 4063228 total, 4063228 free, 0 used. 3475308 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
4517 root 20 0 1236 4 0 R 20.3 0.0 0:06.48 sh
- 查看 /sys/fs/cgroup
[root@centos ~]# cat /sys/fs/cgroup/cpu/system.slice/docker-52f0ea4715b26f56bb27b46aedaaa326c24040afe520f840e18ace3f7bf99e19.scope/cpu.cfs_period_us
100000
[root@centos ~]# cat /sys/fs/cgroup/cpu/system.slice/docker-52f0ea4715b26f56bb27b46aedaaa326c24040afe520f840e18ace3f7bf99e19.scope/cpu.cfs_quota_us
20000
- 最后记得关闭 busybox 容器
[root@centos ~]# docker container rm -f busybox
一段小程序
用 C 演示 Mount Namespace 的使用,让被隔离进程只看到当前 Namespace 里的 /tmp 目录。
- C 代码
[root@centos ~]# cat > ns.c <<EOF
#define _GNU_SOURCE
#include <sys/mount.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <sched.h>
#include <signal.h>
#include <unistd.h>
#define STACK_SIZE (1024 * 1024)
static char container_stack[STACK_SIZE];
char* const container_args[] = {
"/bin/bash",
NULL
};
int container_main(void* arg)
{
printf("Container - inside the container!\n");
// 如果你的机器的根目录的挂载类型是 shared,那必须先重新挂载根目录
mount("", "/", NULL, MS_PRIVATE, "");
mount("none", "/tmp", "tmpfs", 0, "");
execv(container_args[0], container_args);
printf("Something's wrong!\n");
return 1;
}
int main()
{
printf("Parent - start a container!\n");
int container_pid = clone(container_main, container_stack+STACK_SIZE, CLONE_NEWNS | SIGCHLD , NULL);
waitpid(container_pid, NULL, 0);
printf("Parent - container stopped!\n");
return 0;
}
EOF
- 安装编译环境
[root@centos ~]# yum install -y gcc gcc-c++ cmake
- 编译 C 代码
[root@centos ~]# gcc -o ns ns.c
- 观察宿主机的挂载信息,并查看 /tmp 目录,你会看到好多文件
[root@centos ~]# mount -l | grep /tmp
[root@centos ~]# ls /tmp
- 执行程序,进入隔离进程,观察挂载信息,并查看 /tmp 目录,你不会看到任何宿主机的文件
[root@centos ~]# ./ns
Parent - start a container!
Container - inside the container!
[root@centos ~]# mount -l | grep /tmp
none on /tmp type tmpfs (rw,relatime,seclabel)
[root@centos ~]# ls /tmp
- 退出隔离进程,再次观察宿主机的挂载信息,和查看 /tmp 目录,你又能看到好多文件
[root@centos ~]# exit
exit
Parent - container stopped!
[root@centos ~]# mount -l | grep /tmp
[root@centos ~]# ls /tmp
不难想到,我们可以在容器进程启动之前重新挂载它的整个根目录“/”。而由于 Mount Namespace 的存在,这个挂载对宿主机不可见,所以容器就可以在里面随便折腾了。
而这个挂载在容器根目录上,用来为容器进程提供隔离后执行环境的文件系统,就是所谓的“容器镜像”。它还有一个更为专业的名字,叫做:rootfs(根文件系统)。
rootfs
假设,我们现在有一个 fs 目录,想要把它作为一个 /bin/bash 进程的根目录。
- 创建 fs 目录
[root@centos ~]# mkdir -p fs/{bin,lib64}
- 拷贝 bash 和 ls 命令到 fs 目录对应的 bin 路径下
[root@centos ~]# cp /bin/{bash,ls} fs/bin
- 把 bash 和 ls 命令所需要的 so 文件也拷贝到 fs 目录对应的 lib 路径下
[root@centos ~]# files=$(ldd /bin/{bash,ls} | egrep -o '/lib.*\.[0-9]' | sort | uniq)
[root@centos ~]# for file in $files; do cp $file fs$file; done
[root@centos ~]# tree fs
fs
├── bin
│ ├── bash
│ └── ls
└── lib64
├── ld-linux-x86-64.so.2
├── libacl.so.1
├── libattr.so.1
├── libcap.so.2
├── libc.so.6
├── libdl.so.2
├── libpcre.so.1
├── libpthread.so.0
├── libselinux.so.1
└── libtinfo.so.5
2 directories, 12 files
- 执行 chroot 命令,告诉操作系统,我们将使用 fs 目录作为 /bin/bash 进程的根目录,并查看“/”目录下的文件
[root@centos ~]# chroot fs /bin/bash
bash-4.2# /bin/ls /
bin lib64
我们发现,它返回的都是 fs 目录下面的内容,而不是宿主机的内容。
更重要的是,对于被 chroot 的进程来说,它并不会感受到自己的根目录已经被“修改”成 fs 目录了。
需要明确的是,rootfs 只是一个操作系统所包含的文件、配置和目录,并不包含操作系统内核。实际上,同一台机器上的所有容器,都共享宿主机操作系统的内核。
这就意味着,如果你的应用程序需要配置内核参数、加载额外的内核模块,以及跟内核进行直接的交互,你就需要注意了:这些操作和依赖的对象,都是宿主机操作系统的内核,它对于该机器上的所有容器来说是一个“全局变量”,牵一发而动全身。
本文内容摘自极客时间的《深入剖析Kubernetes》

网友评论