您好,登錄后才能下訂單哦!
微服務調用應答返回時報ClassCastException問題的實例分析,很多新手對此不是很清楚,為了幫助大家解決這個難題,下面小編將為大家詳細講解,有這方面需求的人可以來學習下,希望你能有所收獲。
問題復現demo在這里。
前幾天被拉去看一個問題。某服務(后面稱其為A服務)采用同步模式運行,RPC方式調用其他微服務。在本地調試無問題,線上運行時此服務調用另外一個服務(后面稱其為B服務)的接口會報錯,且通過他們自定義擴展的一個HttpClientFilter
的日志來看,被調用的provider服務已經正常返回了應答消息,但是在后面會報ClassCastException
,無法將InvocationException
轉型為業務代碼的返回值類型。日志如下:
// 業務邏輯被調用 [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服務還是舊版本的沒有升級,于是他們把A服務依賴的B服務的接口jar包替換成了低版本來啟動的。
初步接觸這個問題給人一種很怪異的感覺。如果一個consumer調用provider時都已經拿到了應答,那么會直接把應答返回給consumer的業務邏輯代碼;萬一中間真的出錯了,那產生的InvocationException
也應該是被“拋”出去的,而不是像日志里面顯示的那樣,嘗試“返回”給consumer的業務邏輯才對。
可供分析的信息太少了,只能回頭看一下sdk代碼的相關邏輯,看看能不能復現出這個問題。
RPC調用模式的微服務里,業務邏輯通過provider接口做調用時,實際是通過ServiceComb生成的provider接口類型的代理來做調用的。而在這個代理的背后,實際調用流程的源頭在org.apache.servicecomb.provider.pojo.Invoker
類里面。同步調用模式下,區分應答如何被返回給業務邏輯的關鍵代碼在syncInvoke
方法里:
protected Object syncInvoke(Invocation invocation, SwaggerConsumerOperation consumerOperation) { Response response = InvokerUtils.innerSyncInvoke(invocation); if (response.isSuccessed()) { // 在這里,response內的result會作為正常應答返回給業務邏輯 return consumerOperation.getResponseMapper().mapResponse(response); } // 這里是異常邏輯,response內的result即為錯誤信息,會被包裝為InvocationException拋給業務邏輯 throw ExceptionFactory.convertConsumerException(response.getResult()); }
出現了線上日志中的錯誤說明這個方法沒有走到throw語句,而是走return語句那里返回了。
InvokerUtils.innerSyncInvoke()
方法里觸發的主要流程是Handler->HttpClientFilter->網絡線程,既然在用戶自定義的HTTPClientFilter
實現類的afterReceiveResponse()
方法中已經打印出了B服務返回的應答消息,那么網絡線程部分的嫌疑就可以排除了。問題只可能出在Invoker
、Handler
、HTTPClientFilter
這三塊。這個異常需要被catch住并塞到response
里。同時,為了讓異常作為response body返回,而不是被“拋”出去,response.isSuccessed()
需要返回true
,這就要求response
的Http狀態碼必須是2xx的。通過在demo中加入自定義的HttpClientFilter
,在afterReceiveResponse()
方法中拋出一個狀態碼為200的InvocationException
,我們復現出了這個問題,其日志特征與A服務的線上日志一致。
一個response,里面裝著一個異常,Http狀態碼卻是2xx的,這個場景應該是不會發生的才對。在向A服務的開發同學確認了他們沒有在自定義的Handler
、HttpClientFilter
內直接操作response后,我們通過日志也無法給出問題結論,只能等A服務的開發同學本地復現問題場景了。
好在這個問題本地是能夠復現出來的,根因是在于A服務依賴的B服務接口jar包被替換后,舊版本的業務接口應答類型比新版本的多一個屬性,而且這個屬性的類型是找不到的,大致像下面這樣:
class ResponseType { private InnerFieldType someField; // 這里的InnerFieldType會報ClassNotFound }
于是當DefaultHttpClientFilter
的extractResult()
方法嘗試將Http body中的json串反序列化為業務代碼中的應答對象時,會拋出一個異常,而這個異常被包裝成InvocationException
后,是被“return”回去的,而不是“throw”出去的,并且這個過程中沒有打印任何日志。關鍵代碼在DefaultHttpClientFilter
的85-89行:
try { return produceProcessor.decodeResponse(responseEx.getBodyBuffer(), responseMeta.getJavaType()); } catch (Exception e) { return ExceptionFactory.createConsumerException(e); // 異常被返回 }
“return”回去的異常被作為正常的應答對象塞進了response
中,而response
的狀態碼是Http應答的狀態碼——200,于是就有了線上碰到的錯誤。
ServiceComb框架在此次定位過程中暴露出來的缺少日志的問題會在后續版本中修復。但是對于開發者而言,更重要的是服務上線部署前需要做好充分驗證,臨時替換依賴jar包這種簡單粗暴的處理方式不可取。
那么本地調試過程中碰到這種問題應該如何定位呢?以本文所描述的場景(RPC調用方式,同步運行模式)來看,當業務代碼中觸發一次微服務調用,ServiceComb的處理流程大致是:
Invoker -> InvokerUtils -> Handler -> HTTPClientFilter -> 網絡線程
Invoker
是RPC調用模式下的動態代理,業務代碼通過provider接口做調用時,參數首先被傳到invoke()
方法中。由于consumer工作于同步模式,Invoker
會通過syncInvoke()
方法調用InvokerUtils
的innerSyncInvoke()
方法。在這里,Invocation
的next()
方法被調用,從而觸發Handler鏈執行。在Handler鏈的末尾是TransportClientHandler
,它會調用對應的transport方式發送請求。在Rest over Vertx傳輸方式下,我們需要關注的是RestClientInvocation
的invoke()
方法,這里會遍歷執行HttpClientFilter的beforeSendRequest()方法,然后將請求調度到網絡線程中發送。業務線程此時處于等待返回的狀態(SyncResponseExecutor.waitResponse()
方法中使用CountDownLatch
進行等待)。
當請求應答返回后,RestClientInvocation.processResponseBody()
方法會將Http response body返回給業務線程處理(通過觸發SyncResponseExecutor
的CountDownLatch
)。應答首先會在RestClientInvocation
中遍歷HttpClientFilter的afterReceiveResponse()方法進行處理,然后經過Handler鏈的回調處理,最終返回給InvokerUtils
的syncInvoke()
方法。其中,Http response body是在DefaultHttpClientFilter的extractResult()方法中反序列化為業務接口返回對象的。這個方法會根據response
的HTTP狀態碼判斷如何對待結果,如果是2xx的狀態碼,則response
中的result
會作為正常的應答返回給業務邏輯,否則會將result
包裝到InvocationException
中拋給業務邏輯。
// RestClientInvocation中處理應答的關鍵方法 protected void processResponseBody(Buffer responseBuf) { invocation.getResponseExecutor().execute(() -> { // 同步模式下,應答返回流程從這里開始就是在業務線程里執行的 try { HttpServletResponseEx responseEx = new VertxClientResponseToHttpServletResponse(clientResponse, responseBuf); for (HttpClientFilter filter : httpClientFilters) { // HttpClientFilter處理返回消息體,普通的filter會返回null Response response = filter.afterReceiveResponse(invocation, responseEx); if (response != null) { // DefaultHttpClientFilter會把消息體反序列化為應答對象,裝入response返回 asyncResp.complete(response); // 通過回調觸發handler鏈 return; } } } catch (Throwable e) { asyncResp.fail(invocation.getInvocationType(), e); // 包裝異常,通過回調觸發handler鏈 } }); }
本地分析這類問題的時候,首先需要知道請求發送的流程,了解RPC動態代理的入口、Handler
鏈的起止點、HttpClientFilter
的調用點。這些是流程中的關鍵節點,根據這些信息可以大致確定問題出現的范圍。至于更進一步的定位,就需要大家根據具體的問題進行分析了。
看完上述內容是否對您有幫助呢?如果還想對相關知識有進一步的了解或閱讀更多相關文章,請關注億速云行業資訊頻道,感謝您對億速云的支持。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。