Docker中關於Namespace隔離機制全面解析

🌟 前言

Docker 容器能夠在服務器中高效運行,離不開容器底層技術的支持。
 為瞭更好地理解容器的運行原理,本篇文章將會以 Linux 宿主機為例,介紹容器的底層技術,包括容器的命名空間、控制組、聯合文件系統等。

1. Docker基本架構

Docker 目前采用標準的 C/S 架構,即服務端—客戶端架構,服務端用於管理數據,客戶端負責與用戶交互,將獲取的用戶信息交由服務器處理,如圖所示👇

那麼上面的名詞是什麼意思呢?我這裡重新畫瞭一幅圖來解析👇

服務器與客戶機既可以運行在同一臺機器上,也可以運行在不同機器上,通過 Socket(套接字)或者 RESTful API 進行通信。

🍑 服務端

Docker 服務端也就是 Docker daemon,一般在宿主機後臺運行,接收來自客戶的請求、並處理這些請求。在設計上,Docker 服務端是一個模塊化的架構,通過專門的 Engine 模塊來分發、管理各個來自客戶端的任務。

Docker 服務端默認監聽本地的 unix:///var/run/Docker.sock 套接字,隻允許本地的 root 用戶或 Docker 用戶組成員訪問,可以通過 -H 參數來修改監聽的方式。

例如,讓服務器監聽本地的 TCP 連接 1234 端口,代碼如下所示:

此外,Docker 還支持通過 HTTPS 認證的方式來驗證訪問。

Debian/Ubuntu14.04 等使用 upstart 管理啟動服務的系統中,Docker 服務端的默認啟動配置文件在 /etc/default/Docker
 在使用 systemd 管理啟動服務的系統,配置文件在 /etc/systemd/system/Docker.service.d/Docker.conf

🍑 客戶端

用戶不能與服務端直接交互,Docker 客戶端為用戶提供一系列可執行命令,用戶通過這些命令與 Docker 服務端進行交互。

用戶使用的 Docker 可執行命令就是客戶端程序。與 Docker 服務端不同的是,客戶端發送命令後,等待服務端返回信息,收到返回信息後,客戶端立刻執行結束並退出。用戶執行新的命令時,需要再次調用客戶端命令。

同樣,客戶端默認通過本地的 unix:///var/run/Docker.sock 套接字向服務端發送命令。如果服務端不在默認監聽的地址,則需要用戶在執行命令時指定服務端地址,如圖所示👇

例如:假設服務端監聽本地的 TCP 連接 1234 端口 tcp://127.0.0.1:1234,隻有通過 -H 參數指定瞭正確的地址信息才能連接到服務端。

首先,查看 Docker 信息,示例代碼如下:

從以上示例中可以看到,Docker 並沒有連接到服務端,但 Docker 客戶端仍可以為用戶提供服務。

然後,通過命令指定正確的地址信息,再次查看 Docker 信息,示例代碼如下:

從以上示例中可以看到,指定瞭正確的地址信息之後,Docker 順利連接到服務端。

Docker 服務端運行在主機上, 通過 Socket 連接從客戶端訪問,服務端從客戶端接受命令並管理運行在主機上的容器。

2. Namespace

🍑 Namespace介紹

Linux 操作系統中,容器用來實現“隔離”的技術稱為 Namespace(命名空間)。

Namespace 技術實際上修改瞭應用進程看待整個計算機的 “視圖”,即應用進程的 “視線” 被操作系統做瞭限制,隻能 “看到” 某些指定的內容,如圖所示👇

但對於宿主機來說,這些被進行“隔離”的進程跟其他進程並沒有太大區別。

下面運行一個 CentOS 7 容器,示例代碼如下:

從以上示例中可以看到,bash 是這個容器內部的第 1 號進程,即 PID=1,而這個容器裡一共隻有兩個進程在運行,這就意味著,前面執行的 /bin/sh,以及剛剛執行的 ps,已經被 Docker 隔離在一個與宿主機完全不同的空間當中。

理論上,每當在宿主機上運行一個 /bin/sh 程序,操作系統都會給它分配一個進程編號,例如,PID=100。這個編號是進程的唯一標識,就像員工的工號一樣。所以,PID=100,可以粗略地理解為這個 /bin/sh 是公司裡的第 100 號員工。

而現在,要通過 Docker/bin/sh 運行在一個容器當中。這時,Docker 就會在這個第 100 號員工入職時給他施一個 “障眼法” 讓他永遠看不到前面的其他 99 個員工,這樣,他就會以為自己就是公司裡的第 1 號員工。

這種機制其實就是對被隔離應用的進程空間做瞭手腳,使這些進程隻能看到重新計算過的進程號,例如 PID=1。可實際上,它們在宿主機的操作系統裡,還是原來的第 100號 進程,如圖所示👇

下面通過宿主機查看容器進程號,示例代碼如下:

在宿主機中通過容器的 ID 號查看其進程號,可以看出其進程號為 7548,這就是 Linux 裡的 Namespace 機制。
 在 Linux 系統中創建線程調用是 clone() 函數,例如:int pid = clone(main_function, stack_size, SIGCHLD, NULL); 這個調用會創建一個新的進程,並且返回它的進程號。
 系統調用 clone() 創建一個新進程時,可以在參數中指定 CLONE_NEWPID ,例如:int pid = clone(main_function, stack_size, CLONE_NEWPID | SIGCHLD, NULL); 這時,新創建的這個進程將會 “看到” 一個全新的進程空間,在這空間裡,它的進程號是 1。在宿主機真實的進程空間裡,這個進程的真實進程號不變。
 當然,可以多次執行上面的 clone() 調用,這樣就會創建多個 PID Namespace,而每個 Namespace 裡的應用進程,都會認為自己是當前容器裡的第 1 號進程,它們既看不到宿主機裡真正的進程空間,也看不到其他 PID Namespace 裡的具體情況。

🍑 Namespace的類型

命名空間分為多種類型,對應用程序進行不同程度的隔離,下面挨個介紹。

🍇 Mount namespace

Mount Namespace 將一個文件系統的頂層目錄與另一個文件系統的子目錄關聯起來,使其成為一個整體。該子目錄稱為掛載點,這個動作稱為掛載。

🍇 UTS namespace

UTSUNIX Time-sharing SystemUNIX 分時系統)Namespace 提供主機名和域名的隔離,使子進程有獨立的主機名和域名,這一特性在 Docker 容器技術中被運用,使 Docker 容器在網絡上被視作一個獨立的節點,而不僅僅是宿主機上的一個進程。

🍇 IPC namespace

IPCInter-Process Communication,進程間通信)NamespaceUNIXLinux 下進程間通信的一種方式。
 IPC 有共享內存、信號量、消息隊列等方式。此外,也需要對 IPC 進行隔離,如此一來,隻有在同一個 Namespace 下的進程才能相互通信。
 IPC 需要有一個全局的 ID,既然是全局的,就意味著 Namespace 需要對這個 ID 號進行 隔離,不能讓其他 Namespace 的進程 “看到”。

🍇 PID namespace

PID Namespace 用來隔離進程的 ID 空間,使不同 PID Namespace 裡的進程 ID 號可以重復且相互之間不影響。
 
PID Namespace 可以嵌套,也就是說有父子關系。在當前 Namespace 裡面創建的所有新的 Namespace 都是當前 Namespace 的子 Namespace
 
在父 Namespace 裡面可以 “看到” 所有子 Namespace 裡的進程信息,而在子 Namespace 裡看不到父 Namespacelode 與其他子 Namespacelode 進程信息,如圖所示👇

🍇 Network Namespace

每個容器擁有獨立的網絡設備,IP 地址,IP 路由表,/proc/net 目錄,端口號等。這也使得一個 host 上多個容器內的網絡設備都是互相隔離的。

🍇 User namespace

User Namespace 用來隔離 User 權限相關的 Linux 資源,包括 User IDsGroup IDs
 這是目前實現的 Namespace 中最復雜的一個,因為 User 和權限息息相關,而權限又關聯著容器的安全問題。
 在不同的 User Namespace 中,同樣一個用戶的 User IDGroup ID 可以不一樣。也就是說,一個用戶可以在父 User Namespace 中是普通用戶,在子 User Namespace 中是超級用戶。

🍑 深入理解Namespace

下面通過一段簡單的代碼來查看 Namespace 是如何實現的,示例代碼如下:

在以上示例中,代碼段通過 clone() 調用,傳入各個 Namespace 對應的 clone flag,創建瞭一個新的子進程,該進程擁有自己的 Namespace。根據以上代碼可知,該進程擁有自己的 PIDMountUserNetIPC 以及 UTS Namespace

所以,Docker 在創建容器進程時,指定瞭這個進程所需要啟動的一組 Namespace 參數。這樣,容器就隻能 “看到” 當前 Namespace 所限定的資源、文件、設備、狀態、配置信息等。至於宿主機以及其他不相關的程序,它就完全看不到瞭。容器,其實是 Linux 系統中一種特殊的進程。

LinuxDocker 創建的隔離空間雖然是看不見摸不著,但是一個進程的 Namespace 信息在宿主機上是真實存在的,並且是以文件的方式存在,因為在 Linux 操作系統中,一切皆文件。

一個進程可以選擇加入到某個進程已有的 Namespace 當中,從而達到 “進入” 這個進程所在容器的目的,這正是 docker exec 的實現原理。

下面通過示例進行詳細講解,首先運行一個 CentOS 容器,示例代碼如下:

以上示例中在運行 Docker 容器的命令中添加瞭參數 -d,表示使容器在後臺運行。

查看當前正在運行 Docker 容器的進程號,示例代碼如下:

查看宿主機的 /proc 文件,可以看到這個 8589 進程所有 Namespace 對應的文件,示例代碼如下:

可以看到,一個進程的每種 Namespace 都在它對應的 /proc/[進程號]/ns 下有一個對應的虛擬文件,並且鏈接到一個真實的 Namespace 文件。

有瞭這樣的文件,就可以對 Namespace 做一些實質性的操作。例如,將進程加入到一個已經存在的 Namespace 當中。

這個操作依賴一個名為 setns()Linux 系統調用,示例代碼如下:

上述代碼共接收瞭兩個參數。

arvg[1],即當前進程要加入的 Namespace 文件的路徑,如 /proc/8589/ns/net。用戶要在這個 Namespace 裡運行的進程,如 /bin/bash

代碼的核心操作則是通過 open() 打開指定的 Namespace 文件,並把這個文件的描述符 fd 交給 setns() 使用。在 setns() 執行後,當前進程就加入瞭這個文件對應的 Namespace 當中。

🍑 Namespace的劣勢

強大的 Namespace 機制可以實現容器間的隔離,是容器底層技術中非常重要的一項,但也有不可否認的不足。

下面總結基於 Namespace 的隔離機制相對於虛擬化技術的不足之處,以便在生產環境中設法克服。

🍇 隔離不徹底

容器隻是運行在宿主機上的一種特殊的進程,多個容器之間使用的是同一個宿主機的操作系統內核。
 盡管可以在容器中通過 Mount Namespace 單獨掛載其他版本的操作系統文件,如 CentOS 或者 Ubuntu,但這並不能改變它們共享宿主機內核的事實。
 在 Windows 宿主機上運行 Linux 容器,或者在低版本的 Linux 宿主機上運行高版本的 Linux 容器,都是行不通的。
 相比之下,擁有硬件虛擬化技術和獨立 Guest OS 的虛擬機就要好用得多。最極端的例子是 Microsoft 的雲計算平臺 Azure,它就是運行在 Windows 服務器集群上的,但這並不妨礙用戶在上面創建各種 Linux 虛擬機。

🍇 有些資源和對象不能被Namespace化

如果容器中的程序調用 settimeofday() 修改瞭時間,整個宿主機的時間都會被修改。相較於在虛擬機裡面可以任意做修改,在容器裡部署應用時,需要用戶的操作上更加謹慎。

🍇 安全問題

因為共享宿主機內核,容器中的應用暴露出來的攻擊面很大。
 盡管生產實踐中可以使用 seccomp 等技術,對容器內部發起的所有系統調用進行過濾和甄別來進行安全加固,但這類方法因為多瞭一層對系統調用的過濾,會拖累容器的性能。
 通常情況下,也不清楚到底該開啟哪些系統調用,禁止哪些系統調用。

到此這篇關於Docker中關於Namespace隔離機制的奧秘的文章就介紹到這瞭,更多相關Docker Namespace隔離機制內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: