歡迎光臨
每天分享高質量文章

把docker映象當作桌面系統來用

來自:zasdfgbnm

zasdfgbnm.github.io/2017/12/28/把docker映象當作桌面系統來用/

博主一直都很喜歡思考怎樣管理裝在自己電腦上的桌面系統,這篇算是前作能當主力,能入虛擬機器,還能隨時打包帶走,Linux就是這麼強大的後續探索吧。

近些年來,Docker由於提供了一套非常方便地建立並執行應用容器的方法,而在全球掀起了一股容器化的熱潮。容器透過將軟體及其所需要的執行環境一同打包帶走,從而將人們從依賴的苦海中拯救出來。雖然Docker設計的初衷並不是作業系統容器,更不是一個直接執行在裸機上的作業系統,但是docker這套強大的工具也會給我們管理作業系統帶來巨大的便利。

為什麼要用Docker映象當作桌面系統?這就要從普通桌面系統的不方便之處說起。通常我們都擁有不止一臺電腦,我們希望這些電腦能夠保持一致。這裡所說的“一致”,用一個例子來講,就是我在一臺電腦上編輯了一半的檔案,不需要認為複製到另一臺電腦上,而是直接開啟電腦就能編輯。如果這個檔案只是一個純文字檔案,或者一個Microsoft Word檔案,那麼實現這個一致性非常簡單:把檔案扔到Dropbox之類的雲同步盤就好。然而對於專業使用者來講,這種一致性的保持並非單純的扔到Dropbox裡面那麼簡單:比如說你最近忙於一個專案,這個專案要用到若干程式語言,然後在電腦裡裝了一堆庫,一堆工具軟體,有圖形介面的,也有命令列的。在工作的過程中,你有可能不斷安裝新的工具,或者決定棄用某個之前計劃使用的庫或者工具。要讓你的工作在你的若干臺電腦上都能工作,就要一直維護不同機器的環境的一致性:在一臺機器上安裝的工具,要在所有機器上重新安裝一遍。在一臺機器上升級了的庫,要在所有機器上都升級,稍微有所差池,就有可能出現某個指令碼/程式在一臺機器上跑的好好的,在另一臺機器上卻無法執行的問題。

docker哲學

不熟悉docker的讀者可以戳這裡來瞭解docker。Docker的使用非常簡單:我們透過寫一個Dockerfile,在Dockerfile中寫入相應的命令來安裝以及配置我們想要的庫跟工具。不熟悉docker的讀者可以看一下下麵這個抄來的Dockerfile的例子,來瞭解一下Dockerfile長啥樣子:

FROM ubuntu

MAINTAINER Kimbro Staken

RUN aptget instally softwarepropertiescommon python

RUN addaptrepository ppa:chrislea/node.js

RUN echo “deb http://us.archive.ubuntu.com/ubuntu/ precise universe” >> /etc/apt/sources.list

RUN aptget update

RUN aptget instally nodejs

#RUN apt-get install -y nodejs=0.6.12~dfsg1-1ubuntu1

RUN mkdir /var/www

ADD app.js /var/www/app.js

CMD [“/usr/bin/node”, “/var/www/app.js”]

有了Dockerfile,只需要docker build一條命令就可以建立一個docker映象。同時Docker公司提供一個叫做DockerHub的服務,可以免費託管公開映象。只需要使用docker push就可以直接把映象上傳到DockerHub。在不同的電腦上只需要docker pull就可以從DockerHub獲取最新版的映象。DockerHub還支援自動構建,透過把DockerHub帳號跟GitHub帳號關聯起來,就可以讓DockerHub在GitHub上面的Dockerfile出現更改的時候自動重新生成映象。

本文開頭所說的這種一致性的維護,docker實際上已經在給我們提供答案了:我們透過構建一個docker映象,讓這個映象包含著我們專案所需要的所有的一切。這樣的話,我們開發,測試,部署等等一切任務,都可以在先用docker run來開啟一個容器,然後在容裡面進行所有的工作。當我們決定修改執行環境,比如引入新的庫的時候,就在Dockerfile中進行相應的修改,重新生成映象,然後在不同的機器上用docker pull來更新一下就好。這種使用哲學,透過一個中心化了的倉庫,非常優雅地解決了不同機器上環境一致的問題。美中不足的是,並不是所有的程式都能在容器裡執行的,也並不是所有的程式都方便在容器裡執行的。如果你用到了圖形介面的程式,或者說是一些系統級別的程式,那麼在容器裡面使用這些程式會麻煩很多,有的甚至根本無法實現。於是自然地就會想到,如果我們能夠在每次開機的時候,直接把某個docker生成的映象掛載起來當根目錄來使用,就可以讓這個映象直接在裸機上(而不是在容器中)執行,來做我們的日常桌面系統了。

這種做法,除了在保持一致性方面帶來的便利以外,還有一些其他的好處:

  • 整個系統儲存在雲端,本地的內容僅僅是雲端的一份快取而已,這樣就完全沒有必要定期對系統進行備份了。

  • 你的系統是如何從零開始一步一步配置成你想要的樣子的,所有的一切都清晰地展現在Dockerfile裡面。Dockerfile就是你最好的筆記。

  • 完全不用擔心繫統長時間使用產生一些殘餘的垃圾檔案、或者某些系統中某些程式的資料出現損壞,因為每次開機,我們用的都是一個全新的系統。

  • 要裝一臺新機器,並不需要從頭安裝作業系統,只要從DockerHub拉取映象拿來用就好,安裝系統這個過程變得極其方便。

  • 系統更新的過程實際上就是根據Dockerfile從最新的軟體倉庫重新從頭安裝生成docker映象的過程,不會出現某些更新遇到檔案衝突或者依賴無法處理,需要人為幹預才能完成的問題。

docker儲存驅動的工作原理

Docker的儲存驅動官方有介紹其工作原理,這裡只是簡單概括一下。Docker使用了層的概念,docker在構建映象的時候,會逐行執行我們的Dockerfile中的每一行,每執行一行的時候,docker就會創建出一個新的層來存放新的內容。當我們執行docker pull或者docker push的時候,docker實際上傳跟下載的是這些層之間的增量。每當執行docker run,docker就會把這些下載下來的層組合到一起,組合成一個完整的映象,然後新建一個讀寫層,所有執行過程中的寫入都會被寫入到讀寫層中,而映象本身則是保持只讀,不會被更改。“層”這個概念具體實現起來,根據docker目錄(通常為/var/lib/docker這個目錄)所在的檔案系統的不同而不同,具體的實現在docker中被稱為graph driver,docker自帶的graph driver包括aufs、 overlay、btrfs、zfs、devicemapper等。這些graph driver大多使用了寫時複製的技術,這樣在把各個層組合在一起的過程不需要重新複製一份資料,實際的複製是在寫入的時候發生的。

由於筆者使用的是btrfs,所以本文就以btrfs為例子來介紹怎麼讓系統啟動到docker映象上去。btrfs是一個寫時複製的系統,由於docker的映象是由一個一個的層疊在一起組成的,docker在使用btrfs的時候,每往上疊一層,docker就會建立一個原來層的快照,然後把新層的內容寫到快照裡面去。然後docker會在從映象建立容器的時候,給映象的最頂層做個快照,把這個快照當作容器讀寫層來用。

啟動到docker映象中去

明白了docker儲存驅動的工作原理,還需要知道Linux的啟動過程才能達成我們的標的。Linux在啟動的時候,一般會讓啟動器給核心裝載一個記憶體盤initramfs,然後核心完成簡單的早期初始化以後,就會解壓記憶體盤的內容到根目錄/,然後啟動記憶體盤中的init程式(一般為/init),這個init程式會進行進一步的初始化(比如說載入檔案系統的驅動,對檔案系統進行fsck等),這一步初始化完成了以後,這個init程式就會根據核心選項中的rootrootflags等內容掛載真正的根目錄,然後透過switch_root程式啟動真正根目錄中的init程式,這個init程式則會完成最後的初始化工作,比如掛載fstab、載入圖形介面等等。很多發行版都提供製作initramfs的工具,比如archlinux的mkinitcpio,這些工具通常都是模組化的,允許使用者自己新增hook。

讓系統啟動到docker映象所需要的知識已經完備了。思路也清晰了:透過給initramfs中新增hook,讓initramfs中的init在掛載root之前從docker本地快取中的映象中創建出一個快照作為讀寫層,然後把這個讀寫層當作真正的root來掛載。具體操作上,在啟動管理器裡面寫啟動項的核心選項的時候,root就寫/var/lib/docker所在的分割槽,而rootflags裡面至少要有一項subvol=XXXXX,其中XXXXX是我們打算建立的讀寫層的位置。然後重中之重則是,寫一個hook,這個hook乾的事情是:找到想要的docker映象對應的btrfs子捲,給這個子捲建立一個快照,命名為XXXXX(跟核心選項中的名字保持一致)。這樣的話,在Linux把控制權交給initramfs中的init程式以後,init程式會先去從docker快取中的子捲創造出XXXXX快照,然後把XXXXX快照當作root來掛載以及進行接下來的操作。如果讀者跟筆者一樣使用Arch Linux的話,那麼所有的這些工作筆者已經做了,讀者可以直接拿來用。

筆者的原始碼位於GitHub: https://github.com/zasdfgbnm/mkinitcpio-docker-hooks ,同時讀者也可以直接從AUR中搜索mkinitcpio-docker-hooks來安裝筆者的hook。下麵就來介紹一下這個hook的使用方法。

mkinitcpio-docker-hooks的使用

mkinitcpio-docker-hooks的使用流程大概分為如下幾步:

  1. 確保你的/var/lib/docker位於某個btrfs分割槽中

  2. 準備一個適合在裸機上啟動的docker映象

  3. 然後在這個映象中安裝並配置mkinitcpio-docker-hooks

  4. 準備核心跟initramfs

  5. 準備top layer的內容

  6. 設定啟動管理器

docker映象的準備

要想啟動到docker映象中去,首先你得有一個適合在裸機上啟動的docker映象。很多docker映象,為了減小映象的大小,是不會附帶只有裸機上才能用到的軟體包(比如dhcpcd)的,所以讀者可能需要在Dockerfile中手動安裝這些軟體包,對於Arch Linux來講,只需要安裝base組就可以了。由於接下來要安裝mkinitcpio-docker-hooks,這裡推薦使用一個已經內建yaourt的映象,筆者使用的是自己的archlinux-yaourt映象zasdfgbnm/archlinux-yaourt。所以,Dockerfile的開頭看起來是這個樣子的:

FROM zasdfgbnm/archlinuxyaourt

USER root

RUN pacmanSyunoconfirm base


作為示例,這裡就不安裝base之外的其他軟體了,讀者請自己根據需要安裝其他軟體。

安裝並配置mkinitcpio-docker-hooks

mkinitcpio-docker-hooks的安裝是在docker裡面而不是當前執行在裸機上的系統中進行的。之所以要把這個軟體包安裝在docker映象裡面,很重要的原因是因為Linux核心不提供ABI的穩定性,所以核心模組跟內核的版本必須嚴格對應,不然模組是無法載入的。為了保持這種一致性,我們採取的措施是,在docker裡面安裝mkinitcpio-docker-hooks,在docker中生成initramfs,並且在啟動的時候用映象裡面的核心啟動,就可以確保核心、initramfs中的模組、啟動到映象以後的/lib/modules三者保持一致。安裝過程在Dockerfile中的寫法如下:

RUN sudo -u user yaourt -S –noconfirm mkinitcpio-docker-hooks


安裝完了mkinitcpio-docker-hooks以後還需要配置,配置文在/etc/docker-btrfs.json,初始內容如下:

{

    “docker_image”: “archlinux/base”,

    “docker_tag”: “latest”

}

我們需要做的就是把這兩個變數的值替換為我們想要的值,比如說這裡我打算把我的docker映象的名字叫做“sample_image”。同時,我們還需要在/etc/mkinitcpio.conf中新增docker-btrfs這個hook。綜上,可以在Dockerfile中使用如下命令來配置:


RUN sedi ‘s/archlinux/base/sample_image/g’ /etc/dockerbtrfs.json

RUN perlipe ‘s/(?<=^HOOKS=()(.*)(?=()/$1 docker-btrfs/g’ /etc/mkinitcpio.conf


??????????????Dockerfile??


FROM zasdfgbnm/archlinuxyaourt

USER root

RUN pacmanSyunoconfirm base

RUN sudou user yaourtSnoconfirm mkinitcpiodockerhooks

RUN sedi ‘s/archlinux/base/sample_image/g’ /etc/dockerbtrfs.json

RUN perlipe ‘s/(?<=^HOOKS=()(.*)(?=))/$1 docker-btrfs/g’ /etc/mkinitcpio.conf


只需要透過docker build . -t sample_image就可以構建自己的映象了。

準備核心跟initramfs

映象生成好了以後,下一步就是準備核心跟構建initramfs了。註意這一步操作儘量在你打算用來啟動docker映象的機器上進行,因為mkinitcpio會自動根據機器把相應的核心模組放入initramfs中去,如果在別的機器上進行的話,那就可能會有一些驅動沒有被自動裝入initramfs中。如前所述,這一步的工作是在docker容器中進行的。首先執行容器並開啟一個shell:

docker run –v $(pwd):/workspace -w /workspace -it sample_image bash


然後在容器中執行如下命令:


mkinitcpiop linux

cp /boot/* .

exit

然後就可以在當前目錄下看到準備好的initramfs-linux-fallback.imginitramfs-linux.img以及vmlinuz-linux了。

準備top layer的內容

Top layer是mkinitcpio-docker-hooks引入的新概念,它指的是某個驅動器中的某個目錄,這個目錄會在啟動的時候讀寫層建立之後,真正的root掛載之前,透過busybox的cp -a命令整個複製到讀寫層裡面去。為什麼需要top layer呢?因為我們需要在多臺機器上啟動同一個映象,而不同機器上的往往會根據需要配置不同的配置檔案,比如/etc/fstab以及/etc/X11/xorg.conf。另外,DockerHub免費賬戶上的映象都是公開的,/etc/passwd/etc/shadow等的私密性檔案也不適合放在映象裡面儲存。

準備top layer的內容,實際上就是找一個檔案夾,把需要單獨配置的檔案,按照從根目錄算起的相對路徑存放在這個檔案夾裡面。比如說如果你想給某臺機器單獨配置/etc/fstab,那麼你就應該在top layer的目錄中新增etc/fstab這個檔案。這裡推薦的具體的操作流程是:首先透過docker run -v $(pwd):/workspace -w /workspace -it sample_image bash進入容器中的shell,然後在其中做各種配置,比如useradd ...,完了以後把新生成的配置檔案複製到top layer的檔案夾中去

設定啟動管理器

設定好了top layer以後,我們基本上可以算是萬事具備只差東風了。我們只需要簡單設定一下啟動管理器就可以啟動我們的系統了。這裡以refind為例子,這裡假設我們的所有東西放在一個label為“linux”的btrfs分割槽中,docker目錄(也就是你係統啟動以後會掛載到/var/lib/docker的目錄)存放在這個分割槽根目錄下的一個叫做“docker”的子捲中,而核心、initramfs以及top layer都是位於分割槽根目錄下的“boot_docker”檔案夾中,而我們希望建立的讀寫層名字叫做“docker_rwlayer”,那麼相應的refind.conf中的選單項的程式碼如下:

menuentry archlinuxdocker {

        icon EFI/refind/icons/os_arch.png

        volume linux

        loader boot_docker/vmlinuzlinux

        initrd boot_docker/initramfslinux.img

        options “root=LABEL=linux rootflags=subvol=docker_rwlayer rw docker_path=docker toplayer=LABEL=linux toplayer_path=boot_docker”

}

其中核心選項中,我們透過root來指定docker目錄所在的分割槽,rootflags中的subvol來指定讀寫層的位置,docker_path來指定docker目錄在root中的相對位置;透過toplayer來指定top layer目錄所在的分割槽,toplayerflags來指定top layer所在分割槽的掛載選項,toplayer_path來指定top layer的目錄在toplayer分割槽中的相對位置。

一切就緒,重啟並享受吧!

另外,有興趣的讀者可以看一下筆者自用的docker映象作為參考:
https://cloud.docker.com/swarm/zasdfgbnmsystem/repository/docker/zasdfgbnmsystem/archlinux-kde/general
https://github.com/zasdfgbnm-dockers/archlinux-kde


●本文編號419,以後想閱讀這篇文章直接輸入419即可

●輸入m獲取到文章目錄

推薦↓↓↓

運維

更多推薦18個技術類公眾微信

涵蓋:程式人生、演演算法與資料結構、駭客技術與網路安全、大資料技術、前端開發、Java、Python、Web開發、安卓開發、iOS開發、C/C++、.NET、Linux、資料庫、運維等。

贊(0)

分享創造快樂