ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

分布式日志系统实战:基于ELK搭建集中检索与链路追踪平台

分布式日志系统实战:基于ELK搭建集中检索与链路追踪平台 做分布式日志系统这个事情我前前后后折腾了一个多月从最开始一台台机器翻日志排查问题到最终在 Kibana 里几秒钟定位故障现场这个体验差异是真的巨大。如果你也正在处理多个服务、多台服务器还在靠 grep 和 ssh 登服务器翻日志排查问题那这篇文章应该能帮上忙。我打算用一个真实落地的分布式日志系统项目为例把从需求拆解、技术选型、组件部署、采集链路、常见坑点到运维实践整个流程完整讲一遍。这里面没有那种“只讲概念不给实操”的内容所有配置和参数都是我在实际环境里验证过的你可以直接拿过去用再根据自己团队的情况做微调。1. 项目背景与整体设计思路1.1 不解决日志问题的代价有多大先说个真实的场景。我们当时维护一套微服务架构大概十几个服务部署在接近二十台服务器上有 Java 的、Python 的还有一些中间件和定时任务。平时开发环境大家各查各的日志还好一到生产环境出问题排查链路就变成了一个高成本协作流程先问是谁负责的服务再找是哪个实例再登录服务器 grep 关键字盯着时间戳人工拼接一条请求经过的各个节点。有一次线上订单回调异常链路涉及网关、订单服务、库存服务、消息队列消费者四个环节三个同事分别在各自的服务器上翻日志最后在群里对时间线对了快两个小时才找到根因。那次之后我就下定了决心日志系统这事情没法再拖了。这个项目的核心目标其实就三个第一让所有服务的日志集中到一个地方统一检索第二让链路追踪的信息能串联起来第三给后续的监控告警和数据分析打个底子。说白了就是把“日志”从一个排错工具升级成整个技术团队可观测性体系的基础设施。1.2 技术选型为什么最终选了 ELK 体系市面上做日志集中管理的方案不少有商业的有开源的也有大厂自研后开源出来的组件。我当时的调研范围主要包括这三类一是传统的 Shell 脚本加 rsyslog 转发。这个方案胜在轻量不需要引入额外组件但功能太弱日志格式没法统一解析检索能力和 UI 体验基本为零只能解决“把日志集中起来”解决不了“快速检索和分析”的需求。二是商业方案最典型的是 Splunk。功能确实强大部署和排查体验在商业产品里算是天花板了但授权费对于中小团队来说是一笔不小的支出直接用开源方案替代完全能满足需求。三是 ELK 技术栈Elasticsearch Logstash Kibana再加上 Beat 系列轻量采集器。Elasticsearch 负责存储和检索Logstash 负责采集、解析和清洗Kibana 负责可视化和交互查询三者组合在一起刚好形成一个完整的日志处理闭环。加上这一套是 Apache 2.0 协议的开源软件社区活跃度高遇到问题能搜到的资料也最多。最后我选的是 Elasticsearch、Logstash、Kibana、Filebeat 这套组合。有人会问为什么不直接用 Loki轻量且和 Grafana 集成得很好。我的考虑是团队当时已有 Grafana 监控体系但日志检索还是独立需求ELK 的全文检索能力、索引生命周期管理、以及可扩展的聚合分析能力比 Loki 更适合我们这种多业务线、日志类型杂乱的场景。如果你们团队纯 Kubernetes 环境、日志量特别大Loki 确实是一个值得考虑的备选但如果你也需要做复杂查询和长期趋势分析ELK 的生态明显更成熟。1.3 整体架构设计从采集到展示的一条完整链路最终采用的架构是分层设计的每一层尽量保持单一职责这样后续扩展和替换组件都会比较灵活。数据流转的大致路径是先由部署在各服务器上的 Filebeat 采集应用日志文件把数据推给 LogstashLogstash 收到日志后做解析、清洗、字段补充格式化后写入 Elasticsearch最后用户通过 Kibana 进行检索和可视化分析。这里有个很多人会纠结的点到底是在每个节点上部署 Logstash还是部署 Filebeat 集中转发我的建议是除非你的日志量特别小、格式特别简单否则不要所有节点都部署 Logstash。Logstash 是 JVM 应用内存占用高在业务服务器上跑会挤占业务资源。Filebeat 是一个轻量级 Go 写的采集器资源占用很小更适合作为边车部署把数据统一上报再在 Logstash 里面做集中解析这样维护起来也更方便。如果你所在公司的日志规模已经很大日均上亿条那还要在 Filebeat 和 Logstash 之间加一层 Kafka 做削峰填谷。我这次先没有加 Kafka因为业务量还用不到等后续日志量上来之后加一层就行了架构上已经预留了 Kafka 的接入位置。用 Kafka 缓冲的意义在于如果 ES 集群压力大或者短暂不可用日志不会直接丢失Filebeat 会先把数据发到 Kafka等 ES 恢复后 Logstash 再消费补写。2. 核心组件部署与关键配置2.1 Elasticsearch 安装与初始化调优Elasticsearch 是整个 ELK 体系中最核心的组件它的稳定程度直接决定了日志系统的可用性。部署这一层的时候我踩过不少坑这里把关键点都列出来。安装方式上我用的是二进制包手动部署的方式。虽然用 Docker 也可以但考虑到日志系统要长期固定运行手动部署在系统调优上更直观可控。如果你租用的云主机内存不大8G 以下建议把 ES 单独部署不要让其他服务占用太多内存。如果条件允许ES 集群至少三台节点组成这样在数据量上来后才有故障转移的余地。安装完第一件事是修改配置文件elasticsearch.yml有几个参数是必须调整的。第一个是cluster.name同一个集群的所有节点必须配置相同的集群名而且集群名不能和其他环境混淆不然节点之间会互相尝试加入很危险。第二个是node.name每个节点的名称要唯一我习惯用node-1、node-2这种命名方式方便在报错时快速定位。第三个是network.host生产环境一定要设置为内网 IP 或者0.0.0.0但要注意设置后必须同步配置所有节点地址列表也就是discovery.seed_hosts参数不然节点之间发现不了彼此。JVM 堆内存设置也是必须认真处理的一项。ES 默认的堆内存大小经常不合适必须在jvm.options里显式设置。我的经验是堆内存设置为物理内存的一半但上限不要超过 31GB这是 ES 在压缩指针下的一个经典上限。比如服务器内存 16G就设置-Xms8g -Xmx8g。这里有一个新手容易犯的错误-Xms和-Xmx这两个值一定要设置成一样否则 JVM 在运行过程中动态调整堆大小会造成不必要的停顿。同时要给操作系统留一部分内存做文件缓存因为 ES 的查询性能很大程度上依赖操作系统的 page cache。还有一个关键的系统参数是vm.max_map_count如果不调整ES 启动后大概率会报max virtual memory areas vm.max_map_count [65530] is too low的错误。执行以下命令即可解决sysctl -w vm.max_map_count262144 echo vm.max_map_count262144 /etc/sysctl.conf另外在启动 ES 前需要创建专用的运行用户因为 ES 出于安全考虑不允许用 root 用户直接启动。这块容易忽略我一开始就是直接 root 跑的后来统一改成了专门的用户顺便把数据目录和日志目录的所有权一起处理了。2.2 Logstash 管道配置从输入到输出的完整流程Logstash 的核心配置思想是管道pipeline用三个阶段处理数据输入input、过滤filter、输出output。配置结构非常清晰各个阶段各管一段读写起来都很直观。我当时的配置大概是这样的input { beats { port 5044 } } filter { if [fields][log_type] app { json { source message target app_log } mutate { add_field { service_name %{[fields][service]} } } date { match [[app_log][timestamp], ISO8601] target timestamp } mutate { remove_field [app_log, message] } } } output { elasticsearch { hosts [http://es-node-1:9200, http://es-node-2:9200] index app-logs-%{YYYY.MM.dd} } }这段配置的意思是Logstash 监听 5044 端口接收 Filebeat 上报的数据如果日志的类型是应用日志就尝试把 message 字段里的内容按 JSON 解析然后提取出应用名称和时间戳写入 ES 的时候按天建立索引索引名称形如app-logs-2025.06.11。需要注意的细节是date这个插件。很多人刚接触 Logstash 时会忽略它结果发现日志上报到 Kibana 后时间和实际差了 8 个小时这就是因为没有用日志里的业务时间覆盖timestamp。默认情况下timestamp是 Logstash 处理消息的时间如果日志产生时间和处理时间跨了时区或者有延迟统计就会失真。用date插件解析日志中的时间戳并覆盖timestamp是整个链路里很重要的一步。2.3 Kibana 与 Filebeat 的部署要点Kibana 部署相对简单本质上就是一个 Node.js 应用配置好 ES 地址后启动即可。唯一需要注意的就是访问控制和反向代理设置。我建议在 Kibana 前面加一层 Nginx 反向代理用域名访问并配上 HTTPS 和基础认证不要让 Kibana 裸奔在公网。原因很简单Kibana 能查询到的日志数据包含大量业务信息不加认证直接暴露的话一旦被扫描到就相当于把业务日志给了别人。加上 basic auth 其实很便宜成本就是配置几行 Nginx 的事情。Filebeat 这边我部署在每个业务服务器上采集固定的日志目录。核心配置是filebeat.inputs: - type: filestream enabled: true paths: - /data/logs/*.log fields: log_type: app service: order-service fields_under_root: false output.logstash: hosts: [logstash-node-1:5044]Filebeat 配置里的fields字段很有用它可以给每条日志打上自定义标签比如服务名称、日志类型这样在 Logstash 解析的时候就能根据这些标签做不同的处理。比如所有服务的日志可以都采集到同一个 Logstash 入口但 Logstash 拿到后按log_type字段区分是应用日志还是中间件日志走不同的处理逻辑。一个很多人忽略的配置是multiline规则。应用在打印异常堆栈时日志会跨多行如果 Filebeat 不做处理每行会变成一条独立日志导致一条异常被拆成几十条在 Kibana 里看起来非常混乱也没办法用一条日志的上下文查看完整堆栈。解决方法是配置multiline模式把不是以时间戳或特定关键字开头的行合并到上一条日志中multiline: type: pattern pattern: ^[0-9]{4}-[0-9]{2}-[0-9]{2} negate: true match: after这段配置的意思是新的一行如果是时间戳开头就作为新日志开始否则就续接到上一条日志后面。这样异常堆栈信息就能完整保留了。2.4 索引生命周期管理让日志数据自动化“瘦身”这个问题一开始确实坑了不少人。日志系统运行一段时间后每天一个索引索引越来越多磁盘空间被占满ES 集群状态变黄甚至变红。所以做日志系统一定要在做初期就把索引生命周期管理ILM设计好。Elasticsearch 的 ILM 机制提供了一种标准做法按日志时间建索引然后定义策略让索引随着时间推移自动经历热、温、冷、删四个阶段。比如可以这样设置索引创建后保留 7 天作为热索引每日滚动7 天之后如果存储压力大可以迁移到 warm 节点30 天之后进入 delete 阶段自动清理。策略配置大致如下{ policy: { phases: { hot: { actions: { rollover: { max_size: 50GB, max_age: 1d } } }, delete: { min_age: 30d, actions: { delete: {} } } } } }这里的逻辑是每天生成的索引最多保留 30 天超过 30 天自动删除。在创建索引模板的时候把index.lifecycle.name指向这个策略就完成了自动化管理。这样做最直接的价值就是不需要每天手动清理索引日志数据规模也能够被有效控制在一个合理的范围内而不用时刻担心磁盘告警。3. 日志采集链路与核心功能实现3.1 应用日志规范JSON 结构化是第一步很多日志系统做不好的根源在于源头日志本身就是一团乱麻。应用日志如果只是简单地用log.info(user userId pay failed)这种形式文本内容不具备结构后续在 ES 里做字段级检索、聚合分析就非常困难。引入 ELK 后最好的做法是让应用直接输出结构化日志最常用的是 JSON 格式。以 Java 服务为例我使用logstash-logback-encoder这个库配置好之后日志会以 JSON 格式输出到文件。这样每条日志天然带有timestamp、level、logger、message、traceId等字段Logstash 解析时不需要做额外的正则匹配性能和准确性都高很多。关键配置简化后是这样的appender nameJSON_FILE classch.qos.logback.core.rolling.RollingFileAppender encoder classnet.logstash.logback.encoder.LogstashEncoder includeMdctrue/includeMdc /encoder /appender对于 Python 服务同样有python-json-logger这个库可以把日志格式化成 JSON。核心思路是尽量把日志输出的 key-value 信息结构化而不是塞在一行字符串里。这会在日志解析阶段省下大量时间。有人可能会担心 JSON 格式日志的可读性面向开发环境的本地输出可以继续使用文本格式只在生产环境的文件输出中切换 JSON 格式。这样开发体验不受影响生产环境的日志解析能力也保证了。3.2 跨服务日志串联用 traceId 打通请求全链路分布式日志系统除了集中检索还有一层更重要的价值是链路追踪。一个请求从网关进来经过订单服务、库存服务、最后扣减积分如果每个服务各打印各的日志没有关联字段出了问题是没法串联起来看全貌的。业界标准的解决方案是引入 traceId链路追踪 ID。具体思路是这样的请求在入口处生成一个全局唯一的 traceId通过 HTTP 头或消息队列消息头一路向下传递所有服务在处理这个请求时打印的日志都带上这个 traceId。这样在 Kibana 里搜索这个 traceId就能看到整条链路在不同服务上的完整日志记录。实现方式上Java 生态中 Spring Cloud Sleuth 或者 Micrometer Tracing 可以自动完成 traceId 的生成和传递如果团队不想引入太重的组件也可以自己实现一个拦截器在 HTTP 请求进来时从 header 里取 traceId取不到就生成一个新的然后放进 MDC日志格式里自动带上。Python 服务对应的处理方式也类似使用请求中间件把 traceId 传给日志上下文。第一次在 Kibana 里搜索 traceId 看到一条完整请求链路时那种感觉确实很好。之前需要几个人协作对时间线排查的问题现在一个人一条查询就能扫出全部相关日志效率提升是几何级的。3.3 Logstash 日志解析与字段映射细节如果应用端严格按照 JSON 格式输出日志Logstash 的解析难度会大大降低。但现实是一个团队里总有老的系统、没有改造成 JSON 格式的第三方组件、以及各种中间件日志这些日志还是纯文本格式。这时候 Logstash 的grok插件就派上用场了。grok的作用是把非结构化的文本通过正则表达式拆解成结构化字段。比如一条 Nginx 访问日志192.168.1.10 - - [11/Jun/2025:14:23:45 0800] GET /api/order HTTP/1.1 200 532可以用这个 pattern 解析filter { grok { match { message %{IPORHOST:client_ip} - - \[%{HTTPDATE:request_time}\] \%{WORD:method} %{URIPATHPARAM:request_uri} HTTP/%{NUMBER:http_version}\ %{NUMBER:status} %{NUMBER:body_bytes} } } }解析完之后日志就有了client_ip、method、request_uri、status这些独立的字段就可以在 Kibana 里按状态码聚合、按客户端 IP 分组分析了。grok内置了很多常用模式像 IP、URL、时间戳等大部分场景直接用内置模式就可以了。如果是特别复杂的日志格式也可以先用grok debugger工具调试 pattern确保能正确匹配后再上线避免线上改了配置不生效还不知道问题在哪。mutate插件我也经常用它的能力包括重命名字段、删除和转换数据类型。比如从 Nginx 日志里解析出来的status是字符串类型要转成整数类型才能在 Kibana 里做数值聚合。这种用一行配置就能解决。在实际运维时建议不要一味追求解析特别复杂的业务日志。把公用的、紧要的字段解析出来就够了比如时间、级别、服务名、traceId、状态码其余内容保留在message字段里供全文检索这样既能满足绝大多数排查需求又能把 Logstash 过滤器的维护成本控制在合理范围。3.4 Kibana 可视化与检索技巧数据进到 Elasticsearch 后Kibana 就是日常使用的主阵地了。Kibana 的 Discover 页面允许你通过 KQLKubernetes Query Language语法做快速检索这个语法学起来非常快但掌握几个常用场景就能覆盖绝大多数查询需求。比如查某个服务最近 15 分钟的 ERROR 日志直接用service_name : order-service and level : ERROR and timestamp now-15m想查某个 traceId 对应的全部日志直接搜traceId : abc123。如果你记得日志里某个关键字但是不确定它在哪个字段里Kibana 也支持全文检索直接输入关键字就会把所有字段中匹配的内容都查出来。这个在实际排查时非常顺手不用懂太多语法就可以开始工作。Dashboard 是另一个很有用的功能可以把多个可视化图表拼接在一个页面上做成团队维度或服务维度的总览。我搭建了一个比较简单的运维看板左上角是按服务统计的日志量柱状图右上角是 ERROR 日志趋势曲线下方是最近报错的服务排行外加若干个常用查询的快捷入口。这样每天早上看一眼整体服务的健康状态就心里有数了。如果还想要更主动的告警能力可以配合 Watcher 或 ElastAlert 实现基于日志内容的告警。比如当 error 级别的日志出现频率超过某个阈值时触发钉钉或邮件告警。这部分是可选项但确实能让日志系统从一个被动的“排查工具”向前迈一步变成一个主动的“发现工具”。4. 常见问题与排查经验实录4.1 日志量大导致 ES 频繁写入性能下降日志系统上线后随着接入的服务增多日志量会快速增长。最直接的表现是 ES 的 CPU 和 IO 持续飙升查询响应变慢甚至出现 bulk 写入拒绝的情况。出现这类问题时优先检查这几个地方索引分片数是否合理。分片数不是越多越好每个分片都有内存和 CPU 开销大量的小分片反而会导致性能下降。一般每个分片控制在 30GB 以下是比较常见的参考值。副本数是否设置合理。副本太多会成倍增加写入压力如果数据不是特别重要日志索引的副本数可以设置为 0 或 1。刷新间隔是否过短。日志写入场景对查询延时要求不高可以把index.refresh_interval从默认的 1s 改大为 30s这样能明显降低写入时的开销。实际操作上我们通过日志采集量统计把一小部分低价值日志比如访问日志、健康检查日志单独分流到短周期的索引保留时间短一些高价值的业务日志和错误日志则保留更长时间这样在控制总体存储和性能之间找到了一个平衡。4.2 日志时间与本地时间相差 8 小时这是我最初遇到的经典问题。Kibana 上看到的日志时间比实际业务时间慢了 8 个小时排查了大半天最后发现是时区配置的问题。产生原因有两层第一层是 Elasticsearch 存储的timestamp默认是 UTC 时区第二层是 Kibana 展示时需要指定正确的时区否则会按浏览器本地时区或者默认的 UTC 时区来显示。解决办法也很简单在 Kibana 的 Advanced Settings 里把timezone设置为本地时区比如Asia/Shanghai这样展示就会自动转换时区。如果 Logstash 解析字段时用的是date插件还特别需要注意解析出来的时间戳是否偏离预期。因为date插件默认把解析到的值作为 UTC 处理如果你的日志里写的是2025-06-11 14:23:45但这条日志实际是北京时间下午两点产生的那么 Logstash 会把它当作 UTC 14:23:45 来存储这就差了 8 小时。解决方法是给date插件加上timezone参数指定日志原始的时区。这类问题虽然看着小但在排查跨天类问题时特别容易让人困惑值得提前就处理好。4.3 ES 集群状态变黄甚至变红ES 集群状态是判断系统健康的晴雨表。green表示所有主分片和副本分片都正常分配yellow表示主分片正常但副本分片没有分配red表示有主分片未分配数据出现丢失风险。遇到yellow状态最常见的原因是磁盘空间不足导致 ES 自动将副本分片迁移到其他节点但其他节点也没有空间容纳或者节点数量不满足副本分布条件。处理方法是先检查各节点的磁盘使用率删掉旧的日志索引或者直接扩容磁盘让空间释放出来然后 ES 会自动重新分配副本分片。遇到red状态意味着丢失了主分片这时要优先去定位是哪个节点掉线了查看 ES 日志确认原因。如果掉线节点能恢复集群会自动恢复如果不能恢复就需要用_recoveryAPI 或快照恢复数据。这里也印证了为什么索引生命周期管理要提前做好磁盘空间问题是red状态的头号原因大部分情况都是日志把磁盘塞满了。4.4 Filebeat 与 Logstash 数据重复或乱序在日志量大的情况下Filebeat 在发送数据到 Logstash 时可能会出现因网络抖动或 Logstash 处理慢导致的数据重复。原因在于 Filebeat 的至少一次语义如果 Logstash 处理成功后返回确认帧时网络延迟或丢失Filebeat 就会重新发送数据造成重复。解决重复的通用思路是让 ES 写入端做幂等处理。最简单的做法是在 Logstash 的 output 配置里指定document_id用日志的唯一字段组合作为 ID。比如用文件路径加偏移量生成一个稳定的 ID这样 ES 在写入时如果发现同样 ID 的文档已存在就直接覆盖不会重复output { elasticsearch { hosts [http://es-node-1:9200] index app-logs-%{YYYY.MM.dd} document_id %{[agent][id]}-%{[log][offset]} } }乱序问题本质上和日志产生的时间与处理时间之间的差异有关解决思路是在 Logstash 中解析日志时间并覆盖timestamp然后查询时优先按业务时间字段排序这样基本可以避免因为乱序带来的困扰。4.5 日志内容里的特殊字符导致 JSON 解析报错接入的日志来源复杂会遇到一些日志里包含转义符或者非法 JSON 字符的情况比如\n没有被正确转义导致 JSON 解析失败日志被 Logstash 丢弃或无法写入索引。这类问题最容易出现在第三方 SDK 打印的日志中。解决思路是在 Logstash 的json插件解析失败时走一个兜底分支把原始 message 原样写入一个单独的索引并打上解析失败标记。这样既不会丢日志也能单独审视哪些日志格式不合法反过来推动业务侧调整日志格式filter { json { source message target parsed_json tag_on_failure [_json_parse_failure] } if _json_parse_failure not in [tags] { # 正常处理 } else { mutate { add_field { parse_error true } } } }这个配置的意义在于日志系统本身不应当因为解析失败就吞掉原始数据保留原始内容始终是第一原则解析只是增值。5. 运维心得与后续扩展建议做这个分布式日志系统说难也难难在涉及组件多、链路长、排错时要跨节点说简单也简单因为每个组件单独看都有成熟的文档和大量的社区实践可以参考。真正有价值的是一些在踩坑中总结出来的体会。第一个体会是日志系统的价值不在于“把日志集中了”而在于“把日志结构化并建立关联”。如果只是把文本格式的日志一股脑塞进 ES那 ES 只是一个昂贵的日志存放仓库并没有提高多少排查效率。只有把时间、级别、服务名、traceId 这些字段拆解出来让日志之间产生关联关系这个系统才真正开始发挥威力。第二个体会是架构不要一开始就铺得太大但扩展点要留好。我们最初部署的时候没有引入 Kafka架构很简单Filebeat 直接到 Logstash 再到 ES链路短、故障点少。但设计的时候就把 Kafka 预留在了架构图里后续日志量上来之后在 Filebeat 和 Logstash 之间插入 Kafka改动很小应用层完全无感。这样既保证了快速落地又避免了后续重写架构的麻烦。第三个体会是日志系统的运维要尽量自动化。索引生命周期管理、磁盘监控、每日日志量统计这些都要在前期就配置好。如果一个日志系统还需要人工每天检查磁盘剩余空间那这个系统在运维层面是不合格的。我后来给索引清理、集群健康检查都加了定时任务和告警基本实现了无人值守。如果你正准备动手做分布式日志系统我最想给你的建议是先想清楚你的核心需求是什么。如果只是集中检索那就用最小化的架构Filebeat Logstash ES Kibana 足够如果要做链路追踪那要聚焦在 traceId 的规范制定和传递上如果还要做告警和数据分析再考虑加相应的扩展组件。不要一上来就把 Kafka、ClickHouse、告警平台全拉进来系统复杂度会增加但价值并不一定同步放大。最后再分享一个小技巧在 Kibana 里把几个常用的排错视图保存下来比如“最近1小时所有服务 ERROR 日志”“某个重点服务的超时日志”这样团队新同学在排查问题时不需要从头学查询语法直接打开保存的视图就能开始干活。日志系统的最终目的不是成为少数人才能使用的工具而是让整个团队都能借助它快速定位问题这个收尾虽然不是技术上最复杂的部分却是使用体验上非常关键的一环。
返回列表