ELK 日志分析平台与全链路追踪:从日志聚合到故障定位的工程实践
一、日志治理的现实困境:从日志洪流到精准定位
生产系统的日志量随业务增长呈指数级增长。一个中等规模的微服务集群每天产生数十 GB 日志,故障发生时需要在海量日志中定位关键信息。传统做法是 SSH 到服务器 grep 日志,但在容器化环境中 Pod 随时可能被重建,日志随 Pod 消亡而丢失。
更深层的问题是日志的关联性缺失。一个用户请求经过网关、认证、业务、数据库四个服务,每个服务各自记录日志,但缺少统一的请求标识将它们串联。运维工程师需要手动在四个服务的日志中搜索同一时间窗口的记录,效率极低。全链路追踪通过 Trace ID 将跨服务的日志关联起来,是日志治理的关键基础设施。
二、ELK + 全链路追踪的架构设计
flowchart TB subgraph 数据采集 A[应用日志<br/>JSON 格式] --> B[Fluentd<br/>日志采集器] C[Trace 数据<br/>OpenTelemetry SDK] --> D[OTel Collector<br/>遥测收集器] end subgraph 数据存储 B --> E[Elasticsearch<br/>日志索引] D --> F[Jaeger<br/>链路存储] D --> G[Prometheus<br/>指标存储] end subgraph 数据关联 E --> H[Trace ID 关联<br/>日志 → 链路] F --> H H --> I[Kibana Dashboard<br/>统一查询界面] end subgraph 告警 E --> J[日志告警<br/>Error Rate 突增] F --> K[链路告警<br/>延迟 P99 突增] J --> L[Alertmanager] K --> L end style B fill:#f9f,stroke:#333 style H fill:#9ff,stroke:#333日志与 Trace 的关联是架构的核心设计。应用在记录日志时自动注入 Trace ID 和 Span ID,Fluentd 采集日志时保留这些字段。在 Kibana 中搜索日志时,可以直接点击 Trace ID 跳转到 Jaeger 查看完整链路,实现从日志到链路的无缝切换。
三、ELK + 全链路追踪的核心实现
3.1 结构化日志与 Trace 注入
// LogConfig.java —— 结构化日志配置(Spring Boot + Logback + OpenTelemetry) @Configuration public class LogConfig { /** * 配置日志格式:JSON + Trace ID 自动注入 * 输出示例: * { * "timestamp": "2026-06-19T10:30:00.000Z", * "level": "INFO", * "service": "order-service", * "traceId": "abc123...", * "spanId": "def456...", * "message": "Order created", * "context": { "orderId": "ORD-001", "userId": "USR-123" } * } */ @Bean public LoggerContext loggerContext() { LoggerContext context = (LoggerContext) LoggerFactory.getILoggerFactory(); // JSON 格式布局 JsonLayout layout = new JsonLayout(); layout.setIncludeTimestamp(true); layout.setIncludeLevel(true); layout.setIncludeThreadName(true); layout.setIncludeMDC(true); // MDC 中包含 Trace ID // 控制台输出 ConsoleAppender<ILoggingEvent> consoleAppender = new ConsoleAppender<>(); consoleAppender.setContext(context); consoleAppender.setLayout(layout); consoleAppender.start(); // Root Logger Logger rootLogger = context.getLogger(Logger.ROOT_LOGGER_NAME); rootLogger.addAppender(consoleAppender); rootLogger.setLevel(Level.INFO); return context; } } // TracingFilter.java —— HTTP 请求 Trace 注入 @Component @Order(Ordered.HIGHEST_PRECEDENCE) public class TracingFilter implements Filter { private final Tracer tracer; public TracingFilter(Tracer tracer) { this.tracer = tracer; } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest httpRequest = (HttpServletRequest) request; Span span = tracer.spanBuilder( httpRequest.getMethod() + " " + httpRequest.getRequestURI() ).startSpan(); try (Scope scope = span.makeCurrent()) { // 将 Trace ID 注入 MDC,供日志框架自动记录 MDC.put("traceId", span.getSpanContext().getTraceId()); MDC.put("spanId", span.getSpanContext().getSpanId()); // 传播 Trace 上下文到下游服务 span.setAttribute("http.method", httpRequest.getMethod()); span.setAttribute("http.url", httpRequest.getRequestURL().toString()); span.setAttribute("http.scheme", httpRequest.getScheme()); chain.doFilter(request, response); span.setAttribute("http.status_code", ((HttpServletResponse) response).getStatus()); } catch (Exception e) { span.recordException(e); span.setStatus(StatusCode.ERROR, e.getMessage()); throw e; } finally { span.end(); MDC.remove("traceId"); MDC.remove("spanId"); } } }3.2 Fluentd 采集与 Elasticsearch 索引
# fluentd-configmap.yaml —— Fluentd 采集配置 apiVersion: v1 kind: ConfigMap metadata: name: fluentd-config namespace: logging data: fluent.conf: | # K8s 容器日志采集 <source> @type tail path /var/log/containers/*.log pos_file /var/log/fluentd-containers.log.pos tag kubernetes.* read_from_head true <parse> @type json time_key timestamp time_format %Y-%m-%dT%H:%M:%S.%NZ keep_time_key true </parse> </source> # K8s 元数据注入 <filter kubernetes.**> @type kubernetes_metadata @id filter_kube_metadata skip_labels false skip_container_metadata false skip_master_url true </filter> # 日志清洗:提取关键字段 <filter kubernetes.**> @type record_transformer enable_ruby <record> # 统一字段名 service_name ${record.dig("kubernetes", "labels", "app") || "unknown"} namespace_name ${record.dig("kubernetes", "namespace_name") || "unknown"} pod_name ${record.dig("kubernetes", "pod_name") || "unknown"} # 保留 Trace ID 用于关联 trace_id ${record.dig("traceId") || ""} span_id ${record.dig("spanId") || ""} # 日志级别标准化 log_level ${record.dig("level") || record.dig("severity") || "INFO"} </record> </filter> # 输出到 Elasticsearch <match kubernetes.**> @type elasticsearch @id out_es @log_level info host elasticsearch-master port 9200 scheme http # 按日期滚动索引 logstash_format true logstash_prefix log-app logstash_dateformat %Y.%m.%d # 索引模板 template_name log-app template_file /fluentd/etc/index-template.json # 刷新策略 bulk_message_request_threshold 2097152 flush_interval 5s retry_max_interval 30s retry_forever true # Trace ID 字段映射(用于 Kibana 关联查询) <buffer> @type file path /var/log/fluentd/buffers/kubernetes flush_mode interval flush_interval 5s chunk_limit_size 16MB total_limit_size 1GB overflow_action block </buffer> </match>3.3 Kibana 日志与 Trace 关联查询
// Elasticsearch 索引模板:确保 Trace ID 可被精确查询 { "index_patterns": ["log-app-*"], "template": { "settings": { "number_of_shards": 3, "number_of_replicas": 1, "analysis": { "analyzer": { "trace_analyzer": { "type": "keyword" } } } }, "mappings": { "properties": { "trace_id": { "type": "keyword" }, "span_id": { "type": "keyword" }, "service_name": { "type": "keyword" }, "namespace_name": { "type": "keyword" }, "log_level": { "type": "keyword" }, "message": { "type": "text", "analyzer": "standard" }, "timestamp": { "type": "date" } } } } }四、日志平台的成本治理与性能优化
索引策略:热温冷架构——最近 7 天的索引存储在热节点(SSD),7-30 天存储在温节点(HDD),30 天以上迁移到冷节点或删除。日志保留策略应根据合规要求和存储成本平衡。
日志采样:非错误日志在高流量场景下可以采样(如只记录 10% 的 INFO 日志),错误日志必须全量记录。采样策略应在应用层实现,而非在 Fluentd 层丢弃。
查询优化:避免在 Kibana 中使用通配符开头的查询(如*error*),这会触发全索引扫描。使用 keyword 字段精确匹配,使用 text 字段全文搜索。
五、总结
ELK 日志平台与全链路追踪的组合是云原生可观测性的核心基础设施。结构化日志确保了日志的可查询性,Trace ID 注入实现了日志与链路的关联,Fluentd 采集器保证了日志的可靠传输。日志治理的关键不是收集更多日志,而是确保每条日志都有价值——可查询、可关联、可追溯。成本治理与性能优化是日志平台持续运营的基础,热温冷架构和日志采样是控制成本的有效手段。