天天看點

Docker之Linux NamespaceLinux 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其中一個簡單的功能。

Docker之Linux NamespaceLinux Namespace 介紹

除了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 主要隔離

nodename

domainname

兩個系統辨別。在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

檢視一下系統中程序之間的關系

然後我們輸出一下目前的 PID

# echo $$

           

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

# readlink /proc/19912/ns/uts
uts:[
           

可以看到他們确實不在同一個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: 
           

這裡我們發現是可以看到一個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 $$

           

可以看到,我們列印了目前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

           

因為這裡的/proc還是主控端的,是以我們看到裡面會比較亂,下面我們将/proc mount到我們自己的namesapce下面來。

# mount -t proc proc /proc
# ls /proc

           

可以看到,瞬間少了好多指令。下面我們就可以使用 ps 來檢視系統的程序了。

# ps -ef
UID        PID  PPID  C STIME TTY          TIME CMD
root        
           

可以看到,在目前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(-
           

我們在原來的基礎上增加了

syscall.CLONE_NEWUSER

。首先我們以root來運作這個程式,運作前在主控端上我們看一下目前使用者和使用者組

root@iZ254rt8xf1Z:~/gocode/src/book# id
uid=
           

可以看到我們是root 使用者,我們運作一下程式

root@iZ254rt8xf1Z:~/gocode/src/book# go run main.go
$ id
uid=
           

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

首先我們在主控端上檢視一下自己的網絡裝置。

可以看到我們主控端上有lo, eth0, eth1 等網絡裝置,下面我們運作一下程式去Network namespce 裡面去看看。

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

我們發現,在Namespace 裡面什麼網絡裝置都沒有。這樣就能展現 Network namespace 與主控端之間的網絡隔離。