Docker 基礎(chǔ)技術(shù):Linux Namespace(下)
在 Docker基礎(chǔ)技術(shù):Linux Namespace(上篇)中我們了解了,UTD、IPC、PID、Mount 四個namespace,我們模仿Docker做了一個相當(dāng)相當(dāng)山寨的鏡像。在這一篇中,主要想向大家介紹Linux的User和Network的Namespace。
好,下面我們就介紹一下還剩下的這兩個Namespace。
User Namespace
User Namespace主要是用了CLONE_NEWUSER的參數(shù)。使用了這個參數(shù)后,內(nèi)部看到的UID和GID已經(jīng)與外部不同了,默認(rèn)顯示為65534。那是因?yàn)槿萜髡也坏狡湔嬲腢ID所以,設(shè)置上了最大的UID(其設(shè)置定義在/proc/sys/kernel/overflowuid)。
要把容器中的uid和真實(shí)系統(tǒng)的uid給映射在一起,需要修改 /proc/
ID-inside-ns ID-outside-ns length
其中:
- 第一個字段ID-inside-ns表示在容器顯示的UID或GID,
- 第二個字段ID-outside-ns表示容器外映射的真實(shí)的UID或GID。
- 第三個字段表示映射的范圍,一般填1,表示一一對應(yīng)。
比如,把真實(shí)的uid=1000映射成容器內(nèi)的uid=0
- $cat/proc/2465/uid_map
- 0 1000 1
再比如下面的示例:表示把namespace內(nèi)部從0開始的uid映射到外部從0開始的uid,其最大范圍是無符號32位整形
- $cat/proc/$$/uid_map
- 0 0 4294967295
另外,需要注意的是:
- 寫這兩個文件的進(jìn)程需要這個namespace中的CAP_SETUID (CAP_SETGID)權(quán)限(可參看Capabilities)
- 寫入的進(jìn)程必須是此user namespace的父或子的user namespace進(jìn)程。
- 另外需要滿如下條件之一:1)父進(jìn)程將effective uid/gid映射到子進(jìn)程的user namespace中,2)父進(jìn)程如果有CAP_SETUID/CAP_SETGID權(quán)限,那么它將可以映射到父進(jìn)程中的任一uid/gid。
這些規(guī)則看著都煩,我們來看程序吧(下面的程序有點(diǎn)長,但是非常簡單,如果你讀過《Unix網(wǎng)絡(luò)編程》上卷,你應(yīng)該可以看懂):
- #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)
- staticcharcontainer_stack[STACK_SIZE];
- char*constcontainer_args[] = {
- “/bin/bash”,
- NULL
- };
- intpipefd[2];
- voidset_map(char* file,intinside_id,intoutside_id,intlen) {
- 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);
- }
- voidset_uid_map(pid_t pid,intinside_id,intoutside_id,intlen) {
- charfile[256];
- sprintf(file,”/proc/%d/uid_map”, pid);
- set_map(file, inside_id, outside_id, len);
- }
- voidset_gid_map(pid_t pid,intinside_id,intoutside_id,intlen) {
- charfile[256];
- sprintf(file,”/proc/%d/gid_map”, pid);
- set_map(file, inside_id, outside_id, len);
- }
- intcontainer_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)程間的同步) */
- charch;
- 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”);
- return1;
- }
- intmain()
- {
- constintgid=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());
- intcontainer_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
- //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());
- /* 通知子進(jìn)程 */
- close(pipefd[1]);
- waitpid(container_pid, NULL, 0);
- printf(“Parent – container stopped!\n”);
- return0;
- }
上面的程序,我們用了一個pipe來對父子進(jìn)程進(jìn)行同步,為什么要這樣做?因?yàn)樽舆M(jìn)程中有一個execv的系統(tǒng)調(diào)用,這個系統(tǒng)調(diào)用會把當(dāng)前子進(jìn)程的進(jìn)程空間給全部覆蓋掉,我們希望在execv之前就做好user namespace的uid/gid的映射,這樣,execv運(yùn)行的/bin/bash就會因?yàn)槲覀冊O(shè)置了uid為0的inside-uid而變成#號的提示符。
整個程序的運(yùn)行效果如下:
- hchen@ubuntu:~$id
- uid=1000(hchen) gid=1000(hchen)groups=1000(hchen)
- hchen@ubuntu:~$ ./user#<–以hchen用戶運(yùn)行
- Parent: eUID = 1000; eGID = 1000, UID=1000, GID=1000
- Parent [ 3262] – start a container!
- Parent [ 3262] – Container [ 3263]!
- Parent [ 3262] – user/groupmappingdone!
- Container [ 1] – inside the container!
- Container: eUID = 0; eGID = 0, UID=0, GID=0#<—Container里的UID/GID都為0了
- Container [ 1] – setuphostname!
- root@container:~# id #<—-我們可以看到容器里的用戶和命令行提示符是root用戶了
- uid=0(root) gid=0(root)groups=0(root),65534(nogroup)
雖然容器里是root,但其實(shí)這個容器的/bin/bash進(jìn)程是以一個普通用戶hchen來運(yùn)行的。這樣一來,我們?nèi)萜鞯陌踩詴玫教岣摺?/p>
我們注意到,User Namespace是以普通用戶運(yùn)行,但是別的Namespace需要root權(quán)限,那么,如果我要同時使用多個Namespace,該怎么辦呢?一般來說,我們先用一般用戶創(chuàng)建User Namespace,然后把這個一般用戶映射成root,在容器內(nèi)用root來創(chuàng)建其它的Namesapce。
Network Namespace
Network的Namespace比較啰嗦。在Linux下,我們一般用ip命令創(chuàng)建Network Namespace(Docker的源碼中,它沒有用ip命令,而是自己實(shí)現(xiàn)了ip命令內(nèi)的一些功能——是用了Raw Socket發(fā)些“奇怪”的數(shù)據(jù),呵呵)。這里,我還是用ip命令講解一下。
首先,我們先看個圖,下面這個圖基本上就是Docker在宿主機(jī)上的網(wǎng)絡(luò)示意圖(其中的物理網(wǎng)卡并不準(zhǔn)確,因?yàn)閐ocker可能會運(yùn)行在一個VM中,所以,這里所謂的“物理網(wǎng)卡”其實(shí)也就是一個有可以路由的IP的網(wǎng)卡)

上圖中,Docker使用了一個私有網(wǎng)段,172.40.1.0,docker還可能會使用10.0.0.0和192.168.0.0這兩個私有網(wǎng)段,關(guān)鍵看你的路由表中是否配置了,如果沒有配置,就會使用,如果你的路由表配置了所有私有網(wǎng)段,那么docker啟動時就會出錯了。
當(dāng)你啟動一個Docker容器后,你可以使用ip link show或ip addr show來查看當(dāng)前宿主機(jī)的網(wǎng)絡(luò)情況(我們可以看到有一個docker0,還有一個veth22a38e6的虛擬網(wǎng)卡——給容器用的):
- hchen@ubuntu:~$ ip link show
- 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state …
- link/loopback00:00:00:00:00:00 brd 00:00:00:00:00:00
- 2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc …
- link/ether00:0c:29:b7:67:7d brd ff:ff:ff:ff:ff:ff
- 3: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 …
- link/ether56:84:7a:fe:97:99 brd ff:ff:ff:ff:ff:ff
- 5: veth22a38e6: <BROADCAST,UP,LOWER_UP> mtu 1500 qdisc …
- link/ether8e:30:2a:ac:8c:d1 brd ff:ff:ff:ff:ff:ff
那么,要做成這個樣子應(yīng)該怎么辦呢?我們來看一組命令:
- ## 首先,我們先增加一個網(wǎng)橋lxcbr0,模仿docker0
- brctl addbr lxcbr0
- brctl stp lxcbr0 off
- ifconfiglxcbr0 192.168.10.1/24up#為網(wǎng)橋設(shè)置IP地址
- ## 接下來,我們要創(chuàng)建一個network namespace – ns1
- # 增加一個namesapce 命令為 ns1 (使用ip netns add命令)
- ip netns add ns1
- # 激活namespace中的loopback,即127.0.0.1(使用ip netns exec ns1來操作ns1中的命令)
- ip netnsexecns1 ip linksetdev lo up
- ## 然后,我們需要增加一對虛擬網(wǎng)卡
- # 增加一個pair虛擬網(wǎng)卡,注意其中的veth類型,其中一個網(wǎng)卡要按進(jìn)容器中
- ip link add veth-ns1typeveth peer name lxcbr0.1
- # 把 veth-ns1 按到namespace ns1中,這樣容器中就會有一個新的網(wǎng)卡了
- ip linksetveth-ns1 netns ns1
- # 把容器里的 veth-ns1改名為 eth0 (容器外會沖突,容器內(nèi)就不會了)
- ip netnsexecns1 ip linksetdev veth-ns1 name eth0
- # 為容器中的網(wǎng)卡分配一個IP地址,并激活它
- ip netnsexecns1ifconfigeth0 192.168.10.11/24up
- # 上面我們把veth-ns1這個網(wǎng)卡按到了容器中,然后我們要把lxcbr0.1添加上網(wǎng)橋上
- brctl addif lxcbr0 lxcbr0.1
- # 為容器增加一個路由規(guī)則,讓容器可以訪問外面的網(wǎng)絡(luò)
- ip netnsexecns1 ip route add default via 192.168.10.1
- # 在/etc/netns下創(chuàng)建network namespce名稱為ns1的目錄,
- # 然后為這個namespace設(shè)置resolv.conf,這樣,容器內(nèi)就可以訪問域名了
- mkdir-p/etc/netns/ns1
- echo”nameserver 8.8.8.8″>/etc/netns/ns1/resolv.conf
上面基本上就是docker網(wǎng)絡(luò)的原理了,只不過,
- Docker的resolv.conf沒有用這樣的方式,而是用了上篇中的Mount Namesapce的那種方式
- 另外,docker是用進(jìn)程的PID來做Network Namespace的名稱的。
了解了這些后,你甚至可以為正在運(yùn)行的docker容器增加一個新的網(wǎng)卡:
- ip link add peerAtypeveth peer name peerB
- brctl addif docker0 peerA
- ip linksetpeerA up
- ip linksetpeerB netns ${container-pid}
- ip netnsexec${container-pid} ip linksetdev peerB name eth1
- ip netnsexec${container-pid} ip linkseteth1 up ;
- ip netnsexec${container-pid} ip addr add ${ROUTEABLE_IP} dev eth1 ;
上面的示例是我們?yōu)檎谶\(yùn)行的docker容器,增加一個eth1的網(wǎng)卡,并給了一個靜態(tài)的可被外部訪問到的IP地址。
這個需要把外部的“物理網(wǎng)卡”配置成混雜模式,這樣這個eth1網(wǎng)卡就會向外通過ARP協(xié)議發(fā)送自己的Mac地址,然后外部的交換機(jī)就會把到這個IP地址的包轉(zhuǎn)到“物理網(wǎng)卡”上,因?yàn)槭腔祀s模式,所以eth1就能收到相關(guān)的數(shù)據(jù),一看,是自己的,那么就收到。這樣,Docker容器的網(wǎng)絡(luò)就和外部通了。
當(dāng)然,無論是Docker的NAT方式,還是混雜模式都會有性能上的問題,NAT不用說了,存在一個轉(zhuǎn)發(fā)的開銷,混雜模式呢,網(wǎng)卡上收到的負(fù)載都會完全交給所有的虛擬網(wǎng)卡上,于是就算一個網(wǎng)卡上沒有數(shù)據(jù),但也會被其它網(wǎng)卡上的數(shù)據(jù)所影響。
這兩種方式都不夠完美,我們知道,真正解決這種網(wǎng)絡(luò)問題需要使用VLAN技術(shù),于是Google的同學(xué)們?yōu)長inux內(nèi)核實(shí)現(xiàn)了一個IPVLAN的驅(qū)動,這基本上就是為Docker量身定制的。
Namespace文件
上面就是目前Linux Namespace的玩法。 現(xiàn)在,我來看一下其它的相關(guān)東西。
讓我們運(yùn)行一下上篇中的那個pid.mnt的程序(也就是PID Namespace中那個mount proc的程序),然后不要退出。
- $ sudo ./pid.mnt
- [sudo] passwordforhchen:
- Parent [ 4599] – start a container!
- Container [ 1] – inside the container!
我們到另一個shell中查看一下父子進(jìn)程的PID:
- hchen@ubuntu:~$ pstree -p 4599
- pid.mnt(4599)───bash(4600)
我們可以到proc下(/proc//ns)查看進(jìn)程的各個namespace的id(內(nèi)核版本需要3.8以上)。
下面是父進(jìn)程的:
- hchen@ubuntu:~$sudols-l/proc/4599/ns
- total 0
- lrwxrwxrwx 1 root root 0 4月 7 22:01 ipc -> ipc:[4026531839]
- lrwxrwxrwx 1 root root 0 4月 7 22:01 mnt -> mnt:[4026531840]
- lrwxrwxrwx 1 root root 0 4月 7 22:01 net -> net:[4026531956]
- lrwxrwxrwx 1 root root 0 4月 7 22:01 pid -> pid:[4026531836]
- lrwxrwxrwx 1 root root 0 4月 7 22:01 user -> user:[4026531837]
- lrwxrwxrwx 1 root root 0 4月 7 22:01 uts -> uts:[4026531838]
下面是子進(jìn)程的:
- hchen@ubuntu:~$sudols-l/proc/4600/ns
- total 0
- lrwxrwxrwx 1 root root 0 4月 7 22:01 ipc -> ipc:[4026531839]
- lrwxrwxrwx 1 root root 0 4月 7 22:01 mnt -> mnt:[4026532520]
- lrwxrwxrwx 1 root root 0 4月 7 22:01 net -> net:[4026531956]
- lrwxrwxrwx 1 root root 0 4月 7 22:01 pid -> pid:[4026532522]
- lrwxrwxrwx 1 root root 0 4月 7 22:01 user -> user:[4026531837]
- lrwxrwxrwx 1 root root 0 4月 7 22:01 uts -> uts:[4026532521]
我們可以看到,其中的ipc,net,user是同一個ID,而mnt,pid,uts都是不一樣的。如果兩個進(jìn)程指向的namespace編號相同,就說明他們在同一個namespace下,否則則在不同namespace里面。
這些文件還有另一個作用,那就是,一旦這些文件被打開,只要其fd被占用著,那么就算PID所屬的所有進(jìn)程都已經(jīng)結(jié)束,創(chuàng)建的namespace也會一直存在。比如:我們可以通過:mount –bind /proc/4600/ns/uts ~/uts 來hold這個namespace。
另外,我們在上篇中講過一個setns的系統(tǒng)調(diào)用,其函數(shù)聲明如下:
- intsetns(intfd,intnstype);
其中第一個參數(shù)就是一個fd,也就是一個open()系統(tǒng)調(diào)用打開了上述文件后返回的fd,比如:
- fd = open(“/proc/4600/ns/nts”, O_RDONLY); // 獲取namespace文件描述符
- setns(fd, 0);// 加入新的namespace