上一篇解决了怎么用 Node Stats API 和 hot threads 定位瓶颈在哪一段。这一篇进入调优执行层:拿到瓶颈定位结论之后,heap 大小、GC 策略、batch 与 worker 组合、持久队列磁盘 I/O 各应该怎么调。

核心问题:heap 大小与 GC 停顿之间的取舍;pipeline.workerspipeline.batch.size 的组合效果;持久队列磁盘成为瓶颈时的判断和处置。

调优对象全景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
       调优旋钮分布图
┌────────────────────────────────────────────────────────┐
│ JVM 层 │
│ heap size (jvm.options: -Xms / -Xmx) │
│ GC 算法 (G1GC 默认; ZGC 可选) │
│ GC log 分析 │
├────────────────────────────────────────────────────────┤
│ Pipeline 层 │
│ pipeline.workers (并发 filter+output 线程数) │
│ pipeline.batch.size (每批最大 event 数) │
│ pipeline.batch.delay (批次等待超时 ms) │
├────────────────────────────────────────────────────────┤
│ 持久队列层 │
│ queue.type: persisted │
│ queue.max_bytes (磁盘上限) │
│ queue.page_capacity (单页文件大小) │
│ queue.checkpoint.writes (写多少次做一次 checkpoint) │
├────────────────────────────────────────────────────────┤
│ 插件层 │
│ filter 顺序(cheap first) │
│ codec 选型(dissect vs grok, plain vs json) │
│ output workers / batch_size(per-plugin 参数) │
└────────────────────────────────────────────────────────┘

调优的正确顺序:先用第 13 篇的指标定位瓶颈所在层,再在该层旋钮上做调整,调完后重新测量,不能多层同时乱拧。

JVM heap 调优

Logstash 运行在 JVM 上,heap 配置错误是生产环境最常见的性能隐患。

默认 heap 为 1 GB(-Xms1g -Xmx1g)。生产环境处理中等流量时,4–8 GB 是常见范围。修改位于 config/jvm.options

1
2
3
## config/jvm.options 关键行
-Xms4g
-Xmx4g

-Xms-Xmx 必须设为相同值,避免 JVM 在运行时动态扩展 heap 触发额外 GC 停顿。

heap 上限有两条硬约束:

1
2
3
4
5
6
7
约束一:不超过物理 RAM 的 50%
→ 另一半留给操作系统页缓存(持久队列的 mmap 读写、
以及 input 端的网络缓冲都依赖页缓存)

约束二:不超过 32 GB(JVM 压缩指针阈值)
→ 超过 32 GB 后 JVM 关闭 CompressedOops,
实际可用堆反而可能不升反降,且 GC 压力更大

GC 算法:Logstash 近期版本默认使用 G1GC,是合理的默认选择。对于延迟敏感场景可以考虑 ZGC(-XX:+UseZGC),它的停顿时间更短但吞吐略低。GC 日志对于排查 heap 问题是必要的:

1
2
## jvm.options 中开启 GC 日志
-Xlog:gc*:file=/var/log/logstash/gc.log:time,uptime:filecount=5,filesize=20m

heap 问题的症状:jvm.heap_used_percent 持续在 80% 以上,gc.collectors.old.collection_time_in_millis 快速增长,hot threads 里出现 GC 相关线程。处置方式是先调大 heap,再检查是否有 filter 在 event 里附加了大量不必要字段(每个 worker 持有一批 event 的引用,event 膨胀直接放大 heap 占用)。

pipeline.workers 与 pipeline.batch.size

这两个参数控制 filter 和 output 阶段的并发与批量:

1
2
3
4
5
pipeline.workers    = N   # 同时运行的 filter-output worker 线程数
pipeline.batch.size = B # 每个 worker 每次从队列取的最大 event 数

最大在途 event 数 = N × B
内存占用上界 ≈ N × B × (单个 event 平均大小)

pipeline.workers 的调优方向:

1
2
3
4
5
6
7
8
9
10
11
12
13
场景                          调优方向
──────────────────────────────────────────────────────────
filter CPU 密集(hot threads workers 增加到 CPU 核数
FilterWorker CPU 高) (不超过物理核;超线程
核数通常效果递减)

output I/O 等待(hot threads workers 增加帮助不大;
OutputWorker 卡在网络调用) 应调 output plugin 自身
的 workers/pool 参数

单 pipeline 多 output output 成为串行瓶颈;
下游速度不一致 考虑 Multiple Pipelines
拆分(第 12 篇)

pipeline.batch.size 的调优方向:

1
2
3
4
5
6
batch 大 → 每批摊薄的固定开销(队列锁、output 连接建立)降低
→ 吞吐提升;但单批 event 占用内存更多,且单批处理
时间更长,导致端到端延迟上升

batch 小 → 延迟低;但吞吐下降,尤其对 ES bulk 写入影响明显
(每次 bulk 请求包含的文档数更少)

pipeline.batch.delay(默认 50ms)控制 worker 拿到至少一个 event 后等待凑满一批的超时。低延迟场景可以减小;高吞吐场景可以适当增大,允许 worker 等更久来凑更大的批次。

典型生产配置参考(仅作基准,需实测调整):

1
2
3
4
# logstash.yml
pipeline.workers: 4 # 8 核机器,留 4 核给 OS 和 ES
pipeline.batch.size: 500 # 配合 ES output 的 flush_size
pipeline.batch.delay: 50

持久队列磁盘调优

持久队列(PQ)把 event 落盘,提供 at-least-once 语义,但引入了磁盘 I/O 路径。磁盘成为瓶颈时的表现:filter worker 频繁等待(hot threads 里 FilterWorker 堆栈卡在队列写入),queue_push_duration_in_millis 增长快。

1
2
3
4
5
6
7
8
9
10
11
磁盘 I/O 路径
FilterWorker 产出 event


PQ page file (顺序写,mmap)

▼ 每 queue.checkpoint.writes 次写一次
checkpoint 文件(随机写,fsync)


OutputWorker 读 page,确认后更新 ack checkpoint

关键参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
queue.page_capacity (默认 64mb)
→ 单个 page 文件大小;调大减少文件切换频率,
但单个 page 占内存 mmap 也更大

queue.checkpoint.writes (默认 1024)
→ 每写多少个 event 做一次 checkpoint(fsync);
调大 → 减少 fsync 次数 → I/O 压力下降,但崩溃
时最多丢失更多 checkpoint 间的 event(仍在 PQ
内,重启后可重处理,不是永久丢失)

queue.max_bytes (默认 1gb)
→ PQ 磁盘上限;超过上限后 input 被阻塞
(背压传导,与第 09 篇讨论相同)

SSD 与 HDD 的差异在 checkpoint 的随机写上最为明显:HDD 每次 fsync 耗时数毫秒到数十毫秒,直接拖慢 PQ 写入;SSD 的随机写延迟低一到两个数量级。生产环境 PQ 建议放 SSD。若只有 HDD,调大 queue.checkpoint.writes(如 4096)能减少 fsync 频率,以稍高的崩溃恢复窗口换取吞吐。

插件层调优

插件层的调优通常比 JVM 和 pipeline 参数调整带来更大收益,且不需要重启——但需要修改配置后重载。

filter 执行顺序对性能有直接影响:

1
2
3
4
5
6
7
8
9
10
11
12
13
原则:cheap filter 放前面,expensive filter 放后面

示例(慢到快:grok > dissect > mutate > date):
filter {
# 先用 dissect 快速切分明确格式的字段
dissect { mapping => { "message" => "%{ts} %{level} %{msg}" } }
# 再用 mutate 做简单字段变换
mutate { remove_field => ["message"] }
# 最后只对需要复杂解析的 event 跑 grok(加 if 条件)
if [level] == "ERROR" {
grok { match => { "msg" => "%{GREEDYDATA:detail}" } }
}
}

codec 选型:plain codec + dissect filter 的组合,在固定格式日志上比 json codec 或 grok 有更低的 CPU 开销。json codec 适合输入本身已经是结构化 JSON 的场景,无需 filter 再解析。避免在 json codec 解析后又用 grok 再次解析 message 字段——这是两次序列化/反序列化的叠加。

output 端的 batch 参数需要和 pipeline.batch.size 协调:

1
2
3
4
5
6
7
8
9
10
11
12
ES output 示例
output {
elasticsearch {
hosts => ["http://es:9200"]
index => "logs-%{+YYYY.MM.dd}"
# flush_size 控制 ES bulk 请求的最大文档数
# 建议与 pipeline.batch.size 对齐或为其整数分之一
flush_size => 500
# idle_flush_time 控制批次超时(秒)
idle_flush_time => 1
}
}

调优工作流

调优不应是随机调参,而是一个有方向的循环:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
调优循环
1. 建立基线
→ 稳定流量下采集 events/sec、heap_used_percent、
queue.events_count、per-plugin duration

2. 定位瓶颈(第 13 篇)
→ 确定慢在 filter / output / JVM GC / PQ 磁盘

3. 调一个旋钮
→ 只改一个参数,改完重启(或 reload)

4. 重新测量
→ 对比调前基线,确认改善还是恶化

5. 记录结论
→ 无改善则回滚,有改善则继续下一轮

常见陷阱:同时调多个参数,改善时不知道是哪个起效,恶化时不知道该回滚哪个。调优日志需要记录:参数名、旧值、新值、调整时间、测量结果。

模式提炼

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
模式:分层旋钮 + 单变量实验 + 指标驱动决策

- JVM 层调优解决 GC 停顿,优先于一切其他调优
(GC 停顿会随机拉高所有 worker 的延迟,掩盖真实瓶颈)

- Pipeline 层调优解决并发不足或批量不足:
workers 解决 CPU 瓶颈;batch.size 解决 I/O 往返开销

- PQ 层调优解决磁盘写入瓶颈:
SSD > HDD;checkpoint.writes 是 fsync 频率的直接控制

- 插件层调优成本最低收益最高:
调整 filter 顺序和 codec 选型不需要调架构参数

每一层的旋钮都可以独立验证,遵循单变量原则。

工程迁移表

Logstash 调优概念 Kafka 生态对应 Flink 对应 通用 ETL 对应
-Xmx heap 上限 broker/consumer JVM heap TaskManager heap 任意 JVM 服务 heap
pipeline.batch.size consumer max.poll.records operator buffer 大小 Extract/Load 批量
pipeline.workers consumer 线程数 / 分区数 算子并行度 Transform 并发数
PQ checkpoint.writes producer acks + fsync 频率 checkpoint 间隔 Staging 提交批量
filter 顺序(cheap first) UDF 执行顺序优化 算子链顺序 Transform 步骤排序
dissect 替代 grok 结构化日志直接反序列化 使用 RowData 替代 POJO 固定格式直接解析
ES output flush_size producer batch.size Sink buffer flush 阈值 Load 批量大小

常见误解

误解一:“把 heap 调大一定能提速”。heap 过大会让 GC 的单次停顿时间变长(G1GC 需要扫描更大的 old gen),在延迟敏感场景下适得其反。调 heap 的目标是消除频繁 GC,而不是越大越好。先看 heap_used_percentgc.collection_time_in_millis 增长速率,再决定是否需要扩 heap。

误解二:“pipeline.workers 调到 CPU 核数就最优”。workers 数量超过 CPU 核数时,线程切换开销上升,且如果瓶颈在 output I/O 而非 filter CPU,增加 workers 只会让更多线程同时等待下游,徒增 heap 占用。workers 的上界是 CPU 核数,但最优值需要实测。

误解三:“PQ 开了就等于 exactly-once”。PQ 提供 at-least-once:崩溃恢复后,checkpoint.writes 区间内未确认的 event 会被重放,下游可能收到重复数据。exactly-once 还需要下游的幂等写(ES 用 document_id 去重,Kafka 用事务)配合。

误解四:“filter 顺序不影响性能,只影响正确性”。顺序直接影响性能。如果把高 CPU 的 grok 放在最前面,每个 event 都跑 grok,哪怕后续 filter 会丢弃大部分 event。把廉价的条件判断(if [type] == "app")放在 grok 之前,能让大量不需要 grok 的 event 直接跳过,显著降低 CPU 负载。

练习

  1. 构造一条带 PQ 的 Logstash 管道,向它注入高速事件流,用 iostat -x 1 观察磁盘 %utilawait,对比 HDD 和 SSD(或用 dm-delay 模拟 HDD 延迟)时 queue_push_duration_in_millis 的差异。再把 queue.checkpoint.writes 从 1024 调到 4096,重复测量,量化 fsync 频率降低带来的吞吐变化。

  2. 在同一台机器上,把 pipeline.workers 从 1 逐步增加到 CPU 核数再到 2×核数,每个值采集 30 秒内的 events.out 速率,绘制折线图,找出 workers 边际收益转负的转折点,并用 hot threads 解释转折点处线程的状态。

  3. 为同一批日志分别写一个"grok 优先"版本和"dissect 优先+条件 grok"版本的 filter 配置,各自运行 5 分钟,对比 plugins.filters[i].events.duration_in_millis 和整体 events.out 速率,量化 filter 顺序优化的收益。

系列导航

序号 主题 状态
00 导读:核心对象是 event,骨架是三段管道 已发布
01 Logstash 架构:JRuby、JVM 与 pipeline 的运行形态 已发布
02-08 核心抽象与插件三段 已发布
09-12 管道执行与可靠性 已发布
13 监控:Node Stats API、hot threads 与瓶颈定位 上一篇
14 性能调优:JVM heap、批处理与持久队列磁盘 本篇
15 演进:Logstash vs Beats vs Ingest Pipeline 下一篇
16-17 生态对比:Fluentd、Vector、Elastic Agent 后续阶段

参考资料