第4章 不測の事態対策
サービス不能状態対策

サーバソフトウェアは、「サービス不能」(Denial of Servicve)に陥ることなく、サービスを提供し続ける必要がある。このサービス不能を引き起こす攻撃は「サービス妨害攻撃」(Denial of Service Attack)と呼ばれる。

サービス妨害攻撃とサービス不能状態

ソフトウェア、特に、サーバソフトウェアが所定のサービスを提供し続けることができない状態に陥ることを「サービス不能」(Denial of Servicve)と言う。そして、サービス不能状態を引き起こす攻撃が「サービス妨害攻撃」(Denial of Service Attack)である。

図4-2: サービス妨害攻撃とサービス不能状態

(1) 要因はさまざま

サービス不能が生じる要因はさまざまである。表面的な現象から見ると、プログラムが停止してしまうケースと動き続けるケースの大きくふたつに分けられる:

異常終了と無反応の違いは、プログラム内部で何らかの異常が生じたときにエラー処理機構が過敏に反応するか、検知する仕組みをもたないため問題の発生を見過ごしたままプログラムが動作し続けるかの差である。

この両者の要因となり得るもののひとつに「リソースの食い尽くし」が挙げられる。「リソースの食い尽くし」では、ソフトウェアがコンピュータ資源を使い尽くして、それ以上サービス提供が継続できない状況に追い込まれることになる。食い尽くされるリソースはおおよそ次のようなものである。:

食い尽くしにも2種類のパターンがある:

  1. リサイクル失敗
    ──不要になったリソースが適切に解放されない
    • 例 メモリリーク
  2. リクエスト過多
    ──もともと用意していたリソース量が実際に到着するリクエストの数に見合っていない
    • 例 DB接続チャネル不足

現実にはこのふたつのパターンが複合的に発生することもあるだろう。

サービス不能対策

サービス不能に至る要因にはさまざまなものが考えられる、ここでは次の3つに注目する。

(1) 異常終了を検出する

サービスを提供するメインプロセスと、そのプロセスの状態を管理する監視プロセスに分ける方法がある。
監視プロセスは、メインプロセスに対して定期的にメッセージ等により問い合わせをする。
この問い合わせに対する応答が有れば、正常であり、タイムアウト等の何らかのエラーがあれば、メインプロセスに異常がおきていると判断できる。

Unix では、監視プロセスを親プロセスとして、子プロセスにあたるメインプロセスをfork, exec により起動すれば、親プロセスは、wait (2) により子プロセスの終了を知ることができる。

メインプロセスに異常があれば、メインプロセスを終了させた後、起動(再起動)するようにする。

(2) 無限ループを検出する

まとまった処理にタイマを使用して、処理の異常を検出することができる。
例えば、クライアントからの要求を受け付けたら、タイマをセットし要求の処理が終わったら、タイマをクリアする。この間に、タイムアウトが発生したら無限ループ等の異常がおきていると判断できる。

異常を検出した時には、対象となるプロセスを終了させた後、起動(再起動)するようにする。

(3) リソースの食い尽くしを検出する

サーバ内やプログラム内のリソース消費状況を計測するようにし、あらかじめ定めた限界を超えたら新たなリクエストの受付を制限するような工夫が必要となる。また、ネットワークから到着するリクエストの単位時間あたりの件数やデータ量に一定の閾値を設け、リクエストを受け付けすぎないようにすることも大切である。

ただし、受付の拒絶自体がサービス不能状態であるので、通常の運用状態からいきなり受付拒否状態に移行することは避ける必要がある。

1) プログラム内での対策

GNU/Linuxでは、リソースの制限を設定する関数に setrlimit(2) がある。
この関数は、主には次のようなリソースの制限を設定できる。

    RLIMIT_AS       仮想メモリの最大サイズ(バイト)
    RLIMIT_CPU      CPU 時間の上限(秒)
    RLIMIT_DATA     データ、ヒープ の最大値
    RLIMIT_FSIZE    作成できるファイルの最大サイズ
    RLIMIT_NOFILE   オープンできるファイル数の最大値 +1
    RLIMIT_STACK    スタックの最大サイズ(バイト)
		

また、現在のリソースの使用量を知るには、getrusage() を使用し、下記のような構造体の情報を得ることができる。

   struct rusage {
      struct timeval ru_utime; /* 使用されたユーザー時間 */
      struct timeval ru_stime; /* 使用されたシステム時間 */
      long   ru_maxrss;        /* RAM 上に存在する仮想ページのサイズ
                                  (resident set size) の最大値 */
      long   ru_ixrss;         /* 共有メモリの合計サイズ */
      long   ru_idrss;         /* 非共有データの合計サイズ */
      long   ru_isrss;         /* 非共有スタックの合計サイズ */
      long   ru_minflt;        /* 利用されたページ */
      long   ru_majflt;        /* ページフォールト */
      long   ru_nswap;         /* スワップ */
      long   ru_inblock;       /* ブロック入力操作 */
      long   ru_oublock;       /* ブロック出力操作 */
      long   ru_msgsnd;        /* 送信されたメッセージ */
      long   ru_msgrcv;        /* 受信されたメッセージ */
      long   ru_nsignals;      /* 受信されたシグナル */
      long   ru_nvcsw;         /* 意図したコンテキスト切り替え */
      long   ru_nivcsw;        /* 意図しないコンテキスト切り替え */
   };

Windows では、SetInformationJobObject() によりリソースの制限を設定することができ、QueryInformationJobObject() によりリソースの使用量を知ることができる。

2) ディスク使用量を制限する

ディスクの使用量を制限するには、ファイルシステムの quota を使用する。
GNU/Linux の quota は、パーティション単位にユーザやグループに対して使用量とファイル数の上限を設定することができる。
Windows のクォータは、ドライブ単位にユーザに対して、使用量の上限を設定することができる。

まとめ

サービス妨害攻撃等で引き起こされるサービス不能状態は、さまざまな要因により発生する。プログラムは、異常を検知し自動的にプロセスの終了および再起動を行うようにする必要がある。また、サーバ内やプログラム内のリソース消費状況を計測するようにし、あらかじめ定めた限界を超えたら新たなリクエストの受付を制限するような工夫が必要となる。ただし、受付の拒絶自体がサービス不能状態なので、受付の制限を行う際は十分に注意する必要がある。