Docker 鏡像的元數據node
repository元數據python
repository在本地的持久化文件存放於/var/lib/docker/image/overlay2/repositories.json中linux
[root@service-1 overlay2]# cat repositories.json | python -mjson.tool { "Repositories": { "192.168.10.31/library/nginx": { "192.168.10.31/library/nginx:v1": "sha256:2bcb04bdb83f7c5dc30f0edaca1609a716bda1c7d2244d4f5fbbdfef33da366c", "192.168.10.31/library/nginx@sha256:dabecc7dece2fff98fb00add2f0b525b7cd4a2cacddcc27ea4a15a7922ea47ea": "sha256:2bcb04bdb83f7c5dc30f0edaca1609a716bda1c7d2244d4f5fbbdfef33da366c" }, } }
文件中存儲了全部repository的名字(如192.168.10.31/library/nginx),每一個repository下全部的版本鏡像的名字和tag(如192.168.10.31/library/nginx:v1)以及對應的鏡像ID。而referenceStore的做用即是解析不一樣格式的repository名字,而且管理repository與鏡像ID的映射關係nginx
image元數據算法
image元數據包括了鏡像框架(如amd64)、操做系統(如Linux)、鏡像默認配置、構建該鏡像的容器ID和配置、建立時間、建立該鏡像的Docker版本、構建鏡像的歷史信息以及rootfs組成,其中構建鏡像歷史信息和rootfs組成部分除了具體描述鏡像的做用外,還將鏡像和構建該鏡像的鏡像層關聯起來。Docker會根據歷史信息和rootfs中的diff_ids計算出構建成該鏡像的鏡像層的存儲索引chainID,這也是Docker1.10鏡像存儲中基於內容尋址的核心技術。docker
imageStore則管理鏡像ID與鏡像元數據之間的映射關係,以及元數據的持久化操做。Docker1.13.1版本持久化文件默認位於/var/lib/docker/image/overlay2/imagedb/content/sha256/[image_id]中json
layer元數據ubuntu
layer對應鏡像層的概念,在Docker 1.10版本之前,鏡像經過一個graph結構管理,每個鏡像層擁有的元數據,記錄了該層的構建信息以及父鏡像ID,而最上面的鏡像層會對多記錄一些信息做爲整個鏡像的元數據。擁有graph則根據鏡像ID(即最上層的鏡像ID)和每一個鏡像記錄的父鏡像ID維護一個樹狀的鏡像層結構centos
在Docker 1.10版本後,鏡像元數據管理巨大改變之一即是簡化了,鏡像層的元數據,鏡像層只包含一個具體的鏡像層文件包,用戶在Docker宿主機上下載某個鏡像層以後,Docker會在宿主機上基於鏡像層文件包個image元數據,構建本地的layer與元數據,包括diff、parent、size等。而Docker將宿主機上生產新的鏡像層上傳到registry時,與新鏡像層相關的宿主機上元數據也不會與鏡像層一塊打包上傳bash
Docker中定義了Layer與RWLayper兩種接口,分別用來定義只讀層和可讀寫層一些操做,又定義了roLayer和mountedLayer,分別實現上述兩種接口。其中,roLayer用於描述不可改變得鏡像層,mountedLayer用於描述可讀寫的容器層。
具體來講。roLayer存儲內容主要有索引該鏡像層的chainID、該鏡像層的校驗碼diffID、父鏡像層parent、graphdriver存儲當前鏡像層文件的cacheID、該鏡像層的大小size等內容。這些元數據的持久化文件位於/var/lib/docker/image/overlay2/imagedb/content/sha256/[chainID],其中,ddifID和size能夠經過該鏡像層計算出來;chainID和父鏡像層parent須要從所屬的image元數據中計算獲得;而cacheID是在當前宿主機上隨機生成的一個UUID,在當前宿主機與該鏡像層一一對應,用於標識並索引graphdriver中的鏡像層文件
在layer的全部屬性中,diffID採用SHA256算法,基於鏡像層文件包的內容計算獲得。而chainID是基於內容存儲索引,它根據當前層與全部祖先鏡像層diffID計算出來的
具體算法
- 若是該鏡像層是最底層(沒有父鏡像層),該層的diffID即是chainID
- 該鏡像層的chainID計算公式chainID(n)=SHA256(chainID(n-1)diffID(n)),也就是根據父鏡像層的chainID加一個空格和當前層的diffID,再計算SHA256校驗碼
mountedLayer存儲的內容主要爲索引某個容器的可讀寫層(也叫容器層)的ID(也對應容器的ID)、容器init層在graphdriver中的ID——initID、讀寫層在graphdriver中的ID——mountID以及容器層的父層鏡像的chainID——parent。
Docker 存儲驅動
鏡像層與寫時複製機制,爲了支持這些特性,Docker提供了存儲驅動接口。存儲驅動根據操做系統底層的支持提供了針對某文件系統的初始化操做以及對鏡像的增刪改查和差別比較等操做。目前存儲系統接口已經有aufs、btrfs、devicemapper、vfs、overlay、zfs這6中具體實現,其中vfs不支持寫時複製,是爲使用volume提供的存儲驅動,僅僅簡單文件掛載操做,剩下的5中支持寫時複製,它們的實現有必定的類似之處。在啓動Docker服務時使用 docker daemon -s some_driver_name,來指定使用的存儲驅動,固然指定的存儲驅動必須被底層的操做系統支持
存儲驅動的功能與管理
Docker中管理文件系統的驅動爲graphdriver,其中定義了統一的接口對不一樣文件系統進行管理,在Docker daemon啓動時就會根據不一樣的文件系統選擇合適的驅動
存儲驅動接口定義
GraphDriver主要定義了Dirver和ProtoDriver兩個接口,全部驅動程序經過實行Dirver接口提供相應的功能,而ProtoDriver接口則負責定義其中的基本功能。
String()返回一個表明這個驅動的字符串,一般驅動名字
Create()建立一個新的鏡像層,須要建立者傳入一個惟一的ID和所需的父鏡像ID
Remove()嘗試根據一個ID刪除一個鏡像層
Get()返回指定ID的層的掛載點的絕對路徑
Put()釋放一個層使用的資源,好比卸載一個已掛載的層
Exists()查詢指定的ID對應的層是否存在
Status()返回這個驅動的狀態,這個狀態用一些鍵值表示
Cleanup()釋放由這個驅動管理的全部資源,如卸載全部層
而正常的Driver接口實現經過包含一個ProtoDriver的匿名對象來實現上面8個基本功能,除此以外,Driver還定義了4個其餘方法,用於數據層之間的差別(diff)進行管理
Diff()將指定的ID層相對父鏡像層改動的文件打包並返回
Changes()返回指定鏡像層與父鏡像層的差別列表
ApplyDiff()從差別文件包裏取出差別列表,並應用到指定ID的層與父鏡像層,返回新鏡像層的大小
DiffSize()計算指定ID層與父鏡像層的差別,並返回差別相對基礎文件系統的大小
Graphdriver還提供了naiveDiffDriver結構,這個結構包含了一個ProtoDriver對象並實現了Driver接口中與差別有關的方法,能夠看作Driver接口的一個實現。Docker中任何存儲驅動都須要完成實現上述Driver接口,當咱們在Docker中添加新的存儲驅動時,能夠實現Driver的所有12個方法,或者實現ProtoDriver的8個方法在使用naiveDiffriver進一步封裝。無論哪一種作法,只要集成了基本的存儲操做和差別操做的實現,一個存儲驅動就算開發完成
存儲驅動的建立
首先各種存儲驅動都須要定義一個屬於本身的初始化過程,並在初始化過程當中向Graphdriver註冊本身,Graphdriver維護了一張drivers列表,提供從驅動名到驅動初始化的映射,這用於未來根據驅動名稱查找對應驅動的初始化方法
所謂註冊過程就是存儲驅動經過調用Graphdriver提供本身的名字和對應的初始化函數,這樣Graphdriver就能夠將驅動名和這個初始化方法保存到drivers。當須要建立一個存儲驅動時(如aufs的驅動),Graphdriver會根據名字從對應的drivers中查找,這個驅動對應的初始化方法,而後調用初始化函數獲得對應的Driver對象。
經常使用存儲驅動分析
aufs
aufs是一種支持聯合掛載的文件系統,簡單來講就是支持將不一樣的目錄掛載到同一個目錄下,這些掛載操做對用戶而言是透明的,用戶操做該目錄時並不會以爲與其餘目錄有什麼不一樣。這些目錄掛載是分層次的,一般而言最上層是可讀寫層,下層只讀層,全部,aufs的每一層都是一個其餘文件系統
當須要讀取一個A文件時,會從最頂層的讀寫層開始向下查找,本層沒有,則根據層之間的關係到下一層,直到找到文件A並打開它;當須要寫入一個文件時,若是這個文件不存在,則在讀寫層新建一個;不然像上面的過程同樣從頂層開始查找,直到找到A文件,aufs會把這個文件複製到讀寫層進行修改。由此能夠看出,在第一次修改已有文件時,若是這個文件很大,即便修改幾行字節,也會產出巨大磁盤開銷。當須要刪除一個文件時,若是這個文件僅僅存在於讀寫層中,則能夠直接刪除這個文件,不然須要先刪除讀層中的備份,再在讀寫層中建立一個whiteou文件來標識這個文件不存在,而不是真正刪除底層文件;新建一個文件時,若是這個文件在讀寫層對應的whiteou文件,則先刪除whiteou文件再新建。不然直接在讀寫層新建便可。
鏡像文件在本地存放目錄;咱們知道Docker工做目錄時/var/lib/docker
配置系統支持aufs文件系統,默認centos7不支持
[root@service-3 ~]# wget https://yum.spaceduck.org/kernel-ml-aufs/kernel-ml-aufs.repo --2019-04-09 09:56:33-- https://yum.spaceduck.org/kernel-ml-aufs/kernel-ml-aufs.repo 正在解析主機 yum.spaceduck.org (yum.spaceduck.org)... 63.211.111.86 正在鏈接 yum.spaceduck.org (yum.spaceduck.org)|63.211.111.86|:443... 已鏈接。 已發出 HTTP 請求,正在等待迴應... 200 OK 長度:133 [application/octet-stream] 正在保存至: 「kernel-ml-aufs.repo.1」 100%[=====================================================================================================================================================>] 133 --.-K/s 用時 0s [root@service-3 ~]# mv kernel-ml-aufs.repo /etc/yum.repos.d/ [root@service-3 ~]# yum -y install kernel-ml-aufs 已加載插件:fastestmirror Loading mirror speeds from cached hostfile * base: mirrors.huaweicloud.com * elrepo: hkg.mirror.rackspace.com * extras: mirrors.neusoft.edu.cn * updates: mirrors.aliyun.com kernel-ml-aufs/7/x86_64/primary_db | 9.5 MB 00:04:39 正在解決依賴關係 --> 正在檢查事務 ---> 軟件包 kernel-ml-aufs.x86_64.0.5.0.7-1.el7 將被 安裝 --> 解決依賴關係完成 依賴關係解決 =============================================================================================================================================================================================== Package 架構 版本 源 大小 =============================================================================================================================================================================================== 正在安裝: kernel-ml-aufs x86_64 5.0.7-1.el7 kernel-ml-aufs 46 M 事務概要 =============================================================================================================================================================================================== 安裝 1 軟件包 總下載量:46 M 安裝大小:235 M Downloading packages: kernel-ml-aufs-5.0.7-1.el7.x86_64.rpm | 46 MB 00:25:30 Running transaction check Running transaction test Transaction test succeeded Running transaction 正在安裝 : kernel-ml-aufs-5.0.7-1.el7.x86_64 1/1 驗證中 : kernel-ml-aufs-5.0.7-1.el7.x86_64 1/1 已安裝: kernel-ml-aufs.x86_64 0:5.0.7-1.el7 完畢! [root@service-3 ~]# vi /etc/default/grub GRUB_TIMEOUT=5 GRUB_DISTRIBUTOR="$(sed 's, release .*$,,g' /etc/system-release)" GRUB_DEFAULT=saved GRUB_DISABLE_SUBMENU=true GRUB_TERMINAL_OUTPUT="console" GRUB_CMDLINE_LINUX="crashkernel=auto rd.lvm.lv=centos/root rd.lvm.lv=centos/swap rhgb quiet" GRUB_DISABLE_RECOVERY="true" GRUB_DEFAULT=0 添加此行 [root@service-3 ~]# grub2-mkconfig -o /boot/grub2/grub.cfg Generating grub configuration file ... Found linux image: /boot/vmlinuz-5.0.7-1.el7.x86_64 Found initrd image: /boot/initramfs-5.0.7-1.el7.x86_64.img Found linux image: /boot/vmlinuz-3.10.0-862.el7.x86_64 Found initrd image: /boot/initramfs-3.10.0-862.el7.x86_64.img Found linux image: /boot/vmlinuz-0-rescue-f169d743559a49d98d5ff78bd9df15d8 Found initrd image: /boot/initramfs-0-rescue-f169d743559a49d98d5ff78bd9df15d8.img done [root@service-3 ~]# reboot [root@service-3 ~]# grep aufs /proc/filesystems nodev aufs
切換Docker默認的文件系統
[root@service-3 ~]# vi /etc/docker/daemon.json { "storage-driver" : "aufs" } [root@service-3 ~]# systemctl daemon-reload [root@service-3 ~]# systemctl restart docker
查看/var/lib/docker下另外一個aufs
[root@service-3 ~]# ls /var/lib/docker/aufs/ diff layers mnt
進入其中能夠看到3個目錄,其中mnt 爲aufs的掛載目錄,diff爲實際的數據來源,包括只讀層和讀寫層,因此這些層最終一塊兒被掛載mnt上的目錄,layers下爲與每一層依賴有關的層描述文件。
最初,mnt和layers都是空目錄,文件數據都在diff目錄下。一個Docker容器建立與啓動的過程當中,會在/var/lib/docker/aufs下面新建出對應的文件和目錄。因爲改版後,Docker鏡像管理部分與存儲驅動在設計上徹底分離了,鏡像層或者容器層在存儲驅動中擁有一個新的標示ID,在鏡像層(roLayer)中稱爲cacheID,容器層(mountedLayer)中mountID。在Unix環境下,mountID是隨機生成的並保存在mountedLayer的元數據mountID中,持久化在/var/lib/docker/image/aufs/layerdb/mount-id 中,Docker建立過程當中新建立過程當中新建立的讀寫層,下面以mountID
(1)分別在mnt和diff目錄下建立與該層的mountID同名的子文件夾
(2)在layers目錄下建立與該層的mountID同名的文件,用來記錄該層所依賴的全部的其餘層。
(3)若是參數中的parent項不爲空(這裏因爲是建立容器,parent就是鏡像的最上層),說明該層依賴於其餘的層。Graphdriver就須要將parent的mountID寫入到該層在layers下對mountID的文件裏。而後Graphdriver還須要在layers目錄下讀取與上述parent同mountID的文件,將parent層的全部依賴層也複製到這個新建立層對應的層描述文件中,這樣這個文件才記錄了該層的全部依賴。
隨後Graphdriver會將diff中所屬於容器鏡像的全部層目錄以只讀方式掛載到mnt下,而後diff中生成一個以當前容器對應的<mountID>-init 命名的文件夾做爲最後一層只讀層,能夠看到這個文件與這個容器內的環境息息相關,但不適合被打包做爲鏡像的文件內容,同時這些內容有不該該直接修改在宿主機文件上,全部Docker容器文件存儲中設計了mountID-init這麼一層單獨處理這些文件,這一層只在容器啓動時添加,並會根據系統環境和用戶配置自動生成具體內容(DNS配置等),只有當這些文件在運行過程當中被改後而且docker commit了纔會持久化修改,不然保存鏡像時不會包含這一層的內容
因此嚴格的說,Docker文件系統有3層,讀寫層(未來被commit的內容)、init層和只讀層,但這並不影響咱們傳統認識上可讀寫層+只讀層組成的容器文件系統:由於init層對用戶徹底透明的
接下來會在diff中生成一個容器對mountID爲名的可讀寫目錄,也掛載到mnt目錄下。因此,未來用戶在容器中新建文件就會出如今mnt下面mountID爲名的目錄,而該層對應的實際內容則保存在diff目錄下
至此咱們須要明確,全部文件的實際內容均保存在diff目錄下,包括讀寫層也會以mountID命名出如今diff目錄下,最終會整合到一塊兒聯合掛載到mnt目錄以mountID爲名的文件夾下
[root@service-3 ~]# docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES a939531b372f ubuntu "/bin/bash" 4 seconds ago Up 2 seconds serene_franklin 63943ce7e7ea ubuntu "/bin/sh" 26 seconds ago Up 24 seconds modest_antonelli [root@service-3 docker]# cat image/aufs/layerdb/mounts/a939531b372f0193e60f53ec3d4e5cd40e6987c8b0289597d39acba5900c4485/mount-id 2c493293b698eafbeddc84cc2160b3af4e913654cdf0c80c4fbe2d285ca208c1
查看該容器運行前對應的mnt目錄,看到對應mountID文件夾下是空的
[root@service-3 mnt]# du -h . --max-depth=1 | grep 2c493293b69 0 ./2c493293b698eafbeddc84cc2160b3af4e913654cdf0c80c4fbe2d285ca208c1-init 73M ./2c493293b698eafbeddc84cc2160b3af4e913654cdf0c80c4fbe2d285ca208c1
在容器添加1G的文件
[root@service-3 mnt]# docker exec -it a939531b3 /bin/bash root@a939531b372f:/# ls bin boot dev etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var root@a939531b372f:/# mkdir test root@a939531b372f:/# cd test/ root@a939531b372f:/test# ls root@a939531b372f:/test# dd if=/dev/zero of=test.txt bs=1M count=1024 1024+0 records in 1024+0 records out 1073741824 bytes (1.1 GB, 1.0 GiB) copied, 58.9444 s, 18.2 MB/s root@a939531b372f:/test# df -h Filesystem Size Used Avail Use% Mounted on none 17G 2.7G 15G 16% / tmpfs 64M 0 64M 0% /dev tmpfs 983M 0 983M 0% /sys/fs/cgroup /dev/mapper/centos-root 17G 2.7G 15G 16% /etc/hosts shm 64M 0 64M 0% /dev/shm tmpfs 983M 0 983M 0% /proc/asound tmpfs 983M 0 983M 0% /proc/acpi tmpfs 983M 0 983M 0% /proc/scsi tmpfs 983M 0 983M 0% /sys/firmware root@a939531b372f:/test# df -h test.txt Filesystem Size Used Avail Use% Mounted on none 17G 2.7G 15G 16% /
查看容器外文件變化
[root@service-3 mnt]# du -h . --max-depth=1 | grep 2c493293b69 0 ./2c493293b698eafbeddc84cc2160b3af4e913654cdf0c80c4fbe2d285ca208c1-init 1.1G ./2c493293b698eafbeddc84cc2160b3af4e913654cdf0c80c4fbe2d285ca208c1
在容器裏生成的對應文件出如今對應容器mountID文件夾中的root文件夾內,而當咱們中止容器時,mnt下相應mountID的目錄被卸載,而diff下相應文件夾中的文件依然存在。固然這僅限於當前宿主機,當須要遷移時須要從新作鏡像
最後,當咱們用docker commit把容器提交成鏡像時,就會在diff目錄下生成一個新的cacheID命名的文件,存放在最新的差別變化文件,這時一個新的鏡像層就誕生了,原來的一mountID爲名的文件夾已然存在,之間對應的容器被刪除
Device Mapper
Device Mapper是Linux2.6內核中提供的一種從邏輯設備到物理設備的映射框架機制,該機制下,用戶能夠很方便地根據本身的須要制定實現存儲資源的管理。
簡單來講,Docker Mapper包括3個概念:映射設備、映射表和目標設備。映射設備是內核向外提供的邏輯設備。一個映射設備經過一個映射表與多個目標設備映射起來,映射表包含了多個多元素,每一個多元素記錄了這個映射設備的起始地址、範圍與下一個目標設備的地址偏移量映射關係。目標設備能夠是物理設備,也能夠是一個映射設備,這個映射設備能夠繼續向下迭代。一個映射設備最終經過一個映射樹映射到物理設備上。Device Mapper本質功能就是根據映射關係描述IO處理規則,當映射設備接受到IO請求的時候,這個IO請求會根據映射表逐級準發,直到請求傳到最終物理設備上
Docker 下面的devicemapper存儲驅動是使用的Device Mapper的精簡配置和快照功能實現鏡像分層。這個模塊用了兩快設備(一個用於存儲數據,一個用於存儲元數據),並將其構建成一個資源池用於建立其餘存儲鏡像的塊設備。數據區爲生成其餘塊設備提供資源,元數據存儲了虛擬設備和物理設備的映射關係,Copy onWrite發送在塊存儲級別。devicemapper在構建一個資源池後,會先建立一個有文件系統的基礎設備,再經過從已有的設備建立快照的方式建立新設備,這些新設備的塊設備在寫入新內容以前並不會分配資源。全部的容器層和鏡像層都有本身的塊設備,都是經過其父鏡像建立快照的方法來建立;值得說明的是,devicemapper存儲驅動根據使用的兩個基礎塊設備是真正的塊設備和稀疏文件掛載的loop設備分爲兩種模式,前者稱爲direct-lvm模式,後者是Docker默認的loop-lvm模式。存儲方式不一樣致使二者性能差異很大。考慮到loop-lvm不須要額外配置的易用性,Docker將其做爲devicemapper的默認工做模式,生產推薦使用direct-lvm模式
與aufs同樣,若是Docker使用過devicemapper存儲驅動,在/var/lib/docker/下建立devicemapper以及image/devicemappe目錄,一樣image/devicemapper,也存儲鏡像和邏輯鏡像層的原數據信息。最終具體文件夾下有3個子文件,其中mnt爲設備掛目錄,devicemapper下存儲了loop-lvm模式下的兩個稀疏文件,metadata下存儲了每一個塊設備驅動層的元數據
overlay
一個 overlay 文件系統包含兩個文件系統,一個 upper 文件系統和一個 lower 文件系統,是一種新型的聯合文件系統。overlay是「覆蓋…上面」的意思,overlay文件系統則表示一個文件系統覆蓋在另外一個文件系統上面。爲了更好的展現 overlay 文件系統的原理,現新構建一個overlay文件系統。