本連載では第一線のPerlハッカーが回替わりで執筆していきます。今回のハッカーは面白法人カヤックでWebアプリケーションの運用、インフラを担当している藤原 俊一郎さんで、テーマは「AWS X-Rayによる分散トレーシング」です。
分散システムのボトルネックや障害箇所を特定することは、モノリシックなシステムを相手にするのに比べて難しいものでした。しかし開発手法としてマイクロサービスアーキテクチャが普及してきた昨今、それに対する武器として分散トレーシングと呼ばれる手法が一般化してきました。
本稿ではPerlで実装されたWebアプリケーションに分散トレーシングを組込み、複数コンポーネントの関連やパフォーマンスを可視化する例を紹介します。
分散トレーシングとは何か
分散トレーシングは比較的新しい手法のため、まずはその発祥と概念について説明します。
発祥──GoogleのDapper論文
最初に発表されたのは、2010年のGoogleのDapper論文[1]です。Dapperは、複数のプログラミング言語で構築された複雑で大規模な分散システムとして実装されたインターネットサービスに対して、動作を追跡しパフォーマンスを解析するために作られたものです。
分散システムにおいては、1つの処理を行うために複数のコンポーネントが利用されます。図1の例を見てください。userから発行されたRequest-XをフロントエンドAが受信後、B、CへRPC(Remote Procedure Call、遠隔手続き呼び出し)が発行され、さらにCからはバックエンドD、EにRPCが発行され、最終的にすべての処理が完了した結果Reply-Xが返却されています。ここでのフロントエンドとはブラウザ上で動くものではなく、サーバ側で最初に処理を受け付けるコンポーネントです。
複数のコンポーネントをまたがった処理が行われると、途中の1ヵ所にでもパフォーマンスに劣化が生じたり障害が発生したりすると、処理全体が遅延や失敗をする可能性があります。そのため、問題が起きた処理に影響しているコンポーネントがどこなのか、すばやく特定することが運用上重要になってきます。
概念──コンポーネント間の呼び出しをグラフで表現する
では、どうやって問題が起きている箇所を特定すればよいのでしょうか。フロントエンドからどの順番で、どのタイミングでバックエンドへのRPC呼び出しが行われているかを時系列で可視化したのが図2です。図で示されると、どこで時間がかかったり失敗したりした結果、全体の処理に影響しているのかが一目でわかりますね。
分散トレーシングでは、この処理を可視化したもの全体をトレース(trace)、個々のコンポーネントによる処理がいつ開始され、いつ終了したのかを記録したものをスパン(span)と呼びます。スパンはユニークなIDを持ち、呼び出しもとの親(parent)のIDを保持しています。この構造は、グラフ理論における有向非巡回グラフ(DAG=Directed Acyclic Graph)と呼ばれます。有向とはそれぞれの頂点を結ぶ辺に方向がある、非巡回とはループを作らない、という意味です。こうして構造化すると、1つの処理全体が各コンポーネントで処理されているのかの可視化が行えます。
分散システムの個々のコンポーネントで発生するスパンの情報を1つにまとめるために、すべての関連するスパンに同一のトレースIDを振る必要があります。別コンポーネントへの呼び出しには通信にトレースIDと呼び出しもとのスパンIDを付与し、呼び出された側はそのIDを親に持つスパンを発行します。
具体的なトレーシング方法
実際にアプリケーションに対してトレースを取得する方法は、大きく分けて2種類あります。
アプリケーションへのSDK組込み
一つは、アプリケーションが実装されている言語用のSDK(Software Development Kit)を組込み、アプリケーション内部でトレースを記録する手法です。
この手法では、アプリケーション内部にコードを追加してトレースするため、ネットワーク通信を伴わない内部の挙動(重い計算処理、ローカルファイルに対する操作など)に対してもスパンとしてきめ細かく記録できる利点があります。
反面、コードを組み込むため、アプリケーションの実装言語に対応したSDKが存在しない場合にはトレースが困難であったり、言語によってはSDKを組み込む際のコードの変更が大きくなったりするといった問題もあります。
サービスメッシュのデータプレーンで行う
もう一つは、Envoyなどのサービスメッシュのデータプレーンと呼ばれるプロキシソフトウェアでトレースを記録する手法です。
マイクロサービスアーキテクチャで構築された分散アプリケーションでは、コンポーネント間の通信を直接行わず、プロキシを経由する手法が採られることがあります。プロキシは障害が発生している呼び出し先の切り離しや、リトライを自動的に行います。これにより、アプリケーション内部で切り離しやリトライのために複雑な実装を行わなくても、システム全体としての堅牢さを保ちやすくなります。コンポーネント間の通信はすべてこのプロキシを通過するため、そこでトレースを記録できます。
この手法は、SDKの組込みに比べるとアプリケーションへの変更を少なくできますし、言語依存も小さくなります。システム内に複数の言語で作られたコンポーネントが混在していても、統一した手法でトレースを取得できる利点があります。反面、ネットワーク通信を記録する手法のため、通信を伴わないアプリケーション内部の挙動はトレースできません。
この両者は併用することもできます。本稿では、既存のアプリケーションのミドルウェア構成を大きく変更せずに分散トレーシングを組込むことを目的として、SDKの組込みによる手法を紹介します。
<続きの(2)はこちら。>
- 特集1
イミュータブルデータモデルで始める
実践データモデリング
業務の複雑さをシンプルに表現!
- 特集2
いまはじめるFlutter
iOS/Android両対応アプリを開発してみよう
- 特集3
作って学ぶWeb3
ブロックチェーン、スマートコントラクト、NFT