微服務系列(六)探究Spring Cloud服務調用

微服務系列(六)探究Spring Cloud服務調用

大綱

  1. 分佈式架構的“骨架”
  2. 基於http協議的通訊模式
  3. RestTemplate與Feign
  4. 新的選擇WebClient
  5. 從Spring源碼中我看到了什麼

分佈式架構的“骨架”

分佈式架構是由一個個組件組裝而成,各司其職,對外提供服務。而將他們聯繫起來的則是網絡,組件之間的通訊方式則是這個分佈式架構的“骨架”。

如果沒有“骨架”,每個組件只有做自己的事,不能聯繫起來,也就不能互相配合並提供完整的服務。

對於一個大型的分佈式系統中,如果能使用一個統一的通訊方式來作爲“骨架”,並且這個通訊方式能滿足系統的所有需求,那麼這個“骨架”就是完美的。

而Spring Cloud選擇了HTTP。

我認爲主要原因有以下幾點:

  1. HTTP是web常用協議,易學,易使用
  2. HTTP很靈活,支持短連接、長連接,幾乎能滿足大部分需求
  3. HTTP的通訊代價較小,能滿足常見系統的性能需求

當然,可能對於某些系統,HTTP是沒辦法滿足它的需求,例如:需要實現推送效果、性能要求高、可靠性高、一致性要求高。

對於這樣的系統,就需要選擇一個更適合自己的通訊方式,或是用自定義通訊協議來把各個組件串聯起來,這樣做的代價是很大的,好處也是明顯的,某產則是用了這樣的方式,並且在外層做了一個協議轉換的組件,讓系統可以靈活的對接外部不同協議,並在內部用私有自定義協議進行交互。

對於普通需求,我們選擇Spring Cloud給我們提供的最簡單的方式來做就完事了。

基於http協議的通訊模式

這裏列舉Java中常見的幾個HTTP客戶端:

  • OKHttp
  • HttpClient
  • 原生HttpURLConnection

Spring Cloud選擇了HTTP,而在Spring就實現了一個屏蔽底層HTTP客戶端的Spring http客戶端框架,在Spring Cloud體系中,則是用Spring Cloud Feign實現了更高級的功能,不再像以前一樣通過自己構造對象、注入、使用api來調用,使用了註解的方式來替代。

RestTemplate與Feign

Spring的RestTemplate和Spring Cloud的Feign是怎樣實現的呢

源碼分析的部分會屏蔽一些細節的描述,主要了解它的設計思想和值得我們學習的實現

進入org.springframework.web.client.RestTemplate

大致瞭解下它的API:

  • GET請求:getForObject、getForEntity
  • POST請求:postForLocation、postForObject、postForEntity
  • PUT請求:put
  • DELETE請求:delete
  • OPTIONS請求:optionsForAllow
  • HEAD請求:headForHeaders
  • PATCH請求:patchForObject
  • 通用請求入口:exchange、execute

從代碼上看,前7個類型的請求以及exchange最終都是調用了execute,所以我們只需要弄清楚execute是如何實現的。

    // general execution
    
    @Override
    @Nullable
    public <T> T execute(String url, HttpMethod method, @Nullable RequestCallback requestCallback,
    		@Nullable ResponseExtractor<T> responseExtractor, Object... uriVariables) throws RestClientException {
    
    	URI expanded = getUriTemplateHandler().expand(url, uriVariables);
    	return doExecute(expanded, method, requestCallback, responseExtractor);
    }
    
    @Override
    @Nullable
    public <T> T execute(String url, HttpMethod method, @Nullable RequestCallback requestCallback,
    		@Nullable ResponseExtractor<T> responseExtractor, Map<String, ?> uriVariables)
    		throws RestClientException {
    
    	URI expanded = getUriTemplateHandler().expand(url, uriVariables);
    	return doExecute(expanded, method, requestCallback, responseExtractor);
    }
    
    @Override
    @Nullable
    public <T> T execute(URI url, HttpMethod method, @Nullable RequestCallback requestCallback,
    		@Nullable ResponseExtractor<T> responseExtractor) throws RestClientException {
    
    	return doExecute(url, method, requestCallback, responseExtractor);
    }
    
    /**
     * Execute the given method on the provided URI.
     * <p>The {@link ClientHttpRequest} is processed using the {@link RequestCallback};
     * the response with the {@link ResponseExtractor}.
     * @param url the fully-expanded URL to connect to
     * @param method the HTTP method to execute (GET, POST, etc.)
     * @param requestCallback object that prepares the request (can be {@code null})
     * @param responseExtractor object that extracts the return value from the response (can be {@code null})
     * @return an arbitrary object, as returned by the {@link ResponseExtractor}
     */
    @Nullable
    protected <T> T doExecute(URI url, @Nullable HttpMethod method, @Nullable RequestCallback requestCallback,
    		@Nullable ResponseExtractor<T> responseExtractor) throws RestClientException {
    
    	Assert.notNull(url, "URI is required");
    	Assert.notNull(method, "HttpMethod is required");
    	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);
    	}
    	catch (IOException ex) {
    		String resource = url.toString();
    		String query = url.getRawQuery();
    		resource = (query != null ? resource.substring(0, resource.indexOf('?')) : resource);
    		throw new ResourceAccessException("I/O error on " + method.name() +
    				" request for \"" + resource + "\": " + ex.getMessage(), ex);
    	}
    	finally {
    		if (response != null) {
    			response.close();
    		}
    	}
    }

分解org.springframework.web.client.RestTemplate#doExecute方法,做了以下幾件事:

  1. 構造請求對象
    ClientHttpRequest ClientHttpRequest request = createRequest(url, method);
  2. 處理ClientHttpRequest對象(這裏是將參數request對象中的信息篩選並拷貝到ClientHttpRequest中,做好請求的準備)
       if (requestCallback != null) {
       	requestCallback.doWithRequest(request);
       }

ps.這裏看起來還可以做很多事情,可以交給開發者來擴展,並通過execute來調用

    public <T> T execute(String url, HttpMethod method, @Nullable RequestCallback requestCallback,
          @Nullable ResponseExtractor<T> responseExtractor, Object... uriVariables){
        ..
    }
  1. 執行請求,並拿到響應對象
    response = request.execute();

  2. 處理響應對象(這裏主要是判斷是否需要處理異常,如果有異常,則交給ResponseErrorHandler處理)
    handleResponse(url, method, response);

    ps.這裏可以發現,errorHandler是可以自定義的,也是一個可擴展的地方

    public void setErrorHandler(ResponseErrorHandler errorHandler) {
       Assert.notNull(errorHandler, "ResponseErrorHandler must not be null");
       this.errorHandler = errorHandler;
    }

大致的處理流程可以看出來了,下面繼續思考步驟3.執行請求的邏輯:

這裏先講結果,至於爲什麼會這樣,後面再分析。

默認配置下,這部分的請求最終會執行到org.springframework.http.client.SimpleBufferingClientHttpRequest#executeInternal

內部邏輯就不仔細探究了,簡單說就是這部分的邏輯是用原生HttpUrlConnection發起http請求。

所以問題是,它是根據什麼來選擇發起哪種類型的http請求?

回到步驟2.ClientHttpRequest request = createRequest(url, method);

`ClientHttpRequest request = getRequestFactory().createRequest(url, method);`

而getRequestFactory()方法來自於RestTemplate所繼承的抽象父類HttpAccessor

    public ClientHttpRequestFactory getRequestFactory() {
       return this.requestFactory;
    }

默認配置下,會被賦值SimpleClientHttpRequestFactory

    private ClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();

到這裏會發現一個有意思的設計,RestTemplate繼承自org.springframework.http.client.support.InterceptingHttpAccessor,而InterceptingHttpAccessor繼承自org.springframework.http.client.support.HttpAccessor

實際上,它是利用了模板和裝飾器的設計模式爲http客戶端增加了攔截器處理的能力。

    @Override
    public ClientHttpRequestFactory getRequestFactory() {
       List<ClientHttpRequestInterceptor> interceptors = getInterceptors();
       if (!CollectionUtils.isEmpty(interceptors)) {
          ClientHttpRequestFactory factory = this.interceptingRequestFactory;
          if (factory == null) {
             factory = new InterceptingClientHttpRequestFactory(super.getRequestFactory(), interceptors);
             this.interceptingRequestFactory = factory;
          }
          return factory;
       }
       else {
          return super.getRequestFactory();
       }
    }

包裝後的execute方法

org.springframework.http.client.InterceptingClientHttpRequest.InterceptingRequestExecution#execute

    @Override
    public ClientHttpResponse execute(HttpRequest request, byte[] body) throws IOException {
       if (this.iterator.hasNext()) {
          ClientHttpRequestInterceptor nextInterceptor = this.iterator.next();
          return nextInterceptor.intercept(request, body, this);
       }
       else {
          HttpMethod method = request.getMethod();
          Assert.state(method != null, "No standard HTTP method");
          ClientHttpRequest delegate = requestFactory.createRequest(request.getURI(), method);
          request.getHeaders().forEach((key, value) -> delegate.getHeaders().addAll(key, value));
          if (body.length > 0) {
             if (delegate instanceof StreamingHttpOutputMessage) {
                StreamingHttpOutputMessage streamingOutputMessage = (StreamingHttpOutputMessage) delegate;
                streamingOutputMessage.setBody(outputStream -> StreamUtils.copy(body, outputStream));
             }
             else {
                StreamUtils.copy(body, delegate.getBody());
             }
          }
          return delegate.execute();
       }
    }

所以整理一下request.execute()的處理邏輯:

由於request是由requestFactory生產的對象

而requestFactory是一個包裹起來的對象

外層是InterceptingClientHttpRequestFactory

裏層是SimpleClientHttpRequestFactory

最終生產的request對象是InterceptingClientHttpRequest(SimpleClientHttpRequest())

Note.當然這裏是以默認配置爲例,Spring還提供了mock功能,也可以將SimpleClientHttpRequest切換成OKHttp、NettyClient、HttpComponentsClient等其他HTTP客戶端請求的方式。

到這裏RestTemplate的基本設計模式都看完了,最後簡單介紹下其內部的幾個重要的對象:

    private final List<HttpMessageConverter<?>> messageConverters = new ArrayList<>();
    
    private ResponseErrorHandler errorHandler = new DefaultResponseErrorHandler();
    
    private UriTemplateHandler uriTemplateHandler = new DefaultUriBuilderFactory();
    
    private final ResponseExtractor<HttpHeaders> headersExtractor = new HeadersExtractor();

HttpMessageConverter:上層接口,用於讀取流中的數據並轉化成相應的對象或將相應的對象寫入到流中,Spring提供了豐富的實現,包括gson/jackson/xml等解析

ResponseErrorHandler:上層接口,用於處理HTTP請求拿到的響應異常信息,有默認實現,也可以自定義。

UriTemplateHandler:上層接口,用於restTemplate調用時通過params傳參時進行的URI拼接,有默認實現,也可以自定義。

ResponseExtractor:上層接口,用於提取響應體,通過統一接口屏蔽了head類型的請求類型、T返回體、包裝類ResponseEntiry<T.>的解析過程,分別使用了org.springframework.web.client.RestTemplate.HeadersExtractororg.springframework.web.client.HttpMessageConverterExtractororg.springframework.web.client.RestTemplate.ResponseEntityResponseExtractor#ResponseEntityResponseExtractor(org.springframework.web.client.HttpMessageConverterExtractor)

=============== 分界線

可以看到RestTemplate的實現比較簡單,最主要是學習它的設計方式,相比之下,feign的實現相對就複雜多了。

首先,我們知道Feign的使用:

  1. @FeignClient/@RequestMapping定義一個提供Feign調用的接口
  2. @EnableFeignClients

原理是@EnableFeignClients import了org.springframework.cloud.openfeign.FeignClientsRegistrar

並由它進行了Feign的beanDefinition注入

    BeanDefinitionBuilder definition = BeanDefinitionBuilder
          .genericBeanDefinition(FeignClientFactoryBean.class);

最終Spring容器中的Feign bean是一個代理類,內部持有一個HardCodedTarget對象。

    @Override
    public Object getObject() throws Exception {
       return getTarget();
    }
    
    /**
     * @param <T> the target type of the Feign client
     * @return a {@link Feign} client created with the specified data and the context information
     */
    <T> T getTarget() {
       FeignContext context = applicationContext.getBean(FeignContext.class);
       Feign.Builder builder = feign(context);
    
       if (!StringUtils.hasText(this.url)) {
          String url;
          if (!this.name.startsWith("http")) {
             url = "http://" + this.name;
          }
          else {
             url = this.name;
          }
          url += cleanPath();
          return (T) loadBalance(builder, context, new HardCodedTarget<>(this.type,
                this.name, url));
       }
       if (StringUtils.hasText(this.url) && !this.url.startsWith("http")) {
          this.url = "http://" + this.url;
       }
       String url = this.url + cleanPath();
       Client client = getOptional(context, Client.class);
       if (client != null) {
          if (client instanceof LoadBalancerFeignClient) {
             // not load balancing because we have a url,
             // but ribbon is on the classpath, so unwrap
             client = ((LoadBalancerFeignClient)client).getDelegate();
          }
          builder.client(client);
       }
       Targeter targeter = get(context, Targeter.class);
       return (T) targeter.target(this, builder, context, new HardCodedTarget<>(
             this.type, this.name, url));
    }

那麼當我們使用@FeignClient接口來進行feign調用時,代碼邏輯如下(由於代碼結構比較複雜,省略代碼分析的部分):

  1. 根據Method找到一個對應的feign.InvocationHandlerFactory.MethodHandler執行

  2. 如果Method不是default方法、object原生方法,則MethodHandler類型爲feign.SynchronousMethodHandler#SynchronousMethodHandler

  3. 執行時,根據參數構造執行模板(包括url拼接信息、body信息等),然後交給retryer重試執行

  4. 執行內部邏輯包括增加攔截器處理、日誌記錄、響應消息的解碼、異常處理,而真正的執行邏輯由feign.Client#execute執行

  5. 這裏又會有default和loadbalance兩種實現,簡單說,default則是發起了http的請求並獲取了響應對象,loadbalance則是在外層增加了ribbon負載均衡(替換uri),並且增加了多種http客戶端的支持(包括OKHttp、HttpComponentsClient等)。

這其中有一些值得思考的地方:

  1. 爲什麼要使用代理對象,並且如何根據Method找到合適的handler?
  2. 在構造代理對象時,是通過一個builder串聯起來的,這樣的好處是什麼?
  3. FeignContext是feign特有的上下文,每次build的過程都會從FeignContext取配置類、encoder、client,而不是直接從Spring上下文取,爲什麼要這樣做?
  • 對於問題1.由於@FeignClient標誌的接口本身是沒有實現類的,所以必須構造代理對象,並且爲了適應不同種類的調用方式,就需要使用動態代理技術。另外,爲了隔離每個方法,Feign使用map存儲Method到handler的映射關係,並且在代理對象中將請求轉發到對應的handler處理,這樣就達到了方法間完全隔離的效果,對於每個方法的調用要做變更或加強時,只需要針對每個handler做相應的處理即可。
  • 而問題2.中的builder,是Feign bean構造過程中的核心對象,首先從FeignContext獲取默認配置並構造builder,然後根據FeignContext中的Client對象、Targeter對象,一步步串聯組裝起來,client、targeter各司其職,client負責http請求的執行,targeter則是負責代理對象的構造,這樣一來,我們要想對feign進行擴展,如,想改變http請求的執行過程,則可以通過實現Client接口;想改變代理對象的構造過程,則可以通過實現targeter接口即可。(feign-hystrix的整合僅實現了Feign.Builder接口以及實現了熔斷效果的feign.hystrix.HystrixInvocationHandler)
  • 這裏的FeignContext實現,簡單說則是包裹了Spring容器上下文,並且在內部將不同的Feign bean的配置隔離了,Feign bean在配置過程中根據@FeignClient(name=)中的name屬性隔離不同的配置,用戶在使用的過程中,可以通過configuration增加特定的配置。這樣就通過一個FeignContext讓每個Feign bean擁有獨立的上下文配置,允許靈活的控制每個Feign bean,甚至允許同一Spring容器中的不同的Feign bean使用不同的http客戶端。

新的選擇WebClient?

WebClient是webflux包中的類,是Spring5.0才推出的一個新的HttpClient,在我看來,它的功能完全囊括了RestTemplate並且新增了異步請求功能,api則是完全的鏈式風格,寫起來非常舒服。

Spring 官網是這樣介紹它的:

Spring WebFlux includes a reactive, non-blocking WebClient for HTTP requests. The client has a functional, fluent API with reactive types for declarative composition, see Reactive Libraries. WebFlux client and server rely on the same non-blocking codecs to encode and decode request and response content.

Internally WebClient delegates to an HTTP client library. By default, it uses Reactor Netty, there is built-in support for the Jetty reactive HttpClient, and others can be plugged in through a ClientHttpConnector.

它與RestTemplate不同的主要點在於,它是非阻塞、響應式的,且擁有流式api,而它的內置HTTP client默認使用Reactor Netty。

源碼的部分不做分析了,下面用幾個使用WebClient的小例子,感受一下它和RestTemplate的區別,以及簡單對比下與RestTemplate的性能差異。

定義一個服務端接口,用於被調用:

    /**
     * server端接口
     * @param sleep
     * @return
     * @throws InterruptedException
     */
    @GetMapping("/user")
    public ResponseEntity<User> user(@RequestParam("sleep") long sleep) throws InterruptedException {
        /**
         * 模擬請求耗時
         */
        Thread.sleep(sleep);
        return ResponseEntity.ok(new User());
    }

在同一服務編寫測試程序:

    private static final String URL = "http://localhost:9090/user";
    private static final String SLEEP_KEY = "sleep";
    private static final long SLEEP = 100;
    private static final String GET_URL = URL + "?" + SLEEP_KEY + "=" + SLEEP;
    
    private static final String ERROR_MESSAGE = "client request failed.";
    private static final RuntimeException ERROR = new RuntimeException(ERROR_MESSAGE);
    
    public static User execute(List<Long> metric , FunctionX<User> fx) throws InterruptedException {
            long start = System.currentTimeMillis();
            User returnVal = fx.apply();
            long end = System.currentTimeMillis();
            metric.add(end - start);
            return returnVal;
    }
    
    @GetMapping("/rt/test")
    public List<Long> test() throws InterruptedException {
        execute(metric, ()->{
            for(int i = 0; i < 10; i++){
                ResponseEntity<User> response = restTemplate.exchange(GET_URL, HttpMethod.GET, null, User.class);
                if(response.getStatusCode() == HttpStatus.OK){
                    //ignore
                }else{
                    throw ERROR;
                }
            }
            return null;
        });
        return metric;
    }
    
    @GetMapping("/rt/get")
    public User get() throws InterruptedException {
                return execute(metric, ()->{
                    ResponseEntity<User> response = restTemplate.exchange(GET_URL, HttpMethod.GET, null, User.class);
                    if(response.getStatusCode() == HttpStatus.OK){
                        return response.getBody();
                    }else{
                        throw ERROR;
                    }
                });
    }

單線程分別發起十次請求(server端請求耗時100ms):

RestTemplate

[343,117,107,112,107,106,116,112,105,108]

WebClient

[896,118,114,111,107,116,107,115,111,110]

可以看到,第一次請求的耗時相對較長,推測是進行了一些初始化的操作,而後面的處理耗時差異很小,WebClient稍差於RestTemplate。

爲了對比出異步能帶來某些場景下的性能優勢,對於每次請求,都讓RestTempate循環請求十次,並記錄耗時;對於每次請求,讓WebClient發起十個異步請求,當異步請求均成功後認爲請求完成,並記錄耗時。

    @GetMapping("/rt/test")
    public List<Long> test() throws InterruptedException {
        execute(metric, ()->{
            for(int i = 0; i < 10; i++){
                ResponseEntity<User> response = restTemplate.exchange(GET_URL, HttpMethod.GET, null, User.class);
                if(response.getStatusCode() == HttpStatus.OK){
                    //ignore
                }else{
                    throw ERROR;
                }
            }
            return null;
        });
        return metric;
    }
    
    @GetMapping("/wc/test")
    public List<Long> test() throws InterruptedException {
                CountDownLatch latch = new CountDownLatch(10);
                List<User> users = new ArrayList<>();
                execute(metric, ()->{
                    for(int i = 0; i < 10; i++){
                        WebClient.create(GET_URL)
                                .method(HttpMethod.GET)//.get()
                                .retrieve()
                                .bodyToMono(User.class)
                                .toFuture()
                                .whenComplete((u, t) -> {
                                    users.add(u);
                                    latch.countDown();
                                });
                    }
                    latch.await();
                    return null;
                });
                return metric;
    }

測試10次,觀察WebClient能否帶來較明顯的性能優勢(ps.爲了證明WebClient並不是一無是處的!):

RestTemplate

[1293,1066,1049,1051,1050,1050,1049,1045,1040,1039]

WebClient

[1306,138,133,131,139,146,123,130,118,119]

所以,對於某些場景,由於異步的支持,是可以提升性能的。

當然,我們可以自己增加一個線程池來讓RestTemplate異步處理,這樣做也能達到相同的性能提升,但增加的代碼和複雜度是很明顯的。

首先WebClient是webflux包的,這也是很多項目都接觸不到它的原因之一,從使用感受上說,WebClient明顯寫起來更舒服,用簡潔代碼描述複雜邏輯,最重要的是它支持異步,雖然也因此略微影響到了性能,但實際上對於大部分場景都是不影響的。總的來說,WebClient功能更強大,之所以還沒有取代RestTemplate,是因爲webflux包還沒有被廣泛使用。從文檔上看,Spring是極力推崇reactive的,事實是,異步雖好,但也難控,用的好是一把利劍,用的不好會造成性能瓶頸。

從Spring源碼中我看到了什麼

綜合前面的幾章的分析,列舉我從Spring源碼中悟到的幾個道理或是經驗:

  1. 想熟讀Spring源碼,必須學會這幾個關鍵字wrapper、delegate、factory、proxy、interceptor、builder、template,另外對於handler、converter、extractor、parser、retryer這幾個詞也要能區分其含義
  2. Spring作爲框架,只是粘合各種功能強大的“工具”,所以學習Spring源碼,更多的是教會我們如何實現靈活、易擴展、可增強的抽象層,而不是如何提升底層工具的性能
  3. Spring源碼看似是把簡單問題複雜化了,其實是對高內聚的代碼的分解,作爲開發人員,我們應該學會這樣的思想
  4. Spring非常喜歡reactive(肯定是!至少我從文檔上的理解是這樣的)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章