溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶(hù)服務(wù)條款》

Namespace機(jī)制是什么

發(fā)布時(shí)間:2021-10-23 16:01:01 來(lái)源:億速云 閱讀:109 作者:iii 欄目:編程語(yǔ)言

本篇內(nèi)容主要講解“Namespace機(jī)制是什么”,感興趣的朋友不妨來(lái)看看。本文介紹的方法操作簡(jiǎn)單快捷,實(shí)用性強(qiáng)。下面就讓小編來(lái)帶大家學(xué)習(xí)“Namespace機(jī)制是什么”吧!

Namespace

Linux Namespace 是 Linux 提供的一種內(nèi)核級(jí)別環(huán)境隔離的方法。這種隔離機(jī)制和 chroot 很類(lèi)似,chroot 是把某個(gè)目錄修改為根目錄,從而無(wú)法訪(fǎng)問(wèn)外部的內(nèi)容。Linux Namesapce 在此基礎(chǔ)之上,提供了對(duì) UTS、IPC、Mount、PID、Network、User 等的隔離機(jī)制,如下所示。

分類(lèi)系統(tǒng)調(diào)用參數(shù)相關(guān)內(nèi)核版本
Mount NamespacesCLONE_NEWNSLinux 2.4.19
UTS NamespacesCLONE_NEWUTSLinux 2.6.19
IPC NamespacesCLONE_NEWIPCLinux 2.6.19
PID NamespacesCLONE_NEWPIDLinux 2.6.19
Network NamespacesCLONE_NEWNET始于Linux 2.6.24 完成于 Linux 2.6.29
User NamespacesCLONE_NEWUSER始于 Linux 2.6.23 完成于 Linux 3.8)
★    

Linux Namespace 官方文檔:Namespaces in operation

”  

namespace 有三個(gè)系統(tǒng)調(diào)用可以使用:

  • clone() --- 實(shí)現(xiàn)線(xiàn)程的系統(tǒng)調(diào)用,用來(lái)創(chuàng)建一個(gè)新的進(jìn)程,并可以通過(guò)設(shè)計(jì)上述參數(shù)達(dá)到隔離。
  • unshare() --- 使某個(gè)進(jìn)程脫離某個(gè) namespace
  • setns(int fd, int nstype) --- 把某進(jìn)程加入到某個(gè) namespace

下面使用這幾個(gè)系統(tǒng)調(diào)用來(lái)演示 Namespace 的效果,更加詳細(xì)地可以看 DOCKER基礎(chǔ)技術(shù):LINUX NAMESPACE(上)、 DOCKER基礎(chǔ)技術(shù):LINUX NAMESPACE(下)。

 

UTS Namespace

UTS Namespace 主要是用來(lái)隔離主機(jī)名的,也就是每個(gè)容器都有自己的主機(jī)名。我們使用如下的代碼來(lái)進(jìn)行演示。注意:假如在容器內(nèi)部沒(méi)有設(shè)置主機(jī)名的話(huà)會(huì)使用主機(jī)的主機(jī)名的;假如在容器內(nèi)部設(shè)置了主機(jī)名但是沒(méi)有使用 CLONE_NEWUTS 的話(huà)那么改變的其實(shí)是主機(jī)的主機(jī)名。

#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",
   NULL
};

int container_main(void* arg) {
   printf("Container [%5d] - inside the container!\n", getpid());
   sethostname("container_dawn", 15);
   execv(container_args[0], container_args);
   printf("Something's wrong!\n");
   return 1;
}

int main() {
   printf("Parent [%5d] - start a container!\n", getpid());
   int container_id = clone(container_main, container_stack + STACK_SIZE,
                               CLONE_NEWUTS | SIGCHLD, NULL);
   waitpid(container_id, NULL, 0);
   printf("Parent - container stopped!\n");
   return 0;
}
 
Namespace機(jī)制是什么  
 

PID Namespace

每個(gè)容器都有自己的進(jìn)程環(huán)境中,也就是相當(dāng)于容器內(nèi)進(jìn)程的 PID 從 1 開(kāi)始命名,此時(shí)主機(jī)上的 PID 其實(shí)也還是從 1 開(kāi)始命名的,就相當(dāng)于有兩個(gè)進(jìn)程環(huán)境:一個(gè)主機(jī)上的從 1 開(kāi)始,另一個(gè)容器里的從 1 開(kāi)始。

為啥 PID 從 1 開(kāi)始就相當(dāng)于進(jìn)程環(huán)境的隔離了呢?因此在傳統(tǒng)的 UNIX 系統(tǒng)中,PID 為 1 的進(jìn)程是 init,地位特殊。它作為所有進(jìn)程的父進(jìn)程,有很多特權(quán)。另外,其還會(huì)檢查所有進(jìn)程的狀態(tài),我們知道如果某個(gè)進(jìn)程脫離了父進(jìn)程(父進(jìn)程沒(méi)有 wait 它),那么 init 就會(huì)負(fù)責(zé)回收資源并結(jié)束這個(gè)子進(jìn)程。所以要想做到進(jìn)程的隔離,首先需要?jiǎng)?chuàng)建出 PID 為 1 的進(jìn)程。

但是,【kubernetes 里面的話(huà)】

int container_main(void* arg) {
   printf("Container [%5d] - inside the container!\n", getpid());
   sethostname("container_dawn", 15);
   execv(container_args[0], container_args);
   printf("Something's wrong!\n");
   return 1;
}

int main() {
   printf("Parent [%5d] - start a container!\n", getpid());
   int container_id = clone(container_main, container_stack + STACK_SIZE,
                               CLONE_NEWUTS | CLONE_NEWPID | SIGCHLD, NULL);
   waitpid(container_id, NULL, 0);
   printf("Parent - container stopped!\n");
   return 0;
}
 
Namespace機(jī)制是什么  

如果此時(shí)你在子進(jìn)程的 shell 中輸入 ps、top 等命令,我們還是可以看到所有進(jìn)程。這是因?yàn)?,ps、top 這些命令是去讀 /proc 文件系統(tǒng),由于此時(shí)文件系統(tǒng)并沒(méi)有隔離,所以父進(jìn)程和子進(jìn)程通過(guò)命令看到的情況都是一樣的。

 

IPC Namespace

常見(jiàn)的 IPC 有共享內(nèi)存、信號(hào)量、消息隊(duì)列等。當(dāng)使用 IPC Namespace 把 IPC 隔離起來(lái)之后,只有同一個(gè) Namespace 下的進(jìn)程才能相互通信,因?yàn)橹鳈C(jī)的 IPC 和其他 Namespace 中的 IPC 都是看不到了的。而這個(gè)的隔離主要是因?yàn)閯?chuàng)建出來(lái)的 IPC 都會(huì)有一個(gè)唯一的 ID,那么主要對(duì)這個(gè) ID 進(jìn)行隔離就好了。

想要啟動(dòng) IPC 隔離,只需要在調(diào)用 clone 的時(shí)候加上 CLONE_NEWIPC 參數(shù)就可以了。

int container_main(void* arg) {
   printf("Container [%5d] - inside the container!\n", getpid());
   sethostname("container_dawn", 15);
   execv(container_args[0], container_args);
   printf("Something's wrong!\n");
   return 1;
}

int main() {
   printf("Parent [%5d] - start a container!\n", getpid());
   int container_id = clone(container_main, container_stack + STACK_SIZE,
                               CLONE_NEWUTS | CLONE_NEWPID | CLONE_NEWIPC | SIGCHLD, NULL);
   waitpid(container_id, NULL, 0);
   printf("Parent - container stopped!\n");
   return 0;
}
   

Mount Namespace

Mount Namespace 可以讓容器有自己的 root 文件系統(tǒng)。需要注意的是,在通過(guò) CLONE_NEWNS 創(chuàng)建 mount namespace 之后,父進(jìn)程會(huì)把自己的文件結(jié)構(gòu)復(fù)制給子進(jìn)程中。所以當(dāng)子進(jìn)程中不重新 mount 的話(huà),子進(jìn)程和父進(jìn)程的文件系統(tǒng)視圖是一樣的,假如想要改變?nèi)萜鬟M(jìn)程的視圖,一定需要重新 mount(這個(gè)是 mount namespace  和其他 namespace 不同的地方)。

另外,子進(jìn)程中新的 namespace 中的所有 mount 操作都只影響自身的文件系統(tǒng)(注意這邊是 mount 操作,而創(chuàng)建文件等操作都是會(huì)有所影響的),而不對(duì)外界產(chǎn)生任何影響,這樣可以做到比較嚴(yán)格地隔離(當(dāng)然這邊是除 share mount 之外的)。

下面我們重新掛載子進(jìn)程的 /proc 目錄,從而可以使用 ps 來(lái)查看容器內(nèi)部的情況。

int container_main(void* arg) {
   printf("Container [%5d] - inside the container!\n", getpid());

   sethostname("container_dawn", 15);

   if (mount("proc", "/proc", "proc", 0, NULL) !=0 ) {
       perror("proc");
   }

   execv(container_args[0], container_args);
   printf("Something's wrong!\n");
   return 1;
}

int main() {
   printf("Parent [%5d] - start a container!\n", getpid());
   int container_id = clone(container_main, container_stack + STACK_SIZE,
                               CLONE_NEWUTS | CLONE_NEWPID | CLONE_NEWNS | SIGCHLD, NULL);
   waitpid(container_id, NULL, 0);
   printf("Parent - container stopped!\n");
   return 0;
}
 
Namespace機(jī)制是什么  
★    

這里會(huì)有個(gè)問(wèn)題就是在退出子進(jìn)程之后,當(dāng)再次使用 ps -elf 的時(shí)候會(huì)報(bào)錯(cuò),如下所示

Namespace機(jī)制是什么  

這是因?yàn)?/proc 是 share mount,對(duì)它的操作會(huì)影響所有的 mount namespace,可以看這里:http://unix.stackexchange.com/questions/281844/why-does-child-with-mount-namespace-affect-parent-mounts

”  

上面僅僅重新 mount 了 /proc 這個(gè)目錄,其他的目錄還是跟父進(jìn)程一樣視圖的。一般來(lái)說(shuō),容器創(chuàng)建之后,容器進(jìn)程需要看到的是一個(gè)獨(dú)立的隔離環(huán)境,而不是繼承宿主機(jī)的文件系統(tǒng)。接下來(lái)演示一個(gè)山寨鏡像,來(lái)模仿 Docker 的 Mount Namespace。也就是給子進(jìn)程實(shí)現(xiàn)一個(gè)較為完整的獨(dú)立的 root 文件系統(tǒng),讓這個(gè)進(jìn)程只能訪(fǎng)問(wèn)自己構(gòu)成的文件系統(tǒng)中的內(nèi)容(想想我們平常使用 Docker 容器的樣子)。

  • 首先我們使用 docker export 將 busybox 鏡像導(dǎo)出成一個(gè) rootfs 目錄,這個(gè) rootfs 目錄的情況如圖所示,已經(jīng)包含了 /proc、/sys 等特殊的目錄。

    Namespace機(jī)制是什么    
  • 之后我們?cè)诖a中將一些特殊目錄重新掛載,并使用 chroot() 系統(tǒng)調(diào)用將進(jìn)程的根目錄改成上文的 rootfs 目錄。

    char* const container_args[] = {
       "/bin/sh",
       NULL
    };

    int container_main(void* arg) {
       printf("Container [%5d] - inside the container!\n", getpid());
       sethostname("container_dawn", 15);
       
       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");
       }

       if ( chdir("./rootfs") || chroot("./") != 0 ){
           perror("chdir/chroot");
       }

       // 改變根目錄之后,那么 /bin/bash 是從改變之后的根目錄中搜索了
       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_id = clone(container_main, container_stack + STACK_SIZE,
                                   CLONE_NEWUTS | CLONE_NEWPID | CLONE_NEWNS | SIGCHLD, NULL);
       waitpid(container_id, NULL, 0);
       printf("Parent - container stopped!\n");
       return 0;
    }
  • 最后,查看實(shí)現(xiàn)效果如下圖所示。

    Namespace機(jī)制是什么    

實(shí)際上,Mount Namespace 是基于 chroot 的不斷改良才被發(fā)明出來(lái)的,chroot 可以算是 Linux 中第一個(gè) Namespace。那么上面被掛載在容器根目錄上、用來(lái)為容器鏡像提供隔離后執(zhí)行環(huán)境的文件系統(tǒng),就是所謂的容器鏡像,也被叫做 rootfs(根文件系統(tǒng))。需要明確的是,rootfs 只是一個(gè)操作系統(tǒng)所包含的文件、配置和目錄,并不包括操作系統(tǒng)內(nèi)核

 

User Namespace

容器內(nèi)部看到的 UID 和 GID 和外部是不同的了,比如容器內(nèi)部針對(duì) dawn 這個(gè)用戶(hù)顯示的是 0,但是實(shí)際上這個(gè)用戶(hù)在主機(jī)上應(yīng)該是 1000。要實(shí)現(xiàn)這樣的效果,需要把容器內(nèi)部的 UID 和主機(jī)的 UID 進(jìn)行映射,需要修改的文件是 /proc/<pid>/uid_map/proc/<pid>/gid_map,這兩個(gè)文件的格式是

ID-INSIDE-NS  ID-OUTSIDE-NS LENGTH
 
  • ID-INSIDE-NS :表示在容器內(nèi)部顯示的 UID 或 GID
  • ID-OUTSIDE-NS:表示容器外映射的真實(shí)的 UID 和 GID
  • LENGTH:表示映射的范圍,一般為 1,表示一一對(duì)應(yīng)

比如,下面就是將真實(shí)的 uid=1000 的映射為容器內(nèi)的 uid =0:

$ cat /proc/8353/uid_map
0       1000          1
 

再比如,下面則表示把 namesapce 內(nèi)部從 0 開(kāi)始的 uid 映射到外部從 0 開(kāi)始的 uid,其最大范圍是無(wú)符號(hào) 32 位整型(下面這條命令是在主機(jī)環(huán)境中輸入的)。

$ cat /proc/$$/uid_map
0          0 4294967295
 

默認(rèn)情況,設(shè)置了 CLONE_NEWUSER 參數(shù)但是沒(méi)有修改上述兩個(gè)文件的話(huà),容器中默認(rèn)情況下顯示為 65534,這是因?yàn)槿萜髡也坏秸嬲?UID,所以就設(shè)置了最大的 UID。如下面的代碼所示:

#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 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());

   printf("Container [%5d] - setup hostname!\n", getpid());
   
   //set hostname
   sethostname("container",10);

   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());

   printf("Parent [%5d] - start a container!\n", getpid());

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

   
   printf("Parent [%5d] - Container [%5d]!\n", getpid(), container_pid);

   printf("Parent [%5d] - user/group mapping done!\n", getpid());

   waitpid(container_pid, NULL, 0);
   printf("Parent - container stopped!\n");
   return 0;
}
 

當(dāng)我以 dawn 這個(gè)用戶(hù)執(zhí)行的該程序的時(shí)候,那么會(huì)顯示如下圖所示的效果。使用 root 用戶(hù)的時(shí)候是同樣的:

Namespace機(jī)制是什么  
Namespace機(jī)制是什么  

接下去,我們要開(kāi)始來(lái)實(shí)現(xiàn)映射的效果了,也就是讓 dawn 這個(gè)用戶(hù)在容器中顯示為 0。代碼是幾乎完全拿耗子叔的博客上的,鏈接可見(jiàn)文末:

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

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());

   /* 等待父進(jìn)程通知后再往下執(zhí)行(進(jìn)程間的同步) */
   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_NEWPID | 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
   set_uid_map(container_pid, 0, uid, 1);

   printf("Parent [%5d] - user/group mapping done!\n", getpid());

   /* 通知子進(jìn)程 */
   close(pipefd[1]);

   waitpid(container_pid, NULL, 0);
   printf("Parent - container stopped!\n");
   return 0;
}
 

實(shí)現(xiàn)的最終效果如圖所示,可以看到在容器內(nèi)部將 dawn 這個(gè)用戶(hù) UID 顯示為了 0(root),但其實(shí)這個(gè)容器中的 /bin/bash 進(jìn)程還是以一個(gè)普通用戶(hù),也就是 dawn 來(lái)運(yùn)行的,只是顯示出來(lái)的 UID 是 0,所以當(dāng)查看 /root 目錄的時(shí)候還是沒(méi)有權(quán)限。

Namespace機(jī)制是什么  

User Namespace 是以普通用戶(hù)運(yùn)行的,但是別的 Namespace 需要 root 權(quán)限,那么當(dāng)使用多個(gè) Namespace 該怎么辦呢?我們可以先用一般用戶(hù)創(chuàng)建 User Namespace,然后把這個(gè)一般用戶(hù)映射成 root,那么在容器內(nèi)用 root 來(lái)創(chuàng)建其他的 Namespace。

 

Network Namespace

隔離容器中的網(wǎng)絡(luò),每個(gè)容器都有自己的虛擬網(wǎng)絡(luò)接口和 IP 地址。在 Linux 中,可以使用 ip 命令創(chuàng)建 Network Namespace(Docker 的源碼中,它沒(méi)有使用 ip 命令,而是自己實(shí)現(xiàn)了 ip 命令內(nèi)的一些功能)。

下面就使用 ip 命令來(lái)講解一下 Network Namespace 的構(gòu)建,以 bridge 網(wǎng)絡(luò)為例。bridge 網(wǎng)絡(luò)的拓?fù)鋱D一般如下圖所示,其中 br0 是 Linux 網(wǎng)橋。

Namespace機(jī)制是什么  

在使用 Docker 的時(shí)候,如果啟動(dòng)一個(gè) Docker 容器,并使用 ip link show 查看當(dāng)前宿主機(jī)上的網(wǎng)絡(luò)情況,那么你會(huì)看到有一個(gè) docker0 還有一個(gè) veth****  的虛擬網(wǎng)卡,這個(gè) veth 的虛擬網(wǎng)卡就是上圖中 veth,而 docker0 就相當(dāng)于上圖中的 br0。

那么,我們可以使用下面這些命令即可創(chuàng)建跟 docker 類(lèi)似的效果(參考自耗子叔的博客,鏈接見(jiàn)文末參考,結(jié)合上圖加了一些文字)。

## 1. 首先,我們先增加一個(gè)網(wǎng)橋 lxcbr0,模仿 docker0
brctl addbr lxcbr0
brctl stp lxcbr0 off
ifconfig lxcbr0 192.168.10.1/24 up #為網(wǎng)橋設(shè)置IP地址

## 2. 接下來(lái),我們要?jiǎng)?chuàng)建一個(gè) network namespace ,命名為 ns1

# 增加一個(gè) namesapce 命令為 ns1 (使用 ip netns add 命令)
ip netns add ns1

# 激活 namespace 中的 loopback,即127.0.0.1(使用 ip netns exec ns1 相當(dāng)于進(jìn)入了 ns1 這個(gè) namespace,那么 ip link set dev lo up 相當(dāng)于在 ns1 中執(zhí)行的)
ip netns exec ns1   ip link set dev lo up

## 3. 然后,我們需要增加一對(duì)虛擬網(wǎng)卡

# 增加一對(duì)虛擬網(wǎng)卡,注意其中的 veth 類(lèi)型。這里有兩個(gè)虛擬網(wǎng)卡:veth-ns1 和 lxcbr0.1,veth-ns1 網(wǎng)卡是要被安到容器中的,而 lxcbr0.1 則是要被安到網(wǎng)橋 lxcbr0 中的,也就是上圖中的 veth。
ip link add veth-ns1 type veth peer name lxcbr0.1

# 把 veth-ns1 按到 namespace ns1 中,這樣容器中就會(huì)有一個(gè)新的網(wǎng)卡了
ip link set veth-ns1 netns ns1

# 把容器里的 veth-ns1 改名為 eth0 (容器外會(huì)沖突,容器內(nèi)就不會(huì)了)
ip netns exec ns1  ip link set dev veth-ns1 name eth0

# 為容器中的網(wǎng)卡分配一個(gè) IP 地址,并激活它
ip netns exec ns1 ifconfig eth0 192.168.10.11/24 up


# 上面我們把 veth-ns1 這個(gè)網(wǎng)卡按到了容器中,然后我們要把 lxcbr0.1 添加上網(wǎng)橋上
brctl addif lxcbr0 lxcbr0.1

# 為容器增加一個(gè)路由規(guī)則,讓容器可以訪(fǎng)問(wèn)外面的網(wǎng)絡(luò)
ip netns exec ns1     ip route add default via 192.168.10.1

## 4. 為這個(gè) namespace 設(shè)置 resolv.conf,這樣,容器內(nèi)就可以訪(fǎng)問(wèn)域名了
echo "nameserver 8.8.8.8" > conf/resolv.conf
 

上面基本上就相當(dāng)于 docker 網(wǎng)絡(luò)的原理,只不過(guò):

  • Docker 不使用 ip 命令而是,自己實(shí)現(xiàn)了 ip 命令內(nèi)的一些功能。
  • Docker 的 resolv.conf 沒(méi)有使用這樣的方式,而是將其寫(xiě)到指定的 resolv.conf 中,之后在啟動(dòng)容器的時(shí)候?qū)⑵浜?hostname、host 一起以只讀的方式加載到容器的文件系統(tǒng)中。
  • docker 使用進(jìn)程的 PID 來(lái)做 network namespace 的名稱(chēng)。

同理,我們還可以使用如下的方式為正在運(yùn)行的 docker 容器增加一個(gè)新的網(wǎng)卡

ip link add peerA type veth peer name peerB 
brctl addif docker0 peerA
ip link set peerA up
ip link set peerB netns ${container-pid}
ip netns exec ${container-pid} ip link set dev peerB name eth2
ip netns exec ${container-pid} ip link set eth2 up
ip netns exec ${container-pid} ip addr add ${ROUTEABLE_IP} dev eth2
   

Namespace 情況查看

Cgroup 的操作接口是文件系統(tǒng),位于 /sys/fs/cgroup 中。假如想查看 namespace 的情況同樣可以查看文件系統(tǒng),namespace 主要查看 /proc/<pid>/ns 目錄。

我們以上面的 [PID Namespace 程序](#PID Namespace) 為例,當(dāng)這個(gè)程序運(yùn)行起來(lái)之后,我們可以看到其 PID 為 11702。

Namespace機(jī)制是什么  

之后,我們保持這個(gè)子進(jìn)程運(yùn)行,然后打開(kāi)另一個(gè) shell,查看這個(gè)程序創(chuàng)建的子進(jìn)程的 PID,也就是容器中運(yùn)行的進(jìn)程在主機(jī)中的 PID。

最后,我們分別查看 /proc/11702/ns/proc/11703/ns 這兩個(gè)目錄的情況,也就是查看這兩個(gè)進(jìn)程的 namespace 情況??梢钥吹狡渲?cgroup、ipc、mnt、net、user 都是同一個(gè) ID,而 pid、uts 是不同的 ID。如果兩個(gè)進(jìn)程的 namespace 編號(hào)相同,那么表示這兩個(gè)進(jìn)程位于同一個(gè) namespace 中,否則位于不同 namespace 中。

Namespace機(jī)制是什么  

如果可以查看 ns 的情況之外,這些文件一旦被打開(kāi),只要 fd 被占用著,即使 namespace 中所有進(jìn)程都已經(jīng)結(jié)束了,那么創(chuàng)建的 namespace 也會(huì)一直存在。比如可以使用 mount --bind /proc/11703/ns/uts ~/uts,讓 11703 這個(gè)進(jìn)程的 UTS Namespace 一直存在。

 

總結(jié)

Namespace 技術(shù)實(shí)際上修改了應(yīng)用進(jìn)程看待整個(gè)計(jì)算機(jī)“視圖”,即它的”視圖“已經(jīng)被操作系統(tǒng)做了限制,只能”看到“某些指定的內(nèi)容,這僅僅對(duì)應(yīng)用進(jìn)程產(chǎn)生了影響。但是對(duì)宿主機(jī)來(lái)說(shuō),這些被隔離了的進(jìn)程,其實(shí)還是進(jìn)程,跟宿主機(jī)上其他進(jìn)程并無(wú)太大區(qū)別,都由宿主機(jī)統(tǒng)一管理。只不過(guò)這些被隔離的進(jìn)程擁有額外設(shè)置過(guò)的 Namespace 參數(shù)。那么 Docker 項(xiàng)目在這里扮演的,更多是旁路式的輔助和管理工作。如下左圖所示

Namespace機(jī)制是什么  

因此,相比虛擬機(jī)的方式,容器會(huì)更受歡迎。這是假如使用虛擬機(jī)的方式作為應(yīng)用沙盒,那么必須要由 Hypervisor 來(lái)負(fù)責(zé)創(chuàng)建虛擬機(jī),這個(gè)虛擬機(jī)是真實(shí)存在的,并且里面必須要運(yùn)行一個(gè)完整的 Guest OS 才能執(zhí)行用戶(hù)的應(yīng)用進(jìn)程。這樣就導(dǎo)致了采用虛擬機(jī)的方式之后,不可避免地帶來(lái)額外的資源消耗和占用。根據(jù)實(shí)驗(yàn),一個(gè)運(yùn)行著 CentOS 的 KVM 虛擬機(jī)啟動(dòng)后,在不做優(yōu)化的情況下,虛擬機(jī)就需要占用 100-200 MB 內(nèi)存。此外,用戶(hù)應(yīng)用運(yùn)行在虛擬機(jī)中,它對(duì)宿主機(jī)操作系統(tǒng)的調(diào)用就不可避免地要經(jīng)過(guò)虛擬機(jī)軟件的攔截和處理,這本身就是一層消耗,尤其對(duì)資源、網(wǎng)絡(luò)和磁盤(pán) IO 的損耗非常大。

而假如使用容器的方式,容器化之后應(yīng)用本質(zhì)還是宿主機(jī)上的一個(gè)進(jìn)程,這也就意味著因?yàn)樘摂M機(jī)化帶來(lái)的性能損耗是不存在的;而另一方面使用 Namespace 作為隔離手段的容器并不需要單獨(dú)的 Guest OS,這就使得容器額外的資源占用幾乎可以忽略不計(jì)。

總得來(lái)說(shuō),“敏捷”和“高性能”是容器相對(duì)于虛擬機(jī)最大的優(yōu)勢(shì),也就是容器能在 PaaS 這種更加細(xì)粒度的資源管理平臺(tái)上大行其道的重要原因。

但是!基于 Linux Namespace 的隔離機(jī)制相比于虛擬化技術(shù)也有很多不足之處,其中最主要的問(wèn)題就是隔離不徹底。

  • 首先,容器只是運(yùn)行在宿主機(jī)上的一種特殊進(jìn)程,那么容器之間使用的還是同一個(gè)宿主機(jī)上的操作系統(tǒng)。盡管可以在容器里面通過(guò) mount namesapce 單獨(dú)掛載其他不同版本的操作系統(tǒng)文件,比如 centos、ubuntu,但是這并不能改變共享宿主機(jī)內(nèi)核的事實(shí)。這就意味著你要在 windows 上運(yùn)行 Linux 容器,或者在低版本的 Linux 宿主機(jī)上運(yùn)行高版本的 Linux 容器都是行不通的。

    而擁有虛擬機(jī)技術(shù)和獨(dú)立 Guest OS 的虛擬機(jī)就要方便多了。

  • 其次,在 Linux 內(nèi)核中,有很多資源和對(duì)象都是不能被 namespace 化的,比如時(shí)間。假如你的容器中的程序使用 settimeofday(2) 系統(tǒng)調(diào)用修改了時(shí)間,整個(gè)宿主機(jī)的時(shí)間都會(huì)被隨之修改。

    相比虛擬機(jī)里面可以隨意折騰的自由度,在容器里部署應(yīng)用的時(shí)候,“什么能做,什么不能做” 是用戶(hù)必須考慮的一個(gè)問(wèn)題。之外,容器給應(yīng)用暴露出來(lái)的攻擊面是相當(dāng)大的,應(yīng)用“越獄”的難度也比虛擬機(jī)低很多。雖然,實(shí)踐中可以使用 Seccomp 等技術(shù)對(duì)容器內(nèi)部發(fā)起的所有系統(tǒng)調(diào)用進(jìn)行過(guò)濾和甄別來(lái)進(jìn)行安全加固,但這種方式因?yàn)槎嗔艘粚訉?duì)系統(tǒng)調(diào)用的過(guò)濾,也會(huì)對(duì)容器的性能產(chǎn)生影響。因此,在生產(chǎn)環(huán)境中沒(méi)有人敢把運(yùn)行在物理機(jī)上的 Linux 容器直接暴露到公網(wǎng)上。

另外,容器是一個(gè)“單進(jìn)程”模型。容器的本質(zhì)是一個(gè)進(jìn)程,用戶(hù)的應(yīng)用進(jìn)程實(shí)際上就是容器里 PID=1 的進(jìn)程,而這個(gè)進(jìn)程也是后續(xù)創(chuàng)建的所有進(jìn)程的父進(jìn)程。這也就意味著,在一個(gè)容器中,你沒(méi)辦法同時(shí)運(yùn)行兩個(gè)不同的應(yīng)用,除非能事先找到一個(gè)公共的 PID=1 的程序來(lái)充當(dāng)兩者的父進(jìn)程,比如使用 systemd 或者 supervisord。容器的設(shè)計(jì)更多是希望容器和應(yīng)用同生命周期的,而不是容器還在運(yùn)行,而里面的應(yīng)用早已經(jīng)掛了。

到此,相信大家對(duì)“Namespace機(jī)制是什么”有了更深的了解,不妨來(lái)實(shí)際操作一番吧!這里是億速云網(wǎng)站,更多相關(guān)內(nèi)容可以進(jìn)入相關(guān)頻道進(jìn)行查詢(xún),關(guān)注我們,繼續(xù)學(xué)習(xí)!

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI