縮減Docker映象體積歷程總結

weixin_33797791發表於2019-02-06

容器化的過程中總是免不了要構建映象,一個體積更小的映象除了能夠節省機器的磁碟空間之外,還能夠提升傳輸效率。這篇文章主要是想講述一下自己在優化映象體積時所採取的措施,當然並不是所有方案都對減少映象體積有明顯效果,具體專案還要具體分析。這篇文章我以Rails專案的映象構建作為例子。

為什麼構建出來的映象這麼大?

在優化映象大小之前首先要知道為何我們所構建的映象會這麼大?下面是我專案中用於構建映象的Dockerfile檔案

FROM ruby:2.5.3

RUN apt-get update -y && apt-get install -y \
        build-essential \
	    imagemagick \
        default-libmysqlclient-dev
RUN apt-get install -y \
        nodejs \
        yarn
RUN rm -rf /var/lib/apt/lists/*

WORKDIR /beansmile-web
COPY . /beansmile-web

RUN bundle install
複製程式碼

映象檔案我定義得比較隨意,它所構建出的映象資訊如下

web1                 latest               1a8a32d5253a        9 hours ago         1.26GB
複製程式碼

構建的映象的過程跟平常基於一個作業系統打造供專案執行基礎環境的過程差不多。只是日常的作業系統通常都不只一個專案在執行,因此係統裡所包含的東西是比較全面的。而映象只期望提供給特定的專案使用,因此所依賴的東西比較有針對性,不必要的東西儘量不要加進去。

針對上面的Dockerfile檔案我覺得有以下幾個優化方向

  1. 基礎的ruby:2.5.3是基於buildpack-deps來構建的其中包含大量額外的軟體包,或許用更輕量級的映象來作為基礎映象能夠進一步縮減空間。
  2. 檔案中有太多的RUN命令,每一條命令都會疊加層數,可能會造成體積的變大。
  3. 把整個專案目錄都拷貝到映象中,並不是個好主意,隨著專案的增大,Rails專案中的public目錄下可能會存在著圖片之類的靜態資源,把這些東西打包到映象中意義不大。
  4. 是否能夠採用multi-stage的構建方式,把一部分不那麼緊要的資源丟棄,讓最終映象體積更小?

下面一條條來分析。

具體分析

方向1: 基於更小的作業系統

前面的例子最終構建出來的映象體積十分龐大,主要歸咎於相關的基礎映象本身就很大。

REPOSITORY          TAG                  IMAGE ID            CREATED             SIZE
ruby                2.5.3                60c3a1518797        3 weeks ago         871MB
web1                latest               1a8a32d5253a        9 hours ago         1.26GB
複製程式碼

可見我們的基礎Ruby映象本身就800多M了,構建映象的過程還需要安裝依賴,導致了最終的web映象體積會達到1.26G。這個體積可不利於網路傳輸,官方所提供的Ruby基礎映象有許多個版本,除了Ruby本身的版本不同之外,還有許多基於不同作業系統所構建的基礎映象可以選擇,而這些不同的作業系統所構建出來的Ruby基礎映象的體積相差甚大

REPOSITORY          TAG                  IMAGE ID            CREATED             SIZE
ruby                2.5.3-slim-stretch   20132a4ab93d        2 weeks ago         129MB
ruby                2.5.3                60c3a1518797        3 weeks ago         871MB
ruby                2.5.3-alpine         b3361f13ff1f        3 weeks ago         43.6MB
複製程式碼

基於alpine作業系統的Ruby映象是最迷你的,只有43.6MB。slim-stretch也是個不錯的選擇。或許採用更輕量級的映象將會是一個優化的契機。

經驗小貼士: 從我自己的構建經驗來看,採用slim-stretch或許會是更加親民的選擇,它是Debian系,包管理器跟ubuntu是一樣的都是用apt-get,用慣ubuntu的人肯定會覺得比較親切。alpine所用的包管理器是apk(是不是想到安卓的安裝包?),一些常用包的命名有點不太一樣需要自己慢慢去解決。*

不過無論用哪種方案都避免不了時間的投入,網上也沒那麼多現成的解決方案,迷你映象的話你不得不自己安裝一些構建過程中所依賴的軟體。

方向2: 縮減映象的層數

Docker官網對映象的說法是,它是由一層層的只讀層組成的,層次越少映象的效能表現越出眾。這也是官方建議我們採用特定基礎映象去構建自己的專案映象,而不是基於一個赤裸裸的作業系統映象(如Ubuntu映象)的原因。

上述的例子中我們用了三個RUN命令,這會無意中多構建了兩個層,其實我們可以把它合併成一條RUN命令

RUN apt-get update -y && apt-get install -y \
        build-essential \
	    imagemagick \
        default-libmysqlclient-dev \
        nodejs \
        yarn \
        && rm -rf /var/lib/apt/lists/*
複製程式碼

基於這個改動重新建立一個映象web2

REPOSITORY          TAG                  IMAGE ID            CREATED             SIZE
web2                latest               221a316a6903        14 minutes ago      1.25GB
web1                latest               1a8a32d5253a        9 hours ago         1.26GB
複製程式碼

可見這種改動對於縮減映象體積效果並不明顯

官方的說法是這樣的

In older versions of Docker, it was important that you minimized the number of layers in your images to ensure they were performant.

我們可以得出結論,或許縮減層數主要是為了讓映象操作起來更高效吧,減少層數這個優化方向對於縮減映象體積並沒有多大的幫助,不過我們這樣做還是有好處的。

方向3: 忽略一些檔案

從上面的配置可以看出,為了方便映象的構建我直接把整個專案都移動到映象中去(COPY命令)。然而對於構建的映象而言,並不是所有的檔案我們都應該關心,最為值得關心的應該只有原始碼部分。所以我預想著在構建的映象中可以把以下的目錄剔除掉

  • public/: 用於存放一些靜態檔案的目錄,如果其中包含大量像圖片這樣的資源的話會對映象的體積有較大的影響。
  • tmp/: 用於存放一些快取資源,專案程式檔案等等,這些檔案對於映象而言用處不大。
  • log/: 用於存放日誌相關的資訊。

PS: 當然每個人對實際專案的考量會有所不同,這幾個目錄只是根據我個人的專案情況所做的決定,並不具有通用性。

要忽略這些檔案,我們採用一個名為.dockerignore的檔案,把它放在當前的目錄下即可,它的寫法跟.gitignore檔案很相似,內容大概如下

/public/**
/tmp/**
/log/**
複製程式碼

然後重新構建映象

web3                latest               fb13cc1301b2        About a minute ago   1.2GB
web2                latest               221a316a6903        23 hours ago         1.25GB
web1                latest               1a8a32d5253a        33 hours ago         1.26GB
複製程式碼

這種方式的影響也不怎麼大,這是因為目前我本地這些目錄下所包含的“垃圾”資源所佔的比重較小。

方向4: multi-stage方案

這個是官方推薦的方案,在Docker17.05之後可以使用

In Docker 17.05 and higher, you can do multi-stage builds and only copy the artifacts you need into the final image. This allows you to include tools and debug information in your intermediate build stages without increasing the size of the final image.

好像看起來有點複雜,不過它的原理大概就是先使用一個體積較大,依賴較為齊全的映象來構建所需要的資源,然後把這些資源複製到一個輕量的基礎映象中,並繼續我們的映象構建工作,這樣就可以把原先龐大的基礎映象給拋棄了。這種做法能避免我們最終的映象中包含了一堆無用的依賴,在某種程度上能夠減少最終映象的體積。

這看起來是個很不錯的策略,我也在專案中進行了嘗試。我們決定把bundle依賴包的安裝以及,靜態檔案的編譯都放到一個功能完備的基礎映象中去完成,然後把所需要的資源拷貝到一個輕量級的基礎映象中(類似alpine這種輕量級系統的相關映象)再繼續完成構建步驟。

不過我構建過程中遇到如下問題

  • 用bundle安裝依賴的過程中不僅僅涉及到ruby程式碼的引入,mysql2nokogiri這些第三方庫除了會引入Ruby程式碼之外還會在安裝的時候進行編譯,並生成一些共享庫,如果把依賴資源從一個映象拷貝到另一個映象的話除了要拷貝bundle相關目錄下的ruby程式碼之外,還不得不拷貝這些第三方庫所依賴的共享庫,這比想象中要麻煩。
  • 我們期望在一個映象裡面完成靜態檔案的構建,那麼我們便可以在最終映象中免去了安裝nodejs, yarn這些用於編譯靜態資源相關的依賴了。不過後來還是覺得這種方案不太適用。一方面,安裝了nodejs與沒有安裝nodejs的映象差別也就是30M左右,另一方面,要執行bin/rails c需要依賴JS執行時,這無論對於開發還是生產都是一個比較重要的操作,因此在最終映象中捨棄JS執行時並不是個好主意。

最終構建

前面提到了4個優化的方向,但似乎最終只有

  • 採用更輕量級的作業系統的相關基礎映象來進行構建。
  • multi-stage。

對最終的映象體積影響較大。考慮到multi-stage的解決方案所帶來的好處可能還不如麻煩來得多,因此最終還是捨棄了這個方案,與其這樣繞來繞去還不如直接採用最精簡的ruby:2.5.3-alpine作為基礎映象來打造自己的專案映象。選擇一個精簡的作業系統最大的問題就是在構建專案映象過程中的所有基礎依賴都得自己一個個去解決,要投入不少的時間和精力,以下是我經過反覆測試所得到的Dockerfile檔案(僅供參考,畢竟你的專案所依賴的東西可能有所不同)

FROM ruby:2.5.3-alpine

RUN apk --update --upgrade add \
        # bundle 安裝相關的依賴
        git \
        curl \
        # mysql2 依賴
        mysql-dev \
        # 基礎設施,比如gcc相關的東西
        build-base \
        # nokogiri 相關依賴
        libxslt-dev \
        libxml2-dev \
        # 圖片處理相關依賴
        imagemagick \
        # tz相關,如果沒有bundle的時候會報錯
        tzdata \
        nodejs \
        yarn \
        && rm -rf /var/cache/apk/*

WORKDIR /beansmile-web
COPY . /beansmile-web/
RUN bundle install
複製程式碼

構建出來的映象如下

web4                latest               71b75128d0d9        14 hours ago         586MB
複製程式碼

與之前的映象相比體積大幅度減少了。這是一個我們可以接受的大小了,考慮到時間成本就不進一步壓縮了。

總結

這篇文章主要簡單總結了個人在縮減Rails專案映象方面的探究。為了縮減映象體積提出了4個主要的優化方向,用迷你的作業系統構建映象的方式來減少映象的體積的方式十分有效。不過不同型別,基於不同語言的專案可能會有不同的側重點,不能一概而論,可能有的專案中multi-stage會幫你省下更多的時間。

相關文章