黒曜 / @kokuyouwind
Leaner Technologies Inc. 所属
Railsエンジニア
インフラ・SRE的なこともやっている
APMなど開発周辺ツールの管理
Application Performance Monitoring の略
リクエスト数や応答速度を監視・管理できる
代表的な APM サービス
etc.
大抵の APM は エージェントソフトウェアと
gem を設定するだけ
# Gemfile
gem 'ddtrace', require: 'ddtrace/auto_instrument'
# config/initializers/datadog.rb
Datadog.configure do |c|
  c.service = 'my-service'
endすごいけど、何を見ればいいの…?
導入で満足して、その後画面を開かなくなった
応答速度の遅いエンドポイントを見て、
	「やっぱりここは重いよね〜」と話して終わる
ソースコード上でどこが重いのか、結局わからない
筆者は過去に全部心当たりがあります…(小声)
ひとつの参考事例として、
弊社での APM の使い方を紹介します
簡単な APM の使い方
トレースを使ったパフォーマンス・チューニング
APM Traceable gem を作った話
具体的なチューニング事例紹介
まとめ
簡単な APM の使い方
トレースを使ったパフォーマンス・チューニング
APM Traceable gem を作った話
具体的なチューニング事例紹介
まとめ
大抵の APM は集計単位ごとにページに分かれている
サイト全体
エンドポイントごと
リクエストごと
最初は「サイト全体」の指標で大まかな傾向を見ると良い
細かく掘り下げていくときに「エンドポイントごと」「リクエストごと」の順に見ていく
ここから先は Datadog APM を例に
スクリーンショットを出します
(他APMでも同様の画面があるはず)
リクエスト数も応答速度も、極端な変化がないか確認
曜日や時間帯で傾向が違うため、前週同曜日と比較する
サービスによっては月末・年末なども考慮する
異常値があった場合はトリアージする
一時的要因であれば様子見、問題がありそうなら調査・対応
週1で「APM を見る会」を実施している
悪化傾向を見つけられるよう定期的に見る機会を作る
トリアージもこのタイミングで実施
開発・運用に関わるメンバー全員で一緒に見ると良い
悪化しているエンドポイントに機能追加などの
		心当たりがあるか確認しやすい
簡単な APM の使い方
トレースを使ったパフォーマンス・チューニング
APM Traceable gem を作った話
具体的なチューニング事例紹介
まとめ
重いエンドポイントを改善する際、
リクエストごとのトレースが足がかりになる
何に時間がかかっているか(スパン)を表示できる
DBアクセスやWeb API呼び出しのスパンを自動で収集
SQLクエリを確認し、適切に includes などを設定する
特定条件やデータセットがないと発生しないN+1クエリは
		bullet gem などで見つけづらいケースがある
まずは EXPLAIN で実行計画を確認する
インデックス追加だけで改善することも多い
時間が足りないので
詳しいチューニング手法は割愛
詳細は以前の Kaigi on Rails 2020 で話した
「Railsパフォーマンス・チューニング入門」をご覧ください!
パフォーマンス・チューニングについては
他の方も発表されていたのでおすすめです!
簡単な APM の使い方
トレースを使ったパフォーマンス・チューニング
APM Traceable gem を作った話
具体的なチューニング事例紹介
まとめ
自動トレースは便利だが、
標準のトレースだけだと困るときがある
ActionController スパン直下に SQL クエリのスパンがある
単なるメソッドの呼び出しは標準だとスパンにならない
クエリ構築がどのコードで行われているかわからないと
		修正しようがない
(重いクエリの例を再掲)
Ruby 処理は自動でスパンに区切られない
原因が全くわからないので対処もできなくなってしまう
Datadog::Tracing.trace(name, **options) do |span, trace|
  # 計測したい処理をここに記入
endDatadog では Datadog::Tracing.trace を使って
	手動でスパンを追加できる
渡したブロック内がスパンとして区切られる
Datadog::Tracingの微妙な使いづらさ# ↓ 標準だとこう書く必要がある
class User
  def awesome_method
    Datadog::Tracing.trace(
      'awesome_method', 
      service: 'my-service', 
      resource: 'User') do
      # ... 処理
    end
  end
end処理対象をブロックで囲むとインデントが変わる
差分が大きくなって気軽につけ外ししづらい
引数が多く、記述がやや冗長
APM Traceable gem# ↓ こう書けるようにした
class User
  include ApmTraceable::Tracer
  trace_methods :awesome_method
  def awesome_method
    # ... 処理
  end
endtrace_methods にメソッド名を指定するとスパンとして表示
UseCaseやPresenterなど、独自レイヤークラスの呼び出し
重いメソッドを切り分けたプライベートメソッド
APM Traceable gem各Presenter#call をtrace_methods した結果
APM Traceable gem元々は1モジュールでlib以下に置いていた
以前 ブログ記事 に書いた話
複数プロダクトに必要だったのでgemに切り出した
APM Traceable gem 本体
Datadog への送信用アダプタ
アダプタを作れば他の APM にも切り替えられるようにした
APM Traceable gem - 仕組みmodule ApmTraceable::Tracer
  def self.trace_methods(*method_names)
    wrapper = Module.new do
      method_names.each do |method_name|
        define_method method_name do |*args, **options, &block|
          trace_span(method_name.to_s) { super(*args, **options, &block) }
        end
      end
    end
    prepend(wrapper)
  end
endtrace_methods で各メソッドを持つモジュールを作り、
prepend して呼び出しをラップするだけ
APM Traceable gem - 仕組みClass Userawesome_method
prepended Moduleawesome_method
メソッド呼び出し
Datadog Adaptertrace_span
トレース送信
簡単な APM の使い方
トレースを使ったパフォーマンス・チューニング
APM Traceable gem を作った話
具体的なチューニング事例紹介
まとめ
MySQL 8.0 に更新した際、特定エンドポイントだけ遅くなった
テンポラリテーブルでの SELECT COUNT(*) の問題っぽい…
元のクエリが GROUP BY してから最初の行を取っており、
実質 DISTINCT 相当の処理がテンポラリテーブルで行われていた
DISTINCT を使えばテンポラリテーブルを使わなくなるので直した
外部 API を並列で呼び出す箇所のトレースが取れていなかった
(単独だと取れるが、2箇所以上Parallelだと取れない)
Parallel をまたぐとスパンの親子関係が途切れるようなので、
手動で親ダイジェストを引き渡すよう修正
  def search_parallel(clients)
+   trace_span('search_parallel') do |_span, trace|
      results = Parallel.map(clients, in_processes: clients.count) do |client|
+       trace_span(client.name, continue_from: trace.to_digest) do
          client.search
+       end
      end
+   end
  endこの変更で、きれいにトレースが見られるようになった 🎉
トレースを精査したところ、外部API呼び出しよりも
XMLレスポンスのパース処理に時間がかかっていた
ローカルで再現させ、 Stackprof でプロファイルを取ったところ
DidYouMean::Jaro.distance が大半を占めている…?
XML で未知の attribute が出てきた際、
const_get でNameError が起こる実装になっていた。
const_defined? で確認を挟むよう実装を修正
      klass = "CXML::#{camelize(key)}"
-     send("#{key}=", Object.const_get(klass).new(val))
+     send("#{key}=", Object.const_get(klass).new(val))
+     if Object.const_defined?(klass)
+        send("#{key}=", Object.const_get(klass).new(val))
+     elseエンドポイントのレスポンスタイムも、
修正を境に飛び上がった値が出ることがなくなった
簡単な APM の使い方
トレースを使ったパフォーマンス・チューニング
APM Traceable gem を作った話
具体的なチューニング事例紹介
まとめ
APM は本番で起こっている問題を見つけるのに便利
統計値を定期的に見て、変化に気づけるようにすると良い
トレースを見ると重い原因を見つける足がかりになる
重要なメソッドにスパンを仕込んでおくと原因を掘り下げやすい
これをやりやすいように独自gemを作った
トレースで目星をつけて、掘り下げた調査を別で行うこともある
APM全然使いこなせてないので、いい使い方教えてください!
休憩時間や懇親会でお話しましょう
New Relic
add_method_tracer が trace_method とほぼ同等機能
Scout APM
Auto Instruments だけで十分かもしれない
Open Telemetry