誰再悄咪咪的吃掉異常,我上去就是一 JIO

又到週末了,周更選手申請出站~html

誰再悄咪咪的吃掉異常,我上去就是一 JIO
這圖太魔性了啊java

此次分享一下上個月碰到的離奇的問題。一個簡單的問題,硬是由於異常被悄咪咪吃掉,過關難度直線提高,致使小黑哥排查一個晚上。git

這個美好的晚上,本想着開兩把 LOL 無限火力,在召喚師峽谷來個五殺的~github

哎,就這樣沒了啊!我知道,大家必定能理解這種五殺被搶的感受~apache

下次,真的,誰再讓我看到悄咪咪的吃掉異常,我真的要上去一 Jio 了!框架

誰再悄咪咪的吃掉異常,我上去就是一 JIO

好了,本文可不是水文,看完本篇文章,你能夠學到如下知識點:ide

  • Arthas 排查技巧
  • 啥是 NoClassDefFoundError
  • Dubbo 異常內部處理方式

好了,同窗們,打開小本子,準備記好知識點~工具

誰再悄咪咪的吃掉異常,我上去就是一 JIO

原由

咱們有個業務系統,應用之間調用鏈以下所示:測試

誰再悄咪咪的吃掉異常,我上去就是一 JIO

A 應用是業務發生起始應用,在這個應用中將會根據必定規則選擇最後的通信渠道 C,而後將這個渠道標識傳遞給 B 應用。插件

B 應用的功能相似網關,這個應用將會根據 A 應用傳遞過來的渠道標識,將會請求路由下發到具體的 C 應用,起到服務路由的功能。

C 應用是與外部應用交互的應用,咱們將其稱爲渠道通信機。

假設一次業務中,A 應用根據規則選擇 C2 的渠道標識,而後傳遞給 B 應用。B 應用根據這個標識選擇使用 C2 進行通信,最後 C2 調用外部應用完成一次業務調用。

上述全部應用都基於 Dubbo 進行遠程通信,B 應用實現原理在小黑哥以前文章「支付路由系統演進史」中有寫過,感興趣的同窗能夠查看一下。

介紹完業務的基本狀況,如今咱們來看下到底發生了啥事。

一次業務需求中,須要改動 C2 應用,此次改動功能點真的很小,很快就完成了。小黑哥想着閒着也是閒着,因而就把以前 C2 應用中打印的日誌中一些沒有脫敏的信息,進行脫敏處理。

因爲以前日誌框架脫敏處理存在一些問題,因而就將日誌框架從 Log4j 升級爲 LogBack。升級以後,爲了防止不一樣日誌框架中之間的產生衝突,因而使用 IDEA Maven Helper 插件,統一將應用中全部的 Log4j 相關依賴都給排除了。

改動完成以後,將 C2 應用發佈到測試環境,再次從 A 應用發起測試, B 應用返回異常提示未找到 C2 應用

B 應用業務代碼相似以下:

public Response pay(Request req) {

    try {
        if (!isSupport(req.getChnlCode())) {
            return new Response("ERROR", "未找到相關渠道應用");
        }
        return doPay(req);
    } catch (Exception e) {
        return new Response("ERROR", "未找到相關渠道應用");
    }
}

正常狀況下,如果配置存在問題,B 應用將會返回未找到具體渠道,請求也會在 B 應用結束,不會調用到 C2 應用(也沒辦法調用)。

然而這次配置什麼都沒問題, 並且最詭異的是 C2 應用竟然收到了請求,而且成功處理了業務請求。

排查問題

因爲 B 應用異常處理時,將異常吃掉了,咱們沒辦法得知這個過程到底發生了啥事,因此第一要緊的事獲取異常信息。

最簡單的辦法就是,將 B 應用改造一下,加入打印異常日誌。不過當時比較懶,不想改造應用,就想獲取異常信息,因而想到使用 *Arthas*[1]。

Arthas 排錯技巧

Arthas 是Alibaba開源的Java診斷工具,這裏就再也不詳細介紹這個工具,主要講下此次排錯用到的命令-*watch*[2]。

watch 命令能夠方便觀察指定方的調用狀況,能夠具體觀察方法的返回值拋出異常入參,另外還能夠經過 OGNL表達式查看對應的變量。

這裏咱們主要爲了查看方法拋出的異常信息,執行命令以下:

watch com.dubbo.example.DemoService doPay -e -x 2 '{params,throwExp}'

上述命令將會在方法異常以後觀察方法的入參以及異常信息。

注意,咱們須要查看 doPay 方法,而不是 pay 方法。這是由於 pay方法中咱們將異常捕獲,不太可能會拋出異常哦~

異常信息以下所示:

誰再悄咪咪的吃掉異常,我上去就是一 JIO

真正引發這次錯誤的異常信息爲:

java.lang.NoClassDefFoundError: Could not initialize class xx.xxx.xx.GELogger

因爲這次 B 應用不存在改動,因此推測這個異常實際發生在 C2 應用,因而在 C2 應用處再次使用 Arthas watch 命令,一樣觀察到相同的錯誤信息。

NoClassDefFoundError

NoClassDefFound,從名字上咱們能夠推測是由於類不存在,從而引起的這個錯誤。按照這個思路,咱們首先能夠簡單查看一下 B 應用中是否存在 GELogger 相關類。

查看 B 應用相關依賴包,從中發現了這個類文件,這說明這個類確實存在。

在 IDEA 反編譯查看 GELogger類相關源碼,從中發現了問題。

private static Logger logger;

static {
    System.out.println("static init");
    logger = Logger.getLogger(NoClassDefFoundErrorTestService.class);
    System.out.println("Logger init success");
}

GELogger存在一個靜態代碼塊,用於初始化一個 org.apache.log4j.Logger日誌類。

而後在上面改動中,所有的 Log4j依賴都被排除了,因此這裏運行時應該會拋出另一個找到 org.apache.log4j.Logger 錯誤。

執行如下代碼,模擬拋錯過程。

System.out.println("模擬第一次 Error");
try {
    NoClassDefFoundErrorTestService noClassDefFoundErrorTestService=new NoClassDefFoundErrorTestService();
} catch (Throwable e) {
    e.printStackTrace();
}
System.out.println("模擬第二次 Error");
try {
    NoClassDefFoundErrorTestService noClassDefFoundErrorTestService=new NoClassDefFoundErrorTestService();
} catch (Throwable e) {
    e.printStackTrace();
}

異常信息以下所示:

誰再悄咪咪的吃掉異常,我上去就是一 JIO異常信息

第一次建立 NoClassDefFoundErrorTestService實例時,Java 虛擬機讀取加載時,將會初始化靜態代碼塊時。因爲 org.apache.log4j.Logger類不存在,靜態代碼塊執行異常,從而致使類加載失敗。

第二次再建立 NoClassDefFoundErrorTestService 實例時,Java 虛擬機不會再次讀取加載,因此直接返回瞭如下異常。

java.lang.NoClassDefFoundError: Could not initialize class com.dubbo.example.NoClassDefFoundErrorTestService

找到問題真正緣由,解決辦法也很簡單,直接排除 GELogger 所在依賴包。

Dubbo 內部異常處理

雖然問題到此解決了,可是這裏還有一個疑問,爲什麼 C2 應用發生了異常,卻沒有相關錯誤日誌,而且 C2 業務邏輯也正常處理完成。

這就要說到 Dubbo 內部異常錯誤處理方式,上面 GELogger 其實做用在一個 Dubbo 自定義 Filter 中,用來記錄結果,模擬代碼以下:

@Activate(
        group = {"provider", "consumer"}
)
public class ErrorFilter implements Filter {

    @Override
    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {

        Result result = invoker.invoke(invocation);
        NoClassDefFoundErrorTestService noClassDefFoundErrorTestService=new NoClassDefFoundErrorTestService();
        // 處理業務邏輯
        return result;
    }
}

這個自定義 Filter 中首先執行 invoker 方法,這個方法將會調用真正的業務方法,這就是爲何 C2 應用邏輯是正常處理完成。

業務方法處理完成以後,而後執行後續邏輯。因爲 NoClassDefFoundErrorTestService將會拋出 Error,最終這個 Error,將會在 HeaderExchangeHandler#handleRequest 被捕獲,而後將會把相關異常信息返回給調用 Dubbo 消費者。

誰再悄咪咪的吃掉異常,我上去就是一 JIODubbo 2.7

而在 Dubbo 消費者接受到服務提供者返回信息以後,將會在 DefaultFuture#doReceived轉化成 RemotingException

誰再悄咪咪的吃掉異常,我上去就是一 JIOdubbo consumer 2.7

RemotingException 最終將會在 FailoverClusterInvoker#doInvoke 轉換成 RpcException返回給業務代碼。

誰再悄咪咪的吃掉異常,我上去就是一 JIO

總結

好了,說了這麼多,總結一下本文知識點

  1. 異常捕獲以後,必定要記得打印日誌,而且要記得輸出堆棧信息
  2. 運行時類不存在,將會致使 NoClassDefFoundError,類加載過程失敗,也會致使 NoClassDefFoundError
  3. 對外提供的二方包,最好不要依賴特定日誌框架,如 Log4j,Logback 等,應該使用 Slf4j 框架。

參考資料

[1]Arthas: https://alibaba.github.io/arthas/index.html[2]watch: https://alibaba.github.io/arthas/watch.html[3]當Dubbo趕上Arthas:排查問題的實踐: https://dubbo.apache.org/zh-cn/blog/dubbo-meet-arthas.html[4]java.lang.NoClassDefFoundError 的解決方法一例: https://www.codelast.com/原創-java-lang-noclassdeffounderror-的解決方法一例/[5]noclassdeffounderror-could-not-initialize-class-error: https://stackoverflow.com/questions/1401111/noclassdeffounderror-could-not-initialize-class-error

相關文章
相關標籤/搜索