ぱいぱいにっき

Pythonが好きすぎるけれど、今からPerlを好きになりますにっき

Webサービスの障害対応のときの思考過程

起こってほしくはないのですが、あらゆるWebサービスは完璧に動作する状態を維持することは難しく、やはり障害対応・トラブルシューティングといった作業が発生します。

筆者は普段仕事で障害対応を不幸なことによくやるのですが、障害対応のスキルというのはスピードや判断の正確さが求められるせいか、今までやったことがある人・ノウハウがある人に集中し、それ以外の人は眺めるだけ・あとからログを見返すだけの状態によく陥ることがあります。

これはWebサービスを開発・運用するチームとしてみたときにそういった苦労が特定の人に集中するのは良くないので、それを緩和する目的として、筆者が障害対応時に考えていることを記述してみます。なお、これが唯一の正解ではないとは思っているので、ツッコミや、自分はこう考えているよというのを教えていただければ幸いです。

具体的な手法を避けて思考の方法を述べているのは、障害というのはパターンで解決できることはそんなに無いからです。また、Webサービスによって監視項目やロギング、アプリケーションの特性、インフラ設計などがまるっきり違います。なので、調べ方や考え方など、手法に至る過程を記述したほうがより汎用的かなと考えました。

障害とは

ここで言葉の定義をはっきりしておきましょう。ここでいうWebサービス障害とは、サービスにビジネス上のインパクトを与えるような事象が発生したときのことを指します。インパクトの大小は問わず、例えばデプロイの結果、フロントエンドのUIがデグレってしまって特定の人しか使わないような機能のボタンが隠れてしまったケースも障害とします。人によっては「バグ」だとか、「不具合」と言うかもしれませんが、ここでは乱暴にひっくるめて「Webサービスの障害」として扱います。

障害対応の段階

Webサービスの障害を人間の病気と同様にあてはめてみると、急性期回復期慢性期に当てはめられます。また、病気の期間について調べたところ前兆期という言葉も見つけたのでこれも使ってみます。

前兆期

Webサービスの障害が発現しない段階です。しかし放置すると、障害が発現してしまうような状態を指します。

ここで運用する人が認知できてきたらもうけもので、Webサービス監視というのは、こういった障害の前兆を見つけるためにアラートを仕掛けています。しかし、病気と同様で症状が出ない状態で予兆を見つけるというのは至難の業であり、そもそもこういった前兆が来ると予想してアラートが仕掛けられているのであれば、それが出ないような対策もセットで取られていて、なかなかアラートが出ない、なんてこともあります。とはいえ、対策が無効化されるような変更を、考慮漏れでシステムに加えることもありますし、不要ではありません。

またアラートにはならないが、障害の前兆をメトリックから見つけることは、普段とは違う箇所を探すことでもあり、普段を知らなければ障害を認知することもできないわけです。筆者は、新しくWebサービスを担当することになったら、最初の一ヶ月ぐらいは、毎日数十分はサービスメトリックを見て、普段のWebサービスの負荷特性などを見ます。Webサービスによって負荷が高まる時間帯や曜日なども違い、また負荷が高まったときにまずボトルネックになる部分というのも違ってきます。そのために普段を知るのが重要です。

こういった前兆を見つけた場合、トリアージを行います。たとえば1時間以内に対策を取らないと障害に発展するとなれば、優先度高とし、障害に準じる形の作業を行います。1ヶ月放置しても良い障害であればissueを立てて、週に一回の定例などで共有しタスクの割当などをすればよいでしょう。

急性期

障害が発現した状態です。障害の発現状態には様々あり、サービス全体がダウンすることや、一部の機能が使えなくなる状態、全員もしくは一部のユーザがログインが出来ない状態になったり、ごくごく特定の条件で進行不能に陥るなどあります。

こういった急性期になったとしても、障害が見える形で現れていないこともあります。先述したように一部の状態にあるユーザのみが機能不全に陥っている場合、そのユーザからの報告がないと障害に気付けないこともあります。障害に気づくにはどうするべきか。監視であったり、ログ収集を行って障害を検知できます。一般ユーザ向けのサービスであればTwitterをサービス名でエゴサーチすることも出来ますが、障害が起こったときはだいぶ心が痛むので、はじめのうちはやらないことをおすすめします。

障害が起こったときには、以下のことをリストアップします。

  • 障害の技術的な事象
    • 「ページが見られない」だけでは正確ではなく、ステータスコードは200が返るが白いページ, 404や500などの想定していないステータスコード, 200が返りHTMLは返せているが一部の画像やCSS・JSなどのリソースが取得できずに壊れているなど、技術的な情報を正確に把握することが重要です
    • 「ページが壊れている」という報告だけでは、「なにかが起こっている」しか得られません。技術的な症状の情報を得て、原因を絞り込む手がかりとしていきます。
  • 正確な影響範囲
    • 全員に同じことが起こっているのか、それとも一部の人だけに起こっているかだけでも、原因の範囲を絞り込む手がかりになります。また、対応する人も変わっていきます。全員に起こっている場合、インフラ側に近い問題なことも多いですが、一部の人に起こっている場合は、インフラ側ではなくアプリケーション側・フロントエンド側に原因があることがあります
    • しかし、キャッシュに起因する問題は影響範囲がじわじわと広がっていくなど、これだけで原因の範囲を決めつけるのは早急です。あくまで当たりをつけたり、後述するダメージコントロールの判断材料に使います
  • 具体的な再現方法
    • 影響する人が限られたり、特定のページ・機能だけに障害が起こっている場合、障害が起こっている事がわかって、症状がわかったとしても、その障害を起こすための手順が特殊である場合があります
    • 再現方法を特定するために一番早い方法は、再現した人に前後の操作など聞き取ることですが、開発チームの中の人ではなく一般ユーザからの報告であった場合、困難になります。障害が起こったときから時間が経っている場合は忘れている場合もあり、不正確であることがあります。その情報をもとに調査する場合は、まず裏を取ります。裏を取るというのは、アクセスログであったり、こういった障害やカスタマーサポートのために出している行動ログから報告された行動と照らし合わせます。数字や固有名詞は見間違いなどが起こる事が多いので、裏を取ることが大切です。
    • こういったログがあれば、報告なしにそれだけで再現手順を導けることもあるのですが、当たりをつけるという意味で、一般ユーザさんからの報告は大いに役立ちます。

急性期にはまずここまでができれば御の字で、特に再現方法などは導けないこともあります。また、一つ忘れていると思われることで、「根本的な原因」があります。原因は突き止めることができればそれに越したことはないのですが、急性期に行うべきことであるダメージコントロールには必須ではないです。

ダメージコントロール

急性期に行うことは、根本的な原因を解決するのではなく、障害の被害が拡大するのを止めるダメージコントロールです。具体的なダメージコントロールの手法としては、

  • 障害が起こっている機能・ページを止める
  • サービス全体をメンテナンス入りさせる
  • 負荷に耐えるためにサーバ台数を増やす

などが挙げられます。これらの対処法に原因を特定し、それを直す手法は含まれていません。すぐに直せるのであれば、こういったダメージコントロールを行わずに障害の根本原因を断つのもいいと思いますが、そういった対処をすぐ取れないケースは多々存在します。

この考え方は、ユーザに対して正しくサービスを提供できないサービスはサービスを提供しないことよりも悪いという考え方のもとに立っています。また、アプリケーション側のバグによって、データを破壊していき、それ以後の復旧を困難にするような障害の場合は、直すことよりも止めることをより強く推奨します。

とはいえ、機能を止めたりメンテ入りさせたりするのは、ビジネス上のインパクトが激しく、サービスのステークホルダーの判断が必要です。障害対応はエンジニアだけではなく、サービス全体の判断が下せるステークホルダーの参加も必須になってきます。この場合のステークホルダーとは、お金を管理するプロデューサーなどの人たちを指します。ステークホルダーの人たちも、もしかしたらアラートを受けるべきかもしれません。みなさんが担当するサービスではどうなっていますでしょうか?

またユーザに対して影響が少ない形で、障害を止めるためには、Webサービスそのものに対する理解も必要です。普段、ユーザさんがどのようにサービスを使用しているのか、どういった機能を重点的に使っているのかという知識です。こういった知識がない状態だと、正しくサービスを止める判断ができなくなってしまうので、普段からドッグフーディングをすることも重要です。

複数の障害発生とトリアージ

障害というのは同時に起こることがあります。大体は一つの原因が、別々の障害として表出するのですが、急性期には2つの事象を1つの原因として考えて同時にさばく暇はありません。なので同時に別々の障害に対して2つを対処する状況に陥ります。

まず、2つの事象を明確に切り分けます。この切り分けというのも実は難しい仕事です。切り分けずに全部サービス止めるというのも一つの手です。もし切り分けられたら、優先度をつけます。例えばごくごく一部のユーザにしか出てない現象は後回しにしたり、データを壊さずに表示だけの問題であっても後回しにします。データを壊すような障害は直さない限り永続的に出てしまうので、できれば最優先で直したいものです。

もし、それぞれが優先度中ぐらいで、同じぐらいの優先度であったり、バックエンドととフロントエンドなど、明確に分野が切り分けられる障害であればチームを分けます。それ以後はチームは独立して働き、進捗を報告するぐらいにとどめます。

慢性期

障害の事象自体がダメージコントロールによって収束したとに訪れるのが慢性期です。障害の原因は表出していないが、隠された状態です。まずその隠された原因を調べます。

原因を調べる手がかりとして、急性期に挙げた「障害の事象」「影響範囲」「再現方法」が使えます。また、このときにはまだ調べていなかったログや、アプリケーションコード、インフラの設定などをすべて見直して障害と原因の因果関係について迫ります。

さて、こうして幸いにも障害の原因がわかるとそれを直すということになるのですが、この直し方についても考える必要があります。

例えば、永続的データストアのデータが障害によって汚染されている場合、それを修正した上でないと直したコードであっても障害が残ってしまう場合があります。その場合、データを直すバッチスクリプトなどを流すわけですが、先に直したコードを本番化しなければまたそういった汚染データが発生するなどの状況もありえます。どちらを先にやればいいのでしょうか。

1つの方法としては、ダメージコントロールと同様に障害の原因となった機能を一旦停止して汚染されたデータが新たに発生しないようにした上で、修正バッチスクリプトを流して本番化、それから機能の提供を再開させる手法が考えられます。

もう1つの方法として汚染されたデータを考慮したコードに修正するというのもあります。しかしこういった、後方互換性を考慮したコードは後々消す手間であったり、消し忘れて歴史的経緯が消えて混乱することもあるので、できれば直したらすぐに消してしまいたいところですね。

また、アプリケーションコードの場合、原因を修正するためにはテストコードもセットで書きましょう。デグレが起こって同じ障害が起こることは往々にしてあります。筆者の体感としても、一度障害が起こった場所はまた起こりやすい傾向にあります。なのでテストを書きましょう。テストを書いてCIを回せば世に出る前に障害を防ぐことができます。こんなにコスパがいいことは他のあまりありません。

回復期

晴れて障害が原因まで解消されました。ここで余裕があれば原因の分析を行いましよう。アプリケーションコードが原因であれば、

  • 原因となったPull Request・修正はどこか
    • コードレビューの観点に加えたり、もし自動検知できるものであれば、そういったテストを書いたり、linterを入れる
  • アーキテクチャに問題はないか
    • アーキテクチャが複雑・データのバリデーションが甘い、甘くなりやすい・予想外の挙動が起こりやすいなど
    • 漸進的にアーキテクチャに小修正を加えられるならそれが一番幸せで、全部書き直すのは最終手段
  • 障害に気づくまでの時間が遅くなかったか
    • 監視項目の充実
  • 障害の原因や影響範囲を探るのが困難ではなかったか
    • ログを増して似たような障害に備える

など、見直しをしつつ、障害対応を行わなかった他の人たちが追体験できるような場を設けると、チームとしての障害対応力も向上するかと思います。

まとめ

以上、障害対応を時系列をもって考えるポイントについて挙げてきました。まとめると、

  • 前兆で気付けるのが一番良い。そのために監視とログがある
  • 障害は見つけるのも困難であることが多い
  • 障害レポートは客観的に・技術的に正確に
  • 障害収束は原因の修正が一番の目的ではなく、障害を拡大させないこと
  • 原因を修正するときは直し方も考える
  • 障害対応についての情報を積極的に周りに伝える

といった感じです。この文章がみなさんの障害対応のときの備えになると幸いです。