• k8s-容器技术-Mount Namespace


    问题

    • Mount Namespace 的动机是什么?
    • Mount Namespace 是如何引入 rootfs 的 ?

    概述

        这个章节介绍 Linux 相关的 Namespace 技术

    • linux 容器最基础的两种技术: Namespace 和 Cgroups . 先说什么是 Namespace
    • 对于 Docker 等大多数 Linux 容器来说,Cgroups 技术是用来制造约束的主要手段,而Namespace 技术则是用来修改进程视图的主要方法。
    • 容器,其实是一种特殊的进程而已。 从本篇文章的思想是从 Mount Namespace 延伸到 rootfs , 再延伸到可以修改的 rootfs ,进而介绍了 “可以修改的rootfs” 就是 Docker 中使用的 “Layer” 。

    Namespace 的动机

    本来,每当我们在宿主机上运行了一个 /bin/sh 程序,操作系统都会给它分配一个进程编号,比如 PID=100。这个编号是进程的唯一标识,就像员工的工牌一样。所以 PID=100,可以粗略地理解为这个 /bin/sh 是我们公司里的第 100 号员工,而第 1 号员工就自然是比尔 · 盖茨这样统领全局的人物。
    
    而现在,我们要通过 Docker 把这个 /bin/sh 程序运行在一个容器当中。这时候,Docker 就会在这个第 100 号员工入职时给他施一个“障眼法”,让他永远看不到前面的其他 99 个员工,更看不到比尔 · 盖茨。这样,他就会错误地以为自己就是公司里的第 1 号员工。
    
    这种机制,其实就是对被隔离应用的进程空间做了手脚,使得这些进程只能看到重新计算过的进程编号,比如 PID=1。可实际上,他们在宿主机的操作系统里,还是原来的第 100 号进程。
    
    这种技术,就是 Linux 里面的 Namespace 机制。
    

    Linux 中的 Namespace 技术

    Namespace 使得容器运行的那个进程与外界隔离,在容器内部是看不到外部其他进程的,在容器内部看到的进程只看到自己,如以下在容器中执行 ps命令.

    / # ps
    PID  USER   TIME COMMAND
      1 root   0:00 /bin/sh
      10 root   0:00 ps
    

    会看到自己运行着 PID = 1 的进程,而实际上该线程可能是宿主机的 PID = 100 的进程. 那个PID = 1 的好处是什么呢 ? 除了我们刚刚用到的 PID Namespace,Linux 操作系统还提供了 Mount、UTS、IPC、Network 和 User 这些 Namespace,用来对各种不同的进程上下文进行“障眼法”操作。 比如,Mount Namespace,用于让被隔离进程只看到当前 Namespace 里的挂载点信息;Network Namespace,用于让被隔离进程看到当前 Namespace 里的网络设备和配置。 这,就是 Linux 容器最基本的实现原理了。     Namespace 技术实际上修改了应用进程看待整个计算机“视图”,即它的“视线”被操作系统做了限制,只能“看到”某些指定的内容。Linux 中有很多种类型的 Namespace , 例如

    • PID Namespace
    • Mount Namespace
    • UTS namespaces
    • Network namespaces 等等, 关于 Linux 中的 Namespace 的更多知识可以阅读下面的网站 ,我们只需要的各种 Namespace 起到最大的作用就是"隔离" , 下面我们来学习一个 Mount Namespace

    Mount Namespace

    依据 Namespace 隔离的作用 , 不难知道 Mount 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!
    ");
      execv(container_args[0], container_args);
      printf("Something's wrong!
    ");
      return 1;
    }
     
    int main()
    {
      printf("Parent - start a container!
    ");
      // clone 方法表示创建了一个新的线程 , 
      int container_pid = clone(container_main, container_stack+STACK_SIZE, CLONE_NEWNS | SIGCHLD , NULL);
      waitpid(container_pid, NULL, 0);
      printf("Parent - container stopped!
    ");
      return 0;
    }
    

    这段代码的核心逻辑就是通过调用 linux 执行创建 Mount Namespace 的命令,下面我们将执行这段代码,将为我们展示执行 Mount Namespace 以后的效果

    $ gcc -o ns ns.c
    $ ./ns
    Parent - start a container!
    Container - inside the container!
    

    我们进到运用了 Mount Namespace 的容器内

    $ ls /tmp
    # 你会看到好多宿主机的文件
    
    

    不对呀,我们想要的是隔离的文件系统环境呀,现在和宿主机的文件系统一样的话,就不是隔离了 , 那么有没有可能通过某种方式达到我们想要的效果呢?我们尝试了以下的代码.

    int container_main(void* arg)
    {
      printf("Container - inside the container!
    ");
      // 如果你的机器的根目录的挂载类型是 shared,那必须先重新挂载根目录
      // mount("", "/", NULL, MS_PRIVATE, "");
      mount("none", "/tmp", "tmpfs", 0, "");
      execv(container_args[0], container_args);
      printf("Something's wrong!
    ");
      return 1;
    }
    

    再次运行

    $ gcc -o ns ns.c
    $ ./ns
    Parent - start a container!
    Container - inside the container!
    $ ls /tmp
    

    让我们看一下宿主机是否存在这个 tmp 目录

    # 在宿主机上
    $ mount -l | grep tmpfs
    
    

    通过上面的实践我们得到两个消息 :

    • 新创建的容器会直接继承宿主机的各个挂载点 .
    • 在新的进程中执行 mount 命令, mount 的效果才会生效 . 这就是 Mount Namespace 跟其他 Namespace 的使用略有不同的地方:它对容器进程视图的改变,一定是伴随着挂载操作(mount)才能生效。

    不难想到,我们可以在容器进程启动之前重新挂载它的整个根目录“/”。而由于 Mount Namespace 的存在,这个挂载对宿主机不可见,所以容器进程就可以在里面随便折腾了。要是我提前设置需要挂载的文件夹,然后有种方法可以让新进程只在前面挂载的文件夹操作,而不看到或涉及关于宿主相关的文件夹就好了. 确实有这么一种方法,我们将会介绍 chroot .

    chroot (change root file system)

    chroot是啥,将会产生啥作用呢?见以下实践 . 假设,我们现在有一个 $HOME/test 目录,想要把它作为一个 /bin/bash 进程的根目录。首先,创建一个 test 目录和几个 lib 文件夹:

    $ mkdir -p $HOME/test
    $ mkdir -p $HOME/test/{bin,lib64,lib}
    $ cd $T
    

    然后,把 bash 命令拷贝到 test 目录对应的 bin 路径下:

    $ cp -v /bin/{bash,ls} $HOME/test/bin
    

    接下来,把 bash 命令需要的所有 so 文件,也拷贝到 test 目录对应的 lib 路径下。找到 so 文件可以用 ldd 命令:

    $ T=$HOME/test
    $ 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 进程的根目录:

    $ chroot $HOME/test /bin/bash
    

    这时,你如果执行 "ls /",就会看到,它返回的都是 $HOME/test 目录下面的内容,而不是宿主机的内容。更重要的是,对于被 chroot 的进程来说,它并不会感受到自己的根目录已经被“修改”成 $HOME/test 了。这种视图被修改的原理,是不是跟我之前介绍的 Linux Namespace 很类似呢? 实际上,Mount Namespace 正是基于对 chroot 的不断改良才被发明出来的,它也是 Linux 操作系统里的第一个 Namespace。

    分析 Ubuntu16.04 镜像

    如果你创建过一个 Ubuntu16.04的镜像你就会知道里面的文件系统和真实的 Ubuntu 文件系统一模一样 , 实际上这就是我们前面讲的在新进程创建的时候顺便就挂载好我们需要的文件系统在容器的根目录上,用来为容器进程提供隔离后执行环境的文件系统,就是所谓的“容器镜像”。

    它还有一个更为专业的名字,叫作:rootfs(根文件系统)

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

    也就是说同一台机器上的所有容器,都共享宿主机操作系统的内核. 为什么我们可以在 Centos 运行 Ubuntu16.04 的镜像,然后达到和运行在 Ubuntu16.04 一样的效果,就是说实际上这是更改了内核参数,这样就和宿主的内核参数不同了,所以就可以达到不同系统的效果.这也可以想到 Centos 系统是不可能运行 window 这样的镜像的,因为他们的内核都不同.这就意味着,如果你的应用程序需要配置内核参数、加载额外的内核模块,以及跟内核进行直接的交互,你就需要注意了:这些操作和依赖的对象,都是宿主机操作系统的内核,它对于该机器上的所有容器来说是一个“全局变量”,牵一发而动全身,这就是镜像的弊端.

    rootfs

    前面我们已经知道了 rootfs是什么了,但是假如是这样, 假如你和同事都在 Ubuntu 镜像进行开发,每次难道你们两都要提交完整的 Ubuntu 镜像吗?有没有想 git命令推送到远程仓库时,那样只提交修改过的文件,其他的不懂呢?有的

    增量修改的 rootfs

    Docker 在镜像的设计中,引入了层(layer)的概念。也就是说,用户制作镜像的每一步操作,都会生成一个层,也就是一个增量 rootfs。 而为了实现增量修改的 rootfs 需要用到一种叫做 联合文件系统(Union File System) 的功能 .Union File System 也叫 UnionFS,最主要的功能是将多个不同位置的目录联合挂载(union mount)到同一个目录下。比如,我现在有两个目录 A 和 B,它们分别有两个文件: 例子存在以下的文件路径 :

    $ tree
    .
    ├── A
    │  ├── a
    │  └── x
    └── B
      ├── b
      └── x
    

    然后,我使用联合挂载的方式,将这两个目录挂载到一个公共的目录 C 上:

    $ mkdir C
    $ mount -t aufs -o dirs=./A:./B none ./C
    

    这时,我再查看目录 C 的内容,就能看到目录 A 和 B 下的文件被合并到了一起:

    $ tree ./C
    ./C
    ├── a
    ├── b
    └── x
    

    可以看到,在这个合并后的目录 C 里,有 a、b、x 三个文件,并且 x 文件只有一份。这,就是“合并”的含义。此外,如果你在目录 C 里对 a、b、x 文件做修改,这些修改也会在对应的目录 A、B 中生效。 Linux各发行版实现的UnionFS各不相同,所以Docker在不同linux发行版中使用的也不同。你可以通过docker info来查看docker使用的是哪种,比如:

    • centos, docker18.03.1-ce: Storage Driver: overlay2
    • debain, docker17.03.2-ce: Storage Driver: aufs

    这里不再深入分层应用技术,下面介绍分层镜像.以Ubuntu为例子

    1297993-20210524223004526-121533258.png

    我们看到这个有三个层次

    • 可读写层
    • Init 层
    • 只读层

    可读写层

    它是这个容器的 rootfs 最上面的一层(6e3be5d2ecccae7cc),它的挂载方式为:rw,即 read write。在没有写入文件之前,这个目录是空的。而一旦在容器里做了写操作,你修改产生的内容就会以增量的方式出现在这个层中。
    
    可是,你有没有想到这样一个问题:如果我现在要做的,是删除只读层里的一个文件呢?
    
    为了实现这样的删除操作,AuFS 会在可读写层创建一个 whiteout 文件,把只读层里的文件“遮挡”起来。
    
    比如,你要删除只读层里一个名叫 foo 的文件,那么这个删除操作实际上是在可读写层创建了一个名叫.wh.foo 的文件。这样,当这两个层被联合挂载之后,foo 文件就会被.wh.foo 文件“遮挡”起来,“消失”了。这个功能,就是“ro+wh”的挂载方式,即只读 +whiteout 的含义。我喜欢把 whiteout 形象地翻译为:“白障”。
    
    所以,最上面这个可读写层的作用,就是专门用来存放你修改 rootfs 后产生的增量,无论是增、删、改,都发生在这里。而当我们使用完了这个被修改过的容器之后,还可以使用 docker commit 和 push 指令,保存这个被修改过的可读写层,并上传到 Docker Hub 上,供其他人使用;而与此同时,原先的只读层里的内容则不会有任何变化。这,就是增量 rootfs 的好处。
    

    总的来说 ,该层主要的特点就是 "增量修改"

    Init 层

    它是一个以“-init”结尾的层,夹在只读层和读写层之间。Init 层是 Docker 项目单独生成的一个内部层,专门用来存放 /etc/hosts、/etc/resolv.conf 等信息。
    
    需要这样一层的原因是,这些文件本来属于只读的 Ubuntu 镜像的一部分,但是用户往往需要在启动容器时写入一些指定的值比如 hostname,所以就需要在可读写层对它们进行修改。
    
    可是,这些修改往往只对当前的容器有效,我们并不希望执行 docker commit 时,把这些信息连同可读写层一起提交掉。
    
    所以,Docker 做法是,在修改了这些文件之后,以一个单独的层挂载了出来。而用户执行 docker commit 只会提交可读写层,所以是不包含这些内容的。
    
    最终,这 7 个层都被联合挂载到 /var/lib/docker/aufs/mnt 目录下,表现为一个完整的 Ubuntu 操作系统供容器使用。
    

    这一层最常见的就是我们的配置文件,一般这些文件都不希望被提交(例如 : 你去远程拉去 Ubuntu 镜像 hosts 文件则是需要自己配置的,你修改过后 hosts 提交镜像的时候 , hosts 文件是不应该提交上去的)

    只读层

    这一层最后理解, docker 镜像的核心文件,只可读.

    参考资料

    • 《深入剖析Kubernetes》课程
  • 相关阅读:
    xml 总结(一)数据岛,命名空间
    activiti designer 安装到 myeclipse
    activiti5.15 学习笔记
    goole网址IP
    form 中Enctype=multipart/form-data 的作用
    上传文件form表单enctype="multipart/form-data"传值解决办法(代原代码)
    淘宝初始化样式
    js闭包使用之处
    CSS Sprites
    iframe用的场景
  • 原文地址:https://www.cnblogs.com/Benjious/p/14856849.html
Copyright © 2020-2023  润新知