說透 Docker:虛擬化

痴者工良發表於2021-11-18

本章內容將講解 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 計算機系統的層次結構】

我們平時使用的筆記本、安卓手機、平板電腦、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 等處理器。

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

相關文章