linux namespace and cgroup

namespace

参考

简介

Linux Namespace是Linux提供的一种内核级别环境隔离的方法。

提供了对UTS、IPC、mount、PID、network、User等的隔离机制。

分类

分类

系统调用参数

相关内核版本

隔离内容

Mount namespaces

CLONE_NEWNS

Linux 2.4.19

挂载点(文件系统)

UTS namespaces

CLONE_NEWUTS

Linux 2.6.19

主机名与域名,影响uname(hostname, domainname)

IPC namespaces

CLONE_NEWIPC

Linux 2.6.19

信号量、消息队列和共享内存, inter-process communication,有全局id

PID namespaces

CLONE_NEWPID

Linux 2.6.24

进程编号

Network namespaces

CLONE_NEWNET

始于Linux 2.6.24 完成于 Linux 2.6.29

网络设备、网络栈、端口等等

User namespaces

CLONE_NEWUSER

始于 Linux 2.6.23 完成于 Linux 3.8)

用户和用户组

三个系统调用

调用

作用

clone()

实现线程的系统调用,用来创建一个新的进程,并可以通过设计上述参数达到隔离。

unshare()

使某进程脱离某个namespace

setns()

把某进程加入到某个namespace

详解

测试代码

#define _GNU_SOURCE
#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <sched.h>
#include <signal.h>
#include <unistd.h>
 
/* 定义一个给 clone 用的栈,栈大小1M */
#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");
    /* 直接执行一个shell,以便我们观察这个进程空间里的资源是否被隔离了 */
    sethostname("container",10); /* 设置hostname */
    
    execv(container_args[0], container_args); 
    printf("Something's wrong!\n");
    return 1;
}
 
int main()
{
    printf("Parent - start a container!\n");
    /* 调用clone函数,其中传出一个函数,还有一个栈空间的(为什么传尾指针,因为栈是反着的) */
    // int container_pid = clone(container_main, container_stack+STACK_SIZE, SIGCHLD, NULL);
    // int container_pid = clone(container_main, container_stack+STACK_SIZE, 
            CLONE_NEWUTS | SIGCHLD, NULL); /*启用CLONE_NEWUTS Namespace隔离 */
    int container_pid = clone(container_main, container_stack+STACK_SIZE, 
            CLONE_NEWUTS | CLONE_NEWIPC | SIGCHLD, NULL);

    /* 等待子进程结束 */
    waitpid(container_pid, NULL, 0);
    printf("Parent - container stopped!\n");
    return 0;
}

UTS Namespace

加入 
sethostname("container",10); /* 设置hostname */

int container_pid = clone(container_main, container_stack+STACK_SIZE, 
            CLONE_NEWUTS | SIGCHLD, NULL); /*启用CLONE_NEWUTS Namespace隔离 */
            
root@container:~/testnamespace# uname -a
Linux container 4.4.0-96-generic #119-Ubuntu SMP Tue Sep 12 14:59:54 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux
root@container:~/testnamespace# hostname
container

IPC Namespace

// 如果隔离了 ipcs -q 看不到外面的,否则能看到

root@kube-master:~/testnamespace# ipcs -a

------ Message Queues --------
key        msqid      owner      perms      used-bytes   messages
0x10d91bac 0          root       644        0            0
0xb92f99fd 32769      root       644        0            0
0xfcebd528 65538      root       644        0            0

------ Shared Memory Segments --------
key        shmid      owner      perms      bytes      nattch     status
0x00000000 0          root       644        80         2
0x00000000 32769      root       644        16384      2
0x00000000 65538      root       644        280        2

------ Semaphore Arrays --------
key        semid      owner      perms      nsems
0x000000a7 0          root       600        1

PID Namespace

 int container_pid = clone(container_main, container_stack+STACK_SIZE, 
            CLONE_NEWUTS | CLONE_NEWPID | SIGCHLD, NULL); 

没有隔离
root@container:~/testnamespace# ps -a
  PID TTY          TIME CMD
10079 pts/0    00:00:00 a.out

隔离之后
root@container:~# echo $$
1

但是 ps -a没有变化,这是因为ps, top这些命令会去读/proc文件系统,所以,因为/proc文件系统在父进程和子进程都是一样的,所以这些命令显示的东西都是一样的

pid 1 是一个特殊的pid需要有进程监控和资源回收的能力, docker 1.13 引入了一个 --init 参数解决这个问题

--init false Run an init inside the container that forwards signals and reaps processes

参考 https://blog.phusion.nl/2015/01/20/docker-and-the-pid-1-zombie-reaping-problem/

➜  ke git:(alb) ✗ docker run  alpine  ps
PID   USER     TIME   COMMAND
    1 root       0:00 ps
➜  ke git:(alb) ✗ docker run --init  alpine  ps
PID   USER     TIME   COMMAND
    1 root       0:00 /dev/init -- ps
    5 root       0:00 ps

unshare()和setns()系统调用对PID Namespace的处理不太相同,当unshare PID namespace时,调用进程会为它的子进程分配一个新的PID Namespace,但是调用进程本身不会被移到新的Namespace中。而且调用进程第一个创建的子进程在新Namespace中的PID为1,并成为新Namespace中的init进程。为什么创建其他的Namespace时unshare()和setns()会直接进入新的Namespace,而唯独PID Namespace不是如此呢?因为调用getpid()函数得到的PID是根据调用者所在的PID Namespace而决定返回哪个PID,进入新的PID namespace会导致PID产生变化。而对用户态的程序和库函数来说,他们都认为进程的PID是一个常量,PID的变化会引起这些进程奔溃。换句话说,一旦程序进程创建以后,那么它的PID namespace的关系就确定下来了,进程不会变更他们对应的PID namespace。

Mount Namespace

#include <stdlib.h>
system("mount -t proc proc /proc");
 /* 启用Mount Namespace - 增加CLONE_NEWNS参数 */
int container_pid = clone(container_main, container_stack+STACK_SIZE, 
        CLONE_NEWUTS | CLONE_NEWPID | CLONE_NEWNS | SIGCHLD, NULL);

// 这时候 ps就干净多了
root@vm-master:~/testnamespace# ps -aux
USER       PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND
root         1  0.0  0.0  20036  3868 pts/1    S    12:24   0:00 /bin/bash
root        15  0.0  0.0  36084  3228 pts/1    R+   12:24   0:00 ps -aux
   

关于mount命令

模仿Docker的Mount Namespace。
先要做一个rootfs文件夹
hchen@ubuntu:~/rootfs$ ls
bin  dev  etc  home  lib  lib64  mnt  opt  proc  root  run  sbin  sys  tmp  usr  var

// 拷贝必要的命令
hchen@ubuntu:~/rootfs$ ls ./bin ./usr/bin
  
./bin:
bash   chown  gzip      less  mount       netstat  rm     tabs  tee      top       tty
cat    cp     hostname  ln    mountpoint  ping     sed    tac   test     touch     umount
chgrp  echo   ip        ls    mv          ps       sh     tail  timeout  tr        uname
chmod  grep   kill      more  nc          pwd      sleep  tar   toe      truncate  which
 
./usr/bin:
awk  env  groups  head  id  mesg  sort  strace  tail  top  uniq  vi  wc  xargs


// 拷贝命令要用的sso
hchen@ubuntu:~/rootfs$ ls ./lib64 ./lib/x86_64-linux-gnu/
 
./lib64:
ld-linux-x86-64.so.2
 
./lib/x86_64-linux-gnu/:
libacl.so.1      libmemusage.so         libnss_files-2.19.so    libpython3.4m.so.1
libacl.so.1.1.0  libmount.so.1          libnss_files.so.2       libpython3.4m.so.1.0
libattr.so.1     libmount.so.1.1.0      libnss_hesiod-2.19.so   libresolv-2.19.so
libblkid.so.1    libm.so.6              libnss_hesiod.so.2      libresolv.so.2
libc-2.19.so     libncurses.so.5        libnss_nis-2.19.so      libselinux.so.1
libcap.a         libncurses.so.5.9      libnss_nisplus-2.19.so  libtinfo.so.5
libcap.so        libncursesw.so.5       libnss_nisplus.so.2     libtinfo.so.5.9
libcap.so.2      libncursesw.so.5.9     libnss_nis.so.2         libutil-2.19.so
libcap.so.2.24   libnsl-2.19.so         libpcre.so.3            libutil.so.1
libc.so.6        libnsl.so.1            libprocps.so.3          libuuid.so.1
libdl-2.19.so    libnss_compat-2.19.so  libpthread-2.19.so      libz.so.1
libdl.so.2       libnss_compat.so.2     libpthread.so.0
libgpm.so.2      libnss_dns-2.19.so     libpython2.7.so.1
libm-2.19.so     libnss_dns.so.2        libpython2.7.so.1.0

// 拷贝必要的配置文件
hchen@ubuntu:~/rootfs$ ls ./etc
bash.bashrc  group  hostname  hosts  ld.so.cache  nsswitch.conf  passwd  profile  
resolv.conf  shadow

// 供挂载用的配置文件
hchen@ubuntu:~$ ls ./conf
hostname     hosts     resolv.conf

#define _GNU_SOURCE
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/mount.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",
    "-l",
    NULL
};
 
int container_main(void* arg)
{
    printf("Container [%5d] - inside the container!\n", getpid());
 
    //set hostname
    sethostname("container",10);
 
    //remount "/proc" to make sure the "top" and "ps" show container's information
    if (mount("proc", "rootfs/proc", "proc", 0, NULL) !=0 ) {
        perror("proc");
    }
    if (mount("sysfs", "rootfs/sys", "sysfs", 0, NULL)!=0) {
        perror("sys");
    }
    if (mount("none", "rootfs/tmp", "tmpfs", 0, NULL)!=0) {
        perror("tmp");
    }
    if (mount("udev", "rootfs/dev", "devtmpfs", 0, NULL)!=0) {
        perror("dev");
    }
    if (mount("devpts", "rootfs/dev/pts", "devpts", 0, NULL)!=0) {
        perror("dev/pts");
    }
    if (mount("shm", "rootfs/dev/shm", "tmpfs", 0, NULL)!=0) {
        perror("dev/shm");
    }
    if (mount("tmpfs", "rootfs/run", "tmpfs", 0, NULL)!=0) {
        perror("run");
    }
    /* 
     * 模仿Docker的从外向容器里mount相关的配置文件 
     * 你可以查看:/var/lib/docker/containers/<container_id>/目录,
     * 你会看到docker的这些文件的。
     */
    if (mount("conf/hosts", "rootfs/etc/hosts", "none", MS_BIND, NULL)!=0 ||
          mount("conf/hostname", "rootfs/etc/hostname", "none", MS_BIND, NULL)!=0 ||
          mount("conf/resolv.conf", "rootfs/etc/resolv.conf", "none", MS_BIND, NULL)!=0 ) {
        perror("conf");
    }
    /* 模仿docker run命令中的 -v, --volume=[] 参数干的事 */
    if (mount("/tmp/t1", "rootfs/mnt", "none", MS_BIND, NULL)!=0) {
        perror("mnt");
    }
 
    /* chroot 隔离目录 */
    if ( chdir("./rootfs") != 0 || chroot("./") != 0 ){
        perror("chdir/chroot");
    }
 
    execv(container_args[0], container_args);
    perror("exec");
    printf("Something's wrong!\n");
    return 1;
}
 
int main()
{
    printf("Parent [%5d] - start a container!\n", getpid());
    int container_pid = clone(container_main, container_stack+STACK_SIZE, 
            CLONE_NEWUTS | CLONE_NEWIPC | CLONE_NEWPID | CLONE_NEWNS | SIGCHLD, NULL);
    waitpid(container_pid, NULL, 0);
    printf("Parent - container stopped!\n");
    return 0;
}

进程在创建mount namespace时,会把当前的文件结构复制给新的namespace。新namespace中的所有mount操作都只影响自身的文件系统,而对外界不会产生任何影响。这样做非常严格地实现了隔离,但是某些情况可能并不适用。比如父节点namespace中的进程挂载了一张CD-ROM,这时子节点namespace拷贝的目录结构就无法自动挂载上这张CD-ROM,因为这种操作会影响到父节点的文件系统。

2006 年引入的挂载传播(mount propagation)解决了这个问题,挂载传播定义了挂载对象(mount object)之间的关系,系统用这些关系决定任何挂载对象中的挂载事件如何传播到其他挂载对象(参考自:http://www.ibm.com/developerworks/library/l-mount-namespaces/)。所谓传播事件,是指由一个挂载对象的状态变化导致的其它挂载对象的挂载与解除挂载动作的事件。

进程在创建Mount Namespace时,会把当前的文件结构复制给新的Namespace,新的Namespace中的所有mount操作仅影响自身的文件系统。但随着引入挂载传播的特性,Mount Namespace变得并不是完全意义上的资源隔离,这种传播特性使得多Mount Namespace之间的挂载事件可以相互影响。

挂载传播定义了挂载对象之间的关系,系统利用这些关系来决定挂载对象中的挂载事件对其他挂载对象的影响。其中挂载对象之间的关系描述如下:

  • 共享关系(MS_SHARED):一个挂载对象的挂载事件会跨Namespace共享到其他挂载对象。
  • 从属关系(MS_SLAVE): 传播的方向是单向的,即只能从Master传播到Slave方向。
  • 私有关系(MS_PRIVATE): 不同Namespace的挂载事件是互不影响的(默认选项)。
  • 不可绑定关系(MS_UNBINDABLE): 一个不可绑定的私有挂载,与私有挂载类似,但是不能执行挂载操作

一个挂载状态可能为如下的其中一种:

  • 共享挂载(shared)
  • 从属挂载(slave)
  • 共享/从属挂载(shared and slave)
  • 私有挂载(private)
  • 不可绑定挂载(unbindable)
image

挂载的过程是通过mount系统调用完成的,它有两个参数:一个是已存在的普通文件名,一个是可以直接访问的特殊文件,一个是特殊文件的名字。这个特殊文件一般用来关联一些存储卷,这个存储卷可以包含自己的目录层级和文件系统结构。mount所达到的效果是:像访问一个普通的文件一样访问位于其他设备上文件系统的根目录,也就是将该设备上目录的根节点挂到了另外一个文件系统的页节点上,达到了给这个文件系统扩充容量的目的。

可以通过/proc文件系统查看一个进程的挂载信息,具体做法如下:

cat /proc/$pid/mountinfo  

绑定挂载的引入使得mount的其中一个参数不一定要是一个特殊文件,也可以是该文件系统上的一个普通文件目录。Linux中绑定挂载的用法如下:

mount --bind /home/work /home/qiniu  
mount -o bind /home/work /home/qiniu  

User Namespace

要把容器中的uid和真实系统的uid给映射在一起,需要修改 /proc/<pid>/uid_map 和 /proc/<pid>/gid_map 这两个文件。这两个文件的格式为:

ID-inside-ns ID-outside-ns length

  • 第一个字段ID-inside-ns表示在容器显示的UID或GID,
  • 第二个字段ID-outside-ns表示容器外映射的真实的UID或GID。
  • 第三个字段表示映射的范围,一般填1,表示一一对应。

User namespace主要隔离了安全相关的标识符(identifiers)和属性(attributes),包括用户ID、用户组ID、root目录、key(指密钥)以及特殊权限。说得通俗一点,一个普通用户的进程通过clone()创建的新进程在新user namespace中可以拥有不同的用户和用户组。这意味着一个进程在容器外属于一个没有特权的普通用户,但是他创建的容器进程却属于拥有所有权限的超级用户,这个技术为容器提供了极大的自由。

User Namespace除了隔离用户ID和用户组ID之外,还对每个Namespace进行了Capability的隔离和控制,可以通过添加和删除相应的Capability来控制新Namespace中进程所拥有的权限,比如为新的Namespace中增加CAP_CHOWN权限,那么在这个Namespace的进程拥有改变文件属主的权限。

  • user namespace被创建后,第一个进程被赋予了该namespace中的全部权限,这样这个init进程就可以完成所有必要的初始化工作,而不会因权限不足而出现错误。
  • 我们看到namespace内部看到的UID和GID已经与外部不同了,默认显示为65534,表示尚未与外部namespace用户映射。我们需要对user namespace内部的这个初始user和其外部namespace某个用户建立映射,这样可以保证当涉及到一些对外部namespace的操作时,系统可以检验其权限(比如发送一个信号或操作某个文件)。同样用户组也要建立映射。
  • 还有一点虽然不能从输出中看出来,但是值得注意。用户在新namespace中有全部权限,但是他在创建他的父namespace中不含任何权限。就算调用和创建他的进程有全部权限也是如此。所以哪怕是root用户调用了clone()在user namespace中创建出的新用户在外部也没有任何权限。
  • 最后,user namespace的创建其实是一个层层嵌套的树状结构。最上层的根节点就是root namespace,新创建的每个user namespace都有一个父节点user namespace以及零个或多个子节点user namespace,这一点与PID namespace非常相似。
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/mount.h>
#include <sys/capability.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 pipefd[2];
 
void set_map(char* file, int inside_id, int outside_id, int len) {
    FILE* mapfd = fopen(file, "w");
    if (NULL == mapfd) {
        perror("open file error");
        return;
    }
    fprintf(mapfd, "%d %d %d", inside_id, outside_id, len);
    fclose(mapfd);
}
 
void set_uid_map(pid_t pid, int inside_id, int outside_id, int len) {
    char file[256];
    sprintf(file, "/proc/%d/uid_map", pid);
    set_map(file, inside_id, outside_id, len);
}
 
void set_gid_map(pid_t pid, int inside_id, int outside_id, int len) {
    char file[256];
    sprintf(file, "/proc/%d/gid_map", pid);
    set_map(file, inside_id, outside_id, len);
}
 
int container_main(void* arg)
{
 
    printf("Container [%5d] - inside the container!\n", getpid());
 
    printf("Container: eUID = %ld;  eGID = %ld, UID=%ld, GID=%ld\n",
            (long) geteuid(), (long) getegid(), (long) getuid(), (long) getgid());
 
    /* 等待父进程通知后再往下执行(进程间的同步) */
    char ch;
    close(pipefd[1]);
    read(pipefd[0], &ch, 1);
 
    printf("Container [%5d] - setup hostname!\n", getpid());
    //set hostname
    sethostname("container",10);
 
    //remount "/proc" to make sure the "top" and "ps" show container's information
    mount("proc", "/proc", "proc", 0, NULL);
 
    execv(container_args[0], container_args);
    printf("Something's wrong!\n");
    return 1;
}
 
int main()
{
    const int gid=getgid(), uid=getuid();
 
    printf("Parent: eUID = %ld;  eGID = %ld, UID=%ld, GID=%ld\n",
            (long) geteuid(), (long) getegid(), (long) getuid(), (long) getgid());
 
    pipe(pipefd);
  
    printf("Parent [%5d] - start a container!\n", getpid());
 
    int container_pid = clone(container_main, container_stack+STACK_SIZE, 
            CLONE_NEWUTS  | CLONE_NEWNS | CLONE_NEWUSER | SIGCHLD, NULL);
 
     
    printf("Parent [%5d] - Container [%5d]!\n", getpid(), container_pid);
 
    //To map the uid/gid, 
    //   we need edit the /proc/PID/uid_map (or /proc/PID/gid_map) in parent
    //The file format is
    //   ID-inside-ns   ID-outside-ns   length
    //if no mapping, 
    //   the uid will be taken from /proc/sys/kernel/overflowuid
    //   the gid will be taken from /proc/sys/kernel/overflowgid
    set_uid_map(container_pid, 0, uid, 1);
    set_gid_map(container_pid, 0, gid, 1);
 
    printf("Parent [%5d] - user/group mapping done!\n", getpid());
 
    /* 通知子进程 */
    close(pipefd[1]);
 
    waitpid(container_pid, NULL, 0);
    printf("Parent - container stopped!\n");
    return 0;
}


上面的程序,我们用了一个pipe来对父子进程进行同步,为什么要这样做?因为子进程中有一个execv的系统调用,这个系统调用会把当前子进程的进程空间给全部覆盖掉,我们希望在execv之前就做好user namespace的uid/gid的映射,这样,execv运行的/bin/bash就会因为我们设置了uid为0的inside-uid而变成#号的提示符。

Network Namespace

在Linux下,我们一般用ip命令创建Network Namespace

image

一般情况下,物理网络设备都分配在最初的root namespace(表示系统默认的namespace,在PID namespace中已经提及)中。但是如果你有多块物理网卡,也可以把其中一块或多块分配给新创建的network namespace。需要注意的是,当新创建的network namespace被释放时(所有内部的进程都终止并且namespace文件没有被挂载或打开),在这个namespace中的物理网卡会返回到root namespace而非创建该进程的父进程所在的network namespace。

在建立起veth pair之前,新旧namespace该如何通信呢?答案是pipe(管道)。我们以Docker Daemon在启动容器dockerinit的过程为例。Docker Daemon在宿主机上负责创建这个veth pair,通过netlink调用,把一端绑定到docker0网桥上,一端连进新建的network namespace进程中。建立的过程中,Docker Daemon和dockerinit就通过pipe进行通信,当Docker Daemon完成veth-pair的创建之前,dockerinit在管道的另一端循环等待,直到管道另一端传来Docker Daemon关于veth设备的信息,并关闭管道。dockerinit才结束等待的过程,并把它的“eth0”启动起来。整个效果类似下图所示。

// docker 网络本质做的事就是 1. 创建网桥  2. 创建veth 虚拟网卡,一头在docker ns1,一头插在网桥上 3. 设置ip,路由规则,nat,让docker 网络能经过bridge 出去  外部访问容器网络 也是在本地的 iptable 的 nat 表中添加相应的规则 https://yeasy.gitbooks.io/docker_practice/content/advanced_network/port_mapping.html
calico 也是类似实现,没有用bridge模式

## 首先,我们先增加一个网桥lxcbr0,模仿docker0
brctl addbr lxcbr0
brctl stp lxcbr0 off
ifconfig lxcbr0 192.168.10.1/24 up #为网桥设置IP地址
 
## 接下来,我们要创建一个network namespace - ns1
 
# 增加一个namesapce 命令为 ns1 (使用ip netns add命令)
ip netns add ns1 
 
# 激活namespace中的loopback,即127.0.0.1(使用ip netns exec ns1来操作ns1中的命令)
ip netns exec ns1   ip link set dev lo up 
 
## 然后,我们需要增加一对虚拟网卡
 
# 增加一个pair虚拟网卡,注意其中的veth类型,其中一个网卡要按进容器中
# VETH 设备总是成对出现,送到一端请求发送的数据总是从另一端以请求接受的形式出现。该设备不能被用户程序直接操作,但使用起来比较简单。创建并配置正确后,向其一端输入数据,VETH 会改变数据的方向并将其送入内核网络核心,完成数据的注入。在另一端能读到此数据。

ip link add veth-ns1 type veth peer name lxcbr0.1
 
# 把 veth-ns1 按到namespace ns1中,这样容器中就会有一个新的网卡了
ip link set veth-ns1 netns ns1
 
# 把容器里的 veth-ns1改名为 eth0 (容器外会冲突,容器内就不会了)
ip netns exec ns1  ip link set dev veth-ns1 name eth0 
 
# 为容器中的网卡分配一个IP地址,并激活它
ip netns exec ns1 ifconfig eth0 192.168.10.11/24 up
 
 
# 上面我们把veth-ns1这个网卡按到了容器中,然后我们要把lxcbr0.1添加上网桥上
brctl addif lxcbr0 lxcbr0.1
 
# 为容器增加一个路由规则,让容器可以访问外面的网络
ip netns exec ns1     ip route add default via 192.168.10.1
 
# 在/etc/netns下创建network namespce名称为ns1的目录,
# 然后为这个namespace设置resolv.conf,这样,容器内就可以访问域名了
mkdir -p /etc/netns/ns1
echo "nameserver 8.8.8.8" > /etc/netns/ns1/resolv.conf

CGroup

cgroups可以限制、记录、隔离进程组所使用的物理资源(包括:CPU、memory、IO等),为容器实现虚拟化提供了基本保证,是构建Docker等一系列虚拟化管理工具的基石。

主要提供了如下功能:

  • Resource limitation: 限制资源使用,比如内存使用上限以及文件系统的缓存限制。
  • Prioritization: 优先级控制,比如:CPU利用和磁盘IO吞吐。
  • Accounting: 一些审计或一些统计,主要目的是为了计费。
  • Control: 挂起进程,恢复执行进程。

对开发者来说,cgroups有如下四个有趣的特点:

  • cgroups的API以一个伪文件系统的方式实现,即用户可以通过文件操作实现cgroups的组织管理。
  • cgroups的组织管理操作单元可以细粒度到线程级别,用户态代码也可以针对系统分配的资源创建和销毁cgroups,从而实现资源再分配和管理。
  • 所有资源管理的功能都以“subsystem(子系统)”的方式实现,接口统一。
  • 子进程创建之初与其父进程处于同一个cgroups的控制组。

本质上来说,cgroups是内核附加在程序上的一系列钩子(hooks),通过程序运行时对资源的调度触发相应的钩子以达到资源追踪和限制的目的。

术语

  • task(任务):cgroups的术语中,task就表示系统的一个进程。
  • cgroup(控制组):cgroups 中的资源控制都以cgroup为单位实现。cgroup表示按某种资源控制标准划分而成的任务组,包含一个或多个子系统。一个任务可以加入某个cgroup,也可以从某个cgroup迁移到另外一个cgroup。
  • subsystem(子系统):cgroups中的subsystem就是一个资源调度控制器(Resource Controller)。比如CPU子系统可以控制CPU时间分配,内存子系统可以限制cgroup内存使用量。
  • hierarchy(层级树):hierarchy由一系列cgroup以一个树状结构排列而成,每个hierarchy通过绑定对应的subsystem进行资源调度。hierarchy中的cgroup节点可以包含零或多个子节点,子节点继承父节点的属性。整个系统可以有多个hierarchy
hchen@ubuntu:~$ mount -t cgroup #或者使用lssubsys -m命令: # lscgroup 查询
cgroup on /sys/fs/cgroup/cpuset type cgroup (rw,relatime,cpuset)
cgroup on /sys/fs/cgroup/cpu type cgroup (rw,relatime,cpu)
cgroup on /sys/fs/cgroup/cpuacct type cgroup (rw,relatime,cpuacct)
cgroup on /sys/fs/cgroup/memory type cgroup (rw,relatime,memory)
cgroup on /sys/fs/cgroup/devices type cgroup (rw,relatime,devices)
cgroup on /sys/fs/cgroup/freezer type cgroup (rw,relatime,freezer)
cgroup on /sys/fs/cgroup/blkio type cgroup (rw,relatime,blkio)
cgroup on /sys/fs/cgroup/net_prio type cgroup (rw,net_prio)
cgroup on /sys/fs/cgroup/net_cls type cgroup (rw,net_cls)
cgroup on /sys/fs/cgroup/perf_event type cgroup (rw,relatime,perf_event)
cgroup on /sys/fs/cgroup/hugetlb type cgroup (rw,relatime,hugetlb)

cgroups的使用方法简介

查询cgroup及子系统挂载状态

  • 查看所有的cgroup:lscgroup
  • 查看所有支持的子系统:lssubsys -a
  • 查看所有子系统挂载的位置: lssubsys –m
  • 查看单个子系统(如memory)挂载位置:lssubsys –m memory

创建hierarchy层级并挂载子系统

// 虚拟机操作,会影响系统
mount -t tmpfs cgroups /sys/fs/cgroup
mkdir /sys/fs/cgroup/cg1
// mount -t cgroup -o subsystems name /cgroup/name
mount –t cgroup –o cpu,memory cpu_and_mem /sys/fs/cgroup/cg1

CPU 限制

root@container:~# mkdir -p  /sys/fs/cgroup/cpu/wanglei
root@container:~# cat /sys/fs/cgroup/cpu/wanglei/cpu.cfs_quota_us
-1

测试程序
int main(void)
{
    int i = 0;
    for(;;) i++;
    return 0;
}

top->
  PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
 6121 root      20   0    4224    684    612 R 100.0  0.0   0:05.89 a.out
 
开始限制,6121查到是测试程序的pid
root@container:~/testcgroup# echo 20000 > /sys/fs/cgroup/cpu/wanglei/cpu.cfs_quota_us
root@container:~/testcgroup# echo 6121 >> /sys/fs/cgroup/cpu/wanglei/tasks

top->
  PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
 6121 root      20   0    4224    684    612 R  20.3  0.0   2:31.16 a.out

下面的代码是一个线程的示例

#define _GNU_SOURCE         /* See feature_test_macros(7) */
 
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/syscall.h>
 
 
const int NUM_THREADS = 5;
 
void *thread_main(void *threadid)
{
    /* 把自己加入cgroup中(syscall(SYS_gettid)为得到线程的系统tid) */
    char cmd[128];
    sprintf(cmd, "echo %ld >> /sys/fs/cgroup/cpu/haoel/tasks", syscall(SYS_gettid));
    system(cmd); 
    sprintf(cmd, "echo %ld >> /sys/fs/cgroup/cpuset/haoel/tasks", syscall(SYS_gettid));
    system(cmd);
 
    long tid;
    tid = (long)threadid;
    printf("Hello World! It's me, thread #%ld, pid #%ld!\n", tid, syscall(SYS_gettid));
     
    int a=0; 
    while(1) {
        a++;
    }
    pthread_exit(NULL);
}
int main (int argc, char *argv[])
{
    int num_threads;
    if (argc > 1){
        num_threads = atoi(argv[1]);
    }
    if (num_threads<=0 || num_threads>=100){
        num_threads = NUM_THREADS;
    }
 
    /* 设置CPU利用率为50% */
    mkdir("/sys/fs/cgroup/cpu/haoel", 755);
    system("echo 50000 > /sys/fs/cgroup/cpu/haoel/cpu.cfs_quota_us");
 
    mkdir("/sys/fs/cgroup/cpuset/haoel", 755);
    /* 限制CPU只能使用#2核和#3核 */
    system("echo \"2,3\" > /sys/fs/cgroup/cpuset/haoel/cpuset.cpus");
 
    pthread_t* threads = (pthread_t*) malloc (sizeof(pthread_t)*num_threads);
    int rc;
    long t;
    for(t=0; t<num_threads; t++){
        printf("In main: creating thread %ld\n", t);
        rc = pthread_create(&threads[t], NULL, thread_main, (void *)t);
        if (rc){
            printf("ERROR; return code from pthread_create() is %d\n", rc);
            exit(-1);
        }
    }
 
    /* Last thing that main() should do */
    pthread_exit(NULL);
    free(threads);
}

内存使用限制

测试一个耗尽内存的程序,限制内存,可以看到程序会被kill

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
 
int main(void)
{
    int size = 0;
    int chunk_size = 512;
    void *p = NULL;
 
    while(1) {
 
        if ((p = malloc(chunk_size)) == NULL) {
            printf("out of memory!!\n");
            break;
        }
        memset(p, 1, chunk_size);
        size += chunk_size;
        printf("[%d] - memory is allocated [%8d] bytes \n", getpid(), size);
        sleep(1);
    }
    return 0;
}
root@container:~/testcgroup# mkdir /sys/fs/cgroup/memory/wanglei
root@container:~/testcgroup# echo 64k > /sys/fs/cgroup/memory/wanglei/memory.limit_in_bytes
root@container:~/testcgroup# echo [pid] > /sys/fs/cgroup/memory/haoel/tasks^C

磁盘I/O限制

root@container:~/testcgroup# dd if=/dev/vda of=/dev/null

iotop->
  TID  PRIO  USER     DISK READ  DISK WRITE  SWAPIN     IO>    COMMAND
15660 be/4 root       73.81 M/s    0.00 B/s  0.00 % 82.47 % dd if=/dev/vda of=/dev/null

root@container:~/testcgroup# mkdir /sys/fs/cgroup/blkio/wanglei
root@container:~/testcgroup# ls -l /dev/vda
brw-rw---- 1 root disk 253, 0 Sep 25 12:49 /dev/vda
root@container:~/testcgroup# echo "253:0 1048576" > /sys/fs/cgroup/blkio/wanglei/blkio.throttle.read_bps_device
root@container:~/testcgroup# echo 16221  > /sys/fs/cgroup/blkio/wanglei/tasks

iotop-> 限制得不是很准
  TID  PRIO  USER     DISK READ  DISK WRITE  SWAPIN     IO>    COMMAND
16221 be/4 root      978.21 K/s    0.00 B/s  0.00 % 95.28 % dd if=/dev/vda of=/dev/null

CGroup的子系统

  • blkio: 这个subsystem可以为块设备设定输入/输出限制,比如物理驱动设备(包括磁盘、固态硬盘、USB等)。
  • cpu: 这个subsystem使用调度程序控制task对CPU的使用。
  • cpuacct: 这个subsystem自动生成cgroup中task对CPU资源使用情况的报告。
  • cpuset: 这个subsystem可以为cgroup中的task分配独立的CPU(此处针对多处理器系统)和内存。
  • devices 这个subsystem可以开启或关闭cgroup中task对设备的访问。
  • freezer 这个subsystem可以挂起或恢复cgroup中的task。
  • memory 这个subsystem可以设定cgroup中task对内存使用量的限定,并且自动生成这些task对内存资源使用情况的报告。
  • perfevent 这个subsystem使用后使得cgroup中的task可以进行统一的性能测试。{undefined}
  • *net_cls 这个subsystem Docker没有直接使用,它通过使用等级识别符(classid)标记网络数据包,从而允许 Linux 流量控制程序(TC:Traffic Controller)识别从具体cgroup中生成的数据包。

组织结构与基本规则

大家在namespace技术的讲解中已经了解到,传统的Unix进程管理,实际上是先启动init进程作为根节点,再由init节点创建子进程作为子节点,而每个子节点由可以创建新的子节点,如此往复,形成一个树状结构。而cgroups也是类似的树状结构,子节点都从父节点继承属性。

它们最大的不同在于,系统中cgroup构成的hierarchy可以允许存在多个。如果进程模型是由init作为根节点构成的一棵树的话,那么cgroups的模型则是由多个hierarchy构成的森林。这样做的目的也很好理解,如果只有一个hierarchy,那么所有的task都要受到绑定其上的subsystem的限制,会给那些不需要这些限制的task造成麻烦。

了解了cgroups的组织结构,我们再来了解cgroup、task、subsystem以及hierarchy四者间的相互关系及其基本规则{undefined}。

规则1: 同一个hierarchy可以附加一个或多个subsystem。如下图1,cpu和memory的subsystem附加到了一个hierarchy。

image

图1 同一个hierarchy可以附加一个或多个subsystem

规则2: 一个subsystem可以附加到多个hierarchy,当且仅当这些hierarchy只有这唯一一个subsystem。如下图2,小圈中的数字表示subsystem附加的时间顺序,CPU subsystem附加到hierarchy A的同时不能再附加到hierarchy B,因为hierarchy B已经附加了memory subsystem。如果hierarchy B与hierarchy A状态相同,没有附加过memory subsystem,那么CPU subsystem同时附加到两个hierarchy是可以的。

image

图2 一个已经附加在某个hierarchy上的subsystem不能附加到其他含有别的subsystem的hierarchy上

规则3: 系统每次新建一个hierarchy时,该系统上的所有task默认构成了这个新建的hierarchy的初始化cgroup,这个cgroup也称为root cgroup。对于你创建的每个hierarchy,task只能存在于其中一个cgroup中,即一个task不能存在于同一个hierarchy的不同cgroup中,但是一个task可以存在在不同hierarchy中的多个cgroup中。如果操作时把一个task添加到同一个hierarchy中的另一个cgroup中,则会从第一个cgroup中移除。在下图3中可以看到,httpd进程已经加入到hierarchy A中的/cg1而不能加入同一个hierarchy中的/cg2,但是可以加入hierarchy B中的/cg3。实际上不允许加入同一个hierarchy中的其他cgroup野生为了防止出现矛盾,如CPU subsystem为/cg1分配了30%,而为/cg2分配了50%,此时如果httpd在这两个cgroup中,就会出现矛盾。

image

图3 一个task不能属于同一个hierarchy的不同cgroup

规则4: 进程(task)在fork自身时创建的子任务(child task)默认与原task在同一个cgroup中,但是child task允许被移动到不同的cgroup中。即fork完成后,父子进程间是完全独立的。如下图4中,小圈中的数字表示task 出现的时间顺序,当httpd刚fork出另一个httpd时,在同一个hierarchy中的同一个cgroup中。但是随后如果PID为4840的httpd需要移动到其他cgroup也是可以的,因为父子任务间已经独立。总结起来就是:初始化时子任务与父任务在同一个cgroup,但是这种关系随后可以改变。

image

图4 刚fork出的子进程在初始状态与其父进程处于同一个cgroup

补充

systemd

kuberlet有个systemd文档这么说:

This document describes how the node should be configured, and a set of enhancements that should be made to the kubelet to better integrate with these distributions independent of container runtime.

The Kernel direction for cgroup management is to promote a single-writer model rather than allowing multiple processes to independently write to parts of the file-system.In distributions that run systemd as their init system, the cgroup tree is managed by systemd by default since it implicitly interacts with the cgroup tree when starting units. Manual changes made by other cgroup managers to the cgroup tree are not guaranteed to be preserved unless systemd is made aware. systemd can be told to ignore sections of the cgroup tree by configuring the unit to have the Delegate= option.

是说再linux上就推荐用systemd来管理cgroup?而且这样还能不依赖docker?

sysctl

除了cgroup做资源限制,对于系统级别的资源限制相关的还有一个sysctl命令

sysctl命令被用于在内核运行时动态地修改内核的运行参数,可用的内核参数在目录/proc/sys中。它包含一些TCP/ip堆栈和虚拟内存系统的高级选项, 这可以让有经验的管理员提高引人注目的系统性能。用sysctl可以读取设置超过五百个系统变量。

Parameters are available via /proc/sys/ virtual process file system. The parameters cover various subsystems such as:

  • kernel (common prefix: kernel.)
  • networking (common prefix: net.)
  • virtual memory (common prefix: vm.)
  • MDADM (common prefix: dev.)

docker privileged可以设置,但是有些参数是系统级别的,没有隔离,改了会影响别的容器。后来版本docker做了限制,只能改一些whitelisted sysctls。

Only namespaced kernel parameters can be modified

k8s里面的设置https://github.com/kubernetes/community/blob/master/contributors/design-proposals/node/sysctl.md

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券