リアクティブ プログラミング (3): 単純な HTTP サービス

この本は上記のリアクティブ プログラミングの続きであり、基本的な API を理解したので、実際のアプリケーションを作成し始めます。リアクティブは並行プログラミングを非常にうまく抽象化しており、注意が必要な基礎的な機能が数多くあります。これらの機能を使用すると、これまでコンテナ、プラットフォーム、フレームワークに隠されていた詳細を制御できるようになります。

Spring MVC がブロッキングからリアクティブに移行

リアクティブでは、問題を別の視点から見ることが求められます。従来のリクエスト -> レスポンス モードとは異なり、すべてのデータはシーケンスとしてパブリッシュされ (パブリッシャー)、その後サブスクライブされます (サブスクライバー)。結果が同期的に返されるのを待つのとは異なり、代わりにコールバックを登録します。この方法に慣れていれば、それほど複雑に感じることはありません。ただし、環境全体を同時にリアクティブ モードにする方法はないため、旧式のブロッキング API に対処することは避けられません。

HttpStatus を返すブロッキング メソッドがあるとします。

private RestTemplate restTemplate = new RestTemplate();

private HttpStatus block(int value) {
    return this.restTemplate.getForEntity("http://example.com/{value}", String.class, value)
            .getStatusCode();
}

このメソッドを繰り返し呼び出し、返された結果を処理するには、さまざまなパラメーターを渡す必要があります。これは、典型的な「スキャッターギャザー」アプリケーション シナリオです。たとえば、複数のページから最初の N 個のデータが抽出されます。

以下は間違った方法で行われた例です。

Flux.range(1, 10) (1)
    .log()
    .map(this::block) (2)
    .collect(Result::new, Result::add) (3)
    .doOnSuccess(Result::stop) (4)
  1. インターフェイスを 10 回呼び出します
  2. 閉塞
  3. 結果を要約してオブジェクトに入れる
  4. 最後に処理を終了します(結果は 1 つですMono<Result>

このようにコードを記述しないでください。これは間違った実装であり、呼び出し元のスレッドをブロックします。これは、ループ内で block() を呼び出すのと何ら変わりません。block()適切な実装は、呼び出しをワーカー スレッドに入れることです。以下を返すメソッドを使用できますMono<HttpStatus>

private Mono<HttpStatus> fetch(int value) {
    return Mono.fromCallable(() -> block(value)) (1)
        .subscribeOn(this.scheduler);            (2)
}
  1. ブロッキング コールCallable
  2. ワーカースレッドでサブスクライブする

scheduler共有変数として別途定義:


  Scheduler scheduler = Schedulers.parallel()

次に、次のようにflatMap()置き換えますmap()


Flux.range(1, 10)
    .log()
    .flatMap(                             (1)
        this::fetch, 4)                   (2)
    .collect(Result::new, Result::add)
    .doOnSuccess(Result::stop)
  1. 新しいパブリッシャーでの並列処理
  2. flatMap の並列パラメータ

非リアクティブなサービスを埋め込む

上記のコードをサーブレットのような非リアクティブ サービスに追加する場合は、Spring MVC を使用できます。


@RequestMapping("/parallel")
public CompletableFuture<Result> parallel() {
    return Flux.range(1, 10)
      ...
      .doOnSuccess(Result::stop)
      .toFuture();
}

@RequestMappingjavadocを読むと、このメソッドが value を返しCompletableFuture、アプリケーションは別のスレッドで値を返すことを選択することがわかります。私たちの場合、この単一スレッドは によってscheduler提供されます。

無料のランチはありません

スキャッター/ギャザー計算にワーカー スレッドを使用するのは良いパターンですが、完璧ではありません。呼び出し元はブロックされませんが、それでも何かがブロックされ、問題がそらされるだけです。リクエストごとに 1 つのスレッドで、処理をスレッド プールに入れるノンブロッキング IO HTTP サービスがあります。これはサーブレット コンテナ (たとえば、tomcat) のメカニズムです。リクエストは非同期で処理されるため、Tomcat 内のワーカー スレッドはブロックされずscheduler、 4 つのスレッドが作成されます。10リクエストを処理した場合、理論上の処理性能は4倍になります。簡単に言うと、1 つのスレッドで 10 個のリクエストを順次処理し、それに 1000 ミリ秒かかる場合、使用するメソッドでは 250 ミリ秒しかかかりません。

スレッドを追加する (16 スレッドを割り当てる) ことで、パフォーマンスをさらに向上させることができます。


private Scheduler scheduler = Schedulers.newParallel("sub", 16);

Tomcat はデフォルトでリクエストの処理に 100 のスレッドを割り当てますが、すべてのリクエストが同時に処理されると、スケジューラのスレッド プールがボトルネックになります。スケジューラ スレッド プールの数は Tomcat の数よりもはるかに少ないです。これは、パフォーマンスのチューニングが単純な問題ではなく、さまざまなパラメーターとリソースのマッチングを考慮する必要があることを示しています。

固定数のスレッド プールと比較して、必要に応じてスレッドの数を動的に調整できる、より柔軟なスレッド プールを使用できます。Reactorではすでにこの仕組みが提供されており、実際に使ってみるSchedulers.elastic()とリクエスト数が増えるとそれに応じてスレッド数も増加することがわかります。

リアクティブを全面採用

ブロック呼び出しからリアクティブ呼び出しへのブリッジングは効果的なパターンであり、Spring MVC テクニックを使用して簡単に実装できます。次に、ブロッキング モードを完全に廃止し、新しい API と新しいツールを採用します。最終的にはフルスタックの Reactive を実装しました。

この例では、最初のステップは次spring-boot-starter-web-reactiveのように置き換えるspring-boot-starter-web

メイビン:


<dependencies>
  <dependency>
   <groupId>org.springframework.boot.experimental</groupId>
     <artifactId>spring-boot-starter-web-reactive</artifactId>
  </dependency>
  ...
</dependencies>
    <dependencyManagement>
     <dependencies>
       <dependency>
         <groupId>org.springframework.boot.experimental</groupId>
         <artifactId>spring-boot-dependencies-web-reactive</artifactId>
         <version>0.1.0.M1</version>
         <type>pom</type>
         <scope>import</scope>
      </dependency>
     </dependencies>
    </dependencyManagement>

グラドル:


dependencies {
    compile('org.springframework.boot.experimental:spring-boot-starter-web-reactive')
    ...
}
dependencyManagement {
    imports {
        mavenBom "org.springframework.boot.experimental:spring-boot-dependencies-web-reactive:0.1.0.M1"
    }
}

コントローラーでは使用せずCompletableFuture、代わりに返しますMono


@RequestMapping("/parallel")
public Mono<Result> parallel() {
    return Flux.range(1, 10)
            .log()
            .flatMap(this::fetch, 4)
            .collect(Result::new, Result::add)
            .doOnSuccess(Result::stop);
}

このコードを SpringBoot アプリケーションに入れると、クラスパスがどのパッケージを導入するかに応じて、Tomcat、Jetty、または Netty 上で実行できます。Tomcat がデフォルトのコンテナですが、他のコンテナを使用したい場合は、クラスパスから Tomcat を削除してから、他のコンテナを導入する必要があります。3 つのコンテナーは、起動時間、メモリ使用量、ランタイム リソースにほとんど違いがありません。

引き続きblock()ブロッキングため、呼び出し元をブロックしないようにワーカー スレッドでサブスクライブする必要があります。たとえばWebClient次のものを置き換えるRestTemplate


private WebClient client = new WebClient(new ReactorHttpClientRequestFactory());

private Mono<HttpStatus> fetch(int value) {
    return this.client.perform(HttpRequestBuilders.get("http://example.com"))
            .extract(WebResponseExtractors.response(String.class))
            .map(response -> response.getStatusCode());
}

の戻り値は に変換された Reactive 型ですが、これをサブスクライブしていないことWebClient.perform()注意してください。Mono<HttpStatus>サブスクライブの作業はフレームワークによって実行されます。

制御の反転

ここで、fetch()呼び出し。


@RequestMapping("/netty")
public Mono<Result> netty() {
    return Flux.range(1, 10) (1)
        .log() //
        .flatMap(this::fetch) (2)
        .collect(Result::new, Result::add)
        .doOnSuccess(Result::stop);
}
  1. 10回電話をかける
  2. 新しいパブリッシャーでの並列処理

追加のサブスクリプション スレッドが使用されないため、ブロッキングおよびリアクティブ ブリッジ モードのコードはより簡潔になり、完全なリアクティブ モードになりました。WebClient1 つを返しますMono。もちろん、変換チェーンでそれを使用する必要がありますflatMap()この種のコードを書くことは素晴らしい経験であり、理解しやすく、保守も簡単です。同時に、スレッド プールや同時実行パラメータは必要なく、パフォーマンスに影響を与える魔法の数字 4 もありません。パフォーマンスは、アプリケーションのスレッド制御ではなくシステム リソースに依存します。

アプリケーションは Tomcat、Jetty、または Netty 上で実行できます。Tomcat と Jetty は、Servlet 3.1 に基づく非同期処理をサポートしており、リクエストごとに 1 つのスレッドに制限されています。Netty での実行にはこの制限はありません。クライアントがブロックされない限り、クライアントのリクエストはできるだけ早く配信されます。Netty サービスはリクエストごとにスレッド化されないため、大量のスレッドを使用しません。

多くのアプリケーションは HTTP 呼び出しだけでなくデータベース操作もブロックすることに注意してください。現在、ノンブロッキング クライアントをサポートしているデータベースはほとんどありません (MongoDB と Couchbase を除く)。スレッド プールとブロッキングからリアクティブへのパターンは、長期間にわたって存続します。

無料のランチはまだありません

まず第一に、私たちのコードは宣言的であるため、デバッグには不便であり、エラーが発生したときにそれを見つけるのは簡単ではありません。Spring フレームワークを経由せずに Reactor を直接使用するなど、ネイティブ API を使用すると、多くのエラー処理を自分で行う必要があり、コードを作成するたびに多くの定型コードを記述する必要があるため、状況はさらに悪化します。ネットワーク通話。Spring と Reactor を組み合わせることで、スタック情報とキャッチされなかった例外を簡単に表示できます。実行中のスレッドは私たちの制御下にないため、理解するのは困難です。

次に、プログラミング エラーにより Reactive コールバックがブロックされると、同じスレッド上のすべてのリクエストがハングします。サーブレットコンテナでは、1つのリクエストが1つのスレッドとなるため、1つのリクエストがブロックされても他のリクエストには影響しません。一方、Reactive では、リクエストがブロックされると、すべてのリクエストのレイテンシが増加します。

要約する

非同期処理のすべての側面を制御できるのは非常に便利です。各レベルにスレッド プールとキューがあります。いくつかの層を弾力的にし、負荷に合わせて動的に調整することができます。しかし、これは負担でもあり、より簡潔な方法が期待されます。スケーラビリティ分析の結果は、冗長スレッドを削減する傾向があり、ハードウェア リソースの制約を超えません。

リアクティブはすべての問題に対する解決策ではありません。実際、それ自体が解決策ではなく、特定の種類の問題に対する解決策の生成を促進するだけです。学習、プログラムの調整、その後のメンテナンスのコストがメリットをはるかに上回る可能性があります。したがって、Reactive を使用するかどうかについては十分に注意してください。

おすすめ

転載: blog.csdn.net/haiyan_qi/article/details/79691246