LIVESENSE ENGINEER BLOG

リブセンスエンジニアの活動や注目していることを発信しています

転生会議を支える技術「インフラ編」〜サイト爆速化への道〜

はじめに

転職会議では、2年ぶりとなるエイプリルフール企画を実施しています。 今年のテーマは「転生」、その名も転生会議です!

無料で豪華商品をゲット!今すぐ転生しろ!

「何か楽しいことしたいね」という気持ちで集まった、有志のエンジニアで始まった企画です。 有志で始めた企画のため、一番大切にしたいのは「楽しむこと」です。 さらに言うと、楽しむには業務として堂々と実施させてもらえることが重要であり、業務とするからにはプロダクトや組織に貢献すべきと考え、以下のように二つの目標を掲げました。

表目標:エイプリルフールに便乗して転職会議の認知を上げ、流入を増やす。(プロダクト貢献)
裏目標:技術的挑戦をしながら転職会議システムのレンダリング最速を目指す。(エンジニア組織貢献)

このブログでは、裏目標について詳しくご紹介します。 転生会議の裏では「使ってみたいけど、プロダクション適用にはまだ知見が足りないかも…」という技術をふんだんに使いました。 「やりすぎ」「オーバーエンジニアリング」は褒め言葉です!

転生会議の技術要素

転生会議は次のような技術に支えられています。

  • GKE
  • Firebase
  • golang
  • Elixir
  • Phoenix
  • Vue
  • Nuxt
  • fastly
  • webp
  • nginx
  • Route 53

1回で全部書くと長くなるので各担当者が二回に分けて転生会議を支える技術について述べていきます。 今回はインフラ周りを担当したSREの山内とだいたいなんでもやったyamitaniがお送りします。 ではインフラ編を張り切っていきましょう!

インフラ構成

表側にfastly、裏側にGKEを配置する構成です。
フロント部分はnuxt.jsで実装し、APIはElixirで実装されています。
OGP画像はCloud Functions上で動くGo言語のプログラムによって動的に生成されます。
画像を設置するデータストアはCloud Storageを利用し、データベースはFirebaseを利用しています。

それぞれ誰かが使いたいものを利用しました。

※ 初期構想図です。

やったこと1: fastlyの導入【山内】

速度を追求するにあたって主にやったことは大きく分けて2つです。
1つはできるだけcacheを利用できるようにすることです。

これはfastlyを利用することにより簡単に実現できました。
nginxなどでcacheを実現しようとするとcache hit率を上げるためにcacheを共有することを考える必要があります。
分散させるとcacheを飛ばす処理などめんどくさいことが増えます。
その点fastlyだとcacheの分散について考えなくて良いですしcacheを飛ばすのもapiから気軽にできます。

基本的にはそのままの設定で導入して、リリース前は開発環境以外のアクセスを弾く用に設定していました。
cache時間等の設定は裏側に存在するnginx側の設定で設定していました。

今回はログイン機能が存在しなかったのでとてもシンプルな設定になりました。

やったこと2: 通信処理のチューニング【山内】

cacheの導入の他にやったのは通信処理のチューニングです。 fastlyを導入するとレンダリング完了までのボトルネックがレンダリング処理と通信処理に寄ります。

nuxt.jsで作られたサイトは特に何もしなかった場合次のような流れでレンダリングが行われます。

  1. rootのhtmlをダウンロードする
  2. htmlの記述に従いjsをダウンロードする
  3. ダウンロードしたjsからDOMが生成される
  4. 生成されたDOMに従い画像などの関連ファイルがダウンロードされる
  5. レンダリングが完了するまで2~4を繰り返す

レンダリングをするとき重いデータのダウンロードが発生するとダウンロード時間に引っ張られてレンダリングが遅くなります。
ダウンロード時間を減らすため転生会議では可能な限りデータを圧縮するよう務めています。

Mozilla FirefoxGoogle Chrome はWebPを利用することができるので積極的に採用しました。
ただしWebPに対応していないブラウザでは表示できないことと、開発時に画像を触りたいときWebPだと触りづらいので オリジナルのデータはPNGを利用しています。

WebP対応ブラウザへのWebP配信はnginxで行っています。
nginx側で Accept headerを解釈して配信しています。
次の用に Accept: image/webp,*/* で要求が来たとき 画像ファイル名.png.wepb を配信するようにnginxの設定を記述すると実現できます。

map $http_accept $WebP_suffix {
    default   "";
    "~*image/WebP"  ".WebP";
}

server {
    location ~ \.(png)$ {
        expires 7d;
        add_header Vary 'Accept';
        try_files $uri$WebP_suffix $uri =404;
    }
}

PNGの方も最適化しています。
PNGは zopflipng で再圧縮することにより容量を削減しています。
圧縮する画像にもよりますがオリジナルの画像の 20~80% 程度に圧縮することができます。

転生会議ではフロント部分はすべてnginxの静的ファイル配信機能を利用して配信しています。
静的ファイル配信機能を利用することでnginxの static_gzip を利用できます。
これはnginx側のCPU資源を利用しないことと zopfli という通常のgzip圧縮アルゴリズムより重いアルゴリズムでも安心して利用できることに繋がります。

$ find -E . -regex ".*\.(svg|html|htm|js|css|png)" -type f | xargs -I {} ls -la {} | awk '{ total += $6 }; END { print total }' | gnumfmt --to=iec
4.2M
$ find -E . -regex ".*\.(svg|html|htm|js|css)\.gz|(.*\.png)" -type f | xargs -I {} ls -la {} | awk '{ total += $6 }; END { print total }' | gnumfmt --to=iec
3.8M
$ find -E . -regex ".*\.(svg|html|htm|js|css)\.gz|(.*\.webp)" -type f | xargs -I {} ls -la {} | awk '{ total += $6 }; END { print total }' | gnumfmt --to=iec
941K

圧縮アルゴリズムのすごさがよくわかりますね。
配信物を圧縮することにより通信にかかる時間を圧縮することに成功しました。

しかし、これだけでは不十分でした。
例えば今回css上でttfファイルを呼び出しています。
ttfファイルのダウンロードを開始するにはhtmlをレンダリングしてjsをレンダリングしてcssを解釈してttfファイルをダウンロードするということになります。
これではただでさえ容量が大きいttfファイル(1.5MB)をダウンロードする時間が足を引っ張りレンダリングを遅く知ってしまうことに繋がります。
なので我々はttfファイルをpreloadすることにしました。
preloadすることによりhtmlのダウンロードが終了してhtmlの解釈が完了すると同時にttfファイルがダウンロードされるようになりました。
cssの解釈を待たなくて良いのです。
基本的に画像なども同様の方法を利用することでブロック時間を短縮できます。
素晴らしいですね。

違うドメイン名のサーバーから取得する資産のダウンロードが遅いという問題があります。
すでに接続済みのサーバーから取得する場合は名前解決の時間やTLSのセットアップにかかる時間が省略されます。
しかし違うドメイン名のサーバーに初めてアクセスするときは時間がかかってしまいます。
予め <link rel="preconnect"> をしておくことにより解決しました。
おおよそ15ms前後時間を短くすることができます。

これらのことを改善することではじめのhtmlのダウンロードの直後レンダリングが開始されるきれいな構成になりました。

やったこと3: レスポンスを待たないUIにする【yamitani】

全ての要素を自分で0から構築してないですが、今回はfastly以外は全て触れてました。
各所メンバーのみなさんが実装してくださった部分のつなぎ込みが主な役割でした。
gRPC-Webも挑戦しようと思いましたが、速さを追求するために、普段はやらないサーバーからのレスポンスを置き去りすることにしました。
レスポンスを待たないUIになることで、画面がサクサク動いて非常に気持ちよかったです。

やったこと4: Twitter上でOGPを表示させる【yamitani】

Twitter上でのOGP画像の表示が苦労しました。

当初の計画ではSPA前提の構成で設計していたので、エイプリルフール前日の検証でTwitterのBotがjsのレンダリングに対応していないことにリリース4時間前に気づきました。
リリース時点ではOGP画像の表示対応が間に合わず、後から対応することにしました。

Twitter上でOGP画像を表示するために検討したこと。 1. rendertron など Headless Chromeでjsレンダリングにした結果を返す 2. Nuxt SSR対応 3. 口コミ投稿時にHTMLファイルを生成する

3は画面表示後に自動遷移させる必要があったのでやめました。

リリース初期はTwitterBotのみHTMLのレンダリング結果を返す1番で対応していたのですが、動作が不安定でした。
サーバーのメモリを食ったり、レスポンスの遅延が発生してOGPが表示されなかったりしました。
最終的には2番で対応することでOGPの表示が安定される状態まで持って行きました。
SSRだとUniversal JavaScriptに気を付けなくてはならないので全てSPAで対応したかった。
TwitterやFacebookなどCrawler Botを考慮するならまだまだSSR対応は必要ということがわかりました。
全てのBotがGoogleのBotみたいだと思わない方が良いということが知見になりました。

Nginxの設定

    location / {
        if ( $http_user_agent ~* "Twitterbot/1.0" ) {
            proxy_pass http://localhost:xxxx; # Nuxt SSR用のサーバー
        }

        if ( $http_user_agent !~* "Twitterbot/1.0" ) {
            root /path/to/src; # SPA静的ファイル
        }
    }

ローカルでの検証

curl -L -v -A 'Twitterbot/1.0' 'site_url'

感想

山内

最初はとりあえずfastlyを導入しておけば速くなるだろうって思っていました。
しかし日本で有数の速さを持つサイトに追いつこうと考えると上から下のレイヤーすべてを考える事になり楽しかったです。
たかがエイプリールフールの遊びと思わず本気で楽しめましたし、普段の本番環境でいきなり導入しづらい技術も試せたので有意義な施策だと思いました。

yamitani

SPAはサーバに負荷をかけない(fastlyで完結)BotのみSSR対応すればいいことがわかった。
業務ではAWSを導入しているので、GKEの管理画面を触るなど新鮮だった。
データ連携のつなぎ込み部分を担当していたので、色々な技術要素を幅広くさわれた。
技術的に遊ぶというお題目は十分達成できた!!

普段の業務では恒常的に安定してい運用できるようにしないといけないので、いろんな制約が吹っ飛んだ状態でシステム開発をすると楽しいですね。
みなさんもシステム開発を楽しもう!!

おわりに

転職会議ではマイクロサービス化を進めていることもあり、新技術を小さく試し、徐々に展開していく文化が根付いています。
今回は期間限定企画ということでかなり極端な例になりましたが、技術も楽しみながら社会のためになるサービスを作っていこうと日々奮闘しています。
少しでもご興味があれば、こちらLivesense Engineering Contact からお気軽にお問い合わせください!

まだ間に合う!今すぐ転生しろ!

次回フロント編は後日お楽しみください。