歡迎光臨
我們一直在努力

想要高效上傳下載?試試去中心化的Docker映象倉庫設計

文章摘要: DDR 根據 blob 檔案的 sha256 資訊尋找 P2P 網路中與目標 sha256 值相近的 k 個代理節點DDR Driver 再作為 Client 根據路由去 P2P 網路目的 Docker Registry 節點進行 Push/Pull 映象

介紹

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

在 Docker 生態中,提供儲存、分發和管理映象的服務為 Docker Registry 映象倉庫服務,是 Docker 生態重要組成部分,我甚至認為這是 Docker 流行起來最重要的原因。使用者通過 docker push 命令把打包好的映象釋出到 Docker Registry 映象倉庫服務中,其他的使用者通過 docker pull 從映象倉庫中獲取映象,並由 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 儲存如下:

$ file /var/lib/registry/docker/registry/v2/blobs/sha256/40/4001a1209541c37465e524db0b9bb20744ceb319e8303ebec3259fc8317e2dec/data
data: gzip compressed data
$ sha256sum /var/lib/registry/docker/registry/v2/blobs/sha256/40/4001a1209541c37465e524db0b9bb20744ceb319e8303ebec3259fc8317e2dec/data
4001a1209541c37465e524db0b9bb20744ceb319e8303ebec3259fc8317e2dec

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

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

$ mkdir /tmp/rw /tmp/r /tmp/aufs
$ mount -t aufs -o br=/tmp/rw:/tmp/r none /tmp/aufs

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

$ ls -l /tmp/rw/
-rw-r--r-- 1 root       root       23 Mar 25 14:21 file_in_rw_dir

$ ls -l /tmp/r/
-rw-r--r-- 1 root       root            26 Mar 25 14:20 file_in_r_dir

$ ls -l /tmp/aufs/
-rw-r--r-- 1 root       root            26 Mar 25 14:20 file_in_r_dir
-rw-r--r-- 1 root       root            23 Mar 25 14:21 file_in_rw_dir

可以看到掛載目錄 /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 名稱空間,使用者可以通過 docker push 命令把自己的映象上傳到這個 namespace 名稱空間,其他使用者則可以使用 docker pull 命令從此名稱空間中下載對應的映象,同時一個映象名可以配置不同的 tags 用以表示不同的版本。

Push 上傳映象

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

Pull 下載映象

當用戶執行 docker 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<=i<=256) 到 2^i+1(0<=i<=255) 個 Peer 節點,如 i=2 的子樹包含二進制 000...100、000...101、000...110、000...111 的 4 個節點,每一個這樣的子樹區間形成 bucket 桶,每一個桶設定最大路由數為 5 個,當一個 bucket 桶滿時,則採用 LRU 規則進行更新,優先保證活躍的 Peer 節點存活在路由表中。根據二叉樹的結構,只要知道任何一棵子樹就能遞迴找到任意節點。

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

1. X 與 Y 節點的距離等於 Y 與 X 節點的距離,即 dis(x,y) = dis(y,x),異或運算之間的距離是對稱的。

2. X 與 X 節點的距離是 0,異或運算是等同的。

3. X、Y、Z 節點之間符合三角不等式,即 dis(x,y) <= dis(x,z) + dis(z,y)

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

查詢節點

當節點需要查詢某個 PeerID 時,查詢二叉樹路由表,計算目標 PeerID 在當前哪個子樹區間(bucket 桶)中,並向此 bucket 桶中 n(n<=5) 節點同時傳送 FIND_NODE 請求,n 個節點收到 FIND_NODE 請求後根據自己的路由表資訊返回與目標 PeerID 最接近的節點 PeerID,源節點再根據 FIND_NODE 返回的路由資訊進行學習,再次向新節點發送 FIND_NODE 請求,可見每一次迭代至少保證精確一個 bit 位,以此迭代,並最終找到目標節點,查詢次數為 logN。

查詢映象

在 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 資訊,如 docker pull os/centos:7.2 ,因此,在生成者製作新映象時,需要以 /: 作為輸入同樣生成對應的 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):

// StorageDriver defines methods that a Storage Driver must implement for a
// filesystem-like key/value object storage. Storage Drivers are automatically
// registered via an internal registration mechanism, and generally created
// via the StorageDriverFactory interface (https://godoc.org/github.com/docker/distribution/registry/storage/driver/factory).
// Please see the aforementioned factory package for example code showing how to get an instance
// of a StorageDriver
type StorageDriver interface {
    Name() string
    GetContent(ctx context.Context, path string) ([]byte, error)
    PutContent(ctx context.Context, path string, content []byte) error
    Reader(ctx context.Context, path string, offset int64) (io.ReadCloser, error)
    Writer(ctx context.Context, path string, append bool) (FileWriter, error)
    Stat(ctx context.Context, path string) (FileInfo, error)
    List(ctx context.Context, path string) ([]string, error)
    Move(ctx context.Context, sourcePath string, destPath string) error
    Delete(ctx context.Context, path string) error
    URLFor(ctx context.Context, path string, options map[string]interface{}) (string, error)
    Walk(ctx context.Context, path string, f WalkFn) error
}

DDR Push 上傳映象

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

  1. Client 通過 HEAD /v2/hello-world/blobs/sha256:9bb5a5d4561a5511fa7f80718617e67cf2ed2e6cdcd02e31be111a8d0ac4d6b7 判斷上傳的 blob 資料是否存在,如果本地磁碟不存在,Registry 返回 404 錯誤;
  2. POST /v2/hello-world/blobs/uploads/ 開始上傳的 blob 資料;
  3. PATCH /v2/hello-world/blobs/uploads/ 分段上傳 blob 資料;
  4. PUT /v2/hello-world/blobs/uploads/ 完成分段上傳 blob 資料,DDR 根據 blob 檔案的 sha256 資訊尋找 P2P 網路中與目標 sha256 值相近的 k 個代理節點,傳送包含 blob sha256 的 STORE 訊息,對端 Peer 收到 sha256 資訊後,儲存源 Peer 節點 IP、Port、blob sha256 等資訊,同時也向代理節點 PUT 上傳內容;
  5. HEAD /v2/hello-world/blobs/sha256:9bb5a5d4561a5511fa7f80718617e67cf2ed2e6cdcd02e31be111a8d0ac4d6b7 確認上傳的資料是否上傳成功,Registry 返回 200 成功;
  6. 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 下載互動流程如下:

  1. GET /v2/hello-world/manifests/latest 返回下某個 的 manifest 源資訊,DDR Driver 對 hello-world/manifest/latest 進行 sha256 計算,並向 P2P 網路中傳送 FIND_NODE 和 FIND_VALUE 找到代理節點,通過代理節點找到生產節點,並向生產節點發送 GET 請求獲取 manifest 元資訊。
  2. 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

騰訊 FID

Btrfs Driver

ZFS Driver

JWT

作者簡介:

yangjunsss,曾就職於 IBM、青雲 QingCloud,現就職於華為,研究方向:容器微服務、IaaS、P2P 分散式。所有文章僅代表個人觀點,與所在的公司無關。郵箱 [email protected]

感謝張嬋對本文的審校。

未經允許不得轉載:頭條楓林網 » 想要高效上傳下載?試試去中心化的Docker映象倉庫設計