Perl Hackers Hub

第56回 AWS X-Rayによる分散トレーシング―マイクロサービスのボトルネック,障害箇所の特定(2)

この記事を読むのに必要な時間:およそ 3 分

前回の(1)こちらから。

X-Ray─⁠─AWSによるマネージドサービス

分散トレーシングを行うためのソフトウェアとして,OSSではZipkinJaegerなどが有名です。マネージドサービスではAWSAmazon Web Services⁠ X-RayStackDriver TraceNew RelicDistributed TracingDatadog APMなどがあります。

X-RayはAWSが提供しているサービスです。マネージドサービスのため,データを保存するサーバなどの用意は一切必要なく,アプリケーションからトレースデータを送信するだけで使用できます。料金体系も完全従量制で,初期費用なしで試すことができるため,分散トレーシングを手軽に体験できます。

X-Rayの構成要素

X-Rayは次の4つの要素で構成されています。なお,X-Rayでは「概念」の項で説明した「スパン」の概念は「セグメント」と呼称することに注意してください。

X-Ray API─⁠─管理のためのAPI

データの送信,取得などを行うためのAWS側にあるAPIです。SDKからのデータは直接APIに対して送信されるのではなく,次に説明するエージェントソフトウェア,X-Ray daemonを経由します。

X-Ray daemon─⁠─トレース転送用デーモン

アプリケーションからネットワーク経由でセグメントデータを受信するエージェントです。通常はアプリケーションと同一ホストで稼働し,受信した複数のセグメントデータをまとめてX-Ray APIへ送信する役割を担います。

X-Ray SDK─⁠─アプリケーションに組み込むライブラリ

アプリケーションに組込み,実際にトレースを取得するライブラリです。2019年5月時点ではJava,JavaScript,.Net,Ruby,Go,PHP,PythonのSDKが公式で提供されています。

SDKはアプリケーション内部の処理を捕捉し,X-Ray daemonに対してセグメントデータを送信します。

X-Ray console─⁠─トレースを可視化する

X-Ray APIに送信されたトレースをブラウザ上で可視化して表示するコンソール画面です。どのコンポーネントで障害や遅延が発生しているのかの概観,個々のトレースの表示,検索などが行えます。

X-Rayへのトレース送信方法

X-Rayのアーキテクチャでは,先述のとおりSDKからdaemonへまずデータを送信し,daemonがAPIへバッチ送信します。SDKからdaemonへの通信プロトコルはUDPUser Datagram Protocolで,JSONを改行コード(LF)で連結した値を1パケットに詰めて送信します。

{"format": "json", "version": 1}
{
  "trace_id": "1-5759e988-bd862e3fe1be46a994272793",
  "id": "defdfd9912dc5a56",
  "start_time": 1461096053.37518,
  "end_time": 1461096053.4042,
  "name": "MyApp"
}

最初の改行まではプロトコルのバージョンを指定するヘッダです。次の行では1つのセグメントデータがJSONで表現されています。trace_idはトレース全体に付与されるユニークなID,idはセグメントを特定するID,start_time,end_timeはそれぞれ開始,終了時刻のUNIX time,nameはセグメントの名前です。

呼び出しもとを持つ子セグメントの場合は,親のIDを示すためのparent_idが含まれます。

X-Rayアーキテクチャの利点

アプリケーションからAPIへ直接通信するのではなくX-Ray daemonをいったん経由するアーキテクチャなのは,次の理由があるためです。

  • APIはAWS側にあるため,ネットワーク的に遠くレイテンシが大きい可能性がある
  • APIのプロトコルはHTTPSなので,接続のオーバーヘッドが比較的大きい
  • セグメントデータはアプリケーションの1つの処理に対して多数発行されるため,HTTPSよりも軽量なプロトコルが望ましい

X-Ray SDKからX-Ray daemonへは,JSONを2つ連結した値を1パケットで送信するだけで,HTTPSでの通信に比べて余計なヘッダなどはないため軽量です。UDPは送信後にレスポンスを待つ必要がないプロトコルです。そのため仮にX-Ray daemonが停止していたり,受信に問題が発生していたりしたとしても,アプリケーションに大きく影響する遅延は発生しません。

障害を検知するトレースのしくみに問題が発生したために,アプリケーションに障害を引き起こすのでは本末転倒ですから,極力アプリケーションに影響を与えないアーキテクチャになっているのです。

PerlアプリケーションをX-Rayでトレースする

「X-Ray SDK」の箇所で述べたとおり,Perl用のSDKは本稿執筆時点でAWSから提供されていません。そこで筆者はX-Ray SDK相当のモジュールを独自に開発して,CPANで公開しています。

AWS::XRay─⁠─Perl用トレーシングモジュール

AWS::XRayはコード内の処理をラップして,X-Ray daemonにセグメントデータを送信するモジュールです。使用方法は単純で,セグメントとして計測したい処理をcapture関数で包むだけです。

use AWS::XRay qw/ capture /;
capture "myApp", sub {
    # セグメントmyAppの処理
    capture "internal", sub {
        # myAppを親に持つセグメントinternalの処理
    };
};

これでcaptureの第1引数をセグメント名として,第2引数の関数リファレンスの開始時刻,終了時刻を計測した結果が送信されます。このコードによる呼び出し関係は,図1のようになります。

図1 myAppとinternalの呼び出し関係

図1 myAppとinternalの呼び出し関係

captureを入れ子にした場合は自動で親子関係を把握し,適切なtrace_id,parent_idが追加されます。この親子関係の把握はパッケージや関数が別になっていても有効なため,利用者は明示的に引数などを渡す必要はなく,単に計測したい箇所をcaptureで包んでいけばよいのです。

captureで包まれた関数には,そのセグメントの情報を保持しているAWS::XRay::Segmentオブジェクトが渡されます。外部にトレースIDを付与したHTTPリクエストを行う場合には,trace_headerメソッドで得られる値をリクエストヘッダX-Amzn-Trace-Idに設定します。FurlモジュールでHTTPリクエストを行う例を次に示します。

use AWS::XRay qw/ capture /;
use Furl;
capture "external", sub {
    my $seg = shift; # AWS::XRay::Segmentオブジェクト
    my $furl = Furl->new;
    # トレースID,セグメントIDを付与してHTTPリクエストする
    my $res = $furl->get(
        "https://example.com/",
        [ "X-Amzn-Trace-Id" => $seg->trace_header ],
    );
    # ステータスコードをメタデータとして記録
    $seg->{metadata}->{external} = {
        status => $res->status,
    };
};

セグメントオブジェクトのハッシュキーmetadata,annotationに値を設定すると,メタデータと注釈annotationを追加できます。メタデータと注釈はセグメントに対して利用者が任意に付与できる情報です。注釈には自動的にインデックスが設定され,X-Rayコンソールで検索が可能になります。メタデータはコンソールで閲覧はできますが,検索はできません。

Plack::Middleware::XRay─⁠─HTTPリクエストをトレース

Plack::Middleware::XRayはPlackのミドルウェアとして使用し,HTTPアプリケーションサーバのリクエスト情報を含んだトレースを取得するモジュールです。これも使用方法は単純で,特にオプションを指定しない場合は,名前を指定してenableするだけです。

use Plack::Builder;
builder {
    enable "XRay",
        name => "myPlackApp";
    $app;
};

これだけで,リクエストとレスポンスの情報(URL,メソッド,ステータスコードなど)が含まれたセグメントが生成され,AWS::XRayによってdaemonに送信されます。そのリクエスト処理内で発行されたAWS::XRayのcaptureは,リクエストを起点にしたセグメントの子孫になります。

リクエストヘッダにX-Amzn-Trace-Idヘッダが存在する場合にはそれを読み取り,トレースIDとして引き継いで使用します。ヘッダが存在しない場合には,新規のトレースIDが生成されます。

オプションを設定することで,サンプリングルールの設定やリクエストパラメータをもとにしたメタデータ,注釈の生成ができます。詳細はドキュメントを参照してください。

Devel::KYTProf::Logger::XRay─⁠─任意の関数呼び出しをトレース

AWS::XRayを単体で利用する場合は,captureを計測したい箇所に差し込んでいく必要があります。

実際の既存アプリケーションへ組込む場合,外部へ通信を行う箇所,たとえばデータベースにクエリを行う箇所は大量に存在するため,すべてにコードを追加するのはたいへんな手間になります。また,対象が自らの管理しているコードだけならばともかく,アプリケーションが利用しているORM(O/R Mapper)などのライブラリ内部へ計測コードを追加していくのは,現実的ではありません。

そこで,実績のあるプロファイラDevel::KYTProfと連携して計測を行うことを考えました。

Devel::KYTProfはデータベースへのクエリ(DBI⁠⁠,HTTPリクエスト(LWP::UserAgent,Furl⁠⁠,memcachedへのアクセス(Cache::Memcached::Fast)など,外部モジュールが行った処理に対してプロファイリングを行います。各処理の発行時刻,経過時間,発行したクエリなどをアプリケーションのコードを変更せずに計測できる,たいへん便利なプロファイラです。デフォルトではプロファイル結果は標準出力にログとして出力されますが,ロガーを任意のモジュールに差し替え可能です。

Devel::KYTProf::Logger::XRayをDevel::KYTProfのロガーとして設定すると,先述の各モジュールを利用している呼び出しがすべてX-Rayのセグメントとして記録されます。

use Devel::KYTProf;
use Devel::KYTProf::Logger::XRay;
Devel::KYTProf->logger("Devel::KYTProf::Logger::XRay");

Devel::KYTProf::Profiler以下の名前空間には,独自の計測コードを追加するモジュールを作成できます。次に示すのは,筆者がメンテナンスしているFluent::Logger注1用のプロファイラ,Devel::KYTProf::Profiler::Fluent::Loggerを適用するコードです。

Devel::KYTProf->apply_prof('Fluent::Logger');

モジュールを作成せず,任意のパッケージの関数を計測対象に追加することもできます。次のコードは,MyAppパッケージのfoo関数を計測する例です。

Devel::KYTProf->add_prof('MyApp', 'foo');

Devel::KYTProfと連携することで,AWS::XRayでトレースを取得するためのコード修正が最小限になり,PerlでのX-Rayの利用が実用的になりました。

注1)
Fluentdへのログ送信モジュールです。

<続きの(3)は8月21日(水)公開です。>

WEB+DB PRESS

本誌最新号をチェック!
WEB+DB PRESS Vol.113

2019年10月24日発売
B5判/160ページ
定価(本体1,480円+税)
ISBN978-4-297-10905-9

  • 特集1
    接続エラー,性能低下,権限エラー,クラウド障害
    AWSトラブル解決
    原因調査・対応・予防のノウハウ
  • 特集2
    Ruby書き方ドリル
    要点解説と例題で身に付く!
  • 特集3
    体験
    ドメイン駆動設計
    モデリングから実装までを一気に制覇
  • 一般記事
    FigmaによるUIデザイン
    デザイナーとエンジニアがオンラインで協業できる!
  • 一般記事
    入門
    SwooleによるPHP非同期処理
    高速化のための並列実行はどのように書くのか

著者プロフィール

藤原俊一郎(ふじわらしゅんいちろう)

2011年より面白法人カヤック。技術部SREチームリーダー。ISUCON優勝3回,出題2回。最近の趣味はマネージドサービスの隙間を埋める隙間家具のようなツールをGoで作ってOSSにすること。著書に『みんなのGo言語[現場で使える実践テクニック]』(共著,技術評論社)。

URL:https://sfujiwara.hatenablog.com/