k8s 学习笔记四之容器概念

namespace

为了与宿主机的一些相同组件与相同功能使用范围进行区分,于是有了 namespace。 开发中的 namepsace,像这样 com.matosiki.service.用点区分 而在 docker 容器中,也会使用 namespace 去区分与宿主机相同的组件

如 PID Namespace ,Mount Namespace, Network Namespace 等

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

cgroup

Linux Cgroups 就是 Linux 内核中用来为进程设置资源限制

cgroup 能对进程的优先级、审计以及进程的挂起和恢复等操作。

在 Linux 中,Cgroups 给用户暴露出来的操作接口是文件系统,即它以文件和目录的方式组织在操作系统的 /sys/fs/cgroup 路径下。在 Ubuntu 16.04 机器里,我可以用 mount 指令把它们展示出来,这条命令是:

1
mount -t cgroup

在 /sys/fs/cgroup 下面有很多诸如 cpuset、cpu、 memory 这样的子目录,也叫子系统。这些都是我这台机器当前可以被 Cgroups 进行限制的资源种类。而在子系统对应的资源种类下,你就可以看到该类资源具体可以被限制的方法。比如,对 CPU 子系统来说,我们就可以看到如下几个配置文件.

1
ls /sys/fs/cgroup/cpu

进入 /sys/fs/cgroup/cpu目录

1
2
sudo mkdir -p /sys/fs/cgroup/cpu/container
cd /sys/fs/cgroup/cpu/container

这个目录就称为一个“控制组”。你会发现,操作系统会在你新创建的 container 目录下,自动生成该子系统对应的资源限制文件。

1
while : ; do : ; done &

查看 cpu 使用

1
top
1
2
# 记住这个pid
519 wx11055   20   0   23044   3244      8 R 88.3  0.2   0:16.54 bash

在输出里可以看到,CPU 的使用率已经 100% 了(%Cpu0 :100.0 us)。

而此时,我们可以通过查看 container 目录下的文件,看到 container 控制组里的 CPU quota 还没有任何限制(即:-1),CPU period 则是默认的 100 ms(100000 us):

1
2
3
4
# -1
cat /sys/fs/cgroup/cpu/container/cpu.cfs_quota_us
# 默认是100000
cat /sys/fs/cgroup/cpu/container/cpu.cfs_period_us

接下来,我们可以通过修改这些文件的内容来设置限制。

比如,向 container 组里的 cfs_quota 文件写入 20 ms(20000 us):

1
sudo echo 20000 > /sys/fs/cgroup/cpu/container/cpu.cfs_quota_us

结合前面的介绍,你应该能明白这个操作的含义,它意味着在每 100 ms 的时间里,被该控制组限制的进程只能使用 20 ms 的 CPU 时间,也就是说这个进程只能使用到 20% 的 CPU 带宽。 接下来,我们把被限制的进程的 PID 写入 container 组里的 tasks 文件,上面的设置就会对该进程生效了:

1
2
# 将上面的pid 519 写入tasks文件中
sudo echo 519 > /sys/fs/cgroup/cpu/container/tasks
1
2
top
# 此时cpu立刻降低到20%

总结: 容器是一个“单进程”模型

容器镜像

编译 c 语言源码,生成一个 bash 子进程模拟一个容器视图。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#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");
  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;
}
1
2
3
4
5
$ gcc -o ns ns.c
$ sudo chmod u+x ns
$ ./ns
Parent - start a container!
Container - inside the container!

Mount Namespace 修改的,是容器进程对文件系统“挂载点”的认知

vim ns.c

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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;
}

再次实验

1
2
3
4
5
$ gcc -o ns ns.c
$ ./ns
Parent - start a container!
Container - inside the container!
$ ls /tmp

可以看到,这次 /tmp 变成了一个空目录,这意味着重新挂载生效了。我们可以用 mount -l 检查一下:

1
mount -l | grep tmpfs

可以看到,容器里的 /tmp 目录是以 tmpfs 方式单独挂载的。

更重要的是,因为我们创建的新进程启用了 Mount Namespace,所以这次重新挂载的操作,只在容器进程的 Mount Namespace 中有效。如果在宿主机上用 mount -l 来检查一下这个挂载,你会发现它是不存在的:

1
2
3
# 在宿主机上

$ mount -l | grep tmpfs

这就是 Mount Namespace 跟其他 Namespace 的使用略有不同的地方:它对容器进程视图的改变,一定是伴随着挂载操作(mount)才能生效。

使用 chroot 改变视图方法

1
2
3
4
5
6
7
8
$ mkdir -p $HOME/test
$ mkdir -p $HOME/test/{bin,lib64,lib}
$ T=$HOME/test
$ cd $T
$ cp -v /bin/{bash,ls} $HOME/test/bin
$ list="$(ldd /bin/ls | egrep -o '/lib.*\.[0-9]')"
$ for i in $list; do cp -v "$i" "${T}${i}"; done
$ chroot $HOME/test /bin/bash  # 然后把test目录设置成根目录

** 实际上,Mount Namespace 正是基于对 chroot 的不断改良才被发明出来的,它也是 Linux 操作系统里的第一个 Namespace。** 而这个挂载在容器根目录上、用来为容器进程提供隔离后执行环境的文件系统,就是所谓的“容器镜像”。它还有一个更为专业的名字,叫作:rootfs(根文件系统)。

docker 项目原理就是

1
2
3
4
5
1.启用 Linux Namespace 配置;

2.设置指定的 Cgroups 参数;

3.切换进程的根目录(Change Root)。

需要明确的是,rootfs 只是一个操作系统所包含的文件、配置和目录,并不包括操作系统内核。在 Linux 操作系统中,这两部分是分开存放的,操作系统只有在开机启动时才会加载指定版本的内核镜像。 rootfs 只包括了操作系统的“躯壳”,并没有包括操作系统的“灵魂”