當你爲Docker鏡像選擇基礎鏡像時,Alpine Linux可能被推薦。有人告訴你,用Alpine將使你的鏡像更小,並能加快你的builds。如果你正在用Go,這無疑是個合理的建議。
但如果你使用Python,Alpine Linux會經常:
- 讓你的構建更慢
- 讓你的鏡像更大
- 浪費你的時間
- 偶爾引入一些令人費解的運行時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('"'"'\r\n'"'"', '"'"'\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的構建速度要慢得多,鏡像要大得多,而且我不得不做了很多研究。
你不能解決這些問題嗎?
構建時間
爲了縮短構建時間,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基本兼容,但在實踐中,這種差異可能會導致問題。當問題確實發生時,可能會很奇怪且出乎意料。
下面是一些例子:
- Alpine線程的默認堆棧大小更小,這可能導致Python崩潰。
- Alpine的一位用戶發現,由於musl分配內存的方式與glibc不同,他們的Python應用程序要慢很多。
- 在使用WeWork工作空間的WiFi時,我曾經無法在minikube(虛擬機中的Kubernetes)上運行的Alpine鏡像中查找DNS。原因是WeWork糟糕的DNS設置、Kubernetes和minikube實現DNS的方式,以及musl對這種邊緣情況的處理與glibc的方式不同。musl沒有錯(它符合RFC),但是我不得不浪費時間找出問題所在,然後切換到基於glibc的鏡像。
- 另一個用戶發現了時間格式和解析的問題。
大多數或者說所有這些問題可能都已經得到解決,但毫無疑問,還有更多的問題有待發現。這種出人意料的破壞是又一件需要擔心的事情。
不要將Alpine Linux用於Python鏡像
除非你想要更長的構建時間、更大的鏡像、更多的工作,以及潛在的隱藏Bug,否則你應該避免使用Alpine Linux作爲基礎鏡像。
關於應該使用哪些鏡像的建議,請參閱我的文章“選擇一個好的基礎鏡像”。
英文原文: