尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

VictoriaMetrics 依赖解析:gzhttp 包如何实现 Gzip/Zstd 双压缩的 HTTP 透明压缩中间件

VictoriaMetrics 依赖解析:gzhttp 包如何实现 Gzip/Zstd 双压缩的 HTTP 透明压缩中间件 VictoriaMetrics 依赖解析gzhttp 包如何实现 Gzip/Zstd 双压缩的 HTTP 透明压缩中间件【免费下载链接】VictoriaMetricsVictoriaMetrics: fast, cost-effective monitoring solution and time series database项目地址: https://gitcode.com/GitHub_Trending/vi/VictoriaMetricsgzhttp 是 VictoriaMetrics 仓库 vendor 目录中托管的klauspost/compress库提供的 HTTP 压缩中间件包当前锁定版本为github.com/klauspost/compress v1.19.1见 go.mod。它通过包装 HTTP 服务端 handler 与客户端 Transport为支持相应编码的客户端透明地启用 gzip 或 zstd 压缩/解压两者默认同时启用客户端同时支持时优先选用压缩率与速度更优的 zstd。本文以 gzhttp/README.md 为主体结合 VictoriaMetrics 自身在 lib/httpserver/httpserver.go 中的真实接入方式完整讲解该包的客户端/服务端用法、zstd 协商机制、性能数据、BREACH 缓解方案以及它在 VictoriaMetrics 各组件 API 层中的落地细节。包定位从 nytimes/gziphandler 分叉而来根据 README 说明该包 fork 自已停止维护的 nytimes/gziphandler并扩展了其功能。核心能力分两块服务端包装任意http.Handler对支持压缩的客户端透明压缩响应体客户端提供Transport包装器用比标准库更快的自定义解压实现替换默认的 gzip/zstd 解压逻辑。两个方向的包装器都与其它服务器/客户端完全兼容可以无侵入地叠加在现有链路上。安装方式独立项目场景go get -u github.com/klauspost/compress客户端 Transport更快的 gzip/zstd 解压标准库http.Client会自动为大多数请求附加 gzip 压缩声明并处理响应解压gzhttp 通过包装Transport接管这一过程提供更快的自定义解压器。用法是包装客户端的Transport字段func ExampleTransport() { // Get an HTTP client. client : http.Client{ // Wrap the transport: Transport: gzhttp.Transport(http.DefaultTransport), } resp, err : client.Get(https://google.com) if err ! nil { return } defer resp.Body.Close() body, _ : ioutil.ReadAll(resp.Body) fmt.Println(body:, string(body)) }Transport的完整签名与可选参数见 transport.gofunc Transport(parent http.RoundTripper, opts ...transportOption) http.RoundTripper // L20 func TransportEnableZstd(b bool) transportOption // L40 func TransportEnableGzip(b bool) transportOption // L48 func TransportCustomEval(fn func(header http.Header) bool) transportOption // L58 func TransportAlwaysDecompress(enabled bool) transportOption // L68即客户端侧也可以单独开关 zstd/gzip 声明或用自定义函数决定何时启用解压甚至强制对所有响应解压。README 给出的基准测试约 127KB JSON 负载含 HTTP 请求处理、解析与解压全过程显示 zstd 路线优势明显Single core: BenchmarkTransport/gzhttp-32 1995 609791 ns/op 214.14 MB/s 10129 B/op 73 allocs/op BenchmarkTransport/stdlib-32 1567 772161 ns/op 169.11 MB/s 53950 B/op 99 allocs/op BenchmarkTransport/zstd-32 4579 238503 ns/op 547.51 MB/s 5775 B/op 69 allocs/op Multi Core: BenchmarkTransport/gzhttp-par-32 29113 36802 ns/op 3548.27 MB/s 11061 B/op 73 allocs/op BenchmarkTransport/stdlib-par-32 16114 66442 ns/op 1965.38 MB/s 54971 B/op 99 allocs/op BenchmarkTransport/zstd-par-32 90177 13110 ns/op 9960.83 MB/s 5361 B/op 67 allocs/op单核下 gzhttp 比标准库快约 1.27 倍zstd 快约 3.2 倍多核并行场景下 gzhttp 约为标准库 1.8 倍zstd 约为标准库 5 倍同时分配次数更少。服务端GzipHandler 与 NewWrapper最简单的用法是GzipHandler传入任意http.Handler返回一个会对响应做压缩的新 handler见 compress.go#L565package main import ( io net/http github.com/klauspost/compress/gzhttp ) func main() { handler : http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set(Content-Type, text/plain) io.WriteString(w, Hello, World) }) http.Handle(/, gzhttp.GzipHandler(handler)) http.ListenAndServe(0.0.0.0:8000, nil) }需要自定义配置时使用NewWrapper(opts...)创建一个可复用的包装器再用它包装任意多个 handler见 compress.go#L580package main import ( io log net/http github.com/klauspost/compress/gzhttp github.com/klauspost/compress/gzip ) func main() { handler : http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set(Content-Type, text/plain) io.WriteString(w, Hello, World) }) // Create a reusable wrapper with custom options. wrapper, err : gzhttp.NewWrapper(gzhttp.MinSize(2000), gzhttp.CompressionLevel(gzip.BestSpeed)) if err ! nil { log.Fatalln(err) } http.Handle(/, wrapper(handler)) http.ListenAndServe(0.0.0.0:8000, nil) }服务端可选项在 compress.go 中的定义位置如下可按需组合选项定义位置作用MinSize(size int)compress.go#L757响应体小于该字节数时不压缩避免小响应上压缩反而更大CompressionLevel(level int)compress.go#L773gzip 压缩等级传-3即gzip.StatelessCompression启用无状态压缩EnableZstd(enable bool)compress.go#L801开关 zstd 压缩ZstdCompressionLevel(level int)compress.go#L810设置 zstd 等级1最快2默认3更好4最佳ZstdImplementation(zw writer.ZstdWriterFactory)compress.go#L818注入自定义 zstd writer 实现工厂接口见 writer/interface.goPreferZstd(prefer bool)compress.go#L827客户端同时以相同 qvalue 接受两种编码时是否优先 zstdEnableGzip(enable bool)compress.go#L835开关 gzip 压缩KeepAcceptRanges()compress.go#L912压缩时不删除Accept-Ranges响应头ContentTypeFilter(compress func(ct string) bool)compress.go#L927自定义 Content-Type 过滤逻辑配合CompressAllContentTypeFiltercompress.go#L1207恢复压缩全部类型RandomJitter(n, buffer int, paranoid bool)compress.go#L971BREACH 缓解用的内容感知填充详见下文zstd 压缩选项组合zstd 默认与 gzip 同时启用客户端两者都支持时优先 zstd。典型开关组合// Disable zstd, only use gzip wrapper, _ : gzhttp.NewWrapper(gzhttp.EnableZstd(false)) // Disable gzip, only use zstd wrapper, _ : gzhttp.NewWrapper(gzhttp.EnableGzip(false)) // Prefer gzip when client accepts both with equal qvalues wrapper, _ : gzhttp.NewWrapper(gzhttp.PreferZstd(false)) // Set zstd compression level (1fastest, 2default, 3better, 4best) wrapper, _ : gzhttp.NewWrapper(gzhttp.ZstdCompressionLevel(int(zstd.SpeedDefault))) // Use custom zstd writer implementation wrapper, _ : gzhttp.NewWrapper(gzhttp.ZstdImplementation(myZstdFactory))默认的 zstd 参数偏保守面向广泛兼容性LevelSpeedFastest1追求最大速度Window size128KB最小化内存占用Concurrency1每请求单线程。Accept-Encoding 协商规则服务端依据Accept-Encoding头选择编码规则为客户端只接受gzip→ 响应 gzip 压缩客户端只接受zstd→ 响应 zstd 压缩两者 qvalue 相同 → 默认使用 zstd可通过PreferZstd(false)反转为 gzip客户端显式给出 qvalue如gzip;q1.0, zstd;q0.5→ qvalue 高者胜出。服务端压缩性能Gzip vs ZstdREADME 给出的服务端基准2KB / 20KB / 100KB 负载显示 zstd 在默认配置下显著快于 gzip 且压缩比不劣Single-threaded: BenchmarkGzipHandler_S2k 137.22 MB/s 3532 B/op 15 allocs/op BenchmarkZstdHandler_S2k 219.21 MB/s 1936 B/op 12 allocs/op (1.6x faster, 45% less memory) BenchmarkGzipHandler_S20k 306.91 MB/s 18616 B/op 18 allocs/op BenchmarkZstdHandler_S20k 434.05 MB/s 7595 B/op 12 allocs/op (1.4x faster, 59% less memory) BenchmarkGzipHandler_S100k 198.96 MB/s 66937 B/op 20 allocs/op BenchmarkZstdHandler_S100k 368.70 MB/s 63021 B/op 16 allocs/op (1.9x faster) Parallel: BenchmarkGzipHandler_P2k 997.01 MB/s 3148 B/op 15 allocs/op BenchmarkZstdHandler_P2k 1440.12 MB/s 1985 B/op 12 allocs/op (1.4x faster) BenchmarkGzipHandler_P20k 2129.70 MB/s 17572 B/op 18 allocs/op BenchmarkZstdHandler_P20k 2928.82 MB/s 7498 B/op 12 allocs/op (1.4x faster) BenchmarkGzipHandler_P100k 1678.72 MB/s 67316 B/op 20 allocs/op BenchmarkZstdHandler_P100k 2392.23 MB/s 61122 B/op 16 allocs/op (1.4x faster)相对原始 nytimes/gziphandler默认设置的对比同样突出单核场景耗时下降 47%~54%并行场景下降 52%~62%字节分配下降 22%~75%benchmark old ns/op new ns/op delta BenchmarkGzipHandler_S2k-32 51302 23679 -53.84% BenchmarkGzipHandler_S20k-32 301426 156331 -48.14% BenchmarkGzipHandler_S100k-32 1546203 818981 -47.03% BenchmarkGzipHandler_P2k-32 3973 1522 -61.69% BenchmarkGzipHandler_P20k-32 20319 9397 -53.75% BenchmarkGzipHandler_P100k-32 96079 46361 -51.75% benchmark old MB/s new MB/s speedup BenchmarkGzipHandler_S2k-32 39.92 86.49 2.17x BenchmarkGzipHandler_P100k-32 1065.79 2208.75 2.07x高级行为Range 请求、Flush 与无状态压缩Range 请求分片ranged请求与压缩配合不佳因此任何带Content-Range头的请求不会被压缩同时压缩发生后请求中设置的Accept-Ranges头会被移除以表明分片请求不受支持。若希望保留该行为之外的头使用KeepAcceptRanges()选项。Flush 支持包装器支持http.Flusher接口。唯一的注意点触发 flush 时 writer 可能尚未收到足够字节来判断是否达到MinSize此时会假定最小尺寸已满足若响应 writer 尚无任何写入则不会 flush 任何东西。无状态压缩Stateless compression当需要同时运行成千上万个压缩器、但每个压缩器活动量很小的场景并非普通逐请求的 Web 服务器可以启用无状态压缩// 二选一 gzhttp.CompressionLevel(-3) gzhttp.CompressionLevel(gzip.StatelessCompression)README 同时建议为 writer 套一层小缓冲的bufio.Writer以提升效率。从 gziphandler 迁移本包精简了部分构造函数替换对照表为GzipHandler(h)→GzipHandler(h)保持不变GzipHandlerWithOpts(opts...)→NewWrapper(opts...)MustNewGzipLevelHandler(n)→NewWrapper(CompressionLevel(n))NewGzipLevelAndMinSize(n, s)→NewWrapper(CompressionLevel(n), MinSize(s))注意默认行为差异本包默认排除部分 mime 类型。若要恢复压缩全部类型使用ContentTypeFilter(gzhttp.CompressAllContentTypeFilter)选项。BREACH 缓解RandomJitter 内容感知填充BREACH 是一类侧信道攻击攻击者控制的输入与响应体中的秘密数据混排通过观察压缩后响应体大小差异推断秘密内容。README 给出的判断标准是响应体不包含任何用户提供的内容则相对安全包含用户输入或拿不准时应启用缓解。gzhttp 可应用 Heal the Breach 一类的内容感知填充improved content aware padding核心选项是RandomJitter// RandomJitter adds 1-n random bytes to output based on checksum of payload. // Specify the amount of input to buffer before applying jitter. // This should cover the sensitive part of your response. // This can be used to obfuscate the exact compressed size. // Specifying 0 will use a buffer size of 64KB. // paranoid will use a slower hashing function, that MAY provide more safety. // If a negative buffer is given, the amount of jitter will not be content dependent. // This provides *less* security than applying content based jitter. func RandomJitter(n, buffer int, paranoid bool) option两种压缩格式下的填充方式gzipjitter 以 gzip 头的 Comment 字段承载1 字节固定开销实际附加 2 → n1 字节含端点zstdjitter 以 skippable frameRFC 8878 Section 3.1.2附加在压缩数据之后8 字节固定开销实际附加 9 → n8 字节含端点。使用示例gzhttp.RandomJitter(32, 50000, false)附加 1~32 字节随机数据字节数取决于前 50000 字节的内容响应不足 50000 字节则覆盖全部gzhttp.RandomJitter(32, -1, false)每次调用附加随机数量的 jitter1~32 字节与内容无关安全性低于内容感知模式适合响应很大、内容确定且无法用合理缓冲覆盖变异点的场景。推荐的通用配置是gzhttp.RandomJitter(32, 0, false)32 字节随机量 默认 64KB 缓冲。两个实现细节需要注意flush 会强制立即应用填充因此只有 flush 之前的数据参与内容感知填充计算填充内容为文本Padding-Padding-Padding-Padding-Pad....填充长度由1 crc32c(payload) MOD n决定paranoid模式改用1 sha256(payload) MOD nbuffer 为负时则直接取自crypto/rand的随机数。Paranoid 模式说明默认用 CRC32 决定填充长度。由于 payload 中包含攻击者未知的元素README 认为没有理由相信攻击者能从中推导或预测该余数但如仍不放心可开启 paranoid 模式改用 SHA256。代价是哈希本身慢约两个数量级整体吞吐大约只下降 10%。buffer 为负非内容感知时 paranoid 无效。VictoriaMetrics 中的真实接入gzhttp 并非孤立的第三方示例——VictoriaMetrics 所有 HTTP 服务端组件共用 lib/httpserver 这一 HTTP 服务框架并在其中直接接入了 gzhttp。关键代码见 httpserver.go#L321-L334var gzipHandlerWrapper func() func(http.Handler) http.HandlerFunc { hw, err : gzhttp.NewWrapper( gzhttp.CompressionLevel(1), // Prefer gzip over zstd compression if the client supports both methods // because some intermediate proxies improperly handle zstd-compressed responses. // See https://github.com/VictoriaMetrics/VictoriaMetrics/issues/10535 gzhttp.PreferZstd(false), ) if err ! nil { panic(fmt.Errorf(BUG: cannot initialize gzip http wrapper: %w, err)) } return hw }()这里的选择很有说明性CompressionLevel(1)选用 gzip 最快等级因为监控场景下 API 响应如/metrics、查询结果对压缩率敏感度低于 CPU 开销PreferZstd(false)即使客户端同时支持 gzip 与 zstd也优先回退到 gzip源码注释明确指出原因是部分中间代理不能正确处理 zstd 压缩响应。这正是上文PreferZstd选项的一个典型生产权衡案例——默认 zstd 优先的策略在存在不合规代理的链路中需要显式反转。该包装器在 httpserver.go#L227-L229 被条件装配if !*disableResponseCompression { h gzipHandlerWrapper(h) }对应命令行参数-http.disableResponseCompressionhttpserver.go#L60其帮助文本为 Disable compression of HTTP responses to save CPU resources. By default, compression is enabled to save network bandwidth。该参数在 victoria_metrics_common_flags.md 及各组件参数文档中均有收录适用于 vmselect、vminsert、vmstorage、vmagent、vmauth、vmbackup 等全部组件。也就是说VictoriaMetrics 各组件 API 的响应压缩本质上都建立在 gzhttp 的NewWrapper可复用包装器之上读者可以通过该 flag 在带宽与 CPU 之间自行权衡。小结gzhttp 以约 1800 行的 Go 代码compress.go、transport.go 及 writer/ 下的可插拔压缩 writer 工厂提供了一套生产级的 HTTP 透明压缩方案服务端一个NewWrapper调用即可完成 gzip/zstd 双编码协商、MinSize/等级/Content-Type 过滤等细粒度控制客户端一个Transport包装即获得比标准库快数倍且分配更少的解压路径BREACH 场景下还有跨 gzip/zstd 通用的RandomJitter填充机制。在 VictoriaMetrics 中它通过-http.disableResponseCompression参数暴露给运维侧PreferZstd(false)的配置则展示了在真实代理链路下对 zstd 优先策略的务实取舍是理解监控数据库 API 层性能与兼容性设计的一个典型切入点。该包采用 Apache 2.0 协议见 vendor/github.com/klauspost/compress/LICENSE。【免费下载链接】VictoriaMetricsVictoriaMetrics: fast, cost-effective monitoring solution and time series database项目地址: https://gitcode.com/GitHub_Trending/vi/VictoriaMetrics创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表