5

私のアプリケーションでは、複数の人 (おそらく 5 人以上) が同時にチャットできるライブ チャット機能が必要です。

Java ベースの Google App Engine を使用しています。GAE Datastore を使用するのはこれが初めてです。Oracle/MySQL の使用に慣れているため、私の戦略は間違っていると思います。

注: 簡単にするために、検証/セキュリティ チェックを省略して います。呼び出された一部のサーブレットWriteMessageには、次のコードがあります。

Entity entity = new Entity("ChatMessage");
entity.setProperty("userName", request.getParameter("userName"));
entity.setProperty("message", request.getParameter("message"));
entity.setProperty("time", new Date());
DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
datastore.put(entity);

と呼ばれる他のサーブレットReadMessagesには、次のコードがあります

String id = request.getParameter("id");
DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
Query query = new Query("ChatMessage");
if (id != null) {
  // Client requested only messages with id greater than this id
  Filter idFilter = new FilterPredicate(Entity.KEY_RESERVED_PROPERTY,
        FilterOperator.GREATER_THAN,
        KeyFactory.createKey("ChatMessage", Long.parseLong(id)));
  query.setFilter(idFilter);
}
PreparedQuery pq = datastore.prepare(query);
JsonArray messages = new JsonArray();
for (Entity result : pq.asIterable()) {
  JsonObject jmsg = new JsonObject();

  // Client will use this id on the next request to read to poll only
  // "new" messages
  jmsg.addProperty("id", result.getKey().getId());
  jmsg.addProperty("userName", (String) result.getProperty("userName"));
  jmsg.addProperty("message", (String) result.getProperty("message"));
  jmsg.addProperty("time", ((Date) result.getProperty("time")).getTime());
  messages.add(jmsg);
}
PrintWriter out = response.getWriter();
out.print(messages.toString());

JavaScript クライアント コードでは、WriteMessageサーブレットはユーザーが新しいメッセージを送信するReadMessagesたびに呼び出され、サーブレットは毎秒呼び出されて新しいメッセージを取得します。

最適化するために、javascript は への後続のリクエストで最後に受信したメッセージの ID (またはおそらくこれまでに受信した最大の ID) をReadMessage送信し、以前に見たことのないメッセージのみが応答に含まれるようにします。

最初はこれですべてうまくいくように見えますが、このコードにはいくつか問題があるのではないかと思います。

ここに私が間違っていると思うものがあります:

  • ChatMessage のキーの ID に依存して、JS クライアントが以前に見たメッセージを除外しているため、一部のメッセージが読み取られない可能性があります。信頼できるとは思いませんか?

  • まったく同じ時間に 5 つまたは 6 つの受信書き込みがある可能性があるため、一部の書き込みが失敗する可能性がありますConcurrentModificationException

  • エンティティに渡された日付は、アプリケーション サーバー上の JRE の現在の日付です。SQL で "sysdate()" のようなものを使用する必要があるのではないでしょうか? これが実際に問題であるかどうかはわかりません。

次のようにコードを修正するにはどうすればよいですか。

  1. すべてのチャット メッセージが書き込まれます。リクエストが失敗した場合、javascript が成功するまで再試行するように、フェールオーバーを設定するのが最善でしょうか?

  2. すべてのチャット メッセージが読まれます (例外なし)

  3. 1000 程度のメッセージのみが保存されるように、古いメッセージをクリーンアップします。

4

1 に答える 1

11

SOに質問を投稿する前に、誰かが実際に問題に取り組んでいると、ちょっと新鮮です。

あなたのアプローチで直面している多くの有効な問題をリストしましたが、あなたの最大の問題はコストになると思います. 各チャット メッセージに新しいエンティティを追加しており、さらにそのエンティティをインデックス化する必要があります。つまり、送信されるすべてのメッセージに対して複数の書き込み操作について話しているのです。また、削除するエンティティごとに料金を支払う必要があるため、クリーンアップするために料金を支払う必要があります。

設計のプラス面としては、トランザクションや祖先を使用してエンティティを作成していないため、書き込みパフォーマンスの制限に達するべきではありません。

読み取り側では、メッセージごとに 1 つのエンティティを読み取るため、そこにもコストが加算されます。トランザクションまたは祖先クエリなしでクエリを実行しているという事実は、クエリを実行したときに最新の ChatMessage エンティティが表示されない可能性があることを意味します。

また、SQL とは異なり、GAE データストア ID は単調に増加しないため、ID GREATER_THAN によるクエリは機能しません。

今提案のために。警告しますが、これは大変な作業になります。

  1. 使用するエンティティの数を最小限に抑えます。メッセージごとに新しいエンティティを追加する代わりに、エンティティごとに複数のメッセージを格納するより大きなエンティティを使用します。

  2. メッセージ エンティティをクエリする代わりに、キーでフェッチします。キーでエンティティをフェッチすると、最終的に一貫性のある結果ではなく、強い一貫性のある結果が得られます。これは、最新のチャット メッセージがすべて読まれるようにする場合に重要です (例外はありません)。

これにより、対処する必要がある 2 つの新しい問題が発生します。

  • 複数の書き込みが同じエンティティに行われる場合、何らかの書き込みパフォーマンスの制限に達します。

  • エンティティは大きくなる可能性があるため、1 MB の制限を超えないようにケースを処理する必要があります。

2 つのエンティティの種類が必要です。複数のメッセージを格納する MessageLog Kind が必要です。おそらく、メッセージを MessageLog 内のリストとして保存する必要があります。主に書き込みパフォーマンスのために、特定のチャットに対して複数の MessageLog エンティティが必要になります。(詳細については、「Google App Engine シャーディング」を検索してください)。

基本的に MessageLog キーのリストを格納する Chat Kind が必要です。これにより、複数のチャットを続けることができます。元の実装では、グローバル チャットが 1 つしかないように見えました。または、それが必要な場合は、Chat の単一のインスタンスを使用してください。

キーによってすべてを取得するため、これらのいずれも実際にインデックスを作成する必要はありません。これにより、コストが削減されます。

新しいチャットを開始するときは、必要と予想されるパフォーマンスに基づいて、多数の MessageLog エンティティを作成します。予想される 1 秒あたりの書き込みあたり 1 エンティティ。チャットにもっと多くの人が参加している場合は、さらに MessageLogs を作成します。次に、Chat エンティティを作成し、MessageLog キーのリストをそこに保存します。

メッセージ書き込みでは、次のことを行います: - 適切なチャット エンティティをキーでフェッチすると、MessageLogs のリストが得られます - MessageLog を 1 つ選択して負荷を分散し、すべての書き込みが同じエンティティにヒットしないようにします。1 つを選択するには複数の手法がある場合がありますが、この例ではランダムに 1 つを選択します。- 新しいメッセージをフォーマットし、MessageLog に挿入します。この時点で、MessageLog に古いメッセージをドロップすることも検討してください。また、MessageLog が 1 MB のエンティティ サイズ制限内であることを確認するために、いくつかの安全チェックを行う必要があります。- MessageLog を書き込みます。これにより、新しいエンティティを書き込むための最小 3 つの書き込み操作ではなく、1 つの書き込み操作のみが発生するはずです。推奨: チャット ログ全体を含む特定のチャットの memcache エントリにメッセージを追加します。

読み取りでは、次のことを行います: 推奨: 最初に、指定されたチャットの memcache エントリを確認します。存在する場合は、それを返すだけで完了です。- 適切なチャット エンティティをキーで取得します。これで、MessageLogs のリストが作成されました。 - すべての MessageLogs をキーで取得します。これで、チャットにすべてのメッセージが表示され、最新の状態になりました。- すべての MessageLogs を解析し、チャット ログ全体を再構築します。推奨: 再構築されたメッセージ ログを memcache に保存して、再度行う必要がないようにします。- 再構築されたチャット ログを返します。

Channel API を使用してメッセージを視聴者に送信することを検討してください。ビューアは、この方法で 1 秒に 1 回よりも速くメッセージを受信できます。Channel API は 100% 信頼できるものではないことが個人的にわかっているので、ポーリングを完全に取り除くことはできませんが、バックアップとして 30 秒ごとに 1 回ポーリングすることで問題ないかもしれません。

100 件のメッセージが含まれるチャットを想像してみてください。元の計画では、100 件のメッセージを読み取るのに約 101 回の読み取り操作が必要でした。この新しい方法では、5 ~ 10 個の MessageLog エンティティのようなものがあるため、コストは 6 ~ 11 回の読み取り操作になります。memcache にヒットした場合、読み取り操作は必要ありません。ただし、複数の MessageLog オブジェクトからチャット ログを再構築するコードを記述する必要があります。

于 2013-05-29T15:10:22.730 に答える