在 MacOS 上運行 Docker 太慢!

你是否也覺得,MacOS 中的 Docker 非常慢?本文作者想出了解決辦法,不妨來試試看。

原文連結:https://www.paolomainardi.com/posts/docker-performance-macos/

聲明:本文為 CSDN 翻譯,未經允許,禁止轉載。

作者 | PAOLO MAINARDI

譯者 | 彎月 責編 | 鄭麗媛

在撰寫本文之際,為了獲得良好的性能和開發者體驗,我的選擇包括:

  • 利用 VirtioFS 在 Docker 桌面版、Rancher 桌面版、Colima 之間共享檔案系統,但這種方法仍然存在一些問題。

  • 使用命名卷,如果你使用 VSCode,可以通過 DevContainers 等獲得良好的開發者體驗。

  • PHP 項目可以使用 DDEV + Mutagen。

  • 如果你是 VI/Emacs 使用者,只需要在容器中使用編輯器和工具即可。

如何在 MacOS 上運行 Docker?

在 macOS 和 Windows 上,運行 Docker 引擎需要 Linux 核心。這一點 Windows 和 macOS 並沒有任何特別之處,之所以我們看不到 Linux 核心,是因為有人幫你做了這一切。相反,在任何作業系統上,Docker CLI 和 docker-compose 都是原生的二進位制可執行檔案。

關於微軟,有兩件事值得一提。第一,Windows 支持以原生的方式通過 Docker 在 Windows 上運行容器。這要歸功於 2016 年微軟和 Docker 合作,共同努力創建在 Windows 上實現 Docker 規範的容器引擎。

第二,微軟曾嘗試以原生的方式支持 Linux 進程,具體方式是通過實時轉換系統調用,在 Windows 核心(WSL1)上直接運行 Linux 進程,而不需要對程序進行任何修改。

我們回到在 macOS 上運行 Docker 的話題。

Docker for Mac 是 Docker 公司的官方產品,專為在 macOS 上無縫運行 Docker 容器而設計,甚至支持 Kubernetes。下面是 Docker for Mac 的一些特性:

  • Docker for Mac 在 LinuxKit VM 中運行,最近換成了 Virtualization Framework。

  • 檔案系統共享是利用一種名叫 OSXFS 的專有技術實現的。但如果需要訪問大量檔案,這個系統就太慢了(對,我說的就是 Node 和 PHP),因此我更看好一個新的方案:VirtioFS。

  • 網路基於 VPNKit。

  • 可以運行 Kubernetes。

  • 這是一款閉源產品,根據公司的規模可能還需要付費(「擁有超過 250 名員工或年收入超過 1000 萬美元的公司」)。

在我看來,Docker for Mac 有兩個有效的開源替代方案,二者都使用了 Lima,它們也有檔案系統的常見問題,而且剛開始使用 VirtioFS:

  • Rancher 桌面版

  • Colima

所以,總結一下:

  • Docker 容器仍然是 Linux 進程,需要虛擬機器才能在其他作業系統上運行。

  • Docker-cli 和 docker-compose 是原生二進位制檔案。

  • 由於 Docker 在虛擬機器中運行,因此需要通過其他渠道來與虛擬機器共享主機檔案系統,我們可以在 OSXFS(專有且已棄用)、gRCP FUSE 或 VirtioFS 之間進行選擇。這些技術都有一定的問題,我個人最看好 VirtioFS。

  • 如果想在 Mac 上運行 Docker 容器,你可以以在 Docker for Mac(閉源)、Rancher 桌面版(開源軟體) 和 Colima(開源軟體)之間進行選擇。

什麼是 Docker 的」卷「?

什麼是 Docker 的」卷「?

下面,我們來討論另一個容易引起混淆的話題:Docker 的卷。

Docker 容器不能保存狀態,這意味著只有當容器存在,容器內的檔案才能存在,讓我們看一個例子:

➜  ~ # Run a container and keep it running for 300 seconds.➜  ~ docker run -d --name ephemeral busybox sh -c "sleep 300"d0b02322a9eef184ab00d6eee34cbd22466e7f7c1de209390eaaacaa32a48537➜  ~ # Inspect a container to find the host PID.➜  ~ docker inspect --format '{{ .State.Pid }}' d0b02322a9eef184ab00d6eee34cbd22466e7f7c1de209390eaaacaa32a48537515584➜  ~ # Find the host path of the container filesystem (in this case is a btrfs volume)➜  ~ sudo cat /proc/515584/mountinfo | grep subvolumes906 611 0:23 /@/var/lib/docker/btrfs/subvolumes/b237a173b0ba81eb0a60d35b59b0cc5ed[truncated]➜  ~ # Read the container filesystem from the host.➜  ~ sudo ls -ltr /var/lib/docker/btrfs/subvolumes/b237a173b0ba81eb0a60d35b59b0cc5ed7247423bed4b4dcbb5af57a1c3318ebtotal 4lrwxrwxrwx 1 root   root      3 Nov 17 21:00 lib64 -> libdrwxr-xr-x 1 root   root    270 Nov 17 21:00 libdrwxr-xr-x 1 root   root   4726 Nov 17 21:00 bindrwxrwxrwt 1 root   root      0 Dec  5 22:00 tmpdrwx------ 1 root   root      0 Dec  5 22:00 rootdrwxr-xr-x 1 root   root     16 Dec  5 22:00 vardrwxr-xr-x 1 root   root      8 Dec  5 22:00 usrdrwxr-xr-x 1 nobody nobody    0 Dec  5 22:00 homedrwxr-xr-x 1 root   root      0 Dec 10 18:37 procdrwxr-xr-x 1 root   root      0 Dec 10 18:37 sysdrwxr-xr-x 1 root   root    148 Dec 10 18:37 etcdrwxr-xr-x 1 root   root     26 Dec 10 18:37 dev➜  ~ # Now write something from the container and see if it's reflected on the host filesystem.➜  ~ docker ps -lCONTAINER ID   IMAGE     COMMAND               CREATED         STATUS         PORTS     NAMESd0b02322a9ee   busybox   "sh -c 'sleep 300'"   4 minutes ago   Up 4 minutes             ephemeral➜  ~ docker exec -it d0b02322a9ee touch hello-from-container➜  ~ sudo ls -ltr /var/lib/docker/btrfs/subvolumes/b237a173b0ba81eb0a60d35b59b0cc5ed7247423bed4b4dcbb5af57a1c3318eb | grep hello-rw-r--r-- 1 root   root      0 Dec 10 18:42 hello-from-container➜  ~ # Now write something from the host to see reflected on the container.➜  ~ sudo touch /var/lib/docker/btrfs/subvolumes/b237a173b0ba81eb0a60d35b59b0cc5ed7247423bed4b4dcbb5af57a1c3318eb/hello-from-the-host➜  ~ docker exec -it d0b02322a9ee sh -c "ls  | grep hello-from-the-host"hello-from-the-host➜  ~ # Stop the container.➜  ~ docker stop d0b02322a9ee➜  ~ sudo ls -ltr /var/lib/docker/btrfs/subvolumes/b237a173b0ba81eb0a60d35b59b0cc5ed7247423bed4b4dcbb5af57a1c3318ebtotal 4lrwxrwxrwx 1 root   root      3 Nov 17 21:00 lib64 -> libdrwxr-xr-x 1 root   root    270 Nov 17 21:00 libdrwxr-xr-x 1 root   root   4726 Nov 17 21:00 bindrwxrwxrwt 1 root   root      0 Dec  5 22:00 tmpdrwx------ 1 root   root      0 Dec  5 22:00 rootdrwxr-xr-x 1 root   root     16 Dec  5 22:00 vardrwxr-xr-x 1 root   root      8 Dec  5 22:00 usrdrwxr-xr-x 1 nobody nobody    0 Dec  5 22:00 homedrwxr-xr-x 1 root   root      0 Dec 10 18:44 sysdrwxr-xr-x 1 root   root      0 Dec 10 18:44 procdrwxr-xr-x 1 root   root    148 Dec 10 18:44 etcdrwxr-xr-x 1 root   root     26 Dec 10 18:44 dev-rw-r--r-- 1 root   root      0 Dec 10 18:45 hello-from-the-host➜  ~ # Filesystem still exists until it is just stopped, but now let's remove it.➜  ~ docker rm ephemeralephemeral➜  ~ sudo ls -ltr /var/lib/docker/btrfs/subvolumes/b237a173b0ba81eb0a60d35b59b0cc5ed7247423bed4b4dcbb5af57a1c3318ebls: cannot access '/var/lib/docker/btrfs/subvolumes/b237a173b0ba81eb0a60d35b59b0cc5ed7247423bed4b4dcbb5af57a1c3318eb': No such file or directory

以上資訊說明:

1. 容器在運行和停止狀態下,宿主會為其分配一個檔案系統。

2. 刪除容器,關聯的檔案系統也會被刪除。

現在我們對 Docker 管理檔案系統的方式有了清晰了解,很容易看出如果沒有合適的資料持久層,使用容器就會有非常大的風險,這就是我們使用卷的原因。

綁定掛載

綁定掛載

正如Docker文件的記載:「Docker 從早期就開始支持綁定掛載。與卷相比,綁定掛載的功能有限。我們可以通過綁定掛載,將宿主上的檔案或目錄掛載到容器。綁定掛需要指定宿主上的絕對路徑。相比之下,使用卷會在宿主的 Docker 儲存目錄中創建一個新目錄,並由 Docker 管理目錄。」

如果想在容器內綁定掛載主機的目錄,只需運行以下命令:

docker run -v : 

下面,我們來看看綁定掛載實際的運行情況:

相信你已經看到問題了:綁定掛載允許在容器內掛載宿主的檔案系統,你可以想象在使用者和容器之間有一個虛擬機器,這會讓一切都變得更復雜。

綁定掛載的工作方式大致如下:

綁定掛載的工作方式大致如下

當你從 Mac 掛載某個路徑時,其實是在要求 Linux 虛擬機器掛載 Mac 上的網路共享檔案系統的路徑。

通常,你不需要手動設置這個繁瑣的配置,工具可以幫你自動完成。

你可以按照如下方式查看掛載到 Docker 桌面版上的檔案或目錄:

Preferences -> Resources -> File sharing

如上圖所示,我們將本地路徑 /Users 等掛載到了 Linux 的虛擬機器上,因此,我們也可以像本地目錄一樣掛載 Mac 目錄:

➜  ~ docker run --rm -it -v /Users/paolomainardi:$(pwd) alpine ash2/ # ls -ltr3total 604drwxr-xr-x   12 root     root          4096 May 23  2022 var5drwxr-xr-x    7 root     root          4096 May 23  2022 usr6drwxrwxrwt    2 root     root          4096 May 23  2022 tmp7drwxr-xr-x    2 root     root          4096 May 23  2022 srv8drwxr-xr-x    2 root     root          4096 May 23  2022 run9drwxr-xr-x    2 root     root          4096 May 23  2022 opt10drwxr-xr-x    2 root     root          4096 May 23  2022 mnt11drwxr-xr-x    5 root     root          4096 May 23  2022 media12drwxr-xr-x    7 root     root          4096 May 23  2022 lib13drwxr-xr-x    2 root     root          4096 May 23  2022 home14drwxr-xr-x    2 root     root          4096 May 23  2022 sbin15drwxr-xr-x    2 root     root          4096 May 23  2022 bin16dr-xr-xr-x   13 root     root             0 Dec 11 14:33 sys17dr-xr-xr-x  260 root     root             0 Dec 11 14:33 proc18drwxr-xr-x    1 root     root          4096 Dec 11 14:33 etc19drwxr-xr-x    5 root     root           360 Dec 11 14:33 dev20drwxr-xr-x    3 root     root          4096 Dec 11 14:33 Users21drwx------    1 root     root          4096 Dec 11 14:33 root22/ # ls -ltr /Users/paolomainardi/23total 424drwxr-xr-x    4 root     root           128 Dec 18  2021 Public25drwx------    4 root     root           128 Dec 18  2021 Music26drwx------    5 root     root           160 Dec 18  2021 Pictures27drwx------    4 root     root           128 Dec 18  2021 Movies28drwxr-xr-x    4 root     root           128 Dec 18  2021 bin29drwxr-xr-x    4 root     root           128 Dec 31  2021 go30-rw-r--r--    1 root     root            98 Apr  1  2022 README.md31drwxr-xr-x    5 root     root           160 Apr  3  2022 webapps32drwx------    6 root     root           192 Nov 12 16:50 Applications33drwxr-xr-x    3 root     root            96 Nov 28 21:23 Sites34drwxr-xr-x   14 root     root           448 Nov 28 22:22 temp35drwx------  105 root     root

正如 Docker 文件記載:

卷是持久化 Docker 容器生成和使用的資料的首選機制。雖然綁定掛載主要依賴宿主的目錄結構和作業系統,但卷完全由 Docker 管理。與綁定掛載相比,卷有幾個優點。

卷的幾個優點:

  • 卷比綁定掛載更容易備份或遷移。

  • 你可以使用 Docker CLI 命令或 Docker API 管理卷。

  • 卷可用於 Linux 和 Windows 容器。

  • 卷可以更安全地在多個容器之間共享。

  • 卷驅動程序允許你將卷儲存在遠端主機或雲提供商上,以加密卷的內容或添加其他功能。

  • 新卷的內容可以由容器預先填充。

  • Docker 桌面版上的卷比 Mac 和 Windows 主機上的綁定掛載具有更高的性能。

如上所述,卷相對於綁定掛的優勢是顯而易見,尤其是最後一點,但其最大的缺點是開發者體驗。卷的設計初衷本來就不是滿足這個需求,因此使用它們進行程式設計的難度比較大。

下面,我們來演示一下如何使用卷,我將依次展示:

1.在 localhost 上啟動並運行一個用 node 實現的 API;

2.在宿主上完成整個開發過程;

3.創建一個 Docker 卷;

4.使用 Docker 卷創建容器;

5.在具有卷的容器內實施開發工作流程。

你可能已經注意到了,卷最大的缺點是容器外所做的任何更改都必須使用 docker cp 複製到容器內。

雖然在容器中安裝 VI 可以緩解這個問題,但是這樣就無法訪問我自己的配置檔案(以點開頭的檔案),也沒辦法使用我喜歡的 VSCode。

卷與綁定掛載

通過上述討論,我們可以得出兩個結論:

1.綁定掛載是在容器內移動檔案時最自然的方式,但當 Docker 引擎在虛擬機器中運行時,它們會遇到檔案系統共享引發的嚴重的性能問題。

2.卷的性能遠超綁定掛載,但開發的難度較大(例如本地編輯器無法看到容器外的檔案)。

比較基準

為了演示卷與綁定掛載對性能的影響有多大,我創建了一個簡單的測試套件:在一個空項目(create-react-app)上運行一些 npm install 測試。

環境:

  • 上層:macOS – Docker 桌面版 – 啟用 virtioFS

  • 底層:Linux

測試(npm 安裝):

1.主機上的原生節點;

2.沒有綁定掛載或卷的 Docker;

3.採用綁定掛載的 Docker,但只有 node_modules 採用卷;

4.採用綁定掛載的 Docker。

4.採用綁定掛載的 Docker

我們可以看到性能影響的結果:當僅使用綁定掛載時,macOS 的速度會降低 3.5 倍左右(使用 gRPC Fuse 時會降低 10 倍),「罪魁禍首」是 node_modules 目錄(只是一個什麼都沒做的 React 應用就有 3.7 萬個檔案,佔據了 328M)。將此目錄(node_modules)移動到卷中,消耗的時間就與 Linux 不相上下了,約為 7.65 秒。

上面,我們以 Node 為例,但其他開發生態系統受到的影響大致相同,而對於包含數十萬個小檔案的目錄來說,性能幾乎是災難。

解決問題

解決問題

以上,我們對各個元件及其工作方式做了簡單地介紹,接下來的問題是:為了在 Mac 上使用 Docker 時獲得良好性能和開發者體驗,推薦方式是什麼?

目前來看,答案就是使用 Docker Desktop for Mac 加上 VirtioFS,這樣可以在性能和開發者體驗之間的一個很好的折衷,雖然最終呈現的速度比 Linux 慢,但在大多數情況下,二者的差異可以忽略不計。

在使用 Node 或 PHP 開發新項目時,你不會每次都從頭重新安裝 node_modules 或 vendor,這種情況很少見,如果真的遇到這種情況,就需要花費更多的時間。

不過需要記住以下幾點:

  • 這是一款閉源產品;

  • 你需要使用許可,具體取決於使用情況;

  • 由於這是一項非常新的技術,因此仍然存在許多問題。

那麼,我們究竟有哪些選擇呢?

綁定掛載 + 卷

正如下面的結果所示,這是獲得接近原生性能的最佳方法,但會給開發者體驗帶來嚴重的問題。

讓我們看一個例子:

我們無法從主機上看到容器正在寫入的檔案,這是一個很大的問題,因為它會影響我們編寫軟體的方式以及 IDE 理解我們處理程式碼庫的方式。如何解決這個問題?具體視個人的開發習慣而定。

終端編輯器的使用者

你只需要一個容器,其中包含你所需要的編輯器和工具,然後掛載檔案和程式碼,就可以享受到近乎原生的體驗。

> docker run --rm -v $HOME/.vimrc:/home/node/.vimrc -v $PWD:/usr/src/app node:lts bash

GUI 編輯器/IDE 使用者

這類使用者的情況將更加複雜。桌面應用程序都綁定到了各自的作業系統,通常它們無法理解很多底層抽象,比如 Docker 容器中的檔案系統。

由於此時的問題是我們無法從 Docker 卷讀取檔案或向從 Docker 卷寫入檔案,因此我們需要一個能夠進行這種雙向同步的工具,最常見的解決方案之一是 Mutagen。

你可以利用 Mutagen 實現宿主與容器之間的雙向同步檔案。剛開始使用工具並不太容易,因為它包括一個守護進程、一個 CLI 工具和一些配置檔案。它本來支持 Composer,後來該功能成了Docker 桌面版的一個實驗特性,後被棄用,又被 Mutagen 的作者吸收成為 Docker 桌面版的擴展。最後值得一提的是,它也得到了 DDEV 項目的支持和集成。

但是,如果我們不想安裝額外的軟體該怎麼辦?能不能將 GUI 編輯器作為 Docker 容器運行,以便訪問檔案嗎?在 Linux 上運行 GUI 編輯器很容易,但我們沒有現成的解決方案在 macOS 上運行 Linux GUI 應用程序, 除非你安裝 XQuartz,並使用一些小技巧。

好訊息是,微軟為 Windows 實現了一個 Wayland 合成器,能夠運行原生 Linux GUI 應用程序:WSLg。然而,這並不是使用編輯器的常見方式,而且功能非常有限,與作業系統的集成度也很低。

最好的解決方案是在本地系統上運行 IDE,而且還能夠使用 Docker 容器,魚與熊掌兼得。那麼,我們需要編寫這樣的工具嗎?答案是不需要,因為我們已有現成的工具:開發容器(https://containers.dev/)。

開發容器

開發容器(Development Container)是一個開放規範,它為容器添加了一些專用於開發的內容和設置。

開發容器允許你將容器作為功能齊全的開發環境。你可以利用它運行應用程序,並與程式碼庫所需的工具、庫或運行時相分離,而且還能輔助持續集成和測試。開發容器可以在私有云或公共雲中運行,既可以在本地運行,也可以通過遠端運行。

這個項目是微軟創建的,規範在 CC4 許可下發布。完整的「支持工具」列表,請參見這裡(https://github.com/devcontainers/spec/blob/main/docs/specs/supporting-tools.md)。可以看到,幾乎所有的微軟編輯工具和雲服務都得到了支持,當然也包括 Visual Studio Code 和 GitHub Codespaces。

然而,這個項目也有一些競爭對手:

  • Gitpod

  • GCP 雲工作站

  • AWS Cloud9

  • Docker 開發環境

如何使用開發容器

你需要在項目的根目錄下創建一個檔案 .devcontainer/devcontainer.json,供 VsCode 使用。

總的來說,這個項目的整體思路非常好,並且由於它是一個開放標準,因此可以很容易地被其他供應商採用,例如:

  • Gitpod

  • Jetbrains

  • nVIM

總結要點:

  • 一切都是基於容器的。

  • 我們可以自定義 VSCode 擴展,定義一個可在團隊成員之間共享的開發環境。

  • 通過開發容器,在容器內使用 Docker 和 Docker-compose。

  • SSH 開箱即用(必須啟動並運行 ssh-agent)。

  • 在本地或雲中重用完全相同的配置。

總結

總結

文字的主要目的是討論如何提高在 macOS 上運行 Docker 的性能,但我覺得首先我們需要充分理解一些基本概念,因為這些基本概念是理解和充分利用系統的基礎。

有人可能想問,為什麼不直接使用 Linux?

日常工作我確實使用 Linux,我認為 Linux 可以作為最佳開發環境。但我們必須考慮硬體生態系統,特別是蘋果晶片已上市,它們在性能、電池壽命和服務方面都有非常出色的表現。

因此,我想魚與熊掌兼得。如今的機器足夠強大,可以無縫運行它們。還有一些創造性的組合方式,比如將 macOS 和 NixOS 組合在一起。我們可以使用自己喜歡的硬體,並尋找最適合自己的工作流程。

相關文章

Docker 映象詳細講解

Docker 映象詳細講解

作者 | 微楓Micromaple 來源 | CSDN部落格 前言 大家好,本文是對 Docker 映象的詳細講解,講解了如何安裝 Dock...

Docker 如何安全地進入到容器內部

Docker 如何安全地進入到容器內部

作者 | 飛向星的客機 來源 | CSDN部落格 🌟 前言 映象是構建容器的藍圖,Docker 以映象為模板,構建出容器。 容器在映象的基礎...

40 張圖 詳解 Docker 容器監控

40 張圖 詳解 Docker 容器監控

作者 | 飛向星的客機 來源 | CSDN部落格 前言 在企業中,通常業務是不允許隨意停止的,否則將給企業帶來巨大的經濟損失。 運維工程師要...

手工模擬實現 Docker 容器網路

手工模擬實現 Docker 容器網路

如今伺服器虛擬化技術已經發展到了深水區。現在業界已經有很多公司都遷移到容器上了。我們的開發寫出來的程式碼大概率是要運行在容器上的。因此深刻理...