Nydus 在約苗平臺的容器映象加速實踐

SOFAStack發表於2023-03-01

圖片
文 | 向申約苗平臺運維工程師 
關注雲原生領域本文字數 
9574閱讀時間24分鐘

本文是來自向申同學的分享,介紹了其在 K8s 生產環境叢集部署 Nydus 的相關實踐。

Nydus 是螞蟻集團,阿里雲和位元組等共建的開源容器映象加速專案,是 CNCF Dragonfly 的子專案,Nydus 在 OCI Image Spec 基礎上重新設計了映象格式和底層檔案系統,從而加速容器啟動速度,提高大規模叢集中的容器啟動成功率。詳情文件請參考如下地址:

Nydus 官方網站:https://nydus.dev/Nydus
Github:https://github.com/dragonflyoss/image-service

PART.1
容器映象的概念

  1. 容器映象

容器映象有一個官方的類比,"生活中常見的集裝箱",雖然擁有不同的規格,但箱子本身是不可變的(Immutable),只是其中裝的內容不同。

對於映象來說,不變的部分包含了執行一個應用軟體(如 MySQL )所需要的所有元素。開發者可以使用一些工具(如 Dockerfile)構建出自己的容器映象,簽名並上傳到網際網路上,然後需要執行這些軟體的人可以透過指定名稱(如 example.com/my-app)下載、驗證和執行這些容器。

  1. OCI 標準映象規範在

OCI 標準映象規範出臺之前,其實有兩套廣泛使用的映象規範,分別是 Appc 和 Docker v2.2,但“合久必分,分久必合”,有意思的是兩者的內容已經在各自的發展中逐步同化了,所以 OCI 組織順水推舟地在 Docker v2.2 的基礎上推出了 OCI Image Format Spec,規定了對於符合規範的映象,允許開發者只要對容器打包和簽名一次,就可以在所有的容器引擎上執行該容器。

這份規範給出了 OCI Image 的定義:
This specification defines an OCI Image, consisting of a manifest, an Image Index (optional), a set of filesystem layers, and a Configuration.

  1. 容器的工作流程
    圖片

一個典型的容器工作流程是從由 Developers 製作容器映象開始的(Build),然後上傳到映象儲存中心(Ship),最後部署在叢集中(RUN)。 

PART.2
OCI 映象格式

通常所說的映象檔案其實指的是一個包含了多個檔案的“包”,“包”中的這些檔案提供了啟動一個容器所需要的所有需要資訊,其中包括但不限於,容器所使用的檔案系統等資料檔案,映象所適用的平臺、資料完整性校驗資訊等配置檔案。當我們使用 Docker pull 或者 Nerdctl pull 從映象中心拉取映象時,其實就是在依次拉取該映象所包含的這些檔案。

Nerdctl 依次拉取了一個 Index 檔案、一個 Manifest 檔案、一個 Config 檔案和若干個 Layer 資料檔案。實際上,一個標準的 OCI 映象通常就是由這幾部分構成的。

其中,Layer 檔案一般是 tar 包或者壓縮後的 tar 包,其包含著映象具體的資料檔案。這些 Layer 檔案會共同組成一個完整的檔案系統(也就是從該映象啟動容器後,進入容器中看到的檔案系統) 。

Config 檔案是一個 JSON 檔案。其中包含映象的一些配置資訊,比如映象時間、修改記錄、環境變數、映象的啟動命令等等。

Manifest 檔案也是一個 JSON 檔案。它可以看作是映象檔案的清單,即說明了該映象包含了哪些 Layer 檔案和哪個 Config 檔案。

下面是一個 Manifest 檔案的典型例子:


"schemaVersion": 2,
  "mediaType": "application/vnd.oci.image.manifest.v1+json",
  "config": {
   "mediaType": "application/vnd.oci.image.config.v1+json",
   "digest": "sha256:0584b370e957bf9d09e10f424859a02ab0fda255103f75b3f8c7d410a4e96ed5",
   "size": 7636
 },
  "layers": [
 {
    "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
    "digest": "sha256:214ca5fb90323fe769c63a12af092f2572bf1c6b300263e09883909fc865d260",
    "size": 31379476
 },
 {
    "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
    "digest": "sha256:50836501937ff210a4ee8eedcb17b49b3b7627c5b7104397b2a6198c569d9231",
    "size": 25338790
 },
 {
    "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
    "digest": "sha256:d838e0361e8efc1fb3ec2b7aed16ba935ee9b62b6631c304256b0326c048a330",
    "size": 600
 },
 {
    "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
    "digest": "sha256:fcc7a415e354b2e1a2fcf80005278d0439a2f87556e683bb98891414339f9bee",
    "size": 893
 },
 {
    "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
    "digest": "sha256:dc73b4533047ea21262e7d35b3b2598e3d2c00b6d63426f47698fe2adac5b1d6",
    "size": 664
 },
 {
    "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
    "digest": "sha256:e8750203e98541223fb970b2b04058aae5ca11833a93b9f3df26bd835f66d223",
    "size": 1394
  }
 ]
}

Index 檔案也是一個 JSON 檔案。它是可選的,可以被認為是 Manifest 的 Manifest。試想一下,一個 tag 標識的映象,比如 Docker.io/library/nginx:1.20 ,會針對不同的架構平臺 (比如 Linux/amd、Linux/arm64 等等) 有不同的映象檔案,每個不同平臺的映象檔案都有一個 Manifest 檔案來描述,那麼我們就需要有個更高層級的檔案來索引這多個 Manifest 檔案。

比如,Docker.io/library/nginx:1.20 的 Index 檔案就包含一個 Manifests 陣列,其中記錄了多個不同平臺的 Manifest 的基本資訊:

{
 "manifests": [
 {
   "digest": "sha256:a76df3b4f1478766631c794de7ff466aca466f995fd5bb216bb9643a3dd2a6bb",
   "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
   "platform": {
     "architecture": "amd64",
     "os": "linux"
  },
   "size": 1570
 },
 {
    "digest": "sha256:f46bffd1049ef89d01841ba45bb02880addbbe6d1587726b9979dbe2f6b556a4",
    "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
    "platform": {
      "architecture": "arm",
      "os": "linux",
      "variant": "v5"
   },
   "size": 1570
 },
 {
    "digest": "sha256:d9a32c8a3049313fb16427b6e64a4a1f12b60a4a240bf4fbf9502013fcdf621c",
    "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
    "platform": {
       "architecture": "arm",
       "os": "linux",
       "variant": "v7"
   },
   "size": 1570
 },
 {
    "digest": "sha256:acd1b78ac05eedcef5f205406468616e83a6a712f76d068a45cf76803d821d0b",
    "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
    "platform": {
       "architecture": "arm64",
       "os": "linux",
       "variant": "v8"
   },
   "size": 1570
 },
 {
    "digest": "sha256:d972eee4f12250a62a8dc076560acc1903fc463ee9cb84f9762b50deed855ed6",
    "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
    "platform": {
       "architecture": "386",
       "os": "linux"
   },
   "size": 1570
 },
 {
    "digest": "sha256:b187079b65b3eff95d1ea02acbc0abed172ba8e1433190b97d0acfddd5477640",
    "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
    "platform": {
       "architecture": "mips64le",
       "os": "linux"
   },
    "size": 1570
 },
 {
    "digest": "sha256:ae93c7f72dc47dbd984348240c02484b95650b8b328464c62559ef173b64ce0d",
    "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
    "platform": {
      "architecture": "ppc64le",
      "os": "linux"
   },
    "size": 1570
 },
 {
    "digest": "sha256:51f45f5871a8d25b65cecf570c6b079995a16c7aef559261d7fd949e32d44822",
    "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
    "platform": {
       "architecture": "s390x",
       "os": "linux"
  },
   "size": 1570
  }
 ],
 "mediaType": "application/vnd.docker.distribution.manifest.list.v2+json",
 "schemaVersion": 2
}

PART.3
OCI 映象所面臨的問題
圖片

  1. 啟動容器慢

我們注意到映象層需要全部堆疊後,容器才能看到整個檔案系統檢視,所以容器需要等到映象的每一層都下載並解壓之後才能啟動。有一篇 FAST 論文研究分析[1] 說映象拉取佔了大約容器 76% 的啟動時間,但卻只有 6.4% 的資料是會被容器讀取的。這個結果很有趣,它激發了我們可以透過按需載入的方式來提高容器啟動速度。另外,在層數較多的情況下,執行時也會有 Overlay 堆疊的開銷。

一般來說容器啟動分為三個步驟:
下載映象;
解壓映象;
使用 Overlayfs 將容器可寫層和映象中的只讀層聚合起來提供容器執行環境。

  1. 較高的本地儲存成本

每層映象是由後設資料和資料組成的,那麼這就導致某層映象中只要有一個檔案後設資料發生變化,例如修改了許可權位,就會導致層的 Hash 發生變化,然後導致整個映象層需要被重新儲存,或重新下載。

  1. 存在大量相似映象

映象是以層為基本儲存單位,資料去重是透過層的 Hash,這也導致了資料去重的粒度較粗。從整個 Registry 儲存上看,映象中的層與層之間,映象與映象之間存在大量重複資料,佔用了儲存和傳輸成本。

PART.4
Nydus 映象解決方案

Nydus 映象加速框架是 Dragonfly[2](CNCF 孵化中專案)的子專案。它相容了目前的 OCI 映象構建、分發、執行時生態。Nydus 執行時由 Rust 編寫,它在語言級別的安全性以及在效能、記憶體和 CPU 的開銷方面非常有優勢,同時也兼具了安全和高可擴充套件性。
圖片

  1. Nydus 基礎架構

Nydus 主要包含一個新的映象格式,和一個負責解析容器映象的 FUSE 使用者態檔案系統程式。

圖片

  1. Nydus 工作流程
    圖片

Nydus 映象格式並沒有對 OCI 映象格式在架構上進行修改,而主要最佳化了其中的 Layer 資料層的資料結構。

Nydus 將原本統一存放在 Layer 層的檔案資料和後設資料 (檔案系統的目錄結構、檔案後設資料等) 分開,分別存放在 “Blob Layer” 和 “Bootstrap Layer” 中。並對 Blob Layer 中存放的檔案資料進行分塊 (Chunk) ,以便於懶載入 (在需要訪問某個檔案時,只需要拉取對應的 Chunk 即可,不需要拉取整個 Blob Layer) 。

同時,這些分塊資訊,包括每個 Chunk 在 Blob Layer 的位置資訊等也被存放在 Bootstrap Layer 這個後設資料層中。這樣,容器啟動時,僅需拉取 Bootstrap Layer 層,當容器具體訪問到某個檔案時,再根據 Bootstrap Layer 中的元資訊拉取對應 Blob Layer 中的對應的 Chunk 即可。

  1. Nydus 優勢
  • 容器映象按需下載,使用者不再需要下載完整映象就能啟動容器。 
  • 塊級別的映象資料去重,最大限度為使用者節省儲存資源。 
  • 映象只有最終可用的資料,不需要儲存和下載過期資料。 
  • 端到端的資料一致性校驗,為使用者提供更好的資料保護。 
  • 相容 OCI 分發標準和 artifacts 標準,開箱即可用。 
  • 支援不同的映象儲存後端,映象資料不只可以存放在映象倉庫,還可以放到 NAS 或  者類似 S3 的物件儲存上。 
  • 與 Dragonfly 的良好整合。
     
    PART.5
    Nydus 在約苗生產實際運用

約苗平臺作為國內領先的疾病預防資訊與服務平臺,以疫苗預約服務為核心,提供包括疫苗預約、疾病防控科普、“宮頸癌&乳腺癌”篩查預約等專業、全面的疾病預防資訊與服務。

截止 2023 年 2 月約苗平臺累計註冊使用者 3700 萬+ 人,覆蓋 28 個省及直轄市, 200+ 地級市,關聯全國社群公共衛生服務機構 4000+ 家,提供疫苗預約&訂閱服務  1.1  億 + 次。 

約苗業務全部基於 Kubernetes 進行微服務構建,在 Kubernetes 平臺上已經平穩執行了超過 4 年時間,並且緊隨 Kubernetes 的版本迭代及時更新。約苗的叢集規模超過 60 個 Node 節點,目前相關服務容器 POD 已經超過了 1000+,同時每天更有上萬個臨時 Cronjob 型別的 POD 進行建立和銷燬。對平臺的運維釋出的效率有較高的要求。

  1. 問題

Kubernetes 拉取映象時間非常慢,在沿用 OCI 映象時透過觀察,映象拉去時間可達 30s。

  1. 容器啟動慢

透過線上觀察,一個 POD 從建立到準備就緒需要等待 30s 甚至更多,甚至節點沒有快取,時間將會更久。

  1. 更新迭代塊

在更新迭代中,每次批次更新多個服務,迭代週期短而頻繁,在更新多個服務時映象倉庫壓力大。隨著以上問題的產生,經過多方面的調研以及相關測試,公司決定採用開源專案 Nydus 進行對當前業務最佳化。

PART.6
Nydus 部署實踐

Nydus 映象加速,可以直接對接 OCI 映象,同時 Containerd 也支援 Nydus 外掛,識別 Nydus 映象,一般在微服務場景下,使用 CICD ,我們需要在 Docker 打包映象上部署 Nydus 轉換映象的服務,映象轉換後直接會在 Harboar 倉庫生成 Nydus 的映象,我們這裡是用的 CICD 使用的 Jenkins,這裡我就直接把服務部署在 Jenkins 的物理機上。

  1. 下載相關元件

下載連結:https://github.com/dragonflyoss/image-service/releases

cd /nydus-static
sudo install -D -m 755 nydusd nydus-image nydusify nydusctl nydus-overlayfs /usr/bin
  1. OCI 映象轉換 Nydus
nydusify convert --source dockerharboar/nginx:1.2 --target dockerharboar/nginx:1.2-nydus

注意: 

  • Source 這裡表示源 Docker-Harboar 倉庫的映象,這個映象必須私有倉庫已經存在。
  • Target 這裡表示將源倉庫映象轉換為 Nydus 映象。

當使用這條命令後,映象倉庫在同一個目錄層級會生成兩份映象,一份源 OCI 映象,一份 Nydus 映象。

PART. 7
Nydus 對接

K8s 叢集K8s 叢集使用的執行時為 Containerd ,而Containerd 也支援使用外掛 Nydus Snapshotter 來識別 Nydus 映象,同時在使用 Nydus 功能時, Nydus 也是支援原生的  OCI 映象,只是沒有按需載入相關功能。

  1. K8s 叢集節點部署

Nydus官方說明:https://github.com/dragonflyoss/image-service/blob/master/docs/containerd-env-setup.md

注意:要使用 Nydus 功能,K8s 的每個 Node  節點都需要部署 Nydus Snapshotter,除開 K8s-Master 節點。

下載安裝包:
https://github.com/dragonflyoss/image-service/releases
https://github.com/containerd/nydus-snapshotter/releases

tar -xf nydus-snapshotter-v0.5.1-x86_64.tgz
tar -xf nydus-static-v2.1.4-linux-amd64.tgz
安裝相關軟體
sudo install -D -m 755  nydusd nydus-image nydusify nydusctl nydus-overlayfs /usr/bin
sudo install -D -m 755 containerd-nydus-grpc /usr/bin
建立必要目錄
mkdir -p /etc/nydus  && mkdir -p /data/nydus/cache  && mkdir -p $HOME/.docker/

建立nydus配置檔案
sudo tee /etc/nydus/nydusd-config.fusedev.json > /dev/null << EOF
{
  "device": {
    "backend": {
      "type": "registry",
      "config": {
        "scheme": "",
        "skip_verify": true,
        "timeout": 5,
        "connect_timeout": 5,
        "retry_limit": 4
      }
    },
    "cache": {
      "type": "blobcache",
      "config": {
        "work_dir": "/data/nydus/cache"
      }
    }
  },
  "mode": "direct",
  "digest_validate": false,
  "iostats_files": false,
  "enable_xattr": true,
  "fs_prefetch": {
    "enable": true,
    "threads_count": 4
  }
}
EOF
增加docker-harboar認證
sudo tee $HOME/.docker/config.json << EOF
{
  "auths": {
    "docker-harboarxxx": {
      "auth": "xxxxxx"
    }
  }
}
EOF
chmod 600 $HOME/.docker/config.json
docker-harboarxx  #私有倉庫地址
auth 裡是 base64 編碼的 user:pass

2. 啟動 Nydus

cd /data/nydus
nohup /usr/bin/containerd-nydus-grpc --config-path /etc/nydus/nydusd-config.fusedev.json --log-to-stdout &
驗證nydus是否支援
ctr -a /run/containerd/containerd.sock plugin ls | grep nydus
  1. 修改 Containerd
containerd配置檔案新增
[proxy_plugins]
  [proxy_plugins.nydus]
    type = "snapshot"
    address = "/run/containerd-nydus/containerd-nydus-grpc.sock"
[plugins."io.containerd.grpc.v1.cri".containerd]
   snapshotter = "nydus"
   disable_snapshot_annotations = false
  1. 重啟 Containerdsudo 

sudo systemctl restart containerd

PART.8
最終資料測試結果使用原生 OCI 映象

使用原生 OCI 映象

圖片

使用 Nydus 映象

圖片

POD 從 Create 到 Ready:OCI -> 20s
POD 從 Create 到 Ready:Nydus -> 13s

目前業務映象尺寸並不大,大約 200MB,使用 Nydus 已有提升效果,在使用超大映象的場景,例如 AI 計算等,Nydus 能帶來的加速效果會非常的明顯。 

PART.9
總結與未來期望

Nydus 是來自 CNCF 的優秀開源專案,更進一步說,約苗也將繼續對該專案進行更多投入,並與社群展開深入合作,使得約苗平臺變得更加強大和可持續。雲原生技術是基礎設施領域的一場革命,尤其是在彈性和無伺服器方面,我們相信 Nydus 一定會在雲原生生態中扮演重要角色。

相關連結

[1] 《Fast Distribution With Lazy Docker Containers》https://www.usenix.org/conference/fast16/technical-sessions/p...
[2] Dragonflyh
https://github.com/dragonflyoss/image-service

相關文章