Spring WebFlux - WebClient连接池简单测试和代码分析

Spring5.1开始,WebFlux的WebClient支持连接池功能了。
默认情况下,WebClient使用global Reactor Netty资源,也可以不使用全局资源:

    @Bean
    ReactorResourceFactory resourceFactory() {
        ReactorResourceFactory factory = new ReactorResourceFactory();
        factory.setUseGlobalResources(false);
        factory.setConnectionProvider(ConnectionProvider.fixed("httpClient", maxConns, acquireTimeout));
        factory.setLoopResources(LoopResources.create("httpClient", workCounts, true));
        return factory;
    }

可以这样配置WebClient:

    @Bean
    WebClient webClient() {
        Function<HttpClient, HttpClient> mapper = client ->
                client.tcpConfiguration(c ->
                        c.option(CONNECT_TIMEOUT_MILLIS, connTimeout)
                                .option(TCP_NODELAY, true)
                                .doOnConnected(conn -> {
                                    conn.addHandlerLast(new ReadTimeoutHandler(readTimeout));
                                    conn.addHandlerLast(new WriteTimeoutHandler(writeTimeout));
                                }));

        ClientHttpConnector connector =
                new ReactorClientHttpConnector(resourceFactory(), mapper);

        return WebClient.builder().clientConnector(connector).build();
    }

测试一下:

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = RANDOM_PORT)
@ActiveProfiles("constants")
public class WebClientTest {
    @Autowired
    private WebClient client;

    private static String blog = "https://blog.csdn.net/weixin_43364172";
    private static String weibo = "http://www.weibo.com";

    @Test
    public void test() {
        single(blog);
    }

    private void single(String url) {
        Mono<ClientResponse> mono = client.get().uri(url)
                .accept(MediaType.TEXT_HTML)
                .exchange();

        StepVerifier.create(mono)
                .assertNext(clientResponse -> {
                    Assert.assertThat(clientResponse.statusCode(),
                            anyOf(equalTo(OK), equalTo(MOVED_PERMANENTLY)));
                    if (clientResponse.rawStatusCode() == OK.value()) {
/*                        Mono res = clientResponse.bodyToMono(String.class);
                        StepVerifier.create(res)
                                .assertNext(s -> Assert.assertThat(s.length(), greaterThan(0)))
                                .verifyComplete();*/
                        clientResponse.bodyToMono(String.class).subscribe(s -> System.out.println(s.length()));
                    }
                })
                .verifyComplete();
    }


    @Test(timeout = 10000)
    public void tests() {
        concurrency(3, weibo);
        concurrency(10, blog);
        concurrency(2, blog);
        concurrency(2, weibo);
        concurrency(3, weibo);
        concurrency(2, blog);
    }

    private void concurrency(int num, String url) {
        ExecutorService es = Executors.newFixedThreadPool(num);
        for (int i = 0; i < num; i++) {
            es.execute(() -> single(url));
        }
        es.shutdown();
        try {
            es.awaitTermination(10, TimeUnit.SECONDS);
            System.out.println(num + " completes");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}

运行发现,获取weibo和blog页面,各使用了独立的Bootstrap和对应的连接池。
如果想让不同网站使用不用的配置参数,可以让它们各自使用独立的ReactorResourceFactory类型的bean,并使用不同的WebClient。
WebClient好像还不支持失败自动重试功能,需要码更多的代码。

看ConnectionProvider代码,可以发现,目前系统支持三种连接池:

  • elastic - 默认的,弹性连接池,打开新连接不用等待,使用SimpleChannelPool
  • fixed - 固定大小,使用FixedChannelPool
  • newConnection - 总是增加新连接

FixedChannelPool和SimpleChannelPool都是Netty实现的。

新的连接请求到来,先经过TcpClientConnect类的connect方法:

	public Mono<? extends Connection> connect(Bootstrap b) {

		if (b.config()
		     .group() == null) {

			TcpClientRunOn.configure(b,
					LoopResources.DEFAULT_NATIVE,
					TcpResources.get(),
					maxConnections != -1);
		}

		return provider.acquire(b);

	}

PooledConnectionProvider类的disposableAcquire方法:

	static void disposableAcquire(MonoSink<Connection> sink, ConnectionObserver obs, Pool pool) {
		Future<Channel> f = pool.acquire();
		DisposableAcquire disposableAcquire =
				new DisposableAcquire(sink, f, pool, obs);
		// Returned value is deliberately ignored
		f.addListener(disposableAcquire);
		sink.onCancel(disposableAcquire);
	}

调用FixedChannelPool类的acquire方法:

    public Future<Channel> acquire(final Promise<Channel> promise) {
        try {
            if (executor.inEventLoop()) {
                acquire0(promise);
            } else {
                executor.execute(new Runnable() {
                    @Override
                    public void run() {
                        acquire0(promise);
                    }
                });
            }
        } catch (Throwable cause) {
            promise.setFailure(cause);
        }
        return promise;
    }

重点在acquire0方法:

    private void acquire0(final Promise<Channel> promise) {
        assert executor.inEventLoop();

        if (closed) {
            promise.setFailure(POOL_CLOSED_ON_ACQUIRE_EXCEPTION);
            return;
        }
        if (acquiredChannelCount.get() < maxConnections) {
            assert acquiredChannelCount.get() >= 0;

            // We need to create a new promise as we need to ensure the AcquireListener runs in the correct
            // EventLoop
            Promise<Channel> p = executor.newPromise();
            AcquireListener l = new AcquireListener(promise);
            l.acquired();
            p.addListener(l);
            super.acquire(p);
        } else {
            if (pendingAcquireCount >= maxPendingAcquires) {
                promise.setFailure(FULL_EXCEPTION);
            } else {
                AcquireTask task = new AcquireTask(promise);
                if (pendingAcquireQueue.offer(task)) {
                    ++pendingAcquireCount;

                    if (timeoutTask != null) {
                        task.timeoutFuture = executor.schedule(timeoutTask, acquireTimeoutNanos, TimeUnit.NANOSECONDS);
                    }
                } else {
                    promise.setFailure(FULL_EXCEPTION);
                }
            }

            assert pendingAcquireCount > 0;
        }
    }

最终通过父类的acquireHealthyFromPoolOrNew方法返回连接:

    private Future<Channel> acquireHealthyFromPoolOrNew(final Promise<Channel> promise) {
        try {
            final Channel ch = pollChannel();
            if (ch == null) {
                // No Channel left in the pool bootstrap a new Channel
                Bootstrap bs = bootstrap.clone();
                bs.attr(POOL_KEY, this);
                ChannelFuture f = connectChannel(bs);
                if (f.isDone()) {
                    notifyConnect(f, promise);
                } else {
                    f.addListener(new ChannelFutureListener() {
                        @Override
                        public void operationComplete(ChannelFuture future) throws Exception {
                            notifyConnect(future, promise);
                        }
                    });
                }
                return promise;
            }
            EventLoop loop = ch.eventLoop();
            if (loop.inEventLoop()) {
                doHealthCheck(ch, promise);
            } else {
                loop.execute(new Runnable() {
                    @Override
                    public void run() {
                        doHealthCheck(ch, promise);
                    }
                });
            }
        } catch (Throwable cause) {
            promise.tryFailure(cause);
        }
        return promise;
    }

测试的时候,需要注意的一点是:只有读取了LastHttpContent,才会关闭连接。
可以看HttpClientOperations类的源码:

		if (msg instanceof LastHttpContent) {
			if (!started) {
				if (log.isDebugEnabled()) {
					log.debug(format(channel(), "HttpClientOperations received an incorrect end " +
							"delimiter (previously used connection?)"));
				}
				ReferenceCountUtil.release(msg);
				return;
			}
			if (log.isDebugEnabled()) {
				log.debug(format(channel(), "Received last HTTP packet"));
			}
			if (msg != LastHttpContent.EMPTY_LAST_CONTENT) {
				super.onInboundNext(ctx, msg);
			}
			//force auto read to enable more accurate close selection now inbound is done
			channel().config().setAutoRead(true);
			if (markSentBody()) {
				markPersistent(false);
			}
			terminate();
			return;
		}

其中,terminate方法见ChannelOperations类:

	protected final void terminate() {
		if (rebind(connection)) {
			if (log.isTraceEnabled()) {
				log.trace(format(channel(), "Disposing ChannelOperation from a channel"),
						new Exception("ChannelOperation terminal stack"));
			}

			Operators.terminate(OUTBOUND_CLOSE, this);
			listener.onStateChange(this, ConnectionObserver.State.DISCONNECTING);
			// Do not call directly inbound.onInboundComplete()
			// HttpClientOperations need to notify with error
			// when there is no response state
			onInboundComplete();
			if (isPersistent()) {
				channel().writeAndFlush(TERMINATED_OPS, this);
			}
			else {
				// Returned value is deliberately ignored
				setSuccess(null);
			}
		}
	}

向listener发送了DISCONNECTING消息,监听器就是PooledConnectionProvider类的静态子类PooledConnection:

			if (newState == State.DISCONNECTING) {

				if (!isPersistent() && channel.isActive()) {
					//will be released by closeFuture internals
					channel.close();
					owner().onStateChange(connection, State.DISCONNECTING);
					return;
				}

				if (!channel.isActive()) {
					owner().onStateChange(connection, State.DISCONNECTING);
					//will be released by poolResources internals
					return;
				}

				if (log.isDebugEnabled()) {
					log.debug(format(connection.channel(), "Releasing channel"));
				}

				ConnectionObserver obs = channel.attr(OWNER)
						.getAndSet(ConnectionObserver.emptyListener());

				pool.release(channel)
				    .addListener(f -> {
					    if (log.isDebugEnabled() && !f.isSuccess()) {
						    log.debug("Failed cleaning the channel from pool", f.cause());
					    }
					    onTerminate.onComplete();
						obs.onStateChange(connection, State.RELEASED);
				    });
				return;
			}

通过pool.release(channel)释放了Channel资源。

你可能感兴趣的:(Spring5)