當前位置:
首頁 > 最新 > 想要高效上傳下載?試試去中心化的Docker鏡像倉庫設計思路

想要高效上傳下載?試試去中心化的Docker鏡像倉庫設計思路

Docker 作為輕量級容器技術解決應用容器化已經為廣大用戶使用,涵蓋了應用的編譯、打包、部署、測試等整個周期。鏡像為運行 Docker 容器提供了基礎和重要的前提,Docker Registry 提供了一個存儲、分發、管理 Docker 鏡像的倉庫服務,在某些場景下,如跨國部署場景,要求鏡像倉庫服務提供更高效的上傳下載,同時降低複雜度和具備服務高可用。因此,本文設計了一種鏡像倉庫服務的去中心化的新思路。

介紹

Docker 是一種面嚮應用開發和運維人員的開發、部署和運行的容器平台,相對於 Virtual Machine 更加輕量,底層使用 Linux Namespace(UTS、IPC、PID、Network、Mount、User)和 cgroups(Control Groups)技術對應用進程進行虛擬化隔離和資源管控,並擁有靈活性、輕量、可擴展性、可伸縮性等特點。Docker 容器實例從鏡像載入,鏡像包含應用所需的所有可執行文件、配置文件、運行時依賴庫、環境變數等,這個鏡像可以被載入在任何 Docker Engine 機器上。越來越多的開發者和公司都將自己的產品打包成 Docker 鏡像進行發布和銷售。

在 Docker 生態中,提供存儲、分發和管理鏡像的服務為 Docker Registry 鏡像倉庫服務,是 Docker 生態重要組成部分,我甚至認為這是 Docker 流行起來最重要的原因。用戶通過 命令把打包好的鏡像發布到 Docker Registry 鏡像倉庫服務中,其他的用戶通過 從鏡像倉庫中獲取鏡像,並由 Docker Engine 啟動 Docker 實例。

Docker Registry 鏡像倉庫,是一種集中式存儲、應用無狀態、節點可擴展的 HTTP 公共服務。提供了鏡像管理、存儲、上傳下載、AAA 認證鑒權、WebHook 通知、日誌等功能。幾乎所有的用戶都從鏡像倉庫中進行上傳和下載,在跨國上傳下載的場景下,這種集中式服務顯然存在性能瓶頸,高網路延遲導致用戶 pull 下載消耗更長的時間。同時集中式服務遭黑客的 DDos 攻擊會面臨癱瘓。當然你可以部署多個節點,但也要解決多節點間鏡像同步的問題。因此,可以設計一種去中心化的分散式鏡像倉庫服務來避免這種中心化的缺陷。

本文起草了一個純 P2P 式結構化網路無中心化節點的新鏡像倉庫服務 Decentralized Docker Registry(DDR),和阿里的蜻蜓 Dragonfly、騰訊的 FID 混合型 P2P 模式不同,DDR 採用純 P2P 網路結構,沒有鏡像 Tracker 管理節點,網路中所有節點既是鏡像的生產者同時也是消費者,純扁平對等,這種結構能有效地防止拒絕服務 DDos 攻擊,沒有單點故障,並擁有高水平擴展和高並發能力,高效利用帶寬,極速提高下載速度。

鏡像

Docker 是一個容器管理框架,它負責創建和管理容器實例,一個容器實例從 Docker 鏡像載入,鏡像是一種壓縮文件,包含了一個應用所需的所有內容。一個鏡像可以依賴另一個鏡像,並是一種單繼承關係。最初始的鏡像叫做 Base 基礎鏡像,可以繼承 Base 鏡像製作新鏡像,新鏡像也可以被其他的鏡像再繼承,這個新鏡像被稱作 Parent 父鏡像。

而一個鏡像內部被切分稱多個層級 Layer,每一個 Layer 包含整個鏡像的部分文件。當 Docker 容器實例從鏡像載入後,實例將看到所有 Layer 共同合併的文件集合,實例不需要關心 Layer 層級關係。鏡像裡面所有的 Layer 屬性為只讀,當前容器實例進行寫操作的時候,從舊的 Layer 中進行 Copy On Write 操作,複製舊文件,產生新文件,併產生一層可寫的新 Layer。這種 COW 做法能最大化節省空間和效率,層級見也能充分復用。一個典型的鏡像結構如下:

alpine 是基礎鏡像,提供了一個輕量的、安全的 Linux 運行環境,Basic App1 和 Basic App2 都基於和共享這個基礎鏡像 alpine,Basci App 1/2 可作為一個單獨的鏡像發布,同時也是 Advanced App 2/3 的父鏡像,在 Advanced App 2/3 下載的時候,會檢測並下載所有依賴的父鏡像和基礎鏡像,而往往在 registry 存儲節點裡,只會存儲一份父鏡像實例和基礎鏡像,並被其他鏡像所共享,高效節省存儲空間。

一個鏡像內部分層 Layer 結構如下:

Advanced App1 內部文件分為 4 個 layer 層存儲,每一個層 Layer 為 application/vnd.docker.image.rootfs.diff.tar.gzip 壓縮類型文件,並通過文件 sha256 值標識,所有 layer 層的文件組成了最終鏡像的內容,在容器從鏡像啟動後,容器實例看到所有 layer 層的文件內容。如其中一層 Layer 存儲如下:

其中實現這種分層模型的文件系統叫 UnionFS 聯合文件系統,實現有 AUFS、Overlay、Overlay2 等,UnionFS 分配只讀目錄、讀寫目錄、掛載目錄,只讀目錄類似鏡像里的只讀 Layer,讀寫目錄類似可寫 Layer,所有文件的合集為掛載目錄,即掛載目錄是一個邏輯目錄,並能看到所有的文件內容,在 UnionFS 中,目錄叫做 Branch,也即鏡像中的 Layer。

使用 AUFS 構建一個 2 層 Branch 如下:

創建了 2 個層級目錄分別是 /tmp/rw 和 /tmp/r,同時 br= 指定了所有的 branch 層,默認情況下 br=/tmp/rw 為可寫層,: 後面只讀層,/tmp/aufs 為最終文件掛載層,文件目錄如下:

可以看到掛載目錄 /tmp/aufs 下顯示了 /tmp/rw 和 /tmp/r 目錄下的所有文件,通過這種方式實現了鏡像多層 Layer 的結構。除了 UnionFS 能實現這種模型,通過 Snapshot 快照和 Clone 層也能實現類似的效果,如 Btrfs Driver、ZFS Driver 等實現。

Docker Registry

Docker Registry 鏡像倉庫存儲、分發和管理著鏡像,流行的鏡像倉庫服務有 Docker Hub、Quary.io、Google Container Registry。每一個用戶可以在倉庫內註冊一個 namespace 命名空間,用戶可以通過 命令把自己的鏡像上傳到這個 namespace 命名空間,其他用戶則可以使用 命令從此命名空間中下載對應的鏡像,同時一個鏡像名可以配置不同的 tags 用以表示不同的版本。

Push 上傳鏡像

當要上傳鏡像時,Docker Client 向 Docker Daemon 發送 push 命令,並傳入本地通過 打包的上傳地址,即 ://:,創建對應的 manifest 元信息,元信息包括 docker version、layers、image id 等,先通過 HEAD /blob/ 檢查需要上傳的 layer 在 Registry 倉庫中是否存在,如果存在則無需上傳 layer,否則通過 POST /blob/upload 上傳 blob 數據文件,Docker 使用 PUT 分段並發上傳,每一次上傳一段文件的 bytes 內容,最終 blob 文件上傳完成後,通過 PUT /manifest/ 完成元數據上傳並結束整個上傳過程。

Pull 下載鏡像

當用戶執行 命令時,Docker Client 向 Docker Daemon 發送 pull 命令,如果不指定 host 名字,默認 docker daemon 會從 Docker hub 官方倉庫進行下載,如果不指定 tag,則默認為 latest。首先向 Docker Hub 發送 GET /manifest/ 請求,Docker Hub 返回鏡像名字、包含的 Layers 層等信息,Docker Client 收到 Layers 信息後通過 HEAD /blob/ 查詢 Docker Registry 對應的 blob 是否存在,如果存在,通過 GET /blob/ 對所有 Layer 進行並發下載,默認 Docker Client 會並發對 3 個 blob 進行下載,最後完成整個下載過程,鏡像存入本地磁碟。

P2P 網路

P2P 網路從中心化程度看分為純 P2P 網路和混合 P2P 網路,純 P2P 網路沒有任何形式中心伺服器,每一個節點在網路中對等,信息在所有節點 Peer 中交換,如 Gnutella 協議。混合 P2P 網路的 Peer 節點外,還需要維護著一個中心伺服器保存節點、節點存儲內容等信息以供路由查詢,如 BitTorrent 協議。

純 P2P 網路

混合 P2P 網路

P2P 網路從網路組織結構看又分為結構化 P2P 網路和非結構 P2P 網路,Peer 節點之間彼此之間無規則隨機連接生成的網狀結構,稱之為非結構 P2P,如 Gnutella 。而 Peer 節點間相互根據一定的規則連接交互,稱之為結構 P2P,如 Kademlia。

非結構 P2P,之間無序不規則連接

結構 P2P,按照一定的規則相互互聯

DDR 鏡像倉庫服務系統採用純網路和 DHT(Distribution Hash Table) 的 Kademlia 結構化網路實現,根據 Kademlia 的演算法,同樣為每一個 Peer 節點隨機分配一個與鏡像 Layer 標示一致的 sha256 值標識,每一個 Peer 節點維護一張自身的動態路由表,每一條路由信息包含了元素,路由表通過網路學習而形成,並使用二叉樹結構標示,即每一個 PeerID 作為二叉樹的葉子節點,256-bit 位的 PeerID 則包含 256 個子樹,每一個子樹下包含 2^i(0

Kademlia 定義節點之間的距離為 PeerID 之間 XOR 異或運算的值,如 X 與 Y 的距離 dis(x,y) = PeerIDx XOR PeerIDy,這是「邏輯距離」,並不是物理距離,XOR 異或運算符合如下 3 個幾何特性:

因此,Kademlia 定址的過程,實際上不斷縮小距離的過程,每一個節點 根據自身的路由表信息不斷向離目的節點最近的節點進行迭代詢問,直到找到目標為止,這個過程就像現實生活中查找一個人一樣,先去詢問這個人所在的國家,然後詢問到公司,再找到部門,最終找到個人。

查詢節點

當節點需要查詢某個 PeerID 時,查詢二叉樹路由表,計算目標 PeerID 在當前哪個子樹區間(bucket 桶)中,並向此 bucket 桶中 n(n

查詢鏡像

在 DDR 鏡像服務中,需要在 Kademlia 網路中需要找到指定的鏡像,而 Kademlia 查詢只是節點 PeerID 查詢,為了查找指定的 sha256 鏡像,常用的做法是建立節點 PeerID 和文件 LayerID 的映射關係,但這需要依賴全局 Tracker 節點存儲這種映射關係,而並不適合純 P2P 模式。因此,為了找到對應的鏡像,使用 PeerID 存儲 LayerID 路由信息的方法,即同樣或者相近 LayerID 的 PeerID 保存真正提供 LayerID 下載的 PeerID 路由,並把路由信息返回給查詢節點,查詢節點則重定向到真正的 Peer 進行鏡像下載。在這個方法中,節點 Peer 可分為消費節點、代理節點、生產節點、副本節點 4 種角色,生產節點為鏡像真正製作和存儲的節點,當新鏡像製作出來後,把鏡像 Image Layer 的 sha256 LayerID 作為參數進行 FIND_NODE 查詢與 LayerID 相近或相等的 PeerID 節點,並推送生產節點的 IP、Port、PeerID 路由信息。這些被推送的節點稱為 Proxy 代理節點,同時代理節點也作為對生產節點的緩存節點存儲鏡像。當消費節點下載鏡像 Image Layer 時,通過 LayerID 的 sha256 值作為參數 FIND_NODE 查找代理節點,並向代理節點發送 FIND_VALE 請求返回真正鏡像的生產節點路由信息,消費節點對生產節點進行 docker pull 鏡像拉取工作。

在開始 docker pull 下載鏡像時,需要先找到對應的 manifest 信息,如 ,因此,在生成者製作新鏡像時,需要以 作為輸入同樣生成對應的 sha256 值,並類似 Layer 一樣推送給代理節點,當消費節點需要下載鏡像時,先下載鏡像 manifest 元信息,再進行 Layer 下載,這個和 Docker Client 從 Docker Registry 服務下載的流程一致。

DDR 架構

每一個節點都部署 Docker Registry 和 DDR,DDR 分為 DDR Driver 插件和 DDR Daemon 常駐進程,DDR Driver 作為 Docker Registry 的存儲插件承接 Registry 的 blob 和 manifest 數據的查詢、下載、上傳的工作,並與 DDR Daemon 交互,主要對需要查詢的 blob 和 manifest 數據做 P2P 網路定址和在寫入新的 blob 和 manifest 時推送路由信息給 P2P 網路中代理節點。DDR Daemon 作為 P2P 網路中一個 Peer 節點接入,負責 Peer 查詢、Blob、Manifest 的路由查詢,並返迴路由信息給 DDR Driver,DDR Driver 再作為 Client 根據路由去 P2P 網路目的 Docker Registry 節點進行 Push/Pull 鏡像。

DDR 與 Docker Registry 集成

docker registry 鏡像倉庫服務採用可擴展性的設計,允許開發者自行擴展存儲驅動以實現不同的存儲要求,當前倉庫官方支持內存、本地文件系統、S3、Azure、swift 等多個存儲,DDR Driver 驅動實現如下介面 (registry/storage/driver/storagedriver.go):

DDR Push 上傳鏡像

Docker Client 向本地 Docker Registry 上傳一個鏡像時會觸發一系列的 HTTP 請求,這些請求會調用 DDR Driver 對應的介面實現,DDR 上傳流程如下:

Client 通過 HEAD /v2/hello-world/blobs/sha256:9bb5a5d4561a5511fa7f80718617e67cf2ed2e6cdcd02e31be111a8d0ac4d6b7 判斷上傳的 blob 數據是否存在,如果本地磁碟不存在,Registry 返回 404 錯誤;

POST /v2/hello-world/blobs/uploads/ 開始上傳的 blob 數據;

PATCH /v2/hello-world/blobs/uploads/ 分段上傳 blob 數據;

PUT /v2/hello-world/blobs/uploads/ 完成分段上傳 blob 數據,DDR 根據 blob 文件的 sha256 信息尋找 P2P 網路中與目標 sha256 值相近的 k 個代理節點,發送包含 blob sha256 的 STORE 消息,對端 Peer 收到 sha256 信息後,存儲源 Peer 節點 IP、Port、blob sha256 等信息,同時也向代理節點 PUT 上傳內容;

HEAD /v2/hello-world/blobs/sha256:9bb5a5d4561a5511fa7f80718617e67cf2ed2e6cdcd02e31be111a8d0ac4d6b7 確認上傳的數據是否上傳成功,Registry 返回 200 成功;

PUT /v2/hello-world/manifests/latest 完成 manifest 元數據上傳,DDR Driver 按照 /manifest/ 做 sha256 計算值後,尋找 P2P 網路中與目標 sha256 值相近的 k 個代理節點,發送包含 manifest sha256 的 STORE 消息,對端 Peer 收到 sha256 信息後,存儲源 Peer 節點 IP、Port、blob sha256 等信息同時也向代理節點 PUT 元信息內容;

DDR Pull 下載鏡像

Docker Client 向本地 Docker Registry 下載鏡像時會觸發一系列的 HTTP 請求,這些請求會調用 DDR Driver 對應的介面實現,DDR 下載交互流程如下:

GET /v2/hello-world/manifests/latest 返回下某個 的 manifest 源信息,DDR Driver 對 hello-world/manifest/latest 進行 sha256 計算,並向 P2P 網路中發送 FIND_NODE 和 FIND_VALUE 找到代理節點,通過代理節點找到生產節點,並向生產節點發送 GET 請求獲取 manifest 元信息。

Client 獲取 manifest 元信息後,通過 GET /v2/hello-world/blobs/sha256:e38bc07ac18ee64e6d59cf2eafcdddf9cec2364dfe129fe0af75f1b0194e0c96 獲取 blob 數據內容,DDR Driver 以 e38bc07ac18ee64e6d59cf2eafcdddf9cec2364dfe129fe0af75f1b0194e0c96 作為輸入,向 P2P 網路中發送 FIND_NODE 和 FIND_VALUE 找到代理節點,通過代理節點找到生產節點,並向生產節點發送 GET 請求獲取 blob 數據。

總結

以上就是整個 DDR 完全去中心化 P2P Docker 鏡像倉庫的設計,主要利用純網路結構化 P2P 網路實現鏡像的 manifest 和 blob 數據的路由存儲、查詢,同時每一個節點作為一個獨立的鏡像倉庫服務為全網提供鏡像的上傳和下載。

其他工作

Docker Registry 在 push/pull 下載的時候需要對 Client 進行認證工作,類似 Docker Client 需要在 DDR Driver 同樣採用標準的 RFC 7519 JWT 方式進行認證鑒權。

參考鏈接:

阿里巴巴 Dragonfly:

https://github.com/alibaba/Dragonfly

騰訊 FID:

https://ieeexplore.ieee.org/document/8064123/

Btrfs Driver:

https://docs.docker.com/storage/storagedriver/btrfs-driver/

ZFS Driver:

https://docs.docker.com/storage/storagedriver/zfs-driver/

JWT:https://jwt.io/

作者簡介:

活動推薦

面對百萬台伺服器、千萬張網卡、海量的配置項和監控點,怎麼第一時間知道故障點在哪裡?如何快速判斷分析當前故障的影響面和修復途徑?怎樣在不影響業務的情況下快速修復?也許你早就拋棄了 SSH 和 CLI 的方式去運維一套龐大的基礎設施,開始嘗試寫一些自動化腳本和配置。或者已經搭建了一套自動化的監控平台,並在這條前行的路上不斷的踩坑成長。

喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 高效開發運維 的精彩文章:

VMware推出VMware Kubernetes Engine:市場威脅下的防衛舉措?
故障周第二彈:谷歌雲平台全局負載均衡服務發生中斷

TAG:高效開發運維 |