CoreOS Container Linuxにおいてリアルタイムプロセスを実行できない問題

はじめに

こんにちは、技術顧問の武内です。

本記事はサイボウズの次期インフラ開発チーム(Necoチーム)が遭遇した、CoreOS Container Linux (以降 CoreOS)においてリアルタイムプロセスを実行できないという問題について、次のようなことを記載したものです。

  • どういう問題なのか
  • どのように根本原因を突き止めたのか
  • 今後どのように対処するのか

問題要旨

本来なら成功するはずのroot権限におけるリアルタイムプロセスの実行が失敗する

根本原因

  • CoreOSではカーネルのリアルタイムグループスケジューリングという機能が有効になっている
  • 同機能が有効な場合、cpu cgroup配下のプロセスはデフォルトではリアルタイムプロセスを実行できない
  • systemd環境下で生成されたプロセスは何らかのcpu cgroupに所属させられる

対処方法

リアルタイムプロセスが属するcpu cgroupの設定を変更する。具体的には/sys/fs/cgroup/cpu/<cpu cgroup名>/cpu.rt_runtime_usを1以上にする

リアルタイムプロセスとは

リアルタイムプロセスについて知っているかたは本節を飛ばしてもらっても構いません

CPUコア上に複数の実行可能なプロセスが存在している場合、タイムスライスと呼ばれる所定の時間ごとにCPUコア上で動作するプロセスが変化していきます。たとえばCPUコア上に2つのプロセスp0,p1が存在する場合にはp0->p1->p0->p1...というように順番に動作します。これに対してリアルタイムプロセスは通常のプロセスが何個存在していようとも、それらよりも必ず優先的に、自発的にCPUを明け渡すまで無制限に実行できます。

プロセスをリアルタイムプロセスにするにはsched_setscheduler()システムコールやchrtコマンドを用います。実行可能になった場合には何が何でもすぐに実行したいようなリアルタイム性が必要な処理がある場合にリアルタイムプロセスを使います。例えばクラスタのハートビート処理などに用いられます。

リアルタイムプロセスは通常のプロセススケジューリングに求められる「各プロセスを平等に動作させる」という機能を無視して動作するため、バグによって自発的にCPUを明け渡さないときにCPUを無期限に占有してしまいます。このため通常はrootユーザでしか実行できないようになっています。

本記事ではリアルタイムプロセスについてはこれ以上述べませんが、詳細が気になるかたはman sched_setschedulerを参照してください。

調査ログ

問題発生

あるサービスの開発中に、当該サービスをリアルタイムプロセスとして動作させられないか検討した上で、その検証をすることにしました。そのためにはsystemdの当該サービスに関する設定ファイルに次のような記載をします。

...
[Service]
...
CPUSchedulingPolicy=rr           # プロセスをリアルタイムプロセス化する
CPUSchedulingPriority=50       # リアルタイム優先度を設定する。ここではリアルタイム優先度は重要ではないので気にしなくていいです
...

この設定はUbuntu 18.04上で実行した場合はうまく機能したのですが、同じことをCoreOS上で実行するとデーモンのリアルタイムプロセス化処理が権限違反(EPERM)によって失敗しました。このサービスはroot権限で動作させているので、通常は失敗しないはずです。このため、この事象がどういう理由によって発生しているのかを調査することにしました。

最小の再現手順を見つける

問題検出当初の問題再現方法がsystemdを介したものであるため、もっと簡単に再現できる方法を調査しました。幸いにもroot権限でchrtコマンドを使ってプロセスをリアルタイムプロセスとして動かすだけで簡単に再現することがわかりました。

# chrt -r 50 echo hello
chrt: failed to set pid 0's policy: Operation not permitted
# 

straceを使ってプログラムを動かしたところ、次のようにsched_setscheduler()システムコールを発行していました。

...
sched_setscheduler(0, SCHED_RR, { 50 }) = -1 EPERM (Operation not permitted)
...

sched_setscheduler()システムコールにはさまざまなオプションを指定できますが、ここではとくに凝ったことはしていないことがわかりました。

Ubuntu上では次のようにコマンドが成功しました。

# chrt -r 50 echo hello
hello
# 

カーネルソースの違い

問題が発生した原因にはいくつか考えられます。Ubuntuでは成功したのにCoreOSでは失敗したことより、まずはCoreOSのカーネルがリアルタイムプロセスの実行を許さない特殊なものになっていないかを確認しました。問題発生時のCoreOSのカーネルは4.14.4-CoreOS-r1というものでした。このカーネルは公式の安定版カーネル4.14.14に、下記のいくつかCoreOS独自パッチを当てたものです。

github.com

これらパッチをすべて調査してUbuntuのカーネルと比較をしましたが、スケジューラのコードは変更されていませんでした。これによってスケジューラに関してはCoreOSは特殊なカーネルを使っていないことがわかりました。

ソース調査

カーネルソースを見て、どのような条件ならプロセスをリアルタイムプロセスにできないかを調査しました。これに該当するコードは以下ソースの中の__sched_setscheduler()関数です。

github.com

これを見ると上記のようにコマンドを発行した場合、次のような条件でこの関数はEPERMによって失敗することがわかりました。

  • CAP_SYS_NICE capabilityが無い(4192行目)
  • セキュリティモジュールのポリシーによって禁止されている(4244行目のsecurity_task_setscheduler()関数)
  • リアルタイムグループスケジューリング機能(後述)が有効になっている、かつ、プロセスが何らかのcpu cgroupに属している、かつ、そのcpu cgroupのリアルタイムプロセス実行可能時間が0(4291行目のif文)

今回の問題が発生した根本原因は上記いずれかではないかと疑って、総当たり式に確かめることにしました。

CAP_SYS_NICEについての確認

chrtコマンドを実行するときにrootにどのようなcapabilityが付与されているかはgetpcapsコマンドによって確かめられます。

# getpcaps $$
Capabilities for `1582': = cap_chown,cap_dac_override,cap_dac_read_search,cap_fowner,cap_fsetid,cap_kill,cap_setgid,cap_setuid,cap_setpcap,cap_linux_immutable,cap_net_bind_service,cap_net_broadcast,cap_net_admin,cap_net_raw,cap_ipc_lock,cap_ipc_owner,cap_sys_module,cap_sys_rawio,cap_sys_chroot,cap_sys_ptrace,cap_sys_pacct,cap_sys_admin,cap_sys_boot,cap_sys_nice,cap_sys_resource,cap_sys_time,cap_sys_tty_config,cap_mknod,cap_lease,cap_audit_write,cap_audit_control,cap_setfcap,cap_mac_override,cap_mac_admin,cap_syslog,cap_wake_alarm,cap_block_suspend,37+ep
# 

コマンドの出力を見るとCAP_SYS_NICEに該当するcap_sys_niceという文字列がありました。これによってCAP_SYS_NICEはシロだとわかりました。

リアルタイムグループスケジューリング機能についての確認

続いてリアルタイムグループスケジューリング機能について確認しました。ソースの順番的にはセキュリティモジュールのほうが先にありますが、簡単に確かめられそうなこちらを先にやることにしました。

CoreOSのカーネルの設定ファイルを見ると、リアルタイムグループスケジューリング機能は有効になっていました(設定項目はCONFIG_RT_GROUP_SCHED)。これは適当なcpu cgroupの中を見て、cpu.rt{period,runtime}usというファイルがあれば有効と判断できます。

# ls /sys/fs/cgroup/cpu/user.slice/cpu.rt_*
/sys/fs/cgroup/cpu/user.slice/cpu.rt_period_us  /sys/fs/cgroup/cpu/user.slice/cpu.rt_runtime_us
# 

ここでリアルタイムグループスケジューリングについて少し説明しておきます。これはリアルタイムプロセスの暴走を防ぐための機能です。この機能を使うことによってグループ内のリアルタイムプロセスはcpu.rt_period_us(マイクロ秒単位)という期間ごとにcpu.rt_runtime_us(マイクロ秒単位)だけ動作できて、残りの時間は通常のプロセスが動作できるようになります。

続いてbashがどのようなcpu cgroupに属しているかを調べました。systemdは通常のプロセスをuser.slice cgroupに属させますので、chrtコマンドを発行するbashがこのグループ入っているかを確かめました。

# grep $$ /sys/fs/cgroup/cpu/user.slice/tasks
1616
# 

ではこのグループのcpu.rt_runtime_usの値を見てみますと…

# cat /sys/fs/cgroup/cpu/user.slice/cpu.rt_runtime_us
0
# 

0でした。これが原因でrootでもプロセスをリアルタイムプロセス化できなかったというわけです。なお、詳細は述べませんが、リアルタイムスケジューリング機能は新たにcpu cgroupを作るとcpu.rt_runtime_usを0にすることもわかりました。この問題に対する対処方法は単にcpu.rt_runtime_usに1以上の値を書き込むだけです。

なお、Ubuntuのカーネルにおいてはリアルタイムグループスケジューリング機能は無効化されています。

もとの再現方法による再現試験

ここまでで「コマンドラインからroot権限でプロセスをリアルタイムプロセス化したらEPERMで失敗する」ことがわかりましたが、元のサービス起動失敗が同じ原因だとは限りません。このため、もとの再現方法による再現試験をする必要があります。

幸いにもサービスが所属するcpu cgroupのcpu.rt_runtime_usを1以上にしてサービスを起動したところ、成功しました。

対処方法の調査

最後に、実際のシステムにおいてどのように対策すればよいかを調査しました。systemdの設定ファイルに設定項目が無いかと調査していると、systemdのissueに次のようなものを見つけました。

github.com

I am not sure this can be supported in any sane way since the propagation and summing rules are so weird. I generally encourage everyone to disable rt group scheduling in the kerbel these days.

README: document that RT group sched should be turned off by poettering · Pull Request #553 · systemd/systemd · GitHub

+        kernel when using systemd. RT group scheduling effectively
+        makes RT scheduling unavailable for most userspace, since it
+        requires explicit assignment of RT budgets to each unit whose
+        processes making use of RT. As there's no sensible way to
+        assign these budgets automatically this cannot really be
+        fixed, and it's best to disable group scheduling hence.
+           CONFIG_RT_GROUP_SCHED=n

少なくともsystemdについては今のところサービス起動時にcpu.rt_runtime_usの値は変更する方法が無いこと、および将来的にできるようにする気もないことがわかりました。ではCoreOSのkernel configを変えてもらうかというと、

  • 回避方法はあるのでそこまでしてもらう話でもない
  • 説得材料に欠ける
  • 説得できてもいつ修正されるか不明

などの理由によって、他の方法を探すことにしました。今のところはサービスの起動前にcpu.rt_runtime_usを1以上にするスクリプトを仕掛けて問題を解決する予定です。

おわりに

本記事を読まれた結果、読者のみなさまにこのような問題があることに加えて、なんらかの問題が発生した場合にどのように調査するかという思考プロセスの一例を知っていただけたらと思います。