計測プラットフォーム部バックエンドチームの鈴木です。
この記事では、Akka gRPCを利用しているScalaアプリケーションのZOZOMATに対してKamonを通じてAPMを導入した際に得られた知見、うまくいかなかった内容やその対応策を紹介します。
Akkaとは
最初にAkkaについて簡単に紹介します。Akkaは、JVM上で並行および分散アプリケーションの構築を容易にするツールキットとランタイムです。
Actorモデルの実装であるAkka Actorsを中心とし、Akka StreamsやAkka HTTP、Akka Clusterなど様々なツールが提供されています。詳しくは公式ドキュメントやAkka実践バイブルを読むことで深く理解できます。書籍で紹介されているAkkaのAPIは、今では古いものとなっていますが、Akkaの楽しさを知るにはとても良い本です。
私たちが開発しているZOZOMATではAkka Actors、Akka HTTP、Akka Cluster、Akka gRPCなどを利用しており、Akkaのツールセットの恩恵を非常に受けています。
APMとは
アプリケーションを構築していくために、APMの導入も必要になってきます。
APMはApplication Performance Management、つまりアプリケーション性能管理の略称です。アプリケーション性能管理のツールとして私たちはDatadogを利用することにしました。
APMの機能を利用するためのJavaエージェントがDatadogから提供されています。しかし、私たちのアプリケーションは以前からKamonを利用していたので、DatadogのJavaエージェントは利用せずにKamonからDatadogに連携してAPMを利用することにしました。
Kamonを利用した上でも、分散トレーシングとしてDatadogは利用可能です。なお、私たちのアプリケーションでは1つのアプリケーション内でのトレーシングに留まっているので、この記事では単に「トレーシング」と呼称します。
Kamonとは
上述のKamonについて紹介します。
KamonはJVM上で動くアプリケーションのモニタリングツールであり、JVMのメトリクス取得や本記事でも紹介するトレーシングを行うことができます。
Kamonは収集ツールのレポート先としてDatadogやNew Relic、Kamonが提供しているKamon APMなどを選択可能です。
私たちはトレーシングより以前にメトリクス取得のためにKamonを導入していて、Datadogをレポート先として利用していました。
本記事で利用する「Span」とはトレーシングの文脈において1つのアプリケーション操作を表します。「DBへ問合せる」「HTTPリクエストを送る」「計算する」など考えられます。
基本的にはSpanの作成は自由にでき、粒度はアプリケーション実装者が自由に決められます。
KamonにおけるSpanの詳しい説明は以下のページに丁寧な記述があります。 kamon.io
公式ドキュメントを参考に導入してみる
Akka HTTPをJSONサーバとして利用、またはPlay Frameworkを利用している場合、公式ドキュメントに沿って進めることで簡単にアプリケーションに導入できます。
KamonがInstrumentationを提供しているので、それに従って進めれば良いのです。Instrumentationに関しての補足は後述します。
これに従えば、ソースコードの修正はほぼ必要ありません。簡単ですね。
しかし、動作確認をする際に1つポイントがありました。
Kamonのデフォルトのサンプリングロジックでは、アプリケーション起動直後のアクセスは、そのアクセス量に関係なくサンプリングされません。
KamonのコントリビュータによるIssueでも言及されていますが、開発時などに動作確認したいときには、application.conf
に下記のような記述を追加してサンプリングのロジックを変更することをお勧めします。常にサンプリングが行われるロジックが選択されるので、動作確認時の混乱が減ります。
kamon.trace.sampler=always
動かしてみたがSpanがDatadogに送信されない
チュートリアルを参考にしながら、私たちが開発しているZOZOMATのアプリケーションへ導入してみましたが、Spanはいつまで経っても反映されませんでした。
リクエストに紐づくSpanが作成されていることはログを出力することで確認できましたが、そのSpanがDatadogに送信されることはありませんでした。
調査を進めると、私たちのアプリケーションがAkka gRPCを利用していることが原因でした。
では、なぜSpanがDatadogに送信されなかった原因を解説する前に「なぜソースコードの変更なしでトレーシングが実現できるのか」を説明していきます。
なぜソースコードの変更なしでトレーシングが実現できるのか
ずばりKamonから「Instrumentation」が提供されているからです。
この「Instrumentation」はByte Buddyを利用して実装されているモジュールです。Byte BuddyはJavaのバイトコードを操作して既存のクラスを変更できるライブラリです。
KamonのInstrumentationはByte Buddyを利用してAkkaやJDBCの実装を拡張しています。そのため、Akka Actorsがメッセージを送信するときや、JDBCがSQLを実行するときにKamonのAPIを呼び出すように拡張されています。
実際にByte Buddyを利用してJDBCが拡張されている処理は以下で確認できます。 github.com
拡張されたコードの処理は、巡り巡って StatementMonitor のようなKamon自身を呼び出す処理に到達します。
なぜZOZOMATではチュートリアルにそって導入できなかったのか
Akka HTTPのInstrumentationが提供されているのに、なぜ私たちのアプリケーションはチュートリアルに沿って導入できなかったのか。
それはAkka HTTP用のInstrumentationがDirectiveを利用した際にKamonのAPIが呼びだされるようになっていたからでした。
Akka HTTPのDirectiveは簡単に言うとHTTPルーティングを記述するためのクラスです。Akka gRPCはDirectiveを呼び出す代わりに、Akka HTTPを拡張してgRPCを受け付けるライブラリなため、KamonのSpanを送信するために必要なAPIが呼ばれていなかったのです。1
また、私たちのアプリケーションではSpanは送信されないものの、リクエストを受信した時にSpanの作成はされていました。これは、Akka HttpのInstrumentationではAkka gRPCを利用した場合でもAkka Httpの内部で利用されるクラスに拡張がされていたからでした。
Akka gRPCでもKamonのSpanが送信されるようにする
Akka gRPCでもKamonのSpanが送信されるようにする方法を見つけました。
APIからSpanを送信するために必要なtakeSamplingDecision
メソッドを呼ぶようにする。
この対応をすることで、SpanはDatadogに連携されるようになりました。これで解決。
かと思ったら、ここに来て新しい問題に気付きました。
Akka gRPCからのリクエストを処理する部分で例外が発生した場合にSpanが送信されていませんでした。ここまでご覧頂いている方なら察しがつくかと思います。
原因は、ここでも同様にtakeSamplingDecision
を呼ぶ必要がある、という点でした。
Spanの範囲内で処理に失敗したことを宣言するfail
メソッドは、Akka HTTPのInstrumentationではDirectiveモジュール内で呼び出されていました。そのため、Akka gRPCで利用されるモジュールではtakeSamplingDecision
が呼び出されませんでした。
Akka gRPCで生成されるクラスには、エラーハンドリングにおいて共通の処理を記述できる場所が存在します。
この共通処理にfail
メソッドを呼ぶ実装を加えて一件落着しました。
以下が、ここまでに言及してきた修正を加えた簡単なソースコードの例です。XXXServicePowerApiHandler
とXXXServiceImpl
はAkka gRPCがprotoファイルから生成されるクラスと、そのクラス内の処理を実装するクラスを仮定したものです。
object Main extends App { implicit val actorSystem: ActorSystem = ActorSystem() val handler: HttpRequest => Future[HttpResponse] = { request => Kamon.currentSpan().takeSamplingDecision() XXXServicePowerApiHandler( XXXServiceImpl, { actorSystem => throwable => Kamon.currentSpan().fail(throwable) GrpcExceptionHandler.defaultMapper(actorSystem)(throwable) } )(actorSystem)(request) } Http().bindAndHandleAsync( handler, interface = "0.0.0.0" ) }
APMの導入を経て得られたもの
上述の対応により、トレースが正しく動作するようになり、APMの導入を無事に完了できました。レイテンシ増加の疑いのあったソースコードの処理時間を計測できたり、突然発生したエラーの原因調査のための材料が増えました。
導入ができただけでなく、試行錯誤をする過程でKamonについての理解が深められ、Kamonのソースコードを読むだけでなく、バグを発見して簡単なPull Requestを送ることもできました。
なお、Pull Requestの作成は、過去のテックブログの記事にある OSSへの貢献 - Issueから始めるチーム活動 の一環として実施できました。 techblog.zozo.com
今回、Akka gRPCとKamonのインテグレーションの実現のためにアプリケーション側にコードを追加しました。しかし、他のライブラリのようにInstrumentationで実装できるとカッコいいですよね。今後取り組んでいこうと思います。
最後に
計測プラットフォーム部バックエンドチームでは、ZOZOMATをはじめとする計測技術でよりオンラインでの購入体験を向上させたいバックエンドエンジニアを募集しています。ご興味のある方は、以下のリンクからぜひご応募ください!
-
Spanには種類がいくつかあり、Akka HTTPがInstrumentationによって作成するSpanは
takeSamplingDecision
を呼び出す必要があるSpanでした。↩