要約
dockerはデフォルトでセキュリティ機構(Spectre脆弱性の対策)を有効にします。この影響で、RubyやPythonのようなインタプリタは速度が劣化します。特にCPU律速なプログラムで顕著に遅くなります(実行時間が倍くらいになることがあります)。
現象
Rubyで1億回ループするコードを、直接ホスト上で実行する場合と、docker上で実行する場合で実行時間を比較してみます。
直接ホスト上で実行した場合:
$ ruby -ve 't = Time.now; i=0;while i<100_000_000;i+=1;end; puts "#{ Time.now - t } sec"' ruby 2.7.1p83 (2020-03-31 revision a0c7c23c9c) [x86_64-linux] 1.321703922 sec
docker上で実行した場合:
$ docker run -it --rm ruby:2.7 ruby -ve 't = Time.now; i=0;while i<100_000_000;i+=1;end; puts "#{ Time.now - t } sec"' ruby 2.7.1p83 (2020-03-31 revision a0c7c23c9c) [x86_64-linux] 2.452876383 sec
dockerを使うと実行時間がおよそ倍になっています(1.3秒→2.5秒)。
docker runに--security-opt seccomp=unconfined
というオプションを与えて実行すると、ホストと同等程度の速度になります。
$ docker run --security-opt seccomp=unconfined -it --rm ruby:2.7 ruby -ve 't = Time.now; i=0;while i<100_000_000;i+=1;end; puts "#{ Time.now - t } sec"' ruby 2.7.1p83 (2020-03-31 revision a0c7c23c9c) [x86_64-linux] 1.333669449 sec
Ruby特有ではなく、Pythonでも同じ現象が起きます(i=0
/ while i<100000000: i+=1
という2行のコードで7.0秒→11秒)。
なお、カーネルの設定次第では、この現象が再現しないこともあります(後述)。
原因
LinuxカーネルにはSpectre脆弱性の対策がいくつか実装されています。
この対策の中に間接分岐予測を抑制するもの(STIBPと呼ばれる)があり、CPU律速なコードが50%ほど速度低下するということで、Linuxカーネルのデフォルトではオフになっています(参考)。 一方、dockerはデフォルトで、Spectre脆弱性の対策を有効にしてコンテナを実行します。*1
これはほぼすべてのプログラムの速度を低下させます*2が、RubyやPythonのようなインタプリタは間接分岐(switch/caseやdirect threadingなど)を多用しているので、特に強く影響を受けます。dockerの外では対策がオフなので高速に実行され、dockerの中ではオンなので速度劣化が起きます。
証拠として、上記のベンチマークプログラムのperf statを見てみると、branch-missesが圧倒的に増えていることが観察できました(522,663回→199,260,442回)。
https://www.phoronix.com/scan.php?page=article&item=linux-420-stibp&num=2
--security-opt seccomp=unconfined
あり(Spectre対策なし)
Performance counter stats for process id '153095': 1,235.67 msec task-clock # 0.618 CPUs utilized 8 context-switches # 0.006 K/sec 0 cpu-migrations # 0.000 K/sec 2 page-faults # 0.002 K/sec 4,284,307,990 cycles # 3.467 GHz 13,903,977,890 instructions # 3.25 insn per cycle 1,700,742,230 branches # 1376.378 M/sec 522,663 branch-misses # 0.03% of all branches 2.000223507 seconds time elapsed
--security-opt seccomp=unconfined
なし(Spectre対策あり)
Performance counter stats for process id '152556': 3,300.42 msec task-clock # 0.550 CPUs utilized 16 context-switches # 0.005 K/sec 2 cpu-migrations # 0.001 K/sec 2 page-faults # 0.001 K/sec 11,912,594,779 cycles # 3.609 GHz 13,906,818,105 instructions # 1.17 insn per cycle 1,701,237,677 branches # 515.460 M/sec 199,260,442 branch-misses # 11.71% of all branches 6.000985834 seconds time elapsed
なおこの問題は、STIBPがconditional(デフォルトはオフだがseccompで有効にできる)のときだけ起きます。STIBPがdisabled(常にオフ)またはforced(常にオン)の環境では、dockerだけ遅いという現象は起きません。disabledの場合ホストもdockerも速い(そして脆弱)、forcedの場合はホストもdockerも遅い、ということになります。*3
ちなみに、STIBP以外にも、投機的ストアバイアスのサイドチャネル攻撃の対策(spec_store_bypass_disable)もかなり速度劣化を起こすようです。笹田耕一さんの調査によると spectre_v2_user=off spec_store_bypass_disable=off
というカーネルオプションを用いることで、--security-opt seccomp=unconfined
なしのDocker上でも速度劣化がなくなったということでした。
対策
残念ながら、あまりオススメできる対策はありません。--security-opt seccomp=unconfined
をつければ速くなりますが、Spectre攻撃に対して脆弱になるのでやめたほうがいいでしょう(ちなみに--privileged
でも速くなります)。
ただし、これはCPU律速なコードでしか問題にならないと思います。Ruby on Railsを使ったWebアプリは多くの場合IOやGCが律速になっているので、Spectre対策を止めても観測できるほどの速度向上はしないでしょう(たぶん)。なので当面気にしないことをおすすめします。
長期的には、CPU側でSpectre対策がなされればこの問題は解決すると思います(ただし10年スパンで考える必要はある)。もしくは、笹田耕一さんがRubyKaigi 2019で発表していた実行方式(context threading)は間接分岐を多用しないので、これが取り込まれれば解決するかもしれません(数年スパンで考える必要はある)。
謝辞
- 相談ツイートに反応してくださったみなさん(特に@ryotaraiさんのリツイート)
- ruby-jp Slackの #container チャンネルのみなさん(特に件のオプションが効く理由を調査してくれた笹田耕一さん)
- Rubyコミッタのみなさん
余談
Ruby 3の速度目標ベンチマークになってるoptcarrotがこの影響をとても受けて、ホストだと33 fpsなのがdockerだと14 fpsになるので困ってました。原因と(一応の)対策がわかったので、これでベンチマークが取れる。
蛇足(2020/05/23 12:00)
dockerが遅いと言えば、ふつうはdockerが仮想化しているファイルシステムやネットワーク関係を疑うところですが、メイン部で一切syscallをしないCPU律速なコードでも遅くなることがある、というのが意外なところでした。
「Ruby/Pythonコードは」というタイトルにしてしまいましたが、本文に書いた通りだいたいどんなプログラムでも遅くなります(脚注に書いたように、実際Javaやmemcachedなども遅くなるらしい)。最初に気づいたのがRubyで、Rubyにはちょっと詳しいので限定して書いてしまった。他のプログラムよりRuby/Pythonのようなインタプリタは重症度がひどい可能性はあると思ってますが、特に比較はしていないし、ワークロードに強く依存するので比較は難しそうです。
この問題は「誰かが悪い」ということではないです :-) 用途を考えると、dockerがSpectre対策をデフォルトで有効にするのは理解できます(意識してやってるのか、たまたまそうなってるのかは不明ですが)。
*1:笹田注:より正確には、Linuxの起動時の設定が、「通常実行では脆弱性対策を有効にしない(脆弱なまま)だが、seccompなどを利用すると有効にする」となってるときにこの現象が起きるようです。手元の Ubuntu 18.04 がそういう設定でした。最近の Docker は、デフォルトでseccompを有効にするため、何もしないとこれらの対策が入り、速度劣化が観察できる、というようです。Linux Kernel の起動オプションで色々選べるようです。具体的な設定は Spectre Side Channels — The Linux Kernel documentation をご覧下さい。Docker起動時にseccompの設定で制御できるのかはわかりませんでした(出来ても良さそうですが)。
*2:この記事によると、STIBPはJava、Node.js、memcached、PHPなどのベンチマークで速度劣化が確認されたようです。
*3:どういう設定になっているかは /sys/devices/system/cpu/vulnerabilities/spectre_v2 というファイルを見ればわかります。