注:私は『ストレンジャー・スクランブル』のデータベースの設計には参加していませんが、いくつかの議論に参加し、意見を述べただけです。 問題が発生したときは、◯◯龍、暁静、Aplyの判断で解決しました。ですから、Redisに関する私の判断のほとんどは、彼らの議論から聞いた話と私自身の推測に基づいており、RedisのドキュメントやRedisのコードを注意深く読むことはしませんでした。
いずれも最終的には問題を解決していますが、この記事に書かれている技術的な内容は、やはり事実に反する可能性が非常に高いので、学生諸君は自らを選別するために読んでください。
Stranger's Brawlでは、Redisは大規模に使用されていませんでした。Redisはこのアーキテクチャに適していると直感しました。このゲームではデータ処理にデータベースを利用しておらず、データの総量は大きいものの、増加する速度は限られています。1台のサーバーでは処理能力に限界があり、ゲームを分割することもできないため、いつでもどこでもログインしているプレイヤーには1つの世界しか見えません。
そのため、ゲームシステムから独立したデータセンターが必要で、このデータセンターはデータ転送とデータ着地のみを担当します。Redisは、ゲームシステムがプレイヤーIDによってプレイヤーデータをインデックス化するためだけに必要なので、最適な選択と思われます。
データセンターは32のライブラリに分かれており、プレイヤーIDごとに分けられています。データは異なるプレイヤー間で完全に独立しています。設計当時、私はデータセンターに1つのポイントからアクセスすることに断固反対し、各ゲーム・サーバー・ノードが各データ・リポジトリに直接接続することを主張しました。なぜなら、ここに1つのポイントを作る必要はないからです。
事前にゲームデータ量を見積もったところ、4台の物理マシンに32個のデータウェアハウスを配置し、各マシンで8個のRedisプロセスを起動すればよいことがわかりました。当初は64G RAMのマシンを使用し、後に96G RAMに増設しました。各Redisサービスが占有するメモリは4~5ギガバイトと計測され、十分すぎるほどです。
Redisのデータランディングメカニズムはドキュメントからしか理解できないため、どのような落とし穴を踏むことになるかは不明ですが、安全面を考慮して、データのバックアップをホストに同期させるためのスレーブとして4台の物理マシンも装備しています。
Redisは2つのBGSAVE戦略をサポートしています。1つはスナップショット方式で、ドロップコマンドを開始したときにプロセスをフォークしてメモリ全体をハードドライブにダンプします。もう1つはAOF方式で、データベースへの書き込み操作をすべて記録します。もうひとつはAOF法と呼ばれるもので、データベースへの書き込み操作をすべて記録するものです。 ゲームは書き込み操作が頻繁でデータ量も膨大なため、AOFには適していません。
最初の事件が起きたのは、正月休み前の2月3日。正月休み中ということもあり、運転やメンテナンスは比較的緩やかでした。
正午、データサービスホストの1つがゲームサーバーからアクセスできなくなり、一部のユーザーログインに影響が出ました。オンラインで接続を修復しようとしてもうまくいかず、メンテナンスのために2時間のダウンタイムが始まりました。
メンテナンス中に、最初に問題が特定されました。午前中にスレーブの1つがメモリ不足になり、スレーブのデータベースサービスが再起動したことが原因でした。スレーブがホストに再接続した後、8つのRedisが同時にSYNCを送信する猛攻撃がホストをノックアウトしました。
ここで、別々に論じなければならない2つの問題があります:
質問1:スレーブのハードウェア構成はマスターと同じなのに、なぜスレーブが先にメモリ不足になるのですか。
質問2:更新されたSYNC操作によって、ホストが過負荷になるのはなぜですか?
問題1は、新年期間中のユーザー増加率を正確に見積もることなくデータベースを正しくデプロイしなかったため、当時は深く調査されませんでした。データベースのメモリ要件が臨界点まで増加したため、メモリ不足の事故はホストまたはスレーブのいずれかで発生した可能性が非常に高いと感じました。スレーブが最初にハングアップしたのは、おそらく単なる偶然でしょう。初期の頃はBGSAVEはローテーションするタイミングでしたが、データ量が増えてくると、同じ物理マシン上のredisサービスが同時にBGSAVEを行うことで、メモリを消費しすぎる必要のある複数のプロセスがフォークしてしまうことを避けるために、BGSAVEの間隔は適切に調整する必要があります。正月期間中はみんな新年を祝うために家に帰るので、このことも無視されます。
問題2は、マスター・スレーブ同期のメカニズムに対する理解不足によるものです:
そういえば、同期を実装するとしたらどうしますか?同期状態になるまでには時間がかかるので。通常のサービスの邪魔をしないのが一番なので、ロックを使って同期の一貫性を確保するのは得策ではありません。そこでRedisは、接続後にSYNCを発行してスレーブが正しい同期ポイントに到達できるように、同期中にもフォークをトリガします。スレーブマシンが再起動すると、8スレーブのRedisは、同期を開くために同時に、即座にホストの確率の交換パーティションにホストのRedisプロセスになります8 Redisプロセスをフォークアウトに等しいです大幅に増加しています。
この事件の後、スレーブ・マシンはキャンセルされました。これはシステム展開をより複雑にし、多くの不安定性を追加し、必ずしもデータセキュリティを向上させるものではありません。同時に、bgsaveの仕組みが改善されました。 bgsaveのトリガーにタイマーを使う代わりに、スクリプトによって同じ物理マシン上の複数のredis間でbgsaveをローテーションできるようになりました。さらに、スレーブでコールドスペアを行う仕組みはホストに移されました。良い点は、スクリプトを使用してコールドスペアのタイミングを制御し、BGSAVEのIOピークをずらすことができることです。
2つ目の事故は最近起こりました。
一日かけて事故の原因を突き止めました。コールドバックアップの仕組みが原因だとわかりました。redisのデータベースファイルはバックアップのために定期的にコピーされ、パッケージ化されます。オペレーティング・システムはファイルをコピーする際にファイル・キャッシュのために大量のメモリーを使い、それを時間内に解放していないようでした。そのため、システム・メモリの使用量が予想される上限を大幅に超えるBGSAVEが発生しました。
今回、OSのカーネルパラメーターを調整し、キャッシュをオフにしたところ、とりあえず問題は解決しました。
この件があってから、データランディング作戦を反省しました。BGSAVEを定期的に行うのは良い解決策ではないようです。少なくとも無駄です。というのも、BGSAVEのたびにディスク上のすべてのデータが保存されますが、実際にはインメモリデータベース内の大量のデータは変更されていません。現在の10分から20分のセーブサイクルでは、変更されるデータはその間にオンラインだったプレイヤーの数と攻撃したプレイヤーの数だけで、プレイヤーの総数よりはるかに少ないのです。
変更されたデータだけをバックアップしたいのですが、内蔵のAOFメカニズムは使いたくありません。
また、ゲームサービスとデータベースサービスの間に中間層を追加することも望ましくありません。これは、システム全体で重要な読み取りパフォーマンスを無駄に犠牲にすることになります。また、書き込みコマンドだけを転送するのも信頼性に欠けます。読み込みコマンドのタイミングが失われるため、データのバージョンが誤って表示される危険性があります。
ゲームサーバーがデータを書き込みたいときに、データのコピーをRedisと別のデータランディングサービスの両方に同時に送信するとしたらどうでしょう?第一に、異なる場所で受信した書き込み操作の順序を確実に認識できるようにバージョン管理メカニズムを追加する必要があり、第二に、ゲームサーバーとデータサーバー間の書き込み帯域幅が2倍になります。
結局、データサーバーの物理マシン上でガーディアンサービスを起動するというシンプルな方法を思いつきました。ゲームサーバーがデータをデータサービスにプッシュし、それが成功したことを確認すると、同時にそのデータのIDをこのガーディアンサービスに送信します。その後、Redisからデータを読み返し、ローカルに保存します。
このガーディアンシップ・サービスはRedisと1対1で同じマシンに構成されており、ハード・ドライブの書き込み速度はネットワーク帯域幅よりも大きいため、過負荷になってはいけません。Redisに関しては、純粋なインメモリデータベースとなり、BGSAVEは実行されなくなります。
この後見プロセスは、データの着地も行います。データランディングには、数行のコードでLuaラッパーができる 選びました。データベースファイルが1つしかないので、コールドスタンバイも簡単です。もちろん、 levelDBも 良い選択ですし、C++ではなくCで実装されていれば、後者も検討したでしょう。
ゲームサーバーとのインターフェースとして、データベースマシン上でskynetプロセスを別に立ち上げ、sync IDリクエストをリッスンするようにしました。Redisの簡単な操作をいくつか処理するだけでよいので、Redisコマンドは手で書きました。出来上がったサービスは ですが、実際には3つのskynetサービスで構成されています。1つは外部ポートでリッスンするサービス、もう1つはその接続でRedisの同期コマンドを処理するサービス、そしてもう1つはシングルポイントとしてunqliteに書き込むサービスです。データ復旧を効率的に行うために、選手データを保存する際に復旧用のRedisコマンドをまとめるようにしました。こうしておけば、リカバリが必要になったときにunqliteから選手データを読み込んで直接Redisに送るだけです。
これにより、Redis内のホットデータとコールドデータをまとめて管理することができます。長い間ログインしていないプレイヤーは定期的にRedisから消去され、そのプレイヤーが再びログインした場合は、Redisが復元してくれます。
Xiaojingは、私がskynetの実装に頼っていることが気に入らなかったようです。彼は当初、同じものをpythonで実装したかったのですが、その後Go言語に興味を持ち、この要件のためにGo言語で遊んでみたくなったのです。そのため、今日現在、この新しい仕組みは本番環境には導入されていません。





