前言

在使用 Python 的早些年,为了解决 Python 包的隔离与管理 virtualenvwrapper 就成为我的工具箱中重要的一员。后来,随着 Python 3 的普及,virtualenvwrapper 逐渐被 venv 所替换。毕竟 venv 是 Python 3 的标配,优点是显而易见的。而这几年,应用场景的的复杂性越来与高,无论是开发还是部署都需要设置复杂的环境。例如使用 redis 实现消息队列,用 Psycopg 完成对于 PostgreSQL 数据库的存取等等。随之而来 Docker 就变成了程序员必不可少的常备工具。为了掌握如何将我的 Python 应用与 Docker 结合起来,就要学习他人的经验分享。于是一次又一次地看到了下面这样的 Dockerfile 例子:

docker python 镜像装依赖 python打包docker镜像_开发语言


相比起来,我不熟悉 Alpine 这个 Linux 发行版本。我很困惑这个版本难道比其它哪些老字号的 Linux 发行版更适合 Docker 的环境吗?至于我的 Python 应用,究竟选择哪一个 Docker 基础镜像更好呢?

对于 Docker 基础镜像的要求

为我的 Python 应用构建一个 Docker 镜像并不是要从零开始,而是从现有的 Linux 基础镜像开始构建。这些基础镜像除了提到过的 Alpine 以外 还有我更熟悉的 Ubuntu、Centos 、Debian 等等。在决定选择哪一个之前,我们需要回答的一个问题就是:

  • 我们究竟对于这个 Docker 基础镜像有哪些要求?
  • 这些要求既要满足通常意义上我们对操作系统的要求,也要满足对于 Python 应用的特殊的需求。

归纳起来,我们的需求应当包括这几点:

  • 稳定性:要求今天的构建与以后的构建有相同的基本库、目录结构和基础结构。否则我们的 Python 应用程序将将有可能存在不确定的隐患。
  • 安全更新:需要基础镜像得到良好维护,以便及时获取基本操作系统的安全更新
  • 最新的依赖关系:除非我们的应用仅仅是一个简单的 Python 程序,否则就不得不依赖操作系统所提供 的库和应用程序(例如:GCC 编译器、OpenSSL 库等)。我们需要这些应用和库要足够新,否则就会有各种安全性的问题或者功能性的不足。
  • 丰富的库资源:对于某些应用,可能需要安装一些不太流行的库(例如 lxml 等)。这就需要我们选择的基本镜像提供丰富的库资源。
  • 最新的 Python 版本:虽然可以通过自行安装 Python 来解决,但是拥有最新的 Python 的版本无疑可以节省我们的时间、精力。
  • 小型的 Docker 镜像:在所有条件都相同的情况下,拥有尺寸较小的 Docker 镜像无疑更胜一筹。无论是镜像的构建还是占用存储空间的开销,更小的尺寸一定更有优势。

考虑到应用部署在产环境的需要,我们所选择的 Docker 镜像还应当具备长期支持(Long-term support, LTS) 的承诺。

长期支持 (英语:Long-term support,缩写:LTS)是一种软件的产品生命周期政策,特别是开源软件,它增加了软件开发过程及软件版本周期的可靠度。长期支持延长了软件维护的周期;它也改变了软件更新(补丁)的类型及频率以降低风险、费用及软件部署的中断时间,同时提升了软件的可靠性。但这并不必然包含技术支持。
– 维基百科

Linux 镜像版本的选择

围绕着上述的需求,我们很容易就会找到一批候选的版本。乍看起来,这些基础镜像应该能够满足我们的需要。接下来我们需要仔细甄别其具体的差异,以找出最适合我们的版本,尤其是适合我们的 Python 应用。

选项一:传统的 Linux 分发版本 – Ubuntu TLS、CentOS 以及 Debian

这三个 Linux 分发版本历史久远(Debian 早在 1993 年就已出现),名气很大,在 Linux 服务器市场上拥有广泛的用户。

  • Ubuntu 18.04(Docker 镜像的名字 ubuntu:18.04)发布于 2018 年 4 月,由于这是 Canonical 公司的长期支持版本(LTS),意味着该版本的用户在 2023 年之前都将获得安全更新。
  • CentOS 8( Docker 镜像的名字 centos:8 )于 2019 年发布,将在 2024 年前进行全面更新,并在 2029 年前提供维护更新。
  • Debian 10(Docker 镜像的名字 debian:buster)发布于 2019 年 7 月,承诺支持到 2024 年。

需要注意的是这些镜像预安装的 Python 有可能不是最新的版本。例如 Ubuntu 18.04 预安装的是 Python 3.6.7,而 Python 3 的最新稳定版本已经升级为 Python 3.8.1。因此我们必须在构建 Docker 镜像的时候去完成 Python 的安装或者升级。在一些特定的 Linux 分发版本中,我们甚至需要自行通过编译 Python 源码的方式来获得最新版本的 Python。例如在 CentOS 8 中,就需要用这个办法来安装 Python3.8。至于具体的办法,可以参考在“Python 3.8 已经来了,你准备好了吗?”一文中的介绍。

选项二:Docker 官方的 Python 镜像

这个 Docker 镜像由 Docker 官方提供。该版本的最大特点就是预装了 Python,并且提供多个不同 Python 版本的选项,例如 Python 3.7、Python 3.8、Python 3.9。需要注意的是,这个版本提供了多个不同的变体,如果搞不清楚这一点很容易在使用中遇到难以预料的问题。这些差异很大的变体包括了:

  • Alpine Linux, 关于这个版本,后面会有专门的讨论
  • Debian Buster, 这是基于 Debian 的一个分支版本,是基于 Debian 的标准镜像的 Layer 来构建的。特点是基础库很完整,缺点是尺寸较大,磁盘的利用率较低。
  • Debian Buster slim,这个版本是针对 Debian Buster 的“瘦身”后的版本。尺寸小,磁盘利用率高是其优点。但是,它缺少通用的包,可能会导致对部分的应用支持不好。

选项三:云计算上的 Linux 镜像 – Amazon Linux 2

今天当我们谈到 Docker 在生产环境中的部署的时候,不能缺少的一个话题就是云计算。相比较起来,云计算上 Linux 以及 Docker 的使用与部署与我们熟悉的传统方式有一些区别。例如安全性的要求、与云计算平台的集成以及管理工具等。于是云计算的平台 Linux 分发版本以及 Docker 镜像也就应需而生。AWS 就提供了 Amazon Linux 的两个版本:Amazon Linux 2 和 Amazon Linux。其中,Amazon Linux 2 是 Amazon Linux 的新一代的 Linux 服务器操作系统版本,这也是 AWS 官方推荐使用的版本。至于选择 Amazon Linux 2 的的理由,简单来说这是一个由 Amazon 提供长期支持的(LTS)、进行了针对性性能优化的、强调安全的、免费的 Linux 分发版本以及 Docker 的镜像。

选项四:Alpine 镜像

很难想象,Alpine 这个 Linux 发行版已经有了 14 年的历史。但广为大家所熟知还是要拜 Docker 的流行所赐。最初,Alpine Linux 是 LEAF 项目(Linux Embedded Appliance Framework Project)的一个分支。LEAF 项目的设计的目标是开发出一个可以放在单个软盘上的 Linux 发行版,而后继的 Alpine 在此基础之上附加了一些更常用的软件包例如 Squid、Samba 等等。因此,Alpine 的典型特征就是“尺寸小”!当然为达成这个目标就要做出妥协。为减少尺寸就不得不放弃我们熟悉的 Linux 环境中最常见的 GNU C Library(glibc)

此外,我们常用的 Linux 工具(例如 grep、ls、cp 等等)则采用了 BusyBox 以求进一步压缩空间。这种努力的结果是非常有成效的,alpine:latest 镜像的尺寸只有区区的5.59MB,而与之相对的 ubuntu:18.04 的镜像的尺寸却高达 64.2MB。简单计算下来,Alpine 的磁盘空间的需求只是 Ubuntu 18.04 的 8.7% !

对比 – Docker 基础镜像的尺寸

想象一下,在真实的生产环境中我们部署的 Docker 实例的数量可能成百、上千。考虑到数量的因素,Docker 镜像的尺寸就应当是我们考量的一个重要依据。此外启动一个 Docker 实例我们往往需要在尽可能短的时间内完成,Docker 镜像的尺寸无疑也是一个关键因素。那么,我们就将上面列举的 Docker 镜像在尺寸方面做一个对比 :

测试日期:2020 年 2 月 28 日

Linux 分发版本

镜像名称

拉取命令

尺寸

Ubuntu

ubuntu:18.04

docker pull ubuntu:18.04

64.2MB

Alphine

alpine:latest

docker pull alpine:latest

5.59MB

Debian

debian:buster

docker pull amd64/debian:buster

114MB

CentOS

centos:8

docker pull centos:8

237MB

Amazon Linux 2

amazonlinux:latest

docker pull amazonlinux:latest

163MB

Debian

python:3.7

docker pull python:3.7

919MB

Alphine

python:3.7-slim

docker pull python:3.7-slim

179MB

好了,在这一项的测试中名次如下 :

python:3.7 > centos:8 > python:3.7-slim > amazonlinux:latest > debian:buster > ubuntu:18.04 > alpine:latest

如果从这个排名来看 centos 8 无疑表现的差强人意,故被淘汰。从数字来看似乎 alpine 是最好的选择。且慢,我们再来进行下一项测试- 构建时间。

对比 – Docker 镜像的构建时间

在大多数的时间里,我们所使用的 Docker 镜像都需要从基础镜像开始构建。例如下面的这个 Dockerfile 就用来构建一个 Flask 的应用

# Dockerfile-flask

# Simply inherit the Python 3 image.
FROM python:3

# Set an environment variable
ENV APP /app

# Create the directory
RUN mkdir $APP
WORKDIR $APP

# Expose the port uWSGI will listen on
EXPOSE 5000

# Copy the requirements file in order to install
# Python dependencies
COPY requirements.txt .

# Install Python dependencies
RUN pip install -r requirements.txt

# We copy the rest of the codebase into the image
COPY . .

# Finally, we run uWSGI with the ini file

这种模式带来的问题就是我们不得不考虑构建带来的额外的开销。尤其在一个复杂的项目中,我们需要构建的则不仅仅上面这样简单的场景,复杂的应用往往需要一个较长的构建时间。如果构建时间的开销比较大或者比较复杂,则必然增加了额外的管理、部署以及监控的成本。我的这个测试场景比较简单,只是安装 Python3,以及比较常见的 python 包 numpy、matplotlib 和 pandas。看看每一种 Docker 基础镜像的构建所需的时间是多少。

1、ubuntu:18 , 构建时间 1 分 31.044 秒

FROM ubuntu:18.04

RUN apt-get update -y && \
    apt-get install -y python3.7 python3-pip python3.7-dev
RUN pip3 install --upgrade pip
RUN pip3 install --no-cache-dir numpy matplotlib pandas

2、amazonlinux:2 , 构建时间 30.898 秒

FROM amazonlinux:latest

RUN yum update -y && \
yum install -y python3 python3-devel
RUN pip3 install --no-cache-dir numpy matplotlib pandas

3、debian:latest , 构建时间 52.237 秒

FROM debian:buster

RUN apt-get update -y && \
    apt-get install -y python3 python3-pip python3-dev
RUN pip3 install --no-cache-dir numpy matplotlib pandas

4、python:latest,构建时间 35.752 秒

FROM python:3.7

RUN apt-get update -y && \
    apt-get install -y python3-pip

RUN pip3 install pip --upgrade
RUN pip3 install --no-cache-dir numpy matplotlib pandas

5、python:3.7-slim, 构建时间 53.475 秒

FROM python:3.7

RUN apt-get update -y && \
    apt-get install -y python3-pip

RUN pip3 install pip --upgrade
RUN pip3 install --no-cache-dir numpy matplotlib pandas

6、alpine:latest ,构建时间 24 分 43.122 秒

FROM alpine:latest

RUN apk update  && \
    apk add  --no-cache --update \
        gcc make automake gcc g++ python3 python3-dev cython freetype-dev
RUN pip3 install --upgrade pip
RUN pip3 install --no-cache-dir numpy matplotlib pandas

测试的结果出来了:

alpine:latest > ubuntu:18.04 > > python:3.7-slim > debian:buster > python:last > amazonlinux:latest

确实没有看错,被我们寄予厚望的 Alpine 镜像的构建时间居然是24 分钟以上。与构建速度最快的 Amazon Linux 2 比较起来足足慢了有24 倍的时间!!

如果细心一些,你会发现这个 Dockerfile 与上面的几个不同,多出了 gcc、make、automake、g++这些与编译工具和几个库。事实上,在我第一次构建的时候遇到了这样的错误信息 :

docker python 镜像装依赖 python打包docker镜像_docker_02

这真是未曾预料的问题啊!深究之下终于发现在 Appine 使用 pip 安装 matplotlib 以及 pandas 的时候,并不是从 PyPi 的仓库中下载 whl 包,而是需要下载源代码然后编译再进行安装的。标准的预编译的 Python 包居然无法直接安装,这究竟是为什么?

答案原来出在 Alpine 使用的 musl 库上。原来几乎全部 Linux 发行版都使用 GNU 版本的 C 标准库(glibc)。但是 Alpine Linux 为了减小存储空间而使用了 musl 库。而我们通过 pip 安装的这些二进制 Python 包是基于 glibc 编译而成的。因此 Alpine 无法安装这些 python 库,只能通过源码编译的方式来进行安装。这也就是为什么 Alpine 的 Docker file 会与其它的不同,以及花费如此之多的时间进行构建的秘密。

我相信熟悉 Alpine 的用户会与我争论,Alpine 的 Edge 版本会解决这个问题。不幸的是,Edge 版本目前还不是稳定版本。如果用于生产环境,我们还是不要自寻烦恼为好。

docker python 镜像装依赖 python打包docker镜像_python_03

结论

这个测试也许不能覆盖我们需要的各个角度,但至少给我们提供了一个选择的参考。而我在这个测试之后也得到了自己需要的结果,

  • Alpine 显然不适合作为 Python 应用的基础镜像。尽管它提供了惊人存储的空间上效率,但它对于 Python 包支持的不足的缺陷是难以弥补的。也许 Alpine 更适合于一些对于镜像尺寸敏感的场合,还可以考虑将它用于你的 Go 应用。
  • 在 AWS 云计算的环境中,Amazon Linux 2 的性能表现是有目共睹的。如果你希望得到一个稳定、安全以及高性能的 Python 基础镜像,那就不要忘记 Amazon Linux 2 这个选择。
  • Ubuntu 18.04 以及 Debian 10 表现的中规中矩,完全在我的意料之中。考虑到 Debian 10(Buster)较 Ubuntu 更新一些。这应该是一个好选择。不过随着 Ubuntu 20.04 LTS 即将发布,在我的候选清单上也许要多出一个。
  • 至于 Docker 官方的 Python 镜像,并没有看出明显的优点。考虑到安全性与维护性的问题,我不认为这是个好的选择。
  • 关于 Docker 基础镜像的选择,还需要考虑的一点就是 Linux 一致性的问题。杂乱的、多样化的 Linux 版本会在大规模应用的时候带来不可预估的风险,不可掉以轻心。