
日志平台这件事我踩过的坑比吃过的盐还多。早些年业务量小几台机器上tail -f加grep就能凑合后来服务拆得七零八落一天几百GB的日志往磁盘里灌出故障时运维几个人围着终端翻日志翻到眼睛发酸还定位不到根因。那时候我就下定决心得搭一套能扛住量、能快速检索、还能给后续告警和可视化留出接口的日志分析平台。ELK这套组合拳——Elasticsearch、Logstash、Kibana再加上Filebeat做采集、Kafka做缓冲、Zookeeper做协调——是我实测下来在中等规模场景里性价比最高、也最容易复现的方案。这篇文章我不打算写成官方文档的翻译而是把整套平台从设计思路到落地细节、从参数计算到排障经验按我实际搭建的顺序掰开揉碎讲清楚。适合谁看如果你手头有一批服务器、日志分散在各处、想搞一套集中式检索平台又不想一上来就上重型商业方案那这篇内容你应该能直接抄作业。下面所有涉及的主机名、IP、业务名称我都做了脱敏处理你替换成自己的环境即可。1. 整体架构设计与选型思路拆解1.1 为什么是这套组合而不是更简单的方案很多人第一反应是ELK三件套不就够了吗为什么还要塞进Kafka和Zookeeper这个问题我在方案评审时被问过不下五次。答案藏在“日志量”和“可靠性”这两个词里。最朴素的架构是Filebeat直接写Elasticsearch或者Filebeat写Logstash、Logstash再写Elasticsearch。小规模下完全没问题我早期就这么干过。但一旦日志峰值上来问题就暴露了Elasticsearch的写入是有瓶颈的批量索引、段合并、副本同步都会消耗资源当日志产生速度超过消费速度Filebeat端就会积压极端情况下丢数据而Logstash作为消费端如果下游ES抖动或者做一次索引模板变更Logstash会阻塞上游采集端跟着遭殃。Kafka在这里扮演的是“蓄水池”和“解耦器”的角色。Filebeat把日志丢进Kafka就完事不关心下游ES是死是活Logstash从Kafka慢慢消费消费速度可以独立调节。削峰填谷这件事Kafka天生擅长。我实测过一个场景业务做批量任务时日志量瞬间涨到平时的八倍没有Kafka时ES写入队列直接打满、采集端丢日志加了Kafka之后峰值被缓冲在topic里Logstash按自己的节奏消费一条没丢。那Zookeeper呢Kafka集群本身依赖Zookeeper做元数据管理、控制器选举、分区leader选举。虽然新版本Kafka在推KRaft模式想摆脱ZK但生产环境里ZKKafka的组合依然是最成熟、资料最全的搭配出问题好查。所以这套架构的完整链路是Filebeat采集 → Kafka缓冲 → Logstash解析过滤 → Elasticsearch存储索引 → Kibana展示Zookeeper在底层支撑Kafka集群。1.2 各组件职责边界与数据流走向把职责划清楚后面部署才不会乱。我习惯用一张职责表来对齐团队认知组件核心职责部署位置资源敏感点Filebeat轻量采集读日志文件、多行合并、打标签每台业务机CPU极低内存占用小Kafka消息缓冲、削峰、解耦、多消费组复用独立日志集群磁盘IO、网络带宽ZookeeperKafka元数据协调、选举与Kafka同集群磁盘、网络延迟Logstash解析、字段提取、格式转换、路由独立日志集群CPUgrok正则、堆内存Elasticsearch存储、倒排索引、聚合查询独立日志集群内存、磁盘、CPUKibana可视化、检索界面独立日志集群内存数据流的细节值得多说两句。Filebeat读到一个日志文件按行读取多行日志需要配置multiline给每条事件打上fields里定义的业务标签和host信息然后批量推送到Kafka的指定topic。这里有个关键设计topic的划分粒度。我建议按业务线或者日志类型分topic比如app-log、nginx-log、sys-log而不是所有日志塞一个topic。原因有两个一是不同日志的解析规则不同Logstash可以按topic起不同的pipeline二是出问题时能快速定位是哪类日志堵了。分区数则按吞吐量估算后面会讲计算方法。Logstash从Kafka消费时用kafkainput插件指定topic和group。这里group id的设计也有讲究如果你希望同一批日志被多个下游处理比如一份进ES做检索、一份进另一个系统做审计就用不同的group idKafka会各自维护offset如果只是单纯消费一个group即可。消费完之后经过filter链grok解析、date时间戳修正、mutate字段处理、geoip之类再output到ES的指定索引。索引命名我习惯用业务名-日期的格式配合ILM做生命周期管理避免索引无限膨胀。1.3 容量估算与资源规划的计算过程方案能不能落地先算账。我拿一个中等规模场景举例假设有50台业务机每台每天产生约2GB原始日志总日增量100GB。这个数字看着不大但要做三件事的容量规划。第一是Kafka的磁盘。Kafka默认保留7天log.retention.hours168按100GB/天算需要700GB裸容量。但Kafka有副本机制副本数设为2的话就是1.4TB。再留30%余量应对峰值和段文件开销实际规划约1.8TB。分区数怎么定经验公式是分区数 max(目标吞吐 / 单分区吞吐, 消费者线程数)。单分区写入吞吐在普通机械盘上大约10-20MB/s100GB/天平均下来约1.2MB/s峰值按8倍算约10MB/s所以3个分区就够但为了并行消费和未来扩展我一般给每个topic设6个分区。第二是Elasticsearch的磁盘。原始日志100GB经过Logstash解析后体积会膨胀因为加了字段、索引结构通常膨胀系数在1.3到1.8之间取1.5算就是150GB/天。ES默认每个索引1个主分片1个副本副本让存储翻倍就是300GB/天。保留30天的话是9TB。这个数字不小所以ILM策略必须配热数据保留7天在高性能盘温数据转到大容量盘超过30天删除或归档。实际规划时我建议按日增量 × 膨胀系数 × (1副本数) × 保留天数 × 1.2安全系数来算。第三是内存。ES是内存大户官方建议堆内存不超过物理内存的50%且不超过32GB超过32GB会失去指针压缩优势。一台32GB内存的机器给ES堆16GB比较合适。Logstash的堆默认1GBgrok正则复杂的话调到2-4GB。Kafka的堆不用太大6-8GB足够它更吃页缓存。Zookeeper堆1-2GB即可。这些数字不是拍脑袋是我在多套环境里调出来的经验值你可以作为起点再微调。提示容量估算一定要留余量日志量的增长往往是非线性的业务一次大促或者一次故障排查开启debug日志量级可能翻好几倍。我吃过没留余量的亏磁盘写满导致ES索引变只读恢复起来相当麻烦。2. 基础环境准备与集群部署实操2.1 操作系统层面的统一初始化部署之前所有节点先做一遍标准化这一步偷懒后面全是坑。我列一下必做的几项。首先是主机名和hosts解析集群内部通信靠主机名比IP可靠IP变了不用改一堆配置。然后是时间同步日志平台对时间极其敏感Filebeat打的时间戳、ES的timestamp、Kibana展示的时间如果对不齐排查问题时时间线全是乱的。用chrony或者ntpd保证所有节点时间误差在毫秒级。文件句柄数和进程数限制也要调。ES会打开大量文件每个段文件一个句柄Kafka也有大量网络连接默认的1024远远不够。在/etc/security/limits.conf里给对应用户加上nofile 65536和nproc 4096。另外关闭swapES官方明确要求关闭swap否则GC时性能会严重劣化用swapoff -a并在/etc/fstab里注释掉swap行。还有一个容易被忽略的点vm.max_map_count。ES的mmap方式需要大量虚拟内存映射区域默认值65530在数据量大时会不够报max virtual memory areas vm.max_map_count [65530] is too low。在/etc/sysctl.conf里加vm.max_map_count262144然后sysctl -p生效。这个参数我在第一次部署时没设ES启动到一半就挂了日志里就这一行提示查了半天。防火墙方面集群内部需要放通这些端口Zookeeper的2181、2888、3888Kafka的9092ES的9200HTTP和9300节点间通信Kibana的5601Logstash的5044如果走beats input或者不开放走Kafka消费就不需要。生产环境建议只对特定网段开放别图省事全开。2.2 Zookeeper与Kafka集群的搭建顺序这两个组件有依赖关系必须先起Zookeeper再起Kafka。Zookeeper我一般部署3个节点奇数个满足过半选举假设是三台机器。每台的zoo.cfg核心配置如下tickTime2000 initLimit10 syncLimit5 dataDir/data/zookeeper/data clientPort2181 server.1zk-node-1:2888:3888 server.2zk-node-2:2888:3888 server.3zk-node-3:2888:3888dataDir下要建一个myid文件内容分别是1、2、3对应上面的server编号。这个myid搞错了集群起不来我第一次部署时三台机器myid全写成1Zookeeper互相打架日志里全是选举失败。启动后可以用echo stat | nc zk-node-1 2181看角色或者进客户端zkCli.sh执行ls /验证。Kafka的server.properties关键配置broker.id1 listenersPLAINTEXT://kafka-node-1:9092 zookeeper.connectzk-node-1:2181,zk-node-2:2181,zk-node-3:2181 log.dirs/data/kafka/logs num.partitions6 default.replication.factor2 log.retention.hours168 log.segment.bytes1073741824broker.id每台不同listeners写本机主机名。default.replication.factor2意味着每个分区两份一台broker挂了数据不丢。log.segment.bytes设1GB段文件太大清理粒度粗太小文件数多影响性能1GB是个平衡点。启动后创建topickafka-topics.sh --create --bootstrap-server kafka-node-1:9092 \ --topic app-log --partitions 6 --replication-factor 2验证用--describe看分区分布是否均匀如果某个broker上leader过多说明分配不均可以手动reassign。2.3 Elasticsearch集群的节点角色划分ES集群我建议至少3个节点并且做角色分离。小规模可以混部但一旦数据量上来master节点和data节点混在一起master选举时受数据节点负载影响容易出现集群脑裂或者响应变慢。我的做法是3个节点都配node.master: true和node.data: true小规模够用但如果节点数到5个以上就拆出专门的master节点node.data: false和data节点。elasticsearch.yml核心配置cluster.name: log-platform node.name: es-node-1 path.data: /data/elasticsearch/data path.logs: /data/elasticsearch/logs network.host: 0.0.0.0 http.port: 9200 discovery.seed_hosts: [es-node-1, es-node-2, es-node-3] cluster.initial_master_nodes: [es-node-1, es-node-2, es-node-3] bootstrap.memory_lock: truebootstrap.memory_lock: true配合前面的limits.conf里的memlock unlimited防止ES堆内存被换出。cluster.initial_master_nodes只在首次启动时用集群成型后可以去掉避免误操作导致重新选举。JVM堆在jvm.options里设-Xms16g -Xmx16g两个值必须相等避免运行时扩缩堆带来的停顿。启动后curl -XGET http://es-node-1:9200/_cluster/health?pretty看状态status为green才算健康yellow说明有副本未分配单节点部署时正常red说明有主分片不可用必须排查。2.4 Logstash与Kibana的安装要点Logstash的配置分三段input、filter、output。走Kafka消费的input配置input { kafka { bootstrap_servers kafka-node-1:9092,kafka-node-2:9092 topics [app-log] group_id logstash-app codec json consumer_threads 3 } }consumer_threads建议等于分区数或者分区数的约数6个分区配3个线程每个线程消费2个分区并行度合适。codec json是因为Filebeat输出到Kafka时我让它直接输出JSON格式省去Logstash里再做一次JSON解析。Kibana的kibana.yml主要配server.host、elasticsearch.hosts和server.port。启动后浏览器访问5601第一次进去要在Stack Management里创建Index Pattern匹配ES里的索引名比如app-log-*然后选timestamp作为时间字段。这一步不做Discover页面什么都看不到。3. 采集端配置与数据管道打通3.1 Filebeat的采集配置与多行日志处理Filebeat是整条链路的源头它配错了后面全白搭。核心配置文件filebeat.ymlfilebeat.inputs: - type: log enabled: true paths: - /var/log/app/*.log multiline.pattern: ^\d{4}-\d{2}-\d{2} multiline.negate: true multiline.match: after fields: app_name: order-service log_type: app fields_under_root: false output.kafka: hosts: [kafka-node-1:9092, kafka-node-2:9092] topic: app-log partition.round_robin: reachable_only: false required_acks: 1 compression: gzip max_message_bytes: 1000000多行日志是重点。Java应用的异常堆栈一行一条如果不合并ES里就是几十条孤立记录根本没法看。multiline.pattern匹配行首的时间戳negate: true表示不匹配这个模式的行也就是堆栈的续行合并到上一行match: after表示追加到后面。这个配置因日志格式而异你得先head几行日志看看行首长什么样再定正则。fields里打的标签会跟着日志进Kafka、进ES后面Kibana里可以按app_name过滤非常实用。fields_under_root: false表示标签放在fields.子对象下避免和日志本身的字段冲突。output.kafka里required_acks: 1表示leader写入成功即返回兼顾性能和可靠性如果对丢数据零容忍可以设-1所有副本确认但吞吐会降。compression: gzip能显著减少网络传输和Kafka磁盘占用日志文本压缩比通常能到5:1以上强烈建议开。3.2 Logstash的grok解析与字段规范化日志进了Logstash要做的是把非结构化文本变成结构化字段。以一条典型的应用日志为例2024-05-20 14:23:11.234 INFO [order-service,trace-8f3a] com.example.OrderController - Order created, orderId10086, userId2001, amount99.50grok表达式这么写filter { grok { match { message %{TIMESTAMP_ISO8601:log_time} %{LOGLEVEL:level} \[%{DATA:service},%{DATA:trace_id}\] %{DATA:logger} - %{GREEDYDATA:log_msg} } } date { match [log_time, yyyy-MM-dd HH:mm:ss.SSS] target timestamp } mutate { remove_field [log_time, message] } }这里有几个经验点。第一grok正则性能开销大能用%{DATA}就别用复杂的自定义正则%{GREEDYDATA}放最后匹配剩余内容。第二date插件把日志里的时间替换成timestamp这一步至关重要否则ES用的是接收时间日志延迟到达时时间线就错了。第三mutate里删掉原始message字段能省不少存储空间但调试阶段建议先留着等解析稳定了再删。如果日志格式多样可以用多个grok match加tag_on_failure解析失败的打标签走死信队列别让一条坏日志卡住整个pipeline。3.3 端到端联调与数据校验方法配置都写完了怎么确认链路通了我的排查顺序是从后往前。先在Kibana的Dev Tools里GET app-log-*/_count看有没有数据进来。没有的话去ES节点curl http://localhost:9200/_cat/indices?v看索引建了没。索引没建说明Logstash没输出去看Logstash日志有没有报错常见的是ES认证失败或者索引模板冲突。Logstash正常但ES没数据再往上游查Kafka。用kafka-console-consumer.sh --bootstrap-server kafka-node-1:9092 --topic app-log --from-beginning --max-messages 5看topic里有没有消息。有消息说明Filebeat在正常发问题在Logstash消费端没消息说明Filebeat没采到或者没发出去去业务机上看Filebeat日志tail -f /var/log/filebeat/filebeat常见问题是路径配错、权限不足Filebeat用户读不了日志文件。这个从后往前的排查法能快速定位断点在哪一段比从头到尾瞎试高效得多。我一般还会在Filebeat配置里临时开logging.level: debug看它到底读了哪些文件、发了多少事件确认采集端没问题再关掉。4. 性能调优与常见故障排查实录4.1 写入链路的瓶颈定位与参数调优平台跑起来只是开始跑得稳才是本事。写入链路的瓶颈通常出现在三个地方Kafka磁盘IO、Logstash CPU、ES索引速度。定位方法是用监控数据说话。Kafka看kafka.server:typeBrokerTopicMetrics,nameBytesInPerSec如果持续高位且消费lag在涨说明生产快于消费。Logstash看JVM的CPU使用率和GC时间grok复杂时CPU打满是常态。ES看indexing rate和refresh、merge的耗时。调优手段我按优先级排第一ES的批量写入大小。Logstash output里flush_size默认500idle_flush_time默认1秒日志量大时可以调到1000-2000减少请求次数。第二ES索引的refresh_interval默认1秒对写入压力大的索引可以临时调到30秒甚至-1关闭自动刷新查询实时性要求不高的日志场景完全可接受。第三副本数。写入期间可以把副本设为0写完再调回来能显著提升索引速度但要注意这期间没有冗余保护。Kafka这边num.io.threads和num.network.threads可以按CPU核数调整一般设成核数的倍数。log.flush.interval.messages默认是Long.MAX依赖操作系统刷盘可靠性要求高的话可以设个值强制刷盘但会牺牲吞吐。4.2 典型故障速查表与处理思路下面这张表是我这些年遇到的高频问题汇总基本覆盖了八成故障场景现象可能原因排查命令/方法处理方式Kibana看不到新日志索引模式不匹配或时间字段错_cat/indices看索引名重建Index Pattern确认timestampES集群变red磁盘写满或分片损坏_cluster/health、_cat/shards清理磁盘、reroute恢复分片Logstash消费lag持续增长消费线程不足或grok太慢看Kafka consumer lag加consumer_threads、简化grokFilebeat报权限错误运行用户读不了日志ls -l看文件权限加用户组或用root运行谨慎Kafka频繁leader选举ZK会话超时或网络抖动看Kafka和ZK日志调大session.timeout.ms日志时间戳错乱date插件未配置或格式不符对比日志时间和timestamp修正date match格式重点说两个我印象最深的。一个是ES磁盘写满导致索引变只读报blocked by: [FORBIDDEN/12/index read-only / allow delete (api)]。原因是ES的磁盘水位线保护默认85%触发只读。处理办法是先清磁盘删旧索引或者扩盘然后执行PUT _all/_settings {index.blocks.read_only_allow_delete: null}解除只读。根治要靠ILM策略自动滚动删除。另一个是Kafka的NotLeaderForPartitionException客户端连的分区leader刚好在选举。偶发一次无所谓频繁出现就要查ZK和broker之间的会话。我遇到过一次是ZK节点负载太高session.timeout.ms默认10秒不够用调到18秒后稳定了。这类问题没有银弹只能靠日志和监控一点点抠。4.3 索引生命周期管理与存储成本控制日志平台最大的隐性成本是存储。不管理的话索引无限增长磁盘迟早爆。ILMIndex Lifecycle Management是官方给的解法思路是把索引按年龄分阶段hot热频繁写入查询、warm温只读偶尔查、cold冷很少查、delete删除。一个实用的策略hot阶段7天rollover在50GB或者7天时触发warm阶段把副本降到0、段合并30天后delete。配置用Kibana界面或者API都行核心是给索引模板挂上index.lifecycle.name。我实测下来这套策略能把存储成本压到原来的三分之一左右因为大部分日志一周后就没人看了。还有个小技巧不同业务日志的价值不同核心交易日志保留90天调试日志保留7天就够。按业务分索引、分策略别一刀切。另外冷数据可以转到便宜的大容量盘甚至对象存储ES有对应的快照和可搜索快照功能成本能进一步降。5. 可视化看板搭建与平台价值延展5.1 Kibana看板的设计原则数据存进去不是目的能快速看出问题才是。Kibana看板我一般分三类概览类、排障类、业务类。概览类放全局指标比如各业务日志量趋势、错误日志占比、TOP错误类型用饼图、折线图、数据表组合。排障类放明细检索配好常用过滤条件比如按level: ERROR、按trace_id串联一次请求的全链路日志。业务类则结合业务指标比如订单创建量、支付成功率从日志里聚合出来。设计看板有个原则一屏之内能回答“现在有没有问题”。我见过太多看板堆了几十个图表反而没人看。我的做法是顶部放三到五个关键数字今日日志总量、错误数、错误率、平均响应时间下面再放趋势和明细。颜色用红黄绿区分状态一眼就能判断。5.2 从日志到告警的闭环思路日志平台的价值不止于“事后查”更在于“事前警”。Kibana自带告警功能Alerting可以基于查询条件触发。比如配置一条规则过去5分钟内level: ERROR的日志数超过100条就发通知。通知渠道可以接邮件、Webhook到内部协作工具。告警配置的关键是阈值和抑制。阈值太低天天误报团队很快就麻木了太高则漏报。我的经验是先观察一周的正常波动范围取P99值上浮20%作为初始阈值运行一段时间再调。抑制方面同一个问题短时间内重复触发只发一次避免告警风暴。另外告警内容要带上下文比如错误样本、涉及的service、trace_id收到告警的人能直接去Kibana里查而不是只看到一个数字。5.3 平台后续可扩展的方向这套平台搭好之后扩展空间很大。一个方向是接入更多数据源除了应用日志系统日志、中间件日志、审计日志都能走同一条链路Filebeat的模块module已经内置了很多常见软件的解析规则开箱即用。另一个方向是和链路追踪结合日志里的trace_id如果能和APM系统的trace关联排障时能从一条错误日志直接跳到完整调用链效率提升非常明显。再往深了做可以做日志的异常检测。基于历史数据训练基线自动发现日志量的突增突降或者错误模式的异常变化比固定阈值告警更智能。这块我还在摸索阶段用了一些简单的统计方法效果初步可以但离生产级还有距离。不过平台的数据基础打好了后面加什么能力都只是在上层做文章。我个人在实际操作中的体会是日志平台这件事搭建只占三成精力七成在后续的调优和运营。配置能抄但对业务日志的理解、对故障模式的积累是抄不来的。刚开始别追求大而全先把核心业务的日志接进来、能查能看跑顺了再逐步扩展。踩坑不可怕可怕的是踩了坑没留下记录——我现在每处理一个故障都会在内部文档里记一笔现象、原因、解法时间长了这就是团队最宝贵的资产。