把 Logstash 搬到自建服务器上跑,最容易踩的坑不是装不上,而是"跑着跑着就慢了,还不知道慢在哪"。我见过太多这样的现场:日志量明明没涨,采集端却开始堆积,运维第一反应是加 CPU、加内存,加完还是堵。真相往往很反直觉——Logstash 自己没有报错,它只是在老老实实地执行背压:下游写不动了,压力一级一级往回传,最后一头压在采集端。你看到的是"采集端堵了",根因却在 output 那一头。
这篇只讲 Logstash 自身。不谈它和别的采集器怎么选,也不谈谁轻谁重——那是另一个话题。这里只把一台机器上跑 Logstash 的三件核心事说透:队列怎么背压、grok 到底贵在哪、持久化怎么做才算真的不丢。再往下是服务器的选型口径,能直接对着下单。
Logstash 的执行模型可以简化成一条传送带:input 端负责把事件拿进来,queue 负责缓冲,filter 负责改写,output 负责送走。这四个角色里,只有 queue 是"有状态"的,其余三段都是线程在干活。
一条 Apache 访问日志从磁盘被读到、变成检索集群里的一条文档,中间要过四道:file input 按行读取并打上 path 与 host 字段 → 写进队列 → filter worker 取走,跑 grok、date、mutate、geoip 这一串插件 → output 攒够一个 batch(默认 125 条)后一次 bulk 写出。每一步都有成本,但成本差别极大:纯 mutate 的代价可能只有几微秒,一次复杂 grok 可以到几百微秒甚至毫秒级,一次带回溯失败的 grok 能把整批拖慢一个数量级。
理解这个生命周期的意义在于:瓶颈的定位顺序应该从后往前查。先看 output 有没有堆积,再看 filter 的 CPU 是不是打满,最后才怀疑 input。很多人反着查,于是在 input 端反复调参数,调半天没用。
不是所有日志都值得过一遍 Logstash。它适合的场景有三个特征:需要复杂解析(多行堆栈、混合格式)、需要做富化(geoip、useragent、jdbc_static 查库)、需要分流到多个不同的目标端。反过来,如果你的日志本身就是 JSON,且只需要转发,那 Logstash 的价值就只剩下"缓冲和背压"这一条——这时候 grok 基本可以不用,管道的 CPU 开销会低一个量级。
还有一类场景是业务混跑:一台机器上既要收 Nginx 访问日志,又要收 Java 应用的多行异常栈,还要收数据库慢查询。这三类日志的解析代价、目标端、可靠性要求完全不同,硬塞进一条管道,结果就是慢的拖死快的。这种情况要用 pipelines.yml 拆开,后面单独讲。
背压这个词听着玄,机制其实很朴素:每一级在拿不到下一级的"放行"时就停在那里等。
把时间轴拉开看一遍。假设检索集群因为分片恢复或者 JVM Full GC,写入延迟从 20ms 涨到 3 秒。第一步,output 插件的 bulk 请求开始超时、重试,写出的耗时被拉长;第二步,worker 线程被卡在 output 上,它手上那批(默认 125 条)事件就交不出去,也就没法回队列取下批;第三步,所有 worker 都卡住之后,队列只进不出,很快被填满;第四步,队列满了,input 线程往队列写的时候被阻塞——file input 不去读下一行,beats input 不去 accept 新的连接;第五步,发送端的 socket 缓冲区也满了,SDK 或者采集器的本地 spool 开始积压。
注意这五步里,Logstash 自始至终没有抛一个 ERROR。它只是"不读了"。所以你在监控上看到的现象一定是:Logstash 进程的 CPU 掉下来了(因为 worker 在等 IO,不是在算),队列水位打满,input 端吞吐归零,而上游的采集器开始报"发送超时"。这就是"为什么表现为采集端开始堵,而不是 Logstash 报错"的完整答案。
这里面有个很实用的监控经验:把"Logstash CPU 使用率突然下降 + 上游堆积"当成一个组合信号。单独看 CPU 低你会以为很闲,其实是卡住了。真正健康的管道,CPU 应该是稳定在一个中高位(说明 filter 在干活),而不是忽高忽低。
还有一点容易被忽略:内存队列和持久队列在背压发生时的表现完全不同。内存队列的缓冲深度很浅,压力几乎是瞬间传到 input;持久队列能扛住 queue.max_bytes 这么大的缓冲(默认 1024mb,一般配到 4–8gb 甚至更高),所以开启 PQ 之后,同样的下游故障,你会看到"延迟涨了但还没完全堵死",多出来的这段时间就是你抢救的窗口。
答案一句话:grok 就是正则,而正则有回溯。Logstash 跑在 JRuby 上,grok 底层用的是 Oniguruma 系的正则引擎(Joni),它不具备 RE2 那种线性时间的保证。一个写得不严谨的 pattern,遇到不匹配的长字符串,就可能触发大量回溯,单次匹配耗时从微秒级飙到毫秒级。
具体到插件行为上,有几个点决定了它的成本:
失败比成功贵得多。grok 的 match 可以写多个 pattern,插件默认是 break_on_match => true,也就是命中一个就停。但如果一条日志一个都没命中,它就会把列表里所有 pattern 全部跑一遍,然后给你打上 _grokparsefailure 标签。也就是说,那些"格式变了、解析不出来"的脏日志,恰恰是消耗 CPU 最狠的。这是个恶性循环:格式越乱,CPU 越累,管道越慢。
锚定能省掉大量无效尝试。不加 ^ 的 pattern,正则引擎会在字符串的每一个起始位置尝试匹配。一条 800 字节的日志,就意味着最多几百次尝试起点。加上 ^ 之后只在行首试一次,代价立刻降下来。我自己的习惯是:所有针对整行的 grok pattern 一律加 ^,除非你明知道目标字段在中间。
pattern 顺序应该按命中率从高到低排。因为 break_on_match 会让第一个命中的生效,排在前面的 pattern 会被最多事件先试。把命中率 90% 的正常格式放第一位,把命中率 1% 的异常格式放后面,整体期望开销会低很多。反过来排,就是让 90% 的事件先白跑一遍冷门 pattern。
别用 .* 和 .*? 兜底。这是回溯的主要来源。能用 [^ ]+、[^"]+、\d+、\S+ 这类"明确字符集"的地方,就不要用 .*。一条 %{GREEDYDATA:msg} 放中间,能把整条管道的性能吃掉一半。
超时保护要开。grok 插件有 timeout_millis(默认 30000),这是个兜底。如果你的管道常年跑着一批"疑似卡住"的事件,可以把它往下调到 1000–3000,让病态 pattern 快速失败而不是拖死 worker。这不是优化性能,是防止一条脏数据把整个 worker 挂住半分钟。
还有个反常识的点:grok 的开销和日志长度不是线性关系,而是接近平方关系。因为回溯的搜索空间随长度增长得更快。所以对于超长日志(比如带完整 SQL 语句的慢查询日志),先用 mutate 或者 dissect 把长尾巴切掉,再对头部做 grok,效果立竿见影。
Logstash 里能干解析活的插件不止 grok 一个,选对了能把 CPU 省下来一大截。按代价从低到高排:
json filter:最便宜。如果日志本身就是 JSON 字符串,一个 json { source => "message" } 就能把字段全部展开,代价接近一次序列化反序列化,比任何正则都快。很多团队的日志已经由应用侧的 logback/log4j2 输出成 JSON 了,却在 Logstash 里又写一遍 grok 去"重新解析"——这是纯粹的自找麻烦。能走 json 就别走 grok。
dissect:分隔符切分,没有正则引擎。dissect 的工作方式是按你给的分隔符逐段扫描字符串,它不做模式匹配,也就不存在回溯。对于"字段之间用空格/竖线/制表符固定分隔"的日志,dissect 的开销大约是同等功能 grok 的零头。写法上用 %{field} 表示取值,%{} 表示跳过,%{+field} 表示追加拼接。它的短板是不能处理"字段内部出现分隔符"的情况,比如 message 里带空格——这种只能回退到 grok,或者用 %{?skip} 配合固定结构。
kv:键值对的通用解。对于 k1=v1 k2=v2 这类日志,kv 插件用 field_split 和 value_split 切分,代价介于 dissect 和 grok 之间。它比 grok 快是因为不做全模式匹配,但它仍然要扫整个字符串,并且字段数量越多开销越大。用 kv 的时候记得配 trim_key 和 trim_value,否则值里带空格会很难受。
grok:通用但最贵。只在上面三种都搞不定时才用。实际工程里常见的组合是:dissect 先把大结构切开(时间戳、级别、线程名这些固定位置字段),剩下的 msg 部分再按需用 grok 精解析。这样既拿到了 dissect 的性能,又保留了 grok 的灵活性。
再补一个和解析强相关的坑:date 插件的时区。date 插件在日志里不带时区信息时,会用你指定的 timezone 来解析,不指定的话行为会随运行环境而变。中国区的服务器,timezone => "Asia/Shanghai" 这一行不要省。另外一个坑是日志里只有 MMM dd HH:mm:ss 没有年份,date 会按当前年份补,跨年那一两天就会出错——碰到 syslog 格式务必处理年份。解析失败的事件会被打上 _dateparsefailure,这个标签一定要进监控,否则你会发现检索集群里有一批数据的时间戳全落在同一天。
条件判断放哪里也有讲究。把 if [type] == "nginx" {} 这种判断写在 filter 里,意味着每个事件都要过一遍判断,管道里业务越多、if 分支越长,这部分开销越可观。更干净的做法是用 pipeline-to-pipeline:在上游管道里用 if 分流,通过 pipeline { send_to => [...] } 把事件投递给下游的专用管道,各管道独立配置 worker 数、独立队列、互不影响。
内存队列(in-memory,queue.type: memory)是 Logstash 的默认配置。它的行为是同步交接:input 写进去,worker 立刻取走,队列里基本不堆积。好处是零磁盘开销、延迟最低;代价是进程一挂或者机器一断电,队列里在途的那些事件就没了,没有任何补救余地。
持久队列(persistent queue,PQ,queue.type: persisted)会把事件先落盘再交给 filter。它的价值不在"提速",而在两件事:
第一,抗进程崩溃。Logstash 进程被 OOM kill、被误杀、升级重启,队列里的数据还在磁盘上,起来之后接着发,不丢。
第二,扛下游长时间故障。检索集群维护两小时,PQ 能在这两小时里把数据全接下来,恢复之后慢慢回放。内存队列在这两小时里只有两条路:要么把上游堵死,要么丢数据。
但 PQ 不是免费的。它带来三笔账:磁盘写 IO(每条事件都要写盘,虽然是大块顺序写)、磁盘空间(按 queue.max_bytes 预留)、吞吐损失(业内实测的共识区间是开启 PQ 后峰值吞吐下降约 5%–20%(预估,取决于磁盘性能和 checkpoint 配置),NVMe 上掉得少,机械盘上掉得多)。这个损失换的是不丢数据,绝大多数业务都该换。
一个很现实的判断口径:日志能不能容忍丢?访问日志、埋点日志偶尔丢几条问题不大,用内存队列省资源也说得过去;但审计日志、交易日志、安全告警日志丢一条可能就是事故,PQ 必须开。我自己的习惯是:凡是进了检索集群要用来排障的日志,一律开 PQ,磁盘成本远低于排障时缺数据的成本。
还要注意 PQ 的一个副作用:它会掩盖下游故障。因为数据被稳稳接住了,监控上看不出下游已经瘫了两小时,等发现的时候磁盘可能已经写到 90%。所以开 PQ 之后,一定要对"队列水位"和"队列最老事件年龄"做告警,这两个指标比 CPU 更能反映管道健康度。
PQ 有三个参数决定它的行为和代价,配错了要么撑爆磁盘,要么白丢数据。
queue.max_bytes:队列的磁盘上限,默认 1024mb。这个值是硬顶,写满了就开始背压。怎么估?公式就是峰值流量 × 目标端故障时长,再留 30% 余量。举个具体例子:峰值 5000 条/秒,平均每条 500 字节,那就是 2.5 MB/s,一小时 9 GB。你要求目标端故障 2 小时不丢数据,那就是 18 GB,加 30% 余量约 24 GB。所以 queue.max_bytes: 24gb,磁盘上至少留 30 GB 给 PQ 目录。注意这个计算要用峰值不是均值,很多团队按均值算,结果大促那天队列半小时就满了。
queue.max_events:按条数封顶,默认 0(不限制)。一般不设,除非你的磁盘紧张且单条事件特别大,可以用它做第二道保险。
checkpoint 系列:这是 PQ 里唯一需要"做取舍"的地方。checkpoint 的意思是"我已经确认发出去/已经确认落盘"的标记点,Logstash 靠它来判断哪些数据可以安全删除。相关参数有三个:queue.checkpoint.acks(默认 1024,累计多少个 ack 做一次)、queue.checkpoint.writes(默认 1024,累计多少条写入做一次)、queue.checkpoint.interval(默认 1000ms,多久强制做一次)。
这三者的取舍是:checkpoint 越勤,崩溃时丢的数据越少,但磁盘 IO 越多。把 writes 从 1024 调到 1,等于每条都刷一次盘,安全性最高但吞吐会明显下滑;反过来调到 100000,IO 省了,但一旦崩掉可能丢掉近十万条。工程上比较稳的做法是保持 writes 在 1024–8192 这个区间,acks 保持默认,interval 保持 1000ms——这是官方默认值,也是经过大量现场验证的平衡点。除非你的磁盘是 NVMe 且数据极其敏感,否则不建议动这三个值。
另外一个必须知道的事实:PQ 的磁盘是"预占 + 复用"的。它不是一个无限增长的文件,而是一个环形缓冲,写满之后从头覆盖已确认的部分。所以不要看到 PQ 目录占用接近 max_bytes 就慌,那是正常的。真正要看的指标是队列使用率(已用/上限)和未确认事件数。
这三个参数是 Logstash 调优里被改得最勤、也改错得最多的。
pipeline.workers:默认等于 CPU 核数。它决定同时有多少个线程在跑 filter + output。为什么超过核数反而更慢?三个原因:一是上下文切换,线程数超过物理核之后,操作系统要在多个线程之间反复切换,每次切换都要保存恢复寄存器、冲刷 TLB,这些开销是纯损耗;二是JVM 的 GC 线程也要占核,你把 32 核全分给 worker,GC 就得跟业务线程抢 CPU,一次 Full GC 的停顿会更长;三是filter 阶段是 CPU 密集的,grok、date、geoip 都在做计算,多线程并不能凭空造出算力,超线程给你的"逻辑核"也不是完整核心,实测大约只有物理核 60%–70% 的增益(预估)。
所以推荐值是:workers ≈ 物理核数。如果 filter 里有明显的 IO 等待(比如 http filter 调外部接口、jdbc_static 查库、dns 反查),可以适度上调到核数的 1.5–2 倍,因为这类线程大部分时间在等,不是在算。但纯 grok 管道,就老老实实等于核数。
pipeline.batch.size:默认 125。它是每个 worker 一次从队列取多少条。调大它的好处是摊薄 output 的固定开销(一次 bulk 请求的协议开销被更多事件分摊),坏处是内存占用上升、单批失败时重放的量更大。经验区间是 125–1000,写检索集群的场景常用 250–500。别盲目往 5000 调,堆内存扛不住,而且一批失败要重放 5000 条,反而更慢。
pipeline.batch.delay:默认 50ms。它的意思是"凑不够一批也最多等这么久就发出去"。这个值决定了低流量时的延迟:流量小的时候,worker 可能 50ms 才凑到几条,如果 delay 太大,日志就会有明显延迟。低流量场景可以把它降到 5–10ms,高流量场景它基本不生效(因为很快就能凑满一批)。
这三个参数要一起调,单独改一个往往没效果。给一个能直接用的起步组合:8 核机器、grok 中等复杂度的管道,workers=8、batch.size=250、batch.delay=50,然后按监控微调。另外,pipeline.ordered: true 会强制把 workers 压成 1 并禁用并发批次——除非你真的需要严格保序,否则别开,它是吞吐杀手。
堆内存也要跟着配。JVM 的 -Xms 和 -Xmx 必须设成同一个值,避免运行期动态扩容带来的停顿。堆大小的业界共识是:给物理内存的一半,且不超过约 31–32GB——这是 JVM 压缩指针(compressed oops)的临界线,超过之后对象引用从 4 字节变成 8 字节,同样的内存能装的对象反而变少了。剩下的内存留给堆外(PQ 的页缓存、JRuby 运行时、操作系统文件缓存)正合适。
这里必须把一个事实说清楚:Logstash 提供的是 at-least-once,不是 exactly-once。也就是说,它会保证事件至少被投递一次,但不保证只投递一次。重复投递发生在哪些时刻?output 写出成功但 ack 在回程丢了(网络抖动)、Logstash 在 ack 之前崩溃重启、PQ 里的事件已经发出但 checkpoint 还没做就被杀掉。这三种情况下,重启之后 Logstash 会把"它可能没发出去"的那批再发一遍。
所以答案是:输出一定要有幂等键。写检索集群时,用 document_id => "%{[@metadata][fingerprint]}",配合 fingerprint filter 对事件内容(或者对"业务主键 + 时间戳")算一个哈希。相同内容的事件算出相同 id,重复写入就变成了覆盖,数据不会翻倍。这一步不做,你迟早会在某次重启之后发现检索集群里多出一批重复文档。
再来看 dead letter queue(DLQ,死信队列)。它解决的是另一个问题:不是"重复写",而是"写不进去"。比如某条日志缺字段、类型不匹配、超过 mapping 限制,被目标端拒绝,返回 400。这类错误重试一万次也不会成功,如果不处理,它会一直卡在管道里(或者被直接丢弃)。DLQ 的作用就是把这些"永远写不进去"的事件单独存到磁盘上,让主流程继续跑,事后你再单独处理。
开启方式上有个版本差异要注意:早期版本是在 output 插件上配 dead_letter_queue.enable => true,8.x 起建议统一在 logstash.yml 里全局配置。默认情况下 DLQ 只对支持它的 output(典型的是 elasticsearch output)生效,而且它只接"目标端明确拒绝"的事件,不接网络超时这类可重试错误。
PQ 和 DLQ 的分工可以这样记:PQ 管"来不及发",DLQ 管"发不进去"。一个是时间维度的缓冲,一个是数据维度的问题隔离。两者都要开,都要有监控——PQ 看水位,DLQ 看条数。DLQ 里只要有一条,就说明你的解析规则或者目标端 mapping 有问题,这是必须处理的信号,不是可以忽略的噪音。
顺带说一句版本相关的字段规范。Logstash 从 7.x 到 8.x 引入了 pipeline.ecs_compatibility 设置,默认取值随版本变化(7.x 多为 disabled,8.x 多为 v8)。打开 ECS 兼容之后,很多插件的字段命名会变:比如主机名从扁平的 host 变成嵌套的 [host][hostname],输出插件的默认索引名也会从 logstash-* 变成 ecs-logstash-*。升级大版本之前,先把这个设置显式写上(写 disabled 也行),别让它跟着默认值悄悄变,否则升级完你会发现所有基于旧字段名的看板和告警全废了。
把所有业务的日志塞进一条管道,是 Logstash 落地里最常见的架构错误。后果很具体:慢的管道(多行堆栈解析)会把快的管道(JSON 直传)一起拖慢;一个业务的 grok 写崩了,整条管道的事件都被卡住;某个业务突然来一波流量洪峰,别的业务一起排队。
pipelines.yml 就是为了解决这个隔离问题。它让你在一份配置文件里声明多条管道,每条有自己的 input、filter、output、独立的队列、独立的 pipeline.workers 和 pipeline.batch.size。比如可以这样分:
这样每类日志的代价被隔开了,一条管道炸了不影响别的。更进一步,可以用 pipeline { send_to => [...] }(上游)和 pipeline { address => ... }(下游)做 pipeline-to-pipeline 通信,把"收"和"处理"分成两层。这样做的好处是:分流逻辑集中在一条入口管道里,解析逻辑分散在各业务管道里,改 Nginx 的 grok 不用动 Java 那套配置。注意下游管道通过 pipeline.output.workers 和 ensure_delivery 控制并发与可靠性——ensure_delivery: true 时,背压会跨管道传导,慢的下游会反压到上游入口。
隔离带来的另一个价值是可观测性。Logstash 的监控 API 是按管道维度输出指标的,拆开之后你能直接看到"是哪条管道慢",而不是在一堆聚合指标里猜。这个价值在出故障的那十分钟里,比什么都实在。
ECS 那部分再补一句实操建议:新项目直接按 ECS 规范写字段命名(用 [source][ip]、[url][path]、[http][response][status_code] 这类),老项目升级时先设 pipeline.ecs_compatibility: disabled 保住现有看板,再找窗口期迁移。不要指望一次升级就切过去,字段名的改动会牵动告警规则、看板、检索语句一整条链。
下面这张表把两种队列在六个维度上的实际差别摊开,数字是工程上常用的参考区间,具体以你的磁盘和业务量为准。
| 对比维度 | 内存队列(memory) | 持久队列(persisted) | 断电/崩溃时的表现 | 磁盘与 IO 开销 | 怎么选 |
|---|---|---|---|---|---|
| 缓冲深度 | 在途约 batch.size × workers,8 核 250 批约 2000 条 | 默认 1024mb,常用 4–24gb,约合 200 万–4800 万条(按 500B/条) | 内存队列在途事件全部丢失,约 2000 条 | 无磁盘占用;持久队列需按 max_bytes 预留 1.3 倍空间 | 能容忍丢几百条选内存;一条都不能丢选持久 |
| 峰值吞吐损失 | 基准,无额外损失 | 下降约 5%–20%(预估,NVMe 取下限,机械盘取上限) | — | 顺序写为主,NVMe 上几乎无感;机械盘上 IO wait 明显上升 | 磁盘必须 SSD/NVMe,否则吞吐损失翻倍 |
| 下游故障时扛多久 | 几乎为 0,压力秒级传到 input 端 | 等于 max_bytes ÷ 峰值字节速率,8gb 队列 2.5MB/s 流量约 53 分钟 | 进程正常重启时,持久队列未确认事件自动回放 | 恢复后回放阶段磁盘读 IO 会持续跑满一段时间 | 按"峰值流量 × 目标端故障时长 × 1.3"反推 max_bytes |
| 延迟 | 端到端额外延迟接近 0 | 多一次落盘与读取,常态下增加约 1–10ms(预估) | — | checkpoint 越勤延迟越稳定,同时 IO 次数越多 | 实时告警类管道可单独走内存队列 |
| checkpoint 机制 | 不存在,无 ack 记录 | acks 默认 1024,writes 默认 1024,interval 默认 1000ms | 未 checkpoint 的部分可能重复投递,需 document_id 兜底 | writes 调到 1 时每条刷盘,吞吐下滑明显;调到 8192 时省 IO 但风险上升 | 保持默认即可,敏感场景只微调 interval |
| 运维可观测性 | 只能看进程级指标,看不到队列水位 | 可看队列使用率、未确认条数、最老事件年龄,能做水位告警 | 崩溃后重启日志里能看到回放条数 | 需额外监控 PQ 目录所在分区的剩余空间 | 开了 PQ 就必须配水位告警,否则故障会被掩盖 |
把上面这些机制翻译成配置单,就是下面四个维度。
CPU:filter 阶段是纯 CPU 密集,核数直接决定吞吐。上面说过 workers ≈ 物理核数,那么核数就是你吞吐的天花板。一条中等复杂度的 grok 管道,单个 worker 的实测处理能力大约在 3000–8000 条/秒这个区间(预估,取决于 pattern 复杂度和日志长度);如果是 dissect + json 的轻量管道,单 worker 可以到 20000–50000 条/秒(预估)。所以如果你要扛 3 万条/秒的 grok 重日志,8 核是明显不够的,得上 16 核往上。选型时别只看主频,核数优先于主频,因为 Logstash 是多线程并行模型。
内存:堆给一半且不超过 31–32GB,剩下的留给系统。具体算:64GB 物理内存,堆设 31GB(Xms=Xmx=31g),剩下的 33GB 给堆外和文件缓存;32GB 物理内存,堆设 16GB;16GB 物理内存,堆设 8GB。另外要记住 batch 缓冲也要吃内存:workers × batch.size × 单条大小,8 × 250 × 2KB ≈ 4MB,看着不大,但如果单条日志很大(比如带完整堆栈),这个值会快速膨胀,堆栈日志场景下建议把 batch.size 往回收。
磁盘:开 PQ 之后,磁盘从"只读日志文件"变成"持续顺序写"。这是很多人低估的一点。PQ 的写入模式是大块顺序写 + 定期 checkpoint 刷盘,机械盘在这种模式下还能应付,但一旦叠加日志文件的随机读(file input 追踪多个大文件)和系统本身的 IO,机械盘就会成为瓶颈。SSD 是底线,NVMe 是推荐。容量上按 PQ 的 max_bytes 预留 1.3 倍,再加上日志本地留存的空间。举个配置:PQ 给 24GB,日志本地留存 100GB,系统 40GB,那 240GB 的 SSD 是起步,500GB 更从容。
网络:到目标端(检索集群)的出口带宽和压缩是一对取舍。elasticsearch output 的 compression_level 控制 bulk 请求的压缩级别,值越大压得越狠、带宽越省、CPU 越费。日志文本通常能压到原始体积的 15%–30%(预估),也就是说 100Mbps 的实际流量,开高压缩之后可能只要 20–30Mbps。取舍口径是:CPU 有余量、带宽贵 → 提高 compression_level;CPU 已经吃满、带宽便宜 → 降到 1 或 0。国内机房的带宽成本通常高于 CPU 成本,所以多数场景下建议开到中等(4–6)。另外,如果 Logstash 和目标端跨地域,出口带宽的稳定性和延迟比带宽大小更重要,这类场景建议把 Logstash 和目标端放在同一个机房内,跨地域只传汇总后的结果。
落到具体机型上,给两个能直接参考的方向:
#1 一万网络「裸金属 E5-2698v4×2」,双路 20 核共 40 核 80 线程,¥3999 起。这类双路裸金属是 grok 重管道的甜点配置:核数足够把 workers 拉到 32–40,开 PQ 之后还有余量跑压缩;裸金属没有虚拟化层的开销,JVM 的 GC 停顿也更可控。配上 NVMe 系统盘和独立的数据盘,一条日处理 10 亿条的管道单台就能扛住。
#2 一万网络「BGP 多线 + CN2 GIA 服务器租用」,一万云 ¥25 起。日志管道的网络特点是"持续、稳定、长连接",最怕的是抖动。BGP 多线 + CN2 GIA 的链路在跨运营商访问时延迟更稳,bulk 请求不会因为某条链路抽风而大面积超时;配套的 5–20G 免费 DDoS 防护也能挡掉一部分对外的噪音。适合那种采集点分散在多个运营商、需要往中心机房汇总的场景。
一万网络深耕 IDC 19 年(成立于 2007 年),官网明示的服务里,对日志管道这类"不能断"的业务最有用的是这几条:7×24 中文工单、平均 5 分钟响应、硬件故障 10 分钟自动迁移、免费系统盘快照、免费备案协助、工程师 1 对 1 部署。硬件故障 10 分钟自动迁移这一条,配合 Logstash 的 PQ,基本能把单机故障的数据损失压到接近零。
场景不同,配置思路差别很大,分开说。
多行堆栈日志:multiline 是把双刃剑。Java 异常栈、Python traceback 这类日志,一行异常会展开成几十上百行,必须合并成一个事件才有意义。但合并的代价是吃内存:插件要把未完成的事件一直缓存在内存里,直到遇到下一个"新事件开始"的行才吐出来。如果匹配规则写错(比如 negate 搞反了),一个不匹配的事件会导致后面所有行都被吸进同一个缓冲区,内存一路涨到 OOM。另外,多行合并依赖事件到达顺序,多 worker 下顺序不保证,所以这条管道通常要把 pipeline.workers 压到 1,或者干脆在采集侧就合并好。我的建议是:能不在 Logstash 里做多行就不做,代价更低、更可控。真要做,务必设 max_lines 上限和超时,并给这条管道单独的内存预算。
JSON 直传:基本可以不用 grok。应用侧配置 logback 的 JsonLayout 或者 log4j2 的 JsonTemplateLayout,直接输出 JSON,Logstash 这边一个 json filter 展开字段,date 插件处理时间戳,剩下的 mutate 做字段裁剪。整条管道的 CPU 开销比 grok 方案低一个量级,同样的机器能扛 3–5 倍的吞吐(预估)。唯一要注意的是 target 参数:不设 target 会把字段展开到根层级,字段名冲突时会被覆盖;设了 target 会嵌套在一个字段下,看板上要多写一层路径。这个属于规范问题,团队内统一即可。
多业务混跑:一定要拆管道。前面讲过用 pipelines.yml 隔离,这里再补一个实操细节:拆分之后要注意总 worker 数不要超过核数太多。三条管道各配 8 个 worker,加起来 24,跑在 8 核机器上就是典型的过度订阅,整体吞吐反而低于三条各配 2–3 个。正确做法是先按业务优先级分配核数:核心审计管道 4 个,访问日志管道 2 个,堆栈管道 2 个,总和等于核数。优先级低的业务宁可慢一点,也不要让核心管道排队。
还有个容易被忽略的场景:流量洪峰。业务搞活动,日志量瞬间涨 10 倍,管道扛不住。这时候 PQ 的价值就体现出来了——它把洪峰"削平",让下游按自己的节奏消费。所以洪峰明显的业务,PQ 一定要开,且 max_bytes 要按洪峰流量算,不是按均值。
坑一:把"CPU 突然变低"当成管道变健康了。为什么坑:worker 卡在 output 上等待时,CPU 会掉下来,看起来像负载减轻,实际是背压已经发生。怎么避:把"CPU 下降 + 上游堆积 + 队列水位上升"三个指标绑成一个告警,任何一个单独看都会被误导。
坑二:grok pattern 不加 ^ 锚定。为什么坑:不加锚定的 pattern 会在字符串每个位置尝试匹配,800 字节的日志就是几百次尝试,长日志上代价呈非线性增长。怎么避:所有针对整行的 pattern 一律加 ^;同时把 pattern 按命中率从高到低排,让大多数事件在第一条就命中退出。
坑三:用内存队列跑审计日志。为什么坑:内存队列在进程崩溃或断电时会丢掉在途的所有事件,而审计日志丢一条就可能被追责。怎么避:审计、交易、安全告警三类日志一律 queue.type: persisted,并且按"峰值流量 × 目标端最长故障时长 × 1.3"算 max_bytes。
坑四:输出不配 document_id 就重启 Logstash。为什么坑:at-least-once 语义下,重启会重放未确认的事件,没有幂等键就是一批重复文档,检索集群里的数据会翻倍。怎么避:在 filter 里用 fingerprint 算哈希,output 里用 document_id => "%{[@metadata][fingerprint]}",让重复写入变成覆盖。
坑五:把 workers 调到核数的两倍以上想"多开多赚"。为什么坑:filter 是 CPU 密集,超订之后上下文切换、GC 线程争抢、TLB 冲刷都是纯损耗,实测吞吐不升反降。怎么避:workers 设成物理核数;只有当 filter 里有明显的外部 IO(http、jdbc_static、dns)时才上调到 1.5–2 倍。
坑六:在机械盘上开 PQ 还把 max_bytes 配到几十 GB。为什么坑:PQ 是持续顺序写 + 定期 checkpoint,机械盘在这种模式下 IO wait 会飙高,吞吐损失可能从 5% 放大到 30% 以上(预估),而且恢复回放时读 IO 会长时间打满。怎么避:PQ 所在分区必须是 SSD,NVMe 更佳;容量按 1.3 倍预留,并对分区剩余空间做告警。
问一:Logstash 的 JVM 堆到底该给多大?
业界共识是给物理内存的一半,且不超过 31–32GB 这条压缩指针线。比如 64GB 内存的机器设 31GB,32GB 的机器设 16GB,16GB 的机器设 8GB。Xms 和 Xmx 必须相等,避免运行期扩容带来的停顿。剩下的内存留给堆外和文件缓存——PQ 的读写很依赖操作系统的页缓存,堆给太多反而会让 PQ 变慢。
问二:开了持久队列之后吞吐会掉多少?
常见的区间是下降 5%–20%(预估),具体取决于磁盘性能和 checkpoint 配置。NVMe 上通常掉在 5%–10%,SATA SSD 在 10%–15%,机械盘可能到 20% 以上。想把这个损失压下来,最有效的办法是把 PQ 放在独立的 NVMe 盘上,和日志文件读取的 IO 分开,不要和系统盘或者 file input 追踪的日志目录挤在同一块盘。
问三:pipeline.workers 设成 CPU 核数的两倍行不行?
纯 grok 管道不建议,多半会更慢。filter 阶段是 CPU 密集,线程数超过物理核之后就进入超订状态,上下文切换和 GC 线程争抢会吃掉多出来的收益。只有当 filter 里存在明显的外部等待(http 调用、jdbc_static 查库、dns 反查)时,上调到 1.5–2 倍才有意义,因为这类线程大部分时间在等 IO 而不是在算。
问四:grok 老是匹配不上,怎么快速定位是哪一条拖慢了?
先给 grok 加上 tag_on_failure(默认就是 _grokparsefailure),然后把带这个标签的事件单独采样输出到文件或另一个索引,看具体长什么样。同时把 timeout_millis 从默认 30000 调到 1000–3000,让病态 pattern 快速失败。定位到具体格式后,优先改成分段处理:dissect 切固定部分,剩下的短字段再 grok。
问五:重启 Logstash 会不会造成目标端重复数据?
会,只要还有未确认的事件就会。at-least-once 语义决定了 Logstash 宁可重复也不能丢,所以重启、崩溃、网络抖动都可能触发重放。唯一的解法是幂等键:fingerprint filter 算哈希 + output 的 document_id。做了这一步,重复写入就变成覆盖,数据不会翻倍。没做这一步,某次重启之后检索集群里多出一批重复文档几乎是必然。
问六:多行堆栈日志该在 Logstash 里合并吗?
能做,但这是把双刃剑。合并插件要把未完成的事件缓存在内存里,规则写错或者遇到不匹配的输入,缓冲区会一路膨胀到 OOM。而且合并依赖事件到达顺序,多 worker 下顺序不保证,这条管道通常要把 workers 压到 1,等于主动放弃并行能力。更划算的做法是在采集侧就完成合并,Logstash 只负责接收已经合并好的事件。
问七:一台机器上跑多个业务的管道怎么隔离?
用 pipelines.yml 声明多条管道,每条独立配置 workers、batch.size 和队列类型。关键是总 worker 数不要超过核数:三条管道各配 8 个跑在 8 核机器上是过度订阅,不如按优先级分成 4/2/2。拆开之后还有一个额外好处——Logstash 的监控指标是按管道输出的,出故障时你能直接看到是哪条管道慢,不用在一堆聚合数字里猜。
本篇涉及的机制说明,取自 Elastic 官方 Logstash 文档里关于 persistent queue、dead letter queue、pipeline 参数(workers / batch.size / batch.delay / ordered)、grok 与 dissect 插件选项、ECS 兼容性的公开描述;文中所有的吞吐区间、吞吐损失比例、压缩比、单 worker 处理能力等数字,均为工程实践中的经验区间,标注了(预估)的部分属于推算参考,会随日志格式、磁盘性能、JVM 版本和目标端状态而变化,请以你自己环境里的压测结果为准。价格部分,A 类取自一万网络官网明示报价(裸金属 E5-2698v4×2 ¥3999 起、一万云 ¥25 起、AI 算力云弹性切片 ¥210 起、A100 40G ¥2800/月、RTX3090 24G ¥1750/月、T4 ¥900/月),B 类推算值均标注(预估),具体以签约时最新报价与合同为准。
下单之前,有三件事建议先核实清楚:
第一,核数与 PQ 磁盘的配置是否匹配你的峰值流量。用"峰值条/秒 × 平均单条字节 × 目标端最长故障时长 × 1.3"算出 PQ 需要的容量,再反推磁盘;用"峰值条/秒 ÷ 单 worker 处理能力"算出需要的核数。这两个数字算出来之后再去选机型,比拍脑袋选要省事得多。一万网络提供工程师 1 对 1 部署,可以把这两组计算交给他们一起核。
第二,磁盘类型要写进合同。PQ 是持续顺序写,机械盘上吞吐损失会明显放大。下单时明确写清楚 PQ 所在的数据盘是 SSD 还是 NVMe、容量多少、是否与系统盘分离。这一条在配置单上很容易被忽略,等上线发现 IO wait 飙高再换盘,代价远高于一开始选对。
第三,链路与可用性条款要对着业务等级看。日志管道属于"不能断"的基础设施,重点核实 7×24 中文工单、平均 5 分钟响应、硬件故障 10 分钟自动迁移、免费系统盘快照这几条是否覆盖在你的机型上,以及到目标端(检索集群)的出口带宽与链路质量。如果采集点分散在多个运营商,BGP 多线 + CN2 GIA 的链路稳定性会比单纯加大带宽更管用。具体以签约时最新报价与合同为准。
Copyright © 2013-2020 idc10000.net. All Rights Reserved. 一万网络 科技有限公司 版权所有 深圳市科技有限公司 粤ICP备07026347号
本网站的域名注册业务代理北京新网数码信息技术有限公司的产品