Linux でのハングタスクについて

Red Hat でコンサルタントをしている菅原と申します。

この記事では、意外とあまり説明されていないような気がする Linux システムで発生するハングタスクについて少し説明したいと思います。現場のシステムでもハングタスク検知の設定がされていることが多いと思いますが、ハングタスクとは何なのかを正しくご理解いただくことで、ハングタスク検知を行う目的が明確になること、また、実際の障害事例もご紹介することで、通常あまりハングタスクと関連づけて考えないような設定でもハングタスク発生につながる場合があることを知っていただき、少しでもシステム管理や障害の理解、障害対応などのお役に立てれば幸いです。

なお、この記事では RHEL のみを対象に書いていますが、他の Linux ディストリビューションにも適用される内容と思います。

ハングタスク (hung tasks) とは

ハングタスクとは読んで字のごとしでハングしたタスク (Linux カーネルではプロセスとスレッドを統一して取り扱っており、タスクと呼んでいます。以降、本記事では「タスク」と「プロセス」は同義語として捉えていただいて結構です) のことです。症状としては典型的にはタスクの処理が進行しなくなるのでいわゆる「刺さった」ような状態となることが挙げられます。より厳密には、ps コマンドで見たときの状態が "D" で一定以上長く滞留しているとハングしていると見なされます。

タスクの D 状態 (D state) とは

D 状態は英語では uninterruptible sleep と表記され、日本語に訳すと「割り込みできないスリープ状態」になります。この状態にいるタスクに例えば kill コマンドでシグナルを送ったとしても、割り込み不可なのでシグナルの受信は今実行中の処理が終わって D 状態を抜けるまで待たされることになっています (なので、D 状態で刺さっているプロセスを kill してもプロセスを止められないのです)。

実はこの D 状態というのは、ユーザーモードではなくカーネルモードで実行していないと見られない状態です。例えば、ディスク等のデバイスに対して入出力を盛んに実行しているプロセスの状態をよく見ていると D 状態になることは珍しいことではありません。D 状態は短時間であればそれ自体が問題なわけではなく、問題は「長く滞留する」というところです。

また、スリープ状態と呼んではいますが、実際にタスクがスリープしているとは限らず、カーネル内で忙しく何らかの処理を実行している場合も多いことに注意が必要です。ただ、タスクのユーザー空間としては、システムコールを呼び出して戻りを待っており、戻ってくるまでユーザー空間の処理が一切進まないという意味ではスリープ状態 (S 状態) に似ていると言えるかも知れません。

D 状態が必要な理由

D 状態というのは、カーネル内部で何らかの排他制御が必ず関わっています。排他制御というのは、あるコードを複数のタスクが並行処理すると問題が発生するような場合に、「この部分は1度に1個のタスクにしか実行させない」という仕組みを実現するものです。そのような部分のことを「クリティカルセクション」と呼びます。

クリティカルセクションに入るときには「ロック」を取得し、抜けるときに取得したロックを解放します。ひとつのロックはひとつのタスクにしか所有できないため、先にクリティカルセクションを実行しているタスクが存在するときに、後から別のタスクが同じクリティカルセクションを実行するためにロックを取得しようとすると、前のタスクがロックを解放するまで待たされることになります。このようにして、複数のタスクがカーネル内のクリティカルセクションを同時に実行させないようにしています。カーネル内には多種多様なクリティカルセクションが存在し、それぞれのクリティカルセクションによって、複数の異なる種類のロックを目的に合わせて使い分けています。ロックは大きく分けるとロックを待つときにタスクがスリープするものとしないものに分類できますが、D 状態は「割り込みできないスリープ」状態なので、スリープする方のロックが関わっています。後でご紹介する事例2つでは、スリープするタイプのロックとしてどちらも mutex が関わっていました。

参考文献: Linux kernel doc "Lock types and their rules"

また、なぜ D 状態が割り込みできないのかというと、取得したロックをきちんと解放するためです。仮にロックを取得して処理をしている途中でもシグナルの受信などによりタスクの他の部分の処理に飛べてしまったとすると、例えば割り込んだ処理がタスクを終了させてしまったりして取得したロックが正しく解放されない可能性が出てきます。これでは他のタスクが同じクリティカルセクションを実行しようとしても永久にロック待ち状態に陥ってしまうだけでしょう。なので、一度タスクがロックを取得したら、解放するまでクリティカルセクションから出て他の部分へ飛んでしまわないように、シグナルなどによるタスクに対する割り込みを禁止しているわけです。

D 状態へ出入りするタスクの様子を時間軸にそってまとめると、以下のようになります。

  1. タスク状態を D に変更
  2. ロックを取得 (他のタスクによって既に取得されている場合は解放されるまでスリープして待つ)
  3. クリティカルセクションの実行
  4. ロックを解放
  5. タスク状態を D から他の状態に変更

ハングタスク発生時に起きていること

さて、ここまで見てきたように、タスクが長時間 D 状態に滞留しているということは、上記の 1、4、5 には長時間かかる要素はないので、2 か 3 のどちらかで長時間滞留していることになります。長時間ロックを待っているタスクがいるということは、他のタスクが長時間同じロックを保持し続けているということも推測できますので、実際には、

  • クリティカルセクションの実行に長時間かかっているタスク (1個だけ)
  • 長時間ロックを待っているタスク (1個以上)

の両方が同時に存在している確率が高いです。

とここまでの説明で疑問を持たれた方はおられないでしょうか。というのは、D 状態はカーネルモードでしか入れないタスク状態なわけですから、ここで言っているロックはカーネルロックになるわけですし、クリティカルセクションもカーネル内部のコードの一部です。つまり、ハングタスクが発生する原因は排他制御に絡んだカーネルバグなのでは? カーネルバグだとしたら、システム管理者にできることはサポートケースを上げて早く直してもらうことぐらいしかないんじゃないの? と。

ところが、実際にはカーネルバグではない原因によってもハングタスクが発生することがあるんです。システムの構成やシステム運用手順を注意深く設計しないと、バグではない理由でハングタスクにやられることがあるという事例を少しだけご紹介したいと思います。

とその前に、ハングタスクに関するカーネル設定をおさらいしておきしましょう。

ハングタスクに関するカーネル設定

RHEL で設定できる、ハングタスクに関するパラメータは以下の4つです。いずれも sysctl パラメータなので、/etc/sysctl.d/ 配下のファイルや /etc/sysctl.conf 等に書いておいてシステム起動時に自動設定したり、稼働中に sysctl コマンドで動的に設定したりできます。

kernel.hung_task_warnings
kernel.hung_task_timeout_secs
kernel.hung_task_check_count
kernel.hung_task_panic

個々のパラメータの意味と設定方法についてはこちらの KCS に詳しく記載されていますのでご覧ください。なお、日本語版だと RHEL 8/9 に対する言及がありませんが、RHEL 8/9 に対しても適用できるナレッジです。対応する英語版 KCS をご覧いただくと RHEL 8/9 も対象とされていますので気になる方はそちらもチェックしていただくと良いでしょう。

システム管理上特に重要なのは D 状態での滞留時間の上限、つまりこれ以上長く D 状態で滞留しているとハングとみなす時間を決める hung_task_timeout_secs と、ハングタスクを検出したときにカーネルパニックを起こすかどうかを決める hung_task_panic でしょう。

前者は、あまり短いと頻繁にハングタスクを検出する恐れがありますし、一方あまり長く設定するとタスクの処理が進まなくなってから長時間障害の検知ができない危険があり、一概に何秒が良いとは言えないパラメータです。サーバ上で稼働しているワークロードの性質や、SLA によって注意深く設定する必要があります。あくまで目安でしかありませんが、一般的には 120〜600 (単位は秒)ぐらいの範囲で設定値が決定されることが多いようです。

後者は 0 がカーネルパニックなしの設定で、1 がカーネルパニックを起こす設定です。カーネルパニックする場合、kdump が構成されていれば vmcore (カーネルクラッシュダンプ) を取得してハングタスクの原因を解析できる可能性を残すことができます。パニックの有無と関係なく、ハングタスク検出時にはログに "INFO: task xxxxx:NNNNN blocked for more than TTTseconds." のようにメッセージが記録されます。アプリケーションの SLA などによっては、ハング検出するたびにパニックさせなくても、ハングが発生したことがわかれば良いといったケースもあると思います。

ハングタスク事象の解析

実際にハングタスクが発生した際に発生の原因を特定するには、前述の設定で kernel.hung_task_panic=1 を設定してハングタスク検知でカーネルパニックを起こさせ、さらに kdump によってカーネルクラッシュダンプを取得して解析することが必須です。ただし、この記事で説明しきれる内容ではありませんので、kdump 設定方法や解析のやり方などについてはここで解説することはいたしません。知識として、ハングタスクの原因調査にはそのような設定が必要、とだけご承知おきください。

ハングタスクの事例

ようやく前提となるハングタスクの説明が終わりましたので、実際の障害事例をご紹介していきたいと思います。今回は2件の事例をご紹介します。

RPM アップデートをトリガーとして tuned がハングタスク化した事例

実際にお客様先で発生した事例です。polkit RPM をアップデートしたところ、tuned サービスプロセスがハングして、kernel.hung_task_panic=1 の設定をされていたためカーネルパニックが発生しました。

RPM のアップデートはシステムメンテナンスと考えればサービスやアプリケーションを停止した上で行うという考え方もあれば、リブートが不要なアップデート自体は適宜実施するという考え方もあります。この場合は、リブートが不要なアップデートなのでアプリケーションを停止せずに RPM のアップデートを適用したケースです。

詳しい調査の経緯は省略しますが、何が起きていたかといいますと、

  1. polkit RPM のアップデートを適用したところ、RPM インストール後に自動実行されるスクリプト (ポストインストールスクリプトレット) 中で、polkit のサービスの再起動を試行する "systemctl try-restart polkit.service" が実行された
  2. 実は tuned サービスの systemd ユニットファイルの中で polkit サービスを Requires に記述されており、polkit サービス再起動前に tuned サービスが停止された
  3. tuned サービス停止後に polkit サービスを再起動し、tuned サービスを再度起動した
  4. tuned のデフォルト設定ではサービス起動時に /etc/sysctl.conf および /etc/sysctl.d/* に記載されている設定を適用するようになっているので、適用した
  5. このとき、sysctl.conf ファイルに "kernel.nmi_watchdog=0" と "kernel.nmi_wathdog=1" という、矛盾した設定が両方記載されていたため、kernel.nmi_watchdog に対して 0 と 1 両方の値を書き込む動作が発生するようになっていた (設定ファイルの順序として最後に書き込まれるのが 1 だったため、通常稼働時には 1 の設定)
  6. sysconf 適用中、kernel.nmi_watchdog に 0 を書き込んだ時点で tuned サービスタスクが起動途中でハング
  7. kernel.hung_task_timeout_secs に設定した時間が経過したため kernel.hung_task_panic=1 の設定にしたがってカーネルパニック発生し、その後 kdump によりカーネルクラッシュダンプを保存してサーバ再起動

という現象が起きていたことがわかりました。

ちょっとここで nmi_watchdog について説明が必要になりますね。nmi_watchdog とはハードロックアップディテクタと呼ばれる仕組みで、「カーネルモードでハードウェア割り込み禁止の状態で10秒を超える時間ループする」状態がハードロックアップと定義されています。ハングタスクと似ていて少し紛らわしいのですが、ハングタスクの D 状態はタスクの割り込み(主にシグナルによる)が禁止されているだけでハードウェア割り込みは有効なので、D 状態で実行中のタスクがスケジューラによって一時実行を中断され他のタスクに CPU を明け渡したり、ハードウェアイベント待ちのためスリープして CPU を明け渡したりといったことが起こり得ますが、ハードロックアップの場合はハードウェア割り込みを禁止した状態でカーネル内でループするので、スケジューラが中断することもできず、タスクだけではなくシステム全体がハングしたような状態になったりします。nmi_watchdog は、NMI (non-maskable interrupt=マスクできない割り込み) という禁止不可能な特殊な割り込みを使ってこれを検知し、設定によってはカーネルパニックを起こさせてハング状態からの復旧をさせたりすることができる仕組みです。

で、nmi_watchdog を有効にするとシステム上のすべての CPU 上で watchdog カーネルスレッドが起動されます。ps コマンドで見ると、こんなタスクが動いているのが見えるかも知れません。

root          16  0.0  0.0      0     0 ?        S     2022   0:00 [watchdog/0]
root          19  0.0  0.0      0     0 ?        S     2022   0:16 [watchdog/1]
root          26  0.0  0.0      0     0 ?        S     2022   0:16 [watchdog/2]
root          32  0.0  0.0      0     0 ?        S     2022   0:16 [watchdog/3]
...
root          32  0.0  0.0      0     0 ?        S     2022   0:16 [watchdog/31]

"[watchdog/N]" の N の部分は CPU コアの番号を表しています。このように、コマンド名に相当する文字列が [] で囲まれているタスクは、ユーザー空間で動いているものではなく、純粋にカーネル内部で動いているカーネルスレッドと呼ばれているのですが、kernel.nmi_watchdog=1 の設定をすると、すべての CPU コアに対して1つずつ、watchdog スレッドが起動されます。

kernel.nmi_watchdog=1 であるとき、つまり watchdog スレッドが存在しているときに 0 を設定するとハードロックアップディテクタが無効にされ、これらのスレッドも終了させるのですが、スレッドを終了させる kthread_stop() 関数の肝となる部分は以下のようになっています。

        set_bit(KTHREAD_SHOULD_STOP, &kthread->flags);
        kthread_unpark(k);
        wake_up_process(k);
        wait_for_completion(&kthread->exited);

上記は RHEL 9 kernel-5.14.0 の kernel/kthread.c:L646-L649 の抜粋ですが、RHEL 7/8 でも本質的には変化ありませんし、最新の upstream kernel 6.2.5 (下記リンク) でもあまり変わっていませんでした。

ここで行っていることをざっくり説明しますと

  1. スレッドに対して終了を指示するフラグ KTHREAD_SHOULD_STOP をセットして
  2. スレッドを起こして
  3. スレッドの終了を待つ

というロジックになっています。つまり、スレッドを終了させるにしても、一度そのスレッドが実行されて自発的に終了する必要がある仕組みというわけです。

この障害事例では、CPU コアの1つでリアルタイムスケジューリングポリシーで動作するアプリケーションが稼働して CPU を 100% 占有している状態だったため、watchdog スレッドがウェイクアップできない状況になっていました。watchdog スレッドは通常のタスクと同じポリシーを使うようになっていて、リアルタイムスケジューリングポリシーで稼働するタスクの方が優先度が高くなっています。そのため、ウェイクアップされた watchdog スレッドはランキューに入って実行待ちになったまま、リアルタイムアプリケーションが CPU を自発的に明け渡すのを永久に待っている形となっていました。tuned の kernel.nmi_watchdog への 0 の書き込みによって呼び出された kthread_stop() 関数は、実行待ちとなったまま終了もできないスレッドが終了するのを永久に待っており、その間 D 状態が継続していたために tuned タスクがハングしたと見なされたわけです。なお、RHEL のスケジューリングポリシーについて詳しくは下記リンクのドキュメントをご参照ください。

参考文献: Red Hat Enterprise Linux 9 『システムの状態とパフォーマンスの監視と管理』第30章 スケジューリングポリシーの調整

本障害を発生させる条件が、リアルタイムアプリケーション稼働中でかつ kernel.nmi_watchdog=1 の設定で watchdog スレッドが各 CPU コア上に存在している状態において、kernel.nmi_watchdog=0 を設定して watchdog スレッドを終了させようとする、であることがはっきりしましたので、これを解決するには tuned サービスの起動中に nmi_watchdog の有効・無効状態を変更しなければよいはずです。というわけで、sysctl 設定から kernel.nmi_watchdog=0 をすべてコメントアウトして kernel.nmi_watchdog=1 を1つだけ残す設定として再現テストを行ったところ、tuned のハングを回避できることがわかりましたので、お客様にも同じ設定を適用していただくように依頼して解決しました。

CPU リソースが不足してハングタスクとみなされた事例

こちらは、前述の事例とはだいぶ趣が異るハングタスク事例で、コンテナ実行環境で発生した事象になります。OpenShift などコンテナ実行環境では、各コンテナのリソース (メモリや CPU など) に制限をかけるのに、Linux カーネルの cgroups (Control Groups) という機能が利用されています。赤帽エンジニアブログでも "cgroup" あるいは "cgroups" で検索するとヒットしてくる多くの記事が OpenShift を初めとするコンテナ技術の関連記事です。

また、本事象では、アプリケーション以外の雑多な処理 (例えばハードウェア割り込みの処理や、OS の様々な管理など。家事になぞらえて「ハウスキーピング」などと呼ばれます) は実行しないように特定の CPU コアを予約してアプリケーション専用として置いておく、isolcpus も関係しています。

参考記事: 赤帽エンジニアブログ "レッドハットが考えるNFV環境向けのチューニング"

コンテナ実行環境では、コンテナリソースの制限に cgroups という機能が利用されていると書きましたが、cgroups は一般の RHEL システムでも有効になっており、以下のような mount コマンドの出力 (抜粋) に見覚えのある方もおられるのではないでしょうか。これは RHEL 8 での出力例です。

cgroup on /sys/fs/cgroup/net_cls,net_prio type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,net_cls,net_prio)
cgroup on /sys/fs/cgroup/cpu,cpuacct type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,cpu,cpuacct)
cgroup on /sys/fs/cgroup/hugetlb type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,hugetlb)
cgroup on /sys/fs/cgroup/rdma type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,rdma)
cgroup on /sys/fs/cgroup/devices type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,devices)
cgroup on /sys/fs/cgroup/perf_event type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,perf_event)
cgroup on /sys/fs/cgroup/blkio type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,blkio)
cgroup on /sys/fs/cgroup/memory type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,memory)
cgroup on /sys/fs/cgroup/cpuset type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,cpuset)
cgroup on /sys/fs/cgroup/pids type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,pids)
cgroup on /sys/fs/cgroup/freezer type cgroup (rw,nosuid,nodev,noexec,relatime,seclabel,freezer)

このように、cgroups 機能は /proc や /sys のような疑似ファイルシステムを経由して設定にアクセスするようになっており、これらのマウントポイントの下に、タスク (=プロセス・スレッド、そしてこの文脈ではコンテナも) の起動と終了に伴い、動的にディレクトリ構造の作成や削除が行われます。つまり、タスクの起動や終了が増えていくにしたがって、それだけ /sys/fs/cgroup 以下のディレクトリ構造の作成や削除の処理も増大していくということです。そして、このような /sys/fs/cgroup 以下のディレクトリ構造の管理は典型的なハウスキーピングの一種なので、先ほど触れた isolcpus で予約していない、ハウスキーピング用の CPU コアで実行されるというところが重要な意味を持ちます。

この事象では、サーバに搭載された CPU コアの大半を isolcpus でアプリケーション専用と予約しており、残り少ないハウスキーピング CPU がこうした管理をほそぼそと実行するような構成となっていたことが災いしていました。障害発生時には、コンテナの終了に伴い /sys/fs/cgroup 以下に作成されていたディレクトリ構造を削除する際に、処理を実行していた CPU のランキュー (run queue: カーネルのスケジューラが実行待ちタスクを管理するデータ構造) が非常に長くなっていたことがわかっています。1個の CPU の限られた時間を、数多くのタスクが奪い合っていたと言っても良いかも知れません。

この場合はハングしたタスクは systemd で、終了したコンテナに対応する /sys/fs/cgroup/memory 配下のディレクトリ構造を削除していたのですが、処理が完了する前にタイムスライスを使い切って他の実行待ちタスクに CPU を明け渡し、次のタイムスライスを待つ、ということを繰り返していたと見られます。最初の事例でご紹介したハードロックアップのところでも触れましたが、D 状態はタスクへの割り込みは禁止されますが、OS としての動作を見れば割り込みが禁止されてはいませんので、スケジューラにより D 状態にある systemd タスクから他のタスクへ実行を移したり、その逆も行われます。このケースでは、多数のタスクが同じ CPU 上で実行を待っていたため、一度 CPU を明け渡すと次に CPU が割り当てられて実行できるまで、かなりの間実行待ちであったと思われます。

/sys/fs/cgroup は疑似ファイルシステム (本来のデータストレージではなく、カーネルへの API を提供するためのファイルシステムという意味で擬似) ではありますが、ファイルシステムという複雑なデータ構造を管理するカーネルのサブシステムである以上、ディレクトリやファイルの作成や削除などの操作には他のファイルシステムと同様に排他制御が必要です。systemd はシステムコール経由でディレクトリの削除をカーネルに依頼した後、排他制御のため D 状態となってカーネルの処理が完了するのを待っていました。カーネルのディレクトリ削除処理は正常に進行していたのですが、他の多数のハウスキーピングタスクと CPU 時間を奪い合う状況だったため処理の完了までに kernel.hung_task_timeout_secs に設定した時間を超える長時間を要してしまい、ハングタスクとして検知されてカーネルパニックに至ったと考えられます。

緩和策としては、

  • isolcpus に指定するコア数を減らしてハウスキーピング CPU を増やす
  • kernel.hung_task_timeout_secs に指定するタイムアウトを延長して、同じ状況でハングタスクが検知されないようにする

といったことが考えられますが、最初にご紹介した事例とは異なり発生条件が動的で再現が難しいため、緩和策として設定値が妥当であるか判断するには、現実に近い負荷テストを実施して有効性を確認する必要があります。また、前者の対応はサーバのキャパシティ (同時に実行できるアプリケーションコンテナ数) に影響を与えるので、1台のサーバだけの問題ではなくシステム全体への影響を考慮する必要があり、対応するにしても慎重に行う必要があるでしょう。

システムを構築する際、アプリケーション性能を最適化するため、できるだけ多くのハードウェア資源をアプリケーションの実行に振り向けたいのは自然なことですが、ハウスキーピングタスクも OS の稼働には重要な処理ですので、システムの安定稼働にはアプリケーション実行とハウスキーピングに振り分ける資源のバランスも大変重要である、という教訓が得られた事例となりました。

むすび

ちょっと長い記事となってしまいましたし、Linux カーネルの様々な機能や概念が登場してすべてを解説しきれていないので理解が難しい面もあるかも知れませんが、ハングタスクとは何か、また、OS バグではない原因によってもハングタスクが発生する事例を通じて、システムの安定稼働のためにシステムを構成・設定を全体的に検討する必要があることを知っていただけたら幸いです。

* 各記事は著者の見解によるものでありその所属組織を代表する公式なものではありません。その内容については非公式見解を含みます。