背景

一般在生产项目中, Feign会使用HTTP连接池而不是默认的Java原生HTTP单路由单长连接;而是使用连接池。Zuul直接使用Ribbon的Http连接池;Feign和网关Zuul的RPC调用,实际上都是HTTP请求。HTTP请求,如果不配置好HTTP连接池参数的话,会影响性能,或者造成堆积阻塞,对于其中一个微服务的调用影响到其他微服务的调用。

源代码类比解析

本文基于Spring Cloud Dalston.SR4,但是基本思路上,这块比较稳定,不稳定的是Feign本身HttpClient的配置实现上。

不过个人感觉,未来Feign可能也会转去用底层Ribbon的HttpClient。因为可以配置,并且实现的连接池粒度更细一些。

Feign Http客户端解析

Feign调用和网关Zuul调用都用了HttpClient,不同的是,这个HttpClient所在层不一样。Feign调用,利用的是自己这一层的HttpClient,并没有用底层Ribbon,只是从Ribbon中获取了服务实例列表。Zuul没有自己的Httpclient,直接利用底层的Ribbon的HttpClient进行调用。

先看看Feign,Feign的Http客户端默认是ApacheHttpClient。这个可以替换成OkHttpClient(参考: 但是,由于我们其他组件的配置,例如重试等等,导致我们这里只能用默认的ApacheHttpClient)。

打断点,看下核心实现的源代码feign.httpclient.ApacheHttpClient:

public final class ApacheHttpClient implements Client {
    private static final String ACCEPT_HEADER_NAME = "Accept";
    private final HttpClient client;

    public ApacheHttpClient() {
        this(HttpClientBuilder.create().build());
    }

    public ApacheHttpClient(HttpClient client) {
        this.client = client;
    }

    public Response execute(Request request, Options options) throws IOException {
        HttpUriRequest httpUriRequest;
        try {
            httpUriRequest = this.toHttpUriRequest(request, options);
        } catch (URISyntaxException var6) {
            throw new IOException("URL '" + request.url() + "' couldn't be parsed into a URI", var6);
        }

        HttpResponse httpResponse = this.client.execute(httpUriRequest);
        Response response = this.toFeignResponse(httpResponse).toBuilder().request(request).build();
        HttpResponseConvertUtil.convert5XXToException(httpUriRequest, httpResponse);
        return response;
    }
    //其他代码略
}

打断点确认,在某个微服务被调用时,确实HTTP请求在这里的execute方法中发出。我们看下构造方法,发现就是用默认配置的HttpClientBuilder构造的。这样不太好,默认情况下,没有连接池,而是依靠对于不同实例地址的共用不同的一个长连接。而又没找到,可以配置参数的地方,所以选择覆盖这里的源代码,将其无参构造器改成:

public ApacheHttpClient()
{
    HttpClientBuilder httpClientBuilder = HttpClientBuilder.create();
    // 长连接保持30秒
    PoolingHttpClientConnectionManager pollingConnectionManager = new PoolingHttpClientConnectionManager(30, TimeUnit.SECONDS);
    // 总连接数
    pollingConnectionManager.setMaxTotal(1000);
    // 同路由的并发数
    pollingConnectionManager.setDefaultMaxPerRoute(100);
    // 保持长连接配置,需要在头添加Keep-Alive
    httpClientBuilder.setKeepAliveStrategy(new DefaultConnectionKeepAliveStrategy());
    httpClientBuilder.setConnectionManager(pollingConnectionManager);
    this.client = httpClientBuilder.build();
}

但是,这么改只是简单的改了下,首先没有做成可配置的,其次就是没有做成对于每个实例隔离连接池(每个实例用不同的HttpClient)。只是整体上对于服务器做了每个实例最多用100个连接的配置。
个人感觉未来feign未来会更改这部分逻辑,所以没大改,而且,都是内网调用,配置成这样也基本可以接受了。

Zuul Http客户端解析

Zuul利用底层的Ribbon Http客户端,更好用些;同样的,我们先看下核心源码RibbonLoadBalancingHttpClient:

public class RibbonLoadBalancingHttpClient
        extends AbstractLoadBalancingClient<RibbonApacheHttpRequest, RibbonApacheHttpResponse, CloseableHttpClient>
{

    public RibbonLoadBalancingHttpClient(IClientConfig config, ServerIntrospector serverIntrospector)
    {
        super(config, serverIntrospector);
    }

    public RibbonLoadBalancingHttpClient(CloseableHttpClient delegate, IClientConfig config,
            ServerIntrospector serverIntrospector)
    {
        super(delegate, config, serverIntrospector);
    }

    protected CloseableHttpClient createDelegate(IClientConfig config)
    {
        return HttpClientBuilder.create()
                // already defaults to 0 in builder, so resetting to 0 won't hurt
                .setMaxConnTotal(config.getPropertyAsInteger(CommonClientConfigKey.MaxTotalConnections, 0))
                // already defaults to 0 in builder, so resetting to 0 won't hurt
                .setMaxConnPerRoute(config.getPropertyAsInteger(CommonClientConfigKey.MaxConnectionsPerHost, 0))
                .disableCookieManagement().useSystemProperties() // for proxy
                .build();
    }

    @Override
    public RibbonApacheHttpResponse execute(RibbonApacheHttpRequest request, final IClientConfig configOverride)
            throws Exception
    {
        final RequestConfig.Builder builder = RequestConfig.custom();
        IClientConfig config = configOverride != null ? configOverride : this.config;
        builder.setConnectTimeout(config.get(CommonClientConfigKey.ConnectTimeout, this.connectTimeout));
        builder.setSocketTimeout(config.get(CommonClientConfigKey.ReadTimeout, this.readTimeout));
        builder.setRedirectsEnabled(config.get(CommonClientConfigKey.FollowRedirects, this.followRedirects));

        final RequestConfig requestConfig = builder.build();
        if (isSecure(configOverride))
        {
            final URI secureUri = UriComponentsBuilder.fromUri(request.getUri()).scheme("https").build().toUri();
            request = request.withNewUri(secureUri);
        }
        final HttpUriRequest httpUriRequest = request.toRequest(requestConfig);
        final HttpResponse httpResponse = this.delegate.execute(httpUriRequest);

        return new RibbonApacheHttpResponse(httpResponse, httpUriRequest.getURI());
    }

    @Override
    public URI reconstructURIWithServer(Server server, URI original)
    {
        URI uri = updateToHttpsIfNeeded(original, this.config, this.serverIntrospector, server);
        return super.reconstructURIWithServer(server, uri);
    }

    @Override
    public RequestSpecificRetryHandler getRequestSpecificRetryHandler(RibbonApacheHttpRequest request,
            IClientConfig requestConfig)
    {
        return new RequestSpecificRetryHandler(false, false, RetryHandler.DEFAULT, requestConfig);
    }
}

从createDelegate这个方法可以看出通过HttpClientBuilder建立HttpClient,并且是可配置的,配置类是CommonClientConfigKey,我们可以配置这几个参数实现对于连接池大小和每个路由连接大小的控制,就是:

ribbon.MaxTotalConnections=200
ribbon.MaxConnectionsPerHost=100

由于是CommonClientConfigKey下的配置,所以也可以对于每个微服务配置:

service1.ribbon.MaxTotalConnections=200
service1.ribbon.MaxConnectionsPerHost=100
service2.ribbon.MaxTotalConnections=200
service2.ribbon.MaxConnectionsPerHost=100

通过配置以及打断点,可以看出,对于每个微服务的调用,都走的是不同的CloseableHttpClient,我们可以对每个微服务单独配置;例如,假设service1有两个实例,service2有三个实例,service1访问压力大概一共需要100个连接,service2访问压力大概一共需要300个连接.我们假设平均分配没有问题,则可以这么配置:

service1.ribbon.MaxTotalConnections=100
service1.ribbon.MaxConnectionsPerHost=50
service2.ribbon.MaxTotalConnections=300
service2.ribbon.MaxConnectionsPerHost=100

但是,考虑如果某台服务器如果出异常了,这么配置会导致连接也许不够用,所以,最好PerHost的就设置为总共需要多少个连接:

service1.ribbon.MaxTotalConnections=200
service1.ribbon.MaxConnectionsPerHost=100
service2.ribbon.MaxTotalConnections=900
service2.ribbon.MaxConnectionsPerHost=300

更多问题

之后我还发现了多实例重启时,短时间内重试失败的问题,在这篇文章里面说明了