时间和空间的完美结合 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减少了带宽的占用,但是加重了代码的运算,由于工作重复,所以我们又花费了些许的内存将结果缓存了下来,减少了时间上的占用,从而实现了先用时间换空间(减少带宽),又用空间换时间(减少代码的重复运行占用计算机资源)的完美组合。

评论列表