ネットワークサービス部のid:a8544です。
連載19回目の今回は、わたしたちの開発するシステムに備わるヘルスチェック(正常性確認)機能についてご紹介します。
以前の記事で、インフラに着目した間接的なヘルスチェックについてご紹介しました。わたしたちのシステムには、それとは別に、アプリケーションが自身のヘルスチェックを行う機能もあります。
🌐 システムが依存する外部の系
わたしたちが開発している「プライベートブリッジのバックエンド」の機能は、 Pythonのコードが動作するPython処理系以外の外部の系にも依存しています。 たとえば、
があります。
これら外部の系とのやりとりがうまくいかないと、リクエストを正しく処理できず、 場合によってはサービスの障害につながります。
👨⚕️ なぜヘルスチェックをするのか
わたしたちは、システムの運用を通じて前述のような外部の系との通信に起因する障害を 過去に経験しており、そのたびにその個別具体的な問題に対する対処をしてきました。
しかし、障害として問題が発覚してから原因を修正するより、 障害につながりうる異常を早期に検知し、サービスへ影響する前に対処するほうがよいことは明らかです。
わたしたちの過去の経験にあったような異常を検知するためには、
外部から観測しにくいアプリケーションの内部状態を観測する必要がありました。
例えば、「アプリケーションとDBMSとの接続状況」は外部からは観測しにくいです。
下図のように、あるコンテナ中のあるプロセスがもつコネクションプール中の
コネクションの一つ一つまで、それが正常か外部から確認するのは困難です。
ほかに検知が困難だった例として、アプリケーションから外部APIへの通信経路中の異常(プロキシサーバ等)があります。 アプリケーションが実際に使う通信経路で通信を試行しなければ、 その経路中にある問題には気づくことができません。さらには、外部API自体に障害が発生していることもあり、 この場合も実際に通信せずに気づくことは困難です。
この種の監視を、外部に存在する監視基盤 だけで 実現するのは困難だと考えられました。
そこでわたしたちは、外部の系へのアクセスを含めた、実際のリクエスト処理に近い形の 動作をチェックするヘルスチェックを、 アプリケーション自体のAPIの一つとして 実装することにしました。
アプリケーションだけで完結する単純なエコーのようなチェックに比べ、ヘルスチェックの実装は複雑になりますが、 アプリケーションの内部からしか分からない、より複雑な問題を実際のリクエストを処理する前に検知できるようになります。 また、APIの一部として実装することで、外部からの定期的な監視が容易になります。
🧱 ヘルスチェックの構成・実装
わたしたちの開発しているバックエンドは、モジュラーモノリスの構成をとっています。 そのため、ヘルスチェックも各モジュールごとに実装されています。
各モジュールのヘルスチェックでは、複数のヘルスチェック項目についてチェックを実施し、 すべての項目について成功すれば成功を、一つでも失敗すれば失敗とします。
加えて、モジュールごとの結果を集約し、その結果を外部に公開するためのエンドポイントが用意されています。 この集約エンドポイントへアクセスするたびに、ヘルスチェックが行われます。 このエンドポイントでは、各モジュールでのヘルスチェック結果がすべて成功すれば成功を、 一つでも失敗すれば失敗とします。これにより、集約エンドポイントで「成功」が返ることを チェックするだけで、全てのモジュールの全ての項目で成功したことが確認できるようになっています。
各項目の実装
- データベースに関する項目では、実際のリクエスト処理と同様の方法でトランザクションを開始し、 値が読み書きできることを確認します。
- メッセージキューに関する項目では、テスト用に一時的に作成したキューについて、
実際のリクエスト処理と同様の方法でメッセージを発行し、正しくワーカーが処理することを確認します。
メッセージの流れは、可能な限り実際の処理を模倣しています。
- 具体的には、下図のようにワーカー間でヘルスチェック用のメッセージをやりとりさせ、
もとのWeb API ワーカーに戻ってくることを確認します。
これにより、各ワーカーが実際にメッセージを送受信可能な状態にあることが確認できます。
メッセージキューのヘルスチェックの動作
- 具体的には、下図のようにワーカー間でヘルスチェック用のメッセージをやりとりさせ、
もとのWeb API ワーカーに戻ってくることを確認します。
これにより、各ワーカーが実際にメッセージを送受信可能な状態にあることが確認できます。
- 外部APIに関する項目では、インターネット経由で利用するAPIについて、実際のリクエスト処理と同様の方法(使用するプロキシ、リトライ処理等)でリクエストを行います。 この際利用するAPIは、例えばSlack APIにあるテスト用エンドポイントのように、 テスト用のものを使います。
🔎 ヘルスチェックを活用した監視
わたしたちが使っている監視基盤であるZabbixからこの集約エンドポイントに定期的にアクセスして、その結果を監視しています。
このヘルスチェックを定期的に実行する運用を始めてから、 これまで気付いていなかった、システムに潜在する問題に気付くことができています。 例えば、コンテナやワーカープロセスは正常に動作しているように見えて、 実際にはメッセージキューからのメッセージの取得ができなくなっているワーカーが存在しうる問題 がありました。このような問題は、実際にメッセージをエンキュー・デキューする過程を 踏まないと気付きにくいものです。
また、依存する外部サービスのAPIに障害が発生した際にも、実際のリクエストをまたずに 気付くことができ、わたしたちのシステムの正常な運用に必要な対応を すばやくとることができるようになりました。
まとめ
わたしたちのシステムに実装されている、アプリケーションレベルのヘルスチェックについてご紹介しました。
実際のユースケースにより近い内容のチェックを実装することで、 外部からは気付きにくい異常に気づきやすくなります。その際には、アプリケーション側で ヘルスチェックを実装することが有効です。 またヘルスチェックの実施・結果取得を外部から利用しやすい方法で提供することで、 監視基盤との統合がはかりやすくなります。
以前ご紹介したインフラ面のチェックと合わせて、ヘルスチェックが充実していることは、 システムの運用をより自信をもって(安心して)行う助けになっています。
次回からは、テーマを変えて「システム開発戦略編」と題して、わたしたちが システム開発全体を通して取っている戦略をご紹介していきます。 お楽しみに。
連載バックナンバー
CI戦術編
- 【CI戦術編 その1】alembic check コマンドを活用したマイグレーションスクリプト生成忘れ防止
- 【CI戦術編 その2】コードレビューを充実したものにする方法、あるいは一生残る恥ずかしい履歴を作らないように
- 【CI戦術編 その3】Pythonのimportのことならまかせろ isort
- 【CI戦術編 その4】Blackを利用したコーディングスタイルの統一
- 【CI戦術編 その5】Pythonで明示的に型を書く理由
- 【CI戦術編 その6】Python開発の強い味方 Pylint
- 【CI戦術編 その7】 pyupgradeを使って最新の記法に対応してみた
- 【CI戦術編 その8】OAS(OpenAPI Specification)で仕様書を自動生成しよう
- 【CI戦術編 その9】自動生成しか勝たん openapi-typescript
- 【CI戦術編 その10】 Renovateで依存ライブラリのアップデートに負けない方法
- 【CI戦術編 その11】Trivy: あなたの使ってるライブラリ、大丈夫ですか?
- 【CI戦術編 その12】戦術編総まとめ — いつ何を使うべきか
- CI・CD戦略編