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

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

鎮樓小姐姐

36份一線互聯網Java面試電子書

84個Java稀缺面試題視頻


介紹

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 做法能最大化節省空間和效率,層級見也能充分複用。一個典型的鏡像結構如下:

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

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

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

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

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/ 完成元數據上傳並結束整個上傳過程。

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

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 進行下載,最後完成整個下載過程,鏡像存入本地磁盤。

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

P2P 網絡

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

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

純 P2P 網絡

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

混合 P2P 網絡

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

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

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

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

結構 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 節點存活在路由表中。根據二叉樹的結構,只要知道任何一棵子樹就能遞歸找到任意節點。

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

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。

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

查詢鏡像

在 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 服務下載的流程一致。

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

DDR 架構

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

每一個節點都部署 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 方式進行認證鑑權。


分享到:


相關文章: