
Datadog を使った KARTE 管理画面パフォーマンス改善の取り組み
Posted on
エンジニアの大矢です。
Datadog を利用して KARTE の管理画面のパフォーマンスの監視および改善を行いました。
KARTEには計測したエンドユーザーの分析や、計測ユーザーへの配信管理を行う管理画面がありますが、大量のデータを扱うためパフォーマンスが問題になることがしばしばあります。ナビゲーションメニューからページ遷移した時の読み込み速度や、ボタンをクリックした時の反応速度が課題として挙げられます。
この記事ではパフォーマンスの監視をするにあたって考えたことや Datadog の活用のポイント、改善で取り組んだことについて紹介します。
パフォーマンス監視の方針
PLAID ではモニタリングツールとして Datadog を活用しているため、まず Datadog を利用してパフォーマンスの監視をする方法を調べました。
KARTE はマイクロサービスアーキテクチャで構築されているため、分散トレーシングについてはすでに Datadog APM が導入されています。Datadog APM は分散トレーシングツールです。バックエンドのパフォーマンスを計測でき、問題の原因を特定するためにも APM は必須級のツールだと考えています。APM を利用して、どのマイクロサービスのどの API のレイテンシが悪いか、その API のどの DB リクエストに時間がかかっているかなどが特定できます。
しかし、実際に管理画面のユーザー(以下では単に「ユーザー」と呼びます)の画面でコンテンツが表示されるまでにどれくらい時間がかかっているかということはわかりません。今回はAPI 自体をモニタリングの対象とするわけではなく、APM を利用して改善のための調査を行ったり、改善後の効果を計測したりするという役割のツールとして使用しました。
Datadog RUM
今回、パフォーマンス監視はなるべくユーザー目線でやりたかったため、 Datadog RUM を利用しました。パフォーマンスを計測する手段としては他にも下記のような方法が考えられます。
- lighthouse などを使って定期的に特定の画面の Core Web Vitals を計測する
- パフォーマンスが問題になっているページのボトルネックとなる API のレイテンシを APM などを使って計測する
1は実際にツールを自作して行っており、KARTEの社内利用プロジェクトに対して定期的に計測を行い、結果を Datadog のカスタムメトリクスとして収集していました。この方法による計測は安定した結果になります。例えばネットワーク環境やデバイスは一定であり、対象のプロジェクトで扱っているデータもある程度一定なため、実装自体が大きく変わらない限りは1時間後に計測されるメトリクスもほとんど同じ値を示します。一方で、実際のユーザーの環境で計測できる Core Web Vitals の値とは大きく異なる場合もあります。
2はボトルネックを把握している場合や、特定の API のレイテンシがユーザーの体験に大きく影響すると知っている場合には有効です。例えば KARTE Talk では、メッセージ一覧を取得する API のレイテンシやメッセージを送信する API のレイテンシを監視しています。
今回はそもそもどの画面のパフォーマンスが想定以上に悪いのか、その画面は実際によく使われてる画面なのか(改善による効果は大きいのか)といったところを知ることも計測の目的であったため、2に関しては保留しました。
Datadog RUM はブラウザのパフォーマンス、エラーを計測できるツールです。RUM によって収集されたデータは次のような構造を持っています。
View
View は単一のページへのアクセスを示します。ページが切り替わると 別の View として計測されます。View にはいくつかの Performance メトリクスが含まれており、Core Web Vitals における LCP・FID・CLS もここに含まれます。よく使われそうなメトリクスとしては他にも view.loading_time
というものがあります。これはページアクティビティ(ネットワークリクエスト・DOMの更新など)が 100ms 間発生しなくなる状態までにかかった時間です。
さらにリソースリクエストのそれぞれのタイミングが chrome dev tool のように waterfall 形式で可視化されます。さらに、どのリソースの読み込みまでが LCP の計測に関与しているかもわかります。これによってそれぞれの指標に対してボトルネックとなっている Resource を特定することが容易になります。
Resource
Resource にはフロントからの全てのネットワークリクエストが計測されます。
view.loading_time
はレンダリングの完了だけではなくネットワークリクエストの完了も考慮して計測されるため、レンダリングに関与しないネットワークリクエストが恒常的に続く場合には意図した値が計測されません。例えば KARTE の管理画面には KARTE の計測用のスクリプトが埋め込まれており、イベントデータの送信を都度送っていたりします。そのため excludedActivityUrls に url を列挙して、view.loading_time
の計算に関与しないよう設定することが必要です。また、完全に計測を無視したい場合は beforeSend に次のような処理を追記します(beforeSend では false を返すと計測が行われません)。これをしておくと Datadog の画面で waterfall が見やすいです。
// 無視する URL を列挙
const excludedEventSourceUrls = [
'https://client-log.karte.io/dd/metrics',
'https://chat.karte.io/chat/v2/checkSubscription',
'https://b.karte.io/event',
'https://gae.karte.io/rewrite-log',
'https://mirror2.karte.io',
'https://chat.karte.io',
'https://edge-api.karte.io',
'https://notify.bugsnag.com/',
]
DD_RUM.init({
// 他のパラメータは省略
beforeSend: (event) => {
if (event.type === 'resource') {
if (excludedEventSourceUrls.some((url) => event?.resource.url.startsWith(url))) {
return false
}
}
},
excludedActivityUrls: [
(url) => excludedEventSourceUrls.some((excludedEventSourceUrl) => url.startsWith(excludedEventSourceUrl)),
],
});
Action
Action のパフォーマンスを計測することもできます。ユーザーが特定のボタンなどの要素をクリックした時に、新たに発生したネットワークリクエストが完了し、DOM の更新が完了するまでの時間を計測できます。KARTE でもいくつかのアクションのパフォーマンスが悪いことが報告されていたため、それらのアクションのパフォーマンスの計測をしています。
デフォルトではアクションの名前は HTML のテキスト要素が使われてしまうため、個人情報などが含まれてしまう場合があります。 actionNameAttribute を設定してアクションの名前に使用される属性を指定するのがおすすめです。actionNameAttribute を class にすることで個人情報が送信されないようにした上で、計測したい要素には data-dd-action-name を使ってアクションの名前を明示しました。KARTE は使い方によっては非常にデータ量が多くなるサービスです。開発環境ではスムーズに動作しても、クライアントの環境では Popover を1つ開くのにすら数秒かかってしまうケースもあり、そのような状態のモニタリングと原因の特定に役立てました。
Long Task
Long Task というフロントのスレッドをブロックするようなタスクを検知する機能もあります。しかしこれは Long Task が発生したタイミングはわかるものの、フロントエンドのどの箇所の処理でそれが発生したのかということは推測する必要があります。KARTE はサービスの特性上1つの画面で非常に多くの操作ができるようになっており、扱うデータもさまざまなので、Long Task が発生したタイミングだけで問題の箇所を特定するのは容易ではなく、結果あまり使いませんでした。
監視対象
Datadog RUM でできることを一通り理解したあと、監視対象のメトリクスやどういった軸で監視するかというのを決めていきました。
Datadog RUM を導入した際には「RUM - Web App Performance」というダッシュボードが作成されるようになっています。これを見ると、FID(First Input Delay)・CLS(Cumulative Layout Shift)の値はそれぞれ Core Web Vitals では「Good」の評価となる値でした。もともと気になっていたのは読み込み速度だったため、LCP(Largest Contentful Paint) と LT(Loading Time)を監視対象のメトリクスと定義しました。LCP に関して平均を取るか、パーセンタイル値を取るかなども考えられますがこれも Core Web Vitals を参考に75パーセンタイルの値を監視しました。フロントから見たパフォーマンスの計測値にはノイズが入りやすいためサーバサイドの監視よりもパーセンタイルは低めに設定しています。
監視対象の画面も数ページに絞りました。View イベントのカウントを見ることでどの画面がよく利用されているかということが定量的に把握できるため、カウントが上位のページを監視対象としました。
監視する対象のメトリクスが決まったら、RUM Events から Custom Metrics の生成をしておくのがおすすめです。RUM のイベントの保持期間は30日間となっており意外と短いため、後から改善結果を見返すときにデータがなくて困るということが起きます(このブログを書いている時にもとても困りました)。LCP と LT はそれぞれ custom metrics を作成しています。注意点としては、生成されたカスタムメトリクスに対して課金が生じるため、group by にはカーディナリティの低い属性を選び必要以上に追加しないことです。また、Calculate percentiles にチェックを入れることで複数のパーセンタイル値を指標として扱えるようになります。
KARTE には「プロジェクト」と呼ばれる単位があり、契約クライアントごとにプロジェクトが分かれています。プロジェクトによって扱うデータや特性が異なるため、特定のプロジェクトの特定のページだけパフォーマンスが悪いということもあります。そのため、プロジェクトの軸でパフォーマンスを集計できると便利です。setGlobalContextProperty
を利用して、RUM イベントにカスタム属性を付与することができます。これで必要に応じてプロジェクト毎にパフォーマンスを集計することができるようになりました。
DD_RUM.setGlobalContextProperty('project', { id: project.id, name: project.name });
改善の手順
LCP・LT を改善する際に、まず重要なのはボトルネックを特定することだと考えました。先に述べたように View には Resource が紐づいており waterfall として可視化されます。最初にロードされる HTML ファイルの中で Datadog RUM の初期化をおこなっているため、それ以降のネットワークリクエストが次のような順序で記録されます。
- エントリーポイントとなるJavaScriptファイル読み込まれる
- 次に chunk となるJavaScriptファイルが並列で読み込まれる
- その後 API リクエストが呼ばれる
さらに、API リクエストの中には chunk が読み込まれた後すぐに呼び出されるものと、少し経過してから呼び出されるものがあります。正確なことは実装を見てみないとわかりませんが、ある API の結果を参照して API を呼び出しているといったようなケースがあり、さらに LCP、LT のタイマーが完了したタイミングがわかるため、そこに到達するためのクリティカルパスを改善すれば LCP、LT が改善されます。逆にいうと、クリティカルパス上にない API はいくらレイテンシが悪かったとしても、LCP、LT の計測には関与していない可能性があります。
今回ボトルネックを特定するために、いくつかの View の waterfall を1つ1つ見ていきました。特定の API のレイテンシが遅くなっていることを想定してこの調査を行ったのですが、実際にはどの API も(静的ファイルであっても)タイミングによってレイテンシが数秒〜十数秒程度になることがわかりました。
さらに、RUM は APM と接続することができ、View の waterfall からシームレスに APM のトレースを調べることができます。トップレベルのスパンに browser.request というのが追加されています。
これを見ると、フロントエンドからのリクエストが開始されてから、バックエンドの express による処理が始まるまでかなり時間がかかっていることがわかります(サーバサイド言語は Node.js を使用しています)。これはブロッキング操作が長い時間行われており、イベントループが JavaScript の実行を継続できない状態になっていると予想できます。KARTE は k8s で構築されており静的ファイルの配信と API 処理は同じ pod で実行されます。そのためブロッキング操作が長い API が呼ばれた場合、その pod では静的ファイルの配信も遅くなってしまいます。
問題の API を特定するために、Datadog の Continuous Profiler を利用しました。
これは、ある1時間の平均的な CPU 処理の内訳を示しています。時間を指定して比較をすることもでき、普段のフレームグラフと問題が起きた時間帯のフレームグラフの差分を可視化することもできて便利です。
このフレームグラフを見ることで、rpc2.ts
というファイルで findIndex
や ObjectID.toString
といった関数が使用されており、それらが CPU を使用する割合が大きいことがわかります。該当のコードは以下のような感じです(変数名などはいじってます)。
const _ = require('lodash');
function mapMongooseDataToBqResults() {
// 何らかの処理
const results = bqResults.filter(stat => {
const mongooseData = _.find(mongooseDatas, mongooseData => mongooseData._id == bqResults.mongooseId);
// 何らかの処理
}
}
このコードは特に問題なく見えますが、下記のような問題点があります。
- bqResults は特定のプロジェクトでは数十万行程度になる
- lodash の find は JavaScript の native find に比べて3倍程度遅い
- bqResults の要素数 × mongooseDatas の要素数の繰り返し処理が行われ、その度に mongoose の ObjectID.toString 関数が呼ばれる。
次のように書き直すことで、関数の処理速度が大幅に向上しました(紫のドット線が改善前、青の実線が改善後)。
function mapMongooseDataToBqResults() {
// 何らかの処理
const mongooseDatasWithStringId = mongooseDatas.map(mongooseData => {
return { ...mongooseData, _idString: mongooseData._id.toString() };
}
const results = bqResults.filter(stat => {
const mongooseData = mongooseDatasWithStringId.find(mongooseData => mongooseData._idString == bqResults.mongooseId);
// 何らかの処理
}
}
他にも、Profiler を使って CPU を大きく使用している箇所をいくつか特定し改善しました。例えば mongoose の結果をそのまま express response に渡しているような実装になっているところに、lean option をつけるのも効果的でした。
イベントループをブロックするような処理を軽くすることで、改善を行った API を使用していない画面でも LCP・LT の改善が見られました。ロードバランサから見たサーバサイドのレイテンシは p99 では60%以上改善しました。
最終的に、監視対象だった利用頻度が高くパフォーマンスが悪い画面の LCP は50%以上改善しました。
まとめ
この記事では、Datadog の次のサービスを主に使い、KARTE の管理画面のパフォーマンスの改善を行いました。
- Datadog RUM: フロントエンドの初期表示(LCP)が遅いページの調査
- Datadog APM: バックエンドのAPIごとのレイテンシーの調査
- Datadog Continuous Profiler: バックエンドでボトルネックとなっている処理の調査
監視〜改善の一連の流れをやってみて、パフォーマンスが良い状態を保つには計測が非常に大事だと実感しました。
ユーザーの体感に近い指標を改善の目標とすることで意味のある改善ができたと思いますし、エンジニア以外の人にも説明がしやすいと感じました。実際の改善内容は特に難しいことはしていませんが、しっかりと計測してボトルネックを把握することで効果的な改善をすることができました。
今までも何度かパフォーマンス改善の取り組みは行われてきましたが、モニタリングが不十分で改善だけして終わりということになりがちでした。今回は監視の仕組みをしっかりと整えたため、今後も改善していける状態になったと思います。
CX(顧客体験)プラットフォーム「KARTE」を運営するPLAIDでは、データを活用しプロダクトを改善していくエンジニアを募集しています。
詳しくは、弊社採用ページをご覧ください。