Docker之Linux Namespace


Linux Namespace 介紹

我們經常聽到說Docker 是一個使用了Linux Namespace 和 Cgroups 的虛擬化工具,但是什么是Linux Namespace 它在Docker內是怎么被使用的,說到這里很多人就會迷茫,下面我們就先介紹一下Linux Namespace 以及它們是如何在容器里面使用的。

概念

Linux Namespace 是kernel 的一個功能,它可以隔離一系列系統的資源,比如PID(Process ID),User ID, Network等等。一般看到這里,很多人會想到一個命令chroot,就像chroot允許把當前目錄變成根目錄一樣(被隔離開來的),Namesapce也可以在一些資源上,將進程隔離起來,這些資源包括進程樹,網絡接口,掛載點等等。

比如一家公司向外界出售自己的計算資源。公司有一台性能還不錯的服務器,每個用戶買到一個tomcat實例用來運行它們自己的應用。有些調皮的客戶可能不小心進入了別人的tomcat實例,修改或者關閉了其中的某些資源,這樣就會導致各個客戶之間互相干擾。也許你會說,我們可以限制不同用戶的權限,讓用戶只能訪問自己名下的tomcat,但是有些操作可能需要系統級別的權限,比如root。我們不可能給每個用戶都授予root權限,也不可能給每個用戶都提供一台全新的物理主機讓他們互相隔離,因此這里Linux Namespace就派上了用場。使用Namespace, 我們就可以做到UID級別的隔離,也就是說,我們可以以UID為n的用戶,虛擬化出來一個namespace,在這個namespace里面,用戶是具有root權限的。但是在真實的物理機器上,他還是那個UID為n的用戶,這樣就解決了用戶之間隔離的問題。當然這個只是Namespace其中一個簡單的功能。

PID映射關系圖

除了User Namespace ,PID也是可以被虛擬的。命名空間建立系統的不同視圖, 對於每一個命名空間,從用戶看起來,應該像一台單獨的Linux計算機一樣,有自己的init進程(PID為1),其他進程的PID依次遞增,A和B空間都有PID為1的init進程,子容器的進程映射到父容器的進程上,父容器可以知道每一個子容器的運行狀態,而子容器與子容器之間是隔離的。從圖中我們可以看到,進程3在父命名空間里面PID 為3,但是在子命名空間內,他就是1.也就是說用戶從子命名空間 A 內看進程3就像 init 進程一樣,以為這個進程是自己的初始化進程,但是從整個 host 來看,他其實只是3號進程虛擬化出來的一個空間而已。

當前Linux一共實現六種不同類型的namespace。

Namespace類型 系統調用參數 內核版本
Mount namespaces CLONE_NEWNS 2.4.19
UTS namespaces CLONE_NEWUTS 2.6.19
IPC namespaces CLONE_NEWIPC 2.6.19
PID namespaces CLONE_NEWPID 2.6.24
Network namespaces CLONE_NEWNET 2.6.29
User namespaces CLONE_NEWUSER 3.8

Namesapce 的API主要使用三個系統調用

  • clone() - 創建新進程。根據系統調用參數來判斷哪種類型的namespace被創建,而且它們的子進程也會被包含到namespace中
  • unshare() - 將進程移出某個namespace
  • setns() - 將進程加入到namesp中

UTS Namespace

UTS namespace 主要隔離nodenamedomainname兩個系統標識。在UTS namespace里面,每個 namespace 允許有自己的hostname。

下面我們將使用Go來做一個UTS Namespace 的例子。其實對於 Namespace 這種系統調用,使用 C 語言來描述是最好的,但是本書的目的是去實現 docker,由於 docker 就是使用 Go 開發的,那么我們就整體使用 Go 來講解。先來看一下代碼,非常簡單:

package main

import (
    "os/exec" "syscall" "os" "log" ) func main() { cmd := exec.Command("sh") cmd.SysProcAttr = &syscall.SysProcAttr{ Cloneflags: syscall.CLONE_NEWUTS, } cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { log.Fatal(err) } } 

解釋一下代碼,exec.Command('sh') 是去指定當前命令的執行環境,我們默認使用sh來執行。下面的就是設置系統調用參數,像我們前面講到的一樣,使用CLONE_NEWUTS這個標識符去創建一個UTS Namespace。Go幫我們封裝了對於clone()函數的調用,這個代碼執行后就會進入到一個sh 運行環境中。

我們在ubuntu 14.04上運行這個程序,kernel版本3.13.0-65-generic,go 版本1.7.3,執行go run main.go,我們在這個交互式環境里面使用pstree -pl查看一下系統中進程之間的關系

|-sshd(19820)---bash(19839)---go(19901)-+-main(19912)-+-sh(19915)--- pstree(19916) 

然后我們輸出一下當前的 PID

# echo $$ 19915 

驗證一下我們的父進程和子進程是否不在同一個UTS namespace

# readlink /proc/19912/ns/uts uts:[4026531838] # readlink /proc/19915/ns/uts uts:[4026532193] 

可以看到他們確實不在同一個UTS namespace。由於UTS Namespace是對hostname做了隔離,那么我們在這個環境內修改hostname應該不影響外部主機,下面我們來做一下實驗。

在這個sh環境內執行

修改hostname 為bird然后打印出來 # hostname -b bird # hostname bird 

我們另外啟動一個shell在宿主機上運行一下hostname看一下效果

root@iZ254rt8xf1Z:~# hostname iZ254rt8xf1Z 

可以看到外部的 hostname 並沒有被內部的修改所影響,由此就了解了UTS Namespace的作用。

IPC Namespace

IPC Namespace 是用來隔離 System V IPC 和POSIX message queues.每一個IPC Namespace都有他們自己的System V IPC 和POSIX message queue。

我們在上一版本的基礎上稍微改動了一下代碼

package main

import (
    "log" "os" "os/exec" "syscall" ) func main() { cmd := exec.Command("sh") cmd.SysProcAttr = &syscall.SysProcAttr{ Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC, } cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { log.Fatal(err) } } 

可以看到我們僅僅增加syscall.CLONE_NEWIPC代表我們希望創建IPC Namespace。下面我們需要打開兩個shell 來演示隔離的效果。

首先在宿主機上打開一個 shell

查看現有的ipc Message Queues
root@iZ254rt8xf1Z:~# ipcs -q ------ Message Queues -------- key msqid owner perms used-bytes messages 下面我們創建一個message queue root@iZ254rt8xf1Z:~# ipcmk -Q Message queue id: 0 然后再查看一下 root@iZ254rt8xf1Z:~# ipcs -q ------ Message Queues -------- key msqid owner perms used-bytes messages 0x5e8f3f1e 0 root 644 0 0 

這里我們發現是可以看到一個queue了。下面我們使用另外一個shell去運行我們的程序。

root@iZ254rt8xf1Z:~/gocode/src/book# go run main.go # ipcs -q ------ Message Queues -------- key msqid owner perms used-bytes messages 

通過這里我們可以發現,在新創建的Namespace里面,我們看不到宿主機上已經創建的message queue,說明我們的 IPC Namespace 創建成功,IPC 已經被隔離。

PID Namesapce

PID namespace是用來隔離進程 id。同樣的一個進程在不同的 PID Namespace 里面可以擁有不同的 PID。這樣就可以理解,在 docker container 里面,我們使用ps -ef 經常能發現,容器內在前台跑着的那個進程的 PID 是1,但是我們在容器外,使用ps -ef會發現同樣的進程卻有不同的 PID,這就是PID namespace 干的事情。

再前面的代碼基礎之上,我們再修改一下代碼,添加了一個syscall.CLONE_NEWPID

package main

import (
    "log" "os" "os/exec" "syscall" ) func main() { cmd := exec.Command("sh") cmd.SysProcAttr = &syscall.SysProcAttr{ Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC | syscall.CLONE_NEWPID, } cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { log.Fatal(err) } } 

我們需要打開兩個 shell,首先我們在宿主機上看一下進程樹,找一下我們的進程的真實 PID

root@iZ254rt8xf1Z:~# pstree -pl |-sshd(894)-+-sshd(9455)---bash(9475)---bash(19619) | |-sshd(19715)---bash(19734) | |-sshd(19853)---bash(19872)---go(20179)-+-main(20190)-+-sh(20193) | | | |-{main}(20191) | | | `-{main}(20192) | | |-{go}(20180) | | |-{go}(20181) | | |-{go}(20182) | | `-{go}(20186) | `-sshd(20124)---bash(20144)---pstree(20196) 

可以看到,我們的go main 函數運行的pid為 20190。下面我們打開另外一個 shell 運行一下我們的代碼

root@iZ254rt8xf1Z:~/gocode/src/book# go run main.go # echo $$ 1 

可以看到,我們打印了當前namespace的pid,發現是1,也就是說。這個20190 PID 被映射到 namesapce 里面的 PID 為1.這里還不能使用ps 來查看,因為ps 和 top 等命令會使用/proc內容,我們會在下面的mount namesapce講解。

Mount Namespace

mount namespace 是用來隔離各個進程看到的掛載點視圖。在不同namespace中的進程看到的文件系統層次是不一樣的。在mount namespace 中調用mount()umount()僅僅只會影響當前namespace內的文件系統,而對全局的文件系統是沒有影響的。

看到這里,也許就會想到chroot()。它也是將某一個子目錄變成根節點。但是mount namespace不僅能實現這個功能,而且能以更加靈活和安全的方式實現。

mount namespace是Linux 第一個實現的namesapce 類型,因此它的系統調用參數是NEWNS(new namespace 的縮寫)。貌似當時人們沒有意識到,以后還會有很多類型的namespace加入Linux大家庭。

我們針對上面的代碼做了一點改動,增加了NEWNS 標識。

package main

import (
    "log" "os" "os/exec" "syscall" ) func main() { cmd := exec.Command("sh") cmd.SysProcAttr = &syscall.SysProcAttr{ Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS, } cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { log.Fatal(err) } } 

首先我們運行代碼后,查看一下/proc的文件內容。proc 是一個文件系統,它提供額外的機制可以從內核和內核模塊將信息發送給進程。

# ls /proc
1 14 19872 23 34 43 55 739 865 bus filesystems kpagecount pagetypeinfo sysvipc 10 145 2 24 348 44 57 75 866 cgroups fs kpageflags partitions timer_list 100 1472 20 25 35 45 58 76 869 cmdline interrupts latency_stats sched_debug timer_stats 11 1475 20124 26 353 47 59 77 894 consoles iomem loadavg schedstat tty 1174 15 20129 27 36 48 6 776 9 cpuinfo ioports locks scsi uptime 1192 154 20144 28 37 49 60 78 937 crypto ipmi mdstat self version 12 155 20215 29 38 5 607 796 945 devices irq meminfo slabinfo version_signature 1255 16 20226 3 39 50 61 8 9460 diskstats kallsyms misc softirqs vmallocinfo 1277 17 20229 30 391 51 62 827 967 dma kcore modules stat vmstat 1296 18 20231 31 40 52 63 836 99 driver key-users mounts swaps xen 13 19 21 32 41 53 7 860 acpi execdomains keys mtrr sys zoneinfo 1309 19853 22 33 42 54 733 862 buddyinfo fb kmsg net sysrq-trigger 

因為這里的/proc還是宿主機的,所以我們看到里面會比較亂,下面我們將/proc mount到我們自己的namesapce下面來。

# mount -t proc proc /proc # ls /proc 1 consoles execdomains ipmi kpagecount misc sched_debug swaps uptime 5 cpuinfo fb irq kpageflags modules schedstat sys version acpi crypto filesystems kallsyms latency_stats mounts scsi sysrq-trigger version_signature buddyinfo devices fs kcore loadavg mtrr self sysvipc vmallocinfo bus diskstats interrupts key-users locks net slabinfo timer_list vmstat cgroups dma iomem keys mdstat pagetypeinfo softirqs timer_stats xen cmdline driver ioports kmsg meminfo partitions stat tty zoneinfo 

可以看到,瞬間少了好多命令。下面我們就可以使用 ps 來查看系統的進程了。

# ps -ef UID PID PPID C STIME TTY TIME CMD root  1  0  0 20:15 pts/4 00:00:00 sh root  6  1  0 20:19 pts/4 00:00:00 ps -ef 

可以看到,在當前namesapce里面,我們的sh 進程是PID 為1 的進程。這里就說明,我們當前的Mount namesapce 里面的mount 和外部空間是隔離的,mount 操作並沒有影響到外部。Docker volume 也是利用了這個特性。

User Namesapce

User namespace 主要是隔離用戶的用戶組ID。也就是說,一個進程的User ID 和Group ID 在User namespace 內外可以是不同的。比較常用的是,在宿主機上以一個非root用戶運行創建一個User namespace,然后在User namespace里面卻映射成root 用戶。這樣意味着,這個進程在User namespace里面有root權限,但是在User namespace外面卻沒有root的權限。從Linux kernel 3.8開始,非root進程也可以創建User namespace ,並且此進程在namespace里面可以被映射成 root並且在 namespace內有root權限。

下面我們繼續以一個例子來描述.

package main

import (
    "log" "os" "os/exec" "syscall" ) func main() { cmd := exec.Command("sh") cmd.SysProcAttr = &syscall.SysProcAttr{ Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS | syscall.CLONE_NEWUSER, } cmd.SysProcAttr.Credential = &syscall.Credential{Uid: uint32(1), Gid: uint32(1)} cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { log.Fatal(err) } os.Exit(-1) } 

我們在原來的基礎上增加了syscall.CLONE_NEWUSER。首先我們以root來運行這個程序,運行前在宿主機上我們看一下當前用戶和用戶組

root@iZ254rt8xf1Z:~/gocode/src/book# id uid=0(root) gid=0(root) groups=0(root) 

可以看到我們是root 用戶,我們運行一下程序

root@iZ254rt8xf1Z:~/gocode/src/book# go run main.go $ id uid=65534(nobody) gid=65534(nogroup) groups=65534(nogroup) 

Network Namespace

Network namespace 是用來隔離網絡設備,IP地址端口等網絡棧的namespace。Network namespace 可以讓每個容器擁有自己獨立的網絡設備(虛擬的),而且容器內的應用可以綁定到自己的端口,每個 namesapce 內的端口都不會互相沖突。在宿主機上搭建網橋后,就能很方便的實現容器之間的通信,而且每個容器內的應用都可以使用相同的端口。

同樣,我們在原來的代碼上增加一點。我們增加了syscall.CLONE_NEWNET 這里標識符。

package main

import (
    "log" "os" "os/exec" "syscall" ) func main() { cmd := exec.Command("sh") cmd.SysProcAttr = &syscall.SysProcAttr{ Cloneflags: syscall.CLONE_NEWUTS | syscall.CLONE_NEWIPC | syscall.CLONE_NEWPID | syscall.CLONE_NEWNS | syscall.CLONE_NEWUSER | syscall.CLONE_NEWNET, } cmd.SysProcAttr.Credential = &syscall.Credential{Uid: uint32(1), Gid: uint32(1)} cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr if err := cmd.Run(); err != nil { log.Fatal(err) } os.Exit(-1) } 

首先我們在宿主機上查看一下自己的網絡設備。

root@iZ254rt8xf1Z:~/gocode/src/book# ifconfig docker0 Link encap:Ethernet HWaddr 02:42:d7:5d:c3:b9 inet addr:192.168.0.1 Bcast:0.0.0.0 Mask:255.255.240.0 UP BROADCAST MULTICAST MTU:1500 Metric:1 RX packets:0 errors:0 dropped:0 overruns:0 frame:0 TX packets:0 errors:0 dropped:0 overruns:0 carrier:0 collisions:0 txqueuelen:0 RX bytes:0 (0.0 B) TX bytes:0 (0.0 B) eth0 Link encap:Ethernet HWaddr 00:16:3e:00:38:cc inet addr:10.170.174.187 Bcast:10.170.175.255 Mask:255.255.248.0 UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1 RX packets:5605 errors:0 dropped:0 overruns:0 frame:0 TX packets:1819 errors:0 dropped:0 overruns:0 carrier:0 collisions:0 txqueuelen:1000 RX bytes:7129227 (7.1 MB) TX bytes:159780 (159.7 KB) eth1 Link encap:Ethernet HWaddr 00:16:3e:00:6d:4d inet addr:101.200.126.205 Bcast:101.200.127.255 Mask:255.255.252.0 UP BROADCAST RUNNING MULTICAST MTU:1500 Metric:1 RX packets:15433 errors:0 dropped:0 overruns:0 frame:0 TX packets:6888 errors:0 dropped:0 overruns:0 carrier:0 collisions:0 txqueuelen:1000 RX bytes:13287762 (13.2 MB) TX bytes:1787482 (1.7 MB) lo Link encap:Local Loopback inet addr:127.0.0.1 Mask:255.0.0.0 UP LOOPBACK RUNNING MTU:65536 Metric:1 RX packets:0 errors:0 dropped:0 overruns:0 frame:0 TX packets:0 errors:0 dropped:0 overruns:0 carrier:0 collisions:0 txqueuelen:0 RX bytes:0 (0.0 B) TX bytes:0 (0.0 B) 

可以看到我們宿主機上有lo, eth0, eth1 等網絡設備,下面我們運行一下程序去Network namespce 里面去看看。

root@iZ254rt8xf1Z:~/gocode/src/book# go run main.go $ ifconfig $ 

我們發現,在Namespace 里面什么網絡設備都沒有。這樣就能展現 Network namespace 與宿主機之間的網絡隔離。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM