1. 起因日志还靠“逐台机器 grep”的时候问题有多大几年前我所在的业务组日常排查线上问题的标准动作是这样的先看告警群然后连上一台服务器tail -f看后端日志再顺着调用关系去下一台机器继续找。那时候日志量一天大概 200G单机 Elasticsearch 勉强活着Kibana 上搜中文经常转圈翻到一条有效的错误信息全凭运气。真正让我下决心重做一套企业级日志系统架构的是某次凌晨大促流量应用临时扩容了二十台每台都在写日志但到底哪台报错、哪台接口变慢所有人心里没底最后只能挨个登录机器去 grep KEY 字段。那晚之后我们开始认真规划一套以 Elasticsearch、Filebeat、Kafka、Logstash、Kibana 为核心的日志系统。现在回头看这套架构解决的问题远不止“能搜日志”这么简单它同时承担了削峰填谷、数据缓冲、格式统一、多团队检索隔离、长期留存成本控制这些职责。如果你所在的团队同样是从三五十台服务器的小规模往几百台、上千台容器方向走这篇内容应该能帮你少踩不少坑。很多人会觉得日志链路不复杂把应用日志写到 Elasticsearch再配个 Kibana 就能查。这个判断在小规模下没错可流量一旦上来直接写 ES 的模式会最先崩。日志场景的特点是大部分时间平稳但故障和促销期间流量可能突然翻几倍应用产生的日志量和业务请求量基本成正比。如果采集端直接把日志灌进 ESES 的写入线程池会在几秒钟内被打满bulk 请求开始拒绝查询也跟着变慢。问题扩大之后正常日志和异常日志混在一起你压根没法定位是业务问题还是日志系统引发的链路崩溃。当时我们列了一个很粗的接入需求所有应用统一走一套日志采集通道支持多行异常栈整合、字段拆解、敏感信息脱敏、按业务线和环境隔离开日志至少保留三十天能支撑告警、排障和简单统计采集端丢日志的比例要尽量低。于是架构从“Filebeat 直接发 ES 或 Logstash”调整为“Filebeat 采集Kafka 缓冲Logstash 消费加工ESS 存储Kibana 查询”。这也是本文想分享的核心内容每个组件到底在解决什么问题以及上线后遇到过哪些真实排障过程。2. 先把架构想清楚为什么中间要插一个 Kafka而不是让日志直达 ES如果只是搭一个演示环境Filebeat 直接把日志推到 Elasticsearch 完全够用少一层 Kafka 还少一点运维成本。但在企业级场景里日志链路不可能只看正常状态还得看 ES 集群抖动、Logstash 宕机、应用突发大量日志这种最坏情况。中间加 Kafka本质上是给日志数据加了一个大容量缓冲池。我习惯用一个类比向新同事解释日志源是山上的水Elasticsearch 是楼下的大水桶。水量小时直接拿管子接过去没问题但下暴雨时水突然变大水桶灌满就会溢出来。Kafka 相当于水库上游的水先蓄起来下游按自己的速度处理。水桶不会因为暴雨被冲垮上游也不会因为水桶满了而停掉生产。很多人纠结 Kafka 引入后延迟变高实际上日志场景根本不在乎这几十毫秒真正在乎的是 ES 写入压力和故障期间的数据完整性。再把几个组件的工作边界拆开看组件职责放入链路的原因最容易踩的坑Filebeat读取日志文件、传输到 Kafka资源占用小带读取位置记录重启不丢不重多行日志没合并堆栈被拆成碎行Kafka高吞吐消息缓冲削峰填谷下游故障期间仍可短暂缓存分区数拍脑袋定后期调整成本高Logstash消费 Kafka 数据做过滤、解析、转换支持 grok、date、mutate和 ES 解耦过滤器太重CPU 直接打满Elasticsearch索引、存储、检索核心存储和搜索能力分片过多或映射字段类型失控Kibana可视化、检索、权限面向研发和业务人员的统一入口数据视图和权限配置混乱查询变慢当初我们也认真考虑过一个更简单的方案Logstash 直接从应用服务器读日志文件不走 Kafka。实际测下来有几个问题。第一Logstash 是 JVM 进程内存和 CPU 占用比 Filebeat 高很多在应用服务器上每多跑一个 JVM 都会吃掉本来属于业务的内存。第二Logstash 没有像 Filebeat 那样轻量的 registry 机制来管理文件读取位置进程重启后存在重复消费或者丢位置的风险。第三一旦 Logstash 集群整体缓慢采集端的日志会直接在文件里积压应用磁盘占用率会飙升。所以采集端最终统一选定 Filebeat只负责“快而稳”地把日志挪到 Kafka。选型时我们还否定了“Logstash 直接消费日志后写入 ES”的做法。这不是说 Logstash 不行而是为了保留一个独立的缓冲层。Kafka 在这里不需要很大的存储配置保留两三天就够了目的是给下游留出故障窗口。比如 Elasticsearch 做版本升级、扩容或者集群因为磁盘水位变红时Kafka 还能继续接收新日志等 ES 恢复后再由 Logstash 追着消费。Kafka 的 topic 天然可以按业务线做隔离某个业务日志量大也不影响其他业务线。整体链路变成了应用写本地文件Filebeat 采集Kafka 蓄水Logstash 加工ES 存储Kibana 展示。这也是企业里最常见的 ELK 扩展形态。3. 逐层落地Filebeat、Kafka、Logstash、Elasticsearch 的关键配置与参数取舍3.1 Filebeat把采集场景做“轻”把多行日志做“整”Filebeat 的配置重点不是性能调参而是多行日志和字段扩展。应用日志里最常见的麻烦是异常堆栈跨多行如果一行一行发在 Kibana 里搜到的就是一个支离破碎的Exception上下文完全串不起来。好在 Filebeat 的 filestream input 已经自带 multiline 合并能力我们用的是按时间戳首行匹配filebeat.inputs: - type: filestream enabled: true id: app-json-log paths: - /var/log/apps/app1/*.log parsers: - ndjson: target: - multiline: type: pattern pattern: ^\[[0-9]{4}-[0-9]{2}-[0-9]{2} negate: true match: after fields: app_id: order-center env: prod fields_under_root: true output.kafka: hosts: [kafka001:9092, kafka002:9092, kafka003:9092] topic: app-logs-%{[app_id]} partition: round_robin: reachable_only: false required_acks: 1 compression: lz4 max_message_bytes: 1048576fields_under_root会把app_id和env提升为顶层字段后续 Logstash 和 ES 都能直接按这两个字段做索引和过滤。注意 filestream 的id必须唯一否则同一文件会被重复采集。多行解析里我用的是“以[YYYY-MM-DD开头作为新日志标志其余行都追加到上一条”对常规 springboot 格式的日志很管用。多行日志合并有个很隐蔽的问题超长堆栈会超过 Kafka 的单条消息大小上限。如果堆栈特别厚建议在应用侧把堆栈行数截断比如 logback 的MaxLength或者在 Filebeat 里做include_lines控制避免单条消息超过 1MB。我们线上就遇到过某个应用打印了 3MB 的完整堆栈直接把 Kafka producer 打崩。另外required_acks: 1是在吞吐和可靠性之间的折中日志允许极少量的极端丢失但不需要每个消息都等所有副本确认。部署完 Filebeat 后一定要用filebeat test output验证 Kafka 连通性并用filebeat test config检查语法。我们当时有一台机器配置文件里的 YAML 缩进错误Filebeat 起不来但日志采集任务在监控上没告警数据缺了两小时才发现。所以采集端必须纳入自监控后续我会再细说。3.2 KafkaTopic 规划、分区数与保留时间不要靠拍脑袋Kafka 在整个链路里是被依赖但不参与业务逻辑的组件布好之后很容易被忽略。但很多日志量大的团队最后会被 Kafka 反噬根源往往就是 Topic 与分区数规划不合理。我们按业务线区分 Topic命名规则是app-logs-{appId}。分区的数量按“日志峰值吞吐量”推而不是按日志总量。假设一个核心应用高峰期每秒产生 30MB 日志单个消费者线程稳定消费能力按 5MB/s 到 10MB/s 估算那么至少 6 个分区再考虑下游 Logstash 处理可能暂时卡住还要留一定余量所以我们把核心业务 Topic 定在 12 个分区。分区数也不是越多越好分区多了会导致 ES 侧索引大量小段文件对读写都有额外开销。创建命令大致是kafka-topics.sh --create \ --topic app-logs-order-center \ --partitions 12 \ --replication-factor 3 \ --config cleanup.policydelete \ --config retention.ms259200000 \ --config max.message.bytes1048588 \ --bootstrap-server kafka001:9092副本数如果集群节点少于 4 个可以先设 2副本数多会拖慢写入对日志这种高吞吐低敏感场景没必要每个副本都强同步。保留时间我们设了三天足够覆盖 Logstash 和 ES 同时故障的极端场景。cleanup.policydelete是日志场景最合适的默认策略别因为听过 Kafka 支持 compaction 就随便加上compaction 适合存储 KV 状态不适合纯日志流。Kafka 的消费组策略也要提前定好。所有 Logstash 实例最好使用同一个group_id比如logstash-prod这样多个 Logstash 节点能并行消费同一个 Topic如果每个 Logstash 使用不同 group_id就会出现同一份日志被重复写入 Elasticsearch索引瞬间膨胀而且还会出现不可控的重复计数。我们在联调环境踩过这个坑监控上看半天没发现问题最后排查到是不同节点上的 group_id 写错了。3.3 Logstash过滤链路越短越好解析规则尽量靠近数据源头Logstash 是全链路里最容易成为性能瓶颈的一环因为 grok 正则解析非常耗 CPU。如果业务应用能直接输出 JSON 结构化日志就不必让 Logstash 用 grok 猜字段这样做能省下大量处理时间。我们的处理原则是应用优先打印结构化日志只有历史系统或第三方日志实在改不动时才在 Logstash 里做 grok。一个简化后的 Logstash 管线配置大概长这样input { kafka { bootstrap_servers kafka001:9092,kafka002:9092,kafka003:9092 topics_pattern app-logs-.* group_id logstash-prod consumer_threads 8 codec json auto_offset_reset latest } } filter { if [app_id] order-center { grok { match { message %{LOGLEVEL:level} %{TIMESTAMP_ISO8601:log_time} %{JAVACLASS:logger} - %{GREEDYDATA:content} } overwrite [message] } } date { match [timestamp, UNIX_MS, ISO8601] target timestamp } mutate { remove_field [agent, input, ecs, log] rename { message content } } } output { elasticsearch { hosts [https://es001:9200, https://es002:9200] index app-logs-%{[app_id]}-%{yyyy.MM.dd} user ${ES_LOG_USER} password ${ES_LOG_PWD} ssl_certificate_authorities [/etc/logstash/certs/ca.crt] } }有几个参数需要特别解释。consumer_threads原定 8 是根据 Kafka 分区数设置的超过分区数多余的线程会空转如果你改了 Topic 分区数Logstash 实例数和消费者线程要重新对齐。date过滤器很关键它决定最终 ES 里timestamp是使用日志产生时间还是 Logstash 接收时间。对排障而言日志产生时间才是真实时间否则排查网络延迟或队列积压时看到的全是假的。Logstash 的调优参数大部分写在logstash.yml里。pipeline.workers建议等于 CPU 核数pipeline.batch.size从默认 125 提到 500 在大日志量下有效果但也要小心 JVM 堆内存压力一般给 Logstash 4G 到 8G 堆内存比较平衡。还有一个容易被忽略的细节如果业务高峰期出现 Logstash 积压优先检查下游 ES 的 bulk 响应时间而不是先加pipeline.workers。下游 ES 慢了Logstash 消费速度再快也没用消息全部卡在 Kafka 上。3.4 Elasticsearch索引模板、字段映射和生命周期必须提前管起来日志写入 ES 之后最怕两件事一是 mapping 被动态映射弄出一堆keyword和text类型错乱二是索引分片多到把集群拖垮。企业级日志系统里索引模板和生命周期策略不是上线后补的而是接入第一天就要定好。我先说索引模板。ES 默认会动态映射字段日志里如果混入一个非结构化字段它可能被映射成keyword导致后续无法全文搜索又或者一个本应是date的字段被映射成textKibana 时间筛选直接失效。我们的做法是提前把通用字段控制住PUT _index_template/app-logs-default { index_patterns: [app-logs-*], priority: 100, template: { settings: { number_of_shards: 3, number_of_replicas: 1 }, mappings: { properties: { timestamp: { type: date }, app_id: { type: keyword }, level: { type: keyword }, content: { type: text, fields: { keyword: { type: keyword, ignore_above: 256 } } }, host_name: { type: keyword } } } } }content同时定义text和keyword子字段是为了既支持match模糊搜索又能对整条消息做精确聚合。level、app_id、host_name这些字段用 keyword因为它们的核心用途是过滤和分组不需要分词搜索。生命周期方面我们用了一个最简单但有效的策略按天索引保留 30 天。每天一个新索引app-logs-{appId}-{yyyy.MM.dd}ILM 策略只负责删除过期索引。这里的重点是不要为了追求所谓的“性能”给每个应用都单独调分片数。分片太多是个大坑我们曾统计过集群里有 400 多个 1GB 级别的小索引每分片开销不小最后整体写入变慢缓存的 fielddata 也频繁抖动。后来把所有小日志应用的分片数统一压到 1 或 3集群稳定性明显上来了。ES 还有几个值得关注的开关。如果日志中有大量不需要全文搜索的调试字段可以把index设为false节省存储空间。refresh_interval默认 1 秒日志场景如果不是特别追求实时调到 30 秒能显著降低合并段和写入压力。很多团队把 ES 当成在线数据库调优但日志系统对实时性要求很低适当放慢 refresh 完全无感。3.5 Kibana检索、可视化和权限要从第一天就分层Kibana 是整个链路里离用户最近的一层。我们接入的业务方不只是后端研发还有一部分数据分析同学和运维值班人员。如果只给一个默认 Dashboard或者干脆让大家自己建索引再乱拖图表过不了多久就会变成“Kibana 没人敢打开”。我们按团队划分 Kibana Space比如订单域、支付域、基础设施域各看各的日志。每个 Space 里预置了对应的数据视图、常用检索条件和 Dashboard。权限上用 Kibana 自身的角色机制只读角色不能访问 Discover 之外的 Dev Tools避免有人误改索引模式或者执行大规模聚合把集群拖慢。比起权限更经常被忽略的是“检索视觉”。日志里最核心的查看界面是 Discover我会在数据视图里把常用字段固定为表格首列比如timestamp、app_id、level、content的前 100 个字符。这样打开即用否则每次排障都要手动加字段效率差很多。Kibana 查询语法里最有用的是level: ERROR、content: NullPointerException这类组合配合range语法按绝对时间锁定窗口。告警方面我们用 Kibana 自带的 Alerting 接了一个 webhook 到群里规则就是“某业务线 ERROR 数量 5 分钟内超过阈值”不需要额外引入组件。4. 真正上线会碰到的坑一次积压、一次写入降速和一个 grok 解析谜题4.1 第一次 Kafka 消费积压LAG 越来越高问题出在消费线程数上线大概一个月后某天监控图上kafka-consumer-lag曲线开始持续爬坡从几万条涨到上百万条。排障第一步很常规先看最直接的指标kafka-consumer-groups.sh --bootstrap-server kafka001:9092 \ --describe \ --group logstash-prod输出里LAG列高得让人头皮发麻但 Logstash 进程的 CPU 看着并不高。继续看 Logstash pipeline stats发现它的duration_in_millis很大说明消息其实已经拉下来了只是处理速度跟不上。再往 Kafka 方向看consumer 线程数量只有 2而 Topic 分区数是 12也就是说有大量分区根本没有活跃消费者线程在拉取Kafka 只能等那两个线程慢慢消费。原因查清之后就好办了Logstash 配置里consumer_threads只写了 2明显小于分区数。把consumer_threads改成 8同时给 Logstash JVM 堆内存加了 2G消费速度立刻跟上来LAG 在一个小时内归零。这里有个教训Kafka 的 LAG 不是单纯靠增加分区数就能解决消费者线程数、Logstash 实例数都要跟着分区数匹配否则 Topic 设计得再大也只是浪费。4.2 ES 集群突然写入变慢分片过多和 refresh 参数都在拖后腿第二次问题更隐蔽日志量总体并没有暴涨但 ES 写入指标从正常的几百 ms 一路涨到几十秒Kibana 上的查询也频频超时。先看集群状态是 yellow 但不是 redCPU 和磁盘都没到极限。再用_cat/indices?v看索引列表发现app-logs-*索引数量多得吓人几百个磁盘只有几百 MB 的小索引每个索引还有 3 到 5 个分片这样光分片总数就上千了。分片本身不是问题问题是分片太多之后各个节点之间的协调开销、字段缓存和段合并压力都上来了。针对日志这种小索引最佳实践是分片数宁少勿多。我们通过索引模板把后续应用的默认分片数改成 1对已经存在的小索引做收缩和删除。同时把refresh_interval从默认 1 秒调到 30 秒写入压力明显降下来。这个优化做完之后我的一个体会是ES 日志场景的瓶颈经常不是内存和 CPU而是因为数据组织方式不合理把自己拖垮。4.3 grok 解析失败排查了半天最后发现是日志格式根本不统一有一次我们看 Kibana发现content字段全是原始日志串level字段是空的而tags里出现了_grokparsefailure。这说明 Logstash 的 grok 正则没有匹配上。我最初以为是正则写得不够健壮把日志样本拿过来一遍遍调。后来才发现这个应用的日志格式在不同版本之间变了新版加了traceId字段旧版没有而 grok 正则还是按旧格式写的。新格式一进来就失败大部分日志没有被正确解析排障时没人能看清来龙去脉。这个问题最有效的解法是让 grok 正则基于稳定前缀或者干脆放弃在 Logstash 里解析直接让应用输出 JSON。对于实在改不动的遗留系统我会保留一条兜底管线如果_grokparsefailure出现不丢弃日志而是把原始内容原样写入另一个索引app-logs-parse-failure-*方便后续定期治理而不是让日志静默丢失。很多团队一遇到 grok 解析失败就选择 drop这是最坏的选择——日志的价值在于事后追溯哪怕字段没解析出来原始内容也值得留着。5. 日志治理层面的经验结构化、采样、脱敏与成本控制如果链路搭好了但日志内容本身是乱的那整个系统最多算“能搜”算不上“能用”。我们后来花了不少精力做日志治理核心方向是四个结构化、采样、脱敏和成本控制。结构化是第一位的。应用只要打印 JSON 日志Logstash 就不需要 grokES 映射也稳定后续告警聚合、字段查询都顺理成章。我见过很多团队把 grok 正则写得像天书但真正的问题不是解析难度而是日志源头没有规范化。与其在后端解析补丁补丁地修不如从应用日志框架的 pattern 和字段约定开始定规范。日志里至少要有时间、级别、应用标识、追踪 ID、业务动作和关键参数没有 traceId 的日志分布式链路里基本找不到关联点。采样比很多人想象的更重要。我们需要全量日志和全量可观测性不能兼得吗其实大多数 DEBUG 日志只在排查问题时有价值平时全量存下来就是在烧钱。我们对 DEBUG 日志做了比例采样比如只保留 10%ERROR 和 WARN 全量保留。这样一个简单的策略存储成本直接降了差不多一半而排障能力几乎没受影响。采样位置放在 Logstash 过滤器里离数据源头越近越好否则 Filebeat 已经发出去了采样就没什么意义。脱敏这件事必须做但别指望在 Logstash 里用一两个正则解决所有问题。如果日志里出现手机号、身份证号、银行卡号这类信息建议在应用日志框架层面就不要打印从源头去掉老系统实在控制不了再用 Logstash 的mutate和substitute做后用正则替换。需要注意JSON 日志中某个字段的值被替换后字段类型还是字符串不影响 ES 映射但正则如果写得不严谨很容易误伤正常文本。所以我们内部约定日志中不允许出现明文敏感字段这条写进代码评审 checklist。成本控制是绕不开的话题。日志存储是长期开销保留 30 天听起来简单但如果每天 200G30 天就是 6T再算上副本就是 12T这还不算重构和段合并的额外空间。所以我们会按业务价值分类核心业务日志保留 30 天运营分析类日志保留 15 天未来大概率用不到的低价值日志保留 7 天并且关闭_source里的冗余字段。协调 ILM 策略时用索引模板统一套上去避免每来一个新业务线都手工建索引再手动删。成本控制不是临到月底才看账单而是从接入的第一天就按数据价值定策略。6. 一些长期维护的工作习惯希望你能比我更早意识到链路搭完只是开始后续真正决定系统质量的往往是那些看着不起眼的日常工作习惯。第一所有配置必须入库管理。Filebeat 的 yml、Logstash 的 pipeline、ES 的索引模板和 ILM 策略全部放到配置仓库里走版本化任何变更都留痕。我吃过亏临时手动改过一台 Logstash 配置改了之后现场恢复了但一个月后格式化机器再部署那台机器的配置和别家不一致问题又回来一次。第二每个环节都要有自监控和自愈手段。Filebeat 看采集速率和是否有 filebeat 进程退出Kafka 看 Topic 的 LAGLogstash 看 pipeline 的duration_in_millis和失败日志ES 看写入延迟、查询延迟、分片数和磁盘水位。这些指标不一定都要接入公司级的监控平台至少你自己得有办法在出问题时快速看到。第三定期做故障演练。我们每季度会人为停掉一个 Logstash 节点、或者把 ES 集群里的一个节点摘除一段时间观察 Kafka 的 LAG 变化、恢复后的消费追赶情况。不演练的话真实事故发生时你根本不确定链路是否真的扛得住。第四日志系统的交付不是“能搜到日志”就结束。我会在接入完成后专门做一次面向研发的简单培训告诉大家 traceId 怎么填、时间范围怎么选、常见检索语法有哪些、什么情况下去看 Kibana 而不是登服务器。很多人不愿意花这个时间其实这笔投入的回报非常直接研发少问一次“为什么 Kibana 里查不到”就少占用一次你的排障时间。这个架构用到现在最让我满意的不是吞吐量多高也不是各种组件调参有多精妙而是团队在线上出问题时能迅速聚焦到真正的故障点不再靠登录一台台机器碰运气。
阅读完成 · 觉得有帮助?