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

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

我注意到我的 Python 应用程序是很多在 Ubuntu 上运行python:2-alpine3.6Docker 比不使用 Docker 运行速度要慢。我找到了两个小型基准测试命令,在 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

我还尝试了以下不使用 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

什么原因造成这种差异?

答案1

我也运行了与您相同的基准测试,仅使用 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

导致相差超过2秒:

$ docker run python:3-slim python -c "$BENCHMARK"
3.6475560404360294
$ docker run python:3-alpine3.6 python -c "$BENCHMARK"
5.834922112524509

Alpine 使用不同的实现libc(基础系统库)来自穆斯尔项目镜像网址)。 有许多这些库之间的区别。因此,每个库在某些用例中可能会表现得更好。

这是一个上述命令之间的 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

相同的代码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

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

  • glibc经过 10 次迭代:

    write(1, "0.032388824969530106\n", 210.032388824969530106)
    
  • musl经过 10 次迭代:

    write(1, "0.035214247182011604\n", 210.035214247182011604)
    

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

加载 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

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

再次回顾基准musl在内存分配方面确实稍微慢一些:

                          musl  | glibc
-----------------------+--------+--------+
Tiny allocation & free |  0.005 | 0.002  |
-----------------------+--------+--------+
Big allocation & free  |  0.027 | 0.016  |
-----------------------+--------+--------+

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

答案2

这是有趣的讨论在 alpine 邮件列表中,您可以阅读以下内容:

  • 这种情况是众所周知的吗?

是的。众所周知,一些工作负载(主要涉及 malloc)和大量 C 字符串操作的基准测试结果与 glibc 相比较差。

这主要是因为 musl 和 Alpine 的安全强化功能并非零成本,而且还因为 musl 不包含特定于微架构的优化,这意味着在 glibc 上您可能会获得针对您所使用的确切 CPU 手动调整的 strlen/strcpy 类型的函数。

但实际上,其性能对于大多数工作负载来说已经足够了。

  • musl 内存分配能很好地解释性能差异吗?

众所周知,某些内存分配模式会导致新强化 malloc 的性能不佳。但是,我认为强化 malloc 的安全优势在一定程度上弥补了性能成本。我们仍在努力优化强化 malloc。

一种解决方法可能是改用 jemalloc,它以软件包形式提供。我正在研究一种方法,使在性能关键型工作负载中始终使用 jemalloc 而不是强化 malloc,但这需要与 musl 作者进行讨论,我还没有讨论过。

  • 内存分配能解释整个事情吗?

我想说大概是 70%。

相关内容