背景
最近在做一个 web 版的展示大屏,前端靠 HTTP(S)+JSON 和后端交互,部分图形是密集的地理点位和时间序列,HTTP 返回数据量较大,公网上加载速度不佳。
调研
考虑压缩 HTTP 返回,选了 gzip 这个常规选项。
gzip 在压缩时,得考虑几点:
- 内容类型是否对压缩友好:例如 JPEG 本身已经压缩过,再次压缩收益太小,费力不讨好;
- 内容大小是否值得压缩:太小的内容压缩效果不好(甚至有可能比原文还大,毕竟有字典等开销),另外小内容本身传输时间短,直接放过去还能节约 CPU 和内存。
先考察了 Nginx
- Nginx 的压缩一般是用
ngx_http_gzip_module
; - 根据
Content-Type
判断内容类型是否对压缩友好,要压缩的类型由用户定义; - 根据返回的
Content-Length
判断内容大小是否值得压缩;
当返回的 JSON 大于 2KB 时,Golang 会使用 chunk 的形式传输,这个时候没有 Content-Length
,ngx_http_gzip_module
不会进行内容压缩。
又考察了下 gin-contrib/gzip
- 根据扩展名判断内容类型是否对压缩友好;
- 根据 Path 判断是否启用压缩;
- 不支持判断内容大小是否值得压缩。
根据 Path 来判断确实可行,但太死板,业务和中间件耦合了。
自己造了个轮子
特性
看了 gin-contrib/gzip 和 Caddy 的实现后,我造了个自己的轮子,欢迎试用、Star 以及反馈:
仓库地址: https://github.com/nanmu42/gzip
文档: https://github.com/nanmu42/gzip/blob/master/README.Chinese.md
- 支持 Gin 和 标准库 net/http ;
- 支持基于
Content-Type
、Content-Length
、扩展名判断是否压缩; - 启用压缩的阈值用户可以自定义;
- 压缩级别用户可以自定义;
- 不压缩已经压缩过的返回,不压缩 Head 请求的返回,不影响 HTTP Upgrade ;
- 中间件初始化简单,集成容易;
更进一步
还记得返回的 JSON 大于 2KB 时,Golang 会使用 chunk 的形式传输,这个时候没有 Content-Length 带来无法判断内容是否值得压缩的问题吗?
我取了个巧,如果 Content-Length 不存在,中间件会去观察 http.ResponseWriter.Write(data []byte)
的第一次调用时的 len(data)
,如果此时 len(data)
已经大于启用压缩的阈值,那么可以安全地开始压缩。
调优
这里分享在造轮子时的两个调优点。
此部分可配合项目各阶段 benchmark 食用: https://github.com/nanmu42/gzip/blob/a0b9dac85d4a0a72f4a2183d3b9bfadf215f2168/docs/benchmarks.md
AC 自动机
原本我使用 Strings.Contains()
配合循环来判断文件后缀 /MIME 是否在支持压缩的列表中,但 benchmark 下来效果不太好。做了一些搜索后发现 Cloudflare 实现了一个 AC 自动机来做这个事情。和维护者聊了聊之后,我用了它的一个 fork: https://github.com/signalsciences/ac
Sync.Pool
Sync.Pool
用来做对象重用,以降低系统内存分配和 Go 垃圾回收的压力,一开始我只对 gzip.Writer 做了对象重用,但发现中间件对内存的影响还有一些大,后来我用了第二个 Sync.Pool
重用 wrapper,内存使用量和 CPU 时间都有了可观的改善。
两个调优之后,CPU 时间下降为调优前的 40%,内存使用量下降为原先的一半。
欢迎来到这里!
我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。
注册 关于