web-dev-qa-db-ja.com

Vert.xイベントループ-これはどのように非同期ですか?

私はVert.xで遊んでいて、スレッド/接続モデルとは対照的に、イベントループに基づいたサーバーにはまったく新しいものです。

public void start(Future<Void> fut) {
    vertx
        .createHttpServer()
        .requestHandler(r -> {
            LocalDateTime start = LocalDateTime.now();
            System.out.println("Request received - "+start.format(DateTimeFormatter.ISO_DATE_TIME));
            final MyModel model = new MyModel();
            try {

                for(int i=0;i<10000000;i++){
                    //some simple operation
                }

                model.data = start.format(DateTimeFormatter.ISO_DATE_TIME) +" - "+LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME);

            } catch (Exception e1) {
                // TODO Auto-generated catch block
                e1.printStackTrace();
            }

          r.response().end(
                  new Gson().toJson(model)
                 );
        })
        .listen(4568, result -> {
          if (result.succeeded()) {
            fut.complete();
          } else {
            fut.fail(result.cause());
          }
        });
    System.out.println("Server started ..");
  }
  • このモデルがどのように機能するかを理解するために、長時間実行されるリクエストハンドラーをシミュレートしようとしています。
  • 私が観察したのは、最初のリクエストが完了するまで、いわゆるイベントループがブロックされていることです。少し時間がかかっても、前のリクエストが完了するまで、後続のリクエストは処理されません。
  • 明らかに私はここで作品を見逃しています、そしてそれは私がここに持っている質問です。

これまでの回答に基づいて編集:

  1. 非同期と見なされるすべてのリクエストを受け入れていませんか?前の接続がクリアされたときにのみ新しい接続を受け入れることができる場合、それはどのように非同期ですか?
    • 通常のリクエストには、(リクエストの種類と性質に基づいて)100ミリ秒から1秒の範囲でかかると想定します。つまり、イベントループは、前のリクエストが終了するまで(1秒で終了したとしても)新しい接続を受け入れることができません。そして、私がプログラマーとしてこれらすべてを熟考し、そのような要求ハンドラーをワーカースレッドにプッシュする必要がある場合、それはスレッド/接続モデルとどのように異なりますか?
    • このモデルが従来のスレッド/接続サーバーモデルよりも優れていることを理解しようとしていますか? I/O操作がない、またはすべてのI/O操作が非同期で処理されると仮定しますか?すべての同時リクエストを並行して開始できず、前のリクエストが終了するまで待たなければならない場合、c10k問題をどのように解決しますか?
  2. これらすべての操作をワーカースレッド(プール)にプッシュすることにした場合でも、同じ問題に戻りますね。スレッド間のコンテキスト切り替え? 賞金のためにこの質問を編集してトッピングする

    • このモデルがどのように非同期であると主張されているかを完全には理解していません。
    • Vert.xには非同期JDBCクライアント(Asyncronousがキーワード)があり、RXJavaに適合させようとしました。
    • これがコードサンプルです(関連部分)

    server.requestStream()。toObservable()。subscribe(req-> {

            LocalDateTime start = LocalDateTime.now();
            System.out.println("Request for " + req.absoluteURI() +" received - " +start.format(DateTimeFormatter.ISO_DATE_TIME));
            jdbc.getConnectionObservable().subscribe(
                    conn -> {
    
                        // Now chain some statements using flatmap composition
                        Observable<ResultSet> resa = conn.queryObservable("SELECT * FROM CALL_OPTION WHERE UNDERLYING='NIFTY'");
                        // Subscribe to the final result
                        resa.subscribe(resultSet -> {
    
                            req.response().end(resultSet.getRows().toString());
                            System.out.println("Request for " + req.absoluteURI() +" Ended - " +LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME));
                        }, err -> {
                            System.out.println("Database problem");
                            err.printStackTrace();
                        });
                    },
    
                    // Could not connect
                    err -> {
                        err.printStackTrace();
                    }
                    );
    
    });
    server.listen(4568);
    
    • そこでのselectクエリは、完全なテーブルダンプを返すのに約3秒かかります。
    • 同時リクエストを実行すると(2つだけで試行)、2番目のリクエストは最初のリクエストが完了するのを完全に待機していることがわかります。
    • JDBC selectが非同期の場合、selectクエリが何かを返すのを待つ間、フレームワークが2番目の接続を処理することは当然のことではありませんか。
17
user378101

Vert.xイベントループは、実際、多くのプラットフォームに存在する古典的なイベントループです。そしてもちろん、Node.jsはこのアーキテクチャパターンに基づく最も人気のあるフレームワークであるため、ほとんどの説明とドキュメントはNode.jsで見つけることができます。 Node.jsイベントループの下のメカニズムの1つ多かれ少なかれ良い 説明 を見てください。 Vert.xチュートリアル 「電話しないでください。電話します」と「Verticles」の間にも細かい説明があります。

更新のために編集します:

まず、イベントループを使用している場合、メインスレッドはすべてのリクエストに対して非常に迅速に機能する必要があります。このループでは長い仕事をするべきではありません。そしてもちろん、データベースへの呼び出しに対する応答を待つべきではありません。 -非同期で呼び出しをスケジュールします-結果にコールバック(ハンドラー)を割り当てます-コールバックは、イベントループスレッドではなく、ワーカースレッドで実行されます。たとえば、このコールバックはソケットに応答を返します。したがって、イベントループでの操作は、コールバックを使用してすべての非同期操作をスケジュールし、結果を待たずに次のリクエストに進む必要があります。

通常のリクエストには、(リクエストの種類と性質に基づいて)100ミリ秒から1秒の範囲でかかると想定します。

その場合、リクエストには計算コストのかかる部分やIOへのアクセスが含まれます。イベントループ内のコードは、これらの操作の結果を待つべきではありません。

このモデルが従来のスレッド/接続サーバーモデルよりも優れていることを理解しようとしていますか? I/O操作がない、またはすべてのI/O操作が非同期で処理されると仮定しますか?

同時リクエストが多すぎて従来のプログラミングモデルがある場合は、リクエストごとにスレッドを作成します。このスレッドは何をしますか?彼らは主にIO操作を待っています(たとえば、データベースからの結果)。それは資源の無駄です。イベントループモデルでは、操作をスケジュールする1つのメインスレッドと、長いタスクに事前に割り当てられた量のワーカースレッドがあります。 +これらのワーカーは実際には応答を待機せず、IOの結果を待機している間に別のコードを実行できます(現在のIOジョブのコールバックまたは定期的なチェックステータスとして実装できます進捗)。 Java NIO およびJava NIO 2を実行して、この非同期IOをフレームワーク内に実際に実装する方法を理解することをお勧めします。 グリーンスレッド も非常に関連性の高い概念であり、理解しておくとよいでしょう。グリーンスレッドとコルーチンは、シャドウイベントループの一種であり、同じことを達成しようとします。グリーンスレッドが何かを待っている間にシステムスレッドを再利用できるため、スレッドが少なくなります。

すべての同時リクエストを並行して開始できず、前のリクエストが終了するまで待たなければならない場合、c10k問題をどのように解決しますか?

確かに、前のリクエストに対する応答を送信するためにメインスレッドで待機することはありません。リクエストの取得、長い/ IOタスクの実行のスケジュール、次のリクエスト。

これらすべての操作をワーカースレッド(プール)にプッシュすることにした場合でも、同じ問題に戻りますね。スレッド間のコンテキスト切り替え?

あなたがすべてを正しくするならば-いいえ。さらに、優れたデータの局所性と実行フローの予測が得られます。 1つのCPUコアが短いイベントループを実行し、コンテキストの切り替えだけで非同期作業をスケジュールします。他のコアはデータベースを呼び出して応答を返しますが、これだけです。コールバックを切り替えたり、異なるチャネルでIOステータスを確認したりする場合、実際にはシステムスレッドのコンテキスト切り替えは必要ありません。実際には1つのワーカースレッドで機能しています。したがって、コアごとに1つのワーカースレッドがあり、この1つのシステムスレッドは、たとえばデータベースへの複数の接続からの結果の可用性を待機/チェックします。 Java NIO の概念を再検討して、このように機能する方法を理解してください。 (NIOの典型的な例-多数の並列接続(数千)を受け入れ、他のいくつかのリモートサーバーに要求をプロキシし、応答をリッスンしてクライアントに応答を送り返すことができるプロキシサーバーと、これらすべてを1つまたは2つのスレッドを使用して)

あなたのコードについて、私はサンプルを作成しました project すべてが期待どおりに機能することを示すために:

_public class MyFirstVerticle extends AbstractVerticle {

    @Override
    public void start(Future<Void> fut) {
        JDBCClient client = JDBCClient.createShared(vertx, new JsonObject()
                .put("url", "jdbc:hsqldb:mem:test?shutdown=true")
                .put("driver_class", "org.hsqldb.jdbcDriver")
                .put("max_pool_size", 30));


        client.getConnection(conn -> {
            if (conn.failed()) {throw new RuntimeException(conn.cause());}
            final SQLConnection connection = conn.result();

            // create a table
            connection.execute("create table test(id int primary key, name varchar(255))", create -> {
                if (create.failed()) {throw new RuntimeException(create.cause());}
            });
        });

        vertx
            .createHttpServer()
            .requestHandler(r -> {
                int requestId = new Random().nextInt();
                System.out.println("Request " + requestId + " received");
                    client.getConnection(conn -> {
                         if (conn.failed()) {throw new RuntimeException(conn.cause());}

                         final SQLConnection connection = conn.result();

                         connection.execute("insert into test values ('" + requestId + "', 'World')", insert -> {
                             // query some data with arguments
                             connection
                                 .queryWithParams("select * from test where id = ?", new JsonArray().add(requestId), rs -> {
                                     connection.close(done -> {if (done.failed()) {throw new RuntimeException(done.cause());}});
                                     System.out.println("Result " + requestId + " returned");
                                     r.response().end("Hello");
                                 });
                         });
                     });
            })
            .listen(8080, result -> {
                if (result.succeeded()) {
                    fut.complete();
                } else {
                    fut.fail(result.cause());
                }
            });
    }
}

@RunWith(VertxUnitRunner.class)
public class MyFirstVerticleTest {

  private Vertx vertx;

  @Before
  public void setUp(TestContext context) {
    vertx = Vertx.vertx();
    vertx.deployVerticle(MyFirstVerticle.class.getName(),
        context.asyncAssertSuccess());
  }

  @After
  public void tearDown(TestContext context) {
    vertx.close(context.asyncAssertSuccess());
  }

  @Test
  public void testMyApplication(TestContext context) {
      for (int i = 0; i < 10; i++) {
          final Async async = context.async();
          vertx.createHttpClient().getNow(8080, "localhost", "/",
                            response -> response.handler(body -> {
                                context.assertTrue(body.toString().contains("Hello"));
                                async.complete();
                            })
        );
    }
  }
}
_

出力:

_Request 1412761034 received
Request -1781489277 received
Request 1008255692 received
Request -853002509 received
Request -919489429 received
Request 1902219940 received
Request -2141153291 received
Request 1144684415 received
Request -1409053630 received
Request -546435082 received
Result 1412761034 returned
Result -1781489277 returned
Result 1008255692 returned
Result -853002509 returned
Result -919489429 returned
Result 1902219940 returned
Result -2141153291 returned
Result 1144684415 returned
Result -1409053630 returned
Result -546435082 returned
_

したがって、リクエストを受け入れます。データベースへのリクエストをスケジュールし、次のリクエストに進み、すべてを消費し、データベースですべてが完了した場合にのみ、リクエストごとに応答を送信します。

コードサンプルについて、2つの問題が考えられます。1つは、接続をclose()していないようです。これは、プールに戻すために重要です。次に、プールはどのように構成されていますか?空き接続が1つしかない場合、これらの要求はこの接続を待ってシリアル化されます。

シリアル化する場所を見つけるために、両方のリクエストにタイムスタンプの印刷を追加することをお勧めします。イベントループ内の呼び出しをブロックするものがあります。または...テストでリクエストを並行して送信することを確認してください。前の後に応答を得た後、次ではありません。

31

これはどのように非同期ですか?答えはあなたの質問自体にあります

私が観察したのは、最初のリクエストが完了するまで、いわゆるイベントループがブロックされていることです。少し時間がかかっても、前のリクエストが完了するまで、後続のリクエストは処理されません。

アイデアは、各HTTPリクエストを処理するための新しいものを用意する代わりに、長時間実行するタスクによってブロックしたものと同じスレッドを使用することです。

イベントループの目標は、あるスレッドから別のスレッドへのコンテキスト切り替えにかかる時間を節約し、タスクがIO /ネットワークアクティビティを使用しているときに理想的なCPU時間を利用することです。リクエストの処理中に他のIO /ネットワーク操作が必要になった場合(例:その間にリモートMongoDBインスタンスからデータをフェッチする)、スレッドはブロックされず、代わりに別のリクエストが同じスレッドによって処理されます。これは、の理想的なユースケースです。イベントループモデル(サーバーに同時リクエストが来ることを考慮)。

ネットワーク/ IO操作を伴わない長時間実行タスクがある場合は、代わりにスレッドプールの使用を検討する必要があります。メインイベントループスレッド自体をブロックすると、他の要求が遅延します。つまり、長時間実行されるタスクの場合、サーバーが応答するようにコンテキスト切り替えの代償を払ってもかまいません。

[〜#〜] edit [〜#〜]:サーバーがリクエストを処理する方法はさまざまです。

1)着信要求ごとに新しいスレッドを生成します(このモデルでは、コンテキストスイッチングが高くなり、毎回新しいスレッドを生成するための追加コストが発生します)

2)スレッドプールを使用してリクエストをサーバー化します(同じスレッドセットがリクエストの処理に使用され、追加のリクエストがキューに入れられます)

3)イベントループを使用します(すべての要求に対して単一のスレッド。コンテキストの切り替えは無視できます。たとえば、着信要求をキューに入れるために実行されているスレッドがいくつかあるため)

まず第一に、コンテキスト切り替えは悪くありません。アプリケーションサーバーの応答性を維持する必要がありますが、同時要求の数が多すぎる(約10kを超える)場合、コンテキスト切り替えが多すぎると問題になる可能性があります。より詳細に理解したい場合は、読むことをお勧めします C10K記事

通常のリクエストには、(リクエストの種類と性質に基づいて)100ミリ秒から1秒の範囲でかかると想定します。つまり、イベントループは、前のリクエストが終了するまで(1秒で終了したとしても)新しい接続を受け入れることができません。

多数の同時リクエスト(10kを超える)に応答する必要がある場合は、500ミリ秒を超えると実行時間が長くなると考えます。第二に、私が言ったように、いくつかのスレッド/コンテキストの切り替えが含まれます。たとえば、着信要求をキューに入れるためですが、一度にスレッドが少なすぎるため、スレッド間のコンテキストの切り替えは大幅に減少します。第3に、最初の要求の解決に関連するネットワーク/ IO操作がある場合、2番目の要求は、最初の要求が解決される前に解決される機会があります。これは、このモデルがうまく機能する場所です。

そして、私がプログラマーとしてこれらすべてを熟考し、そのような要求ハンドラーをワーカースレッドにプッシュする必要がある場合、それはスレッド/接続モデルとどのように異なりますか?

Vertxは、スレッドとイベントループを最大限に活用しようとしているため、プログラマーとして、ネットワーク/ IO操作の有無にかかわらず、両方のシナリオでアプリケーションを効率的にする方法を呼び出すことができます。

このモデルが従来のスレッド/接続サーバーモデルよりも優れていることを理解しようとしていますか? I/O操作がない、またはすべてのI/O操作が非同期で処理されると仮定しますか?すべての同時リクエストを並行して開始できず、前のリクエストが終了するまで待たなければならない場合、c10k問題をどのように解決しますか?

上記の説明はこれに答えるはずです。

これらすべての操作をワーカースレッド(プール)にプッシュすることにした場合でも、同じ問題に戻りますね。スレッド間のコンテキスト切り替え?

私が言ったように、どちらにも長所と短所があり、vertxは両方のモデルを提供し、ユースケースに応じてシナリオに最適なものを選択する必要があります。

7
prasun

この種の処理エンジンでは、yoは、長時間実行されるタスクを非同期で実行される操作に変換することになっています。これは、重要なスレッドができるだけ早く完了して別のタスクを実行します。つまり、IO操作がフレームワークに渡され、IOが完了したときにコールバックされます。

フレームワークは、これらの非同期タスクの生成と実行をサポートするという意味で非同期ですが、コードを同期から非同期に変更することはありません。

3
Peter Lawrey