Podman 與 Docker 作為主流容器引擎,在容器管理和佈署方面都提供了便捷的工具。然而,它們的底層架構存在顯著差異。Podman 採用無守護行程設計,不依賴常駐後台服務,而是直接透過 fork/exec 執行容器,降低了系統資源消耗和安全風險。相較之下,Docker 則需要 dockerd 守護行程來管理容器生命週期。這也影響了兩者在容器執行時和安全性方面的特性。Podman 支援 runC 和 crun 兩種執行時,並預設使用 crun,crun 的輕量級特性使其在啟動速度和資源佔用方面更具優勢。Conmon 作為 Podman 架構中的關鍵元件,負責監控容器執行時並管理容器的輸出和日誌。此外,Podman 的 Rootless 容器功能允許非 root 使用者執行容器,進一步提升了容器安全性。兩種引擎都遵循 OCI 映象規範,確保了映象的互通性。

Podman 與 Docker 的較勁:底層執行時的選擇與 Conmon 的奧秘

在容器技術的世界裡,Podman 和 Docker 無疑是兩大巨頭。雖然它們都致力於簡化容器的管理與佈署,但在底層架構和實作上卻存在著顯著的差異。其中,容器執行時(Container Runtime)的選擇以及 Conmon 的運用,更是 Podman 獨樹一幟的關鍵。

runC 與 crun:輕量級執行時之爭

容器執行時是容器技術堆疊中至關重要的一環,它負責容器的啟動、執行和資源隔離。Podman 預設使用 runC 或 crun 作為其容器執行時。

在 2019 年,Fedora 專案大膽採用 CGroup V2 作為預設組態,然而當時 runC 尚未完全支援 CGroup V2。為了確保 Podman 在 Fedora 上的正常執行,開發團隊選擇了當時已支援 CGroup V1 和 V2 的 crun 作為預設執行時。

儘管後來 runC 也加入了對 CGroup V2 的支援,但 crun 相較於 runC 仍具備以下優勢:

  • 更小的二進位檔案:crun 的建置檔案大小約為 runC 的 1/50。
  • 更快的執行速度:在相同的執行條件下,crun 在容器啟動速度上更勝一籌。
  • 更低的記憶體佔用:crun 的記憶體消耗量不到 runC 的一半。這在處理大規模容器佈署或物聯網裝置時尤為重要。

crun 也能作為函式庫使用,並整合到其他符合 OCI 標準的專案中。雖然 crun 和 runC 都提供了 CLI,但它們主要由 Podman 或 Docker 等容器引擎來管理容器的生命週期,使用者通常無需直接操作它們。

在 Podman 中切換執行時非常簡單,只需使用 --runtime 標誌指定執行時二進位檔案的路徑即可。例如,以下指令分別使用 runC 和 crun 執行容器:

podman --runtime /usr/bin/runc run --rm fedora echo "Hello World"
podman --runtime /usr/bin/crun run --rm fedora echo "Hello World"

上述範例假設 runC 和 crun 都已安裝在系統中。值得一提的是,crun 和 runC 都支援 eBPF 和 CRIU。

eBPF 與 CRIU:容器技術的兩大利器

eBPF(Extended Berkeley Packet Filter)是一種根據核心的技術,允許使用者在 Linux 核心中執行自定義程式,無需重新編譯核心或載入額外的模組即可為系統增加額外的功能。所有 eBPF 程式都在沙箱虛擬機器中執行,其執行本質上是安全的。如今,eBPF 正受到越來越多的關注,並在網路、安全、可觀測性和追蹤等領域得到廣泛應用。

CRIU(Checkpoint Restore in Userspace)則是一種軟體工具,允許使用者凍結正在執行的容器,並將其狀態儲存到磁碟,以便後續還原。記憶體中儲存的資料結構會被轉儲並相應地還原。

Conmon:容器守護者的角色

在 Podman 的架構中,Conmon 扮演著重要的角色。那麼,Podman(容器引擎)和 runC/crun(OCI 容器執行時)是如何互動的?誰負責啟動容器執行時程式?又該如何監控容器的執行呢?

Conmon 是一個監控和通訊工具,位於容器引擎和執行時之間。每次建立新容器時,都會啟動一個新的 Conmon 例項。它會與容器管理器程式分離並以後台程式的方式執行,同時啟動容器執行時作為子程式。

以下是 Podman 容器的執行流程:

  1. 容器引擎執行 Conmon 程式,該程式會分離並以後台程式的方式執行。
  2. Conmon 程式執行一個容器執行時例項,該例項啟動容器並離開。
  3. Conmon 程式繼續執行以提供監控介面,而管理器/引擎程式已離開或分離。

Podman 容器執行流程

在一台執行著大量容器的系統上,使用者會發現許多 Conmon 程式例項,每個容器都有一個。換句話說,Conmon 充當容器的一個小型專用守護程式。

以下範例使用一個簡單的 Shell 迴圈來建立三個相同的 Nginx 容器:

[root@fedora34 ~]# for i in {1..3}; do podman run -d --rm docker.io/library/nginx; done
592f705cc31b1e47df18f71ddf922ea7e6c9e49217f00d1af8cf18c8e5557bde
4b1e44f512c86be71ad6153ef1cdcadcdfa8bcfa8574f606a0832c647739a0a2
4ef467b7d175016d3fa024d8b03ba44b761b9a75ed66b2050de3fec28232a8a7
[root@fedora34 ~]# ps aux | grep conmon
root 21974 0.0 0.1 82660 2532 ? Ssl 22:31 0:00 /usr/bin/conmon --api-version 1 -c 592f705cc31b1e47df18f71ddf922ea7e6c9e49217f00d1af8cf18c8e5557bde -u 592f705cc31b1e47df18f71ddf922ea7e6c9e49217f00d1af8cf18c8e5557bde -r /usr/bin/crun [..omitted output]
root 22089 0.0 0.1 82660 2548 ? Ssl 22:31 0:00 /usr/bin/conmon --api-version 1 -c 4b1e44f512c86be71ad6153ef1cdcadcdfa8bcfa8574f606a0832c647739a0a2 -u 4b1e44f512c86be71ad6153ef1cdcadcdfa8bcfa8574f606a0832c647739a0a2 -r /usr/bin/crun [..omitted output]
root 22198 0.0 0.1 82660 2572 ? Ssl 22:31 0:00 /usr/bin/conmon --api-version 1 -c 4ef467b7d175016d3fa024d8b03ba44b761b9a75ed66b2050de3fec28232a8a7 -u 4ef467b7d175016d3fa024d8b03ba44b761b9a75ed66b2050de3fec28232a8a7 -r /usr/bin/crun [..omitted output]

玄貓透過以上分析,我們可以更深入地瞭解 Podman 和 Docker 在容器執行時選擇和架構設計上的差異,以及 Conmon 在 Podman 架構中所扮演的關鍵角色。這些知識有助於我們在實際應用中做出更明智的選擇,並更好地利用容器技術。

玄貓解構 Podman 無 Daemon 架構:輕量化容器的幕後功臣

Podman 以其獨特的無 Daemon(daemonless)架構在容器技術領域佔有一席之地。與傳統 Docker 仰賴常駐背景服務不同,Podman 直接透過 fork/exec 執行容器,省去了 Daemon 行程,降低了資源消耗和潛在的安全風險。

當我們啟動 Podman 容器時,實際上是 Conmon 這個輕量級工具在幕後運作。透過簡單的 ps aux 指令,我們可以觀察到 Conmon 行程的身影。即使 Podman 本身已經停止執行,我們仍然可以連線到 Conmon 行程,進而存取容器。Conmon 同時也負責將容器的 Console Socket 和 Log 導向到檔案或 Systemd Journal,方便我們進行監控和除錯。

Conmon 本身以 C 語言編寫,並提供 Go 語言的繫結,讓管理工具和 Runtime 之間可以有效地傳遞組態資訊。這種輕量化的設計,使得 Podman 在資源受限的環境中也能表現出色。

Rootless 容器:解放使用者許可權,強化安全防護

Podman 最吸引人的特性之一,莫過於其對 Rootless 容器的支援。這意味著,即使沒有系統管理員許可權,使用者也能夠執行自己的容器。

Rootless 容器提供了更佳的安全隔離性。每個使用者都能夠獨立執行自己的容器例項,互不幹擾。由於 Podman 採用無 Daemon 架構,Rootless 容器的管理也變得異常簡單。使用者只需使用標準指令和引數,就能啟動 Rootless 容器,例如:

$ podman run -d --rm docker.io/library/nginx

當執行上述指令時,Podman 會建立一個新的使用者名稱空間(User Namespace),並透過 uid_map 檔案在兩個名稱空間之間對映 UID。這使得容器內的 Root 使用者,可以對映到宿主機上的一般使用者,從而降低了容器逃逸的風險。

Rootless 容器的映象資料預設儲存在使用者家目錄下的 $HOME/.local/share/containers/storage。Podman 對於 Rootless 容器的網路連線管理方式,也與傳統需要 Root 許可權的容器有所不同。

OCI 映象規範:容器世界的共通語言

Podman 和 Container/Image 套件都遵循 OCI(Open Container Initiative)映象格式規範。OCI 映象規範定義了容器映象的組成結構和儲存方式,確保了不同容器 Runtime 之間的互通性。

一個 OCI 映象主要由以下幾個要素構成:

  1. Manifest(清單): 描述了映象的組成,包括各層(Layer)的 Hash 值、組態資訊等。
  2. Image Index(映象索引): (可選) 包含了一系列 Manifest,用於支援不同的硬體架構和作業系統。
  3. Image Layout(映象佈局): 定義了映象檔案的目錄結構,以及 Manifest 和 Image Index 的位置。
  4. Filesystem Layer(檔案系統層): 映象是由多個 Layer 疊加而成,每個 Layer 都是一個 TAR 壓縮檔,包含了檔案系統的變更。
  5. Image Configuration(映象組態): 定義了映象的執行引數,例如 Entry Point、Volume、環境變數等。

讓我們更深入地瞭解這些要素。

Manifest:內容可定址的映象描述

映象 Manifest 提供了內容可定址的映象描述。它包含了映象的 Layer 和組態資訊,並針對特定的硬體架構和作業系統(例如 Linux x86_64)。

Image Index:多平台支援的索引

Image Index 是一個物件,包含了與不同硬體架構(例如 amd64、arm64、386)和作業系統相關的 Manifest 列表,以及自定義的 Annotation。

Image Layout:映象檔案的組織方式

OCI 映象佈局定義了映象 Blob 的目錄結構。它提供了必要的 Manifest 位置參考,以及 Image Index(JSON 格式)和 Image Configuration。index.json 檔案包含了指向映象 Manifest 的參考,該 Manifest 作為 Blob 儲存在 OCI 映象 Bundle 中。

Filesystem Layers:構建容器的根本

在映象內部,多個 Layer 疊加在一起,形成容器可以使用的檔案系統。Layer 被封裝成 TAR 壓縮檔(可選 gzip 和 zstd 壓縮)。檔案系統層實作了 Layer 堆積疊的邏輯,以及如何應用變更集 Layer(包含檔案變更的 Layer)。

Podman 預設使用 OverlayFS 作為 Graph Driver 來管理 Layer 堆積疊。

Image Configuration:定義執行引數

Image Configuration 定義了映象 Layer 的組成,以及對應的執行引數,例如 Entry Point、Volume、執行引數或環境變數,以及額外的映象 Metadata。

Image Configuration 的 JSON 檔案是一個不可變的物件。修改它意味著建立一個新的衍生映象。

以下是一個 OCI 映象實作的示意圖,由映象 Layer、Image Index 和 Image Configuration 組成:

(此處應插入圖片,但由於無法直接插入圖片,請參考原文 Figure 2.6 – OCI image implementation)

讓我們檢視一個來自輕量級 Alpine 映象的實際範例:

# tree alpine/
alpine/
├── blobs
│   └── sha256
│       ├── 03014f0323753134bf6399ffbe26dcd75e89c6a7429adfab392d64706649f07b
│       ├── 696d33ca1510966c426bdcc0daf05f75990d68c4eb820f615edccf7b971935e7
│       └── a0d0a0d46f8b52473982a3c466318f479767577551a53ffc9074c9fa7035982e
├── index.json
└── oci-layout

目錄佈局包含一個 index.json 檔案,內容如下:

{
  "schemaVersion": 2,
  "manifests": [
    {
      "mediaType": "application/vnd.oci.image.manifest.v1+json",
      "digest": "sha256:03014f0323753134bf6399ffbe26dcd75e89c6a7429adfab392d64706649f07b",
      "size": 348,
      "annotations": {
        "org.opencontainers.image.ref.name": "latest"
      }
    }
  ]
}

在這個範例中,index.json 檔案指向了 Alpine 映象的 Manifest,其中包含了映象的 Layer 和組態資訊。

Podman 的無 Daemon 架構和對 OCI 映象規範的支援,使其成為一個輕量、安全與易於使用的容器 Runtime。無論是開發者還是系統管理員,都能夠從中受益。

解構Podman的無守護行程架構:從映象到容器的底層探索

在容器技術的世界中,理解映象的底層結構至關重要。Podman 作為一個無守護行程的容器引擎,其映象結構與 Docker 相似,但運作方式卻有顯著差異。本文將探討 Podman 映象的組成,並比較 Podman 與 Docker 在架構上的關鍵區別。

映象結構:分層儲存與中繼資料

Podman 映象由多個層(layers)組成,每一層代表檔案系統的一個變更。這種分層結構有助於節省儲存空間,因為多個映象可以分享相同的層。

以下是一個 Alpine 映象的結構分析:

  1. 索引檔案(index.json): 索引檔案指向一個包含映象中繼資料的 manifests 陣列。

  2. 映象 Manifest: Manifest 包含了映象的 schemaVersion、config 和 layers 等資訊。

    {
      "schemaVersion": 2,
      "config": {
        "mediaType": "application/vnd.oci.image.config.v1+json",
        "digest": "sha256:696d33ca1510966c426bdcc0daf05f75990d68c4eb820f615edccf7b971935e7",
        "size": 585
      },
      "layers": [
        {
          "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
          "digest": "sha256:a0d0a0d46f8b52473982a3c466318f479767577551a53ffc9074c9fa7035982e",
          "size": 2814446
        }
      ]
    }
    
  3. 組態檔案(config.json): 包含了映象的中繼資料、環境變數和命令執行等資訊。

    {
      "created": "2021-08-27T17:19:45.758611523Z",
      "architecture": "amd64",
      "os": "linux",
      "config": {
        "Env": [
          "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
        ],
        "Cmd": [
          "/bin/sh"
        ]
      },
      "rootfs": {
        "type": "layers",
        "diff_ids": [
          "sha256:e2eb06d8af8218cfec8210147357a68b7e13f7c485b991c288c2d01dc228bb68"
        ]
      },
      "history": [
        {
          "created": "2021-08-27T17:19:45.553092363Z",
          "created_by": "/bin/sh -c #(nop) ADD file:aad4290d27580cc1a094ffaf98c3ca2fc5d699fe695dfb8e6e9fac20f1129450 in / "
        },
        {
          "created": "2021-08-27T17:19:45.758611523Z",
          "created_by": "/bin/sh -c #(nop) CMD [\"/bin/sh\"]",
          "empty_layer": true
        }
      ]
    }
    
  4. 映象層(layer.tar.gz): 實際的檔案系統層,以 TAR 壓縮檔的形式儲存。

    file alpine/blobs/sha256/a0d0a0d46f8b52473982a3c466318f479767577551a53ffc9074c9fa7035982e
    

    結果顯示該檔案是一個經過 gzip 壓縮的 TAR 檔案。

Docker vs. Podman:架構的根本差異

Docker 採用守護行程(daemon-centric)架構,而 Podman 則採用無守護行程(daemonless)架構。這意味著 Docker 需要一個持續執行的 dockerd 守護行程來管理容器,而 Podman 則不需要。玄貓認為,這種設計上的差異直接影響了它們的安全性和資源利用率。

Podman 直接以使用者身份執行容器,無需 root 許可權,從而提高了安全性。此外,Podman 使用 Conmon 來協調和監控容器執行時,而 Docker 則使用 runc。在大多數發行版中,Podman 預設使用 crun 作為容器執行時,但在某些情況下(例如 Red Hat Enterprise Linux 8),仍然使用 runc

Rootless 容器:安全性的一大步

Podman 是第一個引入 rootless 容器的容器引擎,這項功能極大地提高了容器的安全性。Rootless 容器允許在沒有 root 許可權的情況下執行容器,從而減少了潛在的安全風險。儘管 Docker 後來也加入了 rootless 容器的支援,但 Podman 在這方面仍然具有領先地位。

命令列介面:相似的體驗

為了方便 Docker 使用者遷移到 Podman,Podman 的開發團隊在設計命令列介面時,盡可能地保持與 Docker 相似。這使得 Docker 使用者可以輕鬆地適應 Podman 的使用方式。

命令 描述
podman run 建立並啟動一個新的容器。這個命令對應於 Docker 的 docker run 命令,用於根據指定的映象建立一個新的容器例項,並立即啟動它。
podman ps 列出正在執行的容器。podman ps 命令與 Docker 的 docker ps 命令功能相同,用於顯示當前正在執行的容器列表,包括容器的ID、名稱、狀態以及其他相關資訊。
podman stop 停止一個或多個正在執行的容器。這個命令對應於 Docker 的 docker stop 命令,用於安全地停止指定的容器。Podman 會向容器傳送一個停止訊號,允許容器內的行程在終止前執行清理操作。
podman images 列出本地儲存的映象。podman images 命令與 Docker 的 docker images 命令類別似,用於顯示本地儲存的所有映象列表,包括映象的倉函式庫名稱、標籤、ID 以及建立時間等資訊。
podman pull 從容器映象倉函式庫提取映象。podman pull 命令與 Docker 的 docker pull 命令功能相同,用於從指定的容器映象倉函式庫(如 Docker Hub)下載映象到本地系統。這使得使用者可以使用該映象建立和執行容器。
podman build 使用 Dockerfile 建立映象。podman build 命令與 Docker 的 docker build 命令對應,用於根據 Dockerfile 中的指令建立新的容器映象。Dockerfile 是一個包含建立映象所需的所有命令的文字檔案。
podman exec 在正在執行的容器中執行命令。podman exec 命令與 Docker 的 docker exec 命令類別似,用於在已經執行的容器內部執行指定的命令。這對於除錯、管理或執行容器內的特定任務非常有用。
podman logs 檢視容器的日誌輸出。podman logs 命令與 Docker 的 docker logs 命令功能相同,用於檢視容器的標準輸出和標準錯誤輸出。這對於監控容器的執行狀態、診斷問題以及收集應用程式日誌非常有用。
podman rm 移除一個或多個容器。podman rm 命令與 Docker 的 docker rm 命令對應,用於刪除不再需要的容器。請注意,這個命令只會移除已停止的容器。若要強制移除正在執行的容器,需要增加 -f 選項。
podman rmi 移除一個或多個映象。podman rmi 命令與 Docker 的 docker rmi 命令類別似,用於從本地儲存中刪除不再需要的映象。請注意,如果映象正在被任何容器使用,則無法直接刪除,需要先刪除使用該映象的容器。