Google Cloud Platform

Fastly の導入事例 : 履歴統計 DB を MySQL から Cloud Bigtable にダウンタイムなしで移行

(この記事では、米 Fastly から許可をいただき、先日同社が公開したブログ記事 How we moved our Historical Stats from MySQL to Bigtable with zero downtime の日本語訳を掲載します)

- By Toru Maesaka, Senior Software Engineer at Fastly

過去から学ぶことは意思決定に不可欠なステップの 1 つです。私たち Fastly は、お客様が過去のイベントに基づいて迅速かつ的確に決定を行えるよう支援するため、Historical Stats API を提供しています。この API を使用すれば、分や時間、日単位でキャッシュに関するあらゆる統計情報を取得できます。これらの履歴統計はイベントに関する重要な洞察を提供し、将来の決定のよりどころとなります。

このブログ記事では、私たちがどのようにして Historical Stats API のデータベースを、自社で管理する MySQL から Google Cloud Bigtable に、ダウンタイムやサービスに悪影響を及ぼすことなく移行したかを詳しく説明します。

優先事項と技術

Historical Stats API(とそのグローバル分散パイプライン)の開発における多くの技術的課題の 1 つが、高速なアクセスのために、膨大な量の小さなデータを処理、保存、整理する必要があることです。

初期のアーキテクチャでは、履歴統計を保存して提供するにあたって、私たちは MySQL ベースのデータ サービスを構築することを選びました。MySQL/InnoDB を選択したのは自然な流れだったのです。強力なエコシステムが存在するのに加え、私たちはこのソフトウェアに精通していました。それは今も変わっておらず、私たちは他の多くのサービスで MySQL を運用しています。

では、なぜ履歴統計データを MySQL から移行したのでしょうか。最初のアーキテクチャはうまく機能しましたが、このアーキテクチャでは規模の拡大に追いつかなくなることが私たちにはわかっていました。データ量が増加の一途をたどり、物理的な限界(ストレージ容量など)にぶつかろうとしていましたし、そのせいでスキーマの変更がやりづらくなっていたのです。

私たちは決断を迫られました。MySQL ベースの統計インフラストラクチャをこのまま拡張し続けるか、それともクラウド データベース プロバイダーなど別の解決策を探すのか。

魅力的なのは後者でした。基盤となるストレージ メカニズムの管理をプロバイダーに任せれば、私たちは分析プロダクトの拡充に集中でき、それは結果として、より大きな価値を顧客に提供することにつながるからです。

最終的に、私たちは Google Cloud Bigtable に白羽の矢を立てました。大量のデータ(時系列データなど)を扱いやすい特性や、スパース テーブル設計、動的ノード メカニズムによるスケーラビリティが決め手となりました。

また、Google Cloud Platform(GCP)も全体として理にかなっていました。コンプライアンスを厳格にサポートしており、Fastly と GCP のグローバルな相互接続も確保できるからです。

Cloud Bigtable は、GCP で提供されている超スケーラブルな多次元ワイドカラム データベース サービスです。このブログ記事は Cloud Bigtable 自体に関するものではないので、その技術的な詳細についてはここでは触れません。Bigtable の設計や低層の詳細に興味がある方は、こちらの面白い論文をご覧ください。

Fastly の Historical Stats API とは

履歴統計は、リクエスト数、帯域幅使用量、ヒット率などの集計データから構成され、トラフィックの包括的な概要を提供します。個人情報やリモート ログ ストリーミング データは一切含まれません。

パブリックな Historical Stats API は、別のサービスから履歴データを取り出す読み取り専用の Ruby アプリケーションによって提供されます。一方、新規データの追加は、私たちのグローバル分散統計パイプラインから受信されるメッセージを取り込んで処理する専用サービスによって実行されます(実際はもっと多くのことが行われていますが、ポイントをつかめるようにここでは割愛します)。

私たちは基本的に、Bigtable とグローバル統計パイプラインを接続するもう 1 つのパイプを作成するとともに、私たちのフロントエンド Ruby アプリケーションが、サービスごとやデータセンターごとなどさまざまな方法で、Bigtable から容易にデータを引き出せるようにする抽象化機能を開発する必要がありました。何とかなりそうですね。さらに見ていきましょう。

潜在的な問題をチェック

従来のデータ サービスから新しいサービスに切り替えて、うまくいくように願うだけでは無責任です。私たちは、この移行によってお客様へのサービスに障害が発生しないように、以下をはじめ、さまざまな項目を考慮し、対処しました。

  • Bigtable 内のデータは MySQL と一致しているか
  • Historical Stats API からのレスポンスは正確か
  • 統計パイプラインで障害が発生すると、何が起こるか
  • 新システムのパフォーマンス特性は許容範囲か
  • エッジ ケースを見落とした可能性はないか
  • 私たちのシステムが行っている監視で異変を発見できるか

大規模な正確性テスト

Bigtable と MySQL でデータが一致していることを確認するため、私たちは複数の検証プログラムを開発し、それらを個別に実行しました。その 1 つはデータベース レイヤーで定期的に実行され、Bigtable、MySQL、他のデータベース内の行を比較します。

もう 1 つの検証プログラムはウェブ API レイヤーで実行され、すべての Historical Stats API リクエストが非同期検証ワーカーに渡されます。この検証ワーカーは個々のリクエストに応じて、Bigtable と MySQL をそれぞれ使って処理を行わせ、そのレスポンスを比較します。検証ワーカーの介在がリクエストのライフサイクルに悪影響を与えることはありません。

不一致が見つかると、検証ワーカーはそのイベントのログをデータ アナリティクス プラットフォームに記録します。これによってデータの不一致がリアルタイムに発見されるとともに、後で分析されるのです。私たちの API フロントエンドは Ruby アプリケーションなので、GitHub が開発・公開している scientist という gem(Ruby ライブラリ)を検証ワーカーで利用しました。このワーカーのおかげで、データの検証を楽に試すことができました。

私たちは数か月間にわたってこの “シャドー” 検証プロセスを実行し、その後で新しいデータ サービスに対する本番リクエストの実行を開始しました。このアプローチはお勧めです。予想外のコンピューティング コストなど、さまざまな興味深い事実を発見できるからです。

たとえば、Bigtable でのレスポンス処理に要する時間の大部分がネットワーク レイテンシに起因していたことが、アプリケーション パフォーマンスの監視によって判明しました。このことから、Stats API サーバーを Bigtable の近くに移す必要があることもわかりました。本番環境には固有の要素があり、それは多くのことを教えてくれます。

API ルーティングを徐々にカットオーバー

数か月にわたるテストのおかげで、私たちは Bigtable 環境(と関連ソフトウェア)で本番リクエストを処理する準備が整っていることに比較的自信を持っていました。それでも私たちは入念な注意を払い、一度に 1 つか 2 つの API ルーティングだけをカットオーバーすることにしました。影響範囲を絞り、動作確認に集中するためです。

こうしてカットオーバーを部分的に行っていくアプローチは、面倒のようにも聞こえます。ですが、VCL(Varnish Configuration Language)でリクエストのルーティングを簡単に構成できるので、非常にスムーズに進みました。概要がわかるように、非常にシンプルな例を以下に示します。

api_nodeapi_experimental_node という 2 つのバックエンドがあるとします。

  backend api_node {
  .host = "127.0.0.1";
  .port = "443";
  … snip ...  
}
 
backend api_experimental_node {
  .host = "127.0.0.2";
  .port = "443";
  … snip ...
}

リクエストのルーティングにおける 1 つの選択肢は、リクエスト パスをチェックし、特定のリソースに対する API リクエストを実験ノードに振り向けることです。

  sub vcl_recv {
  if (req.url ~ "^/path/to/resource") {
    set req.backend = api_experimental_node;
  } else { 
    set req.backend = api_node;
  }
}

同様に、リクエストに特定の HTTP ヘッダが含まれる場合は、リクエストを実験ノードに振ることも可能です。これは、エンドツーエンドのデバッグを行うときに特に便利です。

  sub vcl_recv {
  if (req.http.Api-Debug ~ "experimental" || req.url ~ "^/path/to/resource") {
    set req.backend = api_experimental_node;
  } else { 
    set req.backend = api_node;
  }
}

VCL に精通した読者の方は「パフォーマンスや冗長性のために複数の API ノードがある場合はどうなるのか(こうしたケースはよくあります)」と思われるかもしれませんが、心配は無用です。そうした場合でも構成するのは簡単です。負荷分散構成の微調整に関する私たちのドキュメントをご覧ください

教訓 : 大胆に、そして戦略的に考える

アクティブ サービスの永続データベースを、ダウンタイムやサービス中断を発生させることなく切り替えるのは、難しいだけでなく、忍耐や多くのテストが必要なプロジェクトです。これを適切に実行するのは、古い格言のとおり「言うは易く行うは難し」にほかなりません。このブログ記事で紹介した方法論は、私たちにとって有用なものでした。皆さんが同様の問題に取り組もうとするときにお役に立てることを願っています。

MySQL から Bigtable へのデータベースの切り替えと、その新しいデータ パイプについては、これまでのところうまくいっています。データは一致しており、クライアント サービスは期待されたとおりの動作を継続しています。

とはいえ、最初からすべてが完璧だったと言えばうそになります。私たちはこれまでさまざまな微調整を行ってきました。たとえば、スループットを高めるために、Bigtable に書き込む方法を考え直したり、設計し直したりしました。

私たちはこうした低レベルの作業で学んだことや改良した成果についても、今後のブログ記事で取り上げたいと考えています。どうぞお楽しみに!

Historical Stats API の詳細は、こちらのドキュメントをご覧ください。私たちの Real-Time Analytics API もぜひお試しください。

* この投稿は米国時間 7 月 6 日、Fastly の Senior Software Engineer である Toru Maesaka 氏によって投稿されたもの(投稿はこちら)の抄訳です。