日志平台这件事,我踩过的坑比大多数人走过的路都多。最早用一台机器跑全套ELK,日志量一上来就卡得连SSH都连不上;后来拆成分布式,又因为Kafka和ZooKeeper的版本对不上,折腾了整整两天。所以当我看到"ELK+Filebeat+Kafka+ZooKeeper"这套组合时,第一反应不是它有多先进,而是——这套架构里每一个组件的位置都是有讲究的,少一个环节或者顺序放错,整个链路就会在某个深夜给你脸色看。
这套平台解决的核心问题其实很朴素:当你有几十台甚至上百台机器,每台机器上跑着不同的服务,日志散落在各个角落,出问题时你得一台台登上去tail -f,这种日子过不下去的时候,就需要一个能把日志集中收上来、缓冲住、存下来、还能快速检索和可视化的系统。ELK负责"存和看",Filebeat负责"采",Kafka负责"缓冲和解耦",ZooKeeper负责"协调"。适合谁看?我觉得是那些已经过了单机玩具阶段、开始认真对待日志的团队,以及想搞清楚这套经典架构每个环节为什么这么设计的工程师。
下面我按实际搭建的顺序,把每个环节的选择理由、配置细节和踩坑经验摊开讲。
1. 先把数据流想清楚,再动手装软件
很多人一上来就apt install或者docker run,装完发现数据不通,然后开始瞎猜。我建议动手之前先在纸上画一遍数据流向,因为这套架构的链路比看起来长,任何一个环节的配置错误都会表现为"日志没出来",但根因可能在天涯海角。
1.1 一条日志从产生到可检索的完整旅程
一条日志的完整路径是这样的:应用程序把日志写到本地文件 → Filebeat监控文件变化,读取新增内容 → Filebeat把日志推送到Kafka的某个Topic → Logstash从Kafka消费 → Logstash做解析、过滤、字段加工 → Logstash写入Elasticsearch → Kibana从Elasticsearch查询并展示。
这条链路里,每个箭头都是一次网络传输和一次格式转换。我见过最常见的问题是把Filebeat直接怼到Logstash、跳过Kafka,小规模确实能跑,但一旦Logstash重启或者Elasticsearch抖动,Filebeat就会因为背压而停止读取,日志在本地堆积,严重的会导致应用写日志阻塞。Kafka在这里的角色就是"蓄水池"——上游尽管灌,下游按自己的节奏喝,中间水位高一点低一点不影响两边。
ZooKeeper不在这条数据主链路上,它是Kafka的"幕后管家"。Kafka用ZooKeeper来管理集群元数据、Controller选举、Topic的Partition分布和消费者偏移量(老版本)。虽然新版Kafka在逐步去ZooKeeper化,但大量生产环境仍然依赖它,所以这套组合里它不可或缺。
1.2 为什么不是Filebeat直连ES,也不是Logstash直连Filebeat
有人会问:Filebeat不是能直接输出到Elasticsearch吗?确实可以,而且配置极简。但那种模式适合"日志量不大、格式规整、不需要复杂加工"的场景。一旦你需要做grok解析、字段脱敏、多行日志合并、根据内容路由到不同索引,Filebeat就力不从心了——它的定位是轻量采集器,不做重加工。
那Filebeat直连Logstash呢?这是很常见的简化方案,少一层Kafka。问题在于Logstash是JVM应用,重启、升级、调参都会造成短暂不可用,这期间Filebeat会重试,但如果停机时间长了,Filebeat的队列满了就开始丢数据或者阻塞。加了Kafka之后,Filebeat只管往Kafka写,Logstash挂了也不影响采集端,恢复后从上次offset继续消费,数据不丢。这个"解耦"的价值在稳定运行几个月后你会体会得特别深。
1.3 组件版本搭配的隐性坑
版本这件事我必须单独拎出来说。Elasticsearch、Logstash、Kibana、Filebeat最好保持同一个大版本,比如统一7.17.x或者统一8.x。跨大版本混搭经常出现协议不兼容,尤其是8.x默认开启了安全认证(TLS和账号密码),如果你Filebeat是7.x而ES是8.x,连接会直接失败,报错信息还不一定直白。
Kafka和ZooKeeper的版本对应关系也要注意。Kafka 2.x时代和ZooKeeper 3.4/3.5的搭配有明确的兼容矩阵,Kafka 3.x虽然还支持ZooKeeper模式但推荐KRaft。我这套方案按经典的ZooKeeper模式来讲,因为存量环境多。选版本时我的习惯是:先确定Elasticsearch版本(因为它对JDK和系统要求最挑),然后其余组件围绕它选兼容版本,Kafka选一个稳定的3.x但用ZooKeeper模式,ZooKeeper选3.8.x。
提示:不要追求所有组件都用"最新版"。日志平台是基础设施,稳定压倒一切,选一个社区验证充分的版本组合,比追新重要得多。
2. 基础环境与ZooKeeper、Kafka的落地
环境准备这块,我按"先底层后上层"的顺序来。ZooKeeper和Kafka是底座,先把它们跑稳,后面的ELK才有意义。
2.1 系统层面的统一约定
所有节点我建议做几件统一的事:关闭swap(Elasticsearch强烈要求,Kafka也受益)、调大文件句柄数、统一时间同步、配置好主机名解析。文件句柄数用ulimit -n 65536临时生效,永久生效改/etc/security/limits.conf。时间同步尤其关键,因为Kafka的消息时间戳、Elasticsearch的索引时间、日志本身的时间如果对不齐,排查问题时你会怀疑人生。
内存分配上要有个全局规划。假设你有三台机器做集群,每台32G内存,我的分配习惯是:ZooKeeper给2G堆,Kafka给6G堆(它大量用页缓存,堆不用太大),Elasticsearch给16G堆(但不超过物理内存一半且不超过32G),Logstash给4G堆,剩下的留给系统和页缓存。这个分配不是死的,但原则是别让JVM堆把物理内存吃光,尤其是Elasticsearch,堆太大反而GC停顿长。
2.2 ZooKeeper的配置要点
ZooKeeper的配置文件zoo.cfg里,几个参数值得说道。tickTime默认2000毫秒,是ZK的基本时间单位;initLimit是Follower连接Leader的初始化超时(以tick为单位),syncLimit是Follower和Leader之间心跳超时。集群模式下每个节点要有唯一的myid文件,内容就是1、2、3这样的数字,放在dataDir下。
# zoo.cfg 核心片段 tickTime=2000 initLimit=10 syncLimit=5 dataDir=/data/zookeeper/data clientPort=2181 server.1=zk1:2888:3888 server.2=zk2:2888:3888 server.3=zk3:2888:3888server.X里的两个端口,2888是Follower和Leader同步数据用的,3888是选举用的。这两个端口别搞混,防火墙要放行。我踩过的坑是只开了2181客户端端口,结果集群起不来,日志里一堆"cannot connect to leader",查了半天才发现是2888/3888没通。
2.3 Kafka的关键参数与Topic设计
Kafka的server.properties里,broker.id每个节点唯一,zookeeper.connect指向ZK集群(多个用逗号分隔),log.dirs指定数据目录(建议独立磁盘)。num.partitions是默认分区数,我一般设成和消费者线程数匹配,比如3或6。default.replication.factor生产环境至少2,三节点集群设3最稳。
Topic的设计是个经验活。我建议按"日志来源+环境"来划分,比如app-logs-prod、app-logs-test,而不是所有日志塞一个Topic。分区数决定了并行消费的上限,Logstash的消费线程数不能超过分区数,否则多出来的线程闲着。假设你预估日志峰值每秒5万条,单分区大概能扛几千到上万条(取决于消息大小),那分区数给6到12比较合理。
# 创建Topic示例 kafka-topics.sh --create \ --bootstrap-server kafka1:9092 \ --topic app-logs-prod \ --partitions 6 \ --replication-factor 3创建完用--describe确认一下Leader和ISR分布是否均匀。如果所有Partition的Leader都在同一个broker上,说明分配不均,虽然Kafka会自动再平衡,但初始就不均往往意味着哪里配置有问题。
注意:Kafka的
log.retention.hours默认168小时(7天),意味着日志在Kafka里最多留7天。这个值要根据你的下游消费速度和磁盘容量权衡,别设太大把磁盘撑爆,也别设太小导致下游故障时数据被提前清掉。
3. Filebeat采集端的精细化配置
Filebeat看着简单,配置项不多,但真正决定采集质量的是那些细节:多行日志怎么合并、字段怎么加、什么时候用close_inactive、背压怎么处理。这一层配不好,后面Logstash解析再牛也救不回来。
3.1 输入配置与多行日志合并
Filebeat的filebeat.inputs里,paths指定日志文件路径,支持通配符。fields可以给这批日志打上自定义标签,比如log_type: nginx,后面Logstash根据这个字段决定用哪套解析规则。multiline是多行日志的关键——Java应用的异常堆栈、Python的Traceback都是多行的,如果不合并,每一行都会变成独立事件,检索时惨不忍睹。
filebeat.inputs: - type: filestream id: app-logs paths: - /var/log/myapp/*.log fields: log_type: myapp env: prod multiline: type: pattern pattern: '^\d{4}-\d{2}-\d{2}' negate: true match: after这段配置的意思是:不以日期开头的行,都合并到上一行后面。negate: true配合match: after是处理"续行"的标准写法。这里有个坑:pattern写错会导致要么全合并成一条,要么完全不合并。我的建议是拿一段真实日志用filebeat test config和实际跑一遍验证,别凭感觉。
3.2 输出到Kafka的可靠性设置
Filebeat输出到Kafka的配置里,hosts指向broker列表,topic指定目标Topic。required_acks建议设成-1(即all),表示所有ISR副本都确认才算成功,牺牲一点吞吐换可靠性。compression开gzip能显著减少网络传输量,日志文本压缩比很高。max_message_bytes要和Kafka的message.max.bytes对齐,否则大消息会被拒。
output.kafka: hosts: ["kafka1:9092", "kafka2:9092", "kafka3:9092"] topic: "app-logs-prod" required_acks: -1 compression: gzip max_message_bytes: 1000000还有一个容易被忽略的点:Filebeat的queue.mem(内存队列)大小。默认事件数不多,日志高峰期如果输出端慢,队列满了Filebeat会停止读取文件,这是背压保护机制。适当调大queue.mem.events能缓冲更多,但别调太大把内存吃光。我一般设成4096到8192之间。
3.3 采集端的资源与权限问题
Filebeat以什么身份运行、能不能读到日志文件,这是部署时第一个要确认的。如果应用日志是root创建的、权限600,Filebeat用普通用户跑就读不到,日志里会报permission denied。解决办法要么调整文件权限,要么让Filebeat以合适身份运行,但别图省事直接给root,安全上不合适。
另外Filebeat会记录每个文件的读取位置(registry文件),这个文件别随便删,删了会导致重复采集或者从头采集。升级Filebeat时registry的格式可能变化,跨大版本升级前先备份。
4. Logstash的解析管道设计
Logstash是整个链路里最"重"也最灵活的一环。它的配置文件分三部分:input、filter、output。input从Kafka拉数据,filter做解析加工,output写Elasticsearch。这一层设计得好,后面Kibana里查起来行云流水;设计得差,索引里全是message字段一坨文本,等于白搭。
4.1 从Kafka消费的input配置
input { kafka { bootstrap_servers => "kafka1:9092,kafka2:9092,kafka3:9092" topics => ["app-logs-prod"] group_id => "logstash-consumer-group" consumer_threads => 6 codec => json decorate_events => true } }consumer_threads要和Topic分区数匹配,6个分区就6个线程,多了浪费少了消费慢。group_id很重要,多个Logstash实例用同一个group_id就自动分摊分区,实现水平扩展。codec => json是因为Filebeat输出到Kafka默认是JSON格式,直接解析省事。decorate_events => true会把Kafka的元数据(Topic、分区、offset)加到事件里,排查问题时很有用。
4.2 filter里的grok解析与字段治理
grok是Logstash的看家本领,用正则把非结构化日志变成结构化字段。比如一条Nginx访问日志,用grok可以拆出client_ip、method、request、status、bytes等字段。grok的模式库很丰富,%{IP}、%{NUMBER}、%{WORD}这些预定义模式能覆盖大部分场景。
filter { if [fields][log_type] == "nginx" { grok { match => { "message" => "%{IPORHOST:client_ip} - - \[%{HTTPDATE:timestamp}\] \"%{WORD:method} %{URIPATHPARAM:request} HTTP/%{NUMBER:http_version}\" %{NUMBER:status:int} %{NUMBER:bytes:int}" } } date { match => ["timestamp", "dd/MMM/yyyy:HH:mm:ss Z"] target => "@timestamp" } } }这里有个性能经验:grok正则写得越宽松,匹配越慢。能用%{NUMBER}就别自己写[0-9]+,预定义模式经过优化。另外date过滤器把日志里的时间替换成@timestamp,这一步很关键,否则Kibana里按时间排序全是采集时间而不是日志产生时间,排查问题时时间线是乱的。
字段治理上,我建议遵循"少即是多":只保留真正要检索和聚合的字段,冗余字段既占存储又拖慢索引。remove_field可以删掉解析后不需要的中间字段,mutate可以重命名、转换类型。
4.3 output到Elasticsearch的索引策略
output { elasticsearch { hosts => ["http://es1:9200", "http://es2:9200"] index => "app-logs-%{+YYYY.MM.dd}" user => "elastic" password => "******" } }按天分索引是标准做法,方便按时间范围查询和定期清理旧索引。索引名用%{+YYYY.MM.dd}这种日期格式,Logstash会自动替换。如果日志量特别大,可以按小时分,但索引数量会暴涨,要配合ILM(索引生命周期管理)来滚动和删除。
写ES时flush_size和idle_flush_time影响批量写入的效率,默认值一般够用,但高吞吐场景可以调大flush_size减少请求次数。另外建议开启ES的refresh_interval调整,写入频繁时把refresh间隔从1秒调到30秒,能显著降低索引压力,代价是数据可见性延迟,日志场景一般能接受。
5. Elasticsearch与Kibana的存储和展示
到了存储和展示层,重点从"数据怎么进来"变成"数据怎么存得下、查得快、看得清"。Elasticsearch的索引设计、分片规划、Kibana的索引模式和可视化,决定了这套平台最终好不好用。
5.1 索引模板与分片规划
ES 7.x以后推荐用索引模板(index template)来统一管理索引的settings和mappings,而不是每个索引单独配。模板里可以指定主分片数、副本数、字段类型、生命周期策略。分片数是个需要提前想清楚的事:分片太多,集群管理开销大;分片太少,单分片数据量过大影响查询和恢复。经验值是单个分片控制在10G到50G之间。
假设每天日志量50G,保留30天,那总数据量1.5T。如果每个索引(每天一个)设3个主分片1个副本,单个分片约16G,在合理区间。副本数至少1保证高可用,三节点集群可以设1,节点多了可以适当增加。
字段类型上,message字段如果全文检索需求不大,可以设成text但关闭fielddata,或者干脆用keyword只做精确匹配。日期字段一定要映射成date类型,否则范围查询会走字符串比较,慢且不准。
5.2 Kibana的索引模式与可视化搭建
Kibana里第一步是创建索引模式(index pattern),匹配app-logs-*这样的通配符,然后指定时间字段为@timestamp。索引模式建好后,Discover页面就能按时间浏览日志了。
可视化方面,我常用的几个:按日志级别(ERROR/WARN/INFO)的饼图看错误占比,按时间的折线图看日志量趋势(突增往往意味着故障),按client_ip的柱状图看访问来源Top N,还有用表格展示最近的ERROR日志。这些在Kibana的Visualize里拖拖拽拽就能做,关键是字段要先在Logstash里解析好。
Dashboard可以把多个可视化拼在一起,做成一个"日志总览"面板。我习惯在面板顶部放一个时间选择器和一个关键词搜索框,运维同事打开就能快速定位。
5.3 集群健康与容量监控
ES集群的健康状态分green、yellow、red三级。green表示所有主分片和副本都正常,yellow表示主分片正常但副本未分配(单节点集群常见),red表示有主分片未分配,数据可能丢失。日常要盯的是yellow和red,用GET _cluster/health查看。
容量监控上,磁盘使用率超过85%就要警惕,ES会自动把分片迁走甚至只读。用GET _cat/allocation?v看各节点磁盘占用,GET _cat/indices?v看各索引大小和文档数。配合ILM策略自动删除过期索引,避免手动清理的麻烦。
提示:ES的堆内存不要超过32G,这是JVM压缩指针的边界,超过后指针膨胀反而降低性能。物理内存大的机器,剩下的内存留给文件系统缓存,ES查询大量依赖页缓存。
6. 联调排错:日志不出现的排查链路
这套架构环节多,出问题时"日志没出来"是唯一症状,但根因可能在任意一层。我把自己常用的排查顺序整理出来,按这个链路走,基本能定位到问题所在。
6.1 从采集端到Kafka的逐段验证
第一步确认Filebeat有没有在采。看Filebeat日志(默认在/var/log/filebeat/),有没有Harvester started之类的记录,有没有报错。用filebeat test output测试到Kafka的连通性。第二步确认Kafka里有没有数据,用kafka-console-consumer.sh --bootstrap-server kafka1:9092 --topic app-logs-prod --from-beginning --max-messages 5消费几条看看。如果Filebeat说发了但Kafka里没有,多半是Topic名写错、required_acks配置导致写入失败、或者网络不通。
第三步确认Logstash有没有在消费。看Logstash日志里有没有Kafka consumer相关的错误,用kafka-consumer-groups.sh --describe看消费组的lag,lag一直涨说明消费跟不上或者消费失败。第四步确认ES里有没有写入,用GET app-logs-*/_count看文档数,或者直接在Kibana里查。
6.2 常见故障的根因对照
| 症状 | 可能根因 | 排查动作 |
|---|---|---|
| Filebeat日志报连接Kafka失败 | broker地址错、防火墙、Kafka未启动 | telnet kafka1 9092测试连通 |
| Kafka有数据但Logstash不消费 | group_id冲突、Topic不匹配、消费线程数超分区 | 查consumer group lag和Logstash日志 |
| Logstash消费了但ES无数据 | grok解析失败、ES认证失败、索引名非法 | 看Logstash日志的_grokparsefailure和ES报错 |
| Kibana查不到数据 | 索引模式时间字段错、时间范围选错 | 检查index pattern的@timestamp映射 |
| 日志时间线错乱 | 未用date过滤器替换@timestamp | 检查filter里的date配置 |
这张表是我这些年遇到问题后总结的,实际排查时按"数据流方向"逐段验证,比东一榔头西一棒子高效得多。特别提醒grok解析失败的情况:Logstash默认会给解析失败的事件打上_grokparsefailure标签,但事件还是会进ES,只是字段没解析出来。所以看到"有数据但字段是空的",先查grok。
6.3 性能瓶颈的定位思路
平台跑起来之后,性能问题会慢慢浮现。常见瓶颈有三个:Kafka消费lag持续增长(消费能力不足,加Logstash实例或线程)、ES写入变慢(分片过多、refresh太频繁、磁盘IO瓶颈)、Kibana查询超时(查询范围太大、字段未优化、分片过多)。
定位性能问题我习惯先看监控指标:Kafka的lag、ES的indexing rate和query latency、各节点的CPU和IO。有一次我们平台查询越来越慢,最后发现是某个索引的分片数设了10个但数据量才2G,大量小分片拖累了集群。把分片数调小、合并旧索引后恢复正常。这种问题不看指标光看日志是发现不了的。
7. 我在这套架构上踩过的几个真实教训
讲完配置和排错,分享几个只有实际跑过才会知道的教训,这些在官方文档里基本找不到。
第一个是关于Filebeat的registry文件。有次一台机器磁盘满了,清理时顺手把Filebeat的data目录删了,结果重启后Filebeat把所有历史日志从头又采了一遍,Kafka里瞬间涌入大量重复数据,ES索引暴涨。后来我养成了习惯:清理磁盘前先确认Filebeat的registry位置,绝不误删,并且给registry目录单独做备份。
第二个是Kafka的Topic自动创建。Kafka默认auto.create.topics.enable=true,Filebeat往一个不存在的Topic写时会自动创建,但用的是默认分区数和副本数,往往不符合预期。有次一个新服务上线,Topic被自动创建成1分区1副本,消费严重不足。后来我把自动创建关掉,所有Topic手动按规范创建,虽然多一步,但避免了隐性坑。
第三个是Logstash的JVM堆和管道worker。Logstash默认worker数等于CPU核数,但每个worker都会占用堆内存,机器核多的时候堆容易被撑爆导致频繁GC。我一般显式设置pipeline.workers为分区数或CPU核数的较小值,同时把pipeline.batch.size调到一个平衡点,批次太小吞吐低,太大延迟高且内存压力大。
第四个是时间字段的时区。日志里的时间和ES的@timestamp如果时区不一致,Kibana里看到的时间会偏移几个小时。date过滤器要正确指定日志时间的时区格式,比如yyyy-MM-dd HH:mm:ss配合timezone => "Asia/Shanghai"。这个坑不显眼,但排查跨时区问题时能把人逼疯。
这套架构不新,甚至有人觉得"老气",但它的每个组件都在解决一个具体问题,组合起来是一套经过大量生产环境验证的可靠方案。真正决定它好不好用的,不是组件本身,而是你对数据流的理解、对每个配置项背后意图的把握,以及那些踩过坑之后积累下来的细节经验。把这些细节抠到位,它就能安安稳稳地帮你把散落各处的日志收拢成一份可查、可看、可分析的数据资产。