為什麼在 Apple Silicon 上裝 Docker 這麼難

LeanCloud發表於2022-02-25

圖為內部 Wiki,我們嘗試過各種不同的 Docker 開發環境

最近公司的很多同事都換上了搭載 M1 Pro 或 M1 Max 的新款 MacBook Pro,雖然日常使用的軟體如 Chrome、Visual Studio Code 和 Slack 都已經適配得很好了,但面對 Docker 卻犯了難。

眾所周知,Docker 用到了 Linux 的兩項特性:namespaces 和 cgroups 來提供隔離與資源限制,因此無論如何在 macOS 上我們都必須通過一個虛擬機器來使用 Docker。

在 2021 年 4 月時,Docker for Mac(Docker Desktop)釋出了 對 Apple Silicon 的實驗性支援,它會使用 QEMU 執行一個 ARM 架構的 Linux 虛擬機器,預設執行 ARM 架構的映象,但也支援執行 x86 的映象。

QEMU 是一個開源的虛擬機器(Virtualizer)和模擬器(Emulator),所謂模擬器是說 QEMU 可以在沒有來自硬體或作業系統的虛擬化支援的情況下,去模擬執行一臺計算機,包括模擬與宿主機不同的 CPU 架構,例如在 Apple Silicon 上模擬 x86 架構的計算機。而在有硬體虛擬化支援的情況下,QEMU 也可以使用宿主機的 CPU 來直接執行,減少模擬執行的效能開銷,例如使用 macOS 提供的 Hypervisor.Framework

Docker for Mac 其實就是分別用到了 QEMU 的這兩種能力來在 ARM 虛擬機器上執行 x86 映象,和在 Mac 上執行 ARM 虛擬機器。

Docker for Mac 確實很好,除了解決新架構帶來的問題之外它還對檔案系統和網路進行了對映,容器可以像執行在本機上一樣訪問檔案系統或暴露網路埠到本機,幾乎感覺不到虛擬機器的存在。但 LeanCloud 加入 TapTap 之後已經不是小公司了,按照 Docker Desktop 在 2021 年 8 月推出的 新版價格方案,我們每個人需要支付至少 $5 每月的訂閱費用。倒不是我們不願意付這個錢,只是我想要找一找開源的方案。

之前在 Intel Mac 上,我們會用 Vagrant 或 minikube 來建立虛擬機器,它們底層會使用 VirtualBox 或 HyperKit 來完成實際的虛擬化。但 VirtualBox 和 HyperKit 都沒有支援 Apple Silicon 的計劃。實際上目前開源的虛擬化方案中只有 QEMU 對 Apple Silicon 有比較好的支援,QEMU 本身只提供命令列的介面,例如 Docker for Mac 呼叫 QEMU 時的命令列引數是這樣:

/Applications/Docker.app/Contents/MacOS/qemu-system-aarch64 -accel hvf \
-cpu host -machine virt,highmem=off -m 2048 -smp 5 \
-kernel /Applications/Docker.app/Contents/Resources/linuxkit/kernel \
-append linuxkit.unified_cgroup_hierarchy=1 page_poison=1 vsyscall=emulate \
panic=1 nospec_store_bypass_disable noibrs noibpb no_stf_barrier mitigations=off \
vpnkit.connect=tcp+bootstrap+client://192.168.65.2:61473/f1c4db329a4a520d73a79eaa1360de7be7d09948a1ac348b04c8e01f6f6eb2c9 \
console=ttyAMA0 -initrd /Applications/Docker.app/Contents/Resources/linuxkit/initrd.img \
-serial pipe:/var/folders/12/_bbrd4692hv8r9bx_ggw5kp80000gn/T/qemu-console1367481183/fifo \
-drive if=none,file=/Users/ziting/Library/Containers/com.docker.docker/Data/vms/0/data/Docker.raw,format=raw,id=hd0 \
-device virtio-blk-pci,drive=hd0,serial=dummyserial -netdev socket,id=net1,fd=3 -device virtio-net-device,netdev=net1,mac=02:50:00:00:00:01 \
-vga none -nographic -monitor none

為了實際使用 QEMU 進行開發,我們需要一個使用上更友好的封裝,能夠自動配置好 Docker 和 Kubernetes(或者至少方便編寫像 Vagrantfile 一樣的指令碼),提供類似 Docker for Mac 的網路對映和檔案對映,於是我找到了 Lima。

Lima 自稱是 macOS 上的 Linux 子系統(macOS subsystem for Linux),它使用 QEMU 執行了一個 Linux 虛擬機器,其中安裝有 rootless 模式的 containerd,還通過 SSH 提供了檔案對映和自動的埠轉發。

但為什麼是 containerd 而不是 Docker 呢?隨著容器編排平臺 Kubernetes 如日中天,社群希望將執行容器這個關鍵環節進行標準化,讓引入 Docker 之外的其他容器執行時更加容易,於是 推出了 Container Runtime Interface (CRI)。containerd 就是從 Docker 中拆分出的一個 CRI 的實現,相比於 Docker 本體更加精簡,現在也交由社群維護。

因此如 Lima 這樣新的的開源軟體會更偏好選擇 containerd 來執行容器,因為元件更加精簡會有更好的效能,也不容易受到 Docker 產品層面變化的影響。nerdctl 是與 containerd 配套的命令列客戶端(nerdcontainerd 的末尾 4 個字母),用法與 docker 或 docker-compose 相似(但並不完全相容)。

所謂 rootless 則是指通過替換一些元件,讓容器執行時(containerd)和容器都執行在非 root 使用者下,每個使用者都有自己的 containerd,這樣絕大部分操作都不需要切換到 root 來進行,也可以減少安全漏洞的攻擊面。

但我們希望能在本地執行完整的 rootful 模式的 dockerd 和 Kubernetes 來儘可能地模擬真實的線上環境,好在 Lima 提供了豐富的 自定義能力,我基於社群中的一些指令碼(docker.yamlminikube.yaml)實現了我們的需求,而且這些自定義的邏輯都被以指令碼的形式寫到了 yaml 描述檔案中,只需一條命令就可以建立出相同的虛擬機器。

~ ❯ limactl start docker.yaml
? Creating an instance "docker" Proceed with the default configuration
INFO[0005] Attempting to download the image from "https://cloud-images.ubuntu.com/focal/current/focal-server-cloudimg-arm64.img"
INFO[0005] Using cache "/Users/ziting/Library/Caches/lima/download/by-url-sha256/ae20df823d41d1dd300f8866889804ab25fb8689c1a68da6b13dd60a8c5c9e35/data"
INFO[0006] [hostagent] Starting QEMU (hint: to watch the boot progress, see "/Users/ziting/.lima/docker/serial.log")
INFO[0006] SSH Local Port: 55942
INFO[0006] [hostagent] Waiting for the essential requirement 1 of 5: "ssh"
INFO[0039] [hostagent] Waiting for the essential requirement 2 of 5: "user session is ready for ssh"
INFO[0039] [hostagent] Waiting for the essential requirement 3 of 5: "sshfs binary to be installed"
INFO[0048] [hostagent] Waiting for the essential requirement 4 of 5: "/etc/fuse.conf to contain \"user_allow_other\""
INFO[0051] [hostagent] Waiting for the essential requirement 5 of 5: "the guest agent to be running"
INFO[0051] [hostagent] Mounting "/Users/ziting"
INFO[0051] [hostagent] Mounting "/tmp/lima"
INFO[0052] [hostagent] Forwarding "/run/lima-guestagent.sock" (guest) to "/Users/ziting/.lima/docker/ga.sock" (host)
INFO[0092] [hostagent] Waiting for the optional requirement 1 of 1: "user probe 1/1"
INFO[0154] [hostagent] Forwarding TCP from [::]:2376 to 127.0.0.1:2376
INFO[0304] [hostagent] Forwarding TCP from [::]:8443 to 127.0.0.1:8443
INFO[0332] [hostagent] Waiting for the final requirement 1 of 1: "boot scripts must have finished"
INFO[0351] READY. Run `limactl shell docker` to open the shell.
INFO[0351] To run `docker` on the host (assumes docker-cli is installed):
INFO[0351] $ export DOCKER_HOST=tcp://127.0.0.1:2376
INFO[0351] To run `kubectl` on the host (assumes kubernetes-cli is installed):
INFO[0351] $ mkdir -p .kube && limactl cp minikube:.kube/config .kube/config

我還發現了另外一個基於 Lima 的封裝 —— Colima,預設提供 rootful 的 dockerd 和 Kubernetes,但 Colima 並沒有對外暴露 Lima 強大的自定義能力,因此我們沒有使用,但對於沒那麼多要求的開發者來說,也是一個更易用的選擇。

在預設的情況下,Lima 中的 Docker 在 Apple Silicon 上只能執行 ARM 架構的映象,但就像前面提到的那樣,我們可以使用 QEMU 的模擬執行的能力來執行其他架構(如 x86)的容器。qemu-user-static 是一個程式級別的模擬器,可以像一個直譯器一樣執行其他架構的可執行檔案,我們可以利用 Linux 的一項 Binfmt_misc中文版)的特性讓 Linux 遇到特定架構的可執行檔案時自動呼叫 qemu-user-static,這種能力同樣適用於容器中的可執行檔案。

社群中也有 qus 這樣的專案,對這些能力進行了封裝,只需執行一行 docker run --rm --privileged aptman/qus -s -- -p x86_64 就可以讓你的 ARM 虛擬機器魔法般地支援執行 x86 的映象。

/usr/bin/containerd-shim-runc-v2
 \_ /qus/bin/qemu-x86_64-static /usr/sbin/nginx -g daemon off;
     \_ /qus/bin/qemu-x86_64-static /usr/sbin/nginx -g daemon off;
     \_ /qus/bin/qemu-x86_64-static /usr/sbin/nginx -g daemon off;
     \_ /qus/bin/qemu-x86_64-static /usr/sbin/nginx -g daemon off;
     \_ /qus/bin/qemu-x86_64-static /usr/sbin/nginx -g daemon off;
使用 qus 執行 x86 映象的程式樹如上,所有程式(包括建立出的子程式)都自動通過 QEMU 模擬執行。

回到題目中的問題,因為 Docker 依賴於 Linux 核心的特性,所以在 Mac 上必須通過虛擬機器來執行;Apple Silicon 作為新的架構,虛擬機器的選擇比較受限,因為有些映象並不提供 ARM 架構的映象,所以有時還有模擬執行 x86 映象的需求;Docker Desktop 作為商業產品,有足夠的精力來去解決這些「髒活累活」,但它在這個時間點選擇不再允許所有人免費使用;開源社群中新的專案都希望去 Docker 化,用 containerd 取代 dockerd,但這又帶來了使用習慣的變化並且可能與線上環境不一致。因為這些原因,目前在 Apple Silicon 上安裝 Docker 還是需要花一些時間去了解背景知識的,但好在依然有這些優秀的開源專案可供選擇。

雖然 雲引擎 也是基於 Docker 等容器技術構建的,但云引擎力圖為使用者提供開箱即用的使用體驗而不必自己配置容器環境、編寫構建指令碼、收集日誌和統計資料。如果想得到容器化帶來的平滑部署、快速回滾、自動擴容等好處但又不想花時間配置,不如來試試雲引擎。

其他參考資料:

Photo by Sigmund on Unsplash.

相關文章