章節概覽#

本章介紹 Linux 世界中的虛擬化 (Virtualization) 技術。Virtual 一詞在計算領域中,主要指一個中介層,將複雜或碎片化的底層轉換為多個消費者可使用的簡化介面。虛擬化的典型目的是:建立隔離的環境,讓多個系統可以互不干擾地運行。

本章涵蓋三種主要的虛擬化形式:

  • Virtual Machines — 完整的硬體虛擬化,執行獨立的作業系統
  • Containers — 作業系統層級的虛擬化,共用同一個 kernel
  • Runtime-Based Virtualization — 程式語言層級的環境隔離

17.1 Virtual Machines#

Virtual machine (VM) 的概念與 virtual memory 類似,但虛擬化的是整台機器的硬體(處理器、記憶體、I/O 介面等),而非僅是記憶體。你可以在 VM 中執行完整的作業系統,包括 kernel。這種類型更精確地稱為 system virtual machine

你可以透過純軟體模擬(稱為 emulator)或盡可能利用底層硬體來建構 VM。在 Linux 中,後者因效能優勢而更常見。

17.1.1 Hypervisors#

管理一台或多台 VM 的軟體稱為 hypervisor(或 virtual machine monitor, VMM),其運作方式類似作業系統管理 process。Hypervisor 分為兩種類型:

  • Type 2 Hypervisor — 在一般作業系統(如 Linux)之上執行。例如 VirtualBox 就是 type 2 hypervisor,可以直接安裝在你的系統上。
  • Type 1 Hypervisor — 本身就像一個作業系統(尤其是 kernel),專門用來執行 VM。所有雲端運算服務(如 AWS)都是在 type 1 hypervisor(如 Xen)上建立 VM。

VM 及其作業系統稱為 guest,而執行 hypervisor 的系統稱為 host

17.1.2 Hardware in a Virtual Machine#

理論上,hypervisor 應該為 guest 提供標準的硬體介面。但純硬體模擬效率低落,因此實務上會做一些調整:

  • Paravirtualization — 繞過虛擬硬體,讓 guest 直接存取 host 資源。網路介面和 block device 最常使用此技術。例如雲端上的 /dev/xvd 裝置,就是 Xen virtual disk,使用 Linux kernel driver 直接與 hypervisor 溝通。
  • 無論機制為何,目標都是讓 guest OS 能像對待真實裝置一樣對待虛擬硬體

Virtual Machine CPU Modes#

  • x86 處理器使用 privilege rings 來區分 kernel mode 與 user mode
  • 早期 x86 VM 的 kernel 在 user mode 下執行,hypervisor 會偵測並模擬受限指令(trap and emulate)
  • 後來 Intel 和 AMD 分別推出 VT-xAMD-V 硬體輔助虛擬化,消除了 trap and emulate 的需求

17.1.3 Common Uses of Virtual Machines#

  • 測試與試驗 — 在安全的「可拋棄」環境中測試軟體或新的 distribution
  • 應用程式相容性 — 需要在不同作業系統上執行程式時
  • 伺服器與雲端服務 — 所有雲端服務都建立在 VM 技術之上,可快速建立 web server 等服務

17.1.4 Drawbacks of Virtual Machines#

  • 安裝和設定系統耗時費力,即使有 Ansible 等自動化工具
  • VM 啟動和重啟速度較慢,因為需要啟動完整的 Linux 系統
  • 每台 VM 都需要獨立維護完整的 Linux 系統(更新、安全性、sshd 等)
  • 應用程式可能與 VM 上的標準軟體產生依賴衝突
  • 將服務隔離在不同 VM 上可能浪費資源且成本高昂,尤其是使用按實例計費的雲端服務時

這些問題在小規模運作時不明顯,但隨著服務增加,時間和金錢成本會變得顯著。這正是 container 技術出現的契機。


17.2 Containers#

Container 是比 VM 更輕量的替代方案。VM 虛擬化整個作業系統,而 container 則是作業系統層級的虛擬化 (operating system-level virtualization),所有 container 共用同一個 Linux kernel。

Container 的基礎概念#

傳統的服務隔離方式包括:

  • chroot() — 改變 process 的 root 目錄,建立所謂的 chroot jail,限制 process 只能存取特定目錄
  • rlimit — kernel 的資源限制功能,限制 CPU 時間和檔案大小等

Container 就是建立在這些概念之上:改變環境並限制 process 可用的資源。Container 中的 process 具有以下特性:

  • 擁有自己的 cgroups
  • 擁有自己的 devicesfilesystem
  • 無法看到或與系統上其他 process 互動
  • 擁有自己的 network interfaces

17.2.1 Docker, Podman, and Privileges#

兩個最流行的 container 工具是 DockerLXC

  • Docker 需要一個 daemon(dockerd)在背景執行,大多數配置需要 superuser 權限
  • Podman 是 Docker 的替代方案,支援 rootless 操作,可以一般使用者身份執行
  • Podman 的命令列與 Docker 相容,可在範例中互換使用

你可能需要將自己加入 docker group 才能以一般使用者身份執行 Docker 命令。

17.2.2 A Docker Example#

建立 Image#

Image 包含 container 的 filesystem 和其他設定。建立步驟:

  1. 撰寫 Dockerfile
FROM alpine:latest
RUN apk add bash
CMD ["/bin/bash"]
  1. 建立 image:
$ docker build -t hlw_test .

Image 和 container 容易混淆。Image 是 container 的 filesystem;process 不在 image 中執行,而是在 container 中執行。修改 container 中的檔案時,並不會修改 image。

Docker build 過程中會大量使用 SHA256 digests 來追蹤各個元件。Dockerfile 中的每個 RUN 指令會在 build 時執行,而 CMD 指令則在啟動 container 時執行。

Running Docker Containers#

$ docker run -it hlw_test
  • -it 選項提供互動式終端(interactive, terminal)
  • Container 內的 shell 以 PID 1 執行,這是 Linux kernel namespaces 的作用 — process 擁有自己的一組 process ID

Overlay Filesystems#

Container 的 root filesystem 使用 overlay filesystem,這是一個 kernel 功能,透過將多個目錄以層 (layer) 的方式堆疊來建立 filesystem:

  • lowerdir — 由冒號分隔的多個目錄,代表 image build 的各步驟(從右到左堆疊)
  • upperdir — 所有對掛載 filesystem 的變更都出現在這裡
  • workdir — filesystem driver 的工作目錄,掛載時必須為空

在 rootless 模式下,Podman 使用 overlay filesystem 的 FUSE 版本。

Networking#

Docker 使用 bridge network 搭配 network namespace (netns) 來實現網路隔離:

Figure 17-1: Bridge network in Docker

  • Docker 在 host 上建立 docker0 網路介面,通常分配 172.17.0.0/16 私有網路
  • 每個 container 取得自己的 network namespace,內含 loopback(lo)介面
  • Docker 建立 virtual interface pairveth),一端在 host namespace,一端在 container namespace
  • Host 上設定 NAT 讓 container 能連接外部網路

Podman 在 rootless 模式下使用不同方式:透過 TAP 介面(tap0)搭配 slirp4netns forwarding daemon,但功能較受限(例如 container 之間無法互相連線)。

Docker Operation#

  • docker ps 顯示執行中的 container(加 -a 顯示所有,包括已退出的)
  • docker rm 移除已終止的 container
  • docker rmi 移除 image 及其不必要的 intermediate image
  • 當以相同 tag 重新標記新 image 時,舊 image 會失去 tag 但仍佔用空間

Docker 預設不顯示已退出的 container,容易累積大量已退出的 container 和舊 image,佔用大量磁碟空間。

Docker Service Process Models#

Container 中的 process 生命週期管理需要注意:

  • Container 內可以執行多個 process,但需確保 parent process 正確收割 (reap) child process
  • 對於簡單的服務,使用 docker run--init 選項,在 container 中建立一個簡單的 init process(PID 1)
  • 對於多服務場景,考慮使用 Supervisor (supervisord) 等 process management daemon

17.2.3 LXC#

LXC 是最早的 container 套件之一,Docker 早期版本就是建立在 LXC 之上。

  • LXC 更像一個 library 和工具集,需要較多手動設定(網路介面、user ID mapping 等)
  • LXC 的設計目標是在 container 內執行盡可能完整的 Linux 系統(包含 init)
  • LXC 不預設使用 overlay filesystem,但可以自行加入;基於 C API,提供更細粒度的控制
  • LXD 是 LXC 的管理套件,提供 REST API,簡化網路建立和 image 管理等工作

17.2.4 Kubernetes#

當需要在多台機器上管理大量 container 時,Kubernetes 成為主流的編排工具。Kubernetes 採用 client-server 架構:

  • Server 端負責在可用機器上執行 container
  • Client 端提供命令列工具來啟動和管理 container 組

如果不想自行架設 Kubernetes server,可以使用 Minikube 在本地 VM 上執行 Kubernetes cluster。

17.2.5 Pitfalls of Containers#

使用 container 需要注意以下問題:

  • 儲存空間浪費 — container 必須包含所有必要的共用函式庫;overlay filesystem 雖可共用基礎檔案,但 runtime data 仍會增長
  • 系統資源限制 — 仍受限於底層系統的 kernel 和 block device 能力
  • 資料儲存策略不同 — overlay filesystem 的變更在 process 結束後消失;需要獨立的資料庫和 log 服務
  • 偏向 web server — 大多數 container 工具和模型針對 web server 設計,其他類型的服務可能不太適合
  • 草率的 build 會導致問題 — 隔離環境不代表可以忽略配置品質
  • 版本控制 — 使用 latest tag 可能導致底層改變而破壞應用程式,建議指定明確的版本
  • 信任問題 — 從 Docker image repository 使用他人的 image 時,等於增加了一層額外的信任風險

Container 不能解決所有問題。無論選擇哪種方式,這些問題都會以某種形式存在。例如,如果應用程式在一般系統上啟動就很慢,在 container 中同樣會慢。


17.3 Runtime-Based Virtualization#

最後一種虛擬化形式是基於程式語言執行環境的隔離。這與 VM 和 container 不同,不是將應用程式放到不同的機器上,而是針對特定應用程式建立隔離的執行環境。

這類環境的存在原因是:同一系統上的多個應用程式可能使用相同的程式語言,造成套件衝突。例如 Python 在典型的 distribution 中有多處使用,且包含許多 add-on 套件。

Python Virtual Environment 範例#

$ python3 -m venv test-venv

這會建立一個包含 binincludelib 等目錄的新環境。啟用方式:

$ . test-venv/bin/activate
  • 啟用本質上是設定環境變數(VIRTUAL_ENV),讓 python 指向 test-venv/bin 中的版本
  • 環境內安裝的套件會放在 test-venv/lib,而非系統的 library
  • 使用 deactivate 退出虛擬環境

並非所有程式語言都像 Python 一樣支援 virtual environment,但了解這個概念有助於釐清 “virtual” 一詞在不同情境下的含義。