Dubbo 全鏈路追蹤日志的實(shí)現(xiàn)

微服務(wù)架構(gòu)的項(xiàng)目退个,一次請(qǐng)求可能會(huì)調(diào)用多個(gè)微服務(wù),這樣就會(huì)產(chǎn)生多個(gè)微服務(wù)的請(qǐng)求日志秤涩,當(dāng)我們想要查看整個(gè)請(qǐng)求鏈路的日志時(shí),就會(huì)變得困難司抱,所幸的是我們有一些集中日志收集工具筐眷,比如很熱門的ELK,我們需要把這些日志串聯(lián)起來习柠,這是一個(gè)很關(guān)鍵的問題匀谣,如果沒有串聯(lián)起來,查詢起來很是很困難资溃,我們的做法是在開始請(qǐng)求系統(tǒng)時(shí)生成一個(gè)全局唯一的id武翎,這個(gè)id伴隨這整個(gè)請(qǐng)求的調(diào)用周期,即當(dāng)一個(gè)服務(wù)調(diào)用另外一個(gè)服務(wù)的時(shí)候溶锭,會(huì)往下傳遞宝恶,形成一條鏈路,當(dāng)我們查看日志時(shí)趴捅,只需要搜索這個(gè)id垫毙,整條鏈路的日志都可以查出來了。

現(xiàn)在以dubbo微服務(wù)架構(gòu)為背景拱绑,舉個(gè)栗子:

A -> B -> C

我們需要將A/B/C/三個(gè)微服務(wù)間的日志按照鏈?zhǔn)酱蛴∽劢妫覀兌贾繢ubbo的RpcContext只能做到消費(fèi)者和提供者共享同一個(gè)RpcContext,比如A->B猎拨,那么A和B都可以獲取相同內(nèi)容的RpcContext膀藐,但是B->C時(shí),A和C就無法共享相同內(nèi)容的RpcContext了红省,也就是無法做到鏈?zhǔn)酱蛴∪罩玖恕?/p>

那么我們是如何做到呢额各?

我們可以用左手交換右手的思路來解決,假設(shè)左手是線程的ThreadLocal吧恃,右手是RpcContext臊泰,那么在交換之前,我們首先將必要的日志信息保存到ThreadLocal中。

在我們的項(xiàng)目微服務(wù)中大致分為兩種容器類型的微服務(wù)缸逃,一種是Dubbo容器针饥,這種容器的特點(diǎn)是只使用spring容器啟動(dòng),然后使用dubbo進(jìn)行服務(wù)的暴露需频,然后將服務(wù)注冊(cè)到zookeeper丁眼,提供服務(wù)給消費(fèi)者;另一種是SpringMVC容器昭殉,也即是我們常見的WEB容器苞七,它是我們項(xiàng)目唯一可以對(duì)外開放接口的容器,也是充當(dāng)項(xiàng)目的網(wǎng)關(guān)功能挪丢。

在了解了微服務(wù)容器之后蹂风,我們現(xiàn)在知道了調(diào)用鏈的第一層一定是在SpringMVC容器層中,那么我們直接在這層寫個(gè)自定義攔截器就ojbk了乾蓬,talk is cheap惠啄,show you the demo code:

舉例一個(gè)Demo代碼,公共攔截器的前置攔截中代碼如下:

public class CommonInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object handler)
        throws Exception {

        // ...

        // 初始化全局的Context容器
        Request request = initRequest(httpServletRequest);
        // 新建一個(gè)全局唯一的請(qǐng)求traceId任内,并set進(jìn)request中
        request.setTraceId(JrnGenerator.genTraceId());
        // 將初始化的請(qǐng)求信息放進(jìn)ThreadLocal中
        Context.initialLocal(request);

        // ...

        return true;
    }
    
    // ...
    
}

系統(tǒng)內(nèi)部上下文對(duì)象:

public class Context {
    
    // ...
    
    private static final ThreadLocal<Request> REQUEST_LOCAL = new ThreadLocal<>();
    
    public final static void initialLocal(Request request) {
        if (null == request) {
            return;
        }
        REQUEST_LOCAL.set(request);
    }
    
    public static Request getCurrentRequest() {
        return REQUEST_LOCAL.get();
    }
    
    // ...
}

攔截器實(shí)現(xiàn)了org.springframework.web.servlet.HandlerInterceptor接口撵渡,它的主要作用是用于攔截處理請(qǐng)求,可以在MVC層做一些日志記錄與權(quán)限檢查等操作死嗦,這相當(dāng)于MVC層的AOP趋距,即符合橫切關(guān)注點(diǎn)的所有功能都可以放入攔截器實(shí)現(xiàn)。

這里的initRequest(httpServletRequest);就是將請(qǐng)求信息封裝成系統(tǒng)內(nèi)容的請(qǐng)求對(duì)象Request越除,并初始化一個(gè)全局唯一的traceId放進(jìn)Request中节腐,然后再把它放進(jìn)系統(tǒng)內(nèi)部上下文ThreadLocal字段中。

接下來講講如何將ThreadLocal中的內(nèi)容放到RpcContext中摘盆,在講之前铜跑,我先來說說Dubbo基于spi擴(kuò)展機(jī)制,官方文檔對(duì)攔截器擴(kuò)展解釋如下:

服務(wù)提供方和服務(wù)消費(fèi)方調(diào)用過程攔截骡澈,Dubbo 本身的大多功能均基于此擴(kuò)展點(diǎn)實(shí)現(xiàn)锅纺,每次遠(yuǎn)程方法執(zhí)行,該攔截都會(huì)被執(zhí)行肋殴,請(qǐng)注意對(duì)性能的影響囤锉。

也就是說我們進(jìn)行服務(wù)遠(yuǎn)程調(diào)用前,攔截器會(huì)對(duì)此調(diào)用進(jìn)行攔截處理护锤,那么就好辦了官地,在消費(fèi)者調(diào)用遠(yuǎn)程服務(wù)之前,我們可以偷偷把ThreadLocal的內(nèi)容放進(jìn)RpcContext容器中烙懦,我們可以基于dubbo的spi機(jī)制擴(kuò)展兩個(gè)攔截器驱入,一個(gè)在消費(fèi)者端生效,另一個(gè)在提供者端生效:

在META-INF中加入com.alibaba.dubbo.rpc.Filter文件,內(nèi)容如下:

provider=com.objcoding.dubbo.filter.ProviderFilter
consumer=com.objcoding.dubbo.filter.ConsumerFilter

消費(fèi)者端攔截處理:


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

        //1.從ThreadLocal獲取請(qǐng)求信息
        Request request = Context.getCurrentRequest();
        //2.將Context參數(shù)放到RpcContext
        RpcContext rpcCTX = RpcContext.getContext();
        // 將初始化的請(qǐng)求信息放進(jìn)ThreadLocal中
        Context.initialLocal(request);

        // ...

    }   
}

Context.getCurrentRequest();就是從ThreadLocal中拿到Request請(qǐng)求內(nèi)容亏较,contextToDubboContext(request);將Request內(nèi)容放進(jìn)當(dāng)前線程的RpcContext容器中莺褒。

很容易聯(lián)想到提供者也就是把RpcContext中的內(nèi)容拿出來放到ThreadLocal中:

public class ProviderFilter extends AbstractDubboFilter implements Filter{
     @Override
    public Result invoke(Invoker<?> invoker, Invocation invocation) 
        throws RpcException {
        // 1.獲取RPC遠(yuǎn)程調(diào)用上下文
        RpcContext rpcCTX = RpcContext.getContext();
        // 2.初始化請(qǐng)求信息
        Request request = dubboContextToContext(rpcCTX);
        // 3.將初始化的請(qǐng)求信息放進(jìn)ThreadLocal中
        Context.initialLocal(request);

        // ...

    }   
}

接下來我們還要配置log4j2,使得我們同一條請(qǐng)求在關(guān)聯(lián)的每一個(gè)容器打印的消息雪情,都有一個(gè)共同的traceId遵岩,那么我們?cè)贓LK想要查詢某個(gè)請(qǐng)求時(shí),只需要搜索traceId巡通,就可以看到整條請(qǐng)求鏈路的日志了尘执。

我們?cè)贑ontext上下文對(duì)象的initialLocal(Request request)方法中在log4j2的上下文中添加traceId信息:

public class Context {
    
    // ...

    final public static String TRACEID = "_traceid";

    public final static void initialLocal(Request request) {
        if (null == request) {
            return;
        }
        // 在log4j2的上下文中添加traceId
        ThreadContext.put(TRACEID, request.getTraceId());
        REQUEST_LOCAL.set(request);
    }
    
    // ...
}

接下來實(shí)現(xiàn)org.apache.logging.log4j.core.appender.rewrite.RewritePolicy

@Plugin(name = "Rewrite", category = "Core", elementType = "rewritePolicy", printObject = true)
public final class MyRewritePolicy implements RewritePolicy {

    // ...
    
    @Override
    public LogEvent rewrite(final LogEvent source) {
        HashMap<String, String> contextMap = Maps.newHashMap(source.getContextMap());
        contextMap.put(Context.TRACEID, contextMap.containsKey(Context.TRACEID) ? contextMap.get(Context.TRACEID) : NULL);
        return new Log4jLogEvent.Builder(source).setContextMap(contextMap).build();
    }
    
    // ...
}

RewritePolicy的作用是我們每次輸出日志,log4j都會(huì)調(diào)用這個(gè)類進(jìn)行一些處理的操作宴凉。

配置log4j2.xml:

<Configuration status="warn">
    <Appenders>
        <Console name="Console" target="SYSTEM_OUT">
            <PatternLayout
                pattern="[%d{yyyy/MM/dd HH:mm:ss,SSS}][${ctx:_traceid}]%m%n" />
        </Console>
        
        <!--定義一個(gè)Rewrite-->
        <Rewrite name="Rewrite">
            <MyRewritePolicy/>
            <!--引用輸出模板-->
            <AppenderRef ref="Console"/>
        </Rewrite>
    </Appenders>
    <Loggers>
       
        <!--使用日志模板-->
        <Logger name="com.objcoding.MyLogger" level="debug" additivity="false">
            <!--引用Rewrite-->
            <AppenderRef ref="Rewrite"/>
        </Logger>
    </Loggers>
</Configuration>

自定義日志類:

public class MyLogger {
    private static final Logger logger = LoggerFactory.getLogger(MyLogger.class);
    
     public static void info(String msg, Object... args) {
        if (canLog() == 1 && logger.isInfoEnabled()) {
            logger.info(msg, args);
        }
    }
    
    public static void debug(String message, Object... args) {
        if (canLog() == 1 && logger.isDebugEnabled()) {
            logger.debug(message, args);
        }
    }
    
    // ..
}

更多精彩文章請(qǐng)關(guān)注作者維護(hù)的公眾號(hào)「后端進(jìn)階」誊锭,這是一個(gè)專注后端相關(guān)技術(shù)的公眾號(hào)。
關(guān)注公眾號(hào)并回復(fù)「后端」免費(fèi)領(lǐng)取后端相關(guān)電子書籍弥锄。
歡迎分享丧靡,轉(zhuǎn)載請(qǐng)保留出處。

公眾號(hào)「后端進(jìn)階」叉讥,專注后端技術(shù)分享窘行!
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末饥追,一起剝皮案震驚了整個(gè)濱河市图仓,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌但绕,老刑警劉巖救崔,帶你破解...
    沈念sama閱讀 218,755評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異捏顺,居然都是意外死亡六孵,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門幅骄,熙熙樓的掌柜王于貴愁眉苦臉地迎上來劫窒,“玉大人,你說我怎么就攤上這事拆座≈魑。” “怎么了?”我有些...
    開封第一講書人閱讀 165,138評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵挪凑,是天一觀的道長孕索。 經(jīng)常有香客問我,道長躏碳,這世上最難降的妖魔是什么搞旭? 我笑而不...
    開封第一講書人閱讀 58,791評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上肄渗,老公的妹妹穿的比我還像新娘镇眷。我一直安慰自己,他們只是感情好恳啥,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,794評(píng)論 6 392
  • 文/花漫 我一把揭開白布偏灿。 她就那樣靜靜地躺著,像睡著了一般钝的。 火紅的嫁衣襯著肌膚如雪翁垂。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,631評(píng)論 1 305
  • 那天硝桩,我揣著相機(jī)與錄音沿猜,去河邊找鬼。 笑死碗脊,一個(gè)胖子當(dāng)著我的面吹牛啼肩,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播衙伶,決...
    沈念sama閱讀 40,362評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼祈坠,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼!你這毒婦竟也來了矢劲?” 一聲冷哼從身側(cè)響起赦拘,我...
    開封第一講書人閱讀 39,264評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎芬沉,沒想到半個(gè)月后躺同,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,724評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡丸逸,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年蹋艺,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片黄刚。...
    茶點(diǎn)故事閱讀 40,040評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡捎谨,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出憔维,到底是詐尸還是另有隱情涛救,我是刑警寧澤,帶...
    沈念sama閱讀 35,742評(píng)論 5 346
  • 正文 年R本政府宣布埋同,位于F島的核電站州叠,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏凶赁。R本人自食惡果不足惜咧栗,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,364評(píng)論 3 330
  • 文/蒙蒙 一逆甜、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧致板,春花似錦交煞、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至萝挤,卻和暖如春御毅,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背怜珍。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評(píng)論 1 270
  • 我被黑心中介騙來泰國打工端蛆, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人酥泛。 一個(gè)月前我還...
    沈念sama閱讀 48,247評(píng)論 3 371
  • 正文 我出身青樓今豆,卻偏偏與公主長得像,于是被迫代替她去往敵國和親柔袁。 傳聞我的和親對(duì)象是個(gè)殘疾皇子呆躲,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,979評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容