CloudWatch Syntheticsを用いたgRPC APIの外形監視

冨田

CloudWatch Syntheticsを用いたgRPC APIの外形監視の目次

  • Service Level Objective (SLO)と監視
  • ログ監視と外形監視
  • gRPC APIをCanaryで監視する
  • 導入の結果

こんにちは。CTOの冨田です。 すこし期間が空いてしまいました。最近は新機能開発の比重は減り、かわりに運用の改善が大きなトピックになっています。kippはBanking as a Serviceという名称でSaaS提供をしています。金融サービスという性質も相まって、可用性は重大なトピックです。

Service Level Objective (SLO)と監視

GCPのドキュメントでも述べられているように、SaaSの健全な発達にはSLOの策定が肝要です。SLOはService Level Indicator (SLI)と、その目標値で設定されます。

SLIはサービス全体のなかからサービスの継続にクリティカルな要素を洗い出し、その要素について測定可能な項目をピックアップする形で決定しました。例えば「正常に」といった定性的な要求は測定できないので、「内部エラーでない応答の数とリクエスト数の比率」といった定量的な指標に置き換えます。たくさんの値がSLIの候補になりますが、たくさんの値を精緻に追跡するのは負担が大きいので重要な値を絞り込みます。実際にkippで採用しているものの中にはAPIの処理成功率、日次のバッチ処理の終了時刻などがあります。

それぞれのSLIに対し閾値を定め、SLOを策定します。 閾値はサービスの性質上達成されなければならない目標と、一般的に要求される水準、そしてインフラ技術が提供する水準を考慮しました。 サービスの性質を考慮すべき代表的な例はバッチ処理です。 たとえば9時から始まるバッチ処理があり、次の社外の処理が12時に予定されていれば、3時間で必ず終了しなければなりません。 この場合、修正・復旧・リトライの時間を考慮し10:10までに終了することをSLOとします。 一般的な水準としては経済産業省のSaaS向けSLAガイドラインを参照しました。 顧客企業に開示や説明を求められたとき、客観的な妥当性のあるソースを示すことを重視しました。 最後に上限としてインフラ技術のSLAを確認しました。 例えばAWS FargateのSLAはmonthly uptime 99.99%ですので、これより厳しい目標を保証するのは困難です。 各SLIに影響する要素技術を確認し、当社の閾値がそれらの最低値に収まるよう考慮しました。

ログ監視と外形監視

SLOに沿って監視システムを構築しました。 社内ではSRE業務のノウハウが不足しており、作業に使える時間も不十分だったため、X-Tech 5さんにご協力いただきました。 過敏すぎるPagerDutyが設定されていただけの状態からDatadogの導入を経て信頼性が高く誤報の少ない体制をわずか数週間で構築していただきました。 アラート頻度が減ると同時に問題があったときの調査スピードも改善できました。

X-Tech 5のメンバーの方との相談で、できるところから開始し、改善していこうという方針を確認しました。 最初、監視対象はログとCloudWatchなどのモニタリングを中心にしました。 これらは予め準備されていたので低コストでスタートでき、またデータ量が豊富なため除外設定など細かい調整が容易なためです。

それからALBの障害など、サーバアプリケーションが原因となる障害ではないが顧客企業に報告するため把握する必要のあるものを追加していきました。 このような障害ではリクエストがサーバに到達しないため、ログを分析しても現象を発見できません。 そこで外形監視を導入することになりました。

外形監視とはシンプルにはネットワーク外からサービスにアクセスするだけのことですから、別系統のサーバでCron jobを回す程度のことでも達成できます。 しかし運用の観点では結果の確認や監視システムとの統合の利便性のほうが重視されます。 この時もX-Tech 5さんにアドバイスを仰ぎ、CloudWatch Syntheticsを紹介していただきました。 Canary Scriptとして設定したJavaScriptを定期的に実行し結果を収集できます。

gRPC APIをCanaryで監視する

CanaryはPuppeteer環境を持ち、画面キャプチャを撮れるなどウェブサービスの監視用の機能が充実したサービスです。 APIの監視にも利用できます。 しかし私達の監視したいAPIはgRPCであり、通常のhttps APIのテンプレートは利用できませんでした。 gRPCはhttp/2.0を要求するため、Node.jsでhttp/2.0とgRPCプロトコルに向けた監視スクリプトを記述しました。

まずGRPC Health Checking Protocolで定められたgrpc.health.v1.Health/Checkに向けてリクエストを送ることにしました。 このgRPC API MethodはRDBMSなどバックエンドの接続性も考慮してAPIが正常に利用可能か応答するように実装してありました。 簡単のため、リクエストパラメタのserviceは無視し、ステータスコードが200のとき正常と定めました。

Canary Scriptではhttps/2.0でgRPCの空メッセージをgrpc.health.v1.Health/Checkに送信し、レスポンスの:statusを確認すれば十分です。 gRPCのライブラリを導入する手間が省けます。

実際にCanary Scriptで上述の処理を記述するとこのようになります。 少し変わっているのは\0\0\0\0\0の部分です。これはgRPCプロトコルに沿って空メッセージをエンコードしたバイト列です。 少しhackyですが、簡単なものならばプロトコルに沿ったエンコード・デコード処理を静的バイト列や標準ライブラリによって実装することで1ファイルに収められます。

var synthetics = require("Synthetics");
const log = require("SyntheticsLogger");
const http2 = require("http2");
const syntheticsConfiguration = synthetics.getConfiguration();

function sendGrpcHealthCheckRequest(server) {
  return new Promise((resolve, reject) => {
    const client = http2.connect(server);
    client.on("error", (err) => {
      synthetics.addExecutionError("http2 error", err);
      reject(err);
    });

    // No compress (0u8) + Length 0 (0u32)
    const buffer = new Buffer("\0\0\0\0\0");

    const req = client.request({
      ":scheme": "https",
      ":path": "/grpc.health.v1.Health/Check",
      ":method": "POST",
      "content-type": "application/grpc+proto",
      "content-length": buffer.length,
    });

    let resHeader = null;
    req.on("response", (headers, flags) => {
      resHeader = [headers, flags];
      log.info(`Res flags:${flags}`);
      for (const name in headers) {
        log.info(`${name}: ${headers[name]}`);
      }
      if (headers[":status"] !== 200) {
        synthetics.addExecutionError("Response status is not 200");
        reject();
      }
    });

    let res = "";
    req.on("data", (chunk) => {
      res += chunk;
    });
    req.on("end", () => {
      log.info(res);
      resHeader.push(res);
      resolve(resHeader);
    });

    req.setEncoding("utf8");
    req.write(buffer);
    req.end();
  });
}

const apiCanaryBlueprint = async function () {
  const server = "https://example.com:443"; // server url

  syntheticsConfiguration.setConfig({
    restrictedHeaders: [],
    restrictedUrlParameters: [],
  });

  await synthetics.executeStep(`Verify ${server}`, async () => {
    return await sendGrpcHealthCheckRequest(server);
  });
};

exports.handler = async () => {
  return await apiCanaryBlueprint();
};

導入の結果

FargateやALBのSLAを考えれば半ば当たり前ですが、外形監視で問題が発見されることはほぼありません。 現在のところ監視スクリプトの設定ミス以外で問題が報告された事例はないです。 しかしアプリケーションログ、ALBのメトリクス、Canaryと複数段階で監視を持つことで障害時の切り分けが半自動的に出来るようになり、お客様に影響しうる事態を最も早く把握できるという自信が付きました。 「備えあれば憂いなし」のとおり、ひとつひとつの準備がいざという時の問題解決を簡単にします。 今後も監視を拡充し、高品質なBaaSを提供しながら枕を高くして眠れる体制を維持していきます。 kippではSRE分野に強味を持つ方も募集しています。 最強の体制の維持に興味をお持ちの方はpeople@kipp-corp.comまでお気軽にご連絡ください。