导语
在云原生火热的当下,大量业务不得不和docker打交道。开发者往往会有很多疑问,该用哪个镜像?安全漏洞怎么修?跨架构镜像如何处理?开发环境和测试环境如何统一?本篇介绍了QAPM项目的第三次业务基础镜像改造,将您的疑问一一解答!
背景信息
在当前以Devops的理念为流程的项目开发过程中,镜像构建作为一项关键的基础设施,对于项目的稳定性、可维护性和扩展性具有重要意义。一个优秀的镜像构建流程可以确保开发、测试和部署过程的高效运行,降低故障风险,提高系统的可靠性。然而,在实际项目中,镜像构建往往面临着诸多挑战。以我所在的QAPM项目为例,其在镜像构建阶段所经历的问题和痛点,正是许多互联网项目在面对镜像构建时所共同面临的困境。
QAPM项目在镜像构建方面经历了两次改造。在第一次改造中,项目组通过构建统一的Centos基础镜像,简化了各模块的编包流程,提高了编包效率。然而,这一改造仍然存在一些问题,如镜像体积较大,影响服务性能;线上调试功能和私有云环境的需求与镜像体积的优化之间存在矛盾。为了解决这些问题,项目组进行了第二次改造,构建了两套镜像体系:big镜像和small镜像,以平衡线上调试需求和镜像体积优化。在第二次改造中,为了划分基础镜像与业务编包逻辑的责任范围,设计了一套编包流程。编包入口为build_docker.sh脚本,Dockerfile不包含编包逻辑,只将业务提供的build.sh脚本拷贝到上下文后执行。

尽管经过两次改造,QAPM项目在镜像构建方面取得了一定的进展,但仍然面临一些挑战。首先,当前使用的Centos镜像不再维护,存在安全隐患,这对于一个私有云项目(经常被客户做安全扫描)来说是不容忽视的问题。其次,复杂的编包逻辑导致错误难定位排查,前端项目的特殊性也导致这套流程无法在后台、前端项目间保持一致,这给项目的统一管理和维护带来了困扰。最后,随着ARM平台的崛起我们需要提供易于维护的多架构构建方案。正是基于这些问题和痛点,项目决定进行第三次改造,旨在进一步优化镜像构建流程,提高项目的安全性和可维护性。那么第三次改造,希望可以真正的保证流程的一致性,以及解决镜像安全和体积大小等问题。
本文将以这次QAPM项目的第三次镜像改造为基础,深入探讨下基础镜像的选型,分析目前市面上主要几款镜像的优劣势,以给有相同选型困扰的同仁们提供一个参考,最终提高项目的稳定性、可维护性和扩展性。
主流镜像对比
首先我们来简单对比一下市面上的几款镜像:
CentOS:尽管 CentOS 是我们的历史选择,但它已经停止维护,不仅软件包更新滞后,而且不会得到任何支持和更新。在这种情况下,继续使用 CentOS 会导致较多的安全和稳定性问题,而且这些安全和稳定性的问题,只能依赖我们团队自己去解决。
Ubuntu:虽然 Ubuntu 和 Debian 具有相似的特点,但 Ubuntu 是由商业公司 Canonical 运作的。这可能会影响项目的长期可持续性,尤其是在许可和商业支持方面。相比之下,Debian 是一个完全由社区驱动的项目,因此更符合开源项目的需求。
Fedora:Fedora 是一个更新周期较快的发行版,这意味着它可能会引入不稳定的软件包和功能。对于需要稳定基础镜像的场景,Fedora 可能不是理想的选择。此外,与其他镜像相比,Fedora 的使用群体较少,这可能会影响到社区支持和问题解决的速度。
Oracle Linux 和 Amazon Linux:这些发行版分别由 Oracle 和 Amazon 公司运作。与 Ubuntu 类似,它们的商业化运作可能会影响到项目的长期可持续性。此外,Amazon Linux 主要针对 AWS 优化,但我们不是在 AWS 环境中运行,它必然不是最佳选择。
Alpine:Alpine 是一个轻量级发行版,专为容器环境设计。它在安全扫描上相比其他发行版,漏洞更少。然而,它使用了 musl libc,可能导致一定兼容性问题。
Debian:Debian 是一个经过长时间测试和验证的 Linux 发行版,因此非常稳定。它的软件包管理器(APT)和软件库非常成熟,可以轻松地安装和更新软件。此外,Debian 拥有庞大的社区支持。
| 系统 | 兼容性 | 安全性 | 社区支持 | 镜像大小 | 开源友好程度 |
|---|---|---|---|---|---|
| Ubuntu | 高 | 高 | 广泛 | 较大 | 中(商业公司运作) |
| Debian | 高 | 高 | 广泛 | 较大 | 高(社区驱动项目) |
| CentOS | 高 | 高 | 有限 | 较大 | 中(已停止维护) |
| Fedora | 高 | 高 | 一般 | 较大 | 高(社区驱动项目) |
| Alpine | 中 | 高 | 一般 | 轻量级 | 高(社区驱动项目) |
综上所述,我们主要使用alpine和debian两类镜像来做对比。他们其中一个会作为我们最终的选择。
进阶对比:alpine与debian
Alpine
优点:
轻量级:Alpine 是一个专为容器环境设计的轻量级 Linux 发行版。它使用了 BusyBox 和 musl libc,这使得镜像的大小非常小(通常在 5 MB 左右)。
安全性:Alpine 默认使用了一些安全增强功能,如地址空间布局随机化(ASLR)和堆栈保护。这有助于提高容器的安全性。alpine在安全扫描上相比其他发行版本,漏洞更少。
构建速度:由于 Alpine 镜像的大小较小,构建速度通常较快。这可以缩短持续集成和部署(CI/CD)流程的时间。
缺点:
软件兼容性:alpine没有使用常见的glibc(GNU C Library),使用的是musl libc,兼容性较差,在其他发行版(如 Debian 或 Ubuntu)上编译的预编译二进制文件通常与 glibc 链接。在 Alpine 上运行这些二进制文件可能会因为缺少 glibc 而导致问题,当然这只是一方面的问题,glibc和musl libc在api、符号版本、线程局部存储方面的差异也会导致兼容性的问题。
Debian
优点:
稳定性:Debian 是一个经过长时间测试和验证的 Linux 发行版,因此它非常稳定。它的软件包管理器(APT)和软件库非常成熟,可以轻松地安装和更新软件,相比alpine的apk,使用的更为广泛。
兼容性:Debian 支持多种硬件架构和平台,这使得它可以在各种环境下运行。此外,许多其他 Linux 发行版(如 Ubuntu)基于 Debian,因此在这些发行版之间迁移应用程序和配置相对容易。
社区支持:Debian 社区庞大且活跃,有大量的文档、教程和论坛供用户参考。这意味着在遇到问题时,很可能能找到解决方案。
缺点:
镜像大小:相对于 Alpine,Debian 镜像的大小较大。会导致更长的构建时间和更高的存储需求。
同维度对比
| 特性 | Alpine | Debian |
|---|---|---|
| 镜像大小 | 轻量级(通常在 5 MB 左右),有助于缩短构建时间和减少存储需求 | 较大,可能导致更长的构建时间和更高的存储需求 |
| 安全性 | 默认使用了一些安全增强功能,如 ASLR 和堆栈保护,漏洞扫描出来较少 | 虽拥有活跃的安全团队,及时发布安全更新和补丁,但是漏洞扫描出来的相对较多 |
| 兼容性 | 使用 musl libc,可能导致与 glibc 相关的兼容性问题 | 支持多种硬件架构和平台,许多其他 Linux 发行版基于 Debian |
| 社区支持 | 社区规模较小,文档和支持相对较少 | 社区庞大且活跃,有大量的文档、教程和论坛供用户参考 |
| 包管理器 | 使用 apk,轻量级且高效,但使用群体相对少 | 使用 APT,成熟且功能强大,使用更为广泛 |
| 稳定性 | 专为容器环境设计,适用于轻量级应用场景 | 经过长时间测试和验证,非常稳定,适用于各种应用场景 |
提到Alpine的主要优势,空间占用很小这里来看,主要是因为它省略了许多其他镜像中原本包含的组件。这里以CentOS和Alpine的包来做对比,CentOS的空间占用为215M,而Alpine仅为5.9M。这里的磁盘差异主要是因为CentOS使用了yum包管理器,而Alpine没有。此外,CentOS还包含了一个额外的Python环境。当Alpine增加Python2之后,其空间占用增加到55.3M。
在深入分析CentOS的空间占用后,我们还可以发现,其主要由以下几个组件占据:
/var/lib/rpm:这个目录包含了RPM包管理器所需要的数据库文件,这些文件存储了已安装的软件包及其元数据的信息。这些信息有助于RPM包管理器执行诸如安装、更新、删除等操作。由于RPM是CentOS的默认包管理器,这个目录在CentOS中占用了24M的空间。
i18n相关文件:这些文件用于支持国际化,即让软件能够适应不同语言和地区的需求。这些文件包含了各种语言的翻译、字符集和编码规则等信息,使得软件能够在不同的语言环境下正常运行。在CentOS中,这些文件占用了9.6M的空间。
cracklib:这是一个密码检查库,用于PAM(Pluggable Authentication Modules,可插拔认证模块)加密。它可以检查用户密码的强度,以防止设置过于简单或容易猜测的密码。在CentOS中,这个库占用了9M的空间。
mime信息:MIME(Multipurpose Internet Mail Extensions,多用途互联网邮件扩展)信息用于描述文件类型和格式,以便在互联网上传输时正确处理文件。这些信息包含了文件扩展名、类型、子类型等元数据。在CentOS中,这些信息占用了5.4M的空间。
zoneinfo:这个目录包含了各种时区的信息,如UTC偏移量、夏令时规则等。这些信息有助于系统在不同地区正确处理时间和日期。在CentOS中,这些信息占用了5.1M的空间。
gconv:这个库用于字符集转换,即在不同字符集之间转换文本。由于不同的语言和编码系统使用不同的字符集,因此在处理多语言文本时,字符集转换是必不可少的。在CentOS中,这个库占用了7.4M的空间。
systemd:这是一个系统和服务管理器,用于初始化、管理和监控系统中的各种服务和资源。它在Linux系统中扮演着核心角色,负责系统启动过程中的各种任务。在CentOS中,systemd大约占用了15M的空间。
此外,如果再安装coreutils、glibc等包,也会占用大约10M的空间。因此,如果在Alpine上安装这些包,其空间占用将达到140M+,再加上其他小项目,总空间占用将超过200M。
因此,Alpine的空间占用优势主要是通过省略一些组件实现的,而这些组件库可能我们在实际项目中,还需要再自行下载。并且,在磁盘资源相对廉价的今天,我们没有必要为了节省这些空间而舍弃兼容性。
debian相比与alpine的一个问题是安全漏洞更多,但扫描出来的安全问题一般还未有修复包,属于可以接受的问题。alpine的兼容性问题要求我们使用musl的库、软件包,这部分支持并不完善,这个缺点我们无法接受。所以经过利弊权衡,我们最终选择了Debian作为我们的基础镜像。
说到musl库的兼容性问题,那就得先介绍下c库这玩意的作用了(毕竟不是每个人都是c或者操作系统的开发人员,对这里那么了解)
C库的重要性
C库(C library)在操作系统及其应用程序中发挥着关键作用,它为程序员提供了一套标准化、跨平台且高度优化的函数库,以简化底层系统操作并实现跨平台兼容性。C库的主要优点和作用可以从以下几个方面进行阐述:
- 系统调用封装:C库将底层操作系统的系统调用封装为高级函数,使得我们能够通过简单的函数调用来执行诸如文件操作、进程管理、内存分配等复杂的系统任务。这种封装降低了程序员与底层系统之间的交互复杂性,使得程序员能够专注于应用程序逻辑的开发,而无需深入了解底层系统的实现细节。
- 跨平台兼容性:C库为各种操作系统提供了统一的应用程序接口(API),使得使用C语言编写的程序可以在不同操作系统上运行,而无需进行大量的修改。这一特性为实现跨平台兼容性提供了基础,使得程序员可以依赖这些API进行编程,而无需关注底层操作系统之间的差异。
- 标准化:C库遵循了C语言的国际标准,定义了一组标准函数,这些函数在所有C语言实现中都是一致的。这使得程序员可以依赖这些标准函数来编写可移植的代码,而无需担心不同编译器或操作系统之间的差异。
- 基本数据结构和算法:C库提供了一些基本的数据结构和算法实现,如字符串操作、数学函数、排序和搜索等。这使得程序员可以直接使用这些功能,而无需从头开始实现它们,从而提高了开发效率。
- 错误处理:C库提供了一套错误处理机制,使得程序员在遇到错误时可以获得有关错误的详细信息,以便进行调试和修复。这种错误处理机制有助于提高程序的健壮性和可维护性。
- 性能优化:C库函数通常经过高度优化,以确保在各种操作系统和硬件平台上提供最佳性能。这使得使用C库函数的应用程序能够获得更好的性能,而无需程序员进行手动优化。
简而言之,C库让程序员能够更轻松地编写代码,同时确保代码在不同操作系统和设备上正常运行,无需关注底层实现细节。同样也就是说,C库不仅涉及了软件层面,也涉及到了操作系统的底层细节。
而glibc则是目前使用的最为广泛的C库实现。它是 GNU 项目对 C 标准库的实现。当然,并非所有标准 C 函数都可以在 glibc 中找到:大多数数学函数实际上是在libm(一个单独的库)中实现的。
到目前为止,glibc 是 Linux 上使用最广泛的 C 库。然而,在 90 年代,有一段时间出现了 glibc 的竞争对手,称为Linux libc(或简称libc),它是从 glibc 1.x 的一个分支中诞生的。有一段时间,Linux libc 是许多 Linux 发行版中的标准 C 库。
经过多年的发展,glibc 被证明比 Linux libc 优越得多,并且所有使用它的 Linux 发行版都转回了 glibc。所以我们可能会有印象在磁盘中会找到名为 libc.so.6的文件,它就是现代的 glibc.h 文件。版本号增加到 6,以避免与以前的 Linux libc 版本混淆(他们无法命名它glibc.so.6:所有 Linux 库都必须以lib前缀开头)。
Musl libc(Musl库)则是一个轻量级的C标准库实现,它旨在提供简洁、高效、可靠的系统库,特别是针对嵌入式系统和资源受限的环境。Musl libc的开发始于2010年,由Rich Felker发起。Musl库的设计理念是为了解决GNU C库(glibc)在某些方面的不足,如代码复杂性、许可证限制等问题。但不可避免的,它和glibc存在了一些兼容性问题。
关于libc兼容性方面alpine存在的问题
如之前我们介绍C库作用中所说的,musl 的兼容性问题的影响不仅限于 C 语言编写的模块,还包括其他语言(如 Python、Ruby、Node.js、Go 等)中使用了 C 扩展或者与 C 库交互的部分。
使用了 C 扩展的 Python、Ruby 或 Node.js 程序。这些扩展通常用 C 语言编写,可能依赖于特定的 glibc 功能。
使用了 FFI(Foreign Function Interface)的程序。FFI 允许在一个语言中调用另一个语言的函数,通常用于与 C 库交互。如果这些库依赖于 glibc,那么在 musl 系统上可能会遇到兼容性问题。
依赖于特定 glibc 功能的预编译二进制文件。这些文件在其他发行版(如 Debian 或 Ubuntu)上编译,通常与 glibc 链接。在 Alpine 上运行这些二进制文件可能会因为缺少 glibc 而导致问题。
在使用alpine的时候,因为musl和glibc的兼容问题,我们也可能会面对c库和内核库有冲突的情况。
且系统使用musl后,会有dns解析的问题:
这可能会导致在/etc/hosts文件中没有填写完整的主机名的时候,解析会出错。
举个简单的例子:
假设你的内部网络中有一个名为 database.tcs.cn 的数据库服务器,其 IP 地址为 192.168.1.10。
然后系统里的 /etc/hosts 文件包含以下内容:
1 | 192.168.1.10 database.tcs.cn |
/etc/resolv.conf 文件包含以下内容:
1 | search example.com example.org |
现在,如果你要在这个系统上运行一个程序,想连接到 database.tcs.cn 服务器。由于 musl 在处理 DNS 查询时的行为,可能会导致以下问题:
性能问题:程序首先尝试解析
database.tcs.cn.example.com,然后尝试解析database.tcs.cn.example.org。这将导致额外的 DNS 查询,即使/etc/hosts文件已经包含了正确的映射。这可能导致连接数据库服务器时产生额外的网络开销和延迟。解析错误:假设 DNS 服务器错误地返回了一个无效的 IP 地址(如 203.0.113.42)作为
database.tcs.cn.example.com的解析结果。由于 musl 优先处理 DNS 查询,而不是先检查本地的/etc/hosts文件,程序将尝试连接到错误的 IP 地址,导致连接失败。
注意:即使
database.tcs.cn已经是一个完整的域名,musl 仍然会尝试将 search 列表中的后缀依次添加到域名后面,然后进行 DNS 查询,从而导致上述的性能问题和解析错误。
基础镜像统一
经过上文的思考,我们最终选择了debian做为我们基础镜像。接下来是考虑如何统一不同语言的基础镜像。
方案一
自研镜像仓库,保持定期更新版本、修复安全问题。

从最基础的基础系统镜像开始,逐层增加软件,构建完整依赖树。目前已经实现一个简单MVP,流水线自动检测变更后编包,并且通过buildx实现跨平台编译。
方案二
使用bitnami系列镜像,同样也是逐层架构,基于他们自研、维护的minideb(debian的精简、安全版)。
方案对比
所有方案对比如下,每一个对比项都包含三个成绩,绿色(良好),黄色(一般),红色(差)。
| 特性 | 每个语言官方基础镜像 | 自研层次基础镜像系统 | bitnami层次基础镜像系统 |
|---|---|---|---|
| 安全性 | 维修慢 | 维修速度中等 | minideb每日更新,保证24小时修复 |
| 可维护性 | 不同系统不同包管理 | 统一debian包管理 | 统一debian包管理 |
| 跨架构性 | buildx加持 | buildx加持 | buildx加持 |
| 自由度 | 所需软件额外安装 | 可任意捆绑所需软件 | 所需软件额外安装 |
| 大小 | 815MB(以golang 1.21.3举例) | 518MB(以golang 1.21.3举例) | 611MB(以golang 1.21.3举例) |
| 源码交付 | dockerhub上,公网可访问 | 自研层次,交付源码需要同样交付 | dockerhub上,公网可访问 |
| 预计人力投入 | 极多维护成本、安全工单问题 | 需要修复安全问题 | 较少,定期更新业务镜像即可 |
我们最终从成本角度考虑,选择了bitnami体系做为项目各个语言基础镜像方案,虽然牺牲了一定的自由度,但避免了重复造轮子的人力浪费。
业务镜像构建统一
为了统一所有语言的编包流程,同时保证项目可维护性,我们定下几个规则:
- 统一使用just做为项目管理工具
- 统一使用buildx实现跨架构镜像编译
- 项目提供docker任务进行镜像编译、推送,便于流水线统一处理
- 所有编译流程写Dockerfile中,放在项目根目录,避免多余的压缩、解压和脚本
以一个模块为例子,给出编包部份代码、配置:
justfile:
1 | default: |
Dockerfile:
1 | FROM --platform=$TARGETPLATFORM bitnami/golang:1.20 AS builder |
针对这几个规则大家一定有一些疑问,这里放几个FAQ:
为什么不用make这种更知名的项目管理工具?
因为它针对C/CPP项目,存在较多怪异语法和奇淫技巧,不适合新时代。很多时候为了使用make而阅读大量文档时,我真的想问自己为什么要用make为什么不用earthly这种更”先进”的编包专用工具?
首先项目不仅仅是编包,还有lint、clean等任务。其次,它对编包流程的魔改太重了,一旦迁移,改造成本极高,无法脱离earthly使用。而传统的Dockerfile方式可以在无just环境正常编包,便于理解。为什么不使用脚本?
脚本存在同步修改问题,并且千人千面,每个人都写自己的逻辑,最终可维护性几乎为0。
至此,所有模块编包流程统一了。
研发流程统一
为了进一步提高能效,我们又提出了新的需求:研发流程统一。这即是指线上环境与开发环境统一(所安装依赖、版本),又是不同产研、不同设备之间的开发环境统一。
需求:
- 我希望可以统一所有研发环境,比如使用的插件、gopls的配置等
- 我希望可以访问研发环境的依赖,比如compose启动的kafka、redis等
- 我希望可以使用宿主机的dockerd,以模拟宿主机的体验
综上所述,我们考虑使用vscode的dev container(ms-vscode-remote.remote-containers)插件。
使用文档参考链接:Dev Container metadata reference
这里给出我们项目的配置(golang):
1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the |
这样就实现了完全不了解项目的开发人员可以零成本启动完整的开发环境并进行编译运行,配合vscode的task、launch甚至可以做到快速调试。
总结
本文通过QAPM项目的第三次镜像改造实践,深入分析了当前主流Linux发行版在Docker容器环境中的应用,重点对比了Alpine和Debian两种镜像的优劣,最终选择了Debian作为基础镜像方案。同时,通过引入just、buildx等工具,实现了多语言项目的编包流程统一,并通过Dev Container进一步统一了开发环境,大幅提升了项目的可维护性和开发效率。
希望本文的分析和实践经验能为面临类似选型困扰的团队提供参考和借鉴。