我在使用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进行调查,这导致我发现我需要-特权来在docker内部运行perf,但随后性能差距消失了:
$ docker run -it --rm --privileged cpu:test ./cpu-test.py
4.1469762325286865
$

搜索与docker和--privileged有关的所有内容通常会导致由于我出于安全考虑不应该使用特权的原因,也没有发现对平凡代码产生严重性能影响的任何原因。

使用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运行时的代码完全不熟悉。

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

谢谢

最佳答案

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

由于添加seccomp:unconfined可以帮助Python程序以接近1.5x-2x的速度运行,因此分析的第一要点是查看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-进入容器,
  • 在带有--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启动容器之后,也执行--privileged flags,然后执行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配置文件运行。

    关于performance - Docker对CPU密集型代码的性能影响达到50%,我们在Stack Overflow上找到一个类似的问题:https://stackoverflow.com/questions/60840320/

    10-12 23:42