spring feign http客戶端連接池配置以及spring zuul http客戶端連接池配置解析

背景

一般在生產項目中, 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(參考:https://segmentfault.com/a/1190000009071952 但是,由於我們其他組件的配置,例如重試等等,導致我們這裏只能用默認的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

更多問題

之後我還發現了多實例重啓時,短時間內重試失敗的問題,在這篇文章裏面說明了

發佈了181 篇原創文章 · 獲贊 247 · 訪問量 142萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章