上一篇解决了怎么用 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
24
25
26
       调优旋钮分布图
┌────────────────────────────────────────────────────────┐
│ JVM 层 │
│ heap size (jvm.options: -Xms / -Xmx) │
│ off-heap (PQ mmap page / direct memory / 线程栈) │
│ 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 (写入侧 fsync 频率) │
│ queue.checkpoint.acks (ack 侧 checkpoint 频率) │
├────────────────────────────────────────────────────────┤
│ 插件层 │
│ filter 顺序(cheap first) │
│ codec 选型(dissect vs grok, plain vs json) │
│ output 连接池(ES output: pool_max / │
│ pool_max_per_route) │
└────────────────────────────────────────────────────────┘

调优的正确顺序:先用第 13 篇的指标定位瓶颈所在层,再在该层旋钮上做调整,调完后重新测量,不能多层同时乱拧。每个旋钮都对应一个可观测量,本篇每一节都会把它挂出来:

1
2
3
4
5
6
7
8
9
旋钮                        判据指标
──────────────────────────────────────────────────────────────
pipeline.workers flow.worker_utilization
pipeline.batch.size batch.event_count 的 P50 / P90
与 pipeline.batch.size 的接近程度
filter 顺序 / 插件选型 插件级 worker_millis_per_event
与插件级 worker_utilization
PQ 磁盘 flow.queue_persisted_growth_events
heap heap_used_percent 与 GC 曲线形态

没有判据的调参就是碰运气:改完之后无法说明改善来自哪里,也无法判断该不该继续往同一个方向走。

JVM heap 调优

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

默认 heap 为 1 GB(-Xms1g -Xmx1g)。修改位于 config/jvm.options

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

-Xms-Xmx 必须设为相同值,避免 JVM 在运行时动态扩展 heap——官方的说法是 resize 本身就是个代价很高的过程。

容器化部署里改 jvm.options 常常不方便,这时用 LS_JAVA_OPTS 环境变量:

1
LS_JAVA_OPTS="-Xms4g -Xmx4g" bin/logstash

它的内容与 jvm.options 里的设置叠加,两处都出现同一个选项时 LS_JAVA_OPTS 覆盖文件里的值。

heap 上限的两条约束:

1
2
3
4
5
6
7
8
9
约束一:典型场景不少于 4 GB、不超过 8 GB
→ 官方对 Logstash 的直接建议就是这个区间。
这是操作性上限,不是"常见范围"——超出 8 GB
需要有实测理由,不是默认可选项

约束二:不超过物理内存的 50–75%
→ 余量留给操作系统、其他进程,以及下面要讲的 off-heap
(PQ 的 mmap page、direct memory、线程栈都不在 -Xmx 里)
→ 物理内存越大,这个比例可以取得越高

至于 32 GB 那条 CompressedOops 阈值,在 Logstash 语境下用不上:推荐上限只有 8 GB,永远碰不到这个门槛。它是从 Elasticsearch 的调优经验里平移过来的约束,对 Logstash 不构成实际限制。

怀疑 heap 给得太小时,官方给了一个不需要任何工具的诊断:直接把 heap 翻倍,看性能是否改善。heap 太低会让 JVM 不停 GC,表现为 CPU 占用被无谓抬高。

off-heap:heap 与 PQ 之间的那笔账

-Xmx 只管一部分内存。操作系统、PQ 的 mmap page、direct memory、线程栈都在它之外,而本篇后半段要讲的 PQ 恰好就落在这一块里:

1
2
3
4
5
内存类型              配置方式                    使用者
──────────────────────────────────────────────────────────────
JVM Heap -Xmx 普通对象分配
JVM direct memory -XX:MaxDirectMemorySize beats / tcp / http input
Native memory 无法配置 PQ 的 page、线程栈

三条需要记住的性质:

  • direct memory 默认大小等于 heap。设了 -Xmx8g 就意味着 JVM 默认还会再要 8 GB 的 direct memory 额度。官方建议考虑把 -XX:MaxDirectMemorySize 设成 heap 的一半,或者任何能容纳预期负载的值。
  • 每条 PQ pipeline 至少要 head 和 tail 两个 page 常驻可访问,默认 page 是 64 MB,也就是每条 PQ pipeline 约 128 MB 起步。这是 per-pipeline 的固定成本,多管道场景下会线性累加。
  • mmap 文件的大小无法设上界。这一块没有对应的 JVM 参数可以约束,只能在容量规划时预留。

官方给了整机估算公式:

1
2
pipelines number * (pipeline threads * stack size + 2 * PQ page size)
+ direct memory + Java heap

代入官方的例子:10 条 pipeline,每条 14 个线程(1 个 pipeline 线程 + 1 个 input 线程 + 12 个 worker),栈按 1 MB 算,heap 4 GB——原生内存 10 × (14 × 1MB + 128MB) = 1.4GB,direct memory 4 GB,heap 4 GB,合计约 9.4 GB。也就是说 -Xmx4g 这台机器实际要预留的是十来个 GB,不是 4 GB 加一点余量。

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 问题的判据是曲线形态,不是某个固定水位:已用堆与上限之间是否还留有余量、gc.collectors.old.collection_time_in_millis 的增长是平滑还是阶梯式上跳、hot threads 里有没有 GC 相关线程。已用堆长期贴着上限、老年代回收耗时快速累积,才是需要动手的信号。处置方式是先按上面那条"翻倍看是否改善"确认方向,再检查是否有 filter 在 event 里附加了大量不必要字段(每个 worker 持有一批 event 的引用,event 膨胀直接放大 heap 占用)。

pipeline.workers 与 pipeline.batch.size

这两个参数控制 filter 和 output 阶段的并发与批量。调它们之前先确认一件事:pipeline.workers 起的那组线程同时执行 filter 和 output,Logstash 里不存在独立的 output 线程池。一个 worker 在一次循环里读一个 batch、跑完全部 filter、再把结果交给 output,然后才回头取下一个 batch。所以下面所有"是 filter 慢还是 output 慢"的讨论,都发生在同一组线程上——output 卡住时 filter 不是"还在并行跑",而是跟着一起停。

默认值先摆出来,否则无从判断自己是在调大还是调小:

1
2
3
4
5
6
7
pipeline.workers    = N   # 默认 = CPU 核数
# 同一组线程同时跑 filter 和 output
pipeline.batch.size = B # 默认 125
# 每个 worker 每次从队列取的最大 event 数

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

pipeline.workers 的调优方向要按瓶颈是 CPU 还是 I/O 分开看,两种情况的上界完全不同:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
瓶颈类型                     调优方向与上界
──────────────────────────────────────────────────────────
CPU 密集
(热点线程栈顶在 grok / workers 加到核数附近就饱和了。
正则 / 序列化里) 再往上加只会增加上下文切换,
吞吐反而掉。上界≈物理核数

I/O 密集
(热点线程栈顶在 HTTP / workers 可以超过核数。官方原话是
socket / ES 客户端里) "may be set higher than the number
of CPU cores since outputs often spend
idle time in I/O wait conditions"——
正因为这些线程大部分时间在等 I/O,
超配才能把等待的时间填上。
上界由 heap 能承载的 N × B 决定,
代价是在途 event 变多、heap 占用上升

单 pipeline 多 output 任一 output 慢会拖住整条管道
下游速度不一致 (filter 和 output 共用 worker)。
这是加 workers 解决不了的结构问题,
要用 Multiple Pipelines 拆开(第 12 篇)

判据指标是 flow.worker_utilization:接近 100 说明 worker 已被占满,加 workers 有意义;明显低于 100 时瓶颈在别处,加 workers 只是多开几个闲着的线程。再往下要用插件级 worker_utilization 确认占满 worker 的是哪个插件,以及它是 CPU 型还是 I/O 型。

pipeline.batch.size 的调优方向:

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

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

判据是批量的实际填充度。Logstash 在 node_stats 里暴露了 pipelines.<id>.batch.event_count,包含 average / p50 / p90 三组分时段统计(这项能力较新,9.4.0 起为技术预览;另有 pipeline.batch.metrics.sampling_mode 控制采样粒度)。读法:

1
2
3
4
5
6
7
8
9
10
P50 / P90 ≈ pipeline.batch.size   批量被填满,吞吐稳定。
若 CPU 与内存有余量,
可以继续调大以摊薄每批开销

P50 / P90 < pipeline.batch.size 输入量本来就不够填满一批。
调大 batch.size 不会有帮助,
只会推高延迟

P90 接近上限而 P50 明显偏低 流量是突发型的。调大能提吞吐,
但延迟波动会变大

再叠加背压:低背压 + 满批 = 健康,可以考虑继续调大 batch 或 workers;高背压 + 满批 = 瓶颈在下游,该加 workers 或资源而不是加 batch;低背压 + 不满批 = 管道本来就没吃满,不需要调。

pipeline.batch.delay(默认 50ms)控制 worker 拿到至少一个 event 后等待凑满一批的超时。官方对它的判断是"rarely needs to be tuned",原因藏在一个乘积公式里:

1
2
worst-case"收到 event 到该 event 进入 filter"的等待
= pipeline.batch.delay × pipeline.batch.size

这不是一个可以忽略的量级。按下面那份配置代入(500 × 50ms),最坏情况下一条 event 要等 25 秒才进 filter。低速流量下这个最坏值是会被真正触及的——每次都差一点凑不满,每次都等满 50ms。

所以 batch.delay 基本不用动。真要动它,得先确认批量确实填不满(P50/P90 明显低于 batch.size),而且改完要盯住两个反向信号:PQ 场景看 queue.events 有没有涨,内存队列场景看 queue_push_duration_in_millis 有没有变长。这两个指标任一上升,说明管道开始向上游施加背压,整体性能是往下走的。

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

1
2
3
4
# logstash.yml
pipeline.workers: 4 # 8 核机器,留 4 核给 OS 和 ES
pipeline.batch.size: 500 # ES bulk 的大小就是这个值
pipeline.batch.delay: 50 # 保持默认;注意 500×50ms 的最坏等待

持久队列磁盘调优

持久队列(PQ)把 event 落盘,提供 at-least-once 语义,但引入了磁盘 I/O 路径。磁盘成为瓶颈时的表现:events.queue_push_duration_in_millis 增长快,input 线程卡在往队列写这一步(hot threads 里能看到 [<id>]<inputname 的栈停在队列写入上)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
磁盘 I/O 路径
input 线程收到 event,写入队列


PQ page file (顺序写,mmap)

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


pipeline worker 读 page,跑完 filter 与 output

▼ 每 queue.checkpoint.acks 次确认强制一次
ack checkpoint 更新(消费进度)

写入侧和确认侧各有自己的 checkpoint 频率,对应两个不同的参数——这个区分在下面的取舍里是关键。

关键参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
queue.page_capacity (默认 64mb)
→ 单个 page 文件大小;调大减少文件切换频率,
但单个 page 占的 mmap 也更大。官方判断是
64mb 对大多数用户就是好值,改它不太可能有收益

queue.checkpoint.writes (默认 1024)
→ 每写多少个 event 强制一次 checkpoint(fsync);
调大 → fsync 次数下降 → I/O 压力下降,
代价见下文

queue.checkpoint.acks (默认 1024)
→ 每确认多少个 event 强制一次 checkpoint;
管的是消费侧的进度记账,与写入侧是两回事

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

queue.checkpoint.writes 是 PQ 调优里唯一一条真正的取舍,而它的代价方向很容易记反。官方在两处把话说得很直白。一处列在"PQ 解决不了的问题"里:“Data may be lost if an abnormal shutdown occurs before the checkpoint file has been committed”;另一处讲降低 checkpoint 频率时:“any data that has not been checkpointed, is lost”。

也就是说,已经写进 page 文件但还没进 checkpoint 的那批 event,在异常关停或硬件故障时是永久丢失,不是"还在 PQ 里等着重启后重处理"。调大这个值等于把"崩溃时最多永久丢多少条"这个数字一起调大。

两个极端官方都给了:

1
2
3
4
5
queue.checkpoint.writes: 1     每写一条 event fsync 一次。
持久性最强,但会严重伤害性能

queue.checkpoint.writes: 0 不限,完全不因写入次数触发 checkpoint。
性能最好,持久性风险最大

SSD 与 HDD 的差异在 checkpoint 的随机写上最为明显:HDD 每次 fsync 耗时数毫秒到数十毫秒,直接拖慢 PQ 写入;SSD 的随机写延迟低一到两个数量级。生产环境 PQ 建议放 SSD。

只有 HDD 时,把 queue.checkpoint.writes 从 1024 调到 4096 确实能把 fsync 频率降到四分之一,但要把账算清楚:这个改动把崩溃时的永久丢失上限从 1024 条提到 4096 条。这笔交易值不值,取决于这条管道的数据能不能从源头重放——如果上游是 Kafka 或别的可重放的源,丢的这批还能再拉一次,代价可控;如果上游是 UDP syslog 这类无法重放的源,4096 条就是真的没了。

判据指标是 flow.queue_persisted_growth_events。官方的说法是它应该趋近于零;持续为正说明写入快于消费、队列在涨,负值代表队列正在收缩、积压在恢复。调完 checkpoint.writes 之后看这个值有没有从正数回落,比看磁盘 %util 更直接。

插件层调优

插件层的旋钮不在 logstash.yml 里,改动方式也不同:只改管道配置文件的话,可以靠 config.reload.automatic 自动重载,或者给进程发 SIGHUP 手动触发一次。前提是管道里的插件都支持 reload——input 和 output 插件常常持有 OS 资源,有些资源不重启进程就释放不掉,官方举的例子是 stdin input,它的存在会直接阻止整条管道重载。另有 pipeline.recoverable 影响重载失败后的可恢复行为。

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}" } }
}
}

filter 顺序和插件选型的判据都是插件级 worker_millis_per_event:调整前后看同一个插件的单条 event 成本有没有下降,以及它在插件级 worker_utilization 里占的比例有没有缩小。

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

ES output 的 bulk 大小没有独立旋钮

这一节最需要说清的是一件"没有旋钮"的事:ES output 发出的 bulk 请求大小就是 pipeline.batch.size。官方对 pipeline.batch.size 的说明里写得很明确,ES output 对每个收到的 batch 尝试发一个 bulk 请求;调 pipeline.batch.size 就是在调 bulk 的大小。

配置里因此不需要、也不能出现 bulk 相关的参数:

1
2
3
4
5
6
7
8
ES output 示例
output {
elasticsearch {
hosts => ["http://es:9200"]
index => "logs-%{+YYYY.MM.dd}"
# bulk 大小由 pipeline.batch.size 决定,此处无对应参数
}
}

历史上的 flush_sizeidle_flush_time 已经在 8.0.0 被标记 obsolete,后续版本的 changelog 明确写了 “Removed obsolete”,当前版本里已无踪迹。它们的后果比"参数不生效"重得多:Logstash 遇到不认识的插件参数会报 Unknown setting 'flush_size' for elasticsearch拒绝启动。抄一份旧配置进去,得到的不是一条警告,而是管道根本起不来。

output 侧真正可调的是连接池,与批量无关:

1
2
pool_max            (默认 1000)  连接池总上限
pool_max_per_route (默认 100) 单个目标地址的连接上限

这里还埋着一个比上面那两个更阴的反例。output plugin 上有一个 workers 参数,写进配置不报错、管道正常启动,但它在 logstash-core/lib/logstash/outputs/base.rb 里的声明是 config :workers, :deprecated => "This parameter will be ignored."——静默忽略,只在日志里留一条 deprecation。ES output 也没有 batch_size 这个配置项。

两类错误的危险程度正好相反于直觉:flush_size 让管道起不来,读者当场就知道要改;workers 让管道正常跑,读者以为 output 并发已经调过了,此后所有基于这个前提的调优推断全都建在空地上。静默无效比启动失败难查得多。

调优工作流

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
调优循环
1. 建立基线
→ 稳定流量下采集:
flow.output_throughput、flow.worker_utilization、
heap_used_percent 与 GC 耗时增长、
queue.events + flow.queue_persisted_growth_events(PQ)
或 events.queue_push_duration_in_millis(内存队列)、
插件级 worker_millis_per_event

2. 定位瓶颈(第 13 篇)
→ 先看 worker_utilization 是否饱和,
再用插件级指标确定是哪个插件、是 CPU 型还是 I/O 型

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

4. 重新测量
→ 对比该旋钮对应的判据指标,而不只是看总吞吐

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

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

模式提炼

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

- JVM 层调优解决 GC 停顿,优先于一切其他调优
(GC 停顿会随机拉高所有 worker 的延迟,掩盖真实瓶颈)
heap 之外还要算上 off-heap:PQ 的 mmap page、direct memory、线程栈

- Pipeline 层调优解决并发不足或批量不足:
workers 的上界看瓶颈是 CPU 型还是 I/O 型;
batch.size 解决 I/O 往返开销,代价是延迟与 heap

- PQ 层调优解决磁盘写入瓶颈:
SSD > HDD;checkpoint.writes 换来的吞吐,
代价是崩溃时永久丢失的上限被同步放大

- 插件层的旋钮不需要动架构参数,改 filter 顺序与 codec 选型即可,
且可以靠 reload 生效(前提是插件都支持 reload)

每一层的旋钮都可以独立验证,遵循单变量原则;
每个旋钮都要绑定一个判据指标,否则无法归因。

工程迁移表

Logstash 调优概念 Kafka 生态对应 Flink 对应 通用 ETL 对应
-Xmx heap 上限 broker/consumer JVM heap TaskManager heap 任意 JVM 服务 heap
off-heap(PQ mmap + direct memory) page cache + 堆外缓冲 Managed / Network memory 进程 RSS 中的非堆部分
pipeline.batch.size(同时决定 ES bulk 大小) consumer max.poll.records operator buffer 大小 Extract/Load 批量
pipeline.workers consumer 线程数 / 分区数 算子并行度 Transform 并发数
PQ checkpoint.writes / checkpoint.acks producer acks + fsync 频率 checkpoint 间隔 Staging 提交批量
filter 顺序(cheap first) UDF 执行顺序优化 算子链顺序 Transform 步骤排序
dissect 替代 grok 结构化日志直接反序列化 使用 RowData 替代 POJO 固定格式直接解析
ES output pool_max / pool_max_per_route producer 连接数 / max.in.flight Sink 客户端连接池 Load 阶段连接池

常见误解

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

误解二:“不区分 CPU-bound 与 IO-bound,照 CPU 核数设 pipeline.workers”。核数只是 CPU 密集场景下的上界。官方明确写了 workers 可以超过核数——“may be set higher than the number of CPU cores since outputs often spend idle time in I/O wait conditions”,另一处更进一步:“Good results can even be found increasing this number past the number of available processors”。理由正是 I/O wait:这些线程大部分时间在等下游响应,超配 workers 才能把等待的时间利用起来。此时真正的上界是 heap 能承载的 workers × batch.size,而不是核数。反过来,如果瓶颈是 grok 这类 CPU 密集操作,加到核数附近就饱和了,再加只会让上下文切换吃掉收益。先用 worker_utilization 和热点线程的栈位置判断是哪一类,再定上界。

误解三:“PQ 里的数据崩溃后总能重来一遍”。要分写入侧和 ack 侧两头看,它们由两个不同的参数管:

1
2
3
4
5
6
7
写入侧(queue.checkpoint.writes,默认 1024)
已写进 page 但未 checkpoint 的 event
→ 异常关停时永久丢失,重启后不存在了

ack 侧(queue.checkpoint.acks,默认 1024)
已处理但确认进度未 checkpoint 的 event
→ 重启后会被重放,下游可能收到重复

所以 PQ 的语义是 at-least-once 而不是 exactly-once(重复来自 ack 侧),同时它也不是"零丢失"(丢失来自写入侧)。要做到 exactly-once,还需要下游的幂等写配合(ES 用 document_id 去重,Kafka 用事务)。

误解四:“output plugin 上的 workers 参数能提高 output 并发”。这个参数确实存在、写进配置也不会报错,但它的声明带着 :deprecated => "This parameter will be ignored."——被完全忽略,只在日志里留一条 deprecation。output 侧的并发要靠连接池参数,ES output 是 pool_maxpool_max_per_route。这类静默无效的参数比拼错参数名更难发现:拼错会让管道起不来,静默无效会让人以为已经调过了。

误解五:“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 延迟)时 events.queue_push_duration_in_millis 的差异。再把 queue.checkpoint.writes 从 1024 调到 4096,重复测量,同时记录 flow.queue_persisted_growth_events 的变化。最后把这次调优的代价写成一句话:这条管道在崩溃时的永久丢失上限从多少条变成了多少条,以及上游能不能重放。

  2. 设计两条管道跑同一份 workers 扫描实验:一条是 CPU 密集的(重 grok,output 用 stdout { codec => dots }),一条是 I/O 密集的(filter 极简,output 指向一个人为加了 200ms 延迟的 http 端点)。两条都把 pipeline.workers 从 1 逐步加到 2×核数,各值采集 30 秒的吞吐与 flow.worker_utilization,画出两条曲线。确认 CPU 密集那条在核数附近就转平或下滑,而 I/O 密集那条可以超过核数继续涨,并用热点线程的栈位置解释差异。

  3. pipeline.batch.size 设为 500、pipeline.batch.delay 保持 50ms,然后用极低速率(比如每 2 秒一条)注入事件,测量单条 event 从注入到出现在 output 的实际延迟。对照 batch.delay × batch.size 算出的最坏值 25 秒,看实测落在什么位置;再把 batch.size 降到 125 重测一次,量化这个乘积对低速流量的影响。

系列导航

序号 主题
00 导读:核心对象是 event,骨架是三段管道
01 架构:JRuby、JVM 与 pipeline 的运行形态
02 event 模型:@timestamp、@metadata 与字段引用
03 codec:字节流与 event 的边界转换
04 input 插件:拉取、监听与 Beats 接入
05 Grok 的本质:命名正则加预定义 pattern
06 dissect 与结构化 filter:放弃回溯换吞吐
07 常用 filter 组合:mutate、date、geoip 与条件
08 output 插件:Elasticsearch output 与批量写入
09 pipeline 执行模型:worker、batch 与背压
10 内存队列 vs 持久队列:可靠性的分界线
11 死信队列(DLQ):无法处理的 event 去哪
12 Multiple Pipelines 与 pipeline-to-pipeline
13 监控:Node Stats API、hot threads 与瓶颈定位
14 性能调优:JVM heap、批处理与持久队列磁盘(本篇)
15 Logstash vs Beats vs Ingest Pipeline:该用谁
16 Logstash vs Fluentd vs Vector:日志管道的三种取舍
17 Logstash 的演进与 Elastic Agent 的冲击

参考资料