SLI/SLO運用の実践 shimesabaによる指標モニタリング

カヤックSREの池田です。
先月は、カヤックのプロダクトの一つ『Tonamel』で導入したエラーバジェット算出ツール『shimesaba』の話をしました。

techblog.kayac.com github.com

今回は、実際にどのようにSLI/SLOを運用しているのか?という内容をshimesabaを使った設定例を交えつつ話します。 SLI/SLOの運用にお悩みの方の助けになれば幸いです。

最初のSLI/SLOはどう決定したのか?

SLI/SLOの運用を始めるにあたって、多くの人が悩むのは以下の2つだと思います。

  • 一体何をSLIとすれば良いのか?
  • 最初のSLOはどのくらいにしたら良いのか?

つまりは、最初の1歩をどうしたら良いか?と言う話ですが、こちらに関しては2つ参考になるものがあります。

  1. 『SLO決定のためのArt of SLO』 https://sre-next.dev/2022/schedule/#jp49 *1 を参考にする。
  2. サイトリライアビリティワークブックを参考にする。

先月の記事で紹介した、『shimesaba』を導入したプロダクト『Tonamel』では、2つ目のサイトリライアビリティワークブックを参考にしました。

より具体的には、こちらの書籍の2章全体、特に2.2.2と2.3.3を参考にしました。 最初のSLIについては、書籍には「プロダクトのメインになるアプリケーションのタイプによって異なる "一般的なもの" を採用するのが手堅い」とありました。以下はその例です。

  • リクエスト駆動: いわゆる普通のHTTPサービスなど
    • 可用性: レスポンスに成功したリクエストの比率
    • レイテンシー: 閾値よりも高速に処理されたリクエストの比率
    • 品質: レジリエンスやグレイスフルデグレーション等を備えてる場合、品質低下の起きてない正常なリクエストの比率
  • パイプライン: バッチプロセスや機械学習パイプライン等
    • 新鮮さ: 時間の閾値よりも最近に更新されたデータの比率
    • 正確性: 正しい出力につながった入力データの比率
    • カバレッジ: 正常に処理された有効なデータの割合
  • ストレージ: Amazon S3やGoogle Cloud Storageのようなサービスなど
    • 耐久性: 期待するデータのうちロストしてない比率

Tonamelの場合は、アプリケーションがHTTPサービスの形をとっているのでリクエスト駆動となります。
グレイスフルデグレーション等は特に行っていないので以下の2種類のSLIからはじめました。

  • 可用性: Application Load Balancer(ALB)のTarget 5xx の比率
  • レイテンシー: Application Load Balancer(ALB)のTarget Response time (p95,p90,平均)

SLOに関しては、直近のエラーバジェットを計算してみるのが手っ取り早かったです。 現在進行系でエラーバジェットが減り続けるようなSLOの場合は最初の一歩としては厳しすぎましすし、 逆に、エラーバジェットの残りが100%で張り付く場合は、最初の一歩としては緩すぎます。
現時点でのエラーバジェットの残りが80~95%くらいで落ち着くSLOを設定しました。

具体的には、図のように、エラーバジェットが減り続けるわけでもなく、かといって1%も減ってない状態ではないSLOを設定しました。 皆様も同様の調整をすれば、最初のSLI/SLOをそれほど悩まずに決定できると思います。

shimesabaを使う場合

shimesabaではMackerelで発報されたアラートを元にエラーバジェットを計算するので、Mackerel側に以下のようなアラートを設定します。

  • 可用性のSLOとして
    • 式監視: ALB Target 5xx / (ALB target 5xx + ALB target 2xx) > 0.1%
  • レイテンシーのSLOとして
    • ホストメトリック監視: ALB Target response time p90 > 300ms
    • ホストメトリック監視: ALB Target response time p99 > 500ms
    • ホストメトリック監視: ALB Target average response time > 200ms

この監視ルールをMackerelのCLIツールmkrで出力したものが以下となります。

$ mkr monitors | jq '.[] | select(.name | startswith("SLO"))'
{
  "id": "<monitor_id>",
  "name": "SLO availability request_error_rate > 0.1%",
  "type": "expression",
  "expression": "alias(scale(divide(\nhost(\n  <host_id>,\n  custom.alb.httpcode_count_per_group.<target_id>.target_5xx\n),\n sum(group(host(\n  <host_id>,\n  custom.alb.httpcode_count_per_group.<target_id>.target_2xx\n),host(\n  <host_id>,\n  custom.alb.httpcode_count_per_group.<target_id>.target_5xx\n))\n\n)\n),100), 'request_success_rate')",
  "operator": ">",
  "warning": 0.1,
  "critical": null
}
{
  "id": "<monitor_id>",
  "name": "SLO latency ALB target response time p90 > 300ms",
  "type": "host",
  "metric": "custom.alb.response_per_group.<target_id>.time_p90",
  "operator": ">",
  "warning": 0.3,
  "critical": null,
  "duration": 1,
  "maxCheckAttempts": 1,
  "scopes": [
    "prod"
  ]
}
{
  "id": "<monitor_id>",
  "name": "SLO latency ALB target response time p99 > 500ms",
  "type": "host",
  "metric": "custom.alb.response_per_group.<target_id>.time_p99",
  "operator": ">",
  "warning": 0.5,
  "critical": null,
  "duration": 1,
  "maxCheckAttempts": 1,
  "scopes": [
    "prod"
  ]
}
{
  "id": "<monitor_id>",
  "name": "SLO latency target response time average > 200ms",
  "type": "host",
  "metric": "custom.alb.response_per_group.<target_id>.time",
  "operator": ">",
  "warning": 0.2,
  "critical": null,
  "duration": 1,
  "maxCheckAttempts": 1,
  "scopes": [
    "prod"
  ]
}

監視設定の最初の閾値は、かなり野心的で厳しい基準にしておくところがポイントです。 上記の監視ルールを使った最初のshimesabaの設定としては以下のようになります。

required_version: ">=1.2.0"

destination:
  service_name:  'prod'
rolling_period: 1d       # 運用を開始したら 28dくらいの長めのものに増やす
calculate_interval: 1m   
error_budget_size: 0.5% 

slo:
  - id: availability
    alert_based_sli:
      - monitor_name_prefix: "SLO availability"

  - id: latency
    alert_based_sli:
      - monitor_name_prefix: "SLO latency"

最初の設定では rollikng_period1d くらいの短めにしておきます。 最初はエラーバジェットがすぐに枯渇すると思います。
しかし、監視ルール側の閾値を徐々に緩めていけば、エラーバジェットが減り続けるわけでもなく、かといって1%も減ってない状態ではないSLOにたどり着くと思います。 そして、SLOが決まったら長めのrolling_periodをとって、最初の一歩を踏み出すと良いでしょう。

SLOの継続的な改善

最初のSLI/SLOを設定できたので、運用が始まりました。 Tonamelでは週次の定例があり、その定例のタイミングでSLOに関する見直しをすることがありました。 最初のうちは、ほぼ毎週見直しをしました。 SLOの見直しには、サイトの信頼性に関わる定性・定量とエラーバジェットの関係があるのかを考えました。

例えば、以下のようなものとエラーバジェットが関係しているのか?ということに注目しました。

  • お問い合わせ件数
  • 『つながらない』『見えれない』などの反応の多さ (可用性のエラーバジェット)
  • 『重い』『遅い』などの反応の多さ (レイテンシーのエラーバジェット)

プロダクトにとって『サイトの信頼性』に関係する現象というのは、様々でこれを見ておけばいいというものがあるとは思えません。
自分たちのプロダクトでは、何を重要視していてどういう事が起きたら、エラーバジェットが損失するというのが良いのか?ということを考えていく必要はあります。Tonamelの場合は、カスタマーサポートの担当者が週に1度、お問い合わせの件数をその内訳も含めて共有する機会があるので、エラーバジェットの増減とお問い合わせの件数についてを注視していました。
他にも開発チームのエンジニアに、なにか問題が起こっていないか?というヒアリングも行なっていました。

その経験を踏まえて、以下の表のようにして見直していくと、ある程度野心的なSLOに収束していくと思います。

エラーバジェット サイトの信頼性に対する評価 アクション
損失が20%以下で安定 特に問題はなさそう (そのまま様子見)
損失が20%以下で安定 なにか問題がありそう SLOを厳しくしてみる
減り続けてる 特に問題なさそう SLOを緩めてみる
減り続けてる なにか問題がありそう (早めにプロダクトを改善しよう!)
損失が20%より多くて安定 特に問題なさそう SLOを緩めてみても良いかもしれない
損失が20%より多くて安定 なにか問題がありそう (注意深く様子見)

特にSLI/SLOの運用の開始初期では、非エンジニアのチームメンバーやユーザーにサイトの信頼性に関する情報を収集して、積極的にSLOを厳しくしたり緩めたりするトライアンドエラーが大事だと感じました。
最初から完璧なSLI/SLOを求めるのではなく、徐々に良いものにしていくという運用をすることで、導入もしやすくなると思います。

shimesabaをお使いの場合は、この辺のSLOを緩めたり厳しくしたりすることは、監視ルールの閾値を変更するだけで実現できるので、柔軟に変動させることができると思います。

プロダクト特有のSLOの設定

最初の1歩では、一般的なSLI/SLOだけを設定して開始しました。 SLI/SLOの運用を続けていくと、場合によっては最初に設定したもの以外のプロダクト特有のSLI/SLOを設定する必要性も出てくる可能性はあります。
Tonamelの場合は、SLI/SLOの運用を開始したときは『可用性』と『レイテンシー』のエラーバジェットのみでしたが、途中で『品質』という名の新しいSLI/SLOを設定しました。
このSLI/SLOの設定の背景には、アプリケーションがGraphQLという技術を採用していることが関係しています。

GraphQLという技術ではエラーの取り扱い方にある一定の文化があり、『HTTPリクエストに対するレスポンスはHTTP 200 Okで正常ではあるが、レスポンスの内容としてはエラーである。』という状況があります 具体的には、HTTP 200 Okで以下のようなレスポンスボディを返す場合があるということです。

{
  "errors":[
    {
      "message": "error message.",
      "extensions":{
        "code":"INTERNAL_SERVER_ERROR"
      }
    }
  ]
}

このようなレスポンスを通称GraphQL Errorと呼んでいますが、このGraphQL Errorが発生すると、フロントエンド側でエラーハンドリングをすることになります。 エラーハンドリングした結果、対応できない場合はユーザーに対してエラーメッセージを表示することになります。 ユーザーに見える形でのエラーメッセージが頻出すると、それはサイトの信頼性低下に繋がってしまいます。 ですので、このGraphQL Errorの発生率をSLI/SLOとして追跡することが重要だということがわかりました。 しかしながら、GraphQL ErrorはHTTPリクエストへのレスポンスとしては正常に返せているので、最初に設定した『可用性』というALBのTarget 5xx の比率をSLIとして用いたSLOには反映されません。 そこで別途、Kinesis Data Streamで収集しているアクセスログからGraphQL Errorの発生件数とGraphQLリクエスト件数を集計し、サービスメトリックとしてを投稿しました。 そして、GraphQL Errorの発生件数/GraphQLリクエスト件数 = GraphQL Error rateをSLIとした『品質』と言う名前のSLOを設定しました。

より具体的には、以下のような式監視を追加し、shimesabaの設定を変更しました。

監視ルール

$ mkr monitors | jq '.[] | select(.name | startswith("SLO quality"))'
{
  "id": "<monitor_id>",
  "name": "SLO quality graphql_error_rate > 0.5",
  "type": "expression",
  "expression": "alias(\nscale(divide(\n  service(\n prod,\n \"graphql.error.request_count\"\n)\n,service(\n prod,\n \"graphql.requests.request_count\"\n)),100),'graphql_error_rate')",
  "operator": ">",
  "warning": 0.5,
  "critical": null
}

shimesabaの設定

required_version: ">=1.2.0"

destination:
  service_name:  'prod'
rolling_period: 28d      
calculate_interval: 30m   
error_budget_size: 200m   

slo:
  - id: availability
    alert_based_sli:
      - monitor_name_prefix: "SLO availability"

  - id: latency
    alert_based_sli:
      - monitor_name_prefix: "SLO latency"

  - id: quality
    alert_based_sli:
      - monitor_name_prefix: "SLO quality"

以上の設定で、1分間のGraphQL Errorの比率が0.5%を超えた場合にエラーバジェットが削れるSLOが設定できました。

このように、アプリケーションが採用している技術やプロダクトの性質由来で、追加のSLI/SLOを設定することや最初のSLI/SLOの定義を変えることはありえます。

まとめ

今回は、SLI/SLOを具体的にどう設定するのか、shimesabaを用いながらどう運用していくのかの話をしました。 これからSLI/SLOの運用をこれから開始する場合は、以下の点を踏まえておくと良いでしょう。

  • 何をSLIとするか?: 最初は、一般的なもので良い(リクエスト駆動のアプリケーションの場合は『可用性』と『レイテンシー』)
  • 最初のSLOはどのくらいにするか?: 試しにエラーバジェットを計算して80%~95%になるくらい。
  • 定期的に見直しをして、積極的にSLOを厳しくしたり緩めたりするのは大事。
  • 場合によっては、プロダクト特有の新たなSLI/SLOを設定することもある。

実際にSLI/SLOの運用をしてみたところ、個人的にはエラーバジェットを常に20%くらい使った状態で安定し続けるくらいのSLOが良いと思っています。 エラーバジェットは、文字どおりエラーに関する予算ですので、使いすぎても、使わなさすぎてもあまり良くないと感じています。 以上、皆様のSLI/SLOの運用にお役に立てば幸いです。

カヤックでは、SLO運用に興味があるエンジニアも募集しています

*1:2022/5/15に開催されてた『SRE Next 2022』でグーグル合同会社による素晴らしい発表