溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

微服務(wù)調(diào)用應(yīng)答返回時(shí)報(bào)ClassCastException問題的實(shí)例分析

發(fā)布時(shí)間:2022-01-05 10:57:16 來源:億速云 閱讀:135 作者:柒染 欄目:云計(jì)算

微服務(wù)調(diào)用應(yīng)答返回時(shí)報(bào)ClassCastException問題的實(shí)例分析,很多新手對(duì)此不是很清楚,為了幫助大家解決這個(gè)難題,下面小編將為大家詳細(xì)講解,有這方面需求的人可以來學(xué)習(xí)下,希望你能有所收獲。

問題描述

問題復(fù)現(xiàn)demo在這里。

前幾天被拉去看一個(gè)問題。某服務(wù)(后面稱其為A服務(wù))采用同步模式運(yùn)行,RPC方式調(diào)用其他微服務(wù)。在本地調(diào)試無問題,線上運(yùn)行時(shí)此服務(wù)調(diào)用另外一個(gè)服務(wù)(后面稱其為B服務(wù))的接口會(huì)報(bào)錯(cuò),且通過他們自定義擴(kuò)展的一個(gè)HttpClientFilter的日志來看,被調(diào)用的provider服務(wù)已經(jīng)正常返回了應(yīng)答消息,但是在后面會(huì)報(bào)ClassCastException,無法將InvocationException轉(zhuǎn)型為業(yè)務(wù)代碼的返回值類型。日志如下:

// 業(yè)務(wù)邏輯被調(diào)用
[INFO] test() is called! com.github.yhs0092.blogdemo.javachassis.service.ConsumerService.test(ConsumerService.java:20)
// 用戶自定義的HttpClientFilter中打印了provider返回的消息
[INFO] get response, status[200], content is [{"content":"returnOK"}] com.github.yhs0092.blogdemo.javachassis.filter.PrintResponseFilter.afterReceiveResponse(PrintResponseFilter.java:26)
// ClassCastException被拋出
[ERROR] invoke failed, invocation=PRODUCER rest client.consumer.test org.apache.servicecomb.swagger.invocation.exception.DefaultExceptionToResponseConverter.convert(DefaultExceptionToResponseConverter.java:35)
java.lang.ClassCastException: org.apache.servicecomb.swagger.invocation.exception.InvocationException cannot be cast to com.github.yhs0092.blogdemo.javachassis.service.TestResponse
	at com.sun.proxy.$Proxy30.test(Unknown Source)
	at com.github.yhs0092.blogdemo.javachassis.service.ConsumerService.test(ConsumerService.java:21)
	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
	at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
	at java.lang.reflect.Method.invoke(Method.java:498)
	at org.apache.servicecomb.swagger.engine.SwaggerProducerOperation.doInvoke(SwaggerProducerOperation.java:160)
	at org.apache.servicecomb.swagger.engine.SwaggerProducerOperation.syncInvoke(SwaggerProducerOperation.java:148)
	at org.apache.servicecomb.swagger.engine.SwaggerProducerOperation.invoke(SwaggerProducerOperation.java:115)
	at org.apache.servicecomb.core.handler.impl.ProducerOperationHandler.handle(ProducerOperationHandler.java:40)

分析問題的過程中,他們提到由于線上的B服務(wù)還是舊版本的沒有升級(jí),于是他們把A服務(wù)依賴的B服務(wù)的接口jar包替換成了低版本來啟動(dòng)的。

分析過程

初步接觸這個(gè)問題給人一種很怪異的感覺。如果一個(gè)consumer調(diào)用provider時(shí)都已經(jīng)拿到了應(yīng)答,那么會(huì)直接把應(yīng)答返回給consumer的業(yè)務(wù)邏輯代碼;萬一中間真的出錯(cuò)了,那產(chǎn)生的InvocationException也應(yīng)該是被“拋”出去的,而不是像日志里面顯示的那樣,嘗試“返回”給consumer的業(yè)務(wù)邏輯才對(duì)。

可供分析的信息太少了,只能回頭看一下sdk代碼的相關(guān)邏輯,看看能不能復(fù)現(xiàn)出這個(gè)問題。

RPC調(diào)用模式的微服務(wù)里,業(yè)務(wù)邏輯通過provider接口做調(diào)用時(shí),實(shí)際是通過ServiceComb生成的provider接口類型的代理來做調(diào)用的。而在這個(gè)代理的背后,實(shí)際調(diào)用流程的源頭在org.apache.servicecomb.provider.pojo.Invoker類里面。同步調(diào)用模式下,區(qū)分應(yīng)答如何被返回給業(yè)務(wù)邏輯的關(guān)鍵代碼在syncInvoke方法里:

protected Object syncInvoke(Invocation invocation, SwaggerConsumerOperation consumerOperation) {
  Response response = InvokerUtils.innerSyncInvoke(invocation);
  if (response.isSuccessed()) {
    // 在這里,response內(nèi)的result會(huì)作為正常應(yīng)答返回給業(yè)務(wù)邏輯
    return consumerOperation.getResponseMapper().mapResponse(response);
  }
  // 這里是異常邏輯,response內(nèi)的result即為錯(cuò)誤信息,會(huì)被包裝為InvocationException拋給業(yè)務(wù)邏輯
  throw ExceptionFactory.convertConsumerException(response.getResult());
}

出現(xiàn)了線上日志中的錯(cuò)誤說明這個(gè)方法沒有走到throw語句,而是走return語句那里返回了。

InvokerUtils.innerSyncInvoke()方法里觸發(fā)的主要流程是Handler->HttpClientFilter->網(wǎng)絡(luò)線程,既然在用戶自定義的HTTPClientFilter實(shí)現(xiàn)類的afterReceiveResponse()方法中已經(jīng)打印出了B服務(wù)返回的應(yīng)答消息,那么網(wǎng)絡(luò)線程部分的嫌疑就可以排除了。問題只可能出在InvokerHandler、HTTPClientFilter這三塊。這個(gè)異常需要被catch住并塞到response里。同時(shí),為了讓異常作為response body返回,而不是被“拋”出去,response.isSuccessed()需要返回true,這就要求response的Http狀態(tài)碼必須是2xx的。通過在demo中加入自定義的HttpClientFilter,在afterReceiveResponse()方法中拋出一個(gè)狀態(tài)碼為200的InvocationException,我們復(fù)現(xiàn)出了這個(gè)問題,其日志特征與A服務(wù)的線上日志一致。

根因確定

一個(gè)response,里面裝著一個(gè)異常,Http狀態(tài)碼卻是2xx的,這個(gè)場(chǎng)景應(yīng)該是不會(huì)發(fā)生的才對(duì)。在向A服務(wù)的開發(fā)同學(xué)確認(rèn)了他們沒有在自定義的Handler、HttpClientFilter內(nèi)直接操作response后,我們通過日志也無法給出問題結(jié)論,只能等A服務(wù)的開發(fā)同學(xué)本地復(fù)現(xiàn)問題場(chǎng)景了。

好在這個(gè)問題本地是能夠復(fù)現(xiàn)出來的,根因是在于A服務(wù)依賴的B服務(wù)接口jar包被替換后,舊版本的業(yè)務(wù)接口應(yīng)答類型比新版本的多一個(gè)屬性,而且這個(gè)屬性的類型是找不到的,大致像下面這樣:

class ResponseType {
  private InnerFieldType someField; // 這里的InnerFieldType會(huì)報(bào)ClassNotFound
}

于是當(dāng)DefaultHttpClientFilterextractResult()方法嘗試將Http body中的json串反序列化為業(yè)務(wù)代碼中的應(yīng)答對(duì)象時(shí),會(huì)拋出一個(gè)異常,而這個(gè)異常被包裝成InvocationException后,是被“return”回去的,而不是“throw”出去的,并且這個(gè)過程中沒有打印任何日志。關(guān)鍵代碼在DefaultHttpClientFilter的85-89行:

try {
  return produceProcessor.decodeResponse(responseEx.getBodyBuffer(), responseMeta.getJavaType());
} catch (Exception e) {
  return ExceptionFactory.createConsumerException(e); // 異常被返回
}

“return”回去的異常被作為正常的應(yīng)答對(duì)象塞進(jìn)了response中,而response的狀態(tài)碼是Http應(yīng)答的狀態(tài)碼——200,于是就有了線上碰到的錯(cuò)誤。

總結(jié)

ServiceComb框架在此次定位過程中暴露出來的缺少日志的問題會(huì)在后續(xù)版本中修復(fù)。但是對(duì)于開發(fā)者而言,更重要的是服務(wù)上線部署前需要做好充分驗(yàn)證,臨時(shí)替換依賴jar包這種簡(jiǎn)單粗暴的處理方式不可取。

那么本地調(diào)試過程中碰到這種問題應(yīng)該如何定位呢?以本文所描述的場(chǎng)景(RPC調(diào)用方式,同步運(yùn)行模式)來看,當(dāng)業(yè)務(wù)代碼中觸發(fā)一次微服務(wù)調(diào)用,ServiceComb的處理流程大致是:

Invoker -> InvokerUtils -> Handler -> HTTPClientFilter -> 網(wǎng)絡(luò)線程

Invoker是RPC調(diào)用模式下的動(dòng)態(tài)代理,業(yè)務(wù)代碼通過provider接口做調(diào)用時(shí),參數(shù)首先被傳到invoke()方法中。由于consumer工作于同步模式,Invoker會(huì)通過syncInvoke()方法調(diào)用InvokerUtilsinnerSyncInvoke()方法。在這里,Invocationnext()方法被調(diào)用,從而觸發(fā)Handler鏈執(zhí)行。在Handler鏈的末尾是TransportClientHandler,它會(huì)調(diào)用對(duì)應(yīng)的transport方式發(fā)送請(qǐng)求。在Rest over Vertx傳輸方式下,我們需要關(guān)注的是RestClientInvocationinvoke()方法,這里會(huì)遍歷執(zhí)行HttpClientFilter的beforeSendRequest()方法,然后將請(qǐng)求調(diào)度到網(wǎng)絡(luò)線程中發(fā)送。業(yè)務(wù)線程此時(shí)處于等待返回的狀態(tài)(SyncResponseExecutor.waitResponse()方法中使用CountDownLatch進(jìn)行等待)。

當(dāng)請(qǐng)求應(yīng)答返回后,RestClientInvocation.processResponseBody()方法會(huì)將Http response body返回給業(yè)務(wù)線程處理(通過觸發(fā)SyncResponseExecutorCountDownLatch)。應(yīng)答首先會(huì)在RestClientInvocation遍歷HttpClientFilter的afterReceiveResponse()方法進(jìn)行處理,然后經(jīng)過Handler鏈的回調(diào)處理,最終返回給InvokerUtilssyncInvoke()方法。其中,Http response body是在DefaultHttpClientFilter的extractResult()方法中反序列化為業(yè)務(wù)接口返回對(duì)象的。這個(gè)方法會(huì)根據(jù)response的HTTP狀態(tài)碼判斷如何對(duì)待結(jié)果,如果是2xx的狀態(tài)碼,則response中的result會(huì)作為正常的應(yīng)答返回給業(yè)務(wù)邏輯,否則會(huì)將result包裝到InvocationException中拋給業(yè)務(wù)邏輯。

  // RestClientInvocation中處理應(yīng)答的關(guān)鍵方法
  protected void processResponseBody(Buffer responseBuf) {
    invocation.getResponseExecutor().execute(() -> {
      // 同步模式下,應(yīng)答返回流程從這里開始就是在業(yè)務(wù)線程里執(zhí)行的
      try {
        HttpServletResponseEx responseEx =
            new VertxClientResponseToHttpServletResponse(clientResponse, responseBuf);
        for (HttpClientFilter filter : httpClientFilters) {
          // HttpClientFilter處理返回消息體,普通的filter會(huì)返回null
          Response response = filter.afterReceiveResponse(invocation, responseEx);
          if (response != null) { // DefaultHttpClientFilter會(huì)把消息體反序列化為應(yīng)答對(duì)象,裝入response返回
            asyncResp.complete(response); // 通過回調(diào)觸發(fā)handler鏈
            return;
          }
        }
      } catch (Throwable e) {
        asyncResp.fail(invocation.getInvocationType(), e); // 包裝異常,通過回調(diào)觸發(fā)handler鏈
      }
    });
  }

本地分析這類問題的時(shí)候,首先需要知道請(qǐng)求發(fā)送的流程,了解RPC動(dòng)態(tài)代理的入口、Handler鏈的起止點(diǎn)、HttpClientFilter的調(diào)用點(diǎn)。這些是流程中的關(guān)鍵節(jié)點(diǎn),根據(jù)這些信息可以大致確定問題出現(xiàn)的范圍。至于更進(jìn)一步的定位,就需要大家根據(jù)具體的問題進(jìn)行分析了。

看完上述內(nèi)容是否對(duì)您有幫助呢?如果還想對(duì)相關(guān)知識(shí)有進(jìn)一步的了解或閱讀更多相關(guān)文章,請(qǐng)關(guān)注億速云行業(yè)資訊頻道,感謝您對(duì)億速云的支持。

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI