首页 > 解决方案 > Docker 对 CPU 密集型代码的性能影响 50%

问题描述

我对使用 docker 或任何容器很陌生,所以如果我错过了其他人都已经知道的明显内容,请保持温和。我已经搜索了我能想到的所有地方,但还没有看到这个问题得到解决。

我正在尝试评估在 docker 中运行基准测试的性能成本,我发现了令人惊讶的巨大差异,这些差异对我来说没有意义。我用这个 Dockerfile 创建了一个简单的 Docker 映像:

FROM ubuntu:18.04

RUN apt -y -q update && apt -y -q install python3 vim strace linux-tools-common \
        linux-tools-4.15.0-74-generic linux-cloud-tools-4.15.0-74-generic

ADD . /workspace
WORKDIR /workspace

我有一个用于测试的简单 python 脚本:

$ cat cpu-test.py
#!/usr/bin/env python3

import math
from time import time

N = range(10)
N_i = range(1_000)
N_j = range(1_000)
x = 1

start = time()
for _ in N:
    for i in N_i:
        for j in N_j:
            x += -1**j * math.sqrt(i)/max(j,2)
stop = time()
print(stop-start)

然后我将正常运行它与在容器中运行进行比较:

$ ./cpu-test.py
4.077672481536865
$ docker run -it --rm cpu:test ./cpu-test.py
6.113868236541748
$

我正在使用 进行调查perf,这使我发现我需要 --privileged 在 docker 中运行 perf,但随后性能差距消失了:

$ docker run -it --rm --privileged cpu:test ./cpu-test.py
4.1469762325286865
$ 

搜索与--privilegeddocker 相关的任何事情,并且大多会导致出于安全考虑我不应该使用特权的原因,但没有发现任何关于对普通代码的严重性能影响的信息。

使用 perf 比较有/无特权运行,它们看起来完全不同:

凭借特权,性能报告中的前 5 名是:

     7.26%  docker   docker            [.] runtime.mapassign_faststr
     6.21%  docker   docker            [.] runtime.mapaccess2
     6.12%  docker   [kernel]          [k] 0xffffffff880015e0
     5.37%  docker   [kernel]          [k] 0xffffffff87faac87
     4.92%  docker   docker            [.] runtime.retake

在没有特权的情况下运行会导致:

    11.11%  docker   docker            [.] runtime.evacuate_faststr
     8.14%  docker   docker            [.] runtime.scanobject
     7.18%  docker   docker            [.] runtime.mallocgc
     5.10%  docker   docker            [.] runtime.mapassign
     4.44%  docker   docker            [.] runtime.growslice

我不知道这是否有意义,因为我对 docker 运行时的代码一点也不熟悉。

难道我做错了什么?或者我需要转动一些特殊的旋钮吗?

谢谢

标签: performancedockerdocker-privileged

解决方案


seccomp:unconfined添加到命令中的标志docker run提高了 python 程序的性能。seccomp是一个 linux 内核功能,可用于限制容器内可用的操作,方法是允许和禁止对主机进行某些系统调用。这减少了容器对主机的访问,并且在安全术语中,有助于减少容器的访问attack surface。默认seccomp配置文件为正在运行的容器禁用 44 个系统调用,包括perf_event_open当您添加标志时,--security-opt seccomp:unconfined为正在运行的容器启用所有系统调用。

由于添加seccomp:unconfined有助于 Python 程序以几乎 1.5 倍至 2 倍的速度运行,因此分析的第一点是查看strace输出并查看在未添加该标志时是否有任何系统调用减慢速度。

  • --security-opt seccomp:unconfined标志的输出
strace -c -f -S name docker run -it --rm --security-opt seccomp:unconfined cpu:test ./cpu-test.py

5.4090752601623535
% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
  2.00    0.000194          32         6         6 access
  0.11    0.000011          11         1           arch_prctl
  0.33    0.000032          11         3           brk
  0.00    0.000000           0         1           capget
  0.10    0.000010           1        16           clone
  0.64    0.000062           4        17           close
  0.00    0.000000           0         5         2 connect
  0.00    0.000000           0         1           epoll_create1
  0.00    0.000000           0        14         2 epoll_ctl
  0.22    0.000021           0        62           epoll_pwait
  0.29    0.000028          28         1           execve
  0.00    0.000000           0         8           fcntl
  0.67    0.000065           8         8           fstat
 68.87    0.006687          22       310        24 futex
  0.02    0.000002           2         1           getgid
  0.00    0.000000           0         3           getpeername
  0.00    0.000000           0         2           getpid
  0.00    0.000000           0         1           getrandom
  0.00    0.000000           0         3           getsockname
  0.10    0.000010           1        17           gettid
  0.02    0.000002           1         2           getuid
  0.00    0.000000           0         5         1 ioctl
  0.00    0.000000           0         1           lseek
  5.83    0.000566           7        84           mmap
  2.12    0.000206           5        39           mprotect
  0.35    0.000034           2        14           munmap
  0.00    0.000000           0        12         9 newfstatat
  1.43    0.000139          10        14           openat
  0.13    0.000013          13         1           prlimit64
 10.21    0.000991          10       102           pselect6
  0.55    0.000053           2        34        10 read
  0.00    0.000000           0         1           readlinkat
  3.14    0.000305           3       120           rt_sigaction
  0.36    0.000035           1        53           rt_sigprocmask
  0.04    0.000004           4         1           sched_getaffinity
  2.04    0.000198           5        42           sched_yield
  0.18    0.000017           1        17           set_robust_list
  0.03    0.000003           3         1           set_tid_address
  0.00    0.000000           0         3           setsockopt
  0.22    0.000021           1        34           sigaltstack
  0.00    0.000000           0         5           socket
  0.00    0.000000           0         7           write
------ ----------- ----------- --------- --------- ----------------
100.00    0.009709                  1072        54 total
  • --security-opt seccomp:unconfined标志输出
strace -c -f -S name docker run -it --rm cpu:test ./cpu-test.py

8.161764860153198
% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
  0.08    0.000033           6         6         6 access
  0.04    0.000015          15         1           arch_prctl
  0.02    0.000007           2         3           brk
  0.00    0.000000           0         1           capget
  0.22    0.000087           6        15           clone
  0.26    0.000102           6        17           close
  0.04    0.000015           3         5         2 connect
  0.00    0.000000           0         1           epoll_create1
  0.14    0.000054           4        14         2 epoll_ctl
  2.31    0.000916          23        40           epoll_pwait
  0.00    0.000000           0         1           execve
  0.00    0.000000           0         8           fcntl
  0.07    0.000027           3         8           fstat
 72.00    0.028580          99       290        21 futex
  0.01    0.000002           2         1           getgid
  0.01    0.000002           1         3           getpeername
  0.00    0.000000           0         2           getpid
  0.00    0.000000           0         1           getrandom
  0.01    0.000002           1         3           getsockname
  0.10    0.000039           2        16           gettid
  0.01    0.000002           1         2           getuid
  0.01    0.000005           1         5         1 ioctl
  0.00    0.000000           0         1           lseek
  1.33    0.000529           7        80           mmap
  0.72    0.000284           8        37           mprotect
  0.31    0.000125           8        15           munmap
  0.07    0.000026           2        12         9 newfstatat
  0.20    0.000080           6        14           openat
  0.01    0.000003           3         1           prlimit64
 20.04    0.007954          42       189           pselect6
  0.21    0.000085           3        34        10 read
  0.00    0.000000           0         1           readlinkat
  0.46    0.000182           2       120           rt_sigaction
  0.52    0.000207           4        50           rt_sigprocmask
  0.01    0.000004           4         1           sched_getaffinity
  0.27    0.000108           5        20           sched_yield
  0.11    0.000045           3        16           set_robust_list
  0.01    0.000003           3         1           set_tid_address
  0.01    0.000002           1         3           setsockopt
  0.32    0.000127           4        32           sigaltstack
  0.02    0.000008           2         5           socket
  0.09    0.000035           5         7           write
------ ----------- ----------- --------- --------- ----------------
100.00    0.039695                  1082        51 total

还没有什么重要的。所以接下来要分析的是 Python 程序本身。

以下所有用于获取执行时间配置文件的命令都运行了 5 次,并从该样本空间中选择了一个。时间的变化非常小。

在后台运行容器,然后exec-ing 进入容器,

  • --security-opt seccomp:unconfined在带有标志的容器内运行的 Python 程序上执行配置文件的输出
docker exec -it cpu-test-seccomp bash
root@133453c7ccc6:/workspace# python3 -m cProfile ./cpu-test.py 

7.339433908462524
         20000069 function calls in 7.340 seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap>:103(release)
        1    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap>:143(__init__)
        1    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap>:147(__enter__)
        1    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap>:151(__exit__)
        1    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap>:157(_get_module_lock)
        1    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap>:176(cb)
        2    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap>:211(_call_with_frames_removed)
        1    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap>:222(_verbose_message)
        1    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap>:232(_requires_builtin_wrapper)
        1    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap>:307(__init__)
        1    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap>:311(__enter__)
        1    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap>:318(__exit__)
        4    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap>:321(<genexpr>)
        1    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap>:369(__init__)
        1    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap>:416(parent)
        1    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap>:424(has_location)
        1    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap>:433(spec_from_loader)
        1    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap>:504(_init_module_attrs)
        1    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap>:564(module_from_spec)
        1    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap>:58(__init__)
        1    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap>:651(_load_unlocked)
        1    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap>:707(find_spec)
        1    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap>:728(create_module)
        1    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap>:736(exec_module)
        1    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap>:753(is_package)
        1    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap>:78(acquire)
        1    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap>:843(__enter__)
        1    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap>:847(__exit__)
        1    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap>:870(_find_spec)
        1    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap>:936(_find_and_load_unlocked)
        1    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap>:966(_find_and_load)
        1    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap>:997(_handle_fromlist)
        1    5.540    5.540    7.340    7.340 cpu-test.py:3(<module>)
        3    0.000    0.000    0.000    0.000 {built-in method _imp.acquire_lock}
        1    0.000    0.000    0.000    0.000 {built-in method _imp.create_builtin}
        1    0.000    0.000    0.000    0.000 {built-in method _imp.exec_builtin}
        1    0.000    0.000    0.000    0.000 {built-in method _imp.is_builtin}
        3    0.000    0.000    0.000    0.000 {built-in method _imp.release_lock}
        2    0.000    0.000    0.000    0.000 {built-in method _thread.allocate_lock}
        2    0.000    0.000    0.000    0.000 {built-in method _thread.get_ident}
        1    0.000    0.000    0.000    0.000 {built-in method builtins.any}
        1    0.000    0.000    7.340    7.340 {built-in method builtins.exec}
        4    0.000    0.000    0.000    0.000 {built-in method builtins.getattr}
        5    0.000    0.000    0.000    0.000 {built-in method builtins.hasattr}
 10000000    1.228    0.000    1.228    0.000 {built-in method builtins.max}
        1    0.000    0.000    0.000    0.000 {built-in method builtins.print}
 10000000    0.571    0.000    0.571    0.000 {built-in method math.sqrt}
        2    0.000    0.000    0.000    0.000 {built-in method time.time}
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}
        2    0.000    0.000    0.000    0.000 {method 'get' of 'dict' objects}
        2    0.000    0.000    0.000    0.000 {method 'rpartition' of 'str' objects}
  • --security-opt在没有标志的容器内运行的 Python 程序上执行配置文件的输出
docker exec -it cpu-test-no-seccomp bash
root@500724539bd0:/workspace# python3 -m cProfile ./cpu-test.py 
11.848757982254028
         20000069 function calls in 11.849 seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap>:103(release)
        1    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap>:143(__init__)
        1    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap>:147(__enter__)
        1    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap>:151(__exit__)
        1    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap>:157(_get_module_lock)
        1    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap>:176(cb)
        2    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap>:211(_call_with_frames_removed)
        1    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap>:222(_verbose_message)
        1    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap>:232(_requires_builtin_wrapper)
        1    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap>:307(__init__)
        1    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap>:311(__enter__)
        1    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap>:318(__exit__)
        4    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap>:321(<genexpr>)
        1    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap>:369(__init__)
        1    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap>:416(parent)
        1    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap>:424(has_location)
        1    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap>:433(spec_from_loader)
        1    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap>:504(_init_module_attrs)
        1    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap>:564(module_from_spec)
        1    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap>:58(__init__)
        1    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap>:651(_load_unlocked)
        1    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap>:707(find_spec)
        1    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap>:728(create_module)
        1    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap>:736(exec_module)
        1    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap>:753(is_package)
        1    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap>:78(acquire)
        1    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap>:843(__enter__)
        1    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap>:847(__exit__)
        1    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap>:870(_find_spec)
        1    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap>:936(_find_and_load_unlocked)
        1    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap>:966(_find_and_load)
        1    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap>:997(_handle_fromlist)
        1    8.654    8.654   11.849   11.849 cpu-test.py:3(<module>)
        3    0.000    0.000    0.000    0.000 {built-in method _imp.acquire_lock}
        1    0.000    0.000    0.000    0.000 {built-in method _imp.create_builtin}
        1    0.000    0.000    0.000    0.000 {built-in method _imp.exec_builtin}
        1    0.000    0.000    0.000    0.000 {built-in method _imp.is_builtin}
        3    0.000    0.000    0.000    0.000 {built-in method _imp.release_lock}
        2    0.000    0.000    0.000    0.000 {built-in method _thread.allocate_lock}
        2    0.000    0.000    0.000    0.000 {built-in method _thread.get_ident}
        1    0.000    0.000    0.000    0.000 {built-in method builtins.any}
        1    0.000    0.000   11.849   11.849 {built-in method builtins.exec}
        4    0.000    0.000    0.000    0.000 {built-in method builtins.getattr}
        5    0.000    0.000    0.000    0.000 {built-in method builtins.hasattr}
 10000000    2.155    0.000    2.155    0.000 {built-in method builtins.max}
        1    0.000    0.000    0.000    0.000 {built-in method builtins.print}
 10000000    1.039    0.000    1.039    0.000 {built-in method math.sqrt}
        2    0.000    0.000    0.000    0.000 {built-in method time.time}
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}
        2    0.000    0.000    0.000    0.000 {method 'get' of 'dict' objects}
        2    0.000    0.000    0.000    0.000 {method 'rpartition' of 'str' objects}

由于此处的分析开销,这两种情况下的时间都略高。但是这里有两件事值得注意 -

  • 内置函数math.sqrtbuiltins.max函数在它们的执行时间上显示出几乎 1.5-2 倍的差异,这种差异变得明显,因为这些函数被调用了 10000000 次。

  • 没有标志的总执行时间会变慢,这可以从builtins.exec函数及其执行时间中看出。

为了更多地了解这种现象,删除了math.sqrt以及功能。max下面的行cpu-test.py-

x += -1**j * math.sqrt(i)/max(j,2)

改为——

x += 1

并且该import math行也被删除,从而减少了import语句的大量开销。

  • --security-opt seccomp:unconfined
root@133453c7ccc6:/workspace# python3 -m cProfile ./cpu-test.py 
0.7199039459228516
         8 function calls in 0.720 seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap>:997(_handle_fromlist)
        1    0.720    0.720    0.720    0.720 cpu-test.py:4(<module>)
        1    0.000    0.000    0.720    0.720 {built-in method builtins.exec}
        1    0.000    0.000    0.000    0.000 {built-in method builtins.hasattr}
        1    0.000    0.000    0.000    0.000 {built-in method builtins.print}
        2    0.000    0.000    0.000    0.000 {built-in method time.time}
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}


  • 没有--security-opt seccomp:unconfined
root@500724539bd0:/workspace# python3 -m cProfile ./cpu-test.py
1.0778992176055908
         8 function calls in 1.078 seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.000    0.000    0.000    0.000 <frozen importlib._bootstrap>:997(_handle_fromlist)
        1    1.078    1.078    1.078    1.078 cpu-test.py:4(<module>)
        1    0.000    0.000    1.078    1.078 {built-in method builtins.exec}
        1    0.000    0.000    0.000    0.000 {built-in method builtins.hasattr}
        1    0.000    0.000    0.000    0.000 {built-in method builtins.print}
        2    0.000    0.000    0.000    0.000 {built-in method time.time}
        1    0.000    0.000    0.000    0.000 {method 'disable' of '_lsprof.Profiler' objects}

perf record -e ./cpu-test.py在用启动容器后也做 a --privileged flags,然后做 a perf report,我们可以看到 -

Samples: 20K of event 'cycles:ppp', Event count (approx.): 17551108136                                                                                                                                      
Overhead  Command  Shared Object      Symbol                                                                                                                                                                
  14.56%  python3  python3.6          [.] 0x0000000000181c0b
  11.65%  python3  python3.6          [.] _PyEval_EvalFrameDefault
   5.75%  python3  python3.6          [.] PyDict_GetItem
   3.43%  python3  python3.6          [.] PyDict_SetItem
   1.69%  python3  python3.6          [.] 0x0000000000181e45
   1.68%  python3  python3.6          [.] 0x0000000000181c23
   1.59%  python3  python3.6          [.] 0x00000000001705c9
   1.54%  python3  python3.6          [.] 0x0000000000181a88
   1.54%  python3  python3.6          [.] 0x0000000000181bfa
   1.48%  python3  python3.6          [.] 0x0000000000181c56
   1.48%  python3  python3.6          [.] 0x0000000000181c71
   1.42%  python3  python3.6          [.] 0x0000000000181c42
   1.37%  python3  python3.6          [.] 0x0000000000181c8a
   1.28%  python3  python3.6          [.] 0x0000000000181c01
   1.09%  python3  python3.6          [.] _PyObject_GC_New
   0.96%  python3  python3.6          [.] PyNumber_Multiply
   0.63%  python3  python3.6          [.] PyLong_AsDouble
   0.59%  python3  python3.6          [.] PyObject_GetAttr
   0.57%  python3  python3.6          [.] 0x00000000000c4df9
   0.57%  python3  python3.6          [.] 0x0000000000165808
   0.56%  python3  python3.6          [.] PyObject_RichCompare
   0.53%  python3  python3.6          [.] PyNumber_Negative

大部分时间都花在 中_PyEval_EvalFrameDefault,这公平地表明大部分时间都花在了解释器执行字节码上。

可以公平地假设增加--security-opt seccomp:unconfined解释器在执行字节码时会加快速度。这需要在Python内部进行一些挖掘。

请注意,在这两种情况下,反汇编的输出是相同的,运行和--seccomp:unconfined使用默认的 seccomp 配置文件。


推荐阅读