做 SpringCloud 项目最难搞的往往不是服务拆分、熔断降级而是日志。服务一拆日志跟着散落一地排查一个订单超时问题要翻七八个服务的文件这个我深有体会。所以前阵子花了两周时间从 0 开始把分布式日志架构这一整套技术栈完整搭了一遍从采集端到缓冲层从清洗管道到检索存储再到可视化告警外加全链路 TraceId 串联。这篇就把完整的搭建过程、调参细节、踩坑实录都记下来给正准备上这套架构的同学当个参考。适合刚接触微服务日志的同学通读一遍也适合已经部署了 ELK 但链路追踪不完整的团队用来对照补课。1. 分布式日志架构的整体设计与技术选型1.1 先搞清楚分布式日志到底要解决什么问题单机时代日志就是tail -f一个文件的事服务挂在本地问题定位靠人肉搜索。微服务化之后这个习惯直接撑不住了。几十个服务拆出来每个服务又是多副本部署日志分布在不同的机器上你在服务器上grep半天只能看到其中一台节点上的片段。更麻烦的是一次请求很可能跨越用户服务、订单服务、支付服务至少三个服务后端日志里没有同一个请求标识你根本不知道这条链路在哪个环节慢了只能靠问上下游同事靠猜。第二个痛点是指数级增长的日志量。单体时代一天可能几百万条日志微服务化之后同样的请求可能产生同等量级但每一条关联的信息更多日积月累就是亿级。传统的grep在这种量级面前基本没有可用性必须上全文索引。第三个痛点也是到了后期才能感受到的就是日志查询必须和链路追踪结合。一个 TraceId 贯穿所有服务日志在 Elasticsearch 里一搜整条链路每一跳的信息全部出来这才是分布式日志架构真正的价值。所以分布式日志架构要解决的就是三个字采、存、查。把所有实例的日志统一采集到集中式平台做好缓冲削峰建立全文索引提供可视化的检索入口同时保留请求的关联关系。理解了这三个本质后面所有组件选型和配置就顺理成章了。1.2 技术栈选型EFK 还是 ELK要不要上 Kafka大多数团队一开始接触的往往是 ELK也就是 Elasticsearch Logstash Kibana 三件套。这套组合在老项目里非常常见Logstash 既能采集日志文件又能做过滤清洗还能直接输出到 Elasticsearch三个组件搞定一切部署最简单。但我这次并没有在一开始选这套组合原因也很直接Logstash 是重量级采集器跑在 Java 环境里内存占用动辄 1GB 以上如果每个服务实例都部署一个 Logstash业务节点资源根本撑不住。因此我的选择是更常见的 EFK Kafka 组合具体技术栈是Filebeat Kafka Logstash Elasticsearch Kibana链路追踪部分用 Spring Cloud Sleuth 做 TraceId 注入。Filebeat 是 Go 写的轻量级采集器内存占用通常在几十 MB 到两三百 MB 之间只负责把日志从文件里搬出来送到 KafkaLogstash 不再承担采集职责退回去做集中式清洗管道从 Kafka 消费数据再做解析、字段映射、脱敏最后写入 ElasticsearchKibana 承担检索可视化Elasticsearch 负责存储和索引。组件职责为什么用它轻量替代方案Filebeat日志采集与传输轻量、稳定、支持多行日志合并Logstash 直接采集重Kafka缓冲削峰、解耦扛高并发写入数据可重放无小流量可去掉Logstash解析、清洗、脱敏、路由管道机制成熟插件丰富Fluentd / VectorElasticsearch存储、全文索引、聚合检索日志场景的事实标准OpenSearch / ClickHouse 日志方案Kibana检索、可视化、面板和 ES 配套上手快Grafana配合 ES 数据源关于要不要上 Kafka我的看法是小流量阶段可以不用但架构上要预留位置。当单日日志量在几 GB 到几十 GB 这个量级Filebeat 直连 Logstash 或者 Filebeat 直连 Elasticsearch 都能跑得动。一旦单日日志量增长到几百 GB 甚至 TB 级别尤其是遇到促销、活动这类流量高峰没有 Kafka 缓冲突发写入会直接把 ES 打挂。Kafka 在这里起到的作用是削峰填谷业务高峰日志量到每秒几万条ES 写入速度跟不上Kafka 先接住慢慢消费日志架构对外表现就是“最多延迟几秒但不会丢”。同时 Kafka 还能解耦日志数据不只是给 ELK 用后面接实时告警、实时数仓、指标分析都从 Kafka 拿数据不需要重新采集。1.3 整体数据链路和关键设计整个架构的数据流向可以用一句话讲清楚Spring Cloud 服务的日志写本地文件Filebeat 实时读取并发送到 KafkaLogstash 从 Kafka 消费、解析清洗后写入 Elasticsearch最终在 Kibana 里检索和展示。链路追踪部分Spring Cloud Sleuth 在每个请求进入时生成 TraceId 和 SpanId写入日志上下文MDC所以每一条日志落地时都带有 TraceId 字段贯穿全链路。这里有一个重要的设计原则尽量让采集端保持“无侵入、无感知”。Filebeat 只是读业务进程写出来的日志文件不进入业务进程内部即使 Filebeat 挂了最多丢日志业务进程不受影响。当然实际操作中要注意磁盘空间如果下游 Kafka 或 ES 故障Filebeat 会一直重试日志文件会不断增长这需要在采集机器上做磁盘告警。搭建顺序我建议自下而上先启动 Elasticsearch再启动 Kibana确认存储层可用然后启动 Kafka接着启动 Logstash 去消费 Kafka 往 ES 写最后配置 Filebeat。如果你先把 Filebeat 起起来它会在找不到 Kafka 时反复重连日志在本地文件里堆积等 Kafka 起来后才会一次性刷过去虽然不会丢但排查问题时容易造成“怎么现在才收到刚才的日志”的错觉。自上而下搭建每层都有明确的验证节点。2. 日志规范化和 TraceId 链路追踪落地2.1 日志格式统一没有规范一切白搭很多团队把 Filebeat、Kafka、ES 都搭起来之后发现 Kibana 里搜出来的日志五花八门有的团队用 JSON 格式有的团队用文本格式有的团队时间格式是yyyy-MM-dd HH:mm:ss有的团队直接打印毫秒时间戳。Logstash 的解析规则看到这种输入直接蒙圈。我在这个项目上最深刻的体会就是日志架构的技术难点不在组件而在日志规范。首先要统一日志框架。Spring Boot 默认用 Logback就不要让部分服务改成 Log4j2统一在父 POM 里指定日志框架避免多个日志框架的桥接冲突。其次要统一日志格式我在这个项目里推的标准格式是这样的pattern%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - [%X{traceId},%X{spanId}] %msg%n/pattern这段 pattern 里%X{traceId}和%X{spanId}是从 MDCMapped Diagnostic Context里读取的值Sleuth 会自动往里放加在模式里后每行日志都会带上链路 ID。团队里所有服务必须使用同一套 pattern谁也不能私下改格式。这一步如果不强制后面 Logstash 的解析要针对每套格式写不同的 grok 正则维护成本直接失控。还有一个容易被忽略的细节不要用中文夹杂时间格式不要把换行打在日志内容里。比如一个对象转 JSON 后通过logger.info()打印如果 JSON 里包含换行这条日志在文本模式下会被拆成多条严重影响采集。我的建议是结构化字段用简洁文本拼装或者直接输出单行 JSON异常堆栈则由 Filebeat 的多行合并机制处理而不是在业务代码里手动换行打印。2.2 TraceId 怎么生成、怎么传递Sleuth 还是手工 MDC在 Spring Cloud 生态里最直接的做法是引入 Spring Cloud Sleuth。它的原理并不神秘在请求进入时通过过滤器生成一个 TraceId向 HTTP 头写入X-B3-TraceId并通过 Feign、RestTemplate、网关的自动拦截器在服务调用间传递同时把 TraceId 写入当前线程的 MDC日志框架从 MDC 取值就能输出。这个过程对业务代码完全透明。接入方式非常简单dependency groupIdorg.springframework.cloud/groupId artifactIdspring-cloud-starter-sleuth/artifactId /dependency对应配置spring: application: name: order-service sleuth: sampler: probability: 1.0我在本地测试时采样率直接设为 1.0保证每条请求都能在 Kibana 里查到。生产环境一般建议0.1不然日志量很难控制遇到线上排查时再临时调高配合动态配置中心下发即可。需要提醒的是Sleuth 在 Spring Cloud 2022.0.x 之后停止迭代新项目建议直接看 Micrometer Tracing它沿用了 Brave TraceId 的底层模型主要 API 和配置基本一致理解这套原理换工具成本不大。异步场景是 TraceId 传递的重灾区。线程池里新开的线程默认没有继承父线程的 MDC导致异步调用后的日志丢了 TraceId。解决办法是写一个 TaskDecorator 包装类在任务提交前把当前线程的 MDC 复制到执行线程public class MdcTaskDecorator implements TaskDecorator { Override public Runnable decorate(Runnable runnable) { MapString, String contextMap MDC.getCopyOfContextMap(); return () - { try { MDC.setContextMap(contextMap); runnable.run(); } finally { MDC.clear(); } }; } }然后在线程池配置里设置setTaskDecorator(new MdcTaskDecorator())。这个细节在排查问题时特别管用如果你发现一个请求主线程链路有 TraceId到了异步线程打印的日志突然没有基本就是这个原因。2.3 脱敏与字段规范别把敏感信息送进 ES日志里最容易被忽略的是敏感信息。手机号、身份证号、银行卡号、密码之类的字段一旦进了 Elasticsearch再被可视化面板或者报表导出就会成为事故。最理想的做法是在业务入口做掩码但我见过太多项目在代码里直接打印登录请求体隐私数据裸奔到日志文件。所以我的方案是双保险业务层尽量不打印明文清洗层再兜底脱敏。Logstash 清洗层用 mutate 的 gsub 做正则替换比如手机号只保留前三位和后四位filter { mutate { gsub [message, (?1[3-9][0-9])[0-9]{4}(?[0-9]{4}), ****] } }实操中要注意一点不要把脱敏逻辑放在 Filebeat 上。Filebeat 的 processors 虽然也能做字段增减但它服务的是“快速搬运”这个职责加了复杂的正则解析会拖慢采集速度而且规则分散在多台采集端不方便统一管理。统一放进 Logstash 的 filter 阶段以后调整规则只需要改管道配置重载一次 Logstash 就好。字段规范也要提前定好。我对所有服务强制要求日志里必须包含service.name、level、traceId、class、message这几个核心字段并且统一字段命名。不要这个服务写appName那个服务写application否则在 Kibana 里建面板时你会发现同一个字段名有一堆变体聚合统计直接没法做。Logstash 里我会把fields.service映射成service.name把日志级别字段固定成level所有下游消费统一走这一层规范。3. 日志采集与缓冲层实战Filebeat Kafka3.1 用 Docker Compose 搭建基础设施日志架构里的基础设施组件比较多用 Docker Compose 在本地起一套最省事。我这里选用的是 apache/kafka 3.x单机模式、Elasticsearch 7.17.x、Kibana 7.17.x版本尽量保持一致。Compose 里几个关键配置我列一下services: elasticsearch: image: docker.elastic.co/elasticsearch/elasticsearch:7.17.22 environment: - discovery.typesingle-node - xpack.security.enabledfalse - ES_JAVA_OPTS-Xms1g -Xmx1g ports: - 9200:9200 kibana: image: docker.elastic.co/kibana/kibana:7.17.22 environment: - ELASTICSEARCH_HOSTShttp://elasticsearch:9200 ports: - 5601:5601 depends_on: - elasticsearch kafka: image: apache/kafka:3.6.0 ports: - 9092:9092 environment: - KAFKA_BROKER_ID1 - KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR1注意如果在本地用 Docker 跑 Elasticsearch容器内discovery.typesingle-node是必须的不然会尝试集群发现导致启动失败。ES 8.x 版本默认启用安全认证不想折腾的话要么关闭xpack.security.enabled要么用 7.17。另外单机环境 Kafka 配置复制因子必须为 1基准环境不需要多副本。这里还要规避一个常见坑Spring Boot 服务如果跑在宿主机上产生的日志目录通过 Volume 挂载给 Filebeat 容器。如果你把 Filebeat 也放进 Compose它访问宿主机上的日志文件需要指定绝对路径挂载filebeat: image: docker.elastic.co/beats/filebeat:7.17.22 user: root volumes: - /var/log/apps:/var/log/apps - ./filebeat.yml:/usr/share/filebeat/filebeat.yml:roFilebeat 镜像默认是非 root 用户运行读取宿主机日志文件时很容易遇到权限问题实测用user: root是最快的解决方案。另外不要忘了挂载/usr/share/filebeat/data目录这个目录保存了 Filebeat 的 registry 文件也就是已经采集到哪个位置的记录。如果容器重建后这个目录丢失Filebeat 会从头读取所有历史日志下游会出现大量重复数据。3.2 Filebeat 采集配置与多行日志处理Filebeat 的配置核心有两个一是采哪些文件的日志二是送到哪里。我把核心配置列出来filebeat.inputs: - type: log enabled: true paths: - /var/log/apps/*.log fields: service: order-service fields_under_root: true multiline: type: pattern pattern: ^[0-9]{4}-[0-9]{2}-[0-9]{2} negate: true match: after max_lines: 1000 timeout: 5s processors: - add_host_metadata: ~ output.kafka: hosts: [kafka:9092] topic: app-log partition: round_robin: reachable_only: truepaths 里的*.log会匹配目录下的所有日志文件。如果你的服务按天滚动生成日志文件Filebeat 会自动识别新文件名并继续采集不需要重启。fields.service是我用来标记某个日志文件属于哪个服务的fields_under_root: true让service字段直接出现在日志事件的顶层这样 Kibana 里可以直接按service字段过滤。多行日志处理是 Java 项目最刚需的功能。异常堆栈从第一行Exception开始后面每一行都是堆栈详情如果 Filebeat 不对它们做合并一条异常会被拆成几十条独立日志在 Kibana 里根本看不出这是一个异常。上面的配置里我用pattern判断行首是否匹配时间格式2023-01-01匹配到的行作为新日志的开始negate: true和match: after表示“不匹配该模式的行追加到上一条日志后面”。这里加两个参数效果更好max_lines: 1000限制单条日志最大行数防止超长堆栈导致的单条事件过大timeout: 5s指定多行合并的等待时间超过 5 秒还没有新行进来强制把当前多行事件发出去。这两个参数不加遇到超大异常堆栈或者日志迟迟不结束的情况采集会一直占着内存。输出端我用的是round_robin分区策略日志事件轮询发送到 Kafka 分区。如果直接不指定partitionFilebeat 默认会按照字段哈希路由好处是同一关键字的消息进同一分区但日志场景下没有这个需要轮询反而让各分区负载更均匀。3.3 Logstash 消费与清洗管道Logstash 在整个链路里的定位是“消费 Kafka 清洗日志 写入 ES”。管道配置文件在logstash.conf里核心输入部分这样写input { kafka { bootstrap_servers kafka:9092 topics [app-log] group_id logstash-log-group consumer_threads 3 auto_offset_reset latest } }consumer_threads很关键它决定 Logstash 用几个线程消费 Kafka。这个数值和 Kafka topic 分区数有关尽量让consumer_threads 分区数否则有些分区没有消费者线程会出现日志延迟。在一个分区数设为 3 的 topic 上consumer_threads 3是最稳妥的起步配置。filter 阶段我在这个项目里优先使用 JSON 解析而不是 grok 正则。原因很简单JSON 解析速度是正则的几十倍。如果日志输出端已经统一为 JSON 格式Logstash 里这样写filter { json { source message target parsed remove_field [message] } date { match [[parsed][logTimestamp], yyyy-MM-dd HH:mm:ss.SSS] target timestamp } }这里把timestamp统一成业务日志里的时间而不是 Logstash 的接收时间。如果你不改这一步Kibana 里看到的时间就是「Logstash 收到日志的时刻」网络延迟和消费滞后都会造成时间不准排查问题时会把你带到沟里。output 部分就是写入 Elasticsearch索引按天切分output { elasticsearch { hosts [http://elasticsearch:9200] index log-%{yyyy.MM.dd} } }Logstash 的吞吐瓶颈主要在 filter 阶段尤其是复杂正则。我在调优时给过一版结论默认管道配置下4 核 8G 的节点用 JSON 解析单机吞吐大约 3 万条/秒如果换成复杂 grok 正则解析同一份数据直接掉到 1 万条/秒以下。能用 JSON、能拆字段就不要写花哨的正则。4. 存储与检索层Elasticsearch 索引设计4.1 索引规划与 ILM 生命周期管理Elasticsearch 存的不是“日志文件”而是“索引”。索引设计直接决定日后的检索效率和磁盘成本。日志场景最常见的索引规划就是按天分索引比如log-2024.01.01、log-2024.01.02。这样分的好处是删除过期数据只需删除整个索引没有碎片操作查询时限定日期范围能自动避开无关索引减少扫描量。我一般建议使用索引模板 ILM 生命周期策略配合。模板负责定义新索引的配置比如分片数、副本数、字段映射ILM 负责处理“存在 30 天的索引自动删除”这类策略。模板配置示例PUT _index_template/logs-template { index_patterns: [log-*], template: { settings: { number_of_shards: 3, number_of_replicas: 1, refresh_interval: 30s } } }这段配置的效果是Logstash 每次写入新索引log-2024.01.01时ES 自动套用模板分片 3 个、副本 1 份refresh_interval30 秒刷新一次。注意这个模板必须在日志开始写入之前就建好否则已经生成的索引不会自动更新配置后面想改只能 reindex麻烦得很。ILM 策略我通常会这样做PUT _ilm/policy/logs-ilm { policy: { phases: { hot: { min_age: 0ms, actions: {} }, delete: { min_age: 30d, actions: { delete: {} } } } } }然后把 ILM 策略挂到log-*索引模板上ES 会定时检查索引年龄超过 30 天自动删除。有些团队先跑起来再说忘了配 ILM结果磁盘被日志塞满ES 进入只读模式这种事故我见过不止一次。4.2 分片与容量估算分片数量在日志场景里有两条经验法则单分片容量控制在 30GB 到 50GB 之间分片数不要超过节点可用 CPU 的若干倍。估算过程其实不复杂拿我这个项目举例单条日志平均约 0.8KB每日请求约 1000 万次每个请求平均产生 3 条日志那么单日日志量约为 2400 万条折合约 20GB保留 30 天就是 600GB加上 1 份副本总占用约 1.2TB。容量的另一头是写入性能。单分片写入速度相对有限为了利用多个节点并行写入我把每日索引拆成 3 个分片让写入可以分布到不同节点。但这里要注意分片不是越多越好。每个分片有固定的内存和文件句柄开销分片过多会导致集群因元数据膨胀而变慢。按我上面的 20GB/天 规模3 个分片已经足够规模再翻十倍再考虑扩到 9 个分片。写入性能调优方面我在 Logstash output 里通常会加大批量参数让 Logstash 攒一批日志再一次性发给 ESoutput { elasticsearch { hosts [http://elasticsearch:9200] index log-%{yyyy.MM.dd} flush_size 1000 bulk_path /_bulk } }同时 ES 端refresh_interval默认 1 秒刷新一次日志场景不需要这么高的实时性调成 30 秒能显著降低写放大。转到底层translog 的刷盘策略也可以从同步改成异步但前提是你能接受极端情况下丢少量最近日志。4.3 磁盘、内存和常见硬件规划ES 的硬件规划有一句老话内存给一半剩下一半留给操作系统页缓存。也就是物理内存 32GB 的机器ES 堆内存设置 16GB不要超过 31GB。原因在于 Lucene 底层大量使用堆外内存做缓存和文件映射堆内存只用来处理查询和聚合两者分得太偏都会出问题。启动参数可以这样设置ES_JAVA_OPTS-Xms16g -Xmx16g日志场景对磁盘要求很高有条件尽量用 SSD。ES 日志检索大量依赖文件系统缓存SSD 的随机读性能直接决定 Kibana 查询体感。机械盘在数据量小的时候感受不明显索引上来之后一个范围查询可能慢到几十秒。还要检查几个系统参数。关闭 swap防止 ES 进程被换出导致节点脱离集群文件句柄数至少 65535vm.max_map_count 在 Linux 上要调到 262144否则 ES 启动时会直接报错。这里分享一个判断标准如果你每天日志总量不到 50GB单机 ES 足够不需要一开始就上三节点先把单机跑稳后面加节点走集群扩容即可。5. 可视化、检索与告警5.1 Kibana 索引模式与常用检索Kibana 打开后第一件事是创建 Data View7.x 版本对应的是 Index Pattern。索引模式填log-*时间字段选择timestamp保存后 Discover 页面就能按时间范围检索所有日志。真正排查问题时的常用检索可以分三类。第一类是按 TraceId 查全链路直接输入traceId: abc123搜索结果里所有携带这个 TraceId 的日志会集中展示从上到下按时间排序一眼就能看到请求在哪些服务间流转、每层耗时多少。第二类是查错误组合查询service.name: order-service AND level: ERROR第三类是查异常详情通常在message字段里搜异常类名比如message: NullPointerExceptionKibana 的查询语法有两种Discover 默认支持 KQL熟练之后效率会高很多。KQL 里用and、or、not组合条件字段名和值都区分大小写。有一个体验优化把经常用的查询保存为 Saved Search下次直接打开避免每次重新输入条件和时间范围。5.2 日志监控面板设计Kibana 的面板设计是让日志数据真正产生业务价值的地方。别只看单条日志把数据汇总成趋势图才容易发现规律。我最先做的三个面板是日志量时间序列、ERROR 级别日志趋势、TOP N 异常服务。用 Lens 拉数据横轴是timestamp纵轴用count聚合再做几个过滤条件就出来了。面板字段依赖前面提到的字段规范。如果你的日志里service.name命名混乱面板上会出现一排脏数据如果你没有统一level字段错误统计根本做不出来。所以我在第 2 章反复强调字段规范可视化阶段就看出价值了。保存 Dashboard 后记得设置自动刷新通常 15 秒一次。大屏投放建议用只读模式不要给所有人编辑权限否则随手拖动面板会把布局搞乱。我还会把 Dashboard 的 URL 分享给运维同学设置好时间范围默认“最近 15 分钟”省去他们自己选时间的操作。5.3 告警错误日志、日志量突降和业务指标监控日志告警其实比应用指标告警更难做因为日志数据量大、文本信息不规则误报率天然偏高。我在 7.x 时代的方案是 ElastAlert2它比 Kibana 自带的告警规则更灵活。常见的告警需求有两种第一种是错误日志量突增。ElastAlert2 里配置一个 frequency 类型规则查询 15 分钟内level: ERROR的日志条数超过阈值就触发通知es_host: elasticsearch es_port: 9200 index: log-* type: frequency num_events: 100 timeframe: minutes: 15 filter: - query: query_string: query: level: ERROR alert: - elastalert2.alerters.webhook.WebhookAlerter http_endpoint: http://your-webhook/message?tokenxxx第二种是日志量突降。这个信号代表“业务停服了”或者“采集链路出了问题”比错误日志更值得重视。规则可以设置 10 分钟内日志总量低于历史均值的一半就告警。注意告警要做静默和防抖比如同一个错误触发了 20 次只需要发一次通知避免告警风暴。我见过凌晨三点被打十几个电话结果只是同一个异常在循环打印。6. 常见问题与排查技巧实录6.1 日志丢失、重复与重复消费日志丢失是最让人头疼的问题而且“丢”的位置不止一个。Filebeat 阶段常见原因是 registry 文件丢失或挂载目录没持久化容器重启后 Filebeat 以为这些日志还没读过会产生重复而不是丢失真正丢失一般发生在 Kafka。Kafka 默认保留时间只有 7 天如果 Logstash 挂掉一周旧数据直接过期清除日志再也找不回来。所以我在测试环境把log.retention.hours调大到 168 小时生产环境则根据实际排查周期设置。Logstash 消费阶段最容易出现的是滞后。如果你发现 Kibana 里的日志时间总是比当前时间晚几分钟甚至几小时先去看 Kafka 消费者组延迟命令是kafka-consumer-groups.sh --bootstrap-server kafka:9092 --describe --group logstash-log-group输出的LAG列如果持续增长说明 Logstash 消费能力跟不上优先加consumer_threads其次加 Logstash 实例。另一个隐蔽问题是分区和消费者线程数不匹配topic 创建了 6 个分区但consumer_threads只配了 2会有 4 个分区处于半闲置状态日志延迟却只产生在这几个分区上很难察觉。重复日志在日志场景里是常态因为 Kafka 本身只保证至少一次语义Logstash 崩溃重启后可能重复消费部分消息。好在日志数据重不重复没那么致命但如果你强迫症上来了可以在 Logstash output 里设置document_id让 ES 幂等写入避免同一日志重复存储output { elasticsearch { hosts [http://elasticsearch:9200] index log-%{yyyy.MM.dd} document_id %{[metadata][kafka][offset]}-%{[parsed][appHost]} } }6.2 TraceId 没有贯穿全链路的几个原因Kibana 里按 TraceId 搜索最让人沮丧的是搜出来只有当前服务的几条日志别的服务完全没有。原因基本逃不出这几类。第一网关转了其它协议比如 HTTP 转 Dubbo默认不会自动传递 HTTP 头里的X-B3-TraceId需要在 RPC 上下文里手动透传。第二服务里自定义了 RestTemplate 拦截器覆盖了 Sleuth 注入的 header这种情况在 Feign 和 RestTemplate 混用的项目里很常见。第三异步线程池丢 MDC前面 2.2 小节已经给了解决方案。定位思路很简单在 Kibana 里搜某个 TraceId看日志断在哪一环。比如下单链路包含 API 网关、订单服务、支付服务网关日志有 TraceId订单服务也有但支付服务完全没有那问题就出在订单服务调用支付服务这一段重点查 Feign 拦截器和 HTTP header。排查调试阶段可以在网关和服务入口加一个打印logger.info(incoming traceId {}, MDC.get(traceId));把入口和出口的 TraceId 都打出来一旦链路断了对比两头的值就能快速确定是哪个环节丢的。这一步能省大量时间。6.3 时区、多行堆栈和时间格式问题日志时间错误是新手最容易踩的坑症状是 Kibana 里的日志时间比服务器本地时间晚 8 小时。根本原因是 Elasticsearch 默认以 UTC 存储时间而 Java 日志默认打印本地时间Asia/Shanghai。如果你在 Logstash 里用date插件解析日志里的时间字段默认按 UTC 解析8 小时偏差就出现了。解决方式很简单在 date 插件里明确指定时区date { match [[parsed][logTimestamp], yyyy-MM-dd HH:mm:ss.SSS] timezone Asia/Shanghai target timestamp }如果日志里输出的是带时区的时间字符串也用这个方式统一转成timestamp。这里还涉及一个团队规范如果所有服务都跑在同一个时区就统一按本地时间打印Logstash 统一指定timezone Asia/Shanghai如果服务跨时区部署更稳妥的是所有服务都打印 UTC 时间展示层再由 Kibana 按浏览器时区渲染。多行堆栈的问题在 3.2 小节已经讲透实际操作中还有一个常见现象配置了 multiline 但还是被拆开。这时候先确认negate和match是不是反了最常见的语义是pattern匹配行首时间negate: true表示非时间行match: after表示接续上一行。如果你日志里第一行不是时间而是服务名或上下文就调整 pattern 的正则去匹配真正的“新日志开始标志”。6.4 资源占用与系统稳定性日志组件稳定运行的前提是资源隔离。Filebeat 如果采集的目录下文件特别多比如业务日志和 access log 混在一起内存会吃得很高。我一般会控制harvester_limit参数限制 Filebeat 同时打开的文件数避免一次性读取过多文件导致内存暴涨ignore_older设置超过 24 小时没有更新的文件直接忽略防止历史文件被重新解析一次。ES 的稳定性要重点关注 full GC 和慢查询。如果频繁出现elasticsearch.log里的slow query先看是不是聚合的字段没有开启doc_values日志场景按service.name聚合是最常见的场景这个字段在模板里就要设置type: keyword并保留doc_values。ES 堆内存如果被打满表现为节点 CPU 飙升、查询超时这时优先调大节点数或堆内存而不是无限堆 SQL 式聚合。我在架构设计里一直坚持一条底线日志链路不能反噬业务。Kafka、ES、Logstash 任何一个挂了都不应该让业务进程卡住。Filebeat 是无侵入的这一点没问题但如果把 Logstash 和业务服务部署在同一台机器上要限制它的内存和 CPU不要让日志管道把业务资源吃光。6.5 顺手整理的 SpringCloud 面试题速记这套日志架构涉及的知识点经常出现在面试里问题来回就这几个顺手整理一下。第一个分布式日志架构中为什么需要引入消息队列回答要点是削峰填谷、解耦采集与存储、数据可重放、多消费者复用。第二个TraceId 是怎么跨服务传递的要点是网关生成 → 写入 HTTP Header → 下游服务从 Header 读取 → 写入 MDC → 继续透传。第三个如何保证日志不丢回答要多层拆解Filebeat 本地 registry、Kafka 副本、Logstash offset 提交、ES 副本。第四个ES 写入变慢怎么排查优先看索引刷新频率、批量大小、堆内存和磁盘水位。第五个微服务日志格式为什么要统一因为 Logstash 解析、Kibana 聚合、告警规则都依赖统一的字段结构。这几个问题答好基本就能说明对日志架构底层逻辑真有理解。踩过这么多坑之后我的整体感受是搭建分布式日志架构真正难的不是某个组件怎么配而是能不能把“日志规范”这件事从第一天就当成架构的一部分来抓。一套能用的日志体系七分靠规范三分靠组件。如果你正准备从头搭建议一定先跑最小闭环一个 Spring Boot 服务写文件Filebeat 采集Logstash 清洗ES 存储Kibana 展示。链路通了再加 Kafka、加 ILM、加告警逐层递进这时候每加一层你能更清楚它是解决什么问题的。最后再分享一个我自己的小习惯上线第一周每天晚上定时看一眼 Kibana 的错误日志占比和日志量趋势连续看一周基本就能摸清这套系统的健康基线。以后再收到告警你至少知道现在的情况是“异常”还是“常态”排查效率会完全不一样。另外真遇到“日志查不到”的问题不要一上来就怀疑组件坏了从 Kibana 逐层往上游检查索引有没有当天数据Logstash 有没有消费Kafka 里有没有堆积Filebeat 有没有读到新文件。按这个顺序查十分钟之内找不到根因的情况很少。