說透 Docker:虛擬化

本章內容將講解 Docker 虛擬化、虛擬化本質、namespace、cgroups。

Docker 虛擬化

關於Docker

本小節將介紹 Docker 虛擬化的一些特點。

 

Docker 是一個開放源程式碼軟體項目,自動化進行應用程式容器化部署,藉此在Linux作業系統上,提供一個額外的軟體抽象層,以及作業系統層虛擬化的自動管理機制。 -From wiki

docker_logo

在接觸 Docker 的過程中,或多或少會了解到 Docker 的虛擬化,最常見的介紹方式是對比 Docker 和虛擬機之間的差別,筆者這裡也給出兩者的對比表格,以便後面詳細地展開來講。

  虛擬機 Docker 容器
隔離程度 硬體級進程隔離 作業系統級進程隔離
系統 每個虛擬機都有一個單獨的作業系統 每個容器可以共享作業系統(共享作業系統內核)
啟動時間 需要幾分鐘 幾秒
體積大小 虛擬機鏡像GB級別 容器是輕量級的(KB/MB)
啟動鏡像 虛擬機鏡像比較難找到 預建的 docker 容器很容易獲得
遷移 虛擬機可以輕鬆遷移到新主機 容器被銷毀並重新創建而不是移動
創建速度 創建 VM 需要相對較長的時間 可以在幾秒鐘內創建容器
資源使用 GB級別 MB級別

Docker 中的虛擬化是依賴於 Windows 和 Linux 內核的,在 Windows 上會要求開啟 Hyper-V,在 Linux 上需要依賴 namespace 和 cgroups 等,因此這裡就不過多介紹 Docker 了,後面主要介紹 Linux 上的虛擬化技術。

傳統虛擬化部署方式

傳統虛擬化方式是在硬體抽象級別虛擬化,其特點是 虛擬化程度高。

![traditional_kvm ](./images/traditional_kvm .jpg)

傳統虛擬化方式的優點是:

1,虛擬機之間通過虛擬化技術隔離互不影響 2,物理機上可部署多台虛擬機,提升資源利用率 3,應用資源分配、擴容通過虛擬管理器直接可配置 4,支援快照、虛擬機克隆多種技術,快速部署、容災減災

傳統虛擬化部署方式的缺點:

1, 資源佔用高,需要額外的作業系統鏡像,需要佔用GB級別的記憶體以及數十GB存儲空間。 2,啟動速度慢,虛擬機啟動需要先啟動虛擬機內作業系統,然後才能啟動應用。 3,性能影響大,應用 => 虛擬機作業系統=> 物理機作業系統=> 硬體資源

Linux 虛擬化

本節簡單地講解 Docker 的實現原理,讀者可以從中了解 Linux 是如何隔離資源的、Docker 又是如何隔離的。

我們知道,作業系統是以一個進程為單位進行資源調度的,現代作業系統為進程設置了資源邊界,每個進程使用自己的記憶體區域等,進程之間不會出現記憶體混用。Linux 內核中,有 cgroups 和 namespaces 可以為進程定義邊界,使得進程彼此隔離。

Linux-Namespace

在容器中,當我們使用 top 命令或 ps 命令查看機器的進程時,可以看到進程的 Pid,每個進程都有一個 Pid,而機器的所有容器都具有一個 Pid = 1 的基礎,但是為什麼不會發生衝突?容器中的進程可以任意使用所有埠,而不同容器可以使用相同的埠,為什麼不會發生衝突?這些都是資源可以設定邊界的表現。

在 Linux 中,namespace 是 Linux 內核提供的一種資源隔離技術,可以將系統中的網路、進程環境等進行隔離,使得每個 namespace 中的系統資源不再是全局性的。目前有以下 6 種資源隔離,Docker 也基本在這 6 種資源上對容器環境進行隔離。

讀者可以稍微記憶一下這個表格,後面會使用到。

namespace 系統調用參數 隔離內容
UTS CLONE_NEWUTS 主機名和域名
IPC CLONE_NEWIPC 訊號量、消息隊列、共享記憶體
PID CLONE_NEWPID 進程編號
Network CLONE_NEWNET 網路設備、網路棧、埠
Mount CLONE_NEWNS 文件系統掛載
User CLONE_NEWUSER 用戶和用戶組

[info] 關於 Mount

namespace 的 Mount 可以實現將子目錄掛載為根目錄。

unshare

Linux 中,unshare 命令行程式可以創建一個 namespace,並且根據參數創建在 namespace 中隔離各種資源,在這裡我們可以用使用這個工具簡單地創建一個 namespace。

為了深刻理解 Linux 中的 namespace,我們可以在 Linux 中執行:

unshare --pid /bin/sh  

--pid 僅隔離進程。

這命令類似於 docker run -it {image}:{tag} /bin/sh 。當我們執行命令後,終端會進入一個 namespace 中,執行 top 命令查看進程列表。

  PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND               
    1 root      20   0  160188   8276   5488 S   0.0  0.4   9:35.58 systemd               
    2 root      20   0       0      0      0 S   0.0  0.0   0:00.08 kthreadd             
    3 root       0 -20       0      0      0 I   0.0  0.0   0:00.00 rcu_gp               
    4 root       0 -20       0      0      0 I   0.0  0.0   0:00.00 rcu_par_gp          

可以看到,進程 PID 是從 1 開始的,說明在這個 namespace 中,與主機的進程是隔離開來的。

這個命令中,只隔離了進程,因為並沒有隔離網路,因此當我們執行 netstat --tlap 命令時,這個命名空間的網路跟其它命名空間的網路是相通的。

在執行 unshare 命令前,使用 pstree 命令查看進程樹:

init─┬─2*[init───init───bash]
     ├─init───init───bash───pstree
     ├─init───init───fsnotifier-wsl
     ├─init───init───server───14*[{server}]
     └─2*[{init}]

為了方便比較,我們使用 unshare --pid top 創建一個 namespace,對比執行了 unshare 命令後:

$>  pstree -lha
init
  ├─init
  │   └─init
  │       └─bash
  │           └─sudo unshare --pid top
  │               └─top
  ├─init
  │   └─init
  │       └─bash
  │           └─pstree -lha
  ├─init
  │   └─init
  │       └─fsnotifier-wsl
  ├─init
  │   └─init
  │       └─bash
  ├─init
  │   └─init
  │       └─server --port 29687 --instance WSL-Ubuntu
  │           └─14*[{server}]
  └─2*[{init}]

而在 namespace 中,查看 top 顯示的內容,發現:

   PID USER      PR  NI    VIRT    RES    SHR S  %CPU  %MEM     TIME+ COMMAND
     1 root      20   0    1904   1136   1020 S   0.0   0.0   0:08.38 init

通過進程樹可以看到,不同 namespace 內的進程處於不同的樹支,他們的進程 PID 也是相互獨立的。其功能類似於 Docker 中的 runc。

由於筆者對 Linux 了解不深,這部分內容就不深入探究了。

在 unshare 命令中,--pid 參數創建 隔離進程的命名空間,此外,還可以隔離多種系統資源:

  • mount :命名空間具有獨立的掛載文件系統;
  • ipc:Inter-Process Communication (進程間通訊)命名空間,具有獨立的訊號量、共享記憶體等;
  • uts:命名空間具有獨立的 hostname 、domainname;
  • net:獨立的網路,例如每個 docker 容器都有一個虛擬網卡;
  • pid:獨立的進程空間,空間中的進程 Pid 都從 1 開始;
  • user:命名空間中有獨立的用戶體系,例如 Docker 中的 root 跟主機的用戶不一樣;
  • cgroup:獨立的用戶分組;

Go 簡單實現 進程隔離

在前面我們使用了 unshare 創建命名空間,在這裡我們可以嘗試使用 Go 調用 Linux 內核的 namespace,通過編程程式碼創建隔離的資源空間。

Go 程式碼示例如下:

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_NEWNS |
			syscall.CLONE_NEWNET |
			syscall.CLONE_NEWPID |
			syscall.CLONE_NEWUSER,
	}
	cmd.Stdin = os.Stdin
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	if err := cmd.Run(); err != nil {
		log.Fatalln(err)
	}
}

[info] 提示

前面已經提到過 UTS 等資源隔離,讀者可以參考表格中的說明,對照程式碼理解 Cloneflags 的作用。

程式碼示例參考 陳顯鷺《自己動手寫 Docker》一書。

在這個程式碼中,我們啟動了 Linux 中的 sh 命令,開啟一個新的進程,這個進程將會使用新的 IPC、PID 等隔離。

讀者可以在 Linux 中,執行 go run main.go ,即可進入新的命名空間。

image-20211027213531037

限於個人水平和篇幅有限,關於 namespace 的介紹就到這裡。

cgroups 硬體資源隔離

前面提到的 namepace 是邏輯形式使得進程之間相互不可見,形成環境隔離,這跟 Docker 容器的日常使用是一樣的,隔離根目錄,隔離網路,隔離進程 PID 等。

當然,Docker 處理環境隔離外,還能限制每個容器使用的物理資源,如 CPU 、記憶體等,這種硬體資源的限制是基於 Linux 內核的 cgroups 的。

在 Docker 中限制容器能夠使用的資源量參數示例:

-m 4G --memory-swap 0 --cpu-period=1000000 --cpu-quota=8000000 

cgroups 是 control groups 的縮寫,是 Linux 內核提供的一種可以進程所使用的物理資源的機制。

cgroups 可以控制多種資源,在 cgroups 中每種資源限制功能對應一個子系統,可以使用命令查看:

mount | grep cgroup

cgroups

[info] 提示

每種子系統的功能概要如下:

  • blkio — 該子系統對進出塊設備的輸入/輸出訪問設置限制,如 USB 等。
  • cpu — 該子系統使用調度程式來提供對 CPU 的 cgroup 任務訪問。
  • cpuacct — 該子系統生成有關 cgroup 中任務使用的 CPU 資源的自動報告。
  • cpuset — 該子系統將單個 CPU和記憶體節點分配給 cgroup 中的任務。
  • devices — 該子系統允許或拒絕 cgroup 中的任務訪問設備。
  • freezer — 該子系統在 cgroup 中掛起或恢復任務。
  • memory — 該子系統對 cgroup 中的任務使用的記憶體設置限制,並生成有關自動報告。
  • net_cls— 允許 Linux 流量控制器 ( tc) 識別源自特定 cgroup 任務的數據包。
  • net_prio — 該子系統提供了一種動態設置每個網路介面的網路流量優先順序的方法。
  • ns命名空間子系統。
  • perf_event — 該子系統識別任務的 cgroup 成員資格,可用於性能分析。

詳細內容請參考:redhat 文檔

我們也可以使用 lssubsys 命令,查看內核支援的子系統。

$> lssubsys -a
cpuset
cpu
cpuacct
blkio
memory
devices
freezer
net_cls
perf_event
net_prio
hugetlb
pids
rdma

[info] 提示

Ubuntu 可以使用 apt install cgroup-tools 安裝工具。

為了避免篇幅過大,讀者只需要知道 Docker 限制容器資源使用量、CPU 核數等操作,其原理是 Linux 內核中的 cgroups 即可,筆者這裡不再贅述。

聊聊虛擬化

本節內容將從底層角度,聊聊虛擬化。

理論基礎

電腦層次結構

從語言角度,一台由軟硬體組成的通用電腦系統可以看作是按功能劃分的多層機器級組成的層次結構。

如果從語言角度來看,電腦系統的層次結構可用下圖所示。

 

 

【圖來源:《電腦組成原理》天勤考研 1.2.5 電腦系統的層次結構】

我們平時使用的筆記型電腦、Android手機、平板電腦、Linux 伺服器等,雖然不同機器的系統和部分硬體差異很大,但是其系統結構是一致的。從 CPU 中電晶體、暫存器 到 CPU 指令集,再到作業系統、彙編,現在使用的通用電腦基本上這種結構。

下面講解一下不同層次的主要特點。

電腦的最底層是硬聯邏輯級,由閘電路,觸發器等邏輯電路組成,特徵是使用極小的元件構成,表示了電腦中的 0、1。

m1

微程式是使用微指令編寫的,一個微程式即一個機器指令,一般直接由硬體執行,它可以表示一個最簡單的操作。例如一個加法指令,由多個邏輯元件構成一個加法器,其元件組成如下圖所示(圖中為一個 8 位全加器)。

m1

傳統機器語言機器級是處理器的指令集所在,我們熟知的 X86、ARM、MIPS、RISC-V 等指令集,便是在這個層次。程式設計師使用指令集中的指令編寫的程式,由低一層微程式解釋。

作業系統機器層是從作業系統基本功能來看的,作業系統需要負責管理電腦中的軟硬體資源,如記憶體、設備、文件等,它是軟硬體的交互介面。常用的作業系統有 Windows、Linux、Unix 等。這個層次使用的語言是機器語言,即 0、1 組成的二進位程式碼,能夠由電腦直接識別和執行。

彙編語言機器層顧名思義是彙編語言所在的位置,彙編語言與處理器有關,相同類型的處理器使用的彙編語言集是一致的。彙編語言需要被彙編語言程式變換為等效的二進位程式碼目標程式。由於電腦中的資源被作業系統所管理,因此彙編語言需要在作業系統的控制下進行。

到了高級語言機器層,便是我們使用的 C、C++ 等程式語言,高級語言是與人類思維相接近的語言。

軟硬體實現等效

 

電腦的某些功能即可以由硬體實現,也可以由軟體來實現。即軟體和硬體在功能意義上是等效的。

一個功能使用硬體來實現還是使用軟體來實現?

硬體實現:速度快、成本高;靈活性差、佔用記憶體少。

軟體實現:速度低、複製費用低;靈活性好、佔用記憶體多。

虛擬化技術是將原本 硬體實現的功能,使用軟體來實現,它們在性能、價格、實現的難易程度是不同的。一個功能既可以使用硬體實現,也可以使用軟體實現,也可以兩者結合實現,可能要根據各種人力成本、研發難度、研發周期等考慮。

虛擬化

 

虛擬化(技術)或虛擬技術是一種資源管理技術,將電腦的各種實體資源(CPU、記憶體、磁碟空間、網路適配器等),予以抽象、轉換後呈現出來並可供分割、組合為一個或多個電腦配置環境。

不同層次的虛擬化

我們應該在很多書籍、文章中,了解到虛擬機跟 Docker 的比較,了解到 Docker 的優點,通過 Docker 打包鏡像後可以隨時在別的地方運行而不需要擔心機器的兼容問題。但是 Docker 的虛擬化並不能讓 Linux 跑 Windows 容器,也不能讓 Windows 跑 Linux 容器,更不可能讓 x86 機器跑 arm 指令集的二進位程式。但是 VMware 可以在 Windows 運行 Linux 、Mac 的鏡像,但 WMWare 也不能由 MIPS 指令構建的 Linux 系統。

Docker 和 VMware 都可以實現不同程度的虛擬化,但也不是隨心所欲的,它們虛擬化的程度相差很大,因為它們是在不同層次進行虛擬化的。

virstual

[Info] 提示

許多虛擬化軟體不單單是在一個層面上,可能具有多種層次的虛擬化能力。

在指令集級別虛擬化中,從指令系統上看,就是要在一種機器上實現另一種機器的指令系統。例如,QEMU 可以實現在 X64 機器上模擬 ARM32/64、龍芯、MIPS 等處理器。

虛擬化程度在於使用硬體實現與軟體實現的比例,硬體部分比例越多一般來說性能就會越強,軟體部分比例越多靈活性會更強,但是性能會下降,不同層次的實現也會影響性能、兼容性等。隨著現在電腦性能越來越猛,很大程度上產生了性能過剩;加之硬體研發的難度越來越高,越來越難突破,非硬體程度的虛擬化將會越來越廣泛。