你可能也有过这样的经历线上服务一出问题第一反应就是登录服务器一层层翻日志文件grep、tail -f、awk几个终端来回切换从凌晨一直翻到天亮才定位到一个报错。我在早期管理系统时就是这种状态直到把 ELKElasticsearch Logstash Kibana整套搭起来以后才真正从“被动翻日志”变成了“主动查日志”。这篇文章不是抄官方文档而是我完整搭建 ELK 日志平台之后的项目复盘包含架构选型、组件搭配、落地配置、常见坑位和调优经验。不管你是刚开始接触日志系统还是已经在用 Elasticsearch 但没玩顺 Logstash 和 Kibana这篇都能给你一些可以直接抄作业的参考。1. ELK 架构到底在解决什么问题1.1 日志系统的演进从单机 grep 到集中式检索不说概念先讲一个真实的场景。早几年某公司有个模块需要临时排查支付回调的问题我登录到两台应用服务器分别跑了两个小时的grep callback /var/log/app/error.log | more。结果呢请求跨多个节点日志散落在不同文件甚至不同机器上我只能一边翻一边拼时间戳效率非常低。后来我意识到一个问题日志本身不是目的从日志里快速找到线索才是。而分布式环境下日志必然分散如果不能让日志汇聚到同一个地方那后续所有排查动作都等于在地里找针。ELK 的核心价值恰恰就在这里Logstash或更轻量的 Beats负责把不同来源的日志收拢并统一处理Elasticsearch 负责存储和索引Kibana 在之上提供检索和可视化界面。这个链路解决的不只是“日志集中”这一个问题还包括三个深层诉求统一的检索入口不再 SSH 到每台机器敲命令只需要在 Kibana 搜索框里输入关键字秒级返回结果。字段化解析原始日志是一行文本经过 Logstash 拆解后变成含timestamp、level、service、trace_id等字段的结构化数据后续聚合统计才有条件。可视化与告警把日志量、异常占比、响应耗时等指标做在仪表盘里出问题不必等用户反馈自己先能看到趋势变化。1.2 ELK 的三层职责划分很多人一上来就纠结“到底要不要上 ELK”但其实你先要明白这套体系里各个组件的角色边界Elasticsearch整个系统的存储与检索引擎。它本身是一个分布式搜索服务数据按分片存储在多个节点上对外提供 RESTful API。日志平台对它的核心要求是写入吞吐和查询响应能力这两者之间要平衡。Logstash数据管道承担采集、解析、过滤、转发的职责。它把原始日志变成结构化事件然后送进 Elasticsearch。功能最全但资源消耗也最明显所以现在很多场景下会用 Filebeat 替代它的采集职责只保留 Logstash 做中转和加工。Kibana可视化前端。通过浏览器访问连接 Elasticsearch 之后可以创建索引模式Index Pattern、执行查询语句、构建仪表盘还能设置告警规则。标准架构里还会引入 Filebeat 作为轻量级采集器部署在每个业务节点上读文件、转发给 Logstash再由 Logstash 做统一处理。这个“Filebeat Logstash Elasticsearch Kibana”的组合在资源占用上要比“每台机器装 Logstash”流畅得多。2. 组件选型与版本搭配2.1 为什么 Logstash 不是唯一选择搭建 ELK 时最容易踩的坑就是迷信“全家桶”然后把 Logstash 装在每一台业务服务器上。Logstash 是 JVM 应用默认配置下内存占用轻松超过 1GB哪怕你只是做一个空管道光 JVM 基础开销就摆在那。在一台 4GB 内存的业务机上再跑一个 Logstash业务服务的资源空间会受到明显挤压。所以现在主流做法是引入 Filebeat。Filebeat 是一个 Go 编写的轻量级日志采集器默认内存占用只有几十 MB。它负责读取日志文件、跟踪文件偏移量避免重复发送或漏发、将数据输出到 Logstash 或 Elasticsearch。你可以把 Filebeat 理解为“搬运工”把 Logstash 理解为“加工车间”。搬运工可以铺设在每台机器上加工车间集中部署一套就够了。当然Logstash 依然有价值。Filebeat 在本地能做的处理非常简单比如简单地重命名字段、删除字段如果涉及复杂的正则解析、数值转换、多行日志合并、地理 IP 解析等操作还是需要 Logstash 的 filter 插件体系。我实际采用的方案是业务节点只装 Filebeat日志先送到中央 Logstash 集群由 Logstash 统一按业务类型拆分管道处理最后写入 Elasticsearch。2.2 版本兼容性这个隐形坑聊到选型就必须说版本。ELK 三个组件是同步发版的但很多人图省事直接各自拉最新的结果装上之后 Kibana 显示版本不兼容或者 Logstash 写入 Elasticsearch 时映射异常。我吃过这个亏有一回 Elasticsearch 升到了 8.xLogstash 还在 7.x运行过程中一直报content-type相关的错误最后被迫同步升级所有组件白白折腾了一个晚上。所以我的建议是所有组件使用同一个大版本号比如统一用 7.17.x 或 8.x。尽量不追最新小版本等社区跑一段时间再升级。日志平台最怕不稳定追新带来的收益远小于风险。要特别留意 7.x 到 8.x 的跳跃。8.x 默认启用安全认证HTTP 端口默认是 9200但内置的账号密码机制与 7.x 完全不同初次部署需要先初始化账号。如果只是内部日志系统7.17 系列其实已经非常成熟且稳定文档也多遇到问题基本能直接搜到答案。8.x 功能更新但需要额外管理 API Key 和 SSL 证书学习成本略高。3. 部署实操与环境搭建3.1 环境准备与基础配置本人在部署时选择三台 Linux 服务器组成一个 Elasticsearch 集群配置均为 8 核 16GB 内存磁盘 500GB。Logstash 和 Kibana 分别部署在同一台独立机器上。下面这套参数是验证过稳定运行半年以上的配置你可以结合自己机器的规格做调整。部署 Elasticsearch 时最先要改的是 JVM 堆内存。默认配置是 1GB对生产环境来说完全不够。修改config/jvm.options中的-Xms和-Xmx两者设为同样的值防止运行期动态扩容引发停顿。一般建议堆内存设置为机器物理内存的一半左右不要超过 31GB否则会触发压缩指针关闭导致内存浪费。例如 16GB 内存的机器堆设 8GB——多给 Lucene 留一些堆外内存做文件缓存性能反而更好。elasticsearch.yml里最基础但最重要的配置如下cluster.name: es-production node.name: es-node-01 path.data: /data/es/data path.logs: /data/es/logs network.host: 0.0.0.0 http.port: 9200 discovery.seed_hosts: [es-node-01, es-node-02, es-node-03] cluster.initial_master_nodes: [es-node-01]生产环境必须显式配置discovery.seed_hosts否则节点之间无法互相发现整个集群会一直处于红色不可用状态。我当时第一次部署时跳过了这个配置只在单机上跑后来加第二个节点时才发现集群根本无法组建折腾了几小时才反应过来。启动之后用curl http://localhost:9200/_cluster/health?pretty查看集群状态。状态为green代表所有主分片和副本分片都分配成功yellow代表主分片已分配但副本缺失red代表存在未分配的主分片这是最需要关注的状态。3.2 Logstash 流水线配置要点Logstash 本身不需要集群模式它是一个单纯的数据管道实例。配置文件主要分输入、过滤、输出三部分。下面是一个我用于处理 Nginx 访问日志的完整配置骨架input { beats { port 5044 } } filter { grok { match { message %{IPORHOST:client_ip} - - \[%{HTTPDATE:timestamp}\] %{WORD:method} %{URIPATHPARAM:request} %{INT:status} %{INT:body_bytes_sent} %{QS:http_referer} %{QS:http_user_agent} } } date { match [timestamp, dd/MMM/yyyy:HH:mm:ss Z] target timestamp } mutate { remove_field [timestamp, path] } } output { elasticsearch { hosts [http://es-node-01:9200, http://es-node-02:9200, http://es-node-03:9200] index nginx-access-%{YYYY.MM.dd} manage_template false } }grok是 Logstash 里最核心的解析能力。本质上是把一串正则表达式命名成有意义的字段比如%{IPORHOST:client_ip}把匹配到的 IP 片段映射为client_ip字段。很多新手喜欢自己写正则实际上 Logstash 内置了上百种模式大部分场景直接组合即可。date插件的作用是把日志文本里的时间戳转成 Elasticsearch 里的timestamp标准格式。很多人不写这一步结果日志时间跟采集机器的系统时间混在一起Kibana 里的时间线完全错乱。日志时间必须优先使用业务日志自带的时间戳而不是发送端的本地时间。mutate插件用来做字段清理。原始日志解析之后往往会多出一些不需要的临时字段不影响搜索但会占用存储空间。像上面把timestamp和path这种字段删掉可以让索引更干净。output的index命名非常关键。建议始终按日期建索引比如nginx-access-2025.06.01。这样后续做索引删除、性能优化、归档都会方便很多——想删掉哪一天的数据直接删掉对应的索引即可。3.3 Kibana 配置与基础串联Kibana 的配置相对简单修改config/kibana.ymlserver.port: 5601 server.host: 0.0.0.0 elasticsearch.hosts: [http://es-node-01:9200, http://es-node-02:9200, http://es-node-03:9200] kibana.index: .kibana启动之后访问http://服务器IP:5601。第一次进入需要创建 Index Pattern比如输入nginx-access-*。注意这里的通配符会把所有以nginx-access-开头的索引都纳入同一个检索范围。Kibana 默认使用timestamp字段作为时间过滤字段所以前面 Logstash 里用date插件规整时间戳在这里就会体现价值。到这里一套最小可用的 ELK 平台已经跑通了Filebeat 采集文件、Logstash 解析转发、Elasticsearch 存储索引、Kibana 展示查询。接下来就是真正把日志数据用起来的过程。4. 核心功能实操从采集到可视化4.1 日志采集与字段解析实战Filebeat 的配置在filebeat.yml里下面是一个采集多个应用日志的示例filebeat.inputs: - type: filestream enabled: true paths: - /var/log/nginx/access.log fields: log_type: nginx_access fields_under_root: true - type: filestream enabled: true paths: - /var/log/app/*.log fields: log_type: app fields_under_root: true output.logstash: hosts: [logstash-host:5044]这里有两个值得注意的地方。一是fields和fields_under_root。给日志手动打上log_type标签后在 Logstash 里就可以按这个字段分流到不同的处理管道或者在 Kibana 里用这个字段快速筛选业务系统。二是这里用的类型是filestream。Filebeat 7.x 之后的推荐输入类型相比旧的log类型它对文件轮转和偏移量跟踪的处理更加稳健不会因为日志文件被切割导致重复采集或漏采集。Logstash 端要同时应对多种输入时可以用条件判断拆分处理input { beats { port 5044 } } filter { if [log_type] nginx_access { grok { match { message ... } } } else if [log_type] app { json { source message } date { match [log_time, yyyy-MM-dd HH:mm:ss] target timestamp } } } output { elasticsearch { hosts [http://es-node:9200] index %{log_type}-%{yyyy.MM.dd} } }业务日志如果是 JSON 格式用json插件解析是最省事的。前提是业务方在写日志时就要输出统一结构的 JSON包括时间、级别、类名、消息内容、trace_id。我做日志平台时推动业务侧改造日志格式开始阻力挺大但上线后大家发现排查效率明显提升了之后再有新服务接入基本都会主动遵守规范。解析 JSON 日志时还需要注意一个细节如果原始message里含有异常堆栈信息通常是一整段多行文本JSON 解析插件会把整段内容当成一个字符串字段存下来。此时建议保留为原始字段方便后续直接搜索堆栈关键词。不要尝试强行拆解堆栈行不仅拆不干净还有额外的性能开销。4.2 Kibana 可视化与仪表盘设计Kibana 里最常用到的功能模块是 Discover探索、Visualize可视化和 Dashboard仪表盘。在 Discover 页面你可以用 KQLKibana Query Language做快速筛选。比如response_time 3000 AND status 500这条查询可以一次筛选出响应时间超过 3 秒且状态码为 5xx 的请求。KQL 的语法非常简单等于是在原来的日志检索体验上加了一层结构化查询能力。以前我要 grep 出一个报错再 p 上去看上下文现在直接拖拽时间范围、输入几个字段条件结果就已经粒度到单个请求了。做仪表盘时我的经验是先想清楚“谁在看”再决定展示什么指标。如果拿给运维看重点是可用性指标比如各节点日志量、错误级别日志占比、JVM 异常出现的频率如果拿给研发看重点是应用自身的请求量、接口响应耗时分布、错误堆栈 Top 榜。不同的观看者仪表盘的需求完全不同。以一个订单服务的应用监控仪表盘为例我通常会配置这样几张图日志量趋势图柱状图按小时统计日志条数观察是否有突发流量或采集断流。错误级别占比图饼图统计level: ERROR所占比例超过阈值可触发告警。耗时区间分布图数据表按response_time字段分桶看 P50、P95、P99 的耗时分布。异常关键字 Top 榜数据表对message字段做关键词聚合看这段时间内出现频率最高的异常类型。可视化的底层原理是 Elasticsearch 的聚合查询Aggregation。简单理解就是 SQL 里的GROUP BY只不过它是在倒排索引上执行。对于日志类的时序数据分析这种聚合查询的速度非常快百亿级数据量下秒级响应也没什么压力。5. 常见问题与排查实录5.1 集群状态异常与磁盘水位生产环境遇到最多的就是 Elasticsearch 集群状态从green变成yellow甚至red。yellow状态最常见的原因是磁盘空间不足。ES 默认当磁盘使用率超过 85% 时不再分配新分片超过 90% 时会尝试迁移分片到其他节点超过 95% 时强制将索引块设置为只读。这是一个保护机制防止磁盘写满之后整个节点崩溃。此时你会看到原本健康的集群突然无法写入新数据Kibana 上所有图表都停了。处理方式分两步解决磁盘空间问题。清理过期索引或扩容磁盘。删除 ELK 中过期索引时建议手动删除而非在 Kibana 里操作。比如在生产环境关闭某个索引的写入后通过curl -X DELETE http://es-node:9200/nginx-access-2025.03.01删除即可。如果集群状态变成了red说明有主分片没有分配。优先查看未分配分片的原因curl http://es-node:9200/_cluster/allocation/explain?pretty返回信息里会直接给出原因比如磁盘空间不足、分片数据损坏、节点失联。根据原因再处理等待节点恢复或删除损坏索引并重新采集数据。磁盘水位线还有一个容易忽略的问题如果你配置了多个数据目录ES 会判断所有数据目录的总空间还是每个目录的单独空间答案是它判断路径中“拥有最多可用空间的目录”而不是整体空间。所以即使总磁盘还有 200GB只要某个目录磁盘已满导致该路径不可写ES 同样会触发只读。5.2 日志采集延迟或丢失Filebeat 已经采集到文件内容但 Kibana 迟迟查不到数据这是另一个高频问题。排查顺序如下确认 Filebeat 进程是否存活ps -ef | grep filebeat查看 Filebeat 日志tail -f /var/log/filebeat/filebeat.log它是否一直在报连接 Logstash 失败在 Logstash 管道配置中打开 stdout 输出直接在控制台看数据是否到达。output { stdout { codec rubydebug } }把 stdout 临时加在 elasticsearch 输出之前能非常直观地看到 Logstash 收到的原始事件和处理后的字段。确认管道没问题之后再检查 Elasticsearch 侧的写入情况。日志延迟还有一种常见原因Logstash 的pipeline.batch.size与pipeline.workers配置不合适。如果单条日志的耗时偏高比如解析包含超复杂正则或大量文本字段而 worker 数量太少就会积压数据。可以适当提高这两个参数pipeline.workers: 4 pipeline.batch.size: 500 pipeline.batch.delay: 50要注意不是越高越好。Worker 数量增加会带来更多的线程上下文切换和内存开销批次大小增大会提高单次写入量但也会占用更多堆内存。我一般是从默认值开始根据监控数据逐步调整而不是一次拉满。5.3 索引生命周期管理与存储优化日志数据最大的特点是只增不减随着时间推移存储压力会越来越大。如果不对索引进生命周期管理半年后你会发现磁盘被占满集群性能骤降。Elasticsearch 提供 ILMIndex Lifecycle Management机制可以自动完成索引从创建到删除的全过程。一个最简单的策略是新索引hot接收写入副本 1 份保留最近 7 天的数据。旧索引warm将副本降为 1强制合并段文件减少存储占用。更旧的索引cold转移到冷节点或关闭搜索功能降低资源开销。过期索引delete超过保留期自动删除。在 Kibana 的 Stack Management 中可以配置 ILM Policy在 Logstash 输出端只需要给 Elasticsearch 输出加上ilm_enabled true和ilm_policy log-policy参数就行。这个机制非常省心唯一麻烦的是初始配置需要理解hot、warm、cold、delete这几个阶段的意义。建议达成目标至少用上 hot 与 delete 两段就可以把“自动删旧数据”的长期运维成本降下来。存储优化方面再分享一个经验对于message这类大文本字段尽量不要开启fielddata对于不需要分词的字段比如status、log_type这类精确值用keyword类型即可避免默认把每个字符串都做全文分词白白浪费索引空间。如果日志量特别大还可以在模板里指定index.mapping.total_fields.limit防止字段数无限膨胀把 mapping 表撑爆。6. 性能调优与资源规划6.1 写入性能调优日志场景与业务搜索场景的负载模型完全不同——写入是主要矛盾。调优时首先要关注三个指标写入吞吐量每秒能写入多少条日志。索引延迟从日志被采集到能够被搜索到间隔多长时间。分片数量一个索引被拆成多少个分片。分片数是最容易被忽略的配置。刚开始搭建 ELK 时我习惯按业务模块建索引然后默认为每个索引配置 5 个分片。结果日志量上来后集群里散落着几百个分片每个分片又有副本严重拖慢了节点之间的协调速度。后来我把索引模板统一调整为小日志量索引 1 个主分片 1 个副本大日志量索引 3 个主分片 1 个副本。为什么这么做分片数量越多单个分片的体积越小查询时并行度越高。但分片越多每个分片都需要维护自己的元信息节点间的通信成本也会上升。分片数量需要根据单索引数据量设计单个分片容量控制在 30GB 到 50GB 左右比较合理。如果一天日志量是 50GB那么每个索引 2 到 3 个分片就够用了。与写入性能密切相关的另一个参数是刷新间隔。ES 默认每秒刷新一次也就是说写入的数据最多 1 秒后就可以被检索到。日志平台其实对实时性要求没那么苛刻可以把刷新间隔调大到 30 秒显著减少创建小段文件的频率降低磁盘 IO 压力curl -X PUT http://es-node:9200/nginx-access/_settings -H Content-Type: application/json -d { index.refresh_interval: 30s }这个调优非常适合按天建索引的日志场景。数据还没有进入热查询阶段时30 秒延迟对排查问题完全没影响但写入性能提升明显。6.2 查询性能与索引模板设计查询侧的性能瓶颈通常来自查询字段没有设计好。ES 默认会对字符串类型的字段同时生成text和keyword两种类型。text用于全文检索支持分词匹配keyword用于精确匹配和聚合。比如message字段天然适合text而status、log_type这种枚举值字段则只保留keyword即可。如果你在 mapping 里没有主动声明ES 会根据首次写入的数据自动推断字段类型。这有时是灾难同一个字段在不同时间写入的数据格式不一致会出现类型冲突。我建议在搭建初期就为索引指定明确的模板Index Template统一字段类型和分片配置{ index_patterns: [*-logs-*], template: { settings: { number_of_shards: 3, number_of_replicas: 1, refresh_interval: 30s }, mappings: { properties: { message: { type: text }, level: { type: keyword }, service: { type: keyword }, timestamp: { type: date } } } } }这样凡是符合*-logs-*命名规则的索引在创建时都会自动套用这套配置。以后新增服务接入只要索引命名规范所有 setting 都会自动生效不需要每接入一个日志源就重新设置一遍。查询侧还有一个容易被忽视的问题Kibana 里的时间过滤器是基于timestamp字段的。如果一个索引里的文档没有正确的timestamp那这个文档在 Kibana 默认时间段内是看不到的。排查时如果总觉得“数据丢了一部分”先确认timestamp字段是否存在且可用。6.3 容量规划经验容量规划很难有一个固定公式因为它跟日志量、日志格式、副本数、保留周期都有关系。我给出一个供参考的计算思路假设单节点日均接收 50GB 日志按天建索引保留 30 天副本数为 1。那么实际存储需求为50GB × 30 天 × 2含副本 3000GB这 3000GB 是原始数据量的存储需求但这不是全部。ES 索引数据通常会因为倒排索引、字段存储、段合并等原因膨胀到原始数据量的 1.1 到 1.5 倍。所以保守估算集群总磁盘应该在 4500GB 左右。如果只有三台 8 核 16GB 配置的机器每台挂 1.5TB 磁盘是比较稳妥的选择。在这个计算基础上还要考虑分片的分布。三节点集群如果每个索引 3 个主分片 1 个副本那么全量分片数就是 6 个正好可以均匀分布在 3 个节点上。如果机器只有两台副本分片数量反而会拖累集群的写可用性——因为 Elasticsearch 默认将主分片和它的副本放在不同节点只有 2 个节点时某一节点宕机某些主分片将无法分配集群直接变红。因此如果是双节点集群要么放弃副本要么加第三个节点没有更省的做法。7. 额外提速引入消息队列缓冲层日志系统一旦流量上来你会遇到一个问题业务高峰期写入速度超过 Elasticsearch 的吞吐时数据会积压在 Logstash 内部日志延迟越来越大。这时候可以在 Logstash 和 Elasticsearch 之间加一层 Kafka 作为缓冲。架构变成Filebeat - Logstash(采集/清洗) - Kafka - Logstash(消费/写入) - Elasticsearch - KibanaKafka 的好处是削峰填谷它允许 Elasticsearch 以自己的节奏消费数据而不是被峰值流量冲垮。引入 Kafka 后就算 Elasticsearch 集群在合并段或重启日志数据也不会丢失全部暂存在 Kafka 中等集群恢复后再消费。但这里要提醒一句Kafka 本身也是运维成本不低的东西又要部署 Zookeeper或 KRaft又要管理 Topic、分区、副本。如果日志量日均不超过 100GB或者峰值写入没有压垮过现有集群不建议一开始就上 Kafka。我的建议是先用纯 ELK 跑通等监控数据证明存在明显的写入积压时再考虑加缓冲层。这个次序调整能帮你省掉大量前期搭建复杂度。8. 写在部署完之后ELK 这套组合真正用起来之后给我最大的感受是排查问题的姿势完全变了。以前是“猜测 翻日志”现在直接在 Kibana 里筛选 trace_id从入口到出口把一条请求路径串起来看以前是用户反馈“你们系统好像有问题”现在看仪表盘上的错误率和耗时曲线就能提前发现异常。我实际部署中还会在 Kibana 里配置几个最基础的可视化页面一个是“全局概览”面向运维团队另一个是“服务详细”面向研发团队。全局概览展示存储使用、采集速率、集群健康状态服务详细则按业务模块统计日志量、错误数、响应时间分布。分角色来看各自都能在最短时间内定位自己关心的问题。ELK 本身并不是银弹它不会自动帮你分析日志也不会帮你判断问题根因它只是把“找日志”这件事变得足够快、足够结构化。而真正值钱的是在它之上沉淀出的排查流程和监控思路。如果你也在计划搭建自己的日志平台可以先从最小架构起步一台 ES 节点 Logstash Kibana Filebeat先把业务日志收上来、跑通检索再逐渐加索引生命周期管理、集群节点、消息队列缓冲层。每加一层都要把它背后的成本想清楚。最后分享一个小技巧在 Logstash 的配置里提前把业务系统的trace_id字段解析出来。很多人一开始没这个意识后来在分布式排查里被折磨得不轻——有了 trace_idKibana 里按这个字段一搜一堆日志可以按调用链串起来看。这个字段的解析在日志平台建设初期就适配好后面能少走很多弯路。
阅读完成 · 觉得有帮助?