MongoDBの無駄なスペースを削減する戦略:フィールド名の短縮とネームストアによるトークン化

Appboyは、モバイルアプリ向けの世界有数のマーケティングオートメーションプラットフォームです。当社では毎月数十億規模のデータポイントを収集しており、顧客のモバイルアプリ上でのユーザーの行動を追跡し、その行動や属性情報に基づいて、メール、プッシュ通知、アプリ内メッセージによるターゲティングを可能にしています。データベース基盤の大部分はMongoDBが支えており、ObjectRocket上で複数のクラスタにまたがる数十のシャードを運用しています。
短いフィールド名による基本戦略
MongoDBにおける一般的なパフォーマンス最適化手法の一つが、ドキュメント内で短いフィールド名を使うことです。つまり、次のようなドキュメントではなく…
{first_name: "Jon", last_name: "Hyman"}…より短いフィールド名を使い、次のようにするということです。
{fn: "Jon", ln: "Hyman"}MongoDBにはカラムや事前定義スキーマの概念がないため、フィールド名はデータベース内のすべてのドキュメントで重複して格納されます。ここが短いフィールド名が有効である理由です。仮に「first_name」フィールドを持つドキュメントが100万件あれば、同じ文字列を100万回保存していることになります。これにより1ドキュメントあたりのサイズが増大し、結果としてメモリに載せられるドキュメント数が減少します。大規模環境では、MongoDBが読み取り時にドキュメントをメモリへマッピングする仕組み上、パフォーマンスにも多少の影響が出かねません。
カスタム属性がもたらす課題
イベントデータの収集に加え、Appboyでは顧客が各ユーザーに対して「カスタム属性」を保存することもできます。例えば、スポーツアプリならユーザーの「お気に入り選手」を、ニュースアプリなら「年間購読者かどうか」を保存したいといったケースです。Appboyでは、追跡対象アプリのエンドユーザーごとにドキュメントを作成し、氏名などの基本フィールドとともにこれらのカスタム属性を格納しています。
容量を節約しパフォーマンスを高めるため、ドキュメントに保存するすべてのフィールド名を短縮しています。事前に分かっているフィールド(名前、メールアドレス、性別など)については独自のエイリアスを定義できます(例:「fn」=「first name」)。ところが、顧客が登録するカスタム属性の名前までは予測できません。もし顧客が「supercalifragilisticexpialidocious」のような長い名前のカスタム属性を作った場合、それを全ドキュメントに保存したくはありません。
「ネームストア」によるトークン化
この問題を解決するのが、私たちが「ネームストア」と呼ぶ仕組みによるカスタム属性フィールド名のトークン化です。これは要するにMongoDB内の一つのドキュメントで、「Favorite Player」のような値を、一意かつ予測可能な極めて短い文字列にマッピングします。しかも、このマップはMongoDBのアトミック演算子だけで生成できます。
ネームストアのドキュメントスキーマは非常にシンプルです。顧客ごとに1つのドキュメントを持ち、各ドキュメントには「list」という配列フィールドが1つあるだけです。この配列にカスタム属性の値をすべて格納し、特定の文字列のインデックスがそのままトークンになります。「Favorite Player」を短くて予測可能なフィールド名に変換したいときは、「list」配列内の位置を確認するだけです。存在しなければ、アトミックなpush操作で配列末尾に要素を追加し(db.custom_attribute_name_stores.update({_id : X, list: {$ne : "Favorite Player"}}, {$push: {list: "Favorite Player"}}))、ドキュメントを再読み込みしてインデックスを特定します。理想を言えば$addToSetを使いたかったところですが、$addToSetは順序を保証しない一方、$pushはデフォルトで末尾に追加すると公式ドキュメントに明記されているためです。
こうして「Favorite Player」のような値を整数値に変換できます。その値が1だとすると、ユーザードキュメントは次のようになります。
{
fn: "Jon",
ln: "Hyman",
custom: {
1: "LeBron James"
}
}フィールド名が短くすっきりしました。この手法には嬉しい副作用もあります。ドル記号($)やピリオド(.)など、MongoDBがエスケープなしでは扱えない文字を顧客が使っても、心配する必要がなくなるのです。
ドキュメントの無制限な成長への対策
MongoDBは絶えず成長し続けるドキュメントを推奨していないため、「ネームストアのドキュメントが無制限に肥大化するのでは?」と思った方もいるでしょう。実際、私たちは実装を少し拡張し、顧客ごとに複数のドキュメントを保持できるようにしています。これにより、新しいドキュメントを生成するまでの配列要素数に妥当な上限を設けられます。しかも、これもすべてMongoDBの機能だけでアトミックに実現できるのがポイントです。
具体的には、各ドキュメントに「leastvalue」というフィールドを追加します。このフィールドは、そのドキュメントが作成される前に、先行するドキュメント群に追加済みの要素数を表します。例えば「leastvalue」が100で「list」が["Season Ticket Holder", "Favorite Player"]のドキュメントがあれば、「Favorite Player」のトークン値は101です(ゼロ始まりのインデックスを採用)。この例では、「list」配列に100個の値を格納した時点で新しいドキュメントを作成します。挿入時にはpush操作を少し改変し、「leastvalue」が最大のドキュメントを対象にすると同時に、「list.99」が存在しないこと(=インデックス99に要素がないこと)を条件にします。その位置に既存要素があれば、push操作は何もしません。その場合は、全ドキュメントの要素総数を「least_value」とする新しいネームストアドキュメントを作成する必要があります。アトミックな$findAndModifyを使えば、新ドキュメントが存在しなければ作成し、取得してから$pushを再試行できます。
トークンのキャッシュと応用
顧客が多数のカスタム属性を持つ場合、値からトークンへの変換のためにネームストアの全ドキュメントを読み出すのは、帯域幅と処理コストの両面で負担になります。しかし、一度計算されたフィールドのトークン値は不変であるため、翻訳を高速化するためにトークンをキャッシュしています。
この「ネームストアトークン」のパラダイムは、アプリケーションのさまざまな箇所に適用されており、柔軟なスキーマを維持したままフィールド名サイズを削減できています。実は値にも応用できます。例えば、ラジオ局アプリが「ユーザーがよく聴く上位50アーティスト」の配列をカスタム属性として保存するケースを考えてみましょう。50個の文字列からなる配列の代わりに、局名をトークン化して50個の整数の配列として保存できます。特定のアーティストが好きなユーザーを検索するには、フィールド名と値の2回のトークン参照が必要になりますが、値からトークンへの変換をキャッシュしているため、キャッシュ層でマルチゲットを使えば、いくら値が増えてもキャッシュへの往復は1回で済みます。
まとめ
この最適化は確かに間接参照と複雑さを招きますが、Appboyのように数億人規模のユーザーデータを扱うなら十分に価値があります。このテクニックのおかげで、高価なSSD容量を数百ギガバイトも節約できました。
さらに詳しく知りたい方は、9月18日にCiprianiで開催されるRackspace Solve NYCカンファレンスにて、AppboyにおけるDevOpsについてお話ししますので、ぜひご参加ください。
-
MongoDBの主なユースケースとは?顧客分析からリアルタイム統合まで徹底解説
本記事は、2018年6月13日にObjectRocket.com/blogで公開された記事をもとにしています。 一貫して優れたカスタマーエクスペリエンス(顧客体験)を提供することは、今や多くの企業にとって重要な課題となっています。ここ数年で「良い顧客体験」に対する期待値は劇的に高まり、かつては斬新だと感じられたものが、今では当たり前の標準になっています。こうした素晴らしい体験を実現する鍵を握っているのが、データの集約です。 本記事では、MongoDB®の最も一般的なユースケースと、それらの選択に影響を与えるさまざまな特性について解説します。 顧客分析 データ集約は、優れた顧客体験を作り出すた
-
MongoDBのディスク使用量を理解する――領域割り当ての仕組みと最適化の判断基準
はじめにMongoDBを使い始めたばかりの方にとって、そのディスク(スペース)使用量は一見すると分かりにくいものです。本記事では、MongoDBがどのようにディスク領域を割り当てるのか、そしてObjectRocketダッシュボードに表示される使用量情報をどう読み解けばよいのかを解説します。これにより、インスタンスのコンパクション(最適化)が必要なタイミングや、シャードを追加して利用可能領域を拡張すべきタイミングを適切に判断できるようになります。検証環境:5GBシングルシャードのMediumインスタンスまず、5GBのシャード1つで構成された真っさらなMediumインスタンスを用意します。このイン