NIKKEI Digital Recruiting Site

RUMとA/Bテストを使ったパフォーマンスのモニタリング

ブラウザのパフォーマンス指標

Web アプリケーションにおける表示速度がサービスの品質・ビジネスに与える影響は大きい。サイトの遅さはブログサービスではユーザの離脱、広告売上の低下、EC サイトではコンバージョン率の低下などに影響を与えるといった点が知られ、Google は 2018 年 7 月からモバイル検索においてもサイトの表示速度を検索インデックスのシグナルとして加味する旨を公開している。

サイトパフォーマンスの改善は CDN やキャッシュサーバなどを用いたサーバサイドでの改善はもちろんだが、フロントエンドのレンダリングやユーザインタラクションの応答速度などの指標も重要である。また、これらは通信環境にも大きく影響する。ここでは主にクライアントサイドのパフォーマンス指標のモニタリングについて取り上げたい。

ブラウザの画面表示までの流れは以下のとおりである。

  • Time To First Byte
  • Start Render
  • First Paint
  • First Meaningful Paint
  • DOM Interactive
  • Visually complete

新しい日経電子版(r.nikkei.com)では、SpeedCurve を用いて各パフォーマンス指標を継続的に計測している。

一方で、Real World では必ずしも意図通りのパフォーマンス結果が得られるとは限らない。低速なセルラー回線やコンピューティングリソースが潤沢でないモバイル端末など、開発者が開発の際に利用する環境とは著しく異なるクライアントの存在がこの乖離を押し広げている。これは発展途上国のようなインフラが整備されていない環境だけでなく、格安 SIM や安価な Android 端末の普及など、国内においても程度の差こそあれ無視できない状況にある。AMP や Instant Article の登場なども、こういった状況が背景として存在している。

RUM(Realtime User Monitoring)

では、より詳細にクライアントの実態を観測するにはどうしたらよいか。多くのブラウザはこれに対する回答を持っている。Performance Timing API だ。Performance Timing API は、ブラウザ内でページのロードにかかった時間を取得できる。取得できる値は、以下のようなものである。

https://www.w3.org/TR/navigation-timing/

この値を収集して計測・可視化できるようにしたツールを RUM(Realtime User Monitoring)といって、上述した SpeedCurve や New Relic などがサービスとして提供している。しかしながら、単純にサーバからの応答速度や DOM 構築からレンダリングの所要時間を確認したいといったシンプルな要件に対して年間 1 万 USD 程度のコストがかかるので採用が憚られた。また、Feature Flags を使った A/B テスト機構と統合した計測を行いたいといった要望もあったことから、データの収集と可視化は自作することを決めた。結果的には、Atlas(日経のようなメディアにおける分析に特化した機能を有するアナリティクスツール。Google Analytics や Adobe SiteCatalyst のようなもの)という内製で開発したユーザデータの集計・アナリティクスツールを利用しているが、基本的にはビッグデータの集計に耐えうるものならバックエンドは何でもいい。Google Analytics 等でも十分に実現可能なように思う。Atlas についてはこちらの資料を参照いただきたい。 https://www.slideshare.net/HajimeSano1/jaws-ug-bigdata-branch-oct-2017

最初に検討したのが、Fastly を beacon の終端とするシステムだ。Fastly を利用した場合、完全にサーバーレスでデータを収集することができるのがメリットとなる。クライアントサイドのコードは以下のようなものである。

window.addEventListener('load', e => {
    const url = new URL('/rum-beacon', location.origin)
    const timing = performance.timing
    const params = {
        dns: timing.domainLookupEnd - timing.domainLookupStart,
        tcp: timing.connectEnd - timing.connectStart,
        request: timing.responseStart - timing.requestStart,
        response: timing.responseEnd - timing.responseStart,
        dom: timing.domComplete - timing.domLoading,
        onload: timing.loadEventEnd - timing.loadEventStart,
        untilResponseStart:
            timing.responseStart - timing.navigationStart,
        untilLoadComplete:
            timing.loadEventEnd - timing.navigationStart,
    }

    Object.keys(params).forEach(key => {
        url.searchParams.append(key, params[key])
    })
    fetch(url.toString())
})

本来であればnavigator.sendBeaconを使うべき部分だが、Fastly VCL 上でパラメータを扱うには GET の方が都合がよいため上記のような実装になっている。集計時に各メトリクスの加算を行うよりは、予め利用することがわかっている場合は送信時に加算しておいた方がよいだろう。また、完全にロードが完了するまでは各メトリクスの値は正確ではないので、onload イベント後に行う実装にすべきだ。

Fastly では以下のような実装を行う。LTSV 形式なので別のフォーマットを使う場合は適宜読み替える必要がある。

    vcl_recv {
      if (req.url ~ "rum-beacon") {
        log {"syslog "} req.service_id {" fastly-log :: "}
          {" timestamp_us:"} time.start.usec
          {" url:"} regsuball(req.http.Referer, {"  "}, "")
          {" dns:"} if (req.url ~ "dns=(\d+)", re.group.1, "")
          {" tcp:"} if (req.url ~ "tcp=(\d+)", re.group.1, "")
          {" request:"} if (req.url ~ "request=(\d+)", re.group.1, "")
          {" response:"} if (req.url ~ "response=(\d+)", re.group.1, "")
          {" dom:"} if (req.url ~ "dom=(\d+)", re.group.1, "")
          {" onload:"} if (req.url ~ "onload=(\d+)", re.group.1, "")
          {" untilResponseStart:"} if (req.url ~ "untilResponseStart=(\d+)", re.group.1, "")
          {" untilLoadComplete:"} if (req.url ~ "untilLoadComplete=(\d+)", re.group.1, "");
        error 950 "Fastly Internal";
      }
    }

    vcl_error {
      if (obj.status == 950) {
        set obj.status = 204;
        set obj.response = "No Content";
        synthetic {""};
        return(deliver);
      }
    }

VCL のロギング設定が完了したら、あとは Fastly の Realtime Log Streaming の機能を用いて任意のバックエンドにログを出力する。弊社では Kibana を利用しているが、その後は好きなツールを用いて可視化することができる。

サーバーレスな設計で RUM の構築が行えることは分かったが、1 つの問題が浮上した。r.nikkei.com では様々な部分で Feature Flags を用いた A/B テストを行っており、パフォーマンス改善の取り組みなども A/B テストのユーザグループごとにモニタリングしたいという要望があった。A/B テストの値は JS の変数として保持されているので、上記実装のパラメータに追加、CDN でパースしてレスポンスに含めることもできたが、高度な VCL 芸が必要になってくるのであまりやりたくない。この計測についても Atlas で対応可能であったため、今回はそちらを利用して集計することにした。

実際のユースケース

ここからは、日経で RUM をどのように利用しているか、実際のユースケースを紹介したい。前提として、現在の r.nikkei.com のアーキテクチャは次のようになっている。

r.nikkei.comのアーキテクチャ全体図

図から分かるように、Fastly が全てのリクエストを最初に受け付けて、キャッシュできるものはキャッシュする構造になっている。しかしながら、β リリース時や一部のパフォーマンスの悪いバックエンドの API のために一部の API レスポンスは ElastiCache(Redis)にもキャッシュされている。一般的にキャッシュ層が複数レイヤーに入ってくることは障害点の増加や改修コストの増加が見込まれるため避けたいが、速度を保つために仕方なく実装したアプリケーションレイヤーでのキャッシュだった。

案の定リリースをしてみると、Fastly 上でのキャッシュが更新されても Redis のキャッシュが更新されないため古いレスポンスが返る、Redis に意図せず巨大なデータサイズのオブジェクトをキャッシュしてしまうなどの問題が起こった。また、日経では 1 日あたり数 100 を超える記事が配信されており、特に速報などの記事の追加や更新はいち早くサイトに反映させたい。それに対応するために、Lambda Function で記事の更新をチェックし、追加や更新のあった記事は Fastly の Instant Purge API を利用して CDN のキャッシュを更新する仕組みを実装した。これによって記事の CDN キャッシュは TTL を飛躍的に長くすることが可能になり、Redis を利用したキャッシュ機構は大部分において不要になった。しかし、既に動作しているシステムにおいてこの変更が本当にサイトの応答速度やサーバ負荷に悪い影響を与えないかを検証するのは難しい。そこで利用するのが、A/B テストと RUM を組み合わせたモニタリングである。

r.nikkei.com では常に複数の A/B テストが行われており、ログインの有無に関わらずユーザは特定のセグメントに属している。この機構を利用して Dark Launch / Canary Release なども行っており、今回も Redis キャッシュを使わずに CDN のキャッシュを長期化する変更を全体の 30%のユーザに限定してリリースした。上記で説明した分析基盤 Atlas には、既に RUM のメトリクスも蓄積されているので、リリース後 Redis をバックエンドに持つユーザと持たないユーザでどのようにパフォーマンスが変化するのかが観測できる。今回はサーバサイドの応答速度が問題なので、 requestの所要時間を確認することでモニタリングが可能となる。

分析基盤AtlasのログをKibanaで可視化し、モニタリングする

ここから実際にモニタリングしてみると、外れ値が多く単純な平均で見るとあまり有用でないことが分かった。クライアントの通信環境や機種によって数値が大きく異なるのは、RUM の特性上避けられないので、パーセンタイル値等でモニタリングする必要がある。true の線が Redis を使わず CDN のキャッシュ時間を長めにとる実装が入ったセグメントで、false が従来通りの Redis が有効となっているセグメントである。request の指標はリクエスト開始からレスポンス開始までの差分なので低いほどよい。図を見ると、Redis の有効/無効がクライアントから見たときにパフォーマンスに大きな影響を与えていないか、僅かながら応答速度が向上していることが分かる。1 週間程度モニタリングを続けてみて、問題なさそうなので Redis を使わない実装をデフォルトにした。CDN 上でのキャッシュ期間が長くなったこととセグメントの数が 1 つ減ったことで、キャッシュ効率が更によくなり切り替え後サイトの応答速度もより速くなった。

このように、RUM と A/B テストの仕組みを利用することで、大きな影響を伴うシステムの変更を最小限に、安全に行うことができた。RUM を使うことでクライアントのパフォーマンス指標の変化をリアルタイムに確認でき、一部のユーザにのみ変更を行うことでサーバ負荷の予測も行いやすくなる。何かしらクライアント側でエラーが起きたり、通信環境に大きく左右されたりすることもあるため RUM を使った細かな計測は難しいが、今回のように A/B テスト機構と組み合わせたり、フロントエンドのパフォーマンスチューニングなどを行ってランタイムのレンダリング開始から完了までの時間を計測したい、といった場面では Synthetic Monitoring だけでなく RUM というアプローチも有用であり、かつ導入も用意なので、利用を検討してみてはいかがだろうか。

宍戸俊哉
ENGINEER宍戸俊哉

Entry

各種エントリーはこちらから

キャリア採用
Entry
新卒採用
Entry