Mackerel における SLO 運用を実現するための AWS 活用術
2025-10-02 | Author : 西川 拓志 (株式会社はてな)
はじめに
株式会社はてな Mackerel 開発チームで Embedded SRE を担当している西川と申します。Mackerel はシステムのオブザーバビリティを高めるプラットフォームとして、Mackerel 自身の信頼性維持と機能開発のバランスを保つために SLO (サービスレベル目標) の策定と運用を行っています。
本記事では、Mackerel の SLO 運用を実現するために AWS をどのように活用しているのかについて詳しく紹介します。
builders.flash メールメンバー登録
builders.flash メールメンバー登録で、毎月の最新アップデート情報とともに、AWS を無料でお試しいただけるクレジットコードを受け取ることができます。
Mackerel の SLI/SLO について
Mackerel で運用している SLI/SLO についてお話しする前に、クリティカルユーザージャーニー (CUJ) から考えてみましょう。CUJ はユーザーが目的を達成するために行うユーザー操作や経路のセットです。ユーザー操作を基に、CUJ を実現する上でユーザーにとって重要なことに分解します。
今回は「Mackerel でメトリクス確認用のダッシュボードを作成する」という CUJ を例に挙げます。この CUJ は 3 つのユーザー操作と 5 つのユーザーにとって重要なことに分解できます。
- メトリクスを投稿する
(1) 投稿したメトリクスが正常に保存される
(2) 投稿したメトリクスが一定時間内で参照できるようになる - ダッシュボードを作成する
(3) ダッシュボードを正常に作成できる - ダッシュボードでメトリクスを閲覧する
(4) 投稿したメトリクスを正常に参照できる
(5) 投稿したメトリクスが欠損、意図しない形でのデータ変更がない状態である
上記からも分かる通り、メトリクスの投稿・閲覧が正常にできるだけではなく、メトリクス自体の状態に関しても考慮する必要があります。例えば、メトリクスの値が欠損している場合、ユーザーにとって知りたいメトリクスの値がわからない = CUJ が実現できていないからです。
整理した内容を踏まえて、SLI/SLO について考えてみましょう。SLO の設計においては、サービスを Request-driven、Pipeline、Storage に分類する考え方があります。これは「サイトリライアビリティワークブック」で言及されている分類方法で、サービスの特性に応じて適切な SLO を設定するために重要な概念です。
今回利用する Request-driven、Pipeline の分類は以下のようなものです。
Request-driven Service
ユーザーからのリクエストに応答するサービスで、以下の SLI 種別が定義されています。
SLIの種別
|
定義
|
---|---|
Availability
|
正常に応答したリクエストの⽐率 |
Latency
|
しきい値より早く応答したリクエストの⽐率 |
Quality
|
特定の品質を満たしたリクエストの⽐率 |
Pipeline Service
データを入力として受け取り、変更し、出力するサービスで、以下のSLI種別が定義されています。
SLIの種別
|
定義
|
---|---|
Freshness
|
ある特定の時間をしきい値にして、それより最近に更新されたデータの⽐率 |
Correctness
|
正しい値の出⼒につながったデータ処理への⼊⼒レコードの⽐率 |
Coverage
|
バッチ処理: ターゲット量以上のデータを処理したジョブの⽐率 |
サービス分類を踏まえて整理した SLI について
先述の CUJ に対して、サービス分類を踏まえて整理した SLI は下記のようになります。今回のケースでは、投稿されたメトリクスをシステムとしては Pipeline として処理しています。そのため、正しい値の出力をメトリクス取得処理に当てはめた Correctness、メトリクスの投稿から取得までを一連のデータ処理として捉えた Freshness も採用することで、ユーザーにとって重要なことのうち (2)、(5) に関しても SLI で表現しています。
メトリクスの投稿
SLIの種別
|
SLI
|
---|---|
Availability
|
正常に処理されたメトリクス投稿のリクエストの割合 |
Latency
|
十分に高速に処理されたメトリクス投稿のリクエストの割合 |
Coverage
|
一定時間内に保存に成功したメトリクスの割合 |
Correctness
|
欠損、意図しない形でのデータ変更なく正しく保存されたメトリクスの割合 |
Freshness
|
投稿したメトリクスがストレージから取得できるまでの時間 |
ダッシュボードの作成
SLIの種別
|
SLI
|
---|---|
Availability
|
正常に処理されたダッシュボード作成のリクエストの割合 |
Latency
|
十分に高速に処理されたダッシュボード作成のリクエストの割合 |
メトリクスの閲覧
SLIの種別
|
SLI
|
---|---|
Availability
|
正常に処理されたメトリクス取得のリクエストの割合 |
Latency
|
十分に高速に処理されたメトリクス取得のリクエストの割合 |
メトリクスの投稿において、Availability と Coverage という似たような SLI が登場していますが、これは後述する投稿されたメトリクスの処理を踏まえて、2 種類の SLI を採用しています。
時系列データベース (Diamond) について
次に、メトリクスの投稿・閲覧処理の根幹を担う Mackerel の時系列データベースについてお話しします。
Diamond は Graphite 互換の時系列データベースです。Mackerel に投稿したメトリクスは Diamond に保存されます。クライアントが投稿したメトリクスはメッセージブローカーに非同期で書き込まれて、Amazon ElastiCache, Amazon DynamoDB などのデータストアに保存されます。メトリクスの取得時には、取得対象の時間に応じたデータストアからメトリクスを取得します。詳細は下記の資料をご確認ください。
まかれるあなとみあ ―Mackerel のしくみを理解する 30 分― @ Hatena Engineer Seminar #16 - Speaker Deck
メトリクスの投稿に関して、Diamond の実装ではメトリクスの受信・保存を非同期処理で分離しています。そのため、SLI の計測においてはメトリクスの受信・保存処理それぞれの処理成功率や処理時間を考慮する必要があると考えました。そのため、メトリクスの受信処理は Availability、保存処理では Coverage というように SLI を分けています。
SLI の計測について
Mackerel では、Pipeline Service に分類される SLI について、計測対象の違いから複数の計測用ツールを AWS 上で動かして計測しています。実装手段については、責務を明確にして用途ごとに複数のツールを運用すると、ツール自体の複雑性も抑えられ、実装の修正やリファクタリングもしやすく、結果として持続的な SLI 計測・SLO 運用につなげることができます。
Diamond 用の SLI 計測用のツール
Diamond に関連する SLI 計測用のツールを Amazon ECS で動かしています。実装としては、計測用のメトリクスを投稿後に取得し、処理時間やデータの突き合わせを行って、その結果をメトリクス投稿の Correctness・Freshness の SLI 用のメトリクスとして投稿しています。
- メトリクス投稿の Correctness = 正しく保存されたメトリクス数 / 総メトリクス数 ※正しい=欠損、意図しない形でのデータ変更がない状態
- メトリクス投稿の Freshness = メトリクスを取得できた時刻 - メトリクスを投稿した時刻
別システムでの SLI 計測では、AWS Lambda + Amazon EventBridge (EventBridge rules) の構成を利用していますが、いずれにしても AWS では軽量な SLI 計測環境の構築が可能です。認知負荷の低いシンプルな構成を採用することで、SLI の計測対象の拡充や SLO 運用自体のハードルを下げることができます。
cloudwatch-logs-aggregator
cloudwatch-logs-aggregator は Amazon CloudWatch Logs に出力されたログを集計し、Mackerel にメトリクスとして投稿するツールです。メトリクス投稿の Coverage の計測において利用しています。
Amazon CloudWatch Logs のログを集計して Mackerel にメトリクスを投稿する - Mackerel ヘルプ
出力ログ
Diamond へのメトリクスの保存成功時にログを出力して、そのログから各データストアへの保存されたメトリクス数などを集計・算出し、Mackerel にメトリクスとして投稿しています。
{
"value": {
"errorCount": 0,
...
"successDynamoDBCount": 14,
"successDynamoDBMetricCount": 1043,
"successRedisCount": 968,
"totalCount": 968
}
}
集計・算出について
集計・算出においては、CloudWatch Logs Insights のクエリ構文を利用しています。
stats
sum(value.totalCount) as total,
sum(value.errorCount) as error,
sum(value.successRedisCount) as success_redis,
sum(value.successDynamoDBCount) as success_dynamodb,
sum(value.successDynamoDBMetricCount) as success_dynamodb_metric
ログを SLI 計測に利用する際の注意点
ログを SLI 計測に利用する際の注意点は、ログ自体の信頼性を適切に維持することです。例えば、筆者が実際に遭遇した事象だと、uber-go/zap というライブラリで特定の設定のプリセットを利用していたのですが、ログのサンプリングがデフォルトで有効になっており、ログを基にしたメトリクスの数値が意図せず変動するという事象がありました。
The production configuration (as returned by NewProductionConfig() enables sampling which will cause repeated logs within a second to be sampled.
ログ自体の信頼性を考慮する必要がありますが、SLI の計測に利用できるメトリクスが存在しない場合、ログからメトリクスへの変換は有効な手段です。
ALB のメトリクスを利用した SLI の計測における注意点
Application Load Balancer (ALB) のメトリクスを利用して SLI を計測する際には、メトリクスの定義を正確に理解することが重要です。特に 4XX エラーのカウント方法には注意が必要で、場合によっては SLO の計算が不正確になる可能性があります。
先述の例に加えて、Mackerel では、OpenTelemetry の仕様に準拠したラベル付きメトリクス投稿処理に関するアベイラビリティーを計測しています。この時に、非正常なイベント (ラベル付メトリクスの投稿に失敗したイベント) の数に関して考えてみます。
現在の Mackerelの アーキテクチャでは、メトリクスの受信側でパフォーマンスの悪化が発生して OpenTelemetry Collector に設定されている送信タイムアウトの時間を超過すると、Collector は connection を切断し、ALB は HTTP 460 エラーコードを返します。
Application Load Balancer のトラブルシューティング - Elastic Load Balancing
OpenTelemetry Collector では デフォルトの送信タイムアウトの設定値は 5 秒 で設定されています。ユーザーがより短い設定値に変更する場合もありますが、大体のユーザーは 5 秒以内にメトリクスの投稿が完了することを期待すると仮定して、HTTP 5xx エラーコードのリクエストに加えて HTTP 460 エラーコードを返したリクエストも非正常なイベントに計上して SLI を算出したいと考えました。
SLI を算出する一般的な計算式が下記だとすると
SLI = (目標とする期間内の正常イベント数)/(目標とする期間内の全てのイベント数) * 100
OpenTelemetry のメトリクス投稿処理に関するアベイラビリティーの SLI の計算式は下記のようになります。
OpenTelemetry のメトリクス投稿処理のアベイラビリティー
= (メトリクス投稿に成功したリクエスト数)/(全てのリクエスト数) * 100
= (全てのリクエスト数 - メトリクス投稿に失敗したリクエスト数) / (全てのリクエスト数) * 100
当初は、ラベル付きメトリクス投稿処理のアベイラビリティーの計測において、ALB の CloudWatch メトリクスを利用して下記のように算出しようと考えました。
メトリクス投稿に失敗したリクエスト数
= HTTPCode_ELB_4XX_Count + HTTPCode_ELB_5XX_Count + HTTPCode_Target_5XX_Count
全てのリクエスト数
= RequestCount + HTTPCode_ELB_4XX_Count + HTTPCode_ELB_5XX_Count
これを踏まえて、ALB の CloudWatch メトリクスの定義 を確認します。RequestCount というメトリクスについては、AWS ドキュメントによるとロードバランサーノードがターゲットを選択できたリクエストに対してのみ増分され、ターゲットが選択される前に拒否されたリクエストはカウントされません。
4XX/5XX レスポンスコードを返したリクエストがどの CloudWatch メトリクスにカウントされるかは、クライアントからのリクエストが「ALB に紐づくターゲットが選択されているかどうか」によって異なります。例えば、4XX レスポンスコードの場合は下記の通りです。
- ターゲットが選択されている場合 : HTTPCode_Target_4XX_Count にカウントされます
- ターゲットが選択されていない場合 : HTTPCode_ELB_4XX_Count にカウントされます
そのため、ターゲットが選択される前に拒否されたリクエスト数を計算で利用したい場合は、HTTPCode_ELB_4XX_Count と HTTPCode_ELB_5XX_Count のメトリクスが利用できそうに見えます。しかし、1 点例外があり、ALB が HTTP 460 を返す場合はターゲットを選択したあとにクライアントから切断されるため RequestCount と HTTPCode_ELB_4XX_Count の両方にカウントされます。
HTTPCode_ELB_4XX_Count リクエストの形式が不正な場合、または不完全な場合は、クライアントエラーが生成されます。ロードバランサーが HTTP 460 エラーコードを返す場合を除き、これらのリクエストはターゲットで受信されませんでした。この数には、ターゲットによって生成される応答コードは含まれません。
そのため、下記のように HTTPCode_ELB_4XX_Countと RequestCount を加算するとHTTP 460 エラーコードを返すリクエストが重複してカウントされてしまい、正確なSLIが算出できません。そもそも、HTTPCode_ELB_4XX_Count にはHTTP 460 エラーコード以外のリクエストも含まれているので、今回想定しているSLIの計算式としては正確ではありません。
メトリクス投稿に失敗したリクエスト数
= HTTPCode_ELB_4XX_Count (HTTP 460 エラーコード以外のリクエストも含む) + HTTPCode_ELB_5XX_Count + HTTPCode_Target_5XX_Count
全てのリクエスト数
= RequestCount (HTTP 460 エラーコードを返すリクエストを含む) + HTTPCode_ELB_4XX_Count (HTTP 460 エラーコードを返すリクエストを含む) + HTTPCode_ELB_5XX_Count
こういった状況を踏まえて、ALBのログから HTTP 460 エラーコードを返すリクエスト数をカウントし、Mackerel にメトリクスとして投稿して SLI の算出に利用しています。
メトリクス投稿に失敗したリクエスト数
= (HTTP 460 エラーコードを返すリクエスト数) + HTTPCode_ELB_5XX_Count + HTTPCode_Target_5XX_Count
全てのリクエスト数
= RequestCount + HTTPCode_ELB_4XX_Count + HTTPCode_ELB_5XX_Count - (HTTP 460 エラーコードを返すリクエスト数)
まとめ
Mackerel の SLO 運用のための AWS 活用術について紹介しました。CUJ をベースにユーザー操作・でユーザーにとって重要なことを洗い出し、非同期処理を考慮して SLI を設計・計測しています。
また、Performance Working Group (PWG) での確認と SLO document に紐づく Revisit date (SLI/SLO を見直す予定日) の設定によって、定期的に確認・見直しを行っています。SLI の推移やエラーバジェットの消費状況を確認した上で必要に応じて原因調査を行い、ビジネスインパクトを考慮して優先度を決めて対応します。
この記事が、SLO の策定と運用を考えている皆様の参考になれば幸いです。
筆者プロフィール
西川 拓志 (@taxin_tt)
株式会社はてな
テクノロジーソリューション本部 Mackerel開発チーム SRE
2023 年 3 月に株式会社はてなに入社。現在は、Mackerel における SLO 運用の改善や OpenTelemetry の導入によるオブザーバビリティの改善に注力しています。
