一、一次线上故障排查花了4小时
2020年,用户反馈下单失败,但监控系统一切正常。
我登录服务器,用grep查日志。结果发现日志分散在8台机器上,每台机器的日志格式还不一样。花了2小时才找到报错的那条日志,又花了2小时才定位到原因------一个第三方支付接口超时。
从那以后,我们引入了统一的日志体系,同样的故障,现在10分钟就能定位。
二、日志规范
2.1 日志级别
日志级别使用规范:
ERROR:系统错误,需要立即处理
- 未捕获的异常
- 外部服务不可用
- 数据不一致
WARN:潜在问题,需要关注
- 业务异常(用户输入错误)
- 性能降级
- 接近阈值
INFO:关键业务流程
- 请求开始/结束
- 业务状态变更
- 外部调用
DEBUG:调试信息(生产环境关闭)
- 方法入参/出参
- 中间变量
- SQL语句
2.2 日志格式
java
/**
* 统一日志格式
*/
@Configuration
public class LogConfig {
/**
* JSON格式日志(推荐)
*/
@Bean
public LoggingEventJsonFormat jsonFormat() {
return LoggingEventJsonFormat.builder()
.timestamp("@timestamp")
.level("level")
.thread("thread")
.logger("logger")
.message("message")
.traceId("traceId")
.spanId("spanId")
.build();
}
}
// 日志输出示例:
// {"@timestamp":"2024-01-01T12:00:00.000Z","level":"INFO","thread":"http-nio-8080-1","logger":"com.example.OrderService","message":"订单创建成功","traceId":"abc123","spanId":"def456","orderId":"ORD001","userId":"USR001","amount":99.9}
2.3 MDC链路追踪
java
/**
* MDC链路追踪
*/
@Component
public class TraceFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
// 从请求头获取TraceId
String traceId = httpRequest.getHeader("X-Trace-Id");
if (traceId == null) {
traceId = UUID.randomUUID().toString().replace("-", "");
}
// 设置MDC
MDC.put("traceId", traceId);
MDC.put("spanId", generateSpanId());
MDC.put("userId", getCurrentUserId(httpRequest));
try {
chain.doFilter(request, response);
} finally {
MDC.clear();
}
}
}
/**
* Feign传递TraceId
*/
@Configuration
public class FeignTraceConfig {
@Bean
public RequestInterceptor traceInterceptor() {
return template -> {
String traceId = MDC.get("traceId");
if (traceId != null) {
template.header("X-Trace-Id", traceId);
}
};
}
}
三、ELK架构
3.1 架构图
┌─────────────────────────────────────────────────────────────────┐
│ ELK架构 │
│ │
│ ┌─────────┐ ┌─────────────┐ ┌──────────┐ │
│ │ 应用 │───▶│ Logstash │───▶│Elastic- │ │
│ │ 日志 │ │ (收集过滤) │ │ search │ │
│ └─────────┘ └─────────────┘ └──────────┘ │
│ │ │ │
│ │ ┌─────────────┐ │ │
│ └─────────────▶│ Filebeat │────────┘ │
│ │ (轻量采集) │ │
│ └─────────────┘ │
│ │ │
│ ┌─────────────┐ │ │
│ │ Kibana │◀─────┘ │
│ │ (可视化) │ │
│ └─────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────┘
3.2 Filebeat配置
yaml
# filebeat.yml
filebeat.inputs:
- type: log
enabled: true
paths:
- /var/log/app/*.log
fields:
app: order-service
env: production
fields_under_root: true
multiline:
pattern: '^\d{4}-\d{2}-\d{2}'
negate: true
match: after
output.logstash:
hosts: ["logstash:5044"]
compression_level: 3
processors:
- add_host_metadata: ~
- add_cloud_metadata: ~
3.3 Logstash配置
ruby
# logstash.conf
input {
beats {
port => 5044
}
}
filter {
# 解析JSON格式日志
json {
source => "message"
target => "log"
}
# 提取TraceId
mutate {
add_field => {
"traceId" => "%{[log][traceId]}"
}
}
# 过滤DEBUG日志
if [log][level] == "DEBUG" {
drop {}
}
# 添加时间戳
date {
match => ["[log][@timestamp]", "ISO8601"]
target => "@timestamp"
}
}
output {
elasticsearch {
hosts => ["elasticsearch:9200"]
index => "app-logs-%{+YYYY.MM.dd}"
template => "/usr/share/logstash/templates/app-logs.json"
template_overwrite => true
}
}
四、日志告警
4.1 基于ElastAlert的告警
yaml
# error_alert.yaml
name: Error Log Alert
type: frequency
index: app-logs-*
# 5分钟内ERROR日志超过10条告警
num_events: 10
timeframe:
minutes: 5
filter:
- term:
log.level: "ERROR"
# 排除已知的非关键错误
filter:
- term:
log.level: "ERROR"
- not:
query:
query_string:
query: "message: *ConnectionReset*"
alert:
- "dingtalk"
dingtalk:
webhook: "https://oapi.dingtalk.com/robot/send?access_token=xxx"
message: "⚠️ ERROR日志告警\n应用: {app}\n环境: {env}\n数量: {num_hits}\n最近一条: {message}"
4.2 日志监控大盘
java
/**
* 日志指标统计
*/
@Service
@Slf4j
public class LogMetricsService {
@Autowired
private MeterRegistry meterRegistry;
/**
* 记录业务指标到日志
*/
public void logBusinessMetric(String metricName, double value, String... tags) {
// 记录到监控系统
Tags metricTags = Tags.of(tags);
meterRegistry.counter("business." + metricName, metricTags).increment(value);
// 同时记录到日志(用于ELK分析)
log.info("BusinessMetric: name={}, value={}, tags={}",
metricName, value, Arrays.toString(tags));
}
}
五、踩坑实录
坑1:日志太多导致磁盘满
DEBUG日志在生产环境开着,每天产生100GB日志,磁盘很快就满了。
解决:生产环境关闭DEBUG,设置日志滚动和自动清理。
坑2:日志格式不统一
每个服务日志格式不一样,ELK解析困难。
解决:统一JSON格式,强制规范。
坑3:日志中打印敏感信息
日志里打印了用户密码和手机号,被安全扫描发现。
解决:日志脱敏,禁止打印敏感信息。
坑4:异步日志丢失
使用异步日志,应用崩溃时缓冲区的日志丢失。
解决:配置ImmediateFlush,关键日志同步写入。
坑5:日志影响性能
在循环中打印日志,QPS下降了30%。
解决:避免在热路径打印日志,使用isDebugEnabled判断。
六、总结
日志体系要点:
| 环节 | 方案 |
|---|---|
| 规范 | 统一格式、级别、脱敏 |
| 采集 | Filebeat + Logstash |
| 存储 | Elasticsearch |
| 展示 | Kibana |
| 告警 | ElastAlert |
| 链路 | MDC + TraceId |
最佳实践:
- 统一JSON格式日志
- MDC传递链路信息
- 生产环境关闭DEBUG
- 日志脱敏
- 告警及时
血的教训:
好的日志体系不是锦上添花,是雪中送炭。出问题时,日志是你唯一的救命稻草。
思考题: 你的日志体系是怎样的?排查问题快吗?
个人观点,仅供参考