linux namespace and cgroup

雨落雲風發表於2019-03-02

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
參考 blog.phusion.nl/2015/01/20/…

➜  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)之間的關係,系統用這些關係決定任何掛載物件中的掛載事件如何傳播到其他掛載物件(參考自:www.ibm.com/developerwo…

程式在建立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
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//uid_map 和 /proc//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
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可以進行統一的效能測試。{![perf: Linux CPU效能探測器,詳見perf.wiki.kernel.org/index.php/M…
  • *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四者間的相互關係及其基本規則{![參照自:access.redhat.com/documentati…

規則1: 同一個hierarchy可以附加一個或多個subsystem。如下圖1,cpu和memory的subsystem附加到了一個hierarchy。

image
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
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
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
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裡面的設定github.com/kubernetes/…

相關文章