时间和空间的完美结合 Gzip/缓存
在用 go 写工具的时候,嵌入了一个前端页面,由于大多是开发工具,所以前端页面采用了全局引用的方式引入。但是使得首次加载的js会变的巨大。编译前端资源的时候,有显示开启gzip可以减少2/3以上的体积。这是一个十分诱人改造方案。
dist/assets/index.77e70491.js 1341.69 KiB / gzip: 354.13 KiB
在gin中,我找了一个 github.com/gin-contrib/gzip
开源库,这个库是一个中间件可以很方便的集成到 gin 中。同时也确实产生了符合预期的效果,前端资源变的很乐观。
过了一段时间,我进行一次压测,从而开启了一次,时间和空间的双重优化。
首先我用的 bombardier 对嵌入程序的静态网站进行了压测。
bombardier -c 30 -n 300000 http://localhost:9012/dt/actor/assets/index.77e70491.js
但是发现无论无是否开启 gzip,压测的结果都是一样的。这让我很困惑,同时压测结果中的io部分也是一样的。我就查询了一下是不是gzip有一些条件。
gpt 给出的答案是
当你开启了 Gzip 压缩后,浏览器会自动根据请求头中的 Accept-Encoding 字段来判断是否需要解压缩。如果浏览器支持并允许压缩,则会接收到经过 Gzip 压缩后的响应体,然后进行解压缩显示。
但是在使用压测工具时,由于压测工具并不像浏览器一样具有自动解压缩的功能,所以可能会导致输出的文件大小一样的情况。这是因为压测工具通常会直接请求服务器返回的原始数据,而不会自动解压缩。因此,在使用压测工具测试 Gzip 压缩效果时,需要注意这一点。
为了验证 Gzip 压缩是否生效,你可以通过查看响应头中的 Content-Encoding 字段来确定。如果该字段的值为 gzip,则表示服务器已经对响应体进行了 Gzip 压缩。另外,你也可以使用类似 Wireshark 的网络抓包工具来查看网络传输过程中的数据流量,从而进一步确认 Gzip 压缩是否生效。
综上所述,开启 Gzip 压缩后,浏览器会自动进行解压缩显示,而压测工具可能会直接请求服务器返回的原始数据,需要特别注意。为了验证 Gzip 压缩是否生效,可以查看响应头中的 Content-Encoding 字段或使用网络抓包工具来进一步确认。
所以加上 header 再测一下
bombardier -c 30 -n 300000 --header="Accept-Encoding: gzip, deflate, br" http://localhost:9012/dt/actor/assets/index.77e70491.js
Bombarding http://localhost:9012/dt/actor/assets/index.77e70491.js with 300000 request(s) using 30 connection(s)
1130 / 300000 [>---------------------------------------------------------------------------------------------------------------] 0.38% 112/s 44m14s 300000 / 300000 [===============================================================================================================] 100.00% 29301/s 10s
Done!
Statistics Avg Stdev Max
Reqs/sec 114.35 90.19 957.69
Latency 258.87ms 170.42ms 1.67s
HTTP codes:
1xx - 0, 2xx - 1195, 3xx - 0, 4xx - 0, 5xx - 0
others - 0
Throughput: 40.36MB/s
真是不测不知道一测吓一跳,在m1的本子上竟然只能跑到不到100的rps。相比较于正常的其他接口我认为在本机如果带宽没有跑满的情况下,至少应该可以达到1w以上的。这时候就需要想办法对这些静态资源展开营救行动了。
首先是在网上找了一下gin相关的中间件,在没有找到合适的中间件之后。决定采用 sync.Map 手动实现一个,因为静态资源本身不多,且本身存在一份嵌入程序中,所以理论上即使所有的静态资源都缓存在sync.Map 也不会产生较大的影响。
但是需要注意的一点是,浏览器是否支持gzip需要在缓存中间件中做好判断,如果浏览器本身不支持gzip,我们还是要把源文件内容返回。
具体的代码如下
package Middleware
import (
"github.com/gin-gonic/gin"
"net/http"
"strings"
"sync"
)
var gzipCache sync.Map
type cachedResponse struct {
contentType string
body []byte
}
type cachingResponseWriter struct {
gin.ResponseWriter
body *[]byte
}
func (w cachingResponseWriter) Write(b []byte) (int, error) {
*w.body = append(*w.body, b...)
return w.ResponseWriter.Write(b)
}
func CacheMiddleware(c *gin.Context) {
// 如果浏览器支持 Gzip 那么就开启缓存,否则就直接执行下个中间件
if acceptEncoding := c.Request.Header.Get("Accept-Encoding"); strings.Contains(acceptEncoding, "gzip") {
key := c.Request.URL.Path
// 检查缓存
if val, ok := gzipCache.Load(key); ok {
cachedResp := val.(cachedResponse)
c.Header("Content-Encoding", "gzip")
c.Data(http.StatusOK, cachedResp.contentType, cachedResp.body)
c.Abort()
return
}
// 使用自定义的ResponseWriter
var respBody []byte
writer := cachingResponseWriter{c.Writer, &respBody}
c.Writer = writer
// 执行下一个handler
c.Next()
// 缓存响应
if c.Writer.Status() == http.StatusOK {
contentType := c.Writer.Header().Get("Content-Type")
gzipCache.Store(key, cachedResponse{contentType, respBody})
}
} else {
c.Next()
}
}
此时我们可以重新压测一下我们的代码。看看是不是飞一样的感觉。
当然这是带宽的极限,不是代码的极限,如果换成其他较小的静态文件,是可以跑到5/6w rps。这个数据可以说是相当可以了。
bombardier -c 30 -n 300000 --header="Accept-Encoding: gzip, deflate, br" http://localhost:9012/dt/actor/assets/index.77e70491.js
Bombarding http://localhost:9012/dt/actor/assets/index.77e70491.js with 300000 request(s) using 30 connection(s)
300000 / 300000 [===============================================================================================================] 100.00% 21673/s 13s
Done!
Statistics Avg Stdev Max
Reqs/sec 21947.65 3585.98 29672.88
Latency 1.36ms 2.55ms 342.96ms
HTTP codes:
1xx - 0, 2xx - 300000, 3xx - 0, 4xx - 0, 5xx - 0
others - 0
Throughput: 7.46GB/s
当然了大家开发项目的时候很多时候前端资源是和后端资源分开的,前端一般是由 nginx 处理静态资源的压缩和缓存,但是在这种嵌入程序中的静态资源,还是需要进行良好的设计才可以更大的发挥硬件性能。
gzip减少了带宽的占用,但是加重了代码的运算,由于工作重复,所以我们又花费了些许的内存将结果缓存了下来,减少了时间上的占用,从而实现了先用时间换空间(减少带宽),又用空间换时间(减少代码的重复运行占用计算机资源)的完美组合。
评论列表