☰
从零搭建分布式日志系统:Filebeat、Kafka与Elasticsearch实践
2026/9/28 13:01:43 网站建设 项目流程

半夜两点被电话吵醒,线上订单服务超时,我登录第一台服务器看日志,没有异常,第二台也没有,直到第七台才看到一串NPE。可真正的麻烦在于,这个报错背后是一个横跨五台机器的调用链,单机日志根本拼不出全貌。那晚之后我开始动手搭建分布式日志系统,现在回头看,这套系统不光把我从on-call的泥潭里拽了出来,更让团队排查问题的平均耗时从小时级降到了分钟级。这篇就聊聊从零到一落地这套系统的完整实现路径:采集端怎么设计、传输链路怎么保证不丢不重、存储层怎么控制成本和查询速度、以及那些文档里写不到,只有踩过坑才会懂的细节。不管你是10台机器还是几百台机器,这套思路都能直接复用。

1. 日志散落在几十台机器上,排查一次故障到底要多久

先说一个反直觉的事实:很多团队的日志总量并没有大到必须上分布式日志系统的程度,真正逼你上系统的,是日志的分散度和关联度。当你的应用从单机变成集群,从单体变成微服务,日志文件就不再是“打开一个文件就能看完”的东西了。

1.1 单体日志时代的四个典型痛点

第一个痛点是没有全局视角。一个用户请求经过网关、订单中心、库存中心、支付中心,每个服务只写自己的日志文件,单独看任何一个文件都无法还原完整调用链。你想知道“这个订单为什么失败”,得先弄清楚请求到底走了哪些节点,然后逐个节点去找蛛丝马迹。这个过程的挫败感,做过线上排障的人都懂。

第二个痛点是定位效率极低。传统模式下排查日志的基本操作是:SSH登录服务器,cd到日志目录,tail或者grep,然后退出,再登录下一台。如果是几十台机器,这个循环操作能把人逼疯。更别提线上故障往往是高并发时段,每台机器上每秒都刷出几百条日志,grep出来的结果可能还有大量干扰项。

第三个痛点是磁盘安全隐患。单机日志看起来不大,但几十台机器累积起来就是很大的量。应用日志、访问日志、慢SQL日志、异常堆栈,日积月累能吃掉大量磁盘空间。日志把根分区写满导致应用崩溃的情况,我见过不止一次。更尴尬的是,当你真的需要排查问题的时候,部分历史日志可能已经被logrotate清理掉了。

第四个痛点是无法做聚合分析。你想统计某个接口的P99耗时、某个错误码出现的频率、某个用户的操作轨迹,靠grep是做不到的。哪怕把所有日志拿到一台机器上,没有结构化的解析和索引,做这种统计几乎等于手工大海捞针。

1.2 分布式日志系统到底解决了什么

分布式日志系统本质上做的事情就四件:统一采集、集中存储、快速检索、关联分析。采集端自动把几十台机器的日志汇聚起来,通过消息队列缓冲削峰,最终落到搜索引擎里,让你在一个搜索框里完成之前需要登录几十台机器才能完成的事。配合trace_id贯穿调用链,一次请求从入口到出口的全部路径,可以在几秒钟内拉出来。

我搭建这套系统之后,团队内部的变化不是“方便了一点”,而是排障模式整个变了。以前是“先问谁负责哪个服务,再登录机器看日志”,现在是“拿到trace_id,搜索,完事”。排查一个跨服务问题的平均耗时,从原来的一个多小时降到了五到十分钟,这个收益是实打实的。

2. 我最终落地的四层架构:采集、缓冲、索引、检索

分布式日志系统的架构并不复杂,业界的主流方案已经趋于稳定。我采用的是经典的四层模型:采集层、缓冲队列层、存储索引层、展示查询层。每一层各司其职,中间用消息队列隔开,这是整套系统最核心的设计决策。

2.1 为什么中间必须有一层消息队列

很多人最开始做日志系统的时候,会想“Filebeat直接往Elasticsearch写不就行了?”,省掉Kafka这一层不是更简单吗?这种方案在小规模下确实能跑,但一旦流量上来或者ES出问题,就会暴露两个致命缺陷。

第一是削峰填谷。应用写日志是突发的,活动大促的时候每秒可能几千条,凌晨低谷的时候可能每秒几十条。Elasticsearch批量写入最喜欢的流量形态是平稳的,突发流量容易把ES的bulk队列打爆,导致写入拒绝。中间加一层Kafka,消费端就可以按照ES能承受的速度匀速消费,把峰值流量抹平。

第二是解耦容错。如果ES发生故障或者升级重启,采集端直连ES的话,日志要么在Filebeat本地积压,要么直接丢弃。有了Kafka做缓冲,ES宕机一两个小时根本不影响采集端,数据安全地躺在Kafka里,等ES恢复后再继续消费。这个特性在生产环境的价值极大,我后面会详细展开说。

2.2 技术选型对比:为什么是Filebeat + Kafka + Elasticsearch

先看采集端,市面上主流的有Filebeat、Fluentd、Logstash、OpenTelemetry Collector。我的选择是Filebeat,原因很直接:它是Go写的,内存占用极小,默认运行只需十几MB的常驻内存,而Logstash默认就要1GB以上的JVM堆。在每台业务机器上部署Agent,资源占用是必须优先考虑的。Logstash更适合做集中式处理,而不是散布到几十台机器上做采集。Fluentd也很优秀,但在Ruby生态下插件的部署维护相对繁琐,对于我这种希望“一把梭”的团队来说,Filebeat的二进部署式更省心。

传输层我用的是Kafka。有些人会用Redis做缓冲,但Redis毕竟是缓存型存储,在日志量大的场景下内存成本很高,而且Kafka的日志持久化、分区消费、消息回溯能力是Redis不具备的。另一个候选是Pulsar,它的架构更现代,但当时团队的运维熟练度不足以支撑多一个中间件的学习成本,Kafka依然是日志传输领域最成熟的选择。

存储与检索引擎,毫无疑问是Elasticsearch。虽然现在Loki和ClickHouse也很火,但ES在全文检索、聚合分析、生态成熟度上依然是最稳妥的选择。关于Loki我后面会专门说,先给结论:如果你不是K8s原生环境且日志量极大,ES依然是首选。

2.3 动手之前先算一笔账:数据量预估

任何架构设计的第一步都不是选型,而是算数据量。以一个30台应用服务器的集群为例,假设峰值业务TPS为2000,每个请求平均产生5条日志(Controller入参、业务逻辑、SQL执行、响应日志、可能的异常堆栈),每条日志平均500字节左右。

那么峰值日志速率是:

  • 每秒日志条数 = 2000 × 5 = 10000条
  • 每秒数据量 = 10000 × 500B = 5MB/s
  • 一天的数据量 = 5MB/s × 86400秒 ≈ 432GB

也就是说,这个规模下每天的原始日志量在400GB左右。经过Kafka的gzip压缩,传输量能降到100GB以内;落ES后再经过压缩存储,配合索引生命周期管理,实际占用可以控制在120GB到150GB左右(含一个副本)。

这个数字直接决定了后续的分区数量、ES分片数量、节点规格和磁盘预算。我见过不少团队不做估算,直接套用网上的“标准配置”,结果要么资源浪费,要么索引分片过多导致集群性能严重下滑。算好这笔账,后面的所有配置才会有依据。

3. 采集端Filebeat的完整落地:多行合并、分区策略与背压

采集端是整套系统的最前线,最常见的问题都发生在这里。我用的Filebeat版本是8.x,配置全部用YAML管理,下面直接给出核心文件的内容。

3.1 Filebeat核心配置解析

filebeat.inputs: - type: filestream enabled: true id: app-logs paths: - /data/logs/*.log fields: service_name: order-center env: production fields_under_root: true parsers: - multiline: type: pattern pattern: '^[0-9]{4}-[0-9]{2}-[0-9]{2}' negate: true match: after output.kafka: hosts: ["kafka-1:9092", "kafka-2:9092", "kafka-3:9092"] topic: "app-log" partition.round_robin: reachable_only: true required_acks: 1 compression: gzip max_message_bytes: 1048576 queue.mem.events: 65536 queue.mem.flush.min_events: 2048 queue.mem.flush.timeout: 5s

这里有几个地方需要特别说明。第一,filestream是Filebeat 8.x推荐的输入类型,相比老的log类型,它对文件状态追踪更可靠,重命名和轮转时不容易丢数据或者重复读。我用filestream之后,再也没有出现过因为logrotate导致的重读问题。

第二,multiline多行合并配置是针对Java异常栈的。Java应用抛异常时,堆栈会跨多行,比如这样的输出:

2024-06-01 12:33:45.123 [http-nio-8080-exec-1] ERROR OrderService - create order failed java.lang.NullPointerException: null at com.example.OrderService.createOrder(OrderService.java:88) ~[app.jar!/:1.0] at com.example.OrderController.submit(OrderController.java:42) ~[app.jar!/:1.0] at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]

如果Filebeat按行读取,这一条异常就会被拆成5条独立日志,在Kibana里看到的就是一堆无法阅读的碎片。multiline配置的意思是:匹配以日期开头的行作为新日志的开始,不匹配的行合并到上一条日志后面,直到下一条日期开头的行出现。这样一整段异常堆栈才会被当成一条完整日志。

第三,fields_under_root: true把service_name和env两个字段提升到文档根级别。这样ES里每条日志都天然带着服务名和环境标识,查询时直接service_name: order-center AND level: ERROR就能过滤,不需要嵌套层级,Kibana的字段展示也干净很多。

3.2 Kafka Topic与分区设计

日志的Topic设计直接关系到后续消费的灵活性和性能。我没有把所有的日志都塞进一个Topic,而是按业务域拆成了三个:

Topic 名称用途数据特征
app-log应用业务日志量大,最重要
access-logNginx与网关访问日志结构固定,量大
audit-log操作审计日志量小,需长期保存

Topic拆开之后,后续可以针对不同Topic设置不同的消费逻辑和保留时间。比如audit-log可能需要保留半年以上,access-log保留30天,app-log保留15天。如果混在一个Topic里,这些差异化策略就没法做了。

分区数的确定也很有讲究。Kafka中,一个分区的消息在同一时间只能被同一个消费组中的一个消费者线程消费,所以分区数就是消费并行度的上限。我们消费端计划部署3个实例,分区数至少是3的倍数,我选择了9个。同时考虑单分区吞吐,我们的日志峰值5MB/s,Kafka单分区写入吞吐轻松超过这个数量级,9个分区完全够用。

还有一个小技巧是partition.round_robin配合reachable_only: true,让Filebeat在写入时轮询所有分区,并自动跳过不可达的leader。这样单分区所在broker出问题时,采集端不会卡死。

3.3 背压:当Kafka不可用,Filebeat会怎么办

这是很多人忽略的一点。Filebeat的内存队列默认只有4096条,如果Kafka长时间不可用,队列满了之后采集就会阻塞,导致业务日志在本地不断堆积,最终可能把业务磁盘写满。我在配置里把queue.mem.events调整到了65536条,相当于在内存里多缓存了6万多条日志。

但内存队列再大也是有上限的。真正稳妥的做法是双保险:第一,给Kafka设置足够长的日志保留时间,比如log.retention.hours=72,确保Kafka恢复后数据还在;第二,在Filebeat侧监控队列积压情况。如果发现队列持续打满,说明Kafka已经完全不可用,这时候要尽快处理Kafka的问题,而不是指望Filebeat无限缓存。

我实测下来,Kafka短暂抖动(一两分钟)的情况下,Filebeat的65536内存队列加Kafka自身的持久化,完全能兜住。这个经验值你可以根据自己的日志量去换算,不要盲目抄。

4. 存储层不踩坑的关键:索引分片、ILM与成本控制

日志系统最容易翻车的地方,不在采集端,而在存储层的规划设计。ES看起来装好就能用,但分片规划不当、索引无限增长、段合并跟不上,三个月之后集群就会开始出现各种慢性病。这一节把我在存储层踩过的坑和最终沉淀下来的配置写清楚。

4.1 索引命名与别名机制

ES的索引设计遵循一个简单原则:按天建索引,按类型分前缀。我用的命名格式是:

app-log-2024.06.01 access-log-2024.06.01 audit-log-2024.06.01

这样每天一个索引,查询时用通配符app-log-*,数据清理时直接删除整个索引,而不是用delete_by_query去逐条删,性能和安全性都更好。同时为每个类型设置一个别名指向最新索引,写入时走别名,写入端不用关心具体索引日期:

POST _aliases { "actions": [ { "add": { "index": "app-log-2024.06.01", "alias": "app-log-write" } } ] }

4.2 分片数量不能拍脑袋

ES分片数的计算逻辑是:单分片数据量控制在20GB到40GB之间。分片太小,集群里的分片数量膨胀,每个分片都有独立的segment和文件句柄,资源开销巨大;分片太大,单分片的查询和写入性能会下降,数据均衡也不灵活。

以我们每天约150GB的存储量来计算(含副本):

  • 主分片数 = 150GB / 25GB ≈ 6个
  • 加一个副本,总分片数为12个

所以我给每个索引设置6个主分片、1个副本。有些资料建议分片数等于节点数,但我实践下来,只要单分片数据量合理,分片数略多于节点数问题不大,反而能打散热点。

需要特别提醒的是,主分片数在建索引之后是无法修改的,只能通过reindex重建索引。所以上线前一定要按数据量估算好,不要图省事直接用默认的1主分片,否则后续数据量上来,你只能痛苦地reindex。

4.3 索引生命周期管理ILM配置

ES 7.x之后提供了内置的Index Lifecycle Management,可以在索引达到指定条件时自动执行转冷和删除。我用的策略是这样的:

PUT _ilm/policy/app-log-lifecycle { "policy": { "phases": { "hot": { "actions": { "rollover": { "max_size": "50gb", "max_age": "1d" } } }, "warm": { "min_age": "3d", "actions": { "shrink": { "number_of_shards": 1 }, "forcemerge": { "max_num_segments": 1 } } }, "delete": { "min_age": "30d", "actions": { "delete": {} } } } } }

这个策略在hot阶段按50GB或1天做rollover,避免单个索引过大;3天后进入warm阶段,shrink将6个分片缩成1个,forcemerge把segment合并到1个,查询慢的问题缓解不少,存储开销也下降;30天后直接删除整个索引。加上nginx日志、审计日志各自保留时间不同,策略文件名需要区分:access-log-lifecycle(保留30天)、audit-log-lifecycle(保留180天)。

然后给索引模板绑定策略:

PUT _index_template/app-log-template { "index_patterns": ["app-log-*"], "template": { "settings": { "number_of_shards": 6, "number_of_replicas": 1, "routing.allocation.require.box_type": "hot" } }, "priority": 100 }

这里我额外加了routing.allocation.require.box_type: hot,配合节点上的标签,让新写入的数据落在热节点上。后面冷数据迁移到warm节点时,再通过ILM自动把索引的allocate设置改掉。这个做法在节点较多、冷热分层的场景下非常实用。

4.4 轻量场景的替代方案:Loki

如果你们的日志量不大,或者跑在Kubernetes环境里,Loki是值得考虑的替代方案。Loki的设计思路是“索引日志的标签,而不是全文索引日志内容”,所以存储成本低很多,部署也更轻。但Loki的全文检索是基于LogQL的,聚合分析和全文搜索能力比ES孱弱不少。

我给的参考结论是:如果你的核心诉求是“集中查看日志 + 关键字搜索”,Loki完全够用,成本优势明显;如果你要做复杂的聚合分析、异常检测、关联查询,或者已经需要ES周边生态(如Watcher告警、Kibana可视化),那还是选ES。我们团队因为排障时经常要做跨服务字段的聚合分析,最终选择了ES,这个决策至今没有后悔过。

5. 从“不丢日志”到“一查到底”:可靠性设计与trace_id串联

日志系统的可靠性要求其实和业务系统不同,不需要“精确一次”那么高的标准,但至少要保证“不丢主干日志”和“能按调用链查到底”。这一节聊聊这两个目标的落地方案。

5.1 “至少一次”语义下的可靠性保障

先说结论:在日志场景下,接受少量重复,但不接受丢失,是更务实的策略。Filebeat从日志文件读取是基于偏移量记录的,数据成功写入Kafka后才会提交偏移量,失败则重试,所以Filebeat到Kafka这一段天然是“至少一次”语义。Kafka自身通过多副本机制保证消息不丢;消费端从Kafka读取并批量写入ES时,如果批次写入部分失败,重试后可能产生重复文档。

要想彻底去重,可以在ES文档中增加一个唯一id字段,用服务名 + trace_id + 日志序号做Hash,写入时用这个值作为文档_id。这样重复投递的消息在ES中会覆盖旧文档,实现幂等写入。但我们实测下来,日志重复率极低且对查询影响可以忽略,所以最终没有做文档级去重,而是把精力放在了更重要的trace_id贯穿上。

5.2 用trace_id把一次请求的所有日志串起来

分布式日志系统真正的杀手锏是trace_id。每个请求在入口处生成一个全局唯一的trace_id,通过HTTP Header传递到所有下游服务,所有服务打印日志时把这个trace_id带进去,这样在Kibana里一搜,一次请求在所有服务上打出的几十条日志就全部拉出来了。

Java端的实现很简单,用SLF4J MDC:

// 请求入口过滤器 String traceId = UUID.randomUUID().toString().replace("-", ""); MDC.put("trace_id", traceId); try { chain.doFilter(request, response); } finally { MDC.remove("trace_id"); }

下游服务从请求头中读取trace_id,再放入自己的MDC:

String traceId = request.getHeader("X-Trace-Id"); if (traceId == null || traceId.isEmpty()) { traceId = UUID.randomUUID().toString().replace("-", ""); } MDC.put("trace_id", traceId);

Logback配置里加上trace_id输出:

<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%X{trace_id}] %logger{36} - %msg%n</pattern>

这里有个非常容易犯的错:线程池里MDC会串线。如果你在业务代码里用ExecutorService异步执行任务,子线程不会自动继承父线程的MDC,导致异步日志的trace_id为空。更隐蔽的是线程池复用线程时,如果上一个任务设置了MDC但没清理,下一个任务会拿到上一个任务的trace_id,排查起来会怀疑人生。

解决方案是在线程池的TaskDecorator里做MDC上下文传递:

ExecutorService executor = new ThreadPoolExecutor( core, max, keepAlive, TimeUnit.SECONDS, new LinkedBlockingQueue<>(), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy() ); ((ThreadPoolExecutor) executor).setThreadFactory(new ThreadFactory() { // ... 常规工厂 }); // 用自定义TaskDecorator包装Runnable

核心逻辑就是:提交任务前把当前线程的MDC快照拷贝一份,执行任务时恢复快照,执行完清理。这样异步线程里的日志也带上正确的trace_id。这个坑我们是在上线后通过对比同一trace_id的日志数量时发现的,排查了很久。

5.3 消费端写ES的批处理优化

消费端从Kafka读数据写ES,不用一条一条地写,而是攒批bulk写入。我自研的Go消费程序核心逻辑很直接:每2000条或者每2秒flush一次,批量提交到ES的bulk接口。批量的大小要实测,我这边ES单节点bulk写入5000条/批次时延迟暴涨,调整到2000条后就稳定了。同时注意,不要在主循环里同步等待ES返回,而是用并发池处理多个批次,消费速度才能跟上生产。

6. 上线后让我失眠的六个问题:完整排错记录

这部分是我最想写的。整套系统跑起来不难,难得是上线后那些预想不到的幺蛾子。我把六个最典型的问题按当时的排查思路完整记录下来,每个都是真实排错过程的浓缩。

6.1 多行异常栈被拆成碎片

现象:Kibana里搜ERROR,能看到很多以java.lang.NullPointerException开头的日志,但没有堆栈详情,或者堆栈的每一行都被显示成独立日志。

排查过程:先确认Filebeat采集到的原始内容和应用日志文件里的内容是否一致。我在Kibana里对比了一条异常日志的完整内容,发现应用日志文件里是一整段多行文本,但ES里被拆成了多条。这基本可以断定是采集端没有做多行合并。

解决方案:在Filebeat的filestream input中配置multiline parser,注意正则必须精确匹配日志行开头的时间戳格式。我们应用的日志是以2024-06-01开头的,所以用了pattern: '^[0-9]{4}-[0-9]{2}-[0-9]{2}'。如果时间格式不是这样,比如带T的ISO格式,就要改成对应的正则。

6.2 日志时间“穿越”一小时

现象:QPS统计图里0点到1点的日志量明显偏低,而1点到2点明显偏高。看起来就像是日志全部延迟了一小时。

排查过程:先看Filebeat采集的日志,原始日志时间戳是正常的北京时间,但ES里@timestamp字段比日志里的时间早8小时。原因很经典:Java应用打印的时间是东八区时间,但ES默认按照UTC存储时间,日志字符串里没有带时区偏移,ES就把它当作UTC时间处理了。

解决方案:最稳妥的做法是统一在应用日志格式中输出带时区的ISO8601时间戳,例如2024-06-01T00:00:00.123+08:00,让ES正确解析时区。同时Kibana的时区设置改为UTC+8。如果历史数据已经错了,可以通过painless script批量修正@timestamp字段,但最好别走这条路,太费劲。

6.3 日志把业务磁盘写满,引发应用雪崩

现象:某天凌晨部分节点的应用响应突然变慢,随后告警显示根分区使用率超过90%,应用的服务进程开始频繁报磁盘相关错误。

排查过程:看日志目录,发现应用自身的log文件已经膨胀到几十GB,正常logrotate配置没有起作用。进一步看Filebeat采集速度,发现消费速度低于日志生产速度,导致日志留在本地没有及时发出去。

解决方案:两层处理。一是应用层,修正logrotate配置,限制单文件大小和保留份数,比如单文件200MB,保留5份;二是采集层,检查Filebeat是否配置了正确的路径,确保它能及时消费日志文件,同时通过监控Filebeat的read/offset指标判断采集是否滞后。这是两个问题的叠加,只处理任何一个都不彻底。

6.4 ES查询越来越慢

现象:上线初期查询很快,两周后Kibana里的搜索开始卡顿,尤其是带聚合的图表明细要好几秒才能转出来。

排查过程:查看ES的segment数量,索引频繁写入后段数量暴涨,单个分片的segment数达到几千。查询时每个segment都要参与,整体开销自然变大。再看分片分布,有些节点的分片明显多于其他节点,热点节点负载很高。

解决方案:ILM策略中加入forcemerge步骤,把旧索引的segment合并到1个,查询性能显著提升。同时重新规划了分片分配,给不同节点设置不同的分片分配权重,让分片更均衡。这里要提醒一句:forcemerge只对只读索引有意义,不要在还在写入的hot索引上执行,否则会造成大量IO开销。

6.5 Kafka消费组卡死,日志延迟越来越大

现象:某次发版后,日志从生产到Kibana可见的延迟从几秒涨到了半小时。看Kafka的consumer lag直线上升,消费组怎么也追不上。

排查过程:先看消费程序的日志,发现大量重复rebalance日志,每个consumer实例都在不停地加入退出消费组。进一步分析是消费线程阻塞导致的,一个消费者线程在ES bulk写入时长时间没有返回,Session超时,触发了rebalance,而每次rebalance都会让其他消费线程暂停,形成恶性循环。

解决方案:把消费端的ES写入改成并发批量模式,多个worker线程并行消费不同分区,同时调大了Kafka session.timeout.ms和max.poll.interval.ms的配置,给单次处理留出合理余量。改完之后consumer lag稳定归零,日志延迟恢复到了秒级。

6.6 敏感信息裸奔

现象:客户投诉后安全团队排查,发现日志里出现了用户的手机号和身份证号。

排查过程:查ES数据,确实在订单服务的日志里找到了大量明文手机号。原因是开发在打印日志时把整个用户对象toString()输出了,里面的手机号、身份证号没有做脱敏。

解决方案:一方面紧急清理ES中的相关数据,另一方面在采集端增加脱敏处理。我使用的处理是在Logstash中转时通过gsub正则把手机号中间四位替换为****,身份证号第7到14位替换为********。更彻底的做法是在应用侧改成结构化日志并严格约束日志规范,从源头不打印敏感字段。

7. 日志系统自身的可观测性,以及下一步怎么走

日志系统上线之后,它自己也是需要被监控的。我见过不少日志系统本身挂了,团队还不知情,直到故障排查时才发现日志早就断了,那就非常尴尬了。

7.1 给日志系统做健康检查

我在日志系统上挂了四类监控指标:

监控对象关键指标告警阈值
Filebeat采集事件数、队列积压数、输出失败数输出失败数 > 0 持续5分钟
Kafkaconsumer lag、broker磁盘使用率、分区ISRconsumer lag > 10000
Elasticsearch集群状态、节点堆内存、segment数量、写入拒绝数集群非green,堆内存 > 75%
端到端日志从采集到可搜索的延迟延迟 > 2分钟

这些指标通过Prometheus采集,Grafana展示,配合Alertmanager做告警。其中我特别在意日志端到端延迟,这个指标直接反映系统整体是否健康,也是最容易感知的“日志还活着吗”的信号。实现方法是消费端处理每批消息时记录当前时间与Kafka消息中自带的生产时间的时间差,作为end_to_end_latency指标暴露给Prometheus。

7.2 trace_id是通往链路追踪的桥梁

日志系统做到这个程度,已经能解决大部分线上排障需求了。但如果你的服务规模继续增长,或者微服务拆分越来越细,你会发现日志里的trace_id只是“半链路追踪”——它能告诉你每台服务的日志,但没法精确还原每个调用的耗时分布和依赖关系。

这时候就该考虑上真正的分布式链路追踪系统,比如SkyWalking、Jaeger或Zipkin。好消息是,你已经在日志里埋好了trace_id,链路追踪系统用的trace_id和日志里的trace_id一旦统一,两者就能互相印证:链路追踪里看到某个接口耗时异常,立刻跳转到对应日志查看业务报错详情。这个结合点是可观测性建设的关键一步。

从个人实际操作体验来说,我最大的体会是:日志系统这种基础设施,最重要的不是一开始就规划得多么大而全,而是把采集、缓冲、存储、查询这条主干链路跑通,然后在真实排障中逐渐丰富细节。如果你正准备做或者正在做分布式日志系统,先把这条主干链路和监控指标搭好,再根据线上问题逐步完善,会比一上来就追求全链路追踪、冷热分离这些高阶能力要踏实得多。最后分享一个小技巧:上线前模拟一次ES宕机,看看日志是否能在Kafka里完整保留,这个演练比任何纸上谈兵的架构评审都管用。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询