BASEプロダクトチームブログ

ネットショップ作成サービス「BASE ( https://thebase.in )」、ショッピングアプリ「BASE ( https://thebase.in/sp )」のプロダクトチームによるブログです。

アプリケーション監視のパターン「Health エンドポイントパターン」を実践する | 書籍『入門 監視 ―モダンなモニタリングのためのデザインパターン』を読んで

f:id:khigashigashi:20190305134900j:plain
出典: 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