kubernetes 的挂载传播(mount propagation)机制

概述

今天在看 kubectl-debug 这个项目的时候,看到其部署文件的 volumeMounuts 中使用了一个 mountPropagation 字段,因为不清楚这个字段的作用,就做了一下了解。mount propagation 背后的东西还是很多的,因此整理了这篇文章,顺便梳理一下知识点。

kubernetes 的 mount propagation 翻译成中文就是挂载传播。挂载传播提供了共享卷挂载的能力,它允许在同一个 Pod,甚至同一个节点内,在多个容器之间共享卷的挂载。

kubernetes 的挂载传播

卷的挂载传播由 Container.volumeMounts 的 mountPropagation 字段控制。它的值有:

  • None: 这种卷挂载将不会收到任何后续由 host 创建的在这个卷上或其子目录上的挂载。同样的,由容器创建的挂载在 host 上也是不可见的。这是默认的模式。这个其实很好理解,就是容器内和 host 的后续挂载完全隔离。

  • HostToContainer: 这种卷挂载将会收到之后所有的由 host 创建在该卷上或其子目录上的挂载。换句话说,如果 host 在卷挂载内挂载的任何内容,在容器中都是可见的。同样,如果任何具有 Bidirectional 的 Pod 挂载传播到该卷挂载上,具有 HostToContainer 的挂载传播都可以看见。整个挂载传播的流程如下:

  • Bidirectional: 这种挂载机制和 HostToContainer 类似。此外,任何在容器中创建的挂载都会传播到 host,然后传播到使用相同卷的所有 Pod 的所有容器。注意:Bidirectional 挂载传播是很危险的。可能会危害到 host 的操作系统。因此只有特权容器在允许使用它。

在了解了这几种挂载传播之后,我们可以做一些实验来验证一下,首先验证的是 None 的挂载传播类型,我们创建一个 nginx 的Pod:

apiVersion: v1
kind: Pod
metadata:
    name: mount-a
    namespace: default
    label:
      app: mount
spec:
    containers:
    - name: main
      image: nginx:latest
      volumeMounts:
      - name: testmount
        mountPath: /home
        mountPropagation: None
    volumes:
    - name: testmount
      hostPath:
        path: /mnt/

然后我们分别向 host 的 /mnt 和容器的 /home 下挂载目录并查看容器和 host 的情况:

容器中:

$ kubectl exec -it mount-a sh
$ cd /home
$ ls 
sda1

host 上:

$ cd /mnt
$ ls 
sda1

然后在 host 上创建挂载:

$ mkdir /mnt/none
$ sudo mount --bind /var /mnt/none
$ ls none
cache  empty  lib  lock  log  run  spool  tmp

这个时候,我们再看容器中的文件:

$ ls none
# 无输出

这说明 host 上在该卷下的挂载并不会改变容器中的文件。接下来我们可以在容器中按照上面的方案来验证容器中的挂载也不会影响 host 中的目录视图。这里就不展示了。接下来看一下 HostToContainer 的挂载传播,我们将上面的 Pod 的 mountPropagation 字段改成 HostToContainer,然后先取消 host 上的挂载:

sudo umoint /mnt/none

然后重新创建 Pod,和上面一样,在 host 上创建挂载,查看容器中的挂载情况:

$ ls none
cache  empty  lib  lock  log  run  spool  tmp

host 上的挂载因为 HostToContainer 机制传播到了容器中。我们继续看最后一种 Bidirectional 机制。这次我们要创建两个 Pod: mount-a, mount-b,并把 mountPropagation 字段改成 Bidirectional。注意,因为 Bidirectional 是危险的,所以只有特权容器才可以使用。因此这里还需要把容器改成特权模式,最后在 mount-a 中的容器执行挂载,验证挂载是否传播到 host 和 mount-b 的容器中。

apiVersion: v1
kind: Pod
metadata:
    name: mount-a
    namespace: default
    labels:
      app: mount
spec:
    containers:
    - name: main
      image: nginx:latest
      securityContext:
        privileged: true
      volumeMounts:
      - name: testmount
        mountPath: /home
        mountPropagation: Bidirectional
    volumes:
    - name: testmount
      hostPath:
        path: /mnt/
---
apiVersion: v1
kind: Pod
metadata:
    name: mount-b
    namespace: default
    labels:
      app: mount
spec:
    containers:
    - name: main
      image: nginx:latest
      securityContext:
        privileged: true
      volumeMounts:
      - name: testmount
        mountPath: /home
        mountPropagation: Bidirectional
    volumes:
    - name: testmount
      hostPath:
        path: /mnt/

然后进入 mount-a,创建挂载:

$ kubectl exec -it mount-a sh
$ su
$ mount --bind /var /home/none
$ ls /home/none
backups  lib    lock  mail  run    tmp
cache    local  log   opt   spool

这时候查看 host 下的 /mnt/none:

$ ls /mnt/none
backups  lib    lock  mail  run    tmp
cache    local  log   opt   spool

可以发现,容器中的挂载传播到了 host 上。这时候再查看 mount-b 中的容器。

$ ls /home/none
backups  lib    lock  mail  run    tmp
cache    local  log   opt   spool

挂载也传播到了 mount-b 的容器中。

linux mount 的几种类型

上面分析了 kubernetes 的挂载传播机制,在 linux mount 中,也有类似的概念。mount 分为下面几种:

  • shared mount: 相当于上面所说的 Bidirectional 的挂载传播
  • slave mount: 每个 slave mount 都有一个 shared master mount,挂载传播只能从 master -> slave,等同于上面的 HostToContainer, host 是 master,container 是 slave。
  • private mount: 很明显,private 就是相当于 None,挂载不会向任何一方传播。
  • unbindable mount:unbindable mount 其实就是 unbindable private mount,也就是不允许使用 --bind 的挂载。

mount namespace 的机制

kubernetes 的挂载传播不是其本身实现的,也不是 docker 之类的容器运行时提供的。这是由容器化技术的基础:linux namespace 提供的,linux namespace 当前共有 6 种:

  • cgroup namespace: 隔离 cgroup 根目录
  • pid namespace: 隔离进程 id
  • ipc namespace: 隔离 System V IPC, POSIX message queues
  • uts namespace: 隔离 Hostname 和 NIS domain name
  • user namespace: 隔离用户和用户组 ID
  • mount namespace: 隔离挂载点
  • network namespace: 隔离网络设备,网络栈,端口等

其中,mount namespace 是这篇文章的重点。我们可以通过 clone 调用来看看 mount namespace 的使用:

#define _GNU_SOURCE
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/mount.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 [%5d] - inside the container!\n", getpid());
    mount("none", "/", NULL, MS_REC|MS_PRIVATE, NULL);
    execv(container_args[0], container_args);
    printf("Something's wrong!\n");
    return 1;
}
int main()
{
    printf("Parent [%5d] - start a container!\n", getpid());
    /* 启用Mount Namespace - 增加CLONE_NEWNS参数 */
    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;
}

编译运行:

$ gcc main.c -o mount
$ sudo ./mount

然后尝试挂载,来验证挂载 MS_PRIVATE 的挂载传播问题。MS_PRIVATE 下 namespace 内和 host 应该是隔离的。MS_PRIVATE 还可以替换成 MS_UNBINDABLEMS_SLAVEMS_SHARED

关于更多的 namespace 的资料,建议看这两篇文章:

参考资料

发表评论

电子邮件地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据