Go 使用 NATS JetStream 发送大消息,如何压缩与兜底?
数据阿明
结论
Go 客户端可以在发布前压缩消息,由消费者解压,以缓解大消息超过 NATS max_payload 的问题。Go 标准库的 compress/gzip 不需要额外依赖,可用于实现端到端压缩。压缩效果取决于数据内容,不能保证每条消息都能压到限制以内。文件压缩后仍超限时,可以改用 MinIO 等对象存储传输文件,并在 JetStream 消息中发送引用信息。
为什么 Stream 压缩解决不了发布限制
题目中的 --compression s2 是服务器侧的存储压缩。消息先发布到 NATS,之后才会在存储过程中压缩,所以它不会缩小发布时传给服务器的 payload,也无法绕过 max_payload 检查。
客户端压缩发生在发布之前,服务器接收的是压缩后的字节。输入材料提到,max_payload 上限为 64 MB,并建议生产环境不超过 8 MB;这些是该问题提供的背景。压缩后的 payload 仍需符合实际部署配置的限制。
Go 示例:发布前 gzip,消费时解压
下面的函数只负责转换字节数据。将 compressGZIP 的返回值传给现有的 JetStream 发布逻辑;消费者收到消息后调用 decompressGZIP。
package message
import (
"bytes"
"compress/gzip"
"io"
)
func compressGZIP(src []byte) ([]byte, error) {
var buf bytes.Buffer
zw := gzip.NewWriter(&buf)
if _, err := zw.Write(src); err != nil {
_ = zw.Close()
return nil, err
}
if err := zw.Close(); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
func decompressGZIP(src []byte) ([]byte, error) {
zr, err := gzip.NewReader(bytes.NewReader(src))
if err != nil {
return nil, err
}
defer zr.Close()
return io.ReadAll(zr)
}
发布前先检查压缩结果的长度,再按项目已有方式发布:
compressed, err := compressGZIP(payload)
if err != nil {
return err
}
if len(compressed) > maxPayloadBytes {
// 转入对象存储或分块流程
return errPayloadTooLarge
}
// 将 compressed 交给现有的 JetStream Publish 调用。
maxPayloadBytes 应与部署配置和消息协议一致。项目需要自行定义示例中的 errPayloadTooLarge;这里没有假定具体的 NATS Go 客户端调用签名。
为超限文件准备兜底
- 压缩后符合限制:直接发布压缩数据。消息中应包含明确的压缩标记或格式字段,告知消费者何时解压,避免让接收端猜测内容格式。
- 压缩后仍然超限:将文件存入 MinIO 等对象存储,并在 JetStream 消息中发送对象标识或可访问地址,以及业务所需的元数据。访问控制、链接有效期和文件清理策略由应用定义。
- 必须通过消息系统传输文件:将文件切成多个块,并附上文件标识、块序号和总块数。消费者收齐并校验后再合并。还要考虑重复投递、乱序、缺块、重试和过期清理,否则可能留下不完整文件或占用存储。
注意事项
文本、JSON 等数据通常更有机会通过压缩缩小体积。已经压缩过的图片、视频和压缩包可能几乎不变,甚至变大。对于可能很大的消息,应根据压缩后的实际字节数判断能否发布,并保留超限处理路径。消费者解压时也应设置合理的输出大小限制,避免异常数据展开成过大的内容。
备注:内容仅供参考。
