MDC 加 ELK 全链路日志系统建设
1. 对应简历段落
对应简历中可以这样描述:
在微服务稳定性治理中,负责建设基于 MDC、Logback、Filebeat、Logstash、Elasticsearch、Kibana 的全链路日志系统。通过 traceId、spanId、userId、bizId、eventId 等上下文字段贯穿 HTTP 请求、Feign 调用、MQ 生产消费和异步线程,统一 JSON 日志格式、索引模板、脱敏规则和查询看板,显著提升商机流转、活动报名、出单回写等跨服务问题的定位效率。
这段经历强调的是可观测性。资深 Java 面试中,日志系统不是“把日志打到 ES”,而是如何让一次业务请求在多个线程、多个服务、多个中间件之间被串起来,并且能在故障时快速缩小范围。
2. 业务背景
商机和营销系统往往是典型微服务链路:入口网关接收请求,活动服务处理报名,客户中心建档,商机中心生成商机,销售工作台生成待办,RocketMQ 发送事件,报表消费者异步落库。任何一个环节失败,业务方看到的可能只是“报名后没有待办”或“出单后商机没更新”。
如果每个服务只打普通文本日志,排查就会非常痛苦。你需要按时间猜测请求,登录多台机器 grep,查完 HTTP 日志再查 MQ 日志,再查消费者日志。遇到高峰期日志量大,几乎无法定位。
全链路日志系统的目标是:拿到一个 traceId、bizId 或 eventId,就能查到这次请求从入口到下游、从同步到异步的完整路径。它不替代链路追踪系统,但能作为最基础、最可控、最贴近业务的排查工具。
3. 核心原理
MDC 是 SLF4J 提供的线程上下文机制,本质上是基于 ThreadLocal 存储一组 key-value。日志框架输出日志时,可以把 MDC 中的字段写入日志。常见字段包括:
traceId:一次入口请求或业务链路的全局追踪 ID。spanId:链路中的局部调用片段。userId:当前登录用户。bizId:业务主键,如商机 ID、订单 ID、保单 ID。eventId:MQ 事件 ID。source:来源系统或渠道。
ELK 负责采集、解析、存储和查询。应用输出 JSON 日志到文件,Filebeat 采集并发送到 Logstash 或 Elasticsearch,Logstash 做解析、过滤、脱敏和路由,Elasticsearch 建索引,Kibana 用于检索和看板。
关键点是上下文传递。MDC 默认只在当前线程有效,跨线程池、异步任务、Feign 调用、MQ 消息都需要显式传递。否则入口日志有 traceId,异步消费者又变成另一条孤立日志。
4. 项目落地
落地可以分四层。
第一层是入口生成 traceId。在网关或服务 Filter 中读取请求头 X-Trace-Id,如果没有则生成一个新的 traceId,写入 MDC,并在响应头返回。这样前端、网关和后端可以共享同一个 ID。
第二层是服务间传递。Feign 或 RestTemplate 拦截器从 MDC 读取 traceId、userId、bizId,写入 HTTP Header。下游 Filter 再从 Header 恢复 MDC。
第三层是 MQ 传递。生产者发送消息时,把 traceId、eventId 放入消息 Header 和消息体;消费者收到消息后,先把这些字段放入 MDC,再执行业务逻辑。消费结束必须清理 MDC,避免线程复用导致串日志。
第四层是日志采集与索引。应用统一输出 JSON 格式,每条日志包含 timestamp、level、service、host、traceId、bizId、eventId、message、exception 等字段。ES 索引按环境、服务、日期拆分,例如 app-prod-opportunity-2026.05.01。
业务上可以建几个常用查询:按 traceId 查全链路,按 opportunityId 查商机流转,按 eventId 查 MQ 生产消费,按 error level 查异常,按接口统计错误率和耗时。
5. 关键配置或伪代码
入口 Filter:
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) {
HttpServletRequest request = (HttpServletRequest) req;
String traceId = Optional.ofNullable(request.getHeader("X-Trace-Id"))
.filter(StringUtils::hasText)
.orElse(IdGenerator.traceId());
try {
MDC.put("traceId", traceId);
MDC.put("path", request.getRequestURI());
chain.doFilter(req, res);
} finally {
MDC.clear();
}
}
Feign 拦截器:
public void apply(RequestTemplate template) {
copyHeader(template, "X-Trace-Id", MDC.get("traceId"));
copyHeader(template, "X-User-Id", MDC.get("userId"));
copyHeader(template, "X-Biz-Id", MDC.get("bizId"));
}
线程池装饰器:
public Runnable decorate(Runnable task) {
Map<String, String> context = MDC.getCopyOfContextMap();
return () -> {
Map<String, String> previous = MDC.getCopyOfContextMap();
try {
if (context != null) {
MDC.setContextMap(context);
}
task.run();
} finally {
if (previous != null) {
MDC.setContextMap(previous);
} else {
MDC.clear();
}
}
};
}
Logback JSON 字段示例:
{
"time": "%d{yyyy-MM-dd'T'HH:mm:ss.SSSXXX}",
"level": "%level",
"service": "${spring.application.name}",
"thread": "%thread",
"traceId": "%X{traceId}",
"bizId": "%X{bizId}",
"eventId": "%X{eventId}",
"logger": "%logger{36}",
"message": "%message",
"exception": "%exception"
}
6. 常见坑
第一个坑是忘记清理 MDC。线程池线程会复用,如果请求结束不 clear,下一个请求可能带上前一个请求的 traceId,排查会被误导。
第二个坑是异步线程丢上下文。@Async、线程池、CompletableFuture、定时任务都不会自动继承 MDC,需要包装任务或使用支持上下文传递的组件。
第三个坑是 MQ 链路断掉。生产者只在日志里有 traceId,但消息体和 Header 没带,消费者就无法恢复上下文。
第四个坑是日志格式不统一。有的服务文本日志,有的服务 JSON,有的字段叫 traceId,有的叫 trace_id,最终 Kibana 很难统一查询。
第五个坑是 ES 索引无节制。日志量大时如果不按日期、服务分索引,不设置生命周期和冷热策略,ES 很快会磁盘告警。
第六个坑是敏感信息泄露。客户手机号、身份证、保单号、银行卡、第三方 token 不能明文输出,要在日志框架或 Logstash 层脱敏。
7. 面试追问
常见追问包括:
- MDC 的底层原理是什么?
- 为什么异步线程会丢 traceId?
- 如何让 Feign 和 MQ 传递 traceId?
- 日志系统和 SkyWalking、Zipkin 有什么区别?
- ES 日志索引如何设计?
- 日志量太大怎么办?
- 如何做敏感字段脱敏?
- 如何通过日志排查一次 MQ 消费失败?
这些问题都要求你讲出工程细节。只说“我们用 ELK 查日志”是不够的。
8. 推荐回答
可以这样回答:
“我们建设全链路日志时,核心是统一上下文字段。入口 Filter 生成或透传 traceId,放到 MDC;日志输出 JSON 时把 MDC 字段一起写出。服务间调用通过 Feign 拦截器把 traceId 放到 Header,下游再恢复到 MDC。MQ 生产时把 traceId 和 eventId 放到消息 Header,消费者收到后先写 MDC 再处理业务。所有 finally 里都会清理 MDC,避免线程复用串上下文。”
“ELK 侧我们统一日志格式和索引规范,按环境、服务、日期建索引。Kibana 可以按 traceId 查一次请求全链路,也可以按 bizId 查商机或订单的完整流转,按 eventId 查 MQ 从生产到消费的日志。对于异常日志,会保留 exception 栈,但手机号、证件号、token 等字段会在应用或 Logstash 层脱敏。”
“我也会说明它和 APM 的关系。SkyWalking 这类链路追踪更适合看拓扑、调用耗时和依赖关系;ELK 日志更适合看业务字段、异常细节和人工排查。两者不是替代关系。很多生产问题最终还是要靠 traceId 把链路追踪、业务日志、MQ 事件和数据库记录串起来。”
9. 延伸学习路线
第一步,理解 SLF4J、Logback、MDC、Appender、Encoder、异步日志的基本机制。
第二步,掌握 traceId 在 HTTP、RPC、MQ、线程池、定时任务中的传递方式,特别是 ThreadLocal 在线程复用下的风险。
第三步,学习 ELK 或 EFK 架构,包括 Filebeat、Logstash、Elasticsearch、Kibana、索引模板和生命周期管理。
第四步,补充可观测性体系,把日志、指标、链路追踪、告警和业务审计放在一起看。
第五步,做几次故障排查演练:接口超时、MQ 消费失败、状态不一致、线程池耗尽。每次都用 traceId 或 bizId 从入口查到根因,形成面试中能讲的真实闭环。