天天看點

Docker Registry V1 與 V2 的差別解析以及靈雀雲的實時同步遷移實踐

今年四月份,随着 Docker registry 2.0 把版本的釋出,之前 Python 版本實作的 registry 正式被标記為 ‘deprecated’。V2 版本用 go 實作,在安全性和性能上做了諸多優化,并重新設計了鏡像的存儲的格式,帶來了翻天覆地的變化。本文詳細解讀了 V2 和 V1 的差別并介紹靈雀雲在生産環境同時支援 V1 和 V2 的鏡像同步遷移實踐以及利用該同步系統對 docker hub 鏡像實作實時同步的方法。

背景

Registry 作為 Docker 的核心元件之一負責鏡像内容的存儲與分發,用戶端的 docker pull 以及 push 指令都将直接與 registry 進行互動。最初版本的 registry 由 Python實作。由于設計初期在安全性,性能以及 API 的設計上有着諸多的缺陷,該版本在 0.9 之後停止了開發,新的項目 distribution 來重新設計并開發下一代 registry。新的項目由 go 語言開發,所有的 API,底層存儲方式,系統架構都進行了全面的重新設計已解決上一代 registry 中存在的問題。四月份 rgistry 2.0 正式釋出,docker 1.6 版本開始支援 registry 2.0, 八月份随着 docker 1.8 釋出,docker hub 正式啟用 2.1 版本 registry 全面替代之前版本 registry。

新版 registry 對鏡像存儲格式進行了重新設計并和舊版不相容,docker 1.5 和之前的版本無法讀取 2.0 的鏡像。同時舊版本的 registry 中已有大量的鏡像需要遷移到新版本 registry 中。在靈雀雲平台的客戶可能會使用各個版本的 docker,也可能更新或者降級使用的版本。為了讓 registry 的版本對使用者透明,靈雀雲在同一域名提供兩套服務的基礎上在背景對鏡像實作了新版和舊版的雙向實時同步,以保證使用者在切換用戶端版本時不會出現 pull 鏡像失敗的情況。在此基礎上,靈雀雲實作了對 docker hub 中官方 library 鏡像到靈雀雲平台的實時同步,通過靈雀雲的國内鏡像社群來為國内使用者提供快速的鏡像下載下傳服務。

Registry V2 的變化

鏡像 id 改進

Docker build 鏡像時會為每個 layer 生成一串 layer id,這個 layer id 是一個用戶端随機生成的字元串,和鏡像内容無關。我們可以通過一個簡單的例子來檢視。

這裡選擇官方的 nginx dockerfile 來 build 鏡像

docker build -t nginx:dockerfile .

Docker Registry V1 與 V2 的差別解析以及靈雀雲的實時同步遷移實踐

接下來重新 build 該鏡像,加上 --no-cache 強制重新 build

docker build --no-cache=true -t nginx:dockerfile .

Docker Registry V1 與 V2 的差別解析以及靈雀雲的實時同步遷移實踐

可以看到除了基礎鏡像 debian 的兩層 layer id 一緻,其餘 layer 的 id 都發生了變化。這種随機 layer id 以及 layer id 與内容無關的設計會帶來很多的問題。

首先, registry v1 通過 id 來判斷鏡像是否存在,客戶需不需要重新 push,而由于鏡像内容和 id 無關,再重新 build 後 layer 在内容不變的情況下很可能 id 發生變化,造成無法利用 registry 中已有 layer 反複 push 相同内容。伺服器端也會有重複存儲造成空間浪費。

其次,盡管 id 由 32 位元組組成但是依然存在 id 碰撞的可能,在存在相同 id 的情況下,後一個 layer 由于 id 和倉庫中已有 layer 相同無法被 push 到 registry 中,導緻資料的丢失。使用者也可以通過這個方法來探測某個 id 是否存在。

最後,同樣是由于這個原因如果程式惡意僞造大量 layer push 到 registry 中占位會導緻新的 layer 無法被 push 到 registry 中。

Docker 官方重新設計新版 registry 的一個主要原因也就是為了解決該問題。

新版的 registry 吸取了舊版的教訓,在伺服器端會對鏡像内容進行哈希,通過内容的哈希值來判斷 layer 在 registry 中是否存在,是否需要重新傳輸。這個新版本中的哈希值被稱為 digest 是一個和鏡像内容相關的字元串,相同的内容會生成相同的 digest。

由于 digest 和内容相關,是以隻要重新 build 的内容相同理論上講無需重新 push,但是由于安全性的考量在特定情況下 layer 依然要重新傳輸。由于 layer 是按 digest 進行存儲,相對 v1 按照随機 id 存儲可以大幅減小磁盤空間占用。registry 服務端會對沖突 digest 進一步進行處理,同時由于 digest 是由 registry 服務端生成,使用者無法僞造 digest 也很大程度上保證了 registry 内容的安全性。

安全性改進

除了對 image 内容進行唯一性哈希外,新版 registry 還在鑒權方式以及 layer 權限上上進行了大幅度調整。

鑒權方式

舊版本的服務鑒權模型如下圖所示:

Docker Registry V1 與 V2 的差別解析以及靈雀雲的實時同步遷移實踐

該模型每次 client 端和 registry 的互動都要多次和 index 打交道,新版本的鑒權模型去除了上圖中的第四第五步,如下圖所示:

Docker Registry V1 與 V2 的差別解析以及靈雀雲的實時同步遷移實踐

新版本的鑒權模型需要 registry 和 authorization service 在部署時分别配置好彼此的資訊,并将對方資訊作為生成 token 的字元串,已減少後續的互動操作。新模型用戶端隻需要和 authorization service 進行一次互動獲得對應 token 即可和 registry 進行互動,減少了複雜的流程。同時 registry 和 authorization service 一一對應的方式也降低了被攻擊的可能。

權限控制

舊版的 registry 中對 layer 沒有任何權限控制,所有的權限相關内容都由 index 完成。在新版 registry 中加入了對 layer 的權限控制,每個 layer 都有一個 manifest 來辨別該 layer 由哪些 repository 共享,将權限做到 repository 級别。

Pull 性能改進

舊版 registry 中鏡像的每個 layer 都包含一個 ancestry 的 json 檔案包含了父親 layer 的資訊,是以當我們 pull 鏡像時需要串行下載下傳,下載下傳完一個 layer 後才知道下一個 layer 的 id 是多少再去下載下傳。如下圖所示:

Docker Registry V1 與 V2 的差別解析以及靈雀雲的實時同步遷移實踐

新版 registry 在 image 的 manifest 中包含了所有 layer 的資訊,用戶端可以并行下載下傳所有的 layer 如下圖所示:

Docker Registry V1 與 V2 的差別解析以及靈雀雲的實時同步遷移實踐

其他改進

  • 全新的 API
  • push 和 pull 支援斷點
  • 後端存儲的插件化
  • notification 機制

靈雀雲的雙向實時同步實踐

靈雀雲上有大量使用者 push 到 v1 版本的鏡像,遷移到 v2 需要對存量進行遷移。同時每天通過使用者 push 和自動建構還會産生大量新的鏡像,為了能夠讓使用者對後端 registry 無感覺可以任意選擇 docker client 的版本,我們實作了鏡像在 V1 和 V2 兩個 registry 的實時同步。

官方方案及其缺陷

docker 官方最近開源了一個新項目 migirator ,提供了一種從 V1 向 V2 遷移的方案。該方案的依賴基礎是 docker 1.6 版本可以同時支援 V1 和 V2 的協定,通過 V1 中的 list api 擷取 registry 中已有的鏡像從 V1 pull 鏡像 push 到 V2。該項目提供了一個 shell 腳本使用者通過配置不同版本 registry 的 endpoint 來自動進行同步。

靈雀雲在該項目公布前已經完成了遷移和同步相關的操作,簡要說一下該方案的缺陷:

  1. 該方案隻适合離線同步,如果平台一直對外提供服務不斷有新的鏡像 push,該方案最終無法收斂。
  2. 該方案是個單機模型,當 registry 中存量鏡像很多時該方法耗時不可接受。
  3. 隻是一個單向遷移方案,雙向同時同步會出現亂序導緻新鏡像被舊鏡像覆寫的情況。
  4. 隻是一個簡單的 demo 産品,缺乏錯誤檢測和監控統計相關功能。

靈雀雲的鏡像雙向實時同步

在靈雀雲的同步中,我們和 migirator 使用了相同的同步基礎,利用 docker 1.6 後版本同時支援兩個版本 registry 的特性,進行 pull 和 push 達到同步的目的。

對于實時的鏡像 push,我們捕獲每一次 push 成功的資訊生成同步任務發送到 rabbitmq 隊列中。對于存量鏡像,我們一次性獲得所有目前 V1 倉庫中的所有鏡像,并以相同的任務格式插入到 rabbitmq 隊列之中。

為了應對存量及實時資料過多,單機同步時間過長的問題,我們将同步的任務分散到多台節點。在 rabbitmq 層面我們将每個同步任務按照 namespace/repository/image 進行哈希分散到多個隊列之中。後端每個工作節點監聽一部分隊列,隻對隊列内的同步任務進行同步。由于每個 image 都會在一個隊列中按序執行,這樣保證了同步的順序,不會出現新的鏡像被舊鏡像覆寫的情況。這樣任務被合理的分散到多台機器。

同時我們會将每個同步任務的狀态記錄到資料庫中,友善統計整體的同步狀況,每個任務的執行情況。我們對任務的格式進行了适度的抽象使得這套同步機制可以靈活運用于其他類型的同步,下面要介紹的官方鏡像同步就利用到了這套同步系統。

官方鏡像實時同步

Docker 官方的鏡像市場 dockerhub 搭建在 AWS 之上,由于國内網絡的原因下載下傳鏡像速度很慢且連接配接不穩定,很容易出現失敗的情況。現在國内常用的做法是通過 mirror registry 來進行加速,但是這種方式也存在着一定的缺陷。

首先通過 mirror registry 下載下傳鏡像時用戶端會去 docker hub 校驗目前鏡像,這一步依然存在網絡不穩定的情況,有一定的失敗機率。其次 mirror registry 中隻能儲存使用者已下載下傳過的鏡像,一些不常用鏡像下載下傳依然很慢,此外 docker hub 的鏡像更新很頻繁,mirror 很容易失效。為了友善國内使用者能夠友善的使用已有鏡像,我們希望用上面的同步系統将 docker hub 官方 library 中的鏡像以及大家較常用的 tutum 鏡像實時同步到國内靈雀雲的鏡像社群,使得國内使用者不用忍受網絡阻塞帶來的痛苦。目前靈雀雲鏡像社群的 library和 tutum 倉庫實時和官方 docker hub 進行同步,目前在官方鏡像更新後數分鐘後靈雀雲社群的鏡像就可以更新。

上述 registry 遷移同步的系統可以直接應用到官方鏡像社群和靈雀雲鏡像社群的同步,但是由于網絡原因,直接使用這種方法同步效率極低,且極易失敗,如何克服網絡障礙成為了問題的關鍵。由于實時同步肯定是同步最新生成的鏡像,是以 mirror 的方式并不實用。vpn 的方式由于本身國際帶寬就有限,且 vpn 很容易被動态識别而被幹擾在進行大資料傳輸的過程中表現十分不穩定,無法達到很好的時效性。土豪的話其實一根專線就可以了,但是我們希望能以更經濟的形式解決這個問題。

我們采用了曲線救國的方式來完成實時同步,首先在海外下載下傳鏡像,在國外的環境可以高速的下載下傳鏡像。接下來通過底層存儲級别的同步技術将存儲内容實時同步到國内的機器上。最後再從國内的機器将鏡像 push 到靈雀雲的鏡像社群中。通過這種曲線救國的方式,我們将同步的延遲控制在分鐘級别,可以滿足絕大多數使用者對最官方新鏡像的需求。希望通過我們的努力可以使使用者更友善快捷的下載下傳并使用到想用的鏡像

Docker-Registry

Docker Distribution Roadmap

Docker Registry v2 authentication via central service

Docker Registry HTTP API V2

Docker migirator