K8S 1.20 棄用 Docker 評估之 Docker 和 OCI 映象格式的差別

東風微鳴發表於2023-03-02

背景

2020 年 12 月初,Kubernetes 在其最新的 Changelog 中宣佈,自 Kubernetes 1.20 之後將棄用 Docker 作為容器執行時。

棄用 Docker 帶來的,可能是一系列的改變,包括不限於:

  • 容器映象構建工具
  • 容器 CLI
  • 容器映象倉庫
  • 容器執行時

專題文章《K8S 1.20 棄用 Docker 評估》會從多方面分析由此帶來的變動和影響,今天先介紹映象格式的改變。

Docker 映象仍然可以使用嗎?

是的,可以使用。Docker 較新版本生成的映象實際上並不是特定於 Docker 的映象,而是 OCI(Open Container Initiative)映象。無論你使用什麼工具構建映象,任何符合 OCI 標準的映象在 Kubernetes 看來都是一樣的。containerd 和 CRI-O 都能夠提取這些映象並執行它們。所以您可以仍然使用 Docker 來構建容器映象,並且可以繼續在 containerd 和 CRI-O 上使用。

那為什麼發現 Docker 映象和 Containerd 映象存在不相容情況?

具體如下:在 K8S > 1.20 版本中,發現 containerd ctr 上傳到映象倉庫的映象與同版本的 docker 映象間存在以下問題

  1. 不能被 docker 使用
  2. docker push不能覆蓋

根本原因還是在於映象格式的差別。下面做詳細解釋。

Docker 和 OCI 映象格式的差別?

目前有以下幾種容器映象格式:

Docker V1 映象

{% note danger %}
嚴重警告

Docker V1 格式早已棄用,請不要再使用!!!

{% note info %}
? 引用

  1. 版本 1.8.3 增加了一個標誌(--disable-legacy-registry=false),用於阻止 docker 守護程式對 v1 映象登入檔進行拉、推和登入操作。儘管預設情況下是啟用的,但這表示不贊成 v1 協議。
  2. 自 2017 年 2 月 28 日起,隨著 Docker v1.13 的釋出,Docker Engine 不再支援 v1 協議
  3. 從 Docker 17.12 開始,對 V1 映象登入檔的支援已經被刪除,並且 --disable-legacy-registry 標誌不再使用,當設定該標誌時 dockerd 時將無法啟動。

Docker V2 映象簡介

Docker V2 映象清單(Image Manifest)是 Docker 的 V2 版本容器映像規範,它允許多架構映象並支援內容可定址映像。

從 2017 年 2 月 28 日開始,Docker V2 映象登入檔規範取代了 Docker V1 規範。Docker V1 規範已被棄用,並且 Docker V1 映像不能再用於 Container Registry。

為支援內容可定址映象並簡化映象圖層的跟蹤,Docker V2 對 Docker 映象格式進行了一系列更改。 Docker V2 映象清單包含映象圖層的所有內容地址(“摘要”),而 Docker V1 映像則不包含這些資訊。

Docker Image Manifest V2 Schema 1

下面簡單介紹下 V2 Schema 1 映象清單的格式。V2 Schema 1 它是一個臨時清單,提供與 V1 Image 格式的相容性,V2 Schema 2 才是當前 Docker 映象格式的最終格式。

{% note default %}
? 備註

V2 Schema 1 由於需要與 V1 的向後相容性原因,它比 V2.2(即 Docker Image Manifest V2 Schema 2) 更復雜。

映象清單描述了一個 Docker 映象的各種組成部分。映象清單可以序列化為 JSON 格式與以下媒體型別:

清單型別(Manifest Type) 媒體型別(Media Type)
manifest “application/vnd.docker.distribution.manifest.v1+json”
signed manifest “application/vnd.docker.distribution.manifest.v1+prettyjws”

清單格式不詳細介紹,下面透過示例說明:

示例清單格式

{
   "name": "hello-world",
   "tag": "latest",
   "architecture": "amd64",
   "fsLayers": [
      {
         "blobSum": "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef"
      },
      {
         "blobSum": "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef"
      },
      {
         "blobSum": "sha256:cc8567d70002e957612902a8e985ea129d831ebe04057d88fb644857caa45d11"
      },
      {
         "blobSum": "sha256:5f70bf18a086007016e948b04aed3b82103a36bea41755b6cddfaf10ace3c6ef"
      }
   ],
   "history": [
      {
         "v1Compatibility": "{\"id\":\"e45a5af57b00862e5ef5782a9925979a02ba2b12dff832fd0991335f4a11e5c5\",\"parent\":\"31cbccb51277105ba3ae35ce33c22b69c9e3f1002e76e4c736a2e8ebff9d7b5d\",\"created\":\"2014-12-31T22:57:59.178729048Z\",\"container\":\"27b45f8fb11795b52e9605b686159729b0d9ca92f76d40fb4f05a62e19c46b4f\",\"container_config\":{\"Hostname\":\"8ce6509d66e2\",\"Domainname\":\"\",\"User\":\"\",\"Memory\":0,\"MemorySwap\":0,\"CpuShares\":0,\"Cpuset\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\"],\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) CMD [/hello]\"],\"Image\":\"31cbccb51277105ba3ae35ce33c22b69c9e3f1002e76e4c736a2e8ebff9d7b5d\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":[],\"SecurityOpt\":null,\"Labels\":null},\"docker_version\":\"1.4.1\",\"config\":{\"Hostname\":\"8ce6509d66e2\",\"Domainname\":\"\",\"User\":\"\",\"Memory\":0,\"MemorySwap\":0,\"CpuShares\":0,\"Cpuset\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\"],\"Cmd\":[\"/hello\"],\"Image\":\"31cbccb51277105ba3ae35ce33c22b69c9e3f1002e76e4c736a2e8ebff9d7b5d\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":[],\"SecurityOpt\":null,\"Labels\":null},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":0}\n"
      },
      {
         "v1Compatibility": "{\"id\":\"e45a5af57b00862e5ef5782a9925979a02ba2b12dff832fd0991335f4a11e5c5\",\"parent\":\"31cbccb51277105ba3ae35ce33c22b69c9e3f1002e76e4c736a2e8ebff9d7b5d\",\"created\":\"2014-12-31T22:57:59.178729048Z\",\"container\":\"27b45f8fb11795b52e9605b686159729b0d9ca92f76d40fb4f05a62e19c46b4f\",\"container_config\":{\"Hostname\":\"8ce6509d66e2\",\"Domainname\":\"\",\"User\":\"\",\"Memory\":0,\"MemorySwap\":0,\"CpuShares\":0,\"Cpuset\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\"],\"Cmd\":[\"/bin/sh\",\"-c\",\"#(nop) CMD [/hello]\"],\"Image\":\"31cbccb51277105ba3ae35ce33c22b69c9e3f1002e76e4c736a2e8ebff9d7b5d\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":[],\"SecurityOpt\":null,\"Labels\":null},\"docker_version\":\"1.4.1\",\"config\":{\"Hostname\":\"8ce6509d66e2\",\"Domainname\":\"\",\"User\":\"\",\"Memory\":0,\"MemorySwap\":0,\"CpuShares\":0,\"Cpuset\":\"\",\"AttachStdin\":false,\"AttachStdout\":false,\"AttachStderr\":false,\"PortSpecs\":null,\"ExposedPorts\":null,\"Tty\":false,\"OpenStdin\":false,\"StdinOnce\":false,\"Env\":[\"PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin\"],\"Cmd\":[\"/hello\"],\"Image\":\"31cbccb51277105ba3ae35ce33c22b69c9e3f1002e76e4c736a2e8ebff9d7b5d\",\"Volumes\":null,\"WorkingDir\":\"\",\"Entrypoint\":null,\"NetworkDisabled\":false,\"MacAddress\":\"\",\"OnBuild\":[],\"SecurityOpt\":null,\"Labels\":null},\"architecture\":\"amd64\",\"os\":\"linux\",\"Size\":0}\n"
      },
   ],
   "schemaVersion": 1,
   "signatures": [
      {
         "header": {
            "jwk": {
               "crv": "P-256",
               "kid": "OD6I:6DRK:JXEJ:KBM4:255X:NSAA:MUSF:E4VM:ZI6W:CUN2:L4Z6:LSF4",
               "kty": "EC",
               "x": "3gAwX48IQ5oaYQAYSxor6rYYc_6yjuLCjtQ9LUakg4A",
               "y": "t72ge6kIA1XOjqjVoEOiPPAURltJFBMGDSQvEGVB010"
            },
            "alg": "ES256"
         },
         "signature": "XREm0L8WNn27Ga_iE_vRnTxVMhhYY0Zst_FfkKopg6gWSoTOZTuW4rK0fg_IqnKkEKlbD83tD46LKEGi5aIVFg",
         "protected": "eyJmb3JtYXRMZW5ndGgiOjY2MjgsImZvcm1hdFRhaWwiOiJDbjAiLCJ0aW1lIjoiMjAxNS0wNC0wOFQxODo1Mjo1OVoifQ"
      }
   ]
}

{% note warning %}
⚠️ 注意

隨著 Docker Image Manifest V2 Schema 2 的釋出,Docker Image Manifest V2 Schema 1 已被棄用。這可能導致尚未更新到 Docker Image Manifest V2 Schema 2 的映象存在相容性和漏洞問題。

Docker Image Manifest V2 Schema 2

然後介紹 V2 Schema 2 的格式。

V2 Schema 2 版本有兩個主要目標。第一種是允許多架構映象,透過“胖清單”引用特定於平臺版本的映象的映象清單。第二種方法是將 Docker 引擎轉向可內容定址的影像,方法是支援一個映象模型,在該模型中,可以對映象的配置進行雜湊,以生成映象的 ID。

清單列表(Manifest List)

清單列表是 Docker V2 Schema 2 和 OCI 映象的一部分。

利用清單列表,您可以使用單個摘要或標記來表示映像的多種形式。

示例清單列表(Manifest List)

{
  "schemaVersion": 2,
  "mediaType": "application/vnd.docker.distribution.manifest.list.v2+json",
  "manifests": [
    {
      "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
      "size": 7143,
      "digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f",
      "platform": {
        "architecture": "ppc64le",
        "os": "linux",
      }
    },
    {
      "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
      "size": 7682,
      "digest": "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270",
      "platform": {
        "architecture": "amd64",
        "os": "linux",
        "features": [
          "sse4"
        ]
      }
    }
  ]
}
映象清單

示例映象清單

{
    "schemaVersion": 2,
    "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
    "config": {
        "mediaType": "application/vnd.docker.container.image.v1+json",
        "size": 7023,
        "digest": "sha256:b5b2b2c507a0944348e0303114d8d93aaaa081732b86451d9bce1f432a537bc7"
    },
    "layers": [
        {
            "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
            "size": 32654,
            "digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f"
        },
        {
            "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
            "size": 16724,
            "digest": "sha256:3c3a4604a545cdc127456d94e421cd355bca5b528f4a9c1905b15da2eb4a4c6b"
        },
        {
            "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
            "size": 73109,
            "digest": "sha256:ec4b8955958665577945c89419d1af06b5f7636b4ac3da7f12184802ad867736"
        }
    ]
}

如何將 Docker V1 或 Manifest V2 Schema 1 升級到:Docker Image Manifest V2 Schema 2 ?

  1. 將老的 Docker V1 或 Manifest V2 Schema 1 使用 docker pull 下來;
  2. 然後用新版本的 Docker docker push 到映象倉庫即可

這樣做將自動將映象轉換為使用最新的映象清單規範。

或者也可以這樣升級:

  1. 可以透過更新 Dockerfile 中的 FROM 語句來重新 build 映象。如果您的映象清單過期了,那麼從 Dockerfile 中的 from 語句中提取的映象也有可能過期。

OCI 格式

OCI 格式是基於 Docker Image Manifest Version 2 Schema 2 格式的容器映象規範。該規範定義瞭如何建立 OCI Image(通常由構建系統完成),並輸出映象清單檔案系統(映象層)序列化映象配置。下面簡要拿映象清單做一個和 Docker 的對比說明,更多就不詳細展開了。

映象索引(Image Index)

映象索引(Image Index)相當於 OCI 映像中的清單列表(Manifest List)。

與清單列表一樣,映象索引清單指的是多個映象清單。映象索引對多平臺映象很有用。

示例映象索引

{
  "schemaVersion": 2,
  "manifests": [
    {
      "mediaType": "application/vnd.oci.image.manifest.v1+json",
      "size": 7143,
      "digest": "sha256:e692418e4cbaf90ca69d05a66403747baa33ee08806650b51fab815ad7fc331f",
      "platform": {
        "architecture": "ppc64le",
        "os": "linux"
      }
    },
    {
      "mediaType": "application/vnd.oci.image.manifest.v1+json",
      "size": 7682,
      "digest": "sha256:5b0bcabd1ed22e9fb1310cf6c2dec7cdef19f0ad69efa1f392e94a4333501270",
      "platform": {
        "architecture": "amd64",
        "os": "linux"
      }
    }
  ],
  "annotations": {
    "com.example.key1": "value1",
    "com.example.key2": "value2"
  }
}

映象清單

示例映象清單

{
  "schemaVersion": 2,
  "config": {
    "mediaType": "application/vnd.oci.image.config.v1+json",
    "size": 7023,
    "digest": "sha256:b5b2b2c507a0944348e0303114d8d93aaaa081732b86451d9bce1f432a537bc7"
  },
  "layers": [
    {
      "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
      "size": 32654,
      "digest": "sha256:9834876dcfb05cb167a5c24953eba58c4ac89b1adf57f28f2f9d09af107ee8f0"
    },
    {
      "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
      "size": 16724,
      "digest": "sha256:3c3a4604a545cdc127456d94e421cd355bca5b528f4a9c1905b15da2eb4a4c6b"
    },
    {
      "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
      "size": 73109,
      "digest": "sha256:ec4b8955958665577945c89419d1af06b5f7636b4ac3da7f12184802ad867736"
    }
  ],
  "annotations": {
    "com.example.key1": "value1",
    "com.example.key2": "value2"
  }
}

映象格式差別總結

首先是 Docker V1 映象,這是非常老舊且已經棄用的映象格式,是 Docker 剛出來的時候,沒有考慮多架構多平臺(如:x86 和 arm 映象),所以通常 Docker V1 映象只有 x86 平臺的映象。⚠️ 注意:請不要再使用,如果使用,請儘快升級到 Docker Image Manifest V2 Schema 2 或 OCI 格式

然後是 Docker Image Manifest V2 Schema 1,這個是過過渡形態的格式,相容 Docker V1 和 Docker Image Manifest V2 使得它更為複雜,它的作用也僅僅是為了過渡而非長期使用。⚠️ 注意:請不要再使用,如果使用,請儘快升級到 Docker Image Manifest V2 Schema 2 或 OCI 格式

最後是 Docker Image Manifest V2 Schema 2 和 OCI 格式,OCI 主要參考的就是 Docker Image Manifest V2 Schema 2 格式,二者是相容的,這也就回答了上文所說的:「Docker 映象仍然可以使用嗎?-- 是的,可以使用。」

但是二者在媒體型別(Media Type)、壓縮方式等細節上存在不同,部分舉例如:

  • 清單(manifest)上:
    • Docker Image Manifest V2 Schema 2 的 Media Type 是:"application/vnd.docker.distribution.manifest.list.v2+json"
    • 而 OCI 是:"application/vnd.oci.image.manifest.v1+json"
  • 每一層映象層上:
    • Docker Image Manifest V2 Schema 2 的 Media Type 是:"application/vnd.docker.image.rootfs.diff.tar.gzip"
    • OCI 的 Media Type 是:application/vnd.oci.image.layer.v1.tar+gzipapplication/vnd.oci.image.layer.v1.tar 等型別。

也正是因為這些差異,最終導致即使是完全相同的映象,二者的 digest、映象大小不盡相同。

所以這也正好解釋了「那為什麼發現 Docker 映象和 Containerd 映象存在不相容情況?」

  1. 「不能被 docker 使用?」 - 可能是 docker 版本過低導致的;
  2. 「docker push 不能覆蓋?」- 因為即使是完全相同的映象,二者的 digest、映象大小不盡相同。所以無法覆蓋。

我的建議

針對映象,因為 OCI 主要參考了 Docker Image Manifest V2 Schema 2,而 Docker Image Manifest V2 Schema 2 是 OCI 的一種實現。所以 Docker 較新版本生成的映象,containerd 和 CRI-O 都能夠提取這些映象並執行它們。

兩種辦法都可以:

  1. 映象構建方式保持不變,仍然是採用 Docker 構建映象(注意 Docker 版本要較新,確保構建的映象是:Docker Image Manifest V2 Schema 2 格式)
  2. 變更映象構建工具,不再使用 Docker,而是使用可以構建 OCI 格式的映象構建工具。

具體您可以視實際情況進行選擇。

三人行, 必有我師; 知識共享, 天下為公. 本文由東風微鳴技術部落格 EWhisper.cn 編寫.

相關文章