微服務(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)保留出處。