?作者丨Martin Heinz
編譯丨千山
在 Linux 的發展史上,各種 Linux 發行版本起了巨大的作用,正是它們加速了 Linux 的應用。其中比較著名的便是商業公司維護的 Red Hat 系列以及社區組織維護的 Debian 系列。
在眾多的 Linux 版本中,每個版本都有自己的特點。今天故事的主角就是一款非商業性的通用 Linux 發行版——Alpine Linux,它是由社區開發的輕型 Linux 發行版,重點關注安全、性能和資源利用率。
區別于其他常見的 Linux 發行版,Alpine Linux 采用了 musl libc 和 busybox,從而減小系統體積,降低運行時的資源消耗。更重要的是,盡管體積小,但 Apline 提供了完整的 Linux 環境,其存儲庫中還包含了大量的軟件包備選。可以稱得上是“小而全”的典范。
Alpine Docker 鏡像繼承了 Alpine Linux 發行版的這些優勢。相比于其他 Docker 鏡像,Alpine 只有 5 MB 大小,并且擁有很友好的包管理機制。
在此背景下,Alpine Linux 逐漸成為當下容器基礎鏡像最流行的選項之一。好處是顯而易見的,鏡像下載速度更快、鏡像安全性提升、占用更少磁盤空間等等,但是也有人在實踐后發現:使用 Alpine 作為容器鏡像,也要承受一些不可避免的麻煩和風險。
1、痛苦之源:處理 DNS 的方式
Alpine 在某些情況下是一個糟糕的選擇,要理解這一點,首先需要談談 musl。
musl 是構建在 Linux 系統調用 API 之上的 C 標準庫的實現,相比其他 Linux 發行版(如Ubuntu)使用的 glibc 更輕量級、更快、更簡單。
這兩種實現在大多數情況下都是可替換的。這就是為什么在大多數情況下,你可以從 Ubuntu 切換到 Alpine,而不會注意到任何不同。
但是再微小的差異也可能導致悲劇。其中主要的問題源于 musl 處理 DNS 的方式,更具體地說,musl(在設計上)不支持 DNS-over- tcp。
通常你不會注意到這個區別,因為大多數情況下,一個 UDP 數據包(512字節)就足以解析主機名……直到它不夠用了。
之前正常工作了數月的應用程序(運行在 Kubernetes 上),突然開始拋出一個特定(非常關鍵)主機名的“未知主機”異常。
最糟糕的是,這可能是隨機出現的,可能發生在任何時候,當一些外部網絡變化導致某些特定域的分辨率需要超過單個 UDP 數據包中可用的 512 字節。
如果你運行數十個甚至數百個基于 Alpine 的微服務/應用程序,它們都突然停止工作,唯一的解決辦法是切換到不同的 Linux 發行版,這需要重新構建所有應用程序并重新部署它們,那么你可能會面臨一個令人抓狂的現實——強破壞性的、持續多日的停機。
總而言之,這個 DNS 問題不會在 Docker 容器中暴露出來。這只會發生在 Kubernetes 中,所以如果你在本地測試,一切都會正常工作,并且只有在將應用程序部署到集群時才會發現不可修復的問題。此外,Kubernetes 文檔聲稱 DNS 問題只與“Alpine 3.3 或更早版本”相關,但我在 Alpine 3.16 上也遇到了上述問題,所以無需贅言。
另外,值得一提的是,許多流行的工具,例如 nicolaka/netshoot 或 giantswarm/tiny-tools,也使用 Alpine 作為基本鏡像,前者是用于容器網絡問題的解決工具。可以想象,當你的故障排除工具也壞了的時候,那只能祝你好運了。
2、交叉編譯的風險
雖然 DNS 是 musl 最常見的問題,但有更多理由需要你審慎考慮。Alpine 使用 Musl Libc 作為傳統的 glibc 的替代,編譯軟件的時候可能會遇到一些不可預知的問題,這一點會導致我們耗費不少不必要的時間。任何依賴于 C 標準庫的編程語言或其庫都會受到 musl 和 glibc 之間差異的影響。
例如,對于 Python,許多流行的庫(如 NumPy 或 Cryptography)都依賴于 C 代碼進行優化。幸運的是,至少對于 Numpy 這樣的一些庫,你可能會找到基于 alpine 的編譯包和相關依賴項。
然而,對于不太受歡迎的編譯器,你可能不得不自己編譯。這樣做真的值得嗎?在我看來,不值得。此外,即使你設法構建了一個包含 numpy 的鏡像,其大小將是 400MB 左右,在這種情況下,因為體積小而使用 Alpine 的理由無疑也站不住腳了。
此外,構建這樣一個鏡像的時間將是殘酷的。你可以自己試試,下面的 Dockerfile 構建大約需要 10 分鐘:
顯然,類似的問題在其他語言中也會發生。例如,Node.js 使用附加組件,這些附加組件是用 C++ 編寫的,并使用 node-gyp 編譯,這些附加組件將依賴于 C 庫,因此依賴于 glibc。
另一個例子是 Golang,它的標準庫——或者更具體地說是 net/http 或 os/user 模塊——依賴于 C 庫,因此依賴于 glibc。如果應用程序需要 CGO_ENABLED=1,即使不使用這些特定的模塊,使用 Alpine 顯然也會遇到問題。
此外,不可忽視的一點是,在 Docker Hub 中,大部分鏡像是沒有 Alpine 版本的,比如 Mysql 和 PHP-Apache,如果我們需要基于這些環境開發,就不得不自己編寫 Alpine 版本,或者找一些第三方鏡像。
3、用什么替代
如果上述問題促使你重新考慮使用 Alpine,那么你可能想知道應該使用什么替代。有很多選擇,它們都有一些利弊需要權衡。
Alpine 最大的吸引力在于它的體積小,所以如果你真的在乎這一點,那么 Wolfi(例如,cgrd .dev/chainguard/Wolfi -base只有 12MB)或 Distroless 都是不錯的選擇。
如果你正在尋找具有合理大小的通用基礎鏡像,而不是基于 musl,那么你可以考慮使用 Red Hat 公司的 UBI(通用基礎鏡像),它的“微型”版本(registry.access.redhat.com/ubi8-micro)只有 26.7MB,這也非常接近 Alpine。
選擇 Alpine 的另一個原因是它的安全性。這也與它的小尺寸有關,因為小尺寸通常意味著更少的包,因此漏洞也更少。在這方面,上述的 Wolfi 是一個特別好的選擇。
讓我們實事求是地說,由于 Alpine 很小而節省幾兆字節的空間并不重要,除非你要拉成千上萬次鏡像(你可能不應該這樣做),所以使用 Ubuntu 或基于 Debian 的基本鏡像也不是一個糟糕的選擇。
可能有人知道,Docker 官方的 Debian 鏡像有個 slim 版本,這個版本的大小比默認的版本要小一倍多。
slim 顧名思義就是“瘦身版”。Debian-slim 是一個很好的折中方案,它比 Alpine 大,但也沒那么大。
有一些上層的鏡像會基于 Debian-slim 進行編寫,比如 Python。如果我們開發 Python 的項目,可以使用 python:slim 這個基礎鏡像。
另外,在 Docker 17.05 版本以后,新引入了多階段編譯(multi-stage builds) 這一概念,這將會極大地簡化所有操作。
簡單來說,多階段編譯支持我們將 Docker 鏡像的編譯分成多個“階段”。比如常見的軟件編譯的情況,我們可以將編譯階段單獨提出來,軟件編譯完成后直接將二進制文件拷貝到一個新的基礎鏡像中,這樣做最大的好處就是,第二個鏡像不再包含任何編譯階段使用的中間依賴,干干凈凈明明白白。
4、結語
雖然使用 Alpine 沒有什么問題,而且它可以作為基本容器鏡像操作系統,但就我個人而言,由于前面描述的 DNS 問題,我可能永遠不會再信任它,或者任何使用 musl 的操作系統。
本文的重點不是要詆毀 Alpine。相反,這是一種預警。雖然考慮到上面列出的問題,Alpine 似乎是一個不錯的選擇,但使用它至少是有風險的,你的決定可能是魯莽的。不過這最終取決于你計劃在哪里使用它。它滿足了很多要求,也有很多優點,所以如果你不擔心或不受本文所描述的問題的影響,你可能應該繼續使用它。
綜上所述,這里的結論應該是——在使用任何東西(無論是容器操作系統、框架還是庫)之前進行合理的研究——它很受歡迎并受到好評并不一定意味著它就一定是一個好的選擇。
參考鏈接:
https://betterprogramming.pub/why-i-will-never-use-alpine-linux-ever-again-a324fd0cbfd6
https://www.ewbang.com/community/article/details/960423446.html