NTTドコモR&Dの技術ブログです。

ちょっとした工夫で障害を減らしたお話~シャットダウンはご安全に~

1. はじめに

はじめまして、NTTドコモ データプラットフォーム部 3年目の東です。
普段はデータ分析ツールを動かすためのクラウド環境の運用・保守やデータ活用プロダクトの内製開発をしています。
今回は前者の業務について、ちょっとした工夫で障害発生回数を減らせることができたため、学んだ経験をシェアできればと思います。

1-1. 本稿で伝えたいこと

本稿で伝えたいことは以下となります。

  • 小さな工夫が大きな成果を生むことがある。
    • ちょっとした工夫で疑似的なグレイスフルシャットダウンを作れるよ。
    • それによって障害発生を減らせるよ。

※本稿で紹介する方法は完全なグレイスフルシャットダウンかといわれるとそうではありません。そのため「疑似的な」という表現を用いています。

一般に運用・保守業務を行っていく上で私たちの時間を奪っていく事象の1つに障害対応があると思います。
往々にしてプログラムやシステム、アプリケーションは、利用者の予期せぬ操作によるエラーや障害で正常に停止ができないことが起こり得ります。
逆に、こうした異常終了を最小限に抑え、システムの安定性を高めることができれば超絶ハッピーですよね。
そのためには、以下の2つのアプローチが考えられます。

  1. システムやアプリケーションの利用者に注意喚起を促す。
  2. プログラムやシステムを稼働させている基盤側で予め対策を講じる。

そこで、私は後者の対策として「グレイスフルシャットダウン」という手法を用いることでシステムの安定性を高めることを実現しました。 私の経験から少しでも皆様に得られるものがあればと思います。

グレイスフルシャットダウンについて
グレイスフルシャットダウンとは、アプリケーションやシステムを安全に、かつ正常に終了させるための手法のことです。
グレイスフルシャットダウンを実装することでリクエストの受付を停止し、処理中のタスクが全て完了するまで待機してから、アプリケーションを終了させるといった流れでシャットダウンを行います。
グレイスフルシャットダウンにより、エラーや障害で異常終了してしまうことを最小限に抑えることでシステムの予期せぬ停止を防ぐことができます。

2. 遭遇した事象

次に、今回私が遭遇した事象を紹介したいと思います。
後述の構成・条件下で運用していたところ以下のような事象が発生しました。

  • 概要
    • 21:00のインスタンス停止時に問題が発生し、翌日の起動時に障害が発生。
  • 障害の内容
    • 21:00に処理中のデータ抽出タスク(データの読み書き)があるにもかかわらず、インスタンスを停止したことでデータベース(PostgreSQL)コンテナが正常終了しない場合があり、データが破損し、翌日に起動が失敗する。

これは、仮に実行中のタスクがあった場合でもシステムを正常に終了させる仕組みをしっかりと機能させる状態になかったために発生した障害でした。

また、簡単にシステム構成や稼働時間を以下に記載します。

  • システム構成

    • 実行基盤はAWS上に構築
    • EC2インスタンス上にコンテナ化されたデータ分析ツール(SPA)をインストール
    • ストレージはEFSを利用
    • SPA上で分析するデータはSnowflake上からアプリ上に腹持ち
    • AWS Systems Manager(以下SSM)ドキュメントやメンテナンスウィンドウ、カレンダー機能を用いて、インスタンスの起動停止を実施
  • 稼働時間

    • 8:00にインスタンス起動、21:00にインスタンス停止

以下、簡略的な構成図です。※今回該当する部分以外は省略しています。

3. 対応策

この障害に対して、先述の通り、疑似的なグレイスフルシャットダウンを実装する対応策を実施しました。
完全なグレイスフルシャットダウンを実装しなかった理由についてですが、アプリケーションが提供する停止コマンドでは、コンテナの正常終了を待ってアプリを停止することができないといった制約がありました。
それにより、その待機時間を延長し、処理が終わるまで待機するといった選択肢を取らざるを得なかったため、こういった対応になりました。

ここでは、大きく以下の3つを実施しました。

  1. サービス停止コマンドの追加
  2. stop_grace_periodによる待機時間の延長
  3. インスタンス停止ステップまでの待機時間の追加・延長

ここでは、特に「サービス停止コマンドの追加」と「stop_grace_periodによるの待機時間の延長」の2つについて、対応した内容をご紹介できればと思います。

3-1. サービス停止コマンドの追加

当初はサービス停止コマンドを送信するステップがなく、

  1. 21:00(停止時間)になる
  2. SSMのメンテナンスウィンドウタスクとして登録しているドキュメントを実行
    • SSMのカレンダー機能により実行可否日を確認
    • インスタンス停止のステップを実行する

といった流れでした。 これでは、停止時間になったらインスタンスがプツンと切れてしまい、安全な停止とは言えずデータ不整合などなにかしらの不整合を起こし障害につながる可能性がありました。

したがって現在ではsystemctl stopコマンドでしっかりとサービスを停止をした後、インスタンスの停止を行なうような処理となっています。
具体的には以下のような追記をしています。※実際はassumeRole、InstanceIdは別途パラメータ設定欄で記述しています。

description: これはEC2を停止するドキュメントです。
schemaVersion: '0.3'
assumeRole: '{{ AutomationAssumeRole }}'
parameters:
  InstanceId:
    type: StringList
  AutomationAssumeRole:
    type: String
    default: ''
==========追記部分その1==========
Script:
    default: sudo systemctl stop サービス名
    description: サービス停止用コマンド
    type: String
==========追記部分その1==========

mainSteps:
- description: カレンダーから実行の可否日を確認するステップ。
    name: check_calendar_state
    action: aws:assertAwsResourceProperty
    nextStep: runcommand
    isEnd: false
    inputs:
      Service: ssm
      Api: GetCalendarState
      PropertySelector: $.State
      DesiredValues:
        - OPEN
      CalendarNames:
        - arn:aws:ssm:ap-northeast-1:アカウント名:document/カレンダー名
==========追記部分その2==========
- description: サービス停止コマンドの実行ステップ。
    name: runcommand
    action: aws:runCommand
    timeoutSeconds: 7200
    nextStep: sleep
    isEnd: false
    onFailure: step:sleep
    inputs:
      Parameters:
        commands:
          - '{{ Script }}'
      InstanceIds:
        - '{{ InstanceId }}'
      DocumentName: AWS-RunShellScript
==========追記部分その2==========

~~~~中略~~~~~

- description: インスタンス停止の実行ステップ。
    name: stop_ec2
    action: aws:executeAwsApi
    isEnd: true
    inputs:
      Service: ec2
      Api: StopInstances
      InstanceIds: '{{ InstanceId }}'

3-2. stop_grace_periodによる待機時間の延長

先述の通り、systemctl stopコマンドを用いてサービスを停止するステップを追加しましたが、これではまだ不十分です。
なぜなら処理中のタスクが終了するまでの時間を考慮せずにサービスの停止を行うと、インスタンス停止でそのままタスクをプツンと切ってしまうのとさほど変わりません。
本来は、PostgreSQLの正常終了を待ってからコンテナを停止させたかったのですが、systemctlで終了シグナルを受け取れなかったため、stop_grace_periodによる待機時間の延長により「コンテナの停止処理が終わるのを待つ」というステップを追加しました。

stop_grace_periodについて
systemctl stopコマンドでサービス停止を行うまでに実際には停止猶予時間があります。それがstop_grace_periodで設定されている値となっています。
※stop_grace_periodの詳細についてはこちら[1]を参照ください。

ここでのsystemctl stopコマンドの実行時はユニットファイルやdocker-compose.ymlに基づき処理がされます。
ユニットファイルとdocker-compose.ymlファイルの例を示します。※今回該当する部分以外は省略しています。

ユニットファイル:

[Unit]
~~~略~~~

[Service]
Environment=COMPOSE_FILE=/usr/local/etc/サービス名/docker-compose.yml
Environment=PROJECT=サービス名-service
~~~略~~~
ExecStop=/usr/local/bin/docker-compose -f ${COMPOSE_FILE} -p ${PROJECT} down
~~~略~~~

[Install]
~~~略~~~

docker-compose.ymlファイル:

services:
  log-server:
    image: イメージ
    container_name: コンテナ名
    hostname: ホスト名
    mem_reservation: 割り当てるメモリ
    stop_grace_period: 300s #ここに任意の秒数を記述。今回は5分待機で設定。

~~~略~~~

  database:
    image: イメージ
    container_name: コンテナ名
    hostname: ホスト名
    stop_grace_period: 300s #ここに任意の秒数を記述。今回は5分待機で設定。
    
~~~略~~~

ここで行われるおおまかな処理の流れは以下です。

  1. systemctl stopコマンドを送信
  2. ユニットファイル内のExecstop部分が処理される
  3. docker-compose.ymlファイル内のstop_grace_periodの時間を待つ
  4. docker-compose downが実行される

21:00(停止時間)になっても処理中の分析タスクが動いているなど想定される場合、こういった待機時間を伸ばす対応を行い処理終了まで待つことで安全に停止する確率をあげることができます。 その結果、障害につながる確率を最低限に抑えられるようにしました。

4. 効果

対応策を実施した結果、以下のような効果がありました。

  • Before

    • 最低でも月に1回の障害発生、最も多い時期で週に1回障害が発生していた。
    • 1回の障害対応にかかる時間は平均6時間。1日の稼働のほとんどを割かれてしまっていた。
    • 加えて障害発生時には複数人で対応にあたっていたので、稼働×人数分となり多大な影響がでていた。
  • After

    • 対応策の実施後3ヶ月以上障害ゼロ!安定的なシステム稼働を実現。
    • 障害対応にかかっていた分、別業務へ稼働を割くことができるようになった。
    • 突発的な障害対応がなくなり、精神的にもハッピーに

5. おわりに

まずはこの場をお借りして、今回とりあげた業務にかかわった皆様に御礼申し上げます。皆様のご協力のおかげで実現した取り組みだと思っております。
また、本稿を執筆するにあたって、参考にさせていただいた文献を末尾に記載しております。併せて御礼申し上げます。

今回の対応では、「処理を待つ」 という考え方を導入することで、疑似的なグレイスフルシャットダウンを実現し、障害発生を大幅に減らすことができました。
本稿で記載の通り、小さな工夫でも大きな効果を発揮することがあります。
これから社会に出る皆様、私と同年代の若手の皆様、ともに学び続け、システム改善に取りこんでいきましょう!

6. 参考

[1]: dockerdocs stop_grace_period https://docs.docker.com/reference/compose-file/services/#stop_grace_period