聊一聊Docker構(gòu)建優(yōu)化解析
在本文中,我將介紹一些經(jīng)常被忽視的概念,這些概念將有助于優(yōu)化Docker鏡像開發(fā)和構(gòu)建過程。
讓我們從Docker構(gòu)建過程的簡短描述開始。這是通過使用Docker CLI工具運(yùn)行docker build命令觸發(fā)的過程。
docker build命令根據(jù)Dockerfile的文件中指定的指令構(gòu)建Docker鏡像。Dockerfile是一個(gè)文本文檔,其中包含用戶在命令行上調(diào)用以組裝映像的所有有序命令。
Docker鏡像由只讀層組成。每層代表一個(gè)Dockerfile指令。這些層是堆疊在一起的,每個(gè)層都是上一層的變化的增量。通??梢哉J(rèn)為這些層是緩存的一種形式。僅對更改的層進(jìn)行更新,而不是對每個(gè)更改進(jìn)行更新。
下面的示例描述了Dockerfile的內(nèi)容:
- FROM registry.docker.com/baseimg/centos7-jdk8:latest
- MAINTAINER Luga "luga_sx@outofmemory.cn";
- RUN mkdir -p /tools/apps/{microserice}
- RUN mkdir -p /tools/apps/{microserice}/cache
- ADD {microserice}.jar /tools/apps/{microserice}/{microserice}.jar
- EXPOSE 9999
- ENV TZ 'Asia/Shanghai'
- ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom -Djava.awt.headless=true -Denv=DEV -Dapollo.cluster=DEFAULT -Dspring.profiles.active=prm -Dfile.encoding=utf-8","-jar","/tools/apps/{microserice}/{microserice}.jar"]
該文件中的每條指令代表Docker鏡像中的單獨(dú)一層。以下是每條指令的簡要說明:
- FROM:從JDK創(chuàng)建一個(gè)層Docker鏡像,(此處的鏡像非Docker Hub上面直接拉取,而是基于源碼自定義制作)
- COPY:從Docker客戶端的當(dāng)前目錄添加文件
- RUN:使用make構(gòu)建您的應(yīng)用程序
- CMD:指定在容器中運(yùn)行什么命令
基于上述命令行,在構(gòu)建過程中執(zhí)行上述命令時(shí),將在Docker鏡像中創(chuàng)建層,一個(gè)完整的Docker鏡像將由此誕生。然而,在實(shí)際的項(xiàng)目活動中,我們需要從性能、穩(wěn)定性、安全性等等方面對我們所創(chuàng)建的Docker鏡像進(jìn)行不斷的調(diào)整、優(yōu)化,以滿足業(yè)務(wù)場景需求。
針對Docker的構(gòu)建過程,我想分享一些優(yōu)化建議,以幫助有效地構(gòu)建鏡像:
臨時(shí)容器
Dockerfile定義的鏡像會生成短暫的容器。在這種情況下,臨時(shí)容器是指可以停放并銷毀,然后重建的容器,并使用絕對最小的設(shè)置和配置替換為新生成的容器。臨時(shí)容器可以認(rèn)為是一次性的。每個(gè)實(shí)例都是新的,并且與以前的容器實(shí)例無關(guān)。在開發(fā)Docker鏡像時(shí),我們應(yīng)該利用盡可能多的臨時(shí)模式。
減少不必要的軟件包
盡量避免安裝不必要的文件和軟件包。Docker鏡像應(yīng)保持精簡。這有助于提高可移植性,縮短構(gòu)建時(shí)間,降低復(fù)雜性并減小文件大小。例如,在大多數(shù)情況下,不需要在容器上安裝文本編輯器。不要安裝任何非必需的應(yīng)用程序或服務(wù)。
實(shí)現(xiàn).dockerignore文件
.dockerignore文件排除與在其中聲明的模式匹配的文件和目錄。這有助于避免將不必要的大文件或敏感文件和目錄發(fā)送到守護(hù)程序,并避免將它們添加到公共鏡像。
要在不重構(gòu)源存儲庫的情況下排除與構(gòu)建無關(guān)的文件,請使用.dockerignore文件。該文件支持類似于.gitignore文件的排除模式。
排序多行參數(shù)
盡可能通過字母數(shù)字排序多行參數(shù)來簡化以后的更改。這有助于避免軟件包重復(fù),并使列表更易于更新。
解耦應(yīng)用
依賴于其他應(yīng)用程序的應(yīng)用程序被視為“已耦合”。在某些情況下,它們托管在同一主機(jī)或計(jì)算節(jié)點(diǎn)上。這在非容器部署中很常見,但對于微服務(wù),每個(gè)應(yīng)用程序應(yīng)存在于其自己的單獨(dú)容器中。將應(yīng)用程序解耦到多個(gè)容器中,可以更輕松地水平縮放和重用容器。例如,一個(gè)解耦的Web應(yīng)用程序堆??赡馨齻€(gè)單獨(dú)的容器,每個(gè)容器都有自己的唯一鏡像:一個(gè)用于管理Web應(yīng)用程序,一個(gè)用于管理數(shù)據(jù)庫的容器以及一個(gè)用于內(nèi)存中緩存的容器。將每個(gè)容器限制為一個(gè)進(jìn)程是一個(gè)很好的經(jīng)驗(yàn)法則。根據(jù)業(yè)務(wù)規(guī)則,使容器保持清潔和模塊化。然后,如果容器相互依賴,則可以使用Docker容器網(wǎng)絡(luò)來確保這些容器可以通信。
最小化層數(shù)
僅使用RUN、COPY和ADD等指令即可創(chuàng)建圖層。其他指令僅僅是創(chuàng)建臨時(shí)的中間鏡像,并且最終不會增加構(gòu)建的大小。在可能的情況下,我們可以在構(gòu)建過程中包含其他工具或者調(diào)試信息,而無需增加最終鏡像的大小。
利用構(gòu)建緩存
在構(gòu)建鏡像時(shí),Docker會逐步執(zhí)行Dockerfile中的指令,并按順序執(zhí)行每個(gè)指令。在每條指令中,Docker都會在其緩存中搜索要使用的現(xiàn)有鏡像,而不是創(chuàng)建新的重復(fù)鏡像。
Docker鏡像通常在構(gòu)建的過程中遵循以下基本規(guī)則:
1、從已在緩存中的父鏡像開始,將下一條指令與從該基本鏡像派生的所有子鏡像進(jìn)行比較,以查看是否其中一個(gè)是使用完全相同的指令構(gòu)建的。如果不是,則高速緩存無效。在大多數(shù)情況下,僅將Dockerfile中的指令與子鏡像之一進(jìn)行比較就足夠。
2、對于ADD和COPY指令,將檢查鏡像中文件的內(nèi)容,并為每個(gè)文件計(jì)算一個(gè)校驗(yàn)標(biāo)識。在這些校驗(yàn)標(biāo)識中通常不考慮文件的最后修改時(shí)間和最后訪問時(shí)間。在緩存查找期間,將校驗(yàn)標(biāo)識與現(xiàn)有鏡像中的進(jìn)行比較。如果文件中的任何內(nèi)容(例如內(nèi)容和元數(shù)據(jù))發(fā)生了更改,則緩存將無效。
3、除了ADD和COPY命令外,緩存檢查不會查看容器中的文件來確定緩存是否匹配。例如,在處理RUN apt-get -y update命令時(shí),不會檢查容器中更新的文件以確定是否存在緩存命中。在這種情況下,命令字符串用于查找匹配項(xiàng)。
4、緩存無效后,所有后續(xù)Dockerfile命令都會生成新鏡像,并且不使用緩存。
在CI管道中優(yōu)化Docker鏡像構(gòu)建
前面幾節(jié)中提到的所有優(yōu)化概念對于在CI管道中實(shí)施都是有效的。特別是緩存。如果Dockerfile發(fā)生了變化,那么利用緩存仍然是減少構(gòu)建時(shí)間的最佳方法。作為CI管道的一部分,這是如何工作的?當(dāng)使用Docker執(zhí)行器作為構(gòu)建作業(yè)的運(yùn)行時(shí),可以利用稱為Docker層緩存(DLC)的功能來加快構(gòu)建速度。
當(dāng)構(gòu)建Docker鏡像是CI流程的常規(guī)部分時(shí),DLC是一項(xiàng)很不錯的功能。DLC將保存在作業(yè)中創(chuàng)建的鏡像層。DLC會緩存在工作期間構(gòu)建的任何Docker鏡像的各個(gè)層,然后在后續(xù)的CircleCI運(yùn)行中重用未更改的鏡像層,而不是每次都重新構(gòu)建整個(gè)鏡像。
Dockerfile提交的次數(shù)越少,鏡像構(gòu)建步驟將運(yùn)行得越快。DLC可以與機(jī)器執(zhí)行程序和遠(yuǎn)程Docker環(huán)境(setup_remote_docker)一起使用。重要的是要注意,DLC僅在使用docker build,docker compose或類似的Docker命令創(chuàng)建自己的Docker鏡像時(shí)有用,它不會減少所有構(gòu)建啟動初始環(huán)境所花費(fèi)的時(shí)間。