ゲーム攻略メディア「神ゲー攻略」の記事配信システムを、五年の歴史がある SSG から二年の歴史がある lit-html による SSR にリプレイスした話

VOYAGE Lighthouse Studio の海老原 (@co3k) です。

ゲーム攻略メディア「神ゲー攻略」の記事は、これまで SSG (Static Site Generator; 静的サイトジェネレータ) を用いて構築、配信されていました。 このたび、従来の SSG を活用した記事配信の仕組みから、 SSR (Server Side Rendering) による仕組みにリプレイスしていくことにしました。

本記事では、そうした新しい記事配信システムの詳細と、移行にまつわる工夫や苦労話などについてご紹介します。

[PR] 本エントリをお読みいただく前に

そもそもリプレイス前の構成ってどんな感じだったの? というか「神ゲー攻略」って何? みたいなのが気になって記事が読み進められないかも〜とご心配の方に耳寄りな情報です。

実は「神ゲー攻略」の事業やシステム構成については『Engineers in VOYAGEー事業をエンジニアリングする技術者たち』の第四章にて、 t-wada さんインタビューのもと、事業背景から技術戦略に至るまで、たっぷりとお話をさせていただいています。

そういった部分も含めてガッツリ理解したいよーという方におかれましては、是非書籍をお買い求めいただければ、この記事を 360° お楽しみいただけると確信しております。

『Engineers in VOYAGEー事業をエンジニアリングする技術者たち』は、私たちのチーム以外にも、 VOYAGE GROUP の大小様々なチームにおける赤裸々な話に触れられる、とても面白い書籍なので、もしまだお読みでない方はこの機会にお手に取られてはいかがでしょうか。

もちろん、本記事は書籍をお持ちでない方でも問題なくお楽しみいただけます!

記事配信システム構成図

まずは全体像を把握していただくために、神ゲー攻略の記事配信システムに関する構成図をババーンとお見せいたします。

f:id:co3k:20201022101026p:plain
神ゲー攻略記事配信システム構成図

図において、青字で解説しているのが旧配信システムに関するものです。

旧配信システムの流れを一言で表現すると、「SSG によって構築された HTML を Amazon S3 に置いて Amazon CloudFront で配信している」です。旧配信システムについては、前述の『Engineers in VOYAGEー事業をエンジニアリングする技術者たち』の第四章で詳しく述べていますので、是非ご一読いただければと思います!

赤字で解説しているものが、本記事においてこれから述べる新配信システムに関するものとなります。

書籍のなかで詳しく説明しているとおりとなりますが、私たちのサービスは検索エンジンからの流入を中心としているため、 SEO (Search Engine Optimization; 検索エンジン最適化) や応答性能などは最大の関心事です。また、事業展開上、旧配信システムにおける簡易なアクセス制御では不足するような事態が出てきていました。新配信システムにおいては、これら要件を満たすための趣向が凝らされています。それぞれについて見ていきましょう。

新配信システムの特徴その 1: lit-html を利用した SSR

旧配信システムは完全なる SSG でしたが、新配信システムは SPA (Single Page Application) に生まれかわっています。これはできるだけ記事のみを無駄なく配信することによるユーザ体験の向上と、転送量等の削減によるコストメリットを狙ってのことです。

さて、 SPA には SEO において懸念があることもよく知られています。

たとえば Google の検索エンジンのボットは JavaScript を解釈しますが、それはレンダリングフェーズにおける話で、クロールフェーズにおいては HTML のみが解釈されます。レンダリングはクロールとは別なキューによって改めて処理されますから、 SPA 技術によって構築されたページがインデックスされるまでには、それぞれの順番待ちの時間が生じることになります。

以下は JavaScript SEO の基本を理解する | Google 検索デベロッパー ガイド | Google Developers 内、「Googlebot が JavaScript を処理する仕組み」 にて紹介されている図となります。この図をご覧いただければどういうことなのか一目瞭然だと思います。

Googlebot が JavaScript を処理してインデックスするまでの図

先述の通り、私たちのサービスは検索エンジンからの流入を中心としており、かつ、競合他社様との競争力も求められることから、こうしたタイムラグは事業的に見て致命傷となりかねません 1

ですから SSR あるいは Dynamic Rendering といった選択肢が検討の俎上に置かれることとなります。しかしながら、

  • Dynamic Rendering はヘッドレスブラウザを用いたレンダリングをおこなう
  • SSR は DOM レンダリングなどをおこなう

という特性から、従来型のウェブアプリケーションに比べ、サーバに求められる性能要件が跳ね上がります。

これらのソリューションはメモリをそれなりに要求しがちです。ウェブサーバ (ないし Dynamic Rendering をおこなうためのリバースプロキシ) のプロセスあたりに要求されるメモリ使用量が大きくなるということは、ひとつのサーバインスタンスで捌けるリクエスト数が少なくなるというトレードオフを覚悟することになります。また、私たちのように Google App Engine を用いる場合は、デフォルトの F1 インスタンスでは不充分で、少なくとも F2 以上のインスタンスクラスでなければたいていの場合まともに動作させられないでしょう。低く見積もって F2 インスタンスを用いるのだとしても、 F1 インスタンスと比べてかかるコストは倍になります。これは PV あたりの運用コストに関わりますし、ひいては収益性に直結します。

幸いなことに、私たちのサイト構成はとてもシンプルです。記事数こそ数十万、月間 PV 数は数億という規模のサービスではあるものの、このシステムで配信されるページは概ね以下の数パターンしかありません。

  • 記事詳細ページ (トップページも含む)
  • 記事一覧ページ
  • その他システムで使うページ (エラー画面など)

これくらいの複雑さのアプリケーションであれば、 React や Vue.js などのメジャーなフレームワークを利用する以外にも、 lit-html が検討候補にあがってくるかと思います。 lit-html は端的に言えば DOM の差分更新をサポートした軽量なテンプレートエンジンです。コンポーネント機構は有しませんが、充分に小さいライブラリなので、ブラウザネイティブの Web Components と組み合わせて利用することができます。

ということで私たちもご多分に漏れず lit-html と Web Components を組み合わせつつ、状態管理を Redux に任せるような形で SPA を実現しています。

さて本題の SSR ですが、 lit-html それ自体は現行バージョンにおいては SSR をサポートしていません。「現行バージョンにおいては」、と書いたのは、次のメジャーリリースである lit-html 2.0 において SSR 可能になるような改良がおこなわれるからです。素晴らしい! ……ですが私たちは未来に生きていないので、どうにかして今すぐに lit-html で SSR したいよーというワガママニーズを満たす必要があります。

さて先ほど lit-html は「軽量なテンプレートエンジン」である、と言いました。 lit-html の解釈するテンプレートは以下のような単純な Tagged Template Literals によって実現されます。

const template = html`<p>Hello, ${name}!</p>`;

現行の lit-html は DOM 実装なしにサーバ上に持っていって動くような代物ではありませんが、 このテンプレート自体を解釈して DOM なしで処理をするような互換実装さえあれば、クライアントとほとんど同様の出力内容をサーバ上で構築することができます。

f:id:co3k:20201023065143p:plain

で、ありがたいことにそのような互換実装が (サードパーティのものですが) あります。 https://github.com/popeindustries/lit-html-server

この実装によってサーバ側での DOM 実装の導入などなしに SSR が実現できる (なぜなら単純な文字列処理に落ちるので) ので、お安く SSR することが可能となります。 もちろん、テンプレート以外にも、たとえばルーティングを router5 のような universal なライブラリによって、クライアント側とサーバ側で共通のルールで処理をさせるなど、クライアント側処理とサーバ側処理の乖離を小さく留めるように工夫しています。

新配信システムの特徴その 2: RDBMS まで来たら負け

新配信システムではなんと RDBMS に記事データを蓄積するようになりました!!!! 五年の時を経てついに普通の Web アプリケーションになった!!!!

例によってフルマネージドなサービスということで、 Google Cloud SQL 上で動かされる MySQL サーバを使っています。

ですが、エンドユーザから参照されるデータの大部分は Google Cloud Datastore から取得するようにしています。 Google Cloud Datastore は高い性能を誇るフルマネージドな NoSQL データベースサービスです。

実は Google Cloud Datastore については、静的な記事ページに動的に埋め込まれるコメント機能のサーバ側実装 2 で既に使い倒しており、その性能は折り紙付きでした。極めて高いスケーラビリティと応答性能、そして強力な可用性は、私たちのような高トラフィックサービスのバックエンドとして据えるにふさわしいものです。

一方で、 Datastore 内のデータをマスタデータとはしていません。マスタデータはあくまで RDBMS に置かれます。これはコメントサーバを実装、運用するなかでの反省から来たものです。

Datastore が Datastore であるために、すなわちその性能要件を維持するため、たとえば RDBMS で可能となる柔軟な条件による結果セットのフィルタリングなどはおこないにくいといった制約が存在します。

たいていの場合、この制約に合わせてアプリケーションを設計、実装することは可能です。しかし、管理画面など、運用上の要求により、柔軟な、時としてアドホックな条件でのデータアクセスが想定されるような場合には、こうした制約がネックとなってきます。コメントサーバにおいては、実際にこれが問題となり、管理上の柔軟性を欠いてしまっている部分があります。

ですので、マスタデータは MySQL 上に、そこから公開用データのみを Datastore 上にそれぞれ格納することで、用等に応じて両データベースを使い分け、双方の利点を享受できるようにしています。

これはパフォーマンス、運用上の柔軟性の獲得といったメリットだけでなく、セキュリティ上の利点ももたらします。

記事のメタデータのなかには編集上のためだけに有用な、公開に差し支えるような情報も含まれます(たとえば、編集担当者のアカウント名など)。このような情報がアプリケーションロジックの問題によって、 API や HTML 出力などを通して誤って公開されてしまうような事態はなんとしても避けねばなりません。あらかじめ公開を意図したフィールドのみに限定したスキーマを持つデータとして射影して格納しておけば、アプリケーションロジックの欠陥によってこれらの情報が誤って公開状態に置かれる可能性を大きく減じられます。

また、 Datastore に至る前には memcached で、さらにその手前では高いキャッシュヒット率 (なんと 90% オーバー) を誇るエッジサーバキャッシュによって、大半のリクエストを処理することができるようにしています。その結果、サイト規模の割に RDBMS への参照がおこなわれることはほとんどありません。そうしたこともあって、エンドユーザのリクエストが RDBMS に来たら「負け」、という思想の元でアプリケーションを組んでいます。

実は、なんと Cloud SQL に関しては一番低いインスタンスタイプで運用されています。チューニングもぜんぜん頑張っていません。にもかかわらず Cloud SQL の CPU 使用率は少ない値を維持し続けており、この工夫が少なくともいまのところはうまくワークしている様子がうかがえます。

f:id:co3k:20201023065202p:plain

新配信システムの特徴その 3: 管理機能は RBAC (ロールベースアクセス制御) による権限管理を実施

攻略サイトで扱う攻略情報のなかには、ゲームのデベロッパ様やパブリッシャ様自身の監修を受けて公開するものも存在します。 そういった情報は、たとえば特定の公開日時までは関係者外秘となるような情報であったりするわけです。

神ゲー攻略の認知度がありがたいことに高まってきたおかげもあり、こうした形態の攻略情報を扱わせていただくことも増えてきましたが、これは旧配信システムで実現するのが(システム的には)難しい要件のひとつでした。旧配信システムは大ざっぱに公開用バケットと非公開用バケット、二種類のバケットに対するデプロイにしか対応しておらず、「特定の関係者のみに公開する情報」や「特定種類の情報しか閲覧できない関係者」などといったきめ細やかなアクセスコントロールは残念ながら実現することはできませんでした。

新配信システムにおいてはこの課題を払拭するために RBAC (ロールベースアクセス制御) による権限管理を実施しています。 これによって、以下の図のように(わかりにくいかもごめんなさい)、個々のロールに対して複雑な権限設定を付与するというのが簡単におこなえるようになっています。

f:id:co3k:20201023065217p:plain

ちなみに、 RBAC の実現にあたっては Casbin というライブラリを用いています。

実装コードの具体的な内容は Casbin 自体の知識がかなり要求されてしまいますので割愛しますが、上の図のようなことをおこなう Negroni の middleware を用意していて、各 API のエンドポイントよりも手前で処理されるようにしています。

リプレイス戦略——いきなり移行はせず、新配信システムを、独立したサブシステムとして稼働させる

さて、こうした機能を備えた新配信システムにリプレイスしていくわけですが、なにぶん大きなアーキテクチャ変更を伴うリプレイスですので、やはりというか何というか一筋縄ではいきません。

旧配信システムはだいぶガタが来ているとはいえ、五年もの歴史があります。安定性や稼働率も抜群ですし、現場で運用するうえでの工夫もたくさん詰まっています。これを一朝一夕でまるごとすげ替えることはできません。いえ、表面上はできるかもしれませんが、配信性能や記事品質など、私たちが大事にしている多くのものをしばらくの間犠牲にすることになるでしょう。

ではどうしたかというと、最初は「リプレイスをしない」という決断をおこないました。

リプレイスは決して簡単な道のりではありませんから、旧配信システムとはまた別な形での記事配信システムとして新配信システムを立ちあげ、新規に構築するタイトルの一部や、新配信システムによって新たにもたらされる管理機能などが有効に働くゲームタイトルの一部などで実験的かつ段階的に導入をおこない、編集部側で運用するにあたっての知見やフィードバックの収集、開発側での技術やシステム運用に関するノウハウの蓄積、そしてもちろんバグの洗い出しと修正や、徐々に増えていく負荷にあたって支障が出てこないかなどを焦らず慎重に進めていきました。

どのくらい慎重かというと、実は 2 年前に書いたエントリ、スキーマ定義言語 Protocol Buffers と protoc-gen-swagger を使って Web API のスキマを埋めようにて触れている「とある派生サービス」というのが、この新配信システムのことだったのです。

な、なんだってーっ(AA 略)。

ということで、新配信システムは、実際に旧配信システムからリプレイスされるまでに、 2 年近く battle-tested な状態に置かれていたということです。これだけの期間があれば、わかりやすいバグは潰れているし、運用側での知見もだいぶ蓄積されています。どこの馬の骨ともつかないシステムに、開発と編集の両方が振り回され、高い品質の記事を安定した配信基盤に載せて素早く出す、という目的が達成できない、……という事態にはならずに済みます。

そうそう、要するにこの戦略は、あたかも一個のサービスの看板を出しておきながら、その実、二種類のシステムを抱えていて、条件に応じて切り替える、というようなモデルなんですが、これが気軽にできてしまうのは、見た目上の違いをほとんど感じさせないように作っているというものあるでしょうが、ゲーム攻略サイトならではの特徴のおかげかもしれません。ゲーム攻略サイトは「検索エンジンからの流入が中心」、「ゲームタイトルごとにユーザが分かれる」という特徴があります。特に後者が重要で、たいていの人は複数のゲームを本当の意味で同時にはプレイしません。従って『AJITO 奪還大作戦』3というゲームをプレイしているときに『Enemy in VOYAGEー船に乗り込む侵入者たち』4というゲームのことを調べたくなったりはあまりしないわけです。それぞれのゲームタイトルごとに攻略サイトが立ち上がっているというような認識をもたれるのがむしろ普通なので、それぞれが別なシステムで動いていたとしても、そもそもそんなに気にならないという土壌が既にあるのだと言えます。

よくある質問 Q1: あれ? ということは、リプレイスはまだ終わっていないってこと?

は、はい……。

旧配信システムから記事などのリソースをインポートして新配信システム上で利用できるようにする仕組みは既に用意しています。 この仕組みを利用して、 100 を超えるゲームタイトルについては、既に旧配信システムから新配信システムに切り替えて記事の配信がおこなわれています。

以下が、実際に記事をインポートしている様子です。

f:id:co3k:20201023123010p:plain

新システムにおいても、 Markdown パーサやそれに付随する CSS や JavaScript のアセット類の一部については、旧システムのものをそのまま移植しています。そのため、記事の内容に踏み込んだ変換処理などは一切おこなっていません。

旧システムは Markdown の表現力を活かしまくってメニューなどを実装しているため (なぜかというとそうするしかなかったからです)、そのあたりを新システムに適合するようにインポートするなど、小さな差異は埋めていく必要がありますが、基本的には新旧システム間でのリソースの互換性を維持するように配慮しているため、このインポート用のコマンドラインツールはもっぱらオートメーションのために機能している具合となります。

また、この切り替えについても一気にはおこなっていません。準備が整ったゲームタイトルから、新配信システムを利用する形に少しずつ移行を進めていっています。 万が一移行に際して致命的な問題が生じたとしても、影響範囲が小さく抑えられるようになっています。

ただ、運用ツールがガラリと変わってしまうため、移行については、システム側での準備だけでなく、運用側での準備が必要不可欠です。 たとえばゲーム内イベントはたくさんのユーザが訪問してくれる大事な機会なわけですが、そのようなチャンスを、私たちの都合によるシステム移行作業で逃してしまっては悔やんでも悔やみきれません。

ということで、ただいま大絶賛リプレイス進行中というわけです! えへん。

え、えへん……

……な、なんだか煮え切らない感じの締めになってしまってすみません。でもこれがありのままの現状なんです。

えっ? こういうありのままの赤裸々な話をするのにうってつけなイベントがあるって本当ですか!?

[PR] 「そーだいなるVOYAGE GROUPの裏側 #VLS 数十万記事のメディアをゼロから立ち上げる」というイベントをやります!

『Engineers in VOYAGEー事業をエンジニアリングする技術者たち』出版イベントである、大好評「そーだいなるVOYAGE GROUPの裏側」の第四弾、 VOYAGE Lighthouse Studio 編が、来る 10/26 (月) の 12:30 より(オンライン)開催されます!

https://voyagegroup.connpass.com/event/192112/

この記事の内容や書籍の第四章の内容であったり、あるいはあまり触れられていなかった部分などについて、どんどん掘り下げていくタイプのイベントとなっています。

特に、本記事においては、旧システムの問題点や、どうして新システムへのリプレイスを決断するに至ったのかについてはあまり触れませんでした。そのあたりが気になる方については、『Engineers in VOYAGEー事業をエンジニアリングする技術者たち』をお読みいただいてももちろんいいのですが、このイベントにおいても、そーだいさん(@soudai1025)にモデレータを務めていただくことで、より深く生々しい苦労話などが明らかにされるはずです。

ということで、興味を持たれた方は是非ご参加いただけると嬉しいです。質問も絶賛受け付け中ですよ!


  1. このあたりのお話はぜひ『Engineers in VOYAGEー事業をエンジニアリングする技術者たち』にてお楽しみください。

  2. 『Engineers in VOYAGEー事業をエンジニアリングする技術者たち』 132 ページ参照

  3. タイトルは架空のものです。同名の何かが万が一実在したら申し訳ございませんが、それは偶然の一致であって実在のものとは無関係です

  4. タイトルは架空のものです。同名の何かが万が一実在したら申し訳ございませんが、それは偶然の一致であって実在のものとは無関係です