前言

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


相比起来,我不熟悉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-rc。需要注意的是,这个版本提供了多个不同的变体,如果搞不清楚这一点很容易在使用中遇到难以预料的问题。这些差异很大的变体包括了:

  • 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),取而代之的是一个迷你版的C标准库musl(musl.libc.org)。此外,我们常用的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++这些与编译工具和几个库。事实上,在我第一次构建的时候遇到了这样的错误信息 :

这真是未曾预料的问题啊!深究之下终于发现在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 版本目前还不是稳定版本。如果用于生产环境,我们还是不要自寻烦恼为好。

结论

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

  • 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版本会在大规模应用的时候带来不可预估的风险,不可掉以轻心。