现在,相信主流的架构都是会选择容器来进行部署,这应该当没有疑问的。
我负责的所有项目,也都会使用容器来再结合容器编排工具(Docker Swarm或K8S,依据大小而定)进行声明式部署,非常方便高效。但在这其中,我也遇到一个问题并一直再思考怎么样才是更好的。
这个问题就是:
对于容器镜像来说,究竟该不该选择alpine镜像做为基础镜像?
一)
首先,要了解这个问题的来源,为什么纠结这种事呢。
当然原因在于,容器镜像虽然方便高效,但它有一个最大的问题就是,比较占用空间了。
就算你随便把一个Java服务构建成镜像,镜像Image大小至少都在100-200M左右。这个大小当然还好,但我做项目都是使用CI/CD自动构建,至少每天或每次commit都要触发一次开发/测试环境的全自动构建流程,一个项目一堆镜像服务,长年累月下来,这个空间占用量还是很恐怖的。
而且基本上Docker的相关教程,一定会讲如何构建空间更小的镜像。使用Docker的你,一定也有特别注意到这一点吧。
尽量使用适当的方式,构建更小的镜像,这肯定是需要的了。
所以问题就来了,基本上很多建议,甚至是Docker官方的建议中,都会有一个点,就是使用更小的基础镜像。而在小的基础镜像中,最多的被提及的就是alpine了。
二)
alpine是一个非常特别的Linux发行版本,它本来是用于嵌入式系统中的,大小才5M左右,对于嵌入式系统,非常合适。
本来并非用于Linux服务领域,但随着容器的流行,这个才5M大小的Linux在Docker中流行起来了,因为太小了,非常节省空间。
这里就对比下同样属于Linux镜像Ubuntu与alpine的大小
REPOSITORY TAG IMAGE ID CREATED SIZE
alpine 3.16.2 9c6f07244728 7 days ago 5.54MB
ubuntu latest df5de72bdb3b 2 weeks ago 77.8MB
可以明显看到,alpine镜像解压后才5.54MB (压缩后2.68M),而流行的Ubuntu解压后的大小是 77.8MB (压缩后是22.04M)
就算以压缩后的大小对比,也有近10倍差距。
再加上有宣称据称更小的alpine在安全性上是优于主流的ubuntu的(原因是因为代码少,所以更安全)
所以,根据网上大量教程的建议,我在构建服务镜像时,尽量都是以alpine为主。
# syntax=docker/dockerfile:1
FROM eclipse-temurin:11.0.16_8-jre-alpine
WORKDIR /app
COPY ./rest-bootstrap-1.0.0-SNAPSHOT.jar .
ENTRYPOINT ["java","-jar","/app/rest-bootstrap-1.0.0-SNAPSHOT.jar"]
三)
不过,我这人好奇欲比较强,用着用着,我就在思考一个问题。
究竟是什么原因,同样是Linux系统,满足同样的功能,alpine能做到这么小
而且,如果alpine能做到这么小,Linux系统不都是开源的,那为什么Ubuntu,Deiban这样的不借鉴alpine,减少自己镜像的大小呢?
这其中肯定是有原因的吧。
于是我就去查证了下这个点,终于让我发现了背后的原因,这促使我进一步去思考,使用alpine究竟合适不合适?
alpine之所以小到这种程度,一个最根本的在于:
它没有使用glibc这个主流Linux的C类库,而是选择了musl实现
简而言之呢,主流Linux都是使用的glibc这个C类库,而在Linux系统上,支持C编译构建是核心需要的能力之一,但是alpine因为当初目标是嵌入式系统,基于大小的考量,选择了另一个实现musl。而musl你可以理解它为一个更小,更轻量的C类库。
这就是最主要的原因。
四)
当然,这两者都支持C程序的编译与构建。也就是,大多数情况下,能在glibc上编译通过的也都能在musl上编译通过。
注意下这个词,大多数情况下
不同的技术,虽然都是对同一种语言的支持,但毕竟是完全不同的实现,肯定是会存在差别的。而主流Linux都是glibc,glibc的性能也高于musl的。
这意味着行业内的一个现状是:大多数在Linux上运行的软件,都是针对glibc进行测试过的,但针对musl,则不一定。毕竟musl非常小众。
那回到这篇文章的主题上来,如果镜像容器选择alpine,则意味着你选择了:
优势
- 你构建了存储大小更小的镜像,节省了存储空间
缺点
- 兼容性问题,很有可能你所运行的软件对musl的兼容性并不好
- 性能,musl的性能是稍差于glibc的
五)
再进一步,回到更小的镜像这一点上。
究竟使用alpine这种基础镜像,能节省多少空间,在稍加思考后,我发现这可能也是一个伪优势。只是假想出来的优势。
我在这里以alpine与ubuntu来构建同一个服务镜像,看下它们的存储大小差距吧。
java-grpc-sample-user ubuntu 42805f4be518 8 seconds ago 304MB
java-grpc-sample-user alpine d2109aa9c0d6 51 seconds ago 208MB
如上所示,同一个Java服务,使用alpine构建的镜像,比使用ubuntu构建镜像节省了100M左右的空间,由于镜像是要压缩才上传下载的,压缩后的大小差距可能只有50M左右了。
于是,我们假设下,如果我们的项目要构建100个服务镜像(算是比较大的项目),这是不是意味着节省了 100 * 100 (M)空间?
错,大错特错。
要知道,容器镜像是分层的,基于同一基础镜像构建的层,其实会共享同一个层。并不会每个镜像都复制一份基础层。这意味着,如果你在一个服务器上有100个服务镜像,但这100镜像是共享了同一个基础层(当然你得使用同一基础镜像来构建)。
也就是,你并不会多100 * 100(M)的空间和网络下载量,一个Docker服务上你只多了100M的空间。
如上图所示,使用同一基础镜像构建的不同镜像,共享了同一基础镜像层,不会每个都重复产生一个。
这意味着,所谓的节省镜像空间,实际产生的价值并未有你想像的那么大。
六)
最后,再提及下安全性问题。一些博客中,在推荐使用alpine时,提及了安全性这一点。
理由是,alpine小,代码更少,理所当然安全性更高。
我也仔细查阅了关于这一点的相关说法,发现这一点在业界并无统一认知,并非共识。反对此观点的人很容易举出反证,更主流的镜像维护的人更多,安全上的问题修复更快,所以更安全。
所以,关于alpine更安全,这是一个有争议的点,至少在当下,尚无足够的证据证明它。因此不能成为理由。
七)
最后,说下Java吧,因为Java依赖C类库,而支持Musl的Java实现,并非主流。
JDK支持alpine,有两种方式来实现
- 通过OpenJDK's project Portola项目来支持,这个是JDK中专门支持Musl的项目,当前仍未发布稳定性。
- 在alpine上安装glibc,替换musl来实现的
所以,其它的不说,JDK使用alpine,是个风险很高的事。为了可能没有实际价值的镜像大小,没有必要做出这样的选择。
于是,我把所有JDK的基础镜像,切换为Deibna/Ubuntu基础镜像了。
八)
最后,我的建议是:
- 如果你的服务,不依赖C,那alpine是合适的选择,否则不应当使用alpine做为基础镜像
- 考虑到部署的一致性,就算是容器,使用同一系的Linux基础镜像是更妥当的选择,比如全是Debian系或RHEL系等
- 优化镜像的空间非常有必要,各种建议仍然非常有价值,但不要走的太过,镜像的空间问题并没有你想像的那么严重。
- 在公司或项目级别,不要使用Docker Hub或其它公有云镜像,使用简易的registry或企业级Harbor,Nexus等搭建一个内网镜像中心,能让容器镜像的上传下载更快捷方便。