100Mにスケーリング:Key-ValueストアとしてMySQLを使い、NoSQL以上のパフォーマンスを出す

MySQLはNoSQLよりも優れています。Key-ValueストアといったNoSQLのユースケースを考えてみると、パフォーマンスや使いやすさ、安定性の点でMySQLの方が合理的です。MySQLには、オペレーションや障害に関することからレプリケーションや異なる使用パターンまでと、多くのオンラインマテリアルが用意されおり、堅実なエンジンです。こういった理由から、比較するまでもなく、MySQLは最近のNoSQLエンジンよりも優れていると言えます。

ここ最近では、NoSQLエンジンが主流になってきています。多くの開発者が、MongoDBやCassandra、Redis、HadoopといったNoSQLエンジンをアプリケーション構築の第一候補としており、それらが全て昔からのSQLエンジンを上回る物と考えています。

NoSQLデータベースが選択される理由には、誇大広告であったり、「リレーショナルデータベースがNoSQLのように機能しない」という間違った憶測が要因にありますが、技術者はしばしば、オペレーション費用や安定性、成熟度といった懸念点を見落としがちです。制約や異なるNoSQL(そしてSQL)エンジンの欠陥に関する詳細については、Aphyrに掲載されているJepsenシリーズの記事をご覧ください。

この記事では、なぜ私たちが「Key-Valueユースケースで、ほとんどの専用のNoSQLエンジンよりMySQLを使用する方が良い」と考えているかを説明すると共に、ガイドラインも提示していきます。

Wixサイトの解決

誰かがWixサイトへのリンクをクリックすると、ブラウザはWixのサーバにサイトアドレスと一緒にHTTPリクエストを送信します。これは、アドレスがWixプレミアムサイトのカスタムドメイン(例えばdomain.com)であっても、Wixドメインのサブドメインにあるフリーサイト(例えばuser.wix.com/site)であっても起こります。このサーバは、送信されたサイトアドレスをもとにKey-Valueの検索を行うことで、サイトのURLを解決しなければなりません。以下の説明ではURLをルートとし表します。

ルーティングテーブルは、サイトアドレスをサイトオブジェクトに転換するのに使われます。なぜなら、サイトは複数のルートに露出することがあり、リレーションは多対一だからです。サイトが一度発見されると、アプリケーションがそれを読み込みます。サイトオブジェクトそのものは、サイトが利用する異なるサービスである子オブジェクトの2つのリストを含む複雑な構造になっています。以下は、スタンダードなSQLと正規化されたスキーマを想定した、私たちのオブジェクトのサンプルモデルです。

MySQL is better NoSQL 1

従来の正規化されたモデルでサイトを更新する場合、データの整合性が保たれることを保証するために、複数のテーブルを更新するトランザクションを行う必要があります(トランザクションは、影響を受けるテーブルへの同時書き込み/読み込みを防止するためにDBロックを使っていることに留意してください)。このモデルでは、各テーブルにシリアルキー、外部キー、そしてルートテーブル内のURLフィールドにおけるインデックスを持つことになります。

しかし、正規化されたスキーマでのモデリングにはいくつかの問題があります。

  • ロックはテーブルへのアクセスを制限するので、ハイスループットのユースケースではパフォーマンスが制限されることがある。
  • オブジェクトの読み込みでは、いくつかのSQLクエリ(この場合は4つ)またはJOINを引き起こす。これはレイテンシと密接な関係があります。
  • シリアルキーがロックを強制的に行い、書き込みのスループットを制限する。

これらの問題は結果的にMySQL(もしくは他のSQLエンジン)から得られるスループットや並行性に制限を与えることになります。このような欠点や、ユースケースが実際はkey-valueであるという事実から、多くの開発者は安定性や整合性や有用性などを犠牲にしてでも、より良いスループットや並行性を提供するNoSQLを選択しているのです。

Wixの場合では、私たちはMySQLをKey-Valueストアとして独創的に使用したとき、これがほとんどのNoSQLエンジンやMySQLの正規化されたデータモデル(上記のようなケース)よりも良い働きをすることを発見しました。簡単にMySQLをNoSQLエンジンとして使ってみます。我々の現在のシステムにはスケーリング、スループット、並行性、レイテンシ形態があり、どのNoSQLエンジンに対しても優れています。以下がいくつかのデータです。

  • 3つのデータセンターに対してアクティブ/アクティブ/アクティブ構成を設定する。
  • スループットは200,000RPM程度のオーダーの大きさ。
  • 10GBのストレージサイズに100,000,000レコード程度のルーティングテーブル。
  • 200GBのストレージサイズに100,000,000レコード程度のサイトテーブル。
  • 平均1.0~1.5ミリ秒の読み込みレイテンシの削減(実際には、1つのデータセンターでは0.2~0.3ミリ秒)。

留意しておいていただきたいのは、1.0ミリ秒程度のレイテンシは、オープンソース、クライドベースどちらの場合でも、ほとんどのKey-Valueエンジンよりも優れていると考えられています。そして、(ベーシックSQLエンジンであると思われている)MySQLでこれを実現しています。

以下が、私たちが使用している実際のスキーマです。

MySQL is better NoSQL 2

CREATE TABLE `routes` (
  `route` varchar(255) NOT NULL,
  `site_id` varchar(50) NOT NULL,
  `last_update_date` bigint NOT NULL,
  PRIMARY KEY (`key`),
  KEY (`site_id`)
)

CREATE TABLE `sites` (
  `site_id` varchar(50) NOT NULL,
  `owner_id` varchar(50) NOT NULL,
  `schema_version` varchar(10) NOT NULL DEFAULT '1.0',
  `site_data` text NOT NULL,
  `last_update_date` bigint NOT NULL,
  PRIMARY KEY (`site_id`)
) /*ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPRESSED KEY_BLOCK_SIZE=16*/;

クエリでの条件として使われていないフィールドは、単一のBLOBフィールドに入れられます(site_dataテキストフィールド)。これには、sub-objテーブルやオブジェクトテーブル自体のあらゆるフィールドも含まれます。また、私たちはシリアルキーを使わずにクライアントで生成されたGUID値であるvarchar(50)を使っていることに注目してください。これについては次のセクションで詳しく説明します。

以下は私たちが使っているクエリで、ハイスループットと低いレイテンシを持っています。

select * from sites where site_id = (
  select site_id from routes where route = ?
)

これは、まずルートテーブル上でユニークインデックスによってクエリを実行します。このクエリは1つの結果だけを返します。そして、プライマリキーでサイトを探します。そして再度、1つのレコードを探します。このネストされたクエリシンタックスは両方のSQLクエリを実行するために、データベースを一度だけ往復することを保証します。

上記にあるように、高トラフィック・高更新レートの条件下において、平均1ミリ秒以下で安定したパフォーマンスを発揮しています。更新は、トランザクションを使用しなくてもおよそトランザクション的に動作します。これは、1つのinsertステートメントにサイト全てを入力していて、ルートが入力されるまでクエリに登場しないからです。ですから、最初にサイトを入力して、次にルートを入力すれば、サイトテーブル内にオーファンデータがあるエッジケースであったとしても整合性が保たれることになります。

NoSQLエンジンとしてMySQLを使用するためのガイドライン

上記の例(そしてWixでの他のケース)から得られた経験を利用して、NoSQLエンジンとしてMySQLを使用するためのガイドラインのリストを簡単に作成しました。

NoSQLエンジンとしてMySQLを使用するときに覚えておいてほしいのは、データベースのロックや複雑なクエリの使用を避けるということです。

  • ロックを引き起こすトランザクションを使用せずに、実用的なトランザクションを使用する。
  • シリアルキーを使わない。シリアルキーはロックや複雑なアクティブ/アクティブ設定を引き起こします。
  • クライアントで生成したユニークキーを使用する。私たちはGUIDを使用しました。

読み込みに最適化なスキーマを設定する際は、以下の追加のガイドラインに沿ってください。

  • 正規化しない。
  • 既存のフィールドだけをインデックスする。インデックスにフィールドが必要ない場合は、単一のblob/テキストフィールドに保存する(JSONやXMLなど)。
  • 外部キーを使用しない。
  • クエリで1行だけを読み込むことができるスキーマを設定する。
  • alter tableコマンドを実行しない。Alter tableコマンドはロックやダウンタイムを引き起こすので、ライブマイグレーションを使用する。

データをクエリする時は以下を意識してください。

  • プライマリキーもしくはインデックスでレコードをクエリする。
  • JOINは使用しない。
  • アグリゲーションは使用しない。
  • マスタデータベース上ではなく、レプリカ上でハウスキーピング処理のクエリ(BIやデータの外挿など)を実行する。

ライブマイグレーションや実用的トランザクションについての詳しい説明は、別のブログで紹介する予定です。

まとめ

この記事で覚えておいていただきたい最も重要なことは、別の考え方もできるということです。NoSQLエンジンとしてMySQLを使うことは素晴らしいことですが、これは本来デザインされた使用方法ではありません。ここで実演したように、この例はあくまでもKey-Valueアクセスのために構築された専用のNoSQLエンジンの代わりにMySQLを使うということです。Wixでは、Key-Valueケース(その他にも共通)の選択の1つとしてMySQLエンジンを挙げています。なぜなら、使いやすく操作が簡単であり、素晴らしいエコシステムだからです。更に、ほとんどのNoSQLエンジンに勝るとも劣らないレイテンシやスループット、並行性メトリクスを提供しているのです。