☰
分布式日志系统搭建实战:从Filebeat采集到Kafka与Elasticsearch检索
2026/10/17 4:17:47 网站建设 项目流程

日志这个东西,平时没人拿它当回事,一出问题它就是唯一的线索。年初我们做某个多节点服务的时候,排查问题越来越吃力:十几台机器,出问题要先登录服务器,一条一条 grep 翻,时间还要对来对去,经常对不上,整个人都快麻了。后来我花了大概两周时间,把一套分布式日志系统搭了起来:采集端用轻量级 Agent 方式,消息中转用 Kafka,存储检索用 Elasticsearch,前端用 Kibana 做可视化,后面还接了解析清洗和告警规则。整套流程跑下来,最直接的收益是——排查问题的时间从按小时算,降到了按分钟算。这篇就把整个实现过程、选型原因、关键配置和踩坑记录整理出来,希望能给同样被日志折磨的人一些参考。

1. 需求拆解:单机日志到底“死”在哪

1.1 单机模式解决不了的三个问题

在聊方案之前,先把问题的源头说清楚。单机部署模式下,日志是每个进程自己往磁盘文件里写,依靠 logrotate 或类似工具做轮转。这个模式在节点少、流量低的时候完全没问题,最多就是磁盘满的时候来个人删一下文件。但规模一旦上来,三个问题就会非常突出。

第一个是查询链路断裂。一个用户请求往往要经过网关、业务服务、缓存、数据库多个环节,每个环节打的日志散落在各自的机器上。出问题时,你得先知道这个请求经过了哪些节点,再到每台机器上去找对应时间段的日志,手工把链路拼起来。运气好几分钟,运气不好几个小时都拼不出来。

第二个是时间对齐困难。机器之间可能存在时钟偏差,有的快几十毫秒,有的慢几百毫秒。单机模式下你看到的是各机器本地的时间,一旦要把多台日志按时间排序,偏差会让整个过程错乱。这个坑很隐蔽,后期排查起来非常恶心。

第三个是被动式排障。没有集中检索能力,你就只能等用户报障,再上去翻日志。想在问题发生之前主动发现异常,比如某个接口的错误率突然上升、某个节点的内存持续增长,基本做不到。日志只能作为事后追溯的工具,价值打了很大折扣。

这几个痛点叠加起来,就不是“扛一扛”能解决的了。我当时的判断是:继续用单机模式,团队超过五个人、机器超过五台之后,时间成本会呈指数级上升,必须把日志当成一个基础设施来建设。

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

分布式日志系统本质上就做四件事:采集、传输、存储、检索。它把散落在各个节点上的日志,通过统一的管道汇聚到集中的地方,清洗后建立索引,提供全文检索和聚合分析。你可以把它理解成把所有机器的“运行日记”集中到一座有索引、有目录的档案库里,而不是让每个人各自保管一个小纸片。

这个系统解决的核心问题是“可观测性”。线上出现故障时,你能在几秒钟之内把相关联的日志捞出来,按时间线还原现场;流量高峰期时,你能通过实时查询观察到系统行为的变化;安全问题时,你手上有一套完整的审计记录,知道谁在什么时候访问了什么资源。

适合谁来参考呢?我个人的建议是:只要你的服务部署在三台机器以上,或者你的团队已经受不了“登录服务器翻日志”这种模式,就可以考虑搭建。如果你的节点很少、日志量很小,那暂时没有必要搞这么重,一个 rsyslog 集中收集可能就够用了。

1.3 先给方案划定边界

做分布式日志系统之前,有一点要想清楚:不是所有日志都值得收集。业务日志、错误日志、访问日志、安全审计日志,这些是核心。但像 Debug 级别的临时输出、某个模块打出来的无意义心跳日志,收进来只会浪费磁盘和带宽。

我当时的做法是,先把日志分级:

  • ERROR / WARN 级:必须全量采集,实时追踪。
  • INFO 级:选择性采集,保留关键节点和关键路径。
  • DEBUG / TRACE 级:默认丢弃,排查特定问题时再临时开启。

边界划清楚,后面做容量规划和架构设计的时候,才不会一上来就被数据量吓住。

2. 组件选型:四条链路怎么组合最稳

2.1 采集层:为什么我选了轻量 Agent 而不是重量级管道

采集层是离业务进程最近的一环,选型最重要的标准不是功能多,而是“别打扰业务”。当时摆在面前的无非两个方向:用轻量级的 Filebeat 这类专用 Agent,或者用 Logstash 这种通用处理管道做采集。两者功能上有重叠,但定位完全不同。

Filebeat 这类 Agent 的优势是内存占用低,实测下来单实例大概几十 MB 到一两百 MB,CPU 占用也很少,适合部署在每一台业务机器上。它的日志读取机制也成熟:支持多行合并、自动记录读取位置、异常退出后可以从断点续传,这些恰恰是日志采集最核心的需求。

Logstash 功能更全,它可以处理复杂的解析逻辑、可以做丰富的过滤,但 Java 虚拟机本身的资源开销不小,一台机器上再跑一个 Logstash,对小规格实例来说就是额外的负担。更合理的组合方式是:采集用 Filebeat,集中解析和清洗用 Logstash 或相似工具,放到 Kafka 下游,这样谁的角色清晰,谁的资源开销都可控。

我后来在实际部署中总结出一个简单的直觉:采集器占用的额外资源,不能超过业务进程的 5%。超过这个比例,采集本身会给服务稳定性带来压力,得不偿失。

2.2 缓冲层:Kafka 为什么是日志系统的“蓄水池”

日志数据的特点是“峰值明显、突发量大”。某段时间流量集中上来,每秒钟产生的日志可能是平时的十倍甚至几十倍。采集层可以做到一边发送,但如果后端的存储集群写入能力跟不上,就会形成堵塞。这个时候中间就要有一个缓冲层,把洪峰扛住,让消费方按自己的节奏去处理。

Kafka 就是这个角色。它的设计目标就是高吞吐、分布式、持久化、可回溯。日志写进 Kafka 之后,不是马上被丢弃,而是保留一段时间,这样消费者挂了重启,还能从之前的位置继续消费,不会丢数据。

Kafka 里最关键的是分区机制。一个 Topic 分成多个 Partition,每个 Partition 内部是有序的,写入的时候按 key 哈希或者轮询分配到不同分区。这里有个非常重要的选择逻辑:日志场景一般不需要全局有序,只需要按某个维度(比如服务名、节点 ID)局部有序,所以可以根据业务拆分 Topic,而不是强求全局顺序。分区数也不是越多越好,分区越多,文件句柄和内存开销越大,消费者线程数和分区数还要匹配得上。

在日志链路里,我给 Kafka 的定位很朴素:它就是一个巨大的、可靠的缓冲池。存储时效并不需要太长,能保留三到七天就足够应对绝大多数回溯场景。

2.3 存储检索层:Elasticsearch 怎么扛住全文检索

日志系统最终要把数据存下来,并且要支持按关键字、按时间、按级别等条件快速搜索。普通的 MySQL 在这种场景下效率很差,因为日志大多是非结构化文本,用数据库里的 LIKE 查询,数据量一上去基本就是全表扫描,慢到没法用。

Elasticsearch 能解决这个问题的核心是倒排索引。你可以把倒排索引理解为书后面的关键词索引页:它记录的是“这个词出现在哪些文档里”,而不是扫描每一页去找词。搜索一个关键词时,ES 直接查索引,就能快速返回所有包含这个词的文档。这套机制做全文检索,性能和扩展性都经过了大量生产环境的验证。

ES 里的数据是按照索引(Index)组织的,每个索引又分成多个分片(Shard),分片分散到不同的节点上。写入和查询都可以并行处理,这也是它能横向扩展的根本原因。但分片的数量不是拍脑袋定的,我后面会专门讲容量规划。

日志场景还有一个特殊问题:日志数据是时间序列数据,越老的日志价值越低。ES 提供了索引生命周期管理(ILM)机制,可以按时间或大小自动把索引从热节点迁移到温节点、冷节点,最后自动删除。这样你完全不用人工清理旧的日志索引,系统自己就能保持磁盘的健康状态。

2.4 展示查询层:Kibana 和自建 API 怎么取舍

日志存进 ES 之后,要让人能用起来。最常见的界面是 Kibana,它直接对接 ES,可以画看板、做搜索、搞时间线对比,开箱即用。如果团队对可视化的要求不高,只是想在出问题时搜日志,Kibana 其实就够了,没必要花太多精力自己写界面。

但有一个场景需要考虑:如果日志系统要嵌入到公司的运维平台或者内部系统里,那就要做一层对 ES 的封装,提供统一的查询 API。这样做的好处是可以控制查询权限、统一鉴权、加缓存、做限流,不至于让所有人都直连 ES,把集群压垮。

我当时的方案是“两条腿走路”:Kibana 给研发排查问题用,自由度高、灵活;自建 API 给自动化系统和运营后台用,走固定的参数和格式,屏蔽了 ES 的复杂查询语句。两边的底层数据完全相同,只是入口不同、权限不同。这样既保证了灵活性,也控制了风险。

3. 实操搭建:从采集到检索的完整链路

3.1 采集端配置:Filebeat 输入到输出的完整例子

采集端是整套系统的起点,配置得好不好,直接影响日志的完整性和可用性。下面这个配置是我在生产环境里用过的缩略版本,基于 Filebeat 的 filestream 输入方式,已经可以覆盖大部分场景:

filebeat.inputs: - type: filestream id: app-log paths: - /data/logs/app/*.log 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"] topic: "app-logs" partition.hash: reachable_only: true required_acks: 1 compression: lz4 max_message_bytes: 1048576

几个关键点说一下。

filestream比旧的log输入方式更适合生产环境,它内部对文件标识、偏移量跟踪、重命名处理都更稳定。multiline这一段是很多人容易忽略的:如果业务进程输出的是 Java 异常堆栈,栈信息会有很多行不是以日期开头,这一段配置就是把这些行合并成一条完整日志。negate: true表示不符合日期格式的行也算匹配,match: after表示追加到前一条之后。

output 里我选择直接输出到 Kafka,而不是输出到 Logstash 再转发,原因是少一个中转节点,链路更短,故障点也少。压缩用 lz4,性价比很高,CPU 消耗低,压缩比对于纯文本日志来说也很可观。

3.2 Kafka Topic 设计与核心参数

Kafka 端的设计,直接影响整个链路的吞吐和容错。我的规划是:按业务域拆分 Topic,不要所有日志都进一个大 Topic,否则后续的消费、权限管理、数据保留策略都会很难做。

我当时的划分方式是域-环境-日志类型,比如order-prod-error、gateway-prod-access。这样每个 Topic 的消费方很明确,保留策略也可以单独设置。比如访问日志流量大,可以只保留三天;错误日志必须保留三十天以上。

Topic 的核心参数也有讲究。副本因子建议至少设为 2,允许一台 broker 挂掉不丢数据。分区数没有固定的计算公式,但有个经验依据:分区数不宜低于消费者的数量,否则会有消费者线程空转;同时单分区的吞吐能力按几 MB/s 估算,用总日志流量除以单分区吞吐,就可以得到一个下界。我这里习惯的做法是,分区数先按 broker 数量的倍数设置,后续流量增长了再通过扩容来调整。

生产者端要注意配置重试和幂等。Filebeat 自带的 Kafka 输出会自动处理部分重试,但如果用的是自研采集器,务必要开启enable.idempotence: true,避免网络抖动导致的消息重复。日志场景下重复几条问题不大,但没有幂等的话,排查问题时会看到很多假象。

3.3 Elasticsearch 索引模板与生命周期管理

ES 的配置重点不在集群搭建,而在索引管理和数据生命周期策略。日志数据是典型的时序数据,我每天按天生成新的索引,索引名包含日期,比如app-logs-2025.01.15。这样查询时可以利用索引名过滤,数据清理也非常方便。

通过模板(Index Template)来自动匹配这类索引,可以保证每天的索引配置一致。下面这个模板设置了分片数、副本数和基础字段的映射:

{ "index_patterns": ["app-logs-*"], "template": { "settings": { "number_of_shards": 3, "number_of_replicas": 1, "refresh_interval": "30s" }, "mappings": { "properties": { "@timestamp": { "type": "date" }, "level": { "type": "keyword" }, "host": { "type": "keyword" }, "message": { "type": "text" } } } } }

分片数设置成 3,是考虑到数据量、节点数和单分片大小综合之后的结果。ES 官方建议单分片数据量控制在 20-40GB 左右,用这个作为反向推算的依据:一天的数据量约 60GB,分 3 片,每片 20GB,正好落在合理区间。日志类索引的refresh_interval我不建议设成默认的 1s,因为日志场景对近实时性要求没那么高,30s 刷新一次可以大幅降低写入压力。

生命周期管理(ILM)是另一个必须配置的东西。我设置了四个阶段:hot 阶段超过 50GB 自动滚动;warm 阶段做段合并优化;cold 阶段数据降级存储;delete 阶段超过保留期自动删除。这套机制跑稳定之后,运维基本不需要手动关心历史索引。

3.4 查询与展示:一个搜索请求的完整链路

数据链路通了以后,再来看看查询链路。

假设我们要查最近 30 分钟某个服务的 ERROR 日志,用 ES 的查询 API 是这样的:

curl -X GET "http://es-host:9200/app-logs-*/_search" -H 'Content-Type: application/json' -d '{ "query": { "bool": { "filter": [ { "term": { "level": "ERROR" } }, { "range": { "@timestamp": { "gte": "now-30m" } } } ] } }, "sort": [ { "@timestamp": "desc" } ] }'

这里的逻辑很简单:先过滤级别和时间范围,再按时间倒序排序。term是精确匹配,对 keyword 类型的字段最合适;match才是全文检索,适合用在对message字段的模糊搜索上。

Kibana 侧的操作就更直观了,进去之后选索引模式,然后在搜索框里写level: ERROR和@timestamp范围就能看到结果。我比较习惯的做法是做一个固定的排查看板,默认展示最近一小时的 ERROR 分布、Top 10 错误关键字、慢查询日志条数这几个视图,出问题时先瞄一眼看板,再深入搜单个 trace ID,效率比直接盲搜高很多。

4. 真实踩坑:上线一周我遇到了什么问题

4.1 日志大面积丢失:问题出在采集端的内存缓冲

系统上线后,第一次真实的线上事故就给我上了一课。某天业务反馈说某些操作报错了,我按照往常一样去日志系统里查,结果发现关键时间段的日志缺失,而且不是零星的丢,是大面积缺了一段。

排查了好一阵才发现,是采集端默认的缓冲策略问题。从采集到 Kafka 之间有一段内存队列,队列满之后的新日志在默认策略下可能被直接丢弃。高峰期时,日志产生的速度超过了发送到 Kafka 的速度,队列不断堆积,触发丢弃策略之后,大量日志就悄悄没了。

处理方案有两层。第一层是调整采集端的 spool 大小和队列长度,让它在内存允许的范围内尽可能多地暂存数据;第二层是打开 Kafka 生产者的重试机制,配合required_acks保证消息写入成功之后再继续下一条。但最根本的,还是要保证下游的消费能力不能成为瓶颈,这条链的每一环都要定期压测,不能到了高峰期才暴露问题。

4.2 时间戳错乱:Kibana 里显示时间对不上

运行了一段时间之后,有人反馈在 Kibana 里看到的日志时间总是比服务器本地时间晚了八个小时。这不是服务器时钟问题,而是 Elasticsearch 默认以 UTC 存储时间,Kibana 展示时如果不设置正确的时区,就会显示 UTC 时间。

这个坑看起来小,但在排查跨时区或者对比业务日志时间时极其容易混淆。最终的解决办法是统一规范:所有日志在写入 ES 之前,时间字段统一转成 ISO 8601 标准的 UTC 字符串,ES 端按 date 类型存,Kibana 里显示时设置timezone为本地时区。关键是这个规则要在采集端就定好,后续所有字段解析都遵循同一个约定,而不是各写各的。

4.3 Java 异常堆栈被拆成了十几条日志

业务服务是 Java 写的,上线后发现一个很烦人的现象:某一条异常本来是一个堆栈,结果在日志系统里变成了十几条,每一条都是堆栈中单独的一行,根本没法看。

原因就是前面提过的 multiline 配置没生效。Java 异常堆栈的第一行通常包含异常类型和描述,后面跟着at com.xxx.ClassName.method(...)这样的调用栈行。默认情况下,采集器按行读取,每一行都当成新日志发出去。配置了 multiline 之后,才能把第一行和后续的堆栈行合并成一条完整的事件。

这个问题的排查思路很直接:先在采集端看原始日志文件,确认堆栈确实存在;再检查 multiline 的正则是否匹配到了正确行。我的教训是,上线前一定用真实业务日志做一遍回放测试,不要只拿几行测试文本验证。

4.4 ES 索引字段爆炸导致磁盘急升

日志量越来越大之后,又遇到一个新的问题:磁盘使用率突然飙升,索引创建速度也异常变慢。后来一查,罪魁祸首是 ES 默认的动态映射(dynamic mapping)。

业务日志里如果带了请求参数对象,字段名可能是动态拼接的,比如metadata.user_id、metadata.request_ip。每来一种新字段,ES 就自动创建对应的映射。几天下来,索引的字段数量从几十个涨到了几百上千个,每个文档都要维护庞大的字段结构,磁盘和内存开销急剧上升。

解决办法是禁用动态映射,在索引模板里显式声明允许收集的字段。可以用"dynamic": "false"来禁止自动新增字段,或者用"dynamic": "runtime"把未知字段转成运行时字段,不落盘但可查询。我推荐日志场景直接false,因为日志字段价值密度低,未知字段大多数没有分析必要,保留它们只会拖累性能。

4.5 Kafka 堆积导致消费延迟持续拉高

还有一次压测过程中,发现日志链路整体延迟从几秒慢慢涨到了几分钟,查了下监控,是 Kafka 的消费延迟持续拉高。

定位思路是这样的:如果生产者端没问题,那就要看消费者端是否处理不过来了。结果是下游清洗逻辑里有一步调用外部接口做 IP 归属地解析,外部服务偶尔超时,导致消费者线程被阻塞,消息堆积越来越多。

解决办法不复杂,但很直接:把外部调用从主流程中摘出去,解析失败的数据先落到另一个“慢通道”异步处理;同时调整消费线程数和批处理参数,让单位时间内消费的消息量明显提升。从这次之后,我把“任何外部依赖都不能阻塞日志主链路”写进了设计规范里。

5. 运维心得:容量评估与日常盯防要领

5.1 容量先算清楚再动手

搭完系统之后最怕什么?最怕磁盘满了都不知道,日志写不进去还毫无察觉。容量评估这件事,应该在搭建之前就做,而不是等出问题之后再来算。

我当时的估算方法是这样的。先统计现有的单机日志产生速度,比如某核心服务每秒产生 2000 条日志,平均每条 1KB,那就是 2MB/s。一台机器一天产生约 170GB 原始日志。如果这个量级不需要全量保留,按级别和字段裁剪之后,Kafka 端按 30% 的热点日志来设计,那三个节点的日存储量大约在 150GB 左右。再算上 Kafka 的副本(至少 2 份)和 ES 的副本(1 份),实际的磁盘占用预算要再乘两倍到三倍。

有一个经验数据可以参考:纯文本日志在 lz4 压缩下,体积大约能降到原来的 25%-35% 左右。但这只是传输过程中的压缩,ES 内部存储还有倒排索引的开销,实际占用往往比原始文本更大。所以我给自己留的余量是:预估存储量的 1.5 倍作为磁盘规划下限。

5.2 三个维度的监控一个都不能少

日志系统本身也是分布式系统,也要被监控。我的经验是至少要盯三个维度:

采集端要监控进程状态、采集速率、发送失败数。如果采集端自己挂了,日志链路从源头就断了,这比存储端出问题更隐蔽。

Kafka 端要监控消息堆积量,也就是消费者 Lag。Lag 不断增长说明消费速度跟不上生产速度,迟早会出问题。

ES 端要监控磁盘水位、分片状态、查询耗时。磁盘水位超过 85% 就必须预警,超过 90% 很多节点会自动拒绝写入,等写不进再去处理就非常被动了。

下面这张表是我整理的基础监控项,可以直接当参考:

维度监控项预警阈值
采集端Agent 存活 / 发送速率进程退出立即告警
Kafka消费者 Lag持续增长超过 10000 条
ES磁盘水位超过 85%
ES查询 P99 耗时超过 3 秒
链路全链路端到端延迟超过 5 分钟

5.3 日志链路后续还能怎么生长

这套系统跑稳定之后,你会发现它的价值不只是“查日志”。Kafka 里沉淀的数据可以做实时指标计算,比如错误率、请求量、成功率,这些指标完全可以替代一部分独立的监控系统。再往后,可以用它做业务分析、做用户行为审计。

我个人的建议是,先别想着一步到位,把基础的采集、传输、存储、检索做到稳定可靠,就已经解决了团队 80% 的排障痛点。再往上的实时计算、告警平台、审计分析,都是在扎实的地基上往上长。我自己做过一轮之后就一个感受:日志系统不是给人看的功能,是整个技术团队的“事后记录仪”和“事前侦察兵”,值得多花一点心思把它做扎实。

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

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

立即咨询