メトリック収集と分散トレーシングのフレームワーク OpenCensus を使ってみた

こんにちは、滝澤です。

先日、社内ISUCONが開催され、運営側として関わっていました。 その中で、試し解きと技術検証(お遊び)も兼ねて、以前から興味があったメトリック収集と分散トレーシングのフレームワークの OpenCensus を使ってみて、処理時間の可視化をしてみました。 そのときに行った内容などを紹介してみます。

Timeline

OpenCensusとは

OpenCensusはメトリック収集および分散トレーシングを行うためのライブラリ集です。 マイクロサービスやモノリシックなアプリケーションに対して Observability (可観測性)を提供します。

Google社が社内で利用しているメトリック収集およびトレーシングのライブラリ集であるCensusのオープンソース実装として、2018年1月にリリースされました。

提供する機能としては大きく分けて次のものになります。

  • Metrics (メトリック収集) ※ Stats という表記も使われている。
  • Tracing (トレーシング)

サポートしている開発言語としては本記事の執筆時点(2018年9月)では、Java, Go, C++, Erlang, Node.js, Python, PHP, Ruby, C#があります。 開発言語毎にサポート状況は異なるため、詳しくは「FAQ」の「What languages & integrations does OpenCensus support?」を見てください。

OpenCensusはデータを収集するところまでの役割を持っていて、グラフ化したり分析したりするのは他のトレーシング/モニタリングサービスの役割となります。そのため、そのようなサービスへ収集したデータを送信するExporterというものも用意されています。 対応しているサービスとしては本記事の執筆時点(2018年9月)では、Datadog, Instana, Jaeger, Prometheus, SignalFX, Stackdriver, Zipkinがありますが、開発言語や機能毎にサポート状況は異なります。 こちらについても「FAQ」の「What Exporters does OpenCensus support?」を見てください。

なお、OpenCensusの仕様については次のサイトを見るとよいです。

Pythonウェブアプリケーションのトレース

ここでは、Pythonウェブアプリケーションでトレーシングを試した例を紹介します。

参考にしたサイトはこちらになります。

環境は以下のものになります。

  • 開発言語: Python 3.6
  • アプリケーションフレームワーク: Flask
  • 分散トレースシステム: Stackdriver Trace (Google Cloud)

なお、Stackdriver TraceはGoogle Cloudの分散トレースサービスです。

Stackdriver Trace

用語

作業を始める前に、トレーシングで用いられる用語を確認しましょう。 「Stackdriver Traceの概要」から引用してみます。

  • トレース: トレースとは、アプリケーションが受信したリクエストや、リクエストへのレスポンスを生成するために発生したさまざまなイベント(通常は RPC コールまたはインストルメントされたコードのセクション)を正確なタイミングとともに図表として示すものです。こうしたイベントはトレースではスパンとして表されます。
  • スパン: トレースのコンポーネント。通常はアプリケーションへの最初のリクエストの結果として実施された RPC コールを表します。
  • アノテーション: 特定のスパンに関連付けられたメタデータ。アノテーションの一般的な例としては、スパンが実行されたサービスのバージョン、取得された RPC コールの生成元(トレースのルートスパンの呼び出し元ウェブブラウザのユーザー エージェントなど)に関連するデータ、デベロッパーがスパンに含めるよう選択したカスタムデータなどがあります。

Stackdriver Traceの準備

まず、トレーシングサービスとしてStackdriver Traceを利用するために以下のことを行います。

  1. Google Cloud Platform(以降、GCPと略す)のプロジェクトを作成する。
  2. Stackdriver関連のAPIを有効化する。
    • Stackdriver Monitoring API
    • Stackdriver Trace API
  3. サービスアカウントを作成し、サービスアカウントキーのファイル(JSON形式)をダウンロードする。割り当てる役割(Role)としては「Cloud Trace Agent」のみを付与する。
  4. Webアプリケーション用に次の環境変数を読み込むように設定する。
    • GOOGLE_CLOUD_PROJECT : GCPのプロジェクトID
    • GOOGLE_APPLICATION_CREDENTIALS : サービスアカウントキーのファイル名
GOOGLE_CLOUD_PROJECT=tracing-123456
GOOGLE_APPLICATION_CREDENTIALS=/path/to/service-account-file.json

export GOOGLE_CLOUD_PROJECT GOOGLE_APPLICATION_CREDENTIALS

なお、Stackdriver Traceを利用するためのチュートリアルが用意されています。

インストール

次のようにして、OpenCensus Pythonをインストールします。

pip install opencensus

さらに、Stackdriver Traceを利用するために、Stackdriver Traceのクライアントライブラリをインストールします。

pip install google-cloud-trace grpc-google-cloud-monitoring-v3

以上でOpenCensus Traceを利用する準備が整いました。

使い方

使い方は次のサイトに最低限必要なことは記述されています。

まず、exporter の用意をします。今回はStackdriver Traceの exporter を用意します。

from opencensus.trace.exporters import stackdriver_exporter
from opencensus.trace.exporters.transports.background_thread import BackgroundThreadTransport

exporter = stackdriver_exporter.StackdriverExporter(
    transport=BackgroundThreadTransport
)

exporter の送信処理にはデフォルトでは SyncTransport が使われます。これは特別に何かするわけではなく、コード実行中にデータ送信が差し込まれるため、実行処理の遅延が発生します。 コード実行時のブロックを避けるために BackgroundThreadTransport というバックグラウンドスレッドでデータ送信を行う機能が用意されています。上述の例ではこれを利用します。

次に sampler を用意します。

from opencensus.trace.samplers import always_on

sampler = always_on.AlwaysOnSampler()

ここでは sampler としてデフォルトの AlwaysOnSampler を指定したので、すべてのデータが送信されます。プロダクション環境での利用でこれを行うと、大量の送信データが発生するため好ましくありません。 そのようなときは、 ProbabilitySampler を使い、送信するデータを減らします。 rate にはサンプリングの比率を指定します。

from opencensus.trace.samplers import probability

sampler = probability.ProbabilitySampler(rate=0.1)

exportersampler の用意ができたら、 tracer を作成します。

from opencensus.trace import tracer as tracer_module

tracer = tracer_module.Tracer(sampler=sampler, exporter=exporter)

以上をまとめると次のようになります。

from opencensus.trace import tracer as tracer_module
from opencensus.trace.samplers import always_on
from opencensus.trace.exporters import stackdriver_exporter
from opencensus.trace.exporters.transports.background_thread import BackgroundThreadTransport

sampler = always_on.AlwaysOnSampler()
exporter = stackdriver_exporter.StackdriverExporter(
    transport=BackgroundThreadTransport
)
tracer = tracer_module.Tracer(sampler=sampler, exporter=exporter)

これでトレーシングを行う準備が完了しました。 後は、コード中に処理時間を計測したいスパンを設定するだけです。

次のように tracer.span() をwith句で呼び出すことにより、スパンを設定できます。

@app.route('/reset', methods=['GET'])
def reset():
    with tracer.span(name='reset'):
        with tracer.span(name='delete_data'):
            delete_data()
        with tracer.span(name='load_data'):
            load_data()
        with tracer.span(name='prepare_files'):
            prepare_files()
    return ('', 204)

Stackdriver TraceのTrace listを見ると次のようなタイムラインが取得できます。

Timeline

また、設定したスパンに対して、add_annotation() によりアノテーションを追加したり、 add_attribute() で属性情報を追加したりすることができます。

    with tracer.span(name='reset') as span:
        span.add_annotation('annotation1', ann='ann1')
        span.add_attribute('attr1', 'value1')

このとき、アノテーションは次のように表示されます。

Annotation

属性情報は次のようになります。

Attribute

Flaskインテグレーション

OpenCensus Trace for Pythonでは、フレームワークやライブラリに対してインテグレーションが用意されています。

Flaskのインテグレーションも用意されており、次のようにして利用可能です。

from opencensus.trace.ext.flask.flask_middleware import FlaskMiddleware

app = Flask(__name__)

middleware = FlaskMiddleware(app=app, sampler=sampler, exporter=exporter)

このときには、次のようなタイムラインが取得できます。HTTPのGETリクエストが加わったことがわかります。

Timeline - flask

サービスインテグレーション

OpenCensus Trace for Pythonでは、MySQLやPostgreSQLなどのサービスへのアクセスに対してインテグレーションが用意されています。

Object Relational Mapperである SQLAlchemy のインテグレーションを使う場合は次のように記述します。

from opencensus.trace import config_integration

integration = ['sqlalchemy']
config_integration.trace_integrations(integration, tracer=tracer)

このときには、次のようなタイムラインが取得できます。 sqlalchemy.query が加わったことがわかります。

Timeline - SQLAlchemy

Timelineの sqlalchemy.query をクリックすると、Detailsにクエリーが表示されます。

Timeline - SQLAlchemy

Noop Tracer

トレーシングを中断したいときには、次のように TracerNoopTracer に差し替えると、他のトレーシングに関連する箇所を書き換えなくてもすみます。

from opencensus.trace.tracers import noop_tracer

# tracer = tracer_module.Tracer(sampler=sampler, exporter=exporter)
tracer = noop_tracer.NoopTracer()

計測例

計測例を一つ紹介します。

テンプレートエンジンの処理中にデータベースへのクエリーが大量に発生しているのが観測されました。

template

実はこれはテンプレート中に次のようなSQLAlchemyのO/Rマッパーを利用した記述がループ中にあり、Lazy Loading(遅延読み込み)が発生したためでした。

  

{{ edudaily.user.name }}

コードを書いた人はこのような処理を認識していますが、コードを書いていない人がボトルネックを探すのに役に立ちそうです。

雑感

分散トレーシングはマイクロサービスアーキテクチャでの利用を背景としていますが、モノリシックなウェブアプリケーションでもAPM(Application Performance Monitoring/Management)ツールとして十分に利用価値はあると思います。 複数のミドルウェアや複数のサーバでの処理時間の計測値を集約して可視化することにより、ボトルネックの分析をしやすくなるでしょう。

実際に使ってみた感じでは、スパンの記述が煩雑で、コードの見やすさを阻害しかねないと思いました。 これはライブラリやフレームワークへのインテグレーションを作成して適応することで解決できそうです。 先の例で実際にやったこととしては、処理が重たそうなメソッドに対してラッパーを作成して、ラッパー内でスパンを定義して元のメソッドを置き換えました。

また、インテグレーションで定義されたスパンが関連したスパンとして一つのトレースとして集約されないこともありました。これについては、インテグレーションではスパンの親子関係の記述ができないので難しいと思います。うまく記述する方法があるのかもしれませんがわかりませんでした。

alone

その他

OpenCensusはまだ若いプロジェクトで、開発中であるところが多々ありますが、動向を追っていきたいと思います。 なお、似たようなプロジェクトに OpenTracing というものがあります。こちらも動向を見ていきたいと思っています。

関連しそうなプロジェクトとして、先日、OpenMetrics というプロジェクトがアナウンスされましたが、ブログ記事「OpenMetrics project accepted into CNCF Sandbox」によると、OpenCensusはOpenMetricsをサポートしていくということが述べられています。 こちらもどうなるか様子を見ていきたいですね。

参考サイト

OpenCensus公式サイト

Google Open Source Blog

OpenCensus Python

Stackdriver Trace

Observability

株式会社ハートビーツのインフラエンジニアから、ちょっとした情報をお届けします。