直入主題,線上應用發現,偶發性出現以下異常日誌java
固然因爲線上具體異常包含信息量過大,秉承讓肥朝的粉絲沒有難調試的代碼
的原則,我特地抽取了一個復現的demo放在了git,讓你不在現場,同樣享受到排查的快樂!可是最近,太多假粉伸手黨拿到地址就跑,所以我把地址藏在本文某個角落,所以認真看文的才能找到!(重點)git
因爲工做性質的緣由,上班時間根本抽不出時間作其餘事,修bug,都只能下班時間來作,所以週六就到公司搬磚了。面試
中文意思就是,併發修改異常
。也就是咱們常說的fail-fast
(快速失敗)。固然肥朝更認爲,快速失敗
是一種思想,好比Spring會在啓動的時候作大量的檢查,什麼bean找不到,依賴注入錯誤等等,都會把一些顯而易見的錯誤檢查出來,防止在項目跑着跑着期間再失敗,也就是提早檢查。不管是業務開發,仍是基礎組件開發,亦或是生活中,這個思想都是能夠用到的。apache
那麼,言歸正傳,這個異常到底什麼意思啊。簡單說就是,當一個集合在遍歷的時候,他的元素也正在被修改。剛學java那會,咱們邊遍歷邊刪除就會出現這個異常。ConcurrentModificationException
的原理這些網上太多,肥朝就暫且不提。那麼咱們來看下異常棧。api
好了,咱們已經找到了RpcContext.getContext().getObjectAttachments()
正在遍歷。那麼,只要找到誰在修改他就好了啊,就這?微信
很明顯,這裏面並不存在遍歷的同時修改元素,Dubbo的代碼還不至於有這個明顯的bug。出現ConcurrentModificationException
,就有多是,A線程在遍歷,B線程在修改。併發
可是肥朝,你說了這麼多,我仍是沒發現這個問題有什麼難的啊!app
這個問題難點主要在於,在Dubbo裏面,RpcContext
是對應一個線程的,你能夠簡單理解爲ThreadLocal
的加強版。也就是說,A線程拿出來的,和B線程拿出來的RpcContext
都不是同一個,何來併發修改同一個之說?固然官方文檔給了我一個啓示異步
會不會有同窗在線程開啓前拿到RpcContext
,而後在新線程中,作set操做(圖中的get操做是沒有問題的)。async
因而,彷佛豁然開朗的我,順着這條線索,週六加了一天班,把代碼翻了個遍,最後發現沒有找到。
併發這東西,要麼不出問題,一旦出問題都是很難找。觀察了線上日誌,重現機率很小,就一小段日誌,而且業務方很忙,也沒時間配合你查問題。因而只能順着源碼,把Dubbo的整個請求到響應的過程在腦海中快速過幾遍,看看哪一個環節有可能出問題,作了無數的假設。隨着一次次的假設失敗,在即將身體索然無味
之際,還真發現了一些蛛絲馬跡!(注意,本文所用到的,都是dubbo2.7.6)
咱們先來看一下官方文檔對RpcContext
的介紹
好了,那麼我問你,下面這段代碼,love
能輸出什麼?
@Service
public class AHelloServiceImpl implements AHelloService {
@Reference
private BHelloService bHelloService;
@Override
public String sayHello() throws Exception{
RpcContext.getContext().setAttachment("我最愛的人是?","肥朝");
bHelloService.sayHello();
String love = RpcContext.getContext().getAttachment("我最愛的人是?");
System.out.println("this is: " + love);
Thread.sleep(10L);
bHelloService.sayHello();
return "歡迎關注微信公衆號:肥朝";
}
}
我在圖都圈得這麼明顯了,看得懂中文都知道,發起一次遠程調用後,參數會被清空,下面確定get不到的啦。可是實際上是get獲得的,不要問肥朝爲何都知道圖是有問題的,還特地圈起來騙你,我只想讓你知道社會險惡。
閱讀過源碼,和對源碼有細節深刻思考,效果是很大不同的。
咱們來看一下源碼就知道了。文中說的會清除,對應的代碼是怎麼樣的呢?
若是做爲正常的客戶端調用,那麼,在調用後確實是會刪除的。可是若是你對源碼細節足夠熟悉你就會發現,在org.apache.dubbo.rpc.filter.ContextFilter
這個類中
你不看代碼直接聽我說也行,這幾段代碼的意思是,在一個提供者的方法中,canRemove
會設置爲false的,因此,他們在這個方法體遠程調用中,是沒辦法清空RpcContext
的,須要在總體調用完纔會清空。
咱們再回顧一下案發現場
@Override
public String sayHello() throws Exception{
bHelloService.sayHello();
Thread.sleep(10L);
bHelloService.sayHello();
return "歡迎關注微信公衆號:肥朝";
}
從目前獲得的信息很明顯知道,第一次遠程調用,和第二次遠程調用,用的是同一個RpcContext
,而且,在第二次遠程調用的時候。這個RpcContext
的內容,給人動了手腳了。
那麼,到底是何人所爲!咱們隨着鏡頭,再次深刻源碼!既然是RpcContext
給人搞了,那麼咱們就從這裏順藤摸瓜,這裏先省略肥朝的心裏戲,咱們來看重點。在RpcContext
中發現一段可疑片斷
public static void restoreContext(RpcContext oldContext) {
LOCAL.set(oldContext);
}
接着繼續順豐摸瓜,發現調用這段代碼的邏輯是
/**
* tmp context to use when the thread switch to Dubbo thread.
*/
private RpcContext tmpContext;
private RpcContext tmpServerContext;
private BiConsumer<Result, Throwable> beforeContext = (appResponse, t) -> {
tmpContext = RpcContext.getContext();
tmpServerContext = RpcContext.getServerContext();
RpcContext.restoreContext(storedContext);
RpcContext.restoreServerContext(storedServerContext);
};
private BiConsumer<Result, Throwable> afterContext = (appResponse, t) -> {
RpcContext.restoreContext(tmpContext);
RpcContext.restoreServerContext(tmpServerContext);
};
public Result whenCompleteWithContext(BiConsumer<Result, Throwable> fn) {
this.responseFuture = this.responseFuture.whenComplete((v, t) -> {
beforeContext.accept(v, t);
fn.accept(v, t);
afterContext.accept(v, t);
});
return this;
}
@Override
public Result invoke(Invocation invocation) throws RpcException {
Result asyncResult;
try {
interceptor.before(next, invocation);
asyncResult = interceptor.intercept(next, invocation);
} catch (Exception e) {
// onError callback
if (interceptor instanceof ClusterInterceptor.Listener) {
ClusterInterceptor.Listener listener = (ClusterInterceptor.Listener) interceptor;
listener.onError(e, clusterInvoker, invocation);
}
throw e;
} finally {
interceptor.after(next, invocation);
}
return asyncResult.whenCompleteWithContext((r, t) -> {
// onResponse callback
if (interceptor instanceof ClusterInterceptor.Listener) {
ClusterInterceptor.Listener listener = (ClusterInterceptor.Listener) interceptor;
if (t == null) {
listener.onMessage(r, clusterInvoker, invocation);
} else {
listener.onError(t, clusterInvoker, invocation);
}
}
});
}
看不懂代碼不要怕,肥朝大白話解釋一下。你就想象一個Dubbo異步場景,Dubbo異步回調結果的時候,是會開啓一個新的線程,那麼,這個回調就和當初請求不在一個線程裏面了,所以這個回調線程是拿不到當初請求的RpcContext
。可是咱們清空RpcContext
是須要在一次請求結束的時候,也就是說,雖然異步回調是另一個線程了,可是咱們仍然須要拿到當初請求時候的RpcContext
來走Filter
,作清空等操做。上面那段代碼就是作,切換線程怎麼拿回以前的RpcContext
。
聽完上面的分析,你是否是明白了點啥?新線程,還能拿到舊的RpcContext
。那麼,有這麼一個場景,咱們在經過提供者方法中,發起兩個異步請求,第一個請求走Filter
的onResponse
(響應結果)的時候,咱們若是在Filter
作RpcContext.getContext().setAttachment
操做,第二個請求又正好發起,而發起又會經歷putAll
這步驟,就會出現這個併發修改異常。因而乎,真相大白!
具體詳情,親自調試一番就會清楚,肥朝公衆號回覆modification
獲取git地址
真相大白就結束了?熟悉肥朝的粉絲都知道,咱們遇到問題,要儘可能壓榨問題的所有價值!好比,你說不要在攔截器中onResponse
方法中用RpcContext.getContext().setAttachment
這樣的操做,可是咱們確實有相似須要,那到底要怎麼寫代碼又不說,你這樣叫我怎麼給你轉發文章!
咱們要知道怎麼正確寫代碼,那直接去抄Dubbo其餘攔截器的代碼不就知道了?好比
@Activate(group = PROVIDER, order = -10000)
public class ContextFilter implements Filter, Filter.Listener {
@Override
public void onResponse(Result appResponse, Invoker<?> invoker, Invocation invocation) {
// pass attachments to result
appResponse.addObjectAttachments(RpcContext.getServerContext().getObjectAttachments());
}
}
咱們很明顯看到,你熟悉一下appResponse
的api和他的做用,就很容易知道,有相似需求,代碼應該怎麼寫了。我光告訴你怎麼寫代碼沒用啊,我要告訴你,遇到問題,怎麼去抄正確代碼,讓你任什麼時候候,都有得cao!
和上一次的【面試官問我,使用Dubbo有沒有遇到一些坑?我笑了。】不同,此次雖然把問題分析並獲得解決,可是此次我笑不出來,由於,這個bug我也有一部分緣由。。。