Docker隔離技術


前言

Docker系列文章:

此篇是Docker系列的第九篇,之前的文章里面或多或少的提到Docker的隔離技術,但是沒有很清楚的去聊這個技術,但是經過這么多文章大家一定對Docker使用和概念有了一定的理解,接下來我們聊下底層一些技術,幫助大家解解惑,先從隔離技術開始吧。此外大家一定要按照我做的Demo都手敲一遍,印象會更加深刻的,加油!

  1. 為什么要學習Docker
  2. Docker基本概念
  3. Docker鏡像基本原理
  4. Docker容器數據卷
  5. Dockerfile
  6. Docker單機網絡上
  7. Docker單機網絡下
  8. Docker單機網絡實戰

如何理解Namespace

進入一個容器內部:

docker exec -it ad9342449b86 /bin/bash

通過ps命令查看容器內部的進程,容器內部只有兩個進程在運行,看不到操作系統的其他進程,一個進程執行的/bin/bash,另外一個執行的ps,說明此刻容器被 Docker 隔離在了一個跟宿主機在不同的環境中。

img
img

對於宿主機來說相當於我們在宿主機上執行一個/bin/bash的進程,宿主機給這個進程起一個獨一無二名字,比如叫PID=800,用來區分與其他進程的不同,Docker在運行/bin/bash的時候,會與宿主機上的其他進程進行隔離,讓他看不到其他進程運行狀況,並且重新計算進程號,也就是我們看到202,但是這個進程實際上是宿主機的進程,這種技術,其實就是對被隔離應用的進程空間做了手腳,使得這些進程只能看到重新計算過的進程編號,這就是Linux里面的Namespace機制,也就是Docker采用的隔離技術。

總結一下,Namespace是Linux內核用來隔離內核資源的方式。通過Namespace可以讓一些進程只能看到與自己相關的一部分資源,而另外一些進程也只能看到與它們自己相關的資源,這兩撥進程根本就感覺不到對方的存在。具體的實現方式是把一個或多個進程的相關資源指定在同一個Namespace中。Linux Namespace是對全局系統資源的一種封裝隔離,使得處於不同Namespace的進程擁有獨立的全局系統資源,改變一個Namespace中的系統資源只會影響當前Namespace里的進程,對其他Namespace中的進程沒有影響。

Namespace類型介紹

目前,Linux 內核實現了6種 Namespace:

img
img
六種命名空間:
  1. UTS Namespace:

    UTS Namespace 對主機名和域名進行隔離。為什么要隔離主機名?因為主機名可以代替IP來訪問。如果不隔離,同名訪問會出沖突。

  2. IPC Namespace

    Linux 提供很多種進程通信機制,IPC Namespace 針對 System V 和 POSIX 消息隊列,這些 IPC 機制會使用標識符來區別不同的消息隊列,然后兩個進程通過標識符找到對應的消息隊列。IPC namespace使得相同的標識符在兩個Namespace代表不同的消息隊列,因此兩個Namespace 中的進程不能通過 IPC 來通信。

  3. PID Namespace

    PID Namespace 用來隔離進程的 PID 空間,使得不同 PID Namespace 里的進程 PID 可以重復且互不影響。PID Namespace 對容器類應用特別重要, 可以實現容器內進程的暫停/恢復等功能,還可以支持容器在跨主機的遷移前后保持內部進程的 PID 不發生變化。

  4. Mount Namespace

    Mount Namespace 為進程提供獨立的文件系統視圖。可以這么理解,Mount Namespace 用來隔離文件系統的掛載點,這樣進程就只能看到自己的Mount Namespace中的文件系統掛載點。進程的Mount Namespace中的掛載點信息可以在 /proc/[pid]/mounts、/proc/[pid]/mountinfo 和 /proc/[pid]/mountstats 這三個文件中找到。在一個 Namespace 里掛載、卸載的動作不會影響到其他 Namespace。

  5. Network Namespace

    Network Namespace 在邏輯上是網絡堆棧的一個副本,它有自己的路由、防火牆規則和網絡設備。默認情況下,子進程繼承其父進程的 Network Namespace。每個新創建的 Network Namespace 默認有一個本地環回接口 lo,除此之外,所有的其他網絡設備(物理/虛擬網絡接口,網橋等)只能屬於一個 Network Namespace。每個 socket 也只能屬於一個 Network Namespace。

  6. User Namespace

    User Namespace 用於隔離安全相關的資源,包括 user IDs and group IDs,keys, 和 capabilities。同樣一個用戶的 user ID 和 group ID 在不同的User Namespace 中可以不一樣(與 PID Namespace 類似)。可以這樣理解,一個用戶可以在一個User Namespace中是普通用戶,但在另一個User Namespace中是root用戶。

Namespace原理介紹

Linux Namespace 是 Linux 提供的一種內核級別環境隔離的方法,因此Linux 提供了多個 API 用來操作 Namespace,它們是 clone()、setns() 和 unshare() 函數,簡單介紹一下三個系統調用的功能:

  1. clone() : 實現線程的系統調用,用來創建一個新的進程,並可以通過設計上述系統調用參數達到隔離的目的;
  2. unshare() : 使某進程脫離某個 namespace;
  3. setns() : 把某進程加入到某個 namespace;

為了確定隔離的到底是哪項 Namespace,在使用這些 API 時,通常需要指定一些調用參數:CLONE_NEWIPC、CLONE_NEWNET、CLONE_NEWNS、CLONE_NEWPID、CLONE_NEWUSER、CLONE_NEWUTS 和 CLONE_NEWCGROUP。下圖是各個Namespace對應的調用參數和Linux內核:

img
img

如果要同時隔離多個 Namespace,可以使用 | (按位或)組合這些參數。同時我們還可以通過 /proc 下面的一些文件來操作 Namespace。下面就讓讓我們看看這些接口的用法:

查看進程所屬的Namespace

從版本號為 3.8 的內核開始,/proc/[pid]/ns 目錄下會包含進程所屬的 Namespace 信息,使用下面的命令可以查看當前進程所屬的 Namespace 信息:

ls -l /proc/$$/ns

這些 Namespace 文件都是鏈接文件。鏈接文件的內容的格式為 xxx:[inode number]。其中的 xxx 為 Namespace 的類型,inode number 則用來標識一個 Namespace,我們也可以把它理解為 Namespace 的 ID。如果兩個進程的某個 Namespace 文件指向同一個鏈接文件,說明其相關資源在同一個 Namespace 中。

clone() 函數

我們可以通過 clone() 在創建新進程的同時創建 Namespace。clone() 在 C 語言庫中的聲明如下:

#define _GNU_SOURCE
#include <sched.h>
int clone(int (*fn)(void *), void *child_stack, int flags, void *arg);

實際上,clone() 是在 C 語言庫中定義的一個封裝(wrapper)函數,它負責建立新進程的堆棧並且調用對編程者隱藏的 clone() 系統調用。四個參數代表的意思是:

  1. child_func : 傳入子進程運行的程序主函數;
  2. child_stack : 傳入子進程使用的棧空間;
  3. flags : 表示使用哪些 CLONE_* 標志位;
  4. args : 用於傳入用戶參數;

clone() 與 fork() 類似,都相當於把當前進程復制了一份,但 clone() 可以更細粒度地控制與子進程共享的資源(可以通過 flags 來控制),包括虛擬內存、打開的文件描述符和信號量等等。一旦指定了標志位 CLONE_NEW*,相對應類型的 Namespace 就會被創建,新創建的進程也會成為該 Namespace 中的一員。

setns() 函數

setns() 函數可以將當前進程加入到已有的 Namespace 中。setns() 在 C 語言庫中的聲明如下:

#define _GNU_SOURCE
#include <sched.h>
int setns(int fd, int nstype);

和 clone() 函數一樣,C 語言庫中的 setns() 函數也是對 setns() 系統調用的封裝:

  1. fd:表示要加入 Namespace 的文件描述符。它是一個指向 /proc/[pid]/ns 目錄中文件的文件描述符,可以通過直接打開該目錄下的鏈接文件或者打開一個掛載了該目錄下鏈接文件的文件得到;
  2. nstype:參數 nstype 讓調用者可以檢查 fd 指向的 Namespace 類型是否符合實際要求。若把該參數設置為 0 表示不檢查;
unshare() 函數

unshare() 函數可以在原進程上進行 Namespace 隔離。也就是創建並加入新的 Namespace 。unshare() 在 C 語言庫中的聲明如下:

#define _GNU_SOURCE
#include <sched.h>
int unshare(int flags);

unshare() 函數也是對 unshare() 系統調用的封裝。調用 unshare() 的主要作用就是:不啟動新的進程就可以起到資源隔離的效果,相當於跳出原先的 Namespace 進行操作。

使用PID Namesapce達到Docker線程隔離狀態

Linux下的每個進程都有一個對應的 /proc/PID 目錄,該目錄包含了大量的有關當前進程的信息。 對一個 PID Namespace 而言,/proc 目錄只包含當前 Namespace 和它所有子孫后代 Namespace 里的進程的信息。

創建一個新的 PID Namespace 后,如果想讓子進程中的 top、ps 等依賴 /proc 文件系統的命令工作,還需要掛載 /proc 文件系統。使用如下命令創建一個新的PID Namespace:

#查看當前進程的PID
echo $$
#查看該線程對應PID Namespace
readlink /proc/$$/ns/pid
#使用unshare命令創建新的PID Namespace
#該命令會同時創建新的PID和Mount namespace
unshare --pid --mount --fork /bin/bash
#查看創建的線程的PID Namespace
readlink /proc/$$/ns/pid

上圖中新生成的PID Namespace的Id並沒有發生改變,我們看到在新創建的PID Namespace也可以看到其他的進程的運行情況,顯然這個和我們說的隔離是不一致的,接下來我們看下原因是什么:

img
img
#查看當前進程的PID
echo $$
#查看當前進程的詳情
ps 1

這個例子說明當前進程被認為是該 PID namespace 中的 1 號進程了,通過PS命名查看1號進程的詳情,發現這個進程是系統的啟動相關(CentOS 7開始也由systemd取代了init作為默認的系統進程管理工具)。

造成混亂的原因是當前進程沒有正確的掛載 /proc 文件系統,由於我們新的 Mount Namespace 的掛載信息是從老的 Namespace 拷貝過來的,所以這里看到的還是老 Namespace 里面的進程號為 1 的信息。執行下面的命令掛載 /proc 文件系統:

mount -t proc proc /proc

接下來我們再來檢查相關的信息,會發現新建的 PID namespace 進程看不到宿主機的進程信息了。

img
img

我們也可以使用如下命令來創建進程,

unshare --pid --mount-proc --fork /bin/bash

這樣在創建了 PID 和 Mount Namespace 后,會自動掛載 /proc 文件系統,就不需要我們手動執行 mount -t proc proc /proc 命令了。

總結

Docker容器是在創建容器進程時,指定了這個進程所需要啟用的一組Namespace參數,這樣容器就只能看到到當前Namespace所限定的資源、文件、設備、狀態,或者配置。而對於宿主機以及其他不相關的程序,它就完全看不到了,因此容器本質上就是一個特殊的進程。

結束

歡迎大家點點關注,點點贊!


免責聲明!

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



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