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
更多問題
之後我還發現了多例項重啟時,短時間內重試失敗的問題,在這篇文章裡面說明了