上个月我帮某公司调了一次日志平台,几十台服务器的业务日志散落一地,每次排障都靠逐台跳上去 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 8
RHEL 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 默认开了 firewalld,ELK 涉及 9200、9300、5044、5601 四个主要端口。ES 节点间通信走 9300,HTTP 查询走 9200,Logstash 接收 Filebeat 数据走 5044,Kibana 页面走 5601。防火墙放行命令如下:
firewall-cmd --zone=public --add-port=9200/tcp --permanent firewall-cmd --zone=public --add-port=9300/tcp --permanent firewall-cmd --zone=public --add-port=5044/tcp --permanent firewall-cmd --zone=public --add-port=5601/tcp --permanent firewall-cmd --reload除非三台机器在完全隔离的内网,否则这四步别省。我见过有人折腾了两个小时,最后发现只是防火墙没放行 5601,Kibana 页面在浏览器里一直打不开。
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.conf:
vm.max_map_count=262144 fs.file-max=6553560执行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 内存的机器给 4G,32G 内存的机器给 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] LimitMEMLOCK=infinity然后执行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,数据就算进了 ES,Kibana 在“过去 15 分钟”的默认时间范围里也搜不到。排查时先把时间范围切换到“最近 30 天”看看。
4.2 Logstash 管道编写:从 beats 到 grok 再到 es
Logstash 的价值在过滤解析,配置文件放在/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 字段标记来源,然后输出到 Logstash:
filebeat.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 写入性能调优:批量、刷新间隔与 translog
ELK 集群最常见的瓶颈是写入,而写入性能往往卡在几个看起来很不起眼的参数上。默认情况下 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 作为消息队列,天然具备削峰填谷的能力,业务高峰期数据先落 Kafka,Logstash 按自己的速度消费,不会把 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.4TB |
14.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 之后立刻盯服务日志和集群健康状态。启动阶段的错误信息最直白,等运行一段时间后再排查,反而需要绕更多弯子。日志平台的价值在于稳定和可回溯,宁可部署慢一点,也要保证每一步变更都能回滚、能被记录。