このエントリは、SmartHR Advent Calendar 2024 シリーズ 2 の 16 日目の記事です。
SmartHR で年末調整機能の開発を担当しているプロダクトエンジニアの hakozaru と motty です。
年末調整機能はみなさまもご存知の通り、年末に集中して多くのお客様にご利用いただくサービスです。
大変ありがたいことに SmartHR をご利用いただくお客様の数は年々増加しており、嬉しい反面それに伴うパフォーマンス面での課題が浮上していました。
特に、一部の環境では顕著なパフォーマンス劣化が観測され、利用体験に大きく影響を与えてしまう状況が発生していました。
しかし、これまでパフォーマンス改善のための体系的な情報収集や分析の仕組みが整っていなかったこともあり、問題の根本原因の特定や改善に大きく時間がかかってしまう、または原因の特定が困難なケースも多くありました。
そこで私たちは、パフォーマンス改善のための仕組みを新たに構築し、適切に対応するプロセスを整えることにしました。
本エントリでは、その過程と成果についてみなさまにご紹介いたします。
New Relic のデータを活用する際にチームが抱えていた課題
SmartHR では、APM(Application Performance Management)ツールとして New Relic を使っていますが、パフォーマンスの改善に役立てるためには 2 つ課題がありました。
1 つ目の点は New Relic に蓄積されるパフォーマンスに関するデータ(ここでは Transactions を指します)が、デフォルトの状態では「どのエンドポイントでどれくらい遅いクエリが発生したか」ということしかわからず、「誰がアクセスしたのか」「どんな権限の人のアクセスだったのか」「どのような検索をしたのか」などといった情報が得られないという点です。
同じエンドポイントでも会社 A のお客様はパフォーマンスに問題なく、会社 B のお客様だとめちゃくちゃ遅くなるといったことがあり、パフォーマンスの調査をするためには「誰が」「どんなアクセスをしたのか」という情報まで集められるようにする必要がありました。
2 つ目の点は New Relic では、APM や一部のデータソースのデータ保持期間は標準で 8 日間、Data Plus オプションで 98 日間となっている点です。
SmartHR では Data Plus オプションは使っていないため、年末調整の繁忙期シーズン中に問い合わせなどに対応しつつ、パフォーマンスの情報も同時に確認することは現実的ではなく、かといって年末調整シーズンが終わってからパフォーマンスの改善に取り組もうとしても、収集されたデータのデータ保持期間を過ぎていてパフォーマンス改善に役立てられないという課題を抱えていました。
ユーザーの情報を収集できるようにする
New Relic(newrelic_rpm gem)には add_custom_attributes というメソッドが用意されており、このメソッドを呼び出すことでカスタム属性の記録ができるようになります。
SmartHR では上述した 1 つ目の課題である「誰がどんなアクセスをしたのかわからない」という課題を解決するため、以下のようなパラメータを記録するようにしました。(実際は他にも様々な情報を記録しています。)
- ユーザーの ID
- アクセスしたユーザーを特定するため
- ユーザーが管理者(admin)かどうか
- 管理者の場合、クエリの絞り込みなどが簡略化されて高速である場合が多いのですが、一部の環境では管理者でもクエリが遅い場合があったため
- Rails の params
- 検索のクエリなどを収集するため
Rails の params を収集しているため、秘匿性が高い情報が含まれる可能性がある POST リクエストは排除し、GET リクエストの情報のみ集めるようにしています。
上の内容をコードで具体的に示すと以下のようになります。
class ApplicationController < ActionController::Base before_action :set_new_relic_context, if: -> { request.get? } def set_new_relic_context NewRelic::Agent.add_custom_attributes( user_id: user.id, admin: user.admin?, params: params.to_unsafe_h, # other_params ) end end
また、年末調整アプリでは非同期処理に sidekiq を利用しており、こちらの非同期処理でもパフォーマンスの悪い箇所を検知するため、以下のように Client Middleware を設定しています。
# config/initializersなどに定義 class ApmContextSetter def call(_worker, job, _queue, _connection_pool) ::NewRelic::Agent.add_custom_attributes( job_id: job['args'][0], tenant_id: job.dig('tenant', 'id'), # other_params ) yield end end # config/initializers/sidekiq.rb Sidekiq.configure_server do |config| config.client_middleware do |chain| chain.add ApmContextSetter end end
以上の設定を行うことで New Relic のNRQL(New Relic で使える SQL のようなもの)で、以下のようにしてデータを取得することができます。
-- 通常のアクセスの場合 SELECT * FROM Transaction WHERE transactionType = 'Web' -- 非同期処理の場合 SELECT * FROM Transaction WHERE transactionType = 'Other'
年末調整期間中のデータを退避する
パフォーマンス調査のために必要な情報を New Relic に記録できるようになったので、続いてデータの退避先を検討しました。
データの退避先として必要な要件は以下のように定めました。
- なるべく新しい仕組みを必要とせず、今すぐに始められること
- 年末調整チームの完璧じゃなくても、まず小さく始めることが大切という教訓からも、なるべく小さく早く始められるという点を意識しました
- 以下の機能があること
- 上述した NRQL を使って New Relic に蓄積したデータを取得するため NerdGraph API を呼び出せること
- API のレスポンスをなるべく簡単に何らかの媒体に記録できること
- API を呼び出すコードが定期実行できること
- なるべくコストがかからないこと
検討した結果、年末調整期間中の New Relic のデータは毎日 Google スプレッドシートへ記録を行うことにしました。
スプレッドシートは GAS(Google Apps Script) との親和性が高く、API を呼び出す => スプレッドシートに書き込むという手順をシームレスに行うことができます。
さらにトリガー機能によって柔軟に定期実行計画を設定でき、なおかつ無料であるなど、十分に要件を満たしていました。
退避先も決まったので、早速 New Relic に蓄積したデータを GAS を用いてスプレッドシートに退避していきます。
New Relic の NerdGraph API は GraphQL 形式の API です。
API のキーなどを用意して、特定のエンドポイントに POST リクエストを送信すればよいだけなので、クエリの形に気をつければ特に難しいことはありません。
簡略化したコード例となりますが、以下のようなクエリで API を呼び出しています。
NerdGraph にはブラウザから実行できるエクスプローラーも用意されていますので、コードを組み立てる際は参考にすると良いと思います。
const query = ` { actor { account(id: ${ACCOUNT_ID}) { id nrql( query: "SELECT * FROM Transaction SINCE '${SINCE}' UNTIL '${UNTIL}' duration >= 10 AND transactionType = 'Web' LIMIT MAX" ) { results } } } } ` const options = { headers: { 'Api-Key': NEW_RELIC_API_KEY, 'Content-Type': 'application/json', }, method: 'POST', payload: JSON.stringify({ query: query }), } const response = UrlFetchApp.fetch('https://api.newrelic.com/graphql', options) JSON.parse(response.toString())
今回は毎日コードを実行し、その日に検出された遅いクエリのみを記録したかったため、以下の絞り込みを追加しています。
- 00:00〜23:59 の期間のデータが取得できるように SINCE と UNTIL を指定
- 「遅いクエリ」を「10 秒以上かかったクエリ」とチームで定義したので、
durationに 10 を指定(単位は秒です)
以上で欲しいデータが得られたので、後で集計しやすいようにレスポンスを整えた後、以下のようにしてスプレッドシートへ書き込んでいけばデータ退避完了となります。
const HEADER = ['データ1', 'データ2', 'データ3'] const response = ['レスポンス1', 'レスポンス2', 'レスポンス3'] const spreadSheet = SpreadsheetApp.getActiveSpreadsheet() const newSheet = spreadSheet.insertSheet() newSheet.setName('yyyy-mm-dd') newSheet.appendRow(HEADER) newSheet.appendRow(response)
最後に GAS の「トリガー」ページから定期実行設定を行えば、あとは自動でスプレッドシートにデータが蓄積されていきます。
年末調整期間中は非常に多くのリクエストがあり、New Relic に蓄積されるデータ量も膨大となるため、1 回のリクエストで全てのデータを取得しようとすると、トリガーの実行時間上限に達してしまいエラーとなってしまいます。
また、1 回の API リクエストに含まれるレスポンスは最大で 5000 件となっていますので、蓄積されたデータが多い場合は一部欠落する可能性がある点にも注意が必要です。
これらの問題を回避するために、SmartHR では API で取得する時間の範囲(上記クエリの SINCE と UNTIL)の指定を調整していくつかに分割し、複数回 API を呼び出して取得するように工夫しました。
- 0 時〜10 時に記録されたデータを取得するコードを実行するトリガー
- 10 時〜16 時に記録されたデータを取得するコードを実行するトリガー
- 16 時〜21 時に記録されたデータを取得するコードを実行するトリガー
- 21 時〜24 時に記録されたデータを取得するコードを実行するトリガー
のように分割するイメージです。
アクセスされる時間帯(=データが蓄積される時間帯)にかなり偏りがあるため、データ量が少ない早朝と深夜の時間を広めに取り、データ量が多い日中の時間を細かく取得するようにしています。

パフォーマンス改善の優先順位付けやスロークエリの再現に役立った
スプレッドシートに記録したことによって、集計がしやすくなる利点がありました。
エンドポイントごとの処理時間を可視化することで、どのエンドポイントを優先的に改善すべきかを判断する材料として活用しました。
例えば、下図のようにデータを整理した結果、/api/hoge は他のエンドポイントに比べて高頻度かつ長時間の処理が発生していることが分かりました。一方、/api/foo のように 10 秒以上の処理がほとんど発生しておらず、パフォーマンスに与える影響が限定的なエンドポイントについては、改善の優先度を下げる判断をしました。

集計の結果、遅い処理が 43 エンドポイントありましたが、最優先の改善対象を 6 つのエンドポイントに絞り込むことができました。
また、得られたユーザー情報や権限、検索条件を参考にして、これまで再現できなかったスロークエリを再現することができました。
スロークエリの再現によって、クエリの実行計画を用いた詳細な分析が可能になり、ボトルネックの特定に繋がりました。
今回は割愛しますが、絞り込んだ 6 つのエンドポイントについて改善を実施した結果、6 つ全てのパフォーマンスを 12 倍以上速くすることができました。
おわりに
今回は、年末調整機能における New Relic を活用したデータ収集の工夫により、これまで再現が困難だったスロークエリの再現に成功した事例を紹介しました!
私たちは今後も年末調整機能を利用されるお客様にとって、安心してご利用いただけるシステムを目指して、引き続き改善に取り組んでいきます。
年末調整機能の開発に興味のある方、ぜひカジュアル面談でお話ししましょう!