出典: https://unsplash.com/photos/JKUTrJ4vK00 BASE BANK株式会社 でソフトウェアエンジニアをやっている東口( @hgsgtk )です。即時に資金調達ができる金融サービス「 YELL BANK(エールバンク) 」というプロダクトを開発・運用しています。 さて、日々、ユーザーに使っていただくサービスを運営していく中で、「サービスを安定的に提供できているか」という観点において、 監視する技法 について関心があります。 そんな折、『 入門 監視――モダンなモニタリングのためのデザインパターン 』という書籍が最近発売され、世間的にも監視について、関心が高まっているかと思います。 今回は、この書籍の中から、実際に業務で実践していた「 Health エンドポイントパターン 」について、 実践例 と 書籍の内容の深掘り を含めて紹介しようと思います。 また、 Mackerel Meetup #13 Tokyo というイベントでも今回の内容を発表いたしましたので、こちらも合わせてご参照ください。 Health エンドポイントパターンとは アプリケーションの健全性を伝えるアプリケーション内のHTTPエンドポイントを作る パターンです。 カナリアエンドポイント(canary endpoint)・ステータスエンドポイント(status endpoint)とも呼ばれ、特に名前がついていることを知らずに使っている方も多いのではないでしょうか。(筆者もその一人でした。) このエンドポイントでは、最低限「HTTPリクエストを受けてレスポンスを返せるか」という情報のみを返すこともできれば、デプロイされたバージョンや依存関係のあるDBなどのサブコンポーネントのステータスといった情報までをレスポンスに含めることもできます。 使用用途としては、次のようなケースがあげられます。 Mackerel など監視SaaSからの 外形監視 ALBなど ロードバランサのヘルスチェック アプリケーション 起動確認のデバッグ 実際に、実践する場合、アプリケーションの状態を伝えるHTTPエンドポイントを作成します。例えば、 /healthcheck や /ping などと言った名前になるでしょうか。 そのエンドポイントは、 ヘルスチェックに成功すればHTTPステータスコード200 を、 失敗した場合は200以外(特に503) を返すという実装になります。 BASE BANKでの実践例 BASE BANKでは、2つのエンドポイントを用途別に用意する方法をとっていて、それぞれ外形監視の対象としています。 ひとつが、 コンテナ単体の生存確認 を主目的とした、 /health 、もう一つが、 依存しているDB・Redisなどへの接続までを確認対象に含める 、 /health/deep です。 なお、この実践例は、 「 Mackerel Meetup #13 Tokyo 」で 山根 ( @fumikony ) より発表した、『 BASEにおけるMackerel利用上の工夫と困りごとのご紹介 』内でも事例として言及しております。 Mackerel 等の監視SaaSとの組み合わせという点では、合わせてこちらを一読いただけるとより良いかなと思います。 単体の生存確認: /health Go言語で実際に実装する場合は、例えば次のようなHTTP Handlerになります。 // HTTPステータスをフィールドに含むレスポンスフォーマット type SimpleResponse struct { Status int `json:"status"` Message string `json:"message,omitempty"` Detail string `json:"string,omitempty"` } // 200レスポンスが返却される func SimpleHealthCheck(w http.ResponseWriter, r *http.Request) { rs := SimpleResponse{ Status: http.StatusOK, } respondJson(w, rs, http.StatusOK) } // JSON形式でレスポンスを返却する func respondJson(w http.ResponseWriter, body interface {}, status int ) { w.Header().Set( "Content-Type" , "application/json; charset=utf-8" ) w.WriteHeader(status) if err := json.NewEncoder(w).Encode(body); err != nil { fmt.Fprintf(os.Stderr, "failed to encode response by error '%#v'" , err) w.WriteHeader(http.StatusInternalServerError) return } } これを、 /simple/.health_check に対してルーティング設定した場合は次のようなレスポンスとなります。 -> % curl -i http://localhost:8080/simple/.health_check HTTP/1.1 200 OK Content-Type: application/json; charset=utf-8 Date: Mon, 04 Mar 2019 13:24:23 GMT Content-Length: 15 {"status":200} これは、ロードバランサからの外形監視などの際に利用しています。 依存サービス込みの動作確認: /health/deep 次に、DBやRedisなど依存している外部サービスに接続できているかどうかを確認対象に含めるエンドポイントです。 func SimpleDeepHealthCheck(w http.ResponseWriter, r *http.Request) { _, err := NewMySQL(config.DB) if err != nil { // DBへのコネクションにてエラーが発生した場合は503レスポンス rs := SimpleResponse{ Status: http.StatusServiceUnavailable, Message: "failed to get connection database" , Detail: err.Error(), } respondJson(w, rs, rs.Status) return } rs := SimpleResponse{ Status: http.StatusOK, Message: "success to connect server" , } respondJson(w, rs, rs.Status) } これを、 /simple/.health_check/deep に対してルーティング設定した場合次のようなレスポンスが返却されます。 -> % curl -i http://localhost:8080/simple/.health_check/deep HTTP/1.1 200 OK Content-Type: application/json; charset=utf-8 Date: Mon, 04 Mar 2019 13:33:22 GMT Content-Length: 53 {"status":200,"message":"success to connect server"} 例えば、データベースに対する接続が失敗した場合は、次のような503レスポンスが返却され、アプリケーションの健康状態について知ることができます。 -> % curl -i http://localhost:8080/simple/.health_check/deep HTTP/1.1 503 Service Unavailable Content-Type: application/json; charset=utf-8 Date: Mon, 04 Mar 2019 13:35:38 GMT Content-Length: 106 {"status":503,"message":"failed to get connection database","string":"sql: connection is already closed"} このパターンのエンドポイントは、 Mackerel からの外形監視で主に利用しています。 利点 外形監視に利用できる APIの通信ができるかといった外形監視は特にアプリケーションを運用していると最低限関心をもつところだと思います。このパターンでは、ステータスコードで状態を判別できるため、外形監視に使いやすいかと思います。 デバッグに有効 起動したアプリケーションが正しくHTTPリクエストを処理する事ができるかを知る上で、重宝しています。 さらに、依存関係のあるDBなどのサービスへのコネクションが取れるかについても最低限確認できるため、起動確認に有用です。 余談:コンテナベースアプリケーションとの親和性 運用している YELL BANK というサービスは、以前公開した『 ECS(Fargate)でコンテナアプリケーションを動かすための設定情報の扱い方 』という記事でも紹介した通り、コンテナ上で動作することを前提としたアプリケーションとして機能提供しています。 コンテナ内で動かすアプリケーションにおいても、外形監視は重要と感じていますが、実際このパターンはコンテナベースアプリケーションにおいてはどのように考えられるでしょうか。 コンテナベースアプリケーション設計として、「Health エンドポイントパターン」をどう評価できるかを考えるにあたり、 redhat が公開している『 Principles of container-based application design 』 から参考になる原則を一つ見てみましょう。 それが、「 HIGH OBSERVABILITY PRINCIPLE (HOP) 高観測可能性の原則 」 という設計原則です。 これは、コンテナ内部をブラックボックスのように扱う設計前提を持った上で、自身のアプリケーションの活動状況や準備状況など、様々な状態チェックについてAPIを提供するという設計について言及しています。 自身の健康状態を伝える「Health エンドポイントパターン」も、観測度を上げる上で有用なパターンと言えそうですね。 ドラフト段階の共通レスポンスフォーマットについて さて、「Health エンドポイントパターン」について、パターンと実践について見たところで、少し話を深掘りして『 Health Check Response Format for HTTP APIs 』という議論中の共通レスポンスフォーマットについて見ていきたいと思います。 これは、『 入門 監視――モダンなモニタリングのためのデザインパターン 』の「付録C」という章で言及されているものです。 どういったレスポンスを返すべきか、議論中のこのフォーマットについて少し深掘りしてみます。 Health Check Response Format for HTTP APIs このフォーマットは、大きく以下の3つの特徴を持ちます。 JSONフォーマット を利用する media-typeは、 application/health+json とする 必須フィールドである status と いくつかのオプショナルなフィールドを含む 必須フィールド: status status には「次のどれかの値を設定する」とされています。 pass : healthy status code: 2xx-3xx range (MUST) その他、下記の選択肢も可能 Node's Terminus をサポートするための ok JavaのSpringBootのための up fail : unhealthy status code: 4xx-5xx range (MUST) その他、下記の選択肢も可能 Node's Terminus をサポートするための error JavaのSpringBootのための down warn : healthy, with some concerns status code: 2xx-3xx range (MUST) warningレベルのタイプが設定できるのは一つ面白いところかなと思いました。 その他フォーマット一覧 必須とされている status 以外にも次のフィールドがオプショナルな項目として提示されています。 status version (optional) - サービスの公開バージョン releaseId (optional) notes (optional) - 健康状態に関する記述 output (optional) - 生のエラー出力、 statusが pass のときは省略すべき details (optional) - 依存しているサービスも含めた詳細情報、 The Details Object というオブジェクトで定義 links (optional) - より詳細情報を得るための外部URLなど serviceId (optional) - アプリケーションスコープなユニーク識別子 description (optional) - 人間に優しい説明 The Details Object 下流の依存関係やRedisなどのアプリケーションから見たサブコンポーネントの状態を伝えるためのオブジェクトとして提示されています。公式の例では、以下のようにcassandraやcpu・memory状態などアプリケーションが依存するものについての健康状態を伝える例を示しています。 " details ": { " cassandra:responseTime ": [ { " componentId ": " dfd6cf2b-1b6e-4412-a0b8-f6f7797a60d2 ", " componentType ": " datastore ", " observedValue ": 250 , " observedUnit ": " ms ", " status ": " pass ", " time ": " 2018-01-17T03:36:48Z ", " output ": "" } ] , " cassandra:connections ": [ { " componentId ": " dfd6cf2b-1b6e-4412-a0b8-f6f7797a60d2 ", " type ": " datastore ", " observedValue ": 75 , " status ": " warn ", " time ": " 2018-01-17T03:36:48Z ", " output ": "", " links ": { " self ": " http://api.example.com/dbnode/dfd6cf2b/health " } } ] , " uptime ": [ { " componentType ": " system ", " observedValue ": 1209600.245 , " observedUnit ": " s ", " status ": " pass ", " time ": " 2018-01-17T03:36:48Z " } ] , } , inadarei.github.io Health Check Response Format for HTTP APIs から学ぶこと これは、現時点では、ドラフト版なので正式に守るべき標準・制約というわけではありません。しかし、実際に「Health エンドパターン」を実践するにあたって、詳細な点について迷いが生まれた際にこのように議論している場所があると知ると、参考になるかと思います。 最後に 私は、PHPやGo言語でのアプリケーション開発がメインのサーバーサイドエンジニアですが、そのような視点でも、『 入門 監視――モダンなモニタリングのためのデザインパターン 』は非常に勉強になります。 迷われている方はぜひお手にとって見てはいかがでしょうか。 また、 BASE株式会社 は、サービスの継続的な提供を守り・発展させていきたいそんな方を募集中です。ご興味があればぜひお気軽に遊びにいらしてください。 binc.jp