Docker 菜鸟篇

2019-02-12  本文已影响3人  33d31a1032df

容器是特殊的进程

容器只是运行在宿主机上的一种特殊的进程,使用的还是同一个宿主机的操作系统内核。

注意:

用 docker 演示启动一个 nginx 服务,操作如下:

  1. 准备一台机器,配置如下:
机器名 IP 系统 内核 配置
centos 192.168.56.103 centos 7.5.1804 linux 3.10.0 2 核 / 4G 内存

安装如下常用工具:

[root@centos ~]# yum install -y vim wget tree
  1. 安装 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
  1. 运行一个 nginx 容器
[root@centos ~]# docker run -d --name nginx nginx:1.12.2
  1. 查看宿主机进程
[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
  1. 最后记得关闭 nginx 容器
[root@centos ~]# docker container rm -f nginx

容器是一种沙盒技术

容器技术的核心功能,就是通过约束和修改进程的动态表现,从而为其创造出一个“边界”。

对于 Docker 项目来说,它最核心的原理实际上就是为待创建的用户进程:

  1. 启用 Linux Namespace 配置
  2. 设置 Linux Control Group 参数
  3. 切换进程的根目录(change root file system)

Linux Namesapce

Namespace 技术实际上修改了应用进程看待整个计算机“视图”,即它的“视线”被操作系统做了限制,只能“看到”某些指定的内容。

用 docker 执行 ping 命令,演示 PID Namespace,操作如下:

  1. 运行一个 busybox 容器(busybox 是一个软件工具箱,里面集成了些常用的命令以及工具),并执行 ping 命令
[root@centos ~]# docker run -d --name busybox busybox:1.29.3 ping baidu.com
  1. 查看 busybox 容器内的进程,ping 进程的 PID 为 1
[root@centos ~]# docker exec busybox ps
PID   USER     TIME  COMMAND
    1 root      0:00 ping baidu.com
  1. 查看宿主机进程,同一个 ping 进程的 PID 却为 1744
[root@centos ~]# ps -ef | grep ping
root      1744  1728  0 15:53 ?        00:00:00 ping baidu.com
  1. 最后记得关闭 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。用来对各种不同的进程上下文进程“障眼法”操作,比如:

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,操作如下:

  1. 运行一个 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
  1. 查看宿主机 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
  1. 查看 /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
  1. 最后记得关闭 busybox 容器
[root@centos ~]# docker container rm -f busybox

一段小程序

用 C 演示 Mount Namespace 的使用,让被隔离进程只看到当前 Namespace 里的 /tmp 目录。

  1. 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
  1. 安装编译环境
[root@centos ~]# yum install -y gcc gcc-c++ cmake
  1. 编译 C 代码
[root@centos ~]# gcc -o ns ns.c
  1. 观察宿主机的挂载信息,并查看 /tmp 目录,你会看到好多文件
[root@centos ~]# mount -l | grep /tmp
[root@centos ~]# ls /tmp
  1. 执行程序,进入隔离进程,观察挂载信息,并查看 /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
  1. 退出隔离进程,再次观察宿主机的挂载信息,和查看 /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 进程的根目录。

  1. 创建 fs 目录
[root@centos ~]# mkdir -p fs/{bin,lib64}
  1. 拷贝 bash 和 ls 命令到 fs 目录对应的 bin 路径下
[root@centos ~]# cp /bin/{bash,ls} fs/bin
  1. 把 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
  1. 执行 chroot 命令,告诉操作系统,我们将使用 fs 目录作为 /bin/bash 进程的根目录,并查看“/”目录下的文件
[root@centos ~]# chroot fs /bin/bash
bash-4.2# /bin/ls /
bin  lib64

我们发现,它返回的都是 fs 目录下面的内容,而不是宿主机的内容。

更重要的是,对于被 chroot 的进程来说,它并不会感受到自己的根目录已经被“修改”成 fs 目录了。

需要明确的是,rootfs 只是一个操作系统所包含的文件、配置和目录,并不包含操作系统内核。实际上,同一台机器上的所有容器,都共享宿主机操作系统的内核。

这就意味着,如果你的应用程序需要配置内核参数、加载额外的内核模块,以及跟内核进行直接的交互,你就需要注意了:这些操作和依赖的对象,都是宿主机操作系统的内核,它对于该机器上的所有容器来说是一个“全局变量”,牵一发而动全身。

本文内容摘自极客时间的《深入剖析Kubernetes》

深入剖析Kubernetes
上一篇下一篇

猜你喜欢

热点阅读