Categories
程式開發

用Alpine會讓Python Docker的構建慢50倍


用Alpine會讓Python Docker的構建慢50倍 1

當你為Docker鏡像選擇基礎鏡像時,Alpine Linux可能被推薦。有人告訴你,用Alpine將使你的鏡像更小,並能加快你的builds。如果你正在用Go,這無疑是個合理的建議。

但如果你使用Python,Alpine Linux會經常:

  1. 讓你的構建更慢
  2. 讓你的鏡像更大
  3. 浪費你的時間
  4. 偶爾引入一些令人費解的運行時Bug

讓我們看看為什麼人們推薦使用Alpine,以及為什麼不應該在Python應用程序中使用它。

為什麼人們推薦使用Alpine

假設我們需要安裝gcc作為鏡像構建的一部分,並且我們想看看Alpine Linux在構建時間和鏡像大小方面與Ubuntu 18.04有何不同。

首先,我將拉取兩個鏡像,並檢查他們的大小:

$ docker pull --quiet ubuntu:18.04
docker.io/library/ubuntu:18.04
$ docker pull --quiet alpine
docker.io/library/alpine:latest
$ docker image ls ubuntu:18.04
REPOSITORY          TAG        IMAGE ID         SIZE
ubuntu              18.04      ccc6e87d482b     64.2MB
$ docker image ls alpine
REPOSITORY          TAG        IMAGE ID         SIZE
alpine              latest     e7d92cdc71fe     5.59MB

如你所見,Alpine的基礎鏡像要小得多。

接下來,我們將嘗試在它們兩個中安裝gcc。首先,在Ubuntu中:

FROM ubuntu:18.04
RUN apt-get update && 
    apt-get install --no-install-recommends -y gcc && 
    apt-get clean && rm -rf /var/lib/apt/lists/*

注意:在我們討論的主題之外,本文中的Dockerfile並不是最佳實踐的示例,因為增加的複雜性會掩蓋本文的主要觀點。因此,如果你打算用Docker在生產環境中運行你的Python應用程序,這裡有兩種方法可以應用最佳實踐:

如果你想DIY:一個詳細的清單、例子和參考資料

如果你想要盡快擁有一個基本夠用的設置:一個模板和為你實現的最佳實踐

然後,我們可以構建並記錄時間:

$ time docker build -t ubuntu-gcc -f Dockerfile.ubuntu --quiet .
sha256:b6a3ee33acb83148cd273b0098f4c7eed01a82f47eeb8f5bec775c26d4fe4aae
real    0m29.251s
user    0m0.032s
sys     0m0.026s
$ docker image ls ubuntu-gcc
REPOSITORY   TAG      IMAGE ID      CREATED         SIZE
ubuntu-gcc   latest   b6a3ee33acb8  9 seconds ago   150MB

現在,我們編制一個類似的Alpine Dockerfile:

FROM alpine
RUN apk add --update gcc

同樣地,構建鏡像並檢查大小:

$ time docker build -t alpine-gcc -f Dockerfile.alpine --quiet .
sha256:efd626923c1478ccde67db28911ef90799710e5b8125cf4ebb2b2ca200ae1ac3
real    0m15.461s
user    0m0.026s
sys     0m0.024s
$ docker image ls alpine-gcc
REPOSITORY   TAG      IMAGE ID       CREATED         SIZE
alpine-gcc   latest   efd626923c14   7 seconds ago   105MB

就像我們所說的那樣,Alpine鏡像構建速度更快,體積更小:15秒而不是30秒,鏡像大小是105MB而不是150MB。這很好!

但是當我們打包Python應用程序時,情況就開始變得糟糕了。

讓我們構建一個Python鏡像

我們希望打包一個使用了panda和matplotlib的Python應用程序。因此,一種選擇是使用基於Debian的官方Python鏡像(我提前拉取的),和以下這個Dockerfile:

FROM python:3.8-slim
RUN pip install --no-cache-dir matplotlib pandas

然後,我們構建它:

$ docker build -f Dockerfile.slim -t python-matpan.
Sending build context to Docker daemon  3.072kB
Step 1/2 : FROM python:3.8-slim
 ---> 036ea1506a85
Step 2/2 : RUN pip install --no-cache-dir matplotlib pandas
 ---> Running in 13739b2a0917
Collecting matplotlib
  Downloading matplotlib-3.1.2-cp38-cp38-manylinux1_x86_64.whl (13.1 MB)
Collecting pandas
  Downloading pandas-0.25.3-cp38-cp38-manylinux1_x86_64.whl (10.4 MB)
...
Successfully built b98b5dc06690
Successfully tagged python-matpan:latest
real    0m30.297s
user    0m0.043s
sys     0m0.020s

結果鏡像大小為363MB。

用Alpine會獲得更好的結果嗎?讓我們試一試。

FROM python:3.8-alpine
RUN pip install --no-cache-dir matplotlib pandas

然後,我們構建它:

$ docker build -t python-matpan-alpine -f Dockerfile.alpine .                                 
Sending build context to Docker daemon  3.072kB                                               
Step 1/2 : FROM python:3.8-alpine                                                             
 ---> a0ee0c90a0db                                                                            
Step 2/2 : RUN pip install --no-cache-dir matplotlib pandas                                                  
 ---> Running in 6740adad3729                                                                 
Collecting matplotlib                                                                         
  Downloading matplotlib-3.1.2.tar.gz (40.9 MB)                                               
    ERROR: Command errored out with exit status 1:                                            
     command: /usr/local/bin/python -c 'import sys, setuptools, tokenize; sys.argv[0] = '"'"'/
tmp/pip-install-a3olrixa/matplotlib/setup.py'"'"'; __file__='"'"'/tmp/pip-install-a3olrixa/matplotlib/setup.py'"'"';f=getattr(tokenize, '"'"'open'"'"', open)(__file__);code=f.read().replace('"'"'rn'"'"', '"'"'n'"'"');f.close();exec(compile(code, __file__, '"'"'exec'"'"'))' egg_info --egg-base /tmp/pip-install-a3olrixa/matplotlib/pip-egg-info                              
...
ERROR: Command errored out with exit status 1: python setup.py egg_info Check the logs for full command output.
The command '/bin/sh -c pip install matplotlib pandas' returned a non-zero code: 1

發生了什麼?

標準的PyPI wheel在Alpine上無效

如果你仔細查看上面基於Debian的構建,就會看到它正在下載matplotlib-3.1.2-cp38-cp38-manylinux1_x86_64.whl。這是一個預編譯的二進制wheel。與此相反,Alpine會下載源代碼(matplotlib-3.1.2.tar.gz),因為標準的Linux wheel在Alpine Linux上無效。

為什麼?大多數Linux發行版使用標準C庫的GNU版本(glibc),Python以及幾乎所有的C程序都需要它。但是Alpine Linux使用musl,這些二進制wheel是針對glibc編譯的,因此Alpine禁用了Linux wheel支持。

現在,大多數Python包都包含了PyPI上的二進制wheel,這大大縮短了安裝時間。但是,如果你使用的是Alpine Linux,那麼你就需要編譯你所使用的每個Python包中的所有C代碼。

這也意味著你需要自己找出每個系統庫依賴項。在這種情況下,為了找出依賴項,我做了一些研究,最後得到下面這個經過更新的Dockerfile:

FROM python:3.8-alpine
RUN apk --update add gcc build-base freetype-dev libpng-dev openblas-dev
RUN pip install --no-cache-dir matplotlib pandas

然後我們構建它,它需要……25分鐘57秒!得到的鏡像是851MB。
下面是兩個基本鏡像的對比:

用Alpine會讓Python Docker的構建慢50倍 2

Alpine的構建速度要慢得多,鏡像要大得多,而且我不得不做了很多研究。

你不能解決這些問題嗎?

構建時間

為了縮短構建時間,Alpine Edge(最終將成為下一個穩定版本)會包含matplotlib和panda。而且安裝系統包非常快。但是,到2020年1月為止,當前的穩定版本還不包括這些流行的包。

然而,即使它們可用了,系統包也幾乎總是滯後於PyPI上的包,Alpine也不太可能打包PyPI上的所有東西。據我所知,在實踐中,大多數Python團隊並沒有將系統包用於Python依賴項,而是依賴於PyPI或Conda Forge。

鏡像大小

一些讀者指出,你可以刪除最初安裝的包,或者添加不緩存包下載的選項,或者使用多階段構建。一位讀者嘗試生成了一個470MB的鏡像

是的,你可以得到一個與基於slim的鏡像大致相當的鏡像,但是Alpine Linux的全部動機是更小的鏡像和更快的構建。如果工作做夠了,你可能會得到一個更小的鏡像,但是你仍然要忍受長達1500秒的構建時間,當你使用python:3.8-slim鏡像時,構建時間只有30秒。

但是等等,還有!

Alpine Linux會導致意料之外的運行時Bug

雖然理論上,Alpine使用的musl C庫與其他Linux發行版使用的glibc基本兼容,但在實踐中,這種差異可能會導致問題。當問題確實發生時,可能會很奇怪且出乎意料。

下面是一些例子:

  1. Alpine線程的默認堆棧大小更小,這可能導致Python崩潰
  2. Alpine的一位用戶發現,由於musl分配內存的方式與glibc不同,他們的Python應用程序要慢很多
  3. 在使用WeWork工作空間的WiFi時,我曾經無法在minikube(虛擬機中的Kubernetes)上運行的Alpine鏡像中查找DNS。原因是WeWork糟糕的DNS設置、Kubernetes和minikube實現DNS的方式,以及musl對這種邊緣情況的處理與glibc的方式不同。 musl沒有錯(它符合RFC),但是我不得不浪費時間找出問題所在,然後切換到基於glibc的鏡像。
  4. 另一個用戶發現了時間格式和解析的問題。

大多數或者說所有這些問題可能都已經得到解決,但毫無疑問,還有更多的問題有待發現。這種出人意料的破壞是又一件需要擔心的事情。

不要將Alpine Linux用於Python鏡像

除非你想要更長的構建時間、更大的鏡像、更多的工作,以及潛在的隱藏Bug,否則你應該避免使用Alpine Linux作為基礎鏡像。

關於應該使用哪些鏡像的建議,請參閱我的文章“選擇一個好的基礎鏡像”。

英文原文:

Using Alpine can make Python Docker builds 50× slower