为什么 Alpine Docker 镜像比 Ubuntu 镜像慢 50% 以上?

Und*_*ryx 50 performance ubuntu docker alpine-linux

我注意到我的 Python 应用程序在运行时比在 Ubuntu 上不使用 Docker时运行慢得多python:2-alpine3.6。我想出了两个小的基准测试命令,当我在 Ubuntu 服务器上运行它们时,以及当我在 Mac 上使用 Docker 时,两种操作系统之间都存在明显的差异。

$ BENCHMARK="import timeit; print(timeit.timeit('import json; json.dumps(list(range(10000)))', number=5000))"
$ docker run python:2-alpine3.6 python -c $BENCHMARK
7.6094589233
$ docker run python:2-slim python -c $BENCHMARK
4.3410820961
$ docker run python:3-alpine3.6 python -c $BENCHMARK
7.0276606959
$ docker run python:3-slim python -c $BENCHMARK
5.6621271420
Run Code Online (Sandbox Code Playgroud)

我还尝试了以下不使用 Python 的“基准”:

$ docker run -ti ubuntu bash
root@6b633e9197cc:/# time $(i=0; while (( i < 9999999 )); do (( i ++
)); done)

real    0m39.053s
user    0m39.050s
sys     0m0.000s
$ docker run -ti alpine sh
/ # apk add --no-cache bash > /dev/null
/ # bash
bash-4.3# time $(i=0; while (( i < 9999999 )); do (( i ++ )); done)

real    1m4.277s
user    1m4.290s
sys     0m0.000s
Run Code Online (Sandbox Code Playgroud)

什么可能导致这种差异?

Tom*_*art 65

我已经运行了与您相同的基准测试,仅使用 Python 3:

$ docker run python:3-alpine3.6 python --version
Python 3.6.2
$ docker run python:3-slim python --version
Python 3.6.2
Run Code Online (Sandbox Code Playgroud)

导致超过 2 秒的差异:

$ docker run python:3-slim python -c "$BENCHMARK"
3.6475560404360294
$ docker run python:3-alpine3.6 python -c "$BENCHMARK"
5.834922112524509
Run Code Online (Sandbox Code Playgroud)

Alpine 正在使用libcmusl项目镜像 URL)不同的(基础系统库)实现。这些库之间存在许多差异。因此,每个库在某些用例中可能表现得更好。

这是上面这些命令之间strace 差异。输出开始与第 269 行不同。当然内存中的地址不同,但除此之外非常相似。大部分时间显然都花在等待python命令完成上。

安装strace到两个容器中后,我们可以获得更有趣的跟踪(我已将基准测试中的迭代次数减少到 10 次)。

例如,glibc以下列方式加载库(第 182 行):

openat(AT_FDCWD, "/usr/local/lib/python3.6", O_RDONLY|O_NONBLOCK|O_DIRECTORY|O_CLOEXEC) = 3
getdents(3, /* 205 entries */, 32768)   = 6824
getdents(3, /* 0 entries */, 32768)     = 0
Run Code Online (Sandbox Code Playgroud)

相同的代码musl

open("/usr/local/lib/python3.6", O_RDONLY|O_DIRECTORY|O_CLOEXEC) = 3
fcntl(3, F_SETFD, FD_CLOEXEC)           = 0
getdents64(3, /* 62 entries */, 2048)   = 2040
getdents64(3, /* 61 entries */, 2048)   = 2024
getdents64(3, /* 60 entries */, 2048)   = 2032
getdents64(3, /* 22 entries */, 2048)   = 728
getdents64(3, /* 0 entries */, 2048)    = 0
Run Code Online (Sandbox Code Playgroud)

我并不是说这是主要区别,但减少核心库中的 I/O 操作数量可能有助于提高性能。从差异中您可以看到,执行相同的 Python 代码可能会导致系统调用略有不同。最重要的可能是优化循环性能。我没有足够的资格来判断性能问题是由内存分配还是其他一些指令引起的。

musl慢了 0.0028254222124814987 秒。随着差异随着迭代次数的增加,我认为差异在于 JSON 对象的内存分配。

如果我们将基准减少到仅导入,json我们会注意到差异并不是那么大:

$ BENCHMARK="import timeit; print(timeit.timeit('import json;', number=5000))"
$ docker run python:3-slim python -c "$BENCHMARK"
0.03683806210756302
$ docker run python:3-alpine3.6 python -c "$BENCHMARK"
0.038280246779322624
Run Code Online (Sandbox Code Playgroud)

加载 Python 库看起来相当。生成list()产生更大的差异:

$ BENCHMARK="import timeit; print(timeit.timeit('list(range(10000))', number=5000))"
$ docker run python:3-slim python -c "$BENCHMARK"
0.5666235145181417
$ docker run python:3-alpine3.6 python -c "$BENCHMARK"
0.6885563563555479
Run Code Online (Sandbox Code Playgroud)

显然,最昂贵的操作是json.dumps(),这可能表明这些库之间的内存分配存在差异。

再看一下基准测试musl内存分配确实稍微慢了一点:

                          musl  | glibc
-----------------------+--------+--------+
Tiny allocation & free |  0.005 | 0.002  |
-----------------------+--------+--------+
Big allocation & free  |  0.027 | 0.016  |
-----------------------+--------+--------+
Run Code Online (Sandbox Code Playgroud)

我不确定“大分配”是什么意思,但musl几乎慢了 2 倍,当您重复数千或数百万次此类操作时,这可能会变得很重要。

  • 只是一些更正。musl 不是 Alpine 的 _own_ glibc 实现。第一个 musl 不是 glibc 的(重新)实现,而是每个 POSIX 标准对 **libc** 的不同实现。第二个 musl 不是 Alpine 的 _own_ 东西,它是一个独立的、不相关的项目,并且 musl 不仅仅在 Alpine 中使用。 (17认同)
  • 0.0028 秒的差异在统计上是否显着?相对偏差仅为 0.0013%,您正在采集 10 个样本。那 10 次运行的(估计)标准偏差是多少(甚至最大-最小差异)? (2认同)
  • 这是使用 alpine 的最新分析:https://pythonspeed.com/articles/alpine-docker-python/ (2认同)
  • 这个问题仍然存在吗?特别是在 musl 1.2.1 发行版之后? https://musl.libc.org/releases.html (2认同)