V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
The Go Programming Language
http://golang.org/
Go Playground
Go Projects
Revel Web Framework
nanmu42
V2EX  ›  Go 编程语言

经历心得分享:用 Web 框架 Gin 开发 API,但业务返回的 JSON 太大,调研了一圈,最后自己写了个中间件做 gzip 压缩,记录下调研和开发调优时的心得

  •  
  •   nanmu42 ·
    nanmu42 · 2019-12-13 16:33:23 +08:00 · 5947 次点击
    这是一个创建于 1806 天前的主题,其中的信息可能已经有所发展或是发生改变。

    背景

    最近在做一个 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-Lengthngx_http_gzip_module不会进行内容压缩。

    又考察了下 gin-contrib/gzip

    • 根据扩展名判断内容类型是否对压缩友好;
    • 根据 Path 判断是否启用压缩;
    • 不支持判断内容大小是否值得压缩。

    根据 Path 来判断确实可行,但太死板,业务和中间件耦合了。

    自己造了个轮子

    特性

    看了gin-contrib/gzipCaddy的实现后,我造了个自己的轮子,欢迎试用、Star 以及反馈:

    仓库地址: https://github.com/nanmu42/gzip

    文档: https://github.com/nanmu42/gzip/blob/master/README.Chinese.md

    • 支持 Gin 和 标准库 net/http ;
    • 支持基于Content-TypeContent-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%,内存使用量下降为原先的一半。

    19 条回复    2019-12-15 03:40:54 +08:00
    lhx2008
        1
    lhx2008  
       2019-12-13 16:36:29 +08:00 via Android
    emmm,这种轮子也要搞的吗,nginx 和 go 两边都没法改?
    nanmu42
        2
    nanmu42  
    OP
       2019-12-13 16:41:53 +08:00
    @lhx2008 感谢回复。

    原因调研那段也提到了,返回的 JSON 大于 2KB 时,Golang 会使用 chunk 的形式传输,这个时候没有 Content-Length,Nginx 这边判断不了是否压缩。

    其实还有个原因,我们的应用部署在云服务的 k8s 集群里,网络本身由云服务的 ingress 提供,不太想再加一层 Nginx.
    monsterxx03
        3
    monsterxx03  
       2019-12-13 17:02:54 +08:00   ❤️ 2
    端 client 显示发送 Accept-Encoding: deflate, gzip   header 的话, nginx 是能 gzip chunk response 的.

    你可以用 curl --compressed http://xxx 然后 tcpdump 抓包看一下, 结果会被压缩的.

    用 k8s 的话, 加个 nginx 做 sidecar container 也不麻烦, 虽然我能理解你不想加的原因...
    janxin
        4
    janxin  
       2019-12-13 17:30:02 +08:00
    咦,这个问题还真没注意过,毕竟目前 JSON 都不大
    Ehco1996
        5
    Ehco1996  
       2019-12-13 18:28:07 +08:00
    请问一下云服务提供的默认的 ingress 是啥?阿里云貌似用的还是 nginx,难道不能简单的配置一下 ingress 来实现压缩的目的么?
    optional
        6
    optional  
       2019-12-13 19:21:40 +08:00 via iPhone
    nginx 可以的。。。
    bolide2005
        7
    bolide2005  
       2019-12-13 19:59:10 +08:00
    我们的服务( API Gateway )用 golang 返回过包括语音在内的大文件,并没有遇到需要自己压缩的情况,感觉有点问题……
    xiangyuecn
        8
    xiangyuecn  
       2019-12-13 20:54:39 +08:00
    不知道文中 ResponseWriter 这玩意有没有 buffer、flush 的概念,也许哪里调大点屁事都没有了😂
    diveIntoWork
        9
    diveIntoWork  
       2019-12-13 23:55:54 +08:00 via Android
    前端怎么解压缩呢。。
    securityCoding
        10
    securityCoding  
       2019-12-14 10:31:26 +08:00
    @diveIntoWork 浏览器引擎干的事 ,前端并不需要关注 , 压缩传输在 http 协议标准中有定义
    richzhu
        11
    richzhu  
       2019-12-14 11:28:27 +08:00
    目前在开发一些东西,想请教一下,如果每个 json 大概在 20 - 50k 之间,内网环境 有必要使用 gzip 吗? 或者说 使用 gzip 优化效果明显吗?
    ihciah
        12
    ihciah  
       2019-12-14 12:20:59 +08:00
    待匹配字符串集完全固定并且很小,感觉用 Map 就可以了
    nanmu42
        13
    nanmu42  
    OP
       2019-12-14 12:35:34 +08:00 via Android
    @richzhu
    我们的是上 M 了(大屏显示用的时序数据),公网传输。

    内网环境 5M 内应该都可以接受的。
    nanmu42
        14
    nanmu42  
    OP
       2019-12-14 12:38:58 +08:00 via Android
    @ihciah 是的,完全匹配 map 是个好办法。
    MIME 不是完全匹配(可能会在末尾多一个; charset: utf8 ),所以没用上。
    benchmark 下来,这个用例下 AC 自动机比 String.Contains 还要快。
    nanmu42
        15
    nanmu42  
    OP
       2019-12-14 12:40:59 +08:00 via Android
    @xiangyuecn 有 2K 的 buffer,私有不可调。
    我觉得压缩这事在 net/http 里解决了的话真的挺好的,当然用中间件也不是不行。
    nanmu42
        16
    nanmu42  
    OP
       2019-12-14 12:41:37 +08:00 via Android
    @diveIntoWork 对前端透明,浏览器解决了,不用关注的。
    ccpp132
        17
    ccpp132  
       2019-12-14 13:23:45 +08:00 via Android
    你的需求把 mime 切一下再用 map 就行了。多模匹配还没必要
    nanmu42
        18
    nanmu42  
    OP
       2019-12-14 15:30:22 +08:00 via Android
    @ccpp132 AC 自动机确实快得飞起,也用不着自己去切,方便。
    xcstream
        19
    xcstream  
       2019-12-15 03:40:54 +08:00
    我觉得研究压缩优化空间不大 加带宽或者用前端缓存更有用
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2720 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 27ms · UTC 12:13 · PVG 20:13 · LAX 04:13 · JFK 07:13
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.