
title: Zstd 级别拉到 19 那天采集 agent 把自己压死了LZ4、Snappy、Zstd 的 4 个真实账topic: 压缩算法对比LZ4、Snappy、Zstdbatch: 6round: 3我们日志采集 agent 早期直接把原始 JSON 往 Kafka 丢带宽一个月烧掉 6 万。有人提议「上 Zstd压缩比高」我脑子一热把压缩级别设成 19Zstd 最高档附近心想压缩比越高越省带宽。结果 agent 部署后 CPU 从 15% 飙到 100%把所在机器的其他进程全拖慢Kafka 没省多少带宽却差点把采集节点搞崩。压缩不是「级别越高越好」它本质是 CPU 和带宽的置换级别调错就是拿 CPU 换那几个百分点的压缩比。这篇文章用我们交的学费把 LZ4、Snappy、Zstd 三个常选算法的真实账算清楚。事故Zstd 级别 19CPU 换来了多少带宽我当时的代码简化public class LogCompressor { private final ZstdCompressor compressor new ZstdCompressor(); public byte[] compress(byte[] raw) { // level 19追求极致压缩比 int level 19; return compressor.compress(raw, level); } }逐行解释为什么这版是坑- 第 4 行new ZstdCompressor()用的是zstd-jniFacebook 的 JNI 封装它调用原生 Zstd 库级别越高压缩时搜索匹配串的窗口越大、尝试越深。- 第 6 行level 19是接近最高的级别。我们一条日志平均 800 字节级别从 3 提到 19压缩比只从 3.4 提到 3.9但单条压缩耗时从 8 微秒涨到 210 微秒涨了 26 倍。- 采集 agent 单机每秒要压 4 万条日志级别 19 时压缩线程直接 100% 满载连带 GC 也涨压缩中间 buffer 多机器 load 从 2 飙到 20同机上的订单上报服务 P99 都受了影响。修正级别降到 3压缩比几乎没掉CPU 砍回去了public class LogCompressor { private static final int LEVEL 3; // 性价比甜区不是越高越好 public byte[] compress(byte[] raw) { return Zstd.compress(raw, LEVEL); // 静态方法避免重复 new } public byte[] decompress(byte[] in, int originalLen) { return Zstd.decompress(in, originalLen); // 解压必须知道原始长度 } }逐行解释- 第 2 行LEVEL 3是 Zstd 的「快速档」。实测级别 1~3 压缩比只比 19 低一点点3.4 vs 3.9但 CPU 只有 19 级的零头。我们把级别定在 3带宽省了和 19 级差不多的量CPU 回到 18%。- 第 6 行Zstd.decompress(in, originalLen)提醒一个坑Zstd 解压需要你提供原始长度或帧里有记录我们第一次解压忘了传长度默认按 0 处理直接报错。压缩框架几乎都要求「压缩时记住原始长度」写工具类时务必一起存。- 第 2 行「静态方法」是我们踩过的另一个坑new ZstdCompressor()每次 new 在高频调用下对象创建开销也不小直接用Zstd.compress静态方法省掉对象。三个算法的真实定位一张对比表光讲 Zstd 不够我们把三个常用算法在日志场景压了一组数算法压缩比日志压缩速度解压速度适合的场景LZ42.1极快极快追求低延迟、CPU 敏感压缩比要求不高Snappy2.4很快很快和 LZ4 同类Google 系生态默认Hadoop/ESZstd (lvl 1-3)3.3快极快要兼顾压缩比和速度带宽贵时首选Zstd (lvl 19)3.9极慢极快离线归档、冷数据CPU 不敏感我的取舍线上传输/采集这种「CPU 和带宽都敏感、且实时」的场景Zstd 级别 1~3 或 LZ4 最稳Snappy 是「生态默认」选比如你们已经在用 Hadoop/Elasticsearch 的管道单独引入没优势。Zstd 19 级只留给「一次写入、多次读、几乎不压缩」的离线归档比如我们的历史日志冷存压缩一次 CPU 烧了无所谓省下的对象存储费用是真金白银。第二个坑小数据压缩反而膨胀我们还有个场景把每条 MQ 消息体平均 200 字节单独压缩后发送。上线后一看带宽没降反升排查发现是「小数据压缩膨胀」。public class SmallMsgCompressor { public byte[] pack(byte[] body) { byte[] z Zstd.compress(body, 3); // 压缩后还要在头部写原始长度、算法标记等元信息 ByteBuffer buf ByteBuffer.allocate(4 1 z.length) .putInt(body.length).put((byte) 1).put(z).array(); return buf; } }逐行解释- 第 5 行ByteBuffer.allocate(4 1 z.length)压缩后我们额外加了 4 字节原始长度 1 字节算法标记。对 200 字节的小消息Zstd 压缩比只有 1.1 左右加上 5 字节头整体反而比原文大。- 更隐蔽的压缩算法为了建字典/写帧头小数据常有固定开销几字节到几十字节数据越短越不划算。我们测过单条 512 字节的消息Zstd 级别 3 平均膨胀 6% 4KB 才开始稳定收益。- 修法是「设阈值 批量」单条 1KB 不压缩直接发或者把多条消息攒成一批比如 64KB 一个 batch再压批内冗余多、压缩比直接到 3.5 以上。我们改成批量压缩后MQ 带宽降了 58%比逐条压还省。第三个坑压缩级别和「数据特征」强相关压缩比不是算法单方面决定的和你的数据冗余度强相关。我们同一套 Zstd 级别 3- 应用日志大量重复字段名、堆栈压缩比 3.4- 已压缩过的图片 base64 / gzip 过的上游报文压缩比 0.98几乎不压甚至略膨胀- JSON 里塞了随机 UUID、签名串的压缩比 1.3这是很多人忽略的如果你要压的数据本身已经「很随机」加密、base64 二进制、UUID 满天飞上什么压缩算法都白搭CPU 白烧。我们的做法是「压缩前先判断数据类型」——带application/octet-stream或已知已压缩的直接跳过压缩。这条规则上线后采集 agent 的无效压缩 CPU 又降了 12%。第四个坑解压侧的 CPU 也是钱别只算压缩侧选算法时大家只盯着「压缩快不快」忘了「解压」也要 CPU。我们的日志进 Kafka 后在 Flink 消费端解压如果生产者用 Zstd 19 级压缩消费者解压虽然比压缩快得多但 Kafka 分区多、并行度高累积解压 CPU 也不小。LZ4/Zstd 的解压都极快比压缩快一个量级Snappy 也是三者解压侧差异不大真正要警惕的是「生产者高压缩级别」会放大「消费者解压总量」。所以级别选择要站在「全链路 CPU 账单」看不是只看发送端。应用层压缩 vs HTTP 传输层压缩该压哪一层我们有个接口既在应用层用 Zstd 压了 body又开着 Nginx 的gzip on结果是「压缩两次」——应用层压完的字节流对 gzip 来说已经是高熵随机数据gzip 几乎压不动还多烧 CPU纯亏。我的取舍很直接要么在传输层统一 gzip/brHTTP 天生支持客户端透明解压要么在应用层压适合进 Kafka/Redis 这种不带传输层压缩的通道二选一别叠加。我们最终定对外 HTTP 接口走 Nginxbrbrotli比 gzip 还狠一点关掉应用层压缩对内 Kafka/Redis 走应用层 Zstd 级别 3。这条规则上线后应用层压缩的 CPU 全省了对外接口带宽靠 br 一样省下来没损失。还有一个常被问的压缩级别和耗时到底是不是线性我们压过一组日志单条 800 字节10 万条批量Zstd 级别 1压缩比 3.1单条 5μsZstd 级别 3压缩比 3.4单条 8μsZstd 级别 6压缩比 3.6单条 22μsZstd 级别 19压缩比 3.9单条 210μs你看级别 1→3压缩比涨 10%、耗时涨 60%还能接受3→19 压缩比只涨 15%、耗时涨 26 倍完全不划算。这条曲线就是我们定级别 3 的硬依据过了 3每多一分压缩比都要几十倍的 CPU 换。压缩失败要有回退压完比原文还大就发原文第四坑之后补一条工程纪律压缩不是「压了就一定发压缩版」要比较压缩后和原文大小膨胀就回退原文。我们工具类最终长这样public class SafeCompressor { public CompressResult pack(byte[] raw, int level) { byte[] z Zstd.compress(raw, level); // 压完比原文还大小数据常见直接发原文省一次误事 if (z.length raw.length) { return CompressResult.raw(raw); } return CompressResult.compressed(z, raw.length); } }逐行解释- 第 5 行if (z.length raw.length)是回退闸门小数据、随机数据压缩后常常比原文大这时候发压缩版既费 CPU 又费带宽不如直接发原文。我们线上小消息占比 30%加这层后这部分流量不再无效压缩。- 第 6 行返回CompressResult.raw(raw)时要在协议头里标「未压缩」消费端按标记决定解不解压。压缩/未压缩必须带标记否则解压端不知道该不该解——这是自研压缩通道最容易漏的一环。复盘真实数字Zstd 级别 19单条压缩 8μs → 210μs采集 agent CPU 15% → 100%同机订单服务 P99 涨了 40%。降到级别 3带宽节省从 19 级的「省 74%」变成「省 70%」但 CPU 回到 18%P99 恢复正常——4 个百分点的压缩比不值 85% 的 CPU。小消息改批量压缩64KB/batchMQ 带宽再降 58%相比逐条压。跳过「已压缩/随机数据」无效压缩 CPU 再降 12%。我的取舍压缩是 CPU↔带宽的置换别替一方代言我不建议无脑上最高压缩级别。先问自己两件事瓶颈在带宽还是 CPU数据冗余度高不高带宽贵、数据冗余高、且实时性要求一般 → Zstd 1~3 或 LZ4CPU 已经紧张 → LZ4 优先别碰 Zstd 高档离线冷存、几乎不压缩 → Zstd 19 随便用。小数据先攒批或设阈值别逐条压已压缩/随机数据直接跳过压缩。一句话压缩级别调高之前先量「每多 1% 压缩比要烧多少 CPU」多数时候级别 3 就是甜区再往上纯属给账单添堵。思考题如果你的消息总线里 80% 是 512 字节的小消息、20% 是大报文你会统一压缩还是按大小分流分流的阈值怎么定才不引入新的判断开销