OpenCensus 指標の使用

このページでは、可用性 SLI とレイテンシ SLI の OpenCensus 指標を作成する基本的事項について説明します。また、OpenCensus 指標を使用して SLO を定義する方法の実装例も紹介します。

OpenCensus の基本

OpenCensus は、OpenCensus の GitHub ページから入手できる、ライブラリの単一のオープンソース ディストリ ビューションです。トレースと指標を自動的に収集して任意のバックエンドに送信します。OpenCensus を使用すると、サービスをインストルメント化して、Cloud Monitoring に取り込むことができるカスタム指標を出力できます。また、これらの指標は、SLI として使用できます。

OpenCensus を使用して、SLI としては特に意図しない Monitoring 指標を作成する例については、OpenCensus を使用したカスタム指標をご覧ください。

指標

OpenCensus を使用してサービスから指標データを収集するには、次の OpenCensus 構成要素を使用する必要があります。

  • Measure。記録する指標のタイプを表します。指標名で指定します。Measure は Int64 または Float64 の値を記録できます。
  • Measurement。特定のイベントについて、Measure によって収集され書き込まれた特定のデータポイントを記録します。たとえば Measurement では、特定のレスポンスのレイテンシが記録される場合があります。
  • ViewMeasure に適用する集約を指定します。OpenCensus では、次の集約タイプがサポートされています。
    • Count: 測定点の数。
    • Distribution: 測定点のヒストグラム分布。
    • Sum: 測定値の合計。
    • LastValue: 測定によって記録された最後の値。

詳細については、OpenCensus の統計情報 / 指標をご覧ください。なお OpenCensus では多くの場合、指標を「統計情報」と呼びます。

インストゥルメンテーション

OpenCensus ライブラリは複数の言語で利用できます。指標を出力するためのサービスのインストルメント化に関する言語固有の情報については、OpenCensus の言語サポートをご覧ください。また、OpenCensus によるカスタム指標では、Monitoring でよく使用される言語の例を示しています。

基本的なケースでは、次の操作を行う必要があります。

  • 指標を記録してエクスポートするためにサービスをインストルメント化する。
  • エクスポータを定義して、指標を受け取る。

指標ごとに Measure を定義し、値の型として Int64 または Float64 を指定する必要があります。また、集約型(Count、Distribution、Sum、LastValue)を指定するには、View を定義して登録する必要があります。Distribution 集約型を使用するには、ヒストグラム バケット境界を明示的に指定する必要もあります。また、指標の名前は View にも指定します。

エクスポータ

最後に、エクスポータを使用して指標を収集し、Cloud Monitoring か別のバックエンドに書き込む必要があります。Monitoring で利用できる言語固有のエクスポータについては、OpenCensus のエクスポータをご覧ください。

また、独自のエクスポータを作成することもできます。詳細については、カスタム エクスポータの作成をご覧ください。

SLI の指標の作成

アプリケーションでは、Cloud Monitoring で SLI として使用できる OpenCensus 指標を作成する必要があります。

  • リクエスト数とエラー数に関する可用性 SLI の場合、Count 集約で Measure を使用します。
  • レイテンシ SLI の場合、Distribution 集約で Measure を使用します。

可用性 SLI の指標

Cloud Monitoring API でリクエスト ベースの可用性 SLI を表現するには、TimeSeriesRatio 構造体を使用し、リクエスト全体に対する「良い」リクエストと「悪い」リクエストの比率を設定します。この比率は、RequestBasedSli 構造体の goodTotalRatio フィールドで使用されます。

アプリケーションでは、この比率の構成に使用できる OpenCensus 指標を作成する必要があります。アプリケーションでは、次の中から少なくとも 2 つを作成する必要があります。

  1. 合計イベントをカウントする指標。この指標は、比率の totalServiceFilter で使用します。

    Count 集約で Int64 型の OpenCensus 指標を作成し、受信したすべてのリクエストに対して 1 の値を記録します。

  2. 「悪い」イベントをカウントする指標。この指標は、比率の badServiceFilter で使用します。

    Count 集約で Int64 型の OpenCensus 指標を作成し、エラーまたは失敗したすべてのリクエストに対して 1 の値を記録します。

  3. 「良い」イベントをカウントする指標。この指標は、比率の goodServiceFilter で使用します。

    Count 集約で Int64 型の OpenCensus 指標を作成し、成功したすべてのレスポンスに対して 1 の値を記録します。

レイテンシ SLI の指標

リクエスト ベースのレイテンシ SLI は、Cloud Monitoring API で DistributionCut 構造体を使用して表現します。この構造体は、RequestBasedSli 構造体の distributionCut フィールドで使用されます。

Distribution 集約型を使用して、View を持つ Int64 または Float64 の Measure を作成できます。また、バケット境界を明示的に定義する必要もあります。なお、望ましいしきい値の範囲内にあるリクエストの割合を正確に測定できる方法で、バケットを定義することが重要です。このトピックについては、サイト信頼性エンジニアリング ワークブックSLO の実装をご覧ください。

実装例

このセクションでは、Node.js で OpenCensus を使用して、基本的な可用性 SLI とレイテンシ SLI の指標を実装する例を示します。

インストゥルメンテーション

OpenCensus を使用して指標を出力するようにサービスをインストルメント化する手順は次のとおりです。

  1. 必要なライブラリを含めます。

    Go

    import (
    	"flag"
    	"fmt"
    	"log"
    	"math/rand"
    	"net/http"
    	"time"
    
    	"contrib.go.opencensus.io/exporter/stackdriver"
    	"go.opencensus.io/stats"
    	"go.opencensus.io/stats/view"
    	"go.opencensus.io/tag"
    )
    

    Node.js

    // opencensus setup
    const {globalStats, MeasureUnit, AggregationType} = require('@opencensus/core');
    const {StackdriverStatsExporter} = require('@opencensus/exporter-stackdriver');

    Python

    from flask import Flask
    from opencensus.ext.prometheus import stats_exporter as prometheus
    from opencensus.stats import aggregation as aggregation_module
    from opencensus.stats import measure as measure_module
    from opencensus.stats import stats as stats_module
    from opencensus.stats import view as view_module
    from opencensus.tags import tag_map as tag_map_module
    
    from prometheus_flask_exporter import PrometheusMetrics
    
  2. エクスポータを定義して登録します。

    Go

    // Sets up Cloud Monitoring exporter.
    sd, err := stackdriver.NewExporter(stackdriver.Options{
    	ProjectID:         *projectID,
    	MetricPrefix:      "opencensus-demo",
    	ReportingInterval: 60 * time.Second,
    })
    if err != nil {
    	log.Fatalf("Failed to create the Cloud Monitoring exporter: %v", err)
    }
    defer sd.Flush()
    
    sd.StartMetricsExporter()
    defer sd.StopMetricsExporter()

    Node.js

    // Stackdriver export interval is 60 seconds
    const EXPORT_INTERVAL = 60;
    const exporter = new StackdriverStatsExporter({
      projectId: projectId,
      period: EXPORT_INTERVAL * 1000,
    });
    globalStats.registerExporter(exporter);

    Python

    def setup_openCensus_and_prometheus_exporter() -> None:
        stats = stats_module.stats
        view_manager = stats.view_manager
        exporter = prometheus.new_stats_exporter(prometheus.Options(namespace="oc_python"))
        view_manager.register_exporter(exporter)
        register_all_views(view_manager)
  3. 指標ごとに Measure を定義します。

    Go

    // Sets up metrics.
    var (
    	requestCount       = stats.Int64("oc_request_count", "total request count", "requests")
    	failedRequestCount = stats.Int64("oc_failed_request_count", "count of failed requests", "requests")
    	responseLatency    = stats.Float64("oc_latency_distribution", "distribution of response latencies", "s")
    )
    

    Node.js

    const REQUEST_COUNT = globalStats.createMeasureInt64(
      'request_count',
      MeasureUnit.UNIT,
      'Number of requests to the server'
    );
    const ERROR_COUNT = globalStats.createMeasureInt64(
      'error_count',
      MeasureUnit.UNIT,
      'Number of failed requests to the server'
    );
    const RESPONSE_LATENCY = globalStats.createMeasureInt64(
      'response_latency',
      MeasureUnit.MS,
      'The server response latency in milliseconds'
    );

    Python

    m_request_count = measure_module.MeasureInt(
        "python_request_count", "total requests", "requests"
    )
    m_failed_request_count = measure_module.MeasureInt(
        "python_failed_request_count", "failed requests", "requests"
    )
    m_response_latency = measure_module.MeasureFloat(
        "python_response_latency", "response latency", "s"
    )
  4. レスポンスのレイテンシに対して、適切な集約型を使用し、Measure ごとに View を定義して登録します。

    Go

    // Sets up views.
    var (
    	requestCountView = &view.View{
    		Name:        "oc_request_count",
    		Measure:     requestCount,
    		Description: "total request count",
    		Aggregation: view.Count(),
    	}
    	failedRequestCountView = &view.View{
    		Name:        "oc_failed_request_count",
    		Measure:     failedRequestCount,
    		Description: "count of failed requests",
    		Aggregation: view.Count(),
    	}
    	responseLatencyView = &view.View{
    		Name:        "oc_response_latency",
    		Measure:     responseLatency,
    		Description: "The distribution of the latencies",
    		// Bucket definitions must be explicitly specified.
    		Aggregation: view.Distribution(0, 1000, 2000, 3000, 4000, 5000, 6000, 7000, 8000, 9000, 10000),
    	}
    )
    
    	// Register the views.
    	if err := view.Register(requestCountView, failedRequestCountView, responseLatencyView); err != nil {
    		log.Fatalf("Failed to register the views: %v", err)
    	}

    Node.js

    const request_count_metric = globalStats.createView(
      'request_count_metric',
      REQUEST_COUNT,
      AggregationType.COUNT
    );
    globalStats.registerView(request_count_metric);
    const error_count_metric = globalStats.createView(
      'error_count_metric',
      ERROR_COUNT,
      AggregationType.COUNT
    );
    globalStats.registerView(error_count_metric);
    const latency_metric = globalStats.createView(
      'response_latency_metric',
      RESPONSE_LATENCY,
      AggregationType.DISTRIBUTION,
      [],
      'Server response latency distribution',
      // Latency in buckets:
      [0, 1000, 2000, 3000, 4000, 5000, 10000]
    );
    globalStats.registerView(latency_metric);

    Python

    # set up views
    latency_view = view_module.View(
        "python_response_latency",
        "The distribution of the latencies",
        [],
        m_response_latency,
        aggregation_module.DistributionAggregation(
            [0, 1000, 2000, 3000, 4000, 5000, 6000, 7000, 8000, 9000, 10000]
        ),
    )
    
    request_count_view = view_module.View(
        "python_request_count",
        "total requests",
        [],
        m_request_count,
        aggregation_module.CountAggregation(),
    )
    
    failed_request_count_view = view_module.View(
        "python_failed_request_count",
        "failed requests",
        [],
        m_failed_request_count,
        aggregation_module.CountAggregation(),
    )
    
    # register views
    def register_all_views(view_manager: stats_module.stats.view_manager) -> None:
        view_manager.register_view(latency_view)
        view_manager.register_view(request_count_view)
        view_manager.register_view(failed_request_count_view)
  5. request-count 指標と error-count 指標の値を記録します。

    Go

    // Counts the request.
    stats.Record(ctx, requestCount.M(1))
    
    // Randomly fails 10% of the time.
    if rand.Intn(100) >= 90 {
    	// Counts the error.
    	stats.Record(ctx, failedRequestCount.M(1))

    Node.js

    // record a request count for every request
    globalStats.record([
      {
        measure: REQUEST_COUNT,
        value: 1,
      },
    ]);
    
    // randomly throw an error 10% of the time
    const randomValue = Math.floor(Math.random() * 9 + 1);
    if (randomValue === 1) {
      // Record a failed request.
      globalStats.record([
        {
          measure: ERROR_COUNT,
          value: 1,
        },
      ]);

    Python

    mmap = stats_recorder.new_measurement_map()
    # count request
    mmap.measure_int_put(m_request_count, 1)
    # fail 10% of the time
    if random.randint(0, 100) > 90:
        mmap.measure_int_put(m_failed_request_count, 1)
        tmap = tag_map_module.TagMap()
        mmap.record(tmap)
        return ("error!", 500)
  6. レイテンシの値を記録します。

    Go

    requestReceived := time.Now()
    // Records latency for failure OR success.
    defer func() {
    	stats.Record(ctx, responseLatency.M(time.Since(requestReceived).Seconds()))
    }()

    Node.js

    globalStats.record([
      {
        measure: RESPONSE_LATENCY,
        value: stopwatch.elapsedMilliseconds,
      },
    ]);

    Python

    start_time = time.perf_counter()
    mmap = stats_recorder.new_measurement_map()
    if random.randint(0, 100) > 90:
        response_latency = time.perf_counter() - start_time
        mmap.measure_float_put(m_response_latency, response_latency)
        tmap = tag_map_module.TagMap()
        mmap.record(tmap)

取り込まれた指標

指標を Cloud Monitoring にエクスポートすると、OpenCensus から発生したことを示す接頭辞付きの指標タイプとして表示されます。たとえば、Node.js の実装で、各 OpenCensus View の名前は、次のようにマッピングされます。

  • request_count_slicustom.googleapis.com/opencensus/request_count_sli になります。
  • error_count_slicustom.googleapis.com/opencensus/error_count_sli になります。
  • response_latency_slicustom.googleapis.com/opencensus/response_latency_sli になります。

サービスを実行した後、Metrics Explorer で指標を検索することで、指標が Monitoring に取り込まれていることを確認できます。

可用性 SLI

Cloud Monitoring では、TimeSeriesRatio 構造体を使用してリクエスト ベースの可用性 SLI を表現します。次の例は、取り込まれた OpenCensus 指標を使用し、サービスの可用性が 98% であると予測する SLO を示しています。この可用性は、28 日間のローリング ウィンドウでの request_count_sli に対する error_count_sli の割合で計算されます。

{
  "serviceLevelIndicator": {
    "requestBased": {
      "goodTotalRatio": {
        "totalServiceFilter":
          "metric.type=\"custom.googleapis.com/opencensus/request_count_sli\",
       "badServiceFilter":
          "metric.type=\"custom.googleapis.com/opencensus/error_count_sli\"
      }
    }
  },
  "goal": 0.98,
  "rollingPeriod": "2419200s",
  "displayName": "98% Availability, rolling 28 days"
}

レイテンシ SLI

Cloud Monitoring では、DistributionCut 構造体を使用してリクエスト ベースのレイテンシ SLI を表現します。次の例は、取り込まれた OpenCensus レイテンシ指標を使用し、1 日のローリング ウィンドウでリクエストの 98% が 1,000 ミリ秒未満で完了すると予測する SLO を示しています。

{
  "serviceLevelIndicator": {
    "requestBased": {
      "distributionCut": {
        "distributionFilter":
          "metric.type=\"custom.googleapis.com/opencensus/response_latency_sli\",
        "range": {
          "min": 0,
          "max": 1000
        }
      }
    }
  },
  "goal": 0.98,
  "rollingPeriod": "86400s",
  "displayName": "98% requests under 1000 ms"
}