摘要:本文主要分析使用cse提供的RestTemplate的場景,其實cse提供的rpc註解(RpcReference)的方式最後的呼叫邏輯和RestTemplate是殊途同歸的。
本文分享自華為雲社群《我是一個請求,我該何去何從(下)》,原文作者:向昊 。
上次我們大概瞭解到了服務端是怎麼處理請求的,那麼傳送請求又是個什麼樣的流程了?本文主要分析使用cse提供的RestTemplate的場景,其實cse提供的rpc註解(RpcReference)的方式最後的呼叫邏輯和RestTemplate是殊途同歸的。
使用
使用cse提供的RestTemplate時候,是這樣初始化的:
RestTemplate restTemplate = RestTemplateBuilder.create(); restTemplate.getForObject("cse://appId:serviceName/xxx", Object.class);
我們可以注意到2個怪異的地方:
- RestTemplate是通過RestTemplateBuilder.create()來獲取的,而不是用的Spring裡提供的。
- 請求路徑開頭是cse而不是我們常見的http、https且需要加上服務所屬的應用ID和服務名稱。
解析
根據url匹配RestTemplate
首先看下RestTemplateBuilder.create(),它返回的是org.apache.servicecomb.provider.springmvc.reference.RestTemplateWrapper,是cse提供的一個包裝類。
// org.apache.servicecomb.provider.springmvc.reference.RestTemplateWrapper // 用於同時支援cse呼叫和非cse呼叫 class RestTemplateWrapper extends RestTemplate { private final List<AcceptableRestTemplate> acceptableRestTemplates = new ArrayList<>(); final RestTemplate defaultRestTemplate = new RestTemplate(); RestTemplateWrapper() { acceptableRestTemplates.add(new CseRestTemplate()); } RestTemplate getRestTemplate(String url) { for (AcceptableRestTemplate template : acceptableRestTemplates) { if (template.isAcceptable(url)) { return template; } } return defaultRestTemplate; } }
AcceptableRestTemplate:這個類是一個抽象類,也是繼承RestTemplate的,目前其子類就是CseRestTemplate,我們也可以看到在初始化的時候會預設往acceptableRestTemplates中新增一個CseRestTemplate。
回到使用的地方restTemplate.getForObject:這個方法會委託給如下方法:
public <T> T getForObject(String url, Class<T> responseType, Object... urlVariables) throws RestClientException { return getRestTemplate(url).getForObject(url, responseType, urlVariables); }
可以看到首先會呼叫getRestTemplate(url),即會呼叫template.isAcceptable(url),如果匹配到了就返回CseRestTemplate,否則就返回常規的RestTemplate。那麼再看下isAcceptable()這個方法:
到這裡我們就清楚了路徑中的cse://的作用了,就是為了使用CseRestTemplate來發起請求,也理解了為啥RestTemplateWrapper可以同時支援cse呼叫和非cse呼叫。
委託呼叫
從上面可知,我們的cse呼叫其實都是委託給CseRestTemplate了。在構造CseRestTemplate的時候會初始化幾個東西:
public CseRestTemplate() { setMessageConverters(Arrays.asList(new CseHttpMessageConverter())); setRequestFactory(new CseClientHttpRequestFactory()); setUriTemplateHandler(new CseUriTemplateHandler()); }
這裡需要重點關注new CseClientHttpRequestFactory():
public class CseClientHttpRequestFactory implements ClientHttpRequestFactory { @Override public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) throws IOException { return new CseClientHttpRequest(uri, httpMethod); } }
最終委託到了CseClientHttpRequest這個類,這裡就是重頭戲了!
我們先把注意力拉回到這句話:restTemplate.getForObject("cse://appId:serviceName/xxx", Object.class),從上面我們知道其邏輯是先根據url找到對應的RestTemplate,然後呼叫getForObject這個方法,最終這個方法會呼叫到:org.springframework.web.client.RestTemplate#doExecute:
protected <T> T doExecute(URI url, @Nullable HttpMethod method, @Nullable RequestCallback requestCallback, @Nullable ResponseExtractor<T> responseExtractor) throws RestClientException { ClientHttpResponse response = null; try { ClientHttpRequest request = createRequest(url, method); if (requestCallback != null) { requestCallback.doWithRequest(request); } response = request.execute(); handleResponse(url, method, response); return (responseExtractor != null ? responseExtractor.extractData(response) : null); } }
createRequest(url, method):會呼叫getRequestFactory().createRequest(url, method),即最終會呼叫到我們初始化CseClientHttpRequest是塞的RequestFactory,所以這裡會返回ClientHttpRequest這個類。
request.execute():這個方法會委託到org.apache.servicecomb.provider.springmvc.reference.CseClientHttpRequest#execute這個方法上。
至此我們知道前面的呼叫最終會委託到CseClientHttpRequest#execute這個方法上。
cse呼叫
接著上文分析:
public ClientHttpResponse execute() { path = findUriPath(uri); requestMeta = createRequestMeta(method.name(), uri); QueryStringDecoder queryStringDecoder = new QueryStringDecoder(uri.getRawSchemeSpecificPart()); queryParams = queryStringDecoder.parameters(); Object[] args = this.collectArguments(); // 異常流程,直接拋異常出去 return this.invoke(args); }
createRequestMeta(method.name(), uri):這裡主要是根據microserviceName去獲取呼叫服務的資訊,並會將獲取的資訊放入到Map中。服務資訊如下:
可以看到裡面的資訊很豐富,例如應用名、服務名、還有介面對應的yaml資訊等。
this.collectArguments():這裡隱藏了一個校驗點,就是會校驗傳入的引數是否符合對方介面的定義。主要是通過這個方法:org.apache.servicecomb.common.rest.codec.RestCodec#restToArgs,如果不符合真個流程就結束了。
準備invocation
從上面分析可知,獲取到介面所需的引數後就會呼叫這個方法:org.apache.servicecomb.provider.springmvc.reference.CseClientHttpRequest#invoke:
private CseClientHttpResponse invoke(Object[] args) { Invocation invocation = prepareInvocation(args); Response response = doInvoke(invocation); if (response.isSuccessed()) { return new CseClientHttpResponse(response); } throw ExceptionFactory.convertConsumerException(response.getResult()); }
prepareInvocation(args):這個方法會準備好Invocation,這個Invocation在上集已經分析過了,不過上集中的它是為服務端服務的,那麼我們們這塊當然就得為消費端工作了
protected Invocation prepareInvocation(Object[] args) { Invocation invocation = InvocationFactory.forConsumer(requestMeta.getReferenceConfig(), requestMeta.getOperationMeta(), args); return invocation; }
從名字也可以看出它是為消費端服務的,其實無論是forProvider還是forConsumer,它們最主要的區別就是載入的Handler不同,這次載入的Handler如下:
- class org.apache.servicecomb.qps.ConsumerQpsFlowControlHandler(流控)
- class org.apache.servicecomb.loadbalance.LoadbalanceHandler(負載)
- class org.apache.servicecomb.bizkeeper.ConsumerBizkeeperHandler(容錯)
- class org.apache.servicecomb.core.handler.impl.TransportClientHandler(呼叫,預設載入的)
前面3個Handler可以參考下這個微服務治理專欄
doInvoke(invocation):初始化好了invocation後就開始呼叫了。最終會呼叫到這個方法上:org.apache.servicecomb.core.provider.consumer.InvokerUtils#innerSyncInvoke
至此,這些動作就是cse中RestTemplate和rpc呼叫的不同之處。不過可以清楚的看到RestTemplate的方式是隻支援同步的,即innerSyncInvoke,但是rpc是可以支援非同步的,即reactiveInvoke
public static Response innerSyncInvoke(Invocation invocation) { invocation.next(respExecutor::setResponse); }
到這裡我們知道了,消費端發起請求還是得靠invocation的責任鏈驅動
啟動invocation責任鏈
好了,我們們的老朋友又出現了:invocation.next,這個方法是個典型的責任鏈模式,其鏈條就是上面說的那4個Handler。前面3個就不分析了,直接跳到TransportClientHandler。
// org.apache.servicecomb.core.handler.impl.TransportClientHandler public void handle(Invocation invocation, AsyncResponse asyncResp) throws Exception { Transport transport = invocation.getTransport(); transport.send(invocation, asyncResp); }
invocation.getTransport():獲取請求地址,即最終傳送請求的時候還是以ip:port的形式。
transport.send(invocation, asyncResp):呼叫鏈為
org.apache.servicecomb.transport.rest.vertx.VertxRestTransport#send
- ->org.apache.servicecomb.transport.rest.client.RestTransportClient#send(這裡會初始化HttpClientWithContext,下面會分析)
- ->org.apache.servicecomb.transport.rest.client.http.RestClientInvocation#invoke(真正傳送請求的地方)
public void invoke(Invocation invocation, AsyncResponse asyncResp) throws Exception { createRequest(ipPort, path); clientRequest.putHeader(org.apache.servicecomb.core.Const.TARGET_MICROSERVICE, invocation.getMicroserviceName()); RestClientRequestImpl restClientRequest = new RestClientRequestImpl(clientRequest, httpClientWithContext.context(), asyncResp, throwableHandler); invocation.getHandlerContext().put(RestConst.INVOCATION_HANDLER_REQUESTCLIENT, restClientRequest); Buffer requestBodyBuffer = restClientRequest.getBodyBuffer(); HttpServletRequestEx requestEx = new VertxClientRequestToHttpServletRequest(clientRequest, requestBodyBuffer); invocation.getInvocationStageTrace().startClientFiltersRequest(); // 觸發filter.beforeSendRequest方法 for (HttpClientFilter filter : httpClientFilters) { if (filter.enabled()) { filter.beforeSendRequest(invocation, requestEx); } } // 從業務執行緒轉移到網路執行緒中去傳送 // httpClientWithContext.runOnContext }
createRequest(ipPort, path):根據引數初始化HttpClientRequest clientRequest,初始化的時候會傳入一個建立一個responseHandler,即對響應的處理。
注意org.apache.servicecomb.common.rest.filter.HttpClientFilter#afterReceiveResponse的呼叫就是在這裡埋下伏筆的,是通過回撥org.apache.servicecomb.transport.rest.client.http.RestClientInvocation#processResponseBody這個方法觸發的(在建立responseHandler時候建立的)
且org.apache.servicecomb.common.rest.filter.HttpClientFilter#beforeSendRequest:這個方法的觸發我們也可以很清楚的看到在傳送請求執行的。
requestEx:注意它的型別是HttpServletRequestEx,雖然名字裡面帶有Servlet,但是開啟它的方法可以發現有很多我們在tomcat中那些常用的方法都直接丟擲異常了,這也是一個易錯點!
httpClientWithContext.runOnContext:用來傳送請求的邏輯,不過這裡還是有點繞的,下面重點分析下
httpClientWithContext.runOnContext
首先看下HttpClientWithContext的定義:
public class HttpClientWithContext { public interface RunHandler { void run(HttpClient httpClient); } private HttpClient httpClient; private Context context; public HttpClientWithContext(HttpClient httpClient, Context context) { this.httpClient = httpClient; this.context = context; } public void runOnContext(RunHandler handler) { context.runOnContext((v) -> { handler.run(httpClient); }); } }
從上面可知傳送請求呼叫的是這個方法:runOnContext,引數為RunHandler介面,然後是以lambda的方式傳入的,lambda的引數為httpClient,這個httpClient又是在HttpClientWithContext的建構函式中初始化的。這個建構函式是在org.apache.servicecomb.transport.rest.client.RestTransportClient#send這個方法中初始化的(呼叫org.apache.servicecomb.transport.rest.client.RestTransportClient#findHttpClientPool這個方法)。
但是我們觀察呼叫的地方:
// 從業務執行緒轉移到網路執行緒中去傳送 httpClientWithContext.runOnContext(httpClient -> { clientRequest.setTimeout(operationMeta.getConfig().getMsRequestTimeout()); processServiceCombHeaders(invocation, operationMeta); try { restClientRequest.end(); } catch (Throwable e) { LOGGER.error(invocation.getMarker(), "send http request failed, local:{}, remote: {}.", getLocalAddress(), ipPort, e); fail((ConnectionBase) clientRequest.connection(), e); } });
其實在這塊邏輯中HttpClient是沒有被用到的,實際上傳送請求的動作是restClientRequest.end()觸發的,restClientRequest是cse中的類RestClientRequestImpl,然後它包裝了HttpClientRequest(vertx中提供的),即restClientRequest.end()最終還是委託到了HttpClientRequest.end()上了。
那麼這個HttpClientRequest是怎麼被初始化的了?它是在createRequest(ipPort, path)這個方法中初始化的,即在呼叫org.apache.servicecomb.transport.rest.client.http.RestClientInvocation#invoke方法入口處。
初始化的邏輯如下:
clientRequest = httpClientWithContext.getHttpClient().request(method, requestOptions, this::handleResponse)
httpClientWithContext.getHttpClient():這個方法返回的是HttpClient,上面說的HttpClient作用就體現出來了,用來初始化了我們傳送請求的關鍵先生:HttpClientRequest。那麼至此我們傳送請求的整體邏輯大概就清晰了。
總結
無論是採用RestTemplate的方式還是採用rpc註解的方式來傳送請求,其底層邏輯其實是一樣的。即首先根據請求資訊匹配到對方的服務資訊,然後經過一些列的Handler處理,如限流、負載、容錯等等(這也是一個很好的擴充套件機制),最終會走到TransportClientHandler這個Handler,然後根據條件去初始化傳送的request,經過HttpClientFilter的處理後就會委託給vertx的HttpClientRequest來真正的發出請求。