
上个月我帮某公司调了一次日志平台几十台服务器的业务日志散落一地每次排障都靠逐台跳上去 grep。我们要把这些日志统一收上来、做清洗、索引、再可视化最后定下来的方案就是 ES Logstash Kibana 这套经典组合跑在 RHEL 8 上。这篇文章把整套部署和优化的路径整理出来覆盖从架构选型、环境准备、组件部署到性能调优、常见排障的完整过程。文章默认你至少有三台 RHEL 8 服务器每台 8G 内存、4 核、带独立数据盘。版本以 Elastic Stack 7.17 为例这是目前生产环境里最稳的 7.x 收官版本集群接口和配置习惯也完全适用于后续 8.x 的迁移。适合正在筹划日志平台的运维、后端和 SRE 同学参考。1. 项目概述与架构拆解1.1 核心思路ELK 集群解决什么问题为什么这样组合先想清楚一个前提日志平台的核心价值不是“把日志存起来”而是让日志能在故障发生时被快速找到、被聚合分析。日志分布在几十台机器上时格式五花八门检索只能靠 grep跨系统的调用链根本串不起来更不用说按状态码、按接口维度做统计。ELK 这套组合正好覆盖了日志处理的完整链路。Elasticsearch 负责存储和倒排索引是检索和聚合的引擎Kibana 负责可视化界面提供图表、仪表盘和数据探索入口Logstash 负责数据清洗和解析把非结构化的日志行变成结构化的字段Filebeat 则部署在业务服务器上轻量采集日志并上报。打个不严谨但好懂的比方Filebeat 是前厅服务员负责把各桌的菜收进厨房Logstash 是后厨备菜工负责洗菜切菜、按菜单分类Elasticsearch 是冷库加货架按编号放好随时可以按条件取Kibana 是餐厅的透明大屏顾客直接看到菜从哪来、卖了多少、哪道菜出了问题。这条链路里最容易被忽略的是 Logstash很多人图简单直接让 Filebeat 把数据推给 ES结果是日志里的 IP 是字符串、状态码是字符串、时间被当成普通字段后续聚合分析根本没法做。1.2 方案选型为什么三个节点为什么选 RHEL 8RHEL 8 在企业服务器里的占比不小生命周期长、软件仓库稳定、systemd 管理体系对服务托管友好。不过在 RHEL 8 上部署 ELK 有几个特有的坑默认开启的 firewalld、SELinux以及系统参数限制都会在你以为配置没问题的时候卡你一下。这些都是后文要重点处理的内容。集群规模怎么定取决于日志量和查询需求而不是越高越好。日增日志不超过 10GB 的场景单节点 ELK 完全够用日增几十 GB 以上或者需要多团队同时查询、聚合压力大的场景至少三节点起步。本文按三节点集群讲三台机器同时承担 master 和 data 角色虽然不算最极致的设计但对大多数中规模业务来说这个组合性能足够、成本可控、部署和维护都简单。等日志量继续涨到日增数百 GB再考虑把 master 和 data 角色拆开甚至引入消息队列缓冲。2. 环境准备与 RHEL 8 特有坑2.1 Java 版本与 dnf 依赖安装ES 7.17 跑在 Java 11 上。虽然 Elasticsearch 的 rpm 包自带了 JDK但建议在系统层面明确安装 java-11-openjdk避免后续 Kibana、Logstash 或其他脚本依赖系统 Java 时出现版本混乱。dnf install -y java-11-openjdk java -version接着安装核心组件。最可控的方式是直接下载对应版本的 rpm 包然后用 dnf 本地安装这样能避免配置官方 yum 仓库带来的源优先级问题也方便固定版本dnf install -y elasticsearch-7.17.*.rpm dnf install -y logstash-7.17.*.rpm dnf install -y kibana-7.17.*.rpm dnf install -y filebeat-7.17.*.rpm安装顺序无所谓但建议先装 ES因为后面所有组件都要连它。装完后先不要急着启动RHEL 8 上还有几道关卡要处理否则大概率会遇到各种诡异的启动失败或连接超时。2.2 firewalld、SELinux 与内核参数RHEL 8 默认开了 firewalldELK 涉及 9200、9300、5044、5601 四个主要端口。ES 节点间通信走 9300HTTP 查询走 9200Logstash 接收 Filebeat 数据走 5044Kibana 页面走 5601。防火墙放行命令如下firewall-cmd --zonepublic --add-port9200/tcp --permanent firewall-cmd --zonepublic --add-port9300/tcp --permanent firewall-cmd --zonepublic --add-port5044/tcp --permanent firewall-cmd --zonepublic --add-port5601/tcp --permanent firewall-cmd --reload除非三台机器在完全隔离的内网否则这四步别省。我见过有人折腾了两个小时最后发现只是防火墙没放行 5601Kibana 页面在浏览器里一直打不开。SELinux 是 RHEL 8 上最容易坑 ES 的地方。ES 启动时绑定端口或访问数据目录时如果 SELinux 策略不允许进程会被直接杀掉而日志里可能只有一段含糊的报错。排查思路是先看审计日志ausearch -m avc -ts recent如果发现有 elasticsearch 相关的 AVC 拒绝记录就要针对行为生成放行策略或者评估当前安全策略下是否允许 ES 监听自定义端口。正规做法是用 audit2allow 生成模块加载而不是直接 setenforce 0 永久关闭。当然如果是内部纯日志环境且安全评估允许临时关闭验证能帮你快速定位是不是 SELinux 在捣乱但生产环境我还是建议保留 SELinux 并配好策略。内核参数方面ES 默认要求vm.max_map_count至少 262144否则节点启动会直接报错。文件描述符限制也建议调大日志型应用并发连接多默认值很容易不够用。把下面内容写入/etc/sysctl.confvm.max_map_count262144 fs.file-max6553560执行sysctl -p生效。文件描述符限制还需要在/etc/security/limits.conf里给 elasticsearch 用户单独设置elasticsearch soft nofile 65535 elasticsearch hard nofile 65535 elasticsearch soft nproc 4096 elasticsearch hard nproc 40962.3 系统账户、目录布局与数据盘挂载生产环境最忌讳把 ES 数据目录放在系统盘日志索引的增长速度远比你想象的快系统盘满了会连带影响整机服务。建议给集群单独挂数据盘比如挂载到/data然后在/data下创建 ES 数据目录。ES 的 rpm 安装包会自动创建 elasticsearch 用户但它默认的数据目录在/var/lib/elasticsearch如果你把数据目录往/data挪必须手动创建并调整属主mkdir -p /data/elasticsearch chown -R elasticsearch:elasticsearch /data/elasticsearch同时把日志目录也单独安排避免原先系统日志和 ES 日志混在一起。目录布局确定后后面配置 elasticsearch.yml 时 path.data 和 path.logs 直接指向这两个位置即可。另外建议在主机的/etc/hosts里写清三台节点的 IP 和主机名映射ES 节点间发现依赖主机名解析如果 DNS 不给力集群状态会一直在红黄之间反复横跳。3. Elasticsearch 集群部署与调优3.1 elasticsearch.yml 核心参数逐个说清ES 的配置集中在/etc/elasticsearch/elasticsearch.yml。三节点集群的配置逻辑很简单除 node.name 外其他核心参数基本一致。参照下面配置cluster.name: elk-prod node.name: node-1 node.roles: [ master, data, ingest ] path.data: /data/elasticsearch path.logs: /var/log/elasticsearch network.host: 0.0.0.0 http.port: 9200 transport.port: 9300 discovery.seed_hosts: [node-1:9300, node-2:9300, node-3:9300] cluster.initial_master_nodes: [node-1, node-2, node-3] gateway.recover_after_nodes: 2 bootstrap.memory_lock: true解释几个容易被忽略的点。node.roles表明该节点同时承担 master 选举、数据存储和 ingest 预处理职责小规模集群这是合理配置如果节点数超过 5 个或写入压力明显大于查询压力就应该把 master 和数据节点拆开避免选主时受数据节点 GC 影响。cluster.initial_master_nodes只在集群初次初始化时使用用来确定哪些节点有资格参与投票集群建立之后可以注释掉。discovery.seed_hosts是节点发现的关键配置ES 7.x 之后不再用老的discovery.zen.minimum_master_nodes这块配置错了就会出现节点之间互相看不到的情况。3.2 JVM 堆、mlockall 与内存锁定的配合ES 对内存的管理是“JVM 堆 操作系统 page cache”双轨制堆用来存索引和查询状态page cache 用来加速磁盘数据读取。很多人犯的错误是把堆调到物理内存的 80%觉得越大越好结果系统 page cache 被挤没了查询反而变慢。合理的做法是分配物理内存的 50% 给 JVM 堆且上限不超过 31GB。堆超过 31GB 之后JVM 压缩指针失效内存浪费严重性能得不偿失。8G 内存的机器给 4G32G 内存的机器给 16G留一半给操作系统做缓存。修改堆配置推荐使用/etc/elasticsearch/jvm.options.d/heap.options用独立文件管理干净又好回滚-Xms4g -Xmx4gXms 和 Xmx 务必设成一样避免运行期动态扩容造成 GC 波动。另一个内存相关的配置是bootstrap.memory_lock: true它要求 ES 锁定内存防止运行时内存被 swap 到磁盘对写入和查询稳定性的提升非常明显。但光在 elasticsearch.yml 里写这样一行还不够systemd 服务还限制着内存锁上限需要追加配置systemctl edit elasticsearch写入以下内容[Service] LimitMEMLOCKinfinity然后执行systemctl daemon-reload systemctl restart elasticsearch。启动后可以看日志确认是否出现memory locking is enabled如果没有这一行说明锁定没生效后面大概率会遇到“GC 频繁 查询毛刺”之类的玄学问题。3.3 索引分片、副本与索引生命周期策略配置日志场景下手动创建索引是不现实的正确做法是让采集组件按日期自动生成索引再通过索引模板统一设置分片数、副本数和生命周期策略。分片数不是越多越好每个分片都有额外的 CPU 和内存开销三节点小集群一个索引 1 到 3 个分片足够。副本数至少 1保证节点故障时数据不丢查询吞吐也能提升。先建一个索引模板让按天生成的nginx-access-*索引都带上规范配置PUT _index_template/nginx_access_logs { index_patterns: [nginx-access-*], template: { settings: { number_of_shards: 3, number_of_replicas: 1 } } }日志类的数据通常只会热查最近几天超过 30 天甚至 15 天的数据基本不会有人主动查。与其手动删索引不如用索引生命周期管理策略自动执行。以下策略实现保留 30 天到期自动删除PUT _ilm/policy/log_retention_30d { policy: { phases: { hot: { actions: { set_priority: 100 } }, delete: { min_age: 30d, actions: { delete: {} } } } } }在索引模板的 settings 中补充index.lifecycle.name: log_retention_30d之后 Logstash 写入nginx-access-2024.06.01这类索引模板会自动生效。数据量更大的生产系统还可以给 hot 阶段加 rollover按分片大小比如 50GB或时间比如 1 天自动滚动索引避免单个索引过大拖慢查询这个属于进阶玩法这里先不作展开。4. Kibana、Logstash 与 Filebeat 的配合4.1 Kibana 部署、监听配置与基础界面设置Kibana 安装后主要改/etc/kibana/kibana.yml三个核心参数监听端口、监听地址、ES 地址列表。如果希望其他机器浏览器能直接访问server.host要设成0.0.0.0只在本机调试可以保持localhost。server.port: 5601 server.host: 0.0.0.0 elasticsearch.hosts: [http://node-1:9200, http://node-2:9200, http://node-3:9200]如果 ES 集群开启了安全认证Kibana 这边还需要配elasticsearch.username和elasticsearch.password。没开安全模块的话保持默认就行。启动 Kibana 后第一次打开页面系统会引导你创建 Index Pattern。这里有个很常见的坑明明 ES 里已经有数据了Kibana 里却提示找不到数据。最可能的原因是时间字段选错了。日志索引里一般都有timestamp字段这是 ES 默认的时间字段选它就对了如果你用了自定义时间字段或者字段类型是字符串而不是 date数据就算进了 ESKibana 在“过去 15 分钟”的默认时间范围里也搜不到。排查时先把时间范围切换到“最近 30 天”看看。4.2 Logstash 管道编写从 beats 到 grok 再到 esLogstash 的价值在过滤解析配置文件放在/etc/logstash/conf.d/下一个管道一个文件。下面以 Nginx 访问日志为例写一个从 Filebeat 接收、grok 解析、时间规整到输出 ES 的完整管道。input { beats { port 5044 } } filter { grok { match { message %{NGINXACCESS} } } date { match [ timestamp, dd/MMM/yyyy:HH:mm:ss Z ] timezone Asia/Shanghai target timestamp } mutate { remove_field [ message ] } } output { elasticsearch { hosts [http://node-1:9200, http://node-2:9200, http://node-3:9200] index nginx-access-%{YYYY.MM.dd} } }grok 是 Logstash 里最有价值的插件它用正则命名组把非结构化日志行拆成字段。这里的%{NGINXACCESS}是内置模式能解析 Nginx access log 的客户端 IP、请求方法、请求路径、状态码、响应字节数、User-Agent 等字段。date 过滤器很关键它把日志里的字符串时间转成 ES 的timestamp日期类型同时指定Asia/Shanghai时区否则日志时间会比北京时间晚 8 个小时。Logstash 的吞吐调优主要在/etc/logstash/logstash.yml。pipeline.workers建议设成 CPU 核数pipeline.batch.size默认 125在内存允许范围内可以调到 300 到 500能显著提升批量写入效率。注意别一次性调太大Logstash 的 JVM 堆默认 1G批处理尺寸翻倍的同时也要把堆相应调大否则频繁 GC 会拖垮拉数据的速度。4.3 Filebeat 采集 Nginx 日志与多行异常堆栈合并Filebeat 安装在每台业务服务器上配置核心是采集路径和输出目标。以下配置采集nginx-access.log添加一个 service 字段标记来源然后输出到 Logstashfilebeat.inputs: - type: log enabled: true paths: - /var/log/nginx/access.log fields: service: nginx fields_under_root: true output.logstash: hosts: [logstash-node:5044]如果日志不是 Nginx access log而是 Java 应用抛出的异常堆栈默认情况下一个堆栈会被拆成多行每条都变成独立事件根本没法看。这时需要用到 multiline 多行合并把连续的非起始行归到上一个事件里。例如按时间戳行开头为事件起点multiline: pattern: ^\d{4}-\d{2}-\d{2} negate: true match: afternegate: true表示不匹配该模式的行会被合并match: after表示合并到上一行后面。Java 堆栈经常以at xx.xx.xx开头也可以用multiline: pattern: ^\s*(at|Caused by|\.\.\.) negate: false match: after这里的逻辑是匹配到at开头的行就把它并到上一条日志后面。改完 Filebeat 配置后用filebeat test config验证语法再重启服务。采集不到数据时别急着怀疑网络先看/var/log/filebeat/filebeat.log注册表状态卡在旧 offset 也是常见问题。5. 生产优化与容量评估5.1 写入性能调优批量、刷新间隔与 translogELK 集群最常见的瓶颈是写入而写入性能往往卡在几个看起来很不起眼的参数上。默认情况下 ES 每 1 秒刷新一次索引生成一个新的 segment日志场景下这是不必要的开销。把刷新间隔调大到 30 秒段文件数量会显著减少合并线程的压力也随之下降。另一个优化点是 translog。ES 写入时会先写 translog 保证数据不丢默认每次请求都 fsync日志场景可以改为异步刷盘PUT nginx-access-*/_settings { index.translog.durability: async, index.refresh_interval: 30s }这个操作有代价如果节点突然宕机可能丢失最近几秒的数据。对于日志平台来说可接受对于支付类或交易类的数据别这么做。批量写入是拉开写入吞吐差距的核心。Filebeat 采集、Logstash 输出时都是走 bulk 批量接口Logstash 的pipeline.batch.size就是控制每条消息攒多少条再批量执行。如果日志量真的非常大还要关注 ES 端thread_pool.write.queue_size的拒绝情况。出现rejected_execution_exception时正确的处理是降低写入端并发或调大批处理大小而不是拼命堆并发因为 ES 的写入线程池是按核数设计的并发压过头只会让任务排队排队再排队。5.2 日志量变大后Filebeat → Kafka → Logstash 的架构演进基础链路 Filebeat → Logstash → ES 能撑住日增几十 GB 的场景但如果流量出现明显峰值或者多个业务系统同时接入就需要考虑引入 Kafka 做缓冲层链路变成 Filebeat → Kafka → Logstash → ES。为什么要多此一举根本原因是 Logstash 和 ES 都不擅长应对突发流量。Logstash 没有本地队列输入端来了多少就推给输出端ES 写入一拥堵Logstash 只能阻塞或丢数。Kafka 作为消息队列天然具备削峰填谷的能力业务高峰期数据先落 KafkaLogstash 按自己的速度消费不会把 ES 压垮。而且 Kafka 还带来了数据回溯能力Logstash 管道误删了字段或者 ES 索引模板配错了可以重放 Kafka 里的原始日志重新处理。不过这个架构不是银弹。第一Kafka 本身至少三个节点起步运维复杂度和硬件成本立刻上来了第二日志多了一条副本链路故障点也变多。我的建议是初期直接 Filebeat → Logstash → ES跑通业务交付价值等日志量确实涨到日增几百 GB、或者出现明显峰值压垮 ES 的迹象再演进到 Kafka 缓冲架构这时的收益会非常明显。5.3 容量估算以每天 200GB 日志为例做容量规划前先记住一条公式存储量 原始日志量 × 索引开销系数 × 副本份数 × 保留天数。索引开销系数主要指 segment 和 doc values 带来的额外空间一般取 1.1 到 1.2。下表是一套日增 200GB 日志的参考估算项目数值原始日志量200GB/天副本数1共 2 份索引开销1.2每天实际占用约 480GB保留 30 天约 14.4TB14.4TB 数据量放在三节点集群每节点约 5TB。这个规模下磁盘性能是核心瓶颈写入侧的并发段合并、查询侧的聚合计算都需要 IO 支撑SSD 是基本盘机械盘做热数据存储会比较吃力。内存方面建议每个节点物理内存 32GB 以上JVM 堆给 16GB剩余留给 OS page cache。如果查询复杂度和并发查询量都不高16GB 堆够用聚合密集的场景优先考虑增加节点而不是无限堆内存。分片规划同样重要。每天 200GB 日志单分片不宜超过 50GB也就是说当天索引要拆成至少 4 个分片。但分片也不是越多越好每多一个分片集群维护成本都在涨。三节点集群把单日索引控制在 4 到 6 个分片是合理区间节点故障时恢复速度也能接受。6. 常见问题排查与避坑实录6.1 集群健康状态从 yellow 到 red数据还能救吗集群健康状态是 ELK 的第一道警报。某个节点挂掉、磁盘空间不足、或者副本数设置超过可用节点数都会让健康状态变成 yellow。索引所有主分片都找不到可用节点落户时就会变成 red此时该索引的部分数据无法读写。遇到异常先看三个视图基本能定位 80% 的问题curl -s localhost:9200/_cluster/health?pretty curl -s localhost:9200/_cat/nodes?v curl -s localhost:9200/_cat/shards?v如果看到大量 unassigned 分片用 explain 接口查看具体原因curl -s localhost:9200/_cluster/allocation/explain?pretty -H Content-Type: application/json -d {index:nginx-access-2024.06.01,shard:0,primary:true}最常见的两种原因是磁盘水印触发和节点角色不匹配。磁盘水位超过 85% 时 ES 默认不再往该节点分配新分片超过 95% 时甚至会把索引设为只读此时优先做索引清理或扩容。如果是节点掉线导致的副本分片未分配节点恢复后 ES 会自动重新平衡个别顽固分片需要手动 reroute 时操作前务必评估数据风险千万别把主分片分配到版本不兼容的旧节点上。6.2 Kibana 里看不到数据与 8 小时时差这个问题的出现频率比想象中高得多。最常见的原因是 Index Pattern 的时间筛选范围不对Kibana 默认显示最近 15 分钟如果你的数据是昨天写入的自然看不到。切换到“最近 30 天”再验证数据还在就说明只是时间范围问题。另一个经典问题是时区偏差。ELK 默认使用 UTC 时间而业务日志普遍是北京时间。如果 Filebeat 或 Logstash 没有在采集链路里做时区转换Kibana 界面上看到的时间就会比实际晚 8 小时。Logstash 的 date filter 里指定timezone Asia/Shanghai是标准解法。如果已经有一批历史数据被写成了 UTC 时间最快的补救办法是在 Kibana 的 Advanced Settings 中把时区改成浏览器本地时区至少保证展示层正确要彻底修正历史数据就得 reindex 并在管道里重新解析时间字段代价不小。6.3 ES 性能杀手段合并膨胀与字段基数过高ES 查询变慢CPU 和磁盘 IO 一路飙高时第一怀疑对象是段合并。高频率的 refresh 会产生大量小 segment后台合并线程需要不断做 compaction抢占 IO 和 CPU。把refresh_interval调到 30 秒、控制单个分片数据量都能显著减少合并压力。如果集群写多读少还可以看看是否有个别大索引长期处于 merge 风暴状态必要时手动缩小分片大小或延长刷新周期。第二个隐藏杀手是 fielddata 和文档数量极大的高基数字段。比如对用户 ID 这种几百万不同取值的字段做聚合分析每个值都要载入内存fielddata 暴涨后直接触发断路器查询报 429 或内存溢出。应对办法是控制 fielddata 断路器比例同时评估映射设计高基数且只需要精确匹配的字段用 keyword 类型需要全文搜索的字段才用 text并且尽量关闭分析和 multi-fields避免为用不到的分词特性支付存储和内存成本。6.4 RHEL 8 上常见的三个环境级问题RHEL 8 系上最容易踩的环境坑其实和 ES 本身没关系而是操作系统的防护机制和服务管理方式。第一个是 SELinux 拦截 Filebeat 读取日志文件。症状是 Filebeat 启动正常但日志文件完全读不到。查/var/log/audit/audit.log如果有大量 avc denied 记录就要为 Filebeat 或对应日志目录生成放行策略。用ausearch -m avc -ts recent定位再通过 audit2allow 生成模块加载这也是最干净的解决路径。第二个是 firewalld 放行了端口但 Kibana 依然无法访问。这种情况先用curl http://localhost:5601验证本机是否正常本机通而外部不通基本就是防火墙策略没生效或没有 reload。检查端口列表时别只盯着服务端口Kibana 和 ES 之间还要确保网络层能互通跨网段的环境里安全组策略也一样重要。第三个是 systemd 对服务资源的隐式限制。ES 进程在写入高峰期突然被杀死systemctl status elasticsearch显示 memory 或 tasks 受限就需要检查systemctl show elasticsearch -p TasksMax -p MemoryMax -p LimitNOFILE如果存在限制用 systemctl edit 写 override 配置放宽再 reload。RHEL 8 的 systemd 版本比较新CGroup 资源控制比老版本严格得多这个问题在 CentOS 7 迁移过来的旧习惯里很容易被忽略。最后再多说一句我自己的习惯每次改 ES 或 Logstash 配置不要急着 systemctl restart先在对应目录备份原文件restart 之后立刻盯服务日志和集群健康状态。启动阶段的错误信息最直白等运行一段时间后再排查反而需要绕更多弯子。日志平台的价值在于稳定和可回溯宁可部署慢一点也要保证每一步变更都能回滚、能被记录。