blog

ストレンジャー・スクランブル(mongoDBの記事)がデータベースに関して踏んだ穴について語る

同社がmongoDBを使い始めたのは、技術的な選択ではなく、最初のゲーム"《》"の開発者がmongoDBを選んだからです。\n代理店契約後、ゲームは1年近くの共同開発期間に入りました。この間、データベ...

Jul 2, 2014 · 7 min. read
シェア

同社がmongoDBを使い始めたのは、技術を選択したからではなく、最初のゲーム 』の開発者が選んだからです。

このゲームは、代理店契約締結後、1年近くの共同開発期間に入りました。その間にデータベース関連の問題が多く発覚し、mongoDBに慣れざるを得なくなりました。その間に構築された運営基盤のデータベースには自然とmongoDBが選ばれ、メンテナンススタッフはある種のデータベースに集中できるようになりました。

ゲームの要件は変化するため、最初にデータ構造を明確に設計することは困難です。また、ゲームのプログラマーの多くは、他の分野とは異なる技術的背景を持っています。

ゲームサーバーを設計する前に、彼らはゲームのクライアントサイドを設計することに関心がありました。グラフィック、キーボードとマウスのインタラクション、UIに最も労力を費やしたのです。彼らはデータベースの使い方をあまり知りませんでした。mongoDBのようなNOSQLデータベースの出番です。mongoDBはドキュメントベースで、テーブルを設計する必要がなく、動的言語との統合がはるかに簡単です。見栄えもよく、ランダムに構造化されたデータオブジェクトをデータベースに差し込み、あとはデータベースシステムが処理してくれることを祈るだけです。もしデータベースが良い仕事をして、パフォーマンスが不足していたとしても、それはデータベースの責任であって、私の責任ではありません。レビューデータを見ると、mongoDBのパフォーマンスは素晴らしいことがわかります。

実際、どんなシステムであれ、パフォーマンスが求められる環境で完全にブラックボックスとして使うのは得策ではありません。

特にゲームではそうです。前の記事でお話ししたように、ゲームのデータの変更をすべてデータベースに放り込んで行うのは絶対に不可能です。従来のデータベースはゲーム用に設計されていません。

たとえば、選手グループの座標をデータベースに同期した場合、特定の選手の周辺にいる選手のリストをクエリできますか?mongoDBはgeoタイプを提供しており、nearまたはinfinコマンドでクエリして、周辺にいるユーザーを取得できます。しかし、10Hzの更新頻度を満たすことができるでしょうか?

選手のbuf数式を1つずつデータベースに入力し、いくつかの属性値を変更することで、buf操作で得られた結果をクエリできるようにすることは可能でしょうか?

この手の問題は非常に多いので、データベースをうまく利用する方法が見つかったとしても、その後のパフォーマンスが気になるところです。特定のデータベースサービスの中で、それらを一つ一つクリアしていくことが可能になると、最終的にデータベースはゲームサーバーになります。

最初のうちは、データベース全体がクエリのためにインデックス化されることは全くありません。データが少ないうちは、すべてのクエリがO(N)であったとしても、データベース全体をトラバースすることは問題ないでしょう。ユーザー数が増えれば、パフォーマンスがどれほど低下するか想像がつくでしょう。

そしてまた、無駄なインデックスや間違った複合インデックスを大量に使ってデータベースを構築し、システムを劣化させていました。感じとしては、性能に問題がありそうなところはどこであれ、インデックスが欠落している結果なのです。このような緊急事態は、プロジェクト開発の後期に現れやすい現象です。実際には、解決策は非常に簡単です:設計をリードする人限り、それについて考える静かな心として、データベースシステムは、実際にはデータを管理するためのクローズドモジュールです。あなたはこれらのデータを管理する場合は、どのようにデータ構造は、特定の検索を満たすために、より助長され、どのようなインデックスデータを支援するために必要です。

究極の問題はアルゴリズムとデータ構造のままですが、違いはそれを実装するのではなく、理解する必要があるということです。

また、データベースは同時並行でアクセスされるように設計されており、同時並行性は常に複雑なものです。mongoDBにはトランザクション操作がないため、ドキュメント操作のアトミック性をモデル化する必要があります。このため、経験の浅い人は間違った使い方をしがちです。

MadBladeにはバグがありました。ユーザー登録時にユーザー名を一意にしたかったので、ユーザーが登録すると、まずデータベースをチェックしてユーザー名が存在するかどうかを確認し、存在しない場合は同じ名前のユーザーを作成できるようにしました。ご想像の通り、立ち上げから1日も経たないうちに、同じ名前のユーザーが現れるでしょう。

会社のプロジェクトでskynetにmongoドライバを追加しました。正直、このドライバを実装したとき、mongoにはほとんど興味がありませんでした。結局、私が実装したのは通信プロトコルの一番下のレイヤーだけで、この部分のデザインはすでにとても醜いものでした。それでも、既製のドライバを使う代わりにこの部分を完成させるのに十分な忍耐力がありました。

公式の mongo ドライバにはソケット通信モジュールが組み込まれています。そのため、プロトコルのパース部分を別に取り出してプロジェクトの IO モデルにアタッチするのは困難です。

WildbladeサーバーはIOにboost.asioを使っていて、公式のmongoDB C++ドライバーをどう統合しているのか興味がありました。驚くなかれ、彼らはmongoのデータを処理するために別のスレッドを開き、そのスレッド全体でデータオブジェクトを送信していました。実装をよく見てみると、問題がわかりました。プログラマーはmongoDBクライアントapiの内部的な意味を誤解しやすいのです。

当初、Wildbladeの開発者は、mongoからクエリ結果のセットをフェッチした後、カーソルのfindnextの呼び出しはオブジェクトのメモリ内をイテレートするだけで、すべての結果は最初から一度に返されると考えていました。彼らはbsonオブジェクトをmongoスレッドからメインスレッドに移動させればいいと考えました。mongo は一度に一組のクエリ結果しか返しません。そして結果が反復処理されると、 findnext は自動的に新しいクエリリクエストを送信します。この時点で、オブジェクトはもう元の mongo スレッドにはありません。

C++を勉強したことがある人なら、自分が関わっていないC++プロジェクトをコードレビューしてバグを見つけるのにどれだけの労力がかかるか想像してみてください。さらに、さまざまなboost.asioコールバック関数によって引き裂かれたビジネス・プロセスも想像に加えなければなりません。そんなわけで、昨年は他の仕事をすべて中断して、何万行ものC++コードを一から読み直した時期がありました。

他人の噂話ばかりするのは見苦しいので、あなたが犯した過ちについて少し。

Stranger Scrambleの最初のサーバー事故は2014年1月中旬の週末でした。正確には、プレイヤーのデータが破損したわけでもなく、計画外の停止があったわけでもないので、運用上の大きな事故ではありませんでした。しかし、私たちはこのとき初めて、初期の設計では何かが十分に考慮されていなかったことに気づきました。

1月12日(日)。17:00頃、SA Aplyは、オペレーション用のログがオペレーション・プラットフォームに到達するまでに3時間遅延していることを発見しました。しかし、データは届いており、システムも安定していたため、調査しませんでした。

午後20時30分、プラットフォームチームのリウ・ヤンから、操作データが5時間遅れているとの報告があり、皆警戒。週末だったため、開発者たちは家に帰って休み、シャオジンが21:00にオンラインでゲームをチェックしたところ、ゲームサーバーのメモリ使用量がいつもと同じ時間帯より10Gも多く、さらに増え続けていることがわかりました。

21:00頃に電話がかかってきて、電話で話し合って分析した結果、ログデータはskynetのロギングサービスから離れて送信されており、ソケットサーバーのチェーンテーブルに滞留している可能性があると思います。コードは複雑ではなく、新しい書き込みデータの挿入はO(1)演算なので、プレイヤーのゲームをブロックする危険性はありません。また、出力ログは短期的にメモリを使い切るほど頻繁ではありません。ゲームサーバーは今のところ安全です。

午後21時40分、インシデントの原因を分析することはできませんでしたが、ただちに緊急対策が実施されました。ゲームサーバー一式が再起動され、旧サーバー上のプレイヤーの80%が新しいバックアップサーバーにオンライン誘導されました。同時に、新しいログデータベースクラスターが開始されました。計画では、月曜日まで待ち、通常のメンテナンス時間中に対処することになっていました。

午後23時、新しく起動したゲームサーバーでもログ出力の遅延が発生しました。操作ログはmongosが管理するクラスタに出力されるため、古いクラスタのインデックスのいくつかを削除しようと試みましたが、効果はありませんでした。

午前0時45分、スタンバイマシンの新しいクラスタを起動し、mongosをキャンセルして、各マシンを独立させて別々のmongoDBに接続させたところ、ようやく状況が改善しました。

上記は当時の事故記録からの抜粋。

事故の原因を完全に解明できたのは火曜日でした。

表面的には、mongosサービスにデータベースのインサートオペレーションを大量に積んでいるように見えます。この一点に負荷がかかっているのです。当初、作戦ログの出力は少し重い方で、例えば、各兵士の訓練には個別のログがあり、これはストレンジャースクランブルゲームでは巨大です。ログの一部を減らしてスリム化しても、クラッシュの根本的な原因は説明できないようです。

mongo はドキュメント中のいくつかのフィールドをシャードキーとして指定できますが、 mongos はこのキーを整数として扱い、整数の間隔に従ってドキュメントをいくつかのバケットに分割します。そしてバケットはその後ろにいるスレーブに均等に分配されます。

もしキーが規則的な数で、バケツ割り当ての公平性を損なわないために規則性が必要であれば、元のキーの選択にハッシュアルゴリズムを適用して、キーが十分にハッシュ化されるようにすることもできます。最初の段階では、キーは自己インクリメントIDのハッシュです。

シャードキーの選択を誤ったことが、今回の事故の原因です。

シーケンシャルな書き込みが大量に行われるため、書き込みがスムーズに行われるようにすることが優先されるべきです。ドキュメントをランダムなハッシュとして見た場合、古いログと新しいログが一緒に割り当てられる可能性が高いです。Mongo はデータをひとつずつ落とすのではなく、チャンク単位で落とします。このようにホットデータとコールドデータが混在すると、ログの実際の出力よりも書き込みディスクの IO のほうがはるかに多くなります。

最終的に、シャードキーをログ時間と自己インクリメントのIDで分けるように調整したところ、mongoのデータランディングのIOが数桁下がりました。

ほら、システムの仕組みを理解することが重要なんです。

ps.この事件の後、私はスカイネットに監視機能を追加し、個々のモジュールの過負荷を簡単に警告できるようにしました。これにより、プロセスの後半で問題を突き止めることができるようになりました。redisの話はまた次回。

3月5日追記

。しかし、今回遭遇した状況とは異なります。

ある生徒が、この記事には一括で記述する場合、 だと書いてあると言っていました。

バッチ挿入ではなく、1行ずつの挿入です。つまり、パフォーマンスが低いのは、小節ごとにgetLastErrorを呼び出すためではなく、書き込みパフォーマンスを確保するためにgetLastErrorを取得しない一方通行のプッシュのためです。ビジネスシーンでは、マシンがタイムスライスごとにデータセットを受け入れることが、より良い活用方法だと思います。

Read next

プログラミング言語におけるいくつかの厄介なルール

プログラマーは、他人が開発したプログラミング言語やオペレーティングシステム、さまざまな開発ツールを使います。前世代の言語開発者やシステム設計者が下した決断のいくつかは、当時は理にかなっていたかもしれませんが、今となっては冗長に思えるかもしれません。

Jun 30, 2014 · 5 min read