白话Docker(NameSpace)

Docker容器无疑是当下最火的技术了。Docker通过镜像明确解决的是应用打包困难的问题。

docker

但是容器本身是没有什么价值的,有价值的是容器编排技术。
容器本身就像一个沙盒一样,将应用程序打包,让应用程序在沙盒中运行,而沙盒中的应用程序也能感知到沙盒的边界,被限制在沙盒之中。
Namespace就是修改进程视图的的方法,通过约束和修改进程的动态表现,从而为其创造一个“边界”。
Namespace提供了MOUNT,UTS ,IPC,PID,NETWORK,USER的视图隔离。

PID Namespace
[root@localhost ~]# docker run -it busybox /bin/sh
/ # ps aux
PID   USER     TIME  COMMAND
    1 root      0:00 /bin/sh
    7 root      0:00 ps aux

可以看到容器内部的1号进程是/bin/sh 。而容器内一共就只有两个进程。在容器中执行一条语句。

/ # sleep 100000
在容器中 再 docker exec -it xxx /bin/sh 进入查看其pid 为 38
/ # ps aux|grep sleep
   38 root      0:00 sleep 100000

而再宿主机上也有一个sleep 100000 的进程,其pid 为 5181
[root@localhost ~]# ps aux|grep sleep
root       5181  0.0  0.0   1284     4 pts/1    S+   22:23   0:00 sleep 100000

这就是Namespace技术。
其实这种技术很早就有了,Linux系统在创建线程的系统调用 clone() 中指定 CLONE_NEWPID 参数。

int pid = clone(main_function, stack_size,CLONE_NEWPID|SIGCHLD,NULL)

这就是Linux容器中最底层的实现了。
有了PID Namespace ,可以让进程觉得自己是 1号进程。有了Mount Namespace,会让进程只能看到自己的挂载的目录和文件。有了Network Namespace,会让进程只能看到Namespace里的网络设备。
所以说,其实Namespace只是障眼法而已

这样的障眼法提供了Linux Namespace的隔离性。但是这种隔离性其实还有很多不足之处。
  • 容器只是宿主机上的一种特殊的进程,那么多个容器之间使用的还是同一个宿主机的内核。(低版本内核的Linux是无法运行高版本内核的Linux容器的,同样Win的内核也运行不了Linux容器)
  • 虽说已经有了很多Namespace。但是时间是不能被Namespace化的,容器中调用了settimeofday()的系统调用,那么宿主机也会被更改。(尽管有seccomp技术,对容器中的系统调用有过滤,但是这会拖累容器性能,而且一般也不知道有多少这种坑的系统调用要被限制)

文件系统的Namespace

进到容器后,可以看到容器内部有 / ,也有 /etc 等等。其实之前提到了,docker 是方便开发者一次打包多地部署的困难点的,但是最容易忽视的是应用程序最大的依赖不是 jvm或者是 程序库,最大的依赖应该是操作系统本身。如果能将整个操作系统都打包才是真正解决了打包的困难。

这里可能想到了 有Mount Namespace 呀。有了它,容器的应用程序里面理应看到的是一份完全独立的文件系统。这样就可以在容器中操作容器内的目录(如 /tmp)。而不影响宿主机本身。

下面有一段小程序,在创建子进程时,开启指定的Namespace。

#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;
}

这段代码的功能时在main函数中,通过clone() 系统调用创建一个新的子进程,并声明为它启用Mount Namespace 。而这个子进程执行的是一个 shell。这个shell运行在 Mount Namespace 隔离环境中。

$ ls /tmp

但是执行后发现还是宿主机的tmp。
其实不难回答,光启用 Namespace 还不行。因为进入容器之后需要重新挂载一次。

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;
}

这时再执行 ls /tmp 。发现 /tmp 为空了。
这就是重新挂载了 /tmp 。
和其他的Namespace 不一样,Mount Namespace 想要生效,必须是伴随着挂载操作才能生效的
而在 Linux中,有一个 chroot 的命令。就是改变进程的根目录到指定的目录。
而这个挂载在容器根目录上,用来为容器提供隔离后执行环境的文件系统,就是所谓的“容器镜像”,也被称作“rootfs(根文件系统)”
所以对docker来讲。最核心的原理就是为待创建的用户进程

  1. 启用Linux Namespace配置
  2. 设置指定的 Cgroups 参数
  3. 切换进程的根目录(Change root)

还是需要明确一下。
rootfs 只是一个操作系统所包含的文件,配置和目录,并不包含操作系统的内核。内核参数是一个全局变量,一处修改,到处生效。

由于rootfs 里打包的不只是应用,而是整个操作系统的文件和目录,也就意味着,应用以及它运行所需要的所有依赖,都被封装在了一起。

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容