一次让性能提升 1500% 的经历

一般来说,我这里的基础设施得益于 CDN、OSS 和网关层,到了这里都不用扛太多的流量,只要写的不怎么车祸,后端都不存在什么性能瓶颈。

但是这次要上一个灰度功能,需要和业务相结合,在此之前我们的灰度都是以 DNS 为标准进行地区式的灰度,但是现在流量必须要直接打到后端才能判断,如果前面缓存了就没办法灰度了——因此没办法,我们只能船到桥头自然直——扛吧。

先来简单描述一下我们的代码逻辑:查询 1 -> 处理逻辑 -> 查询 2 -> 查询 3 -> 处理逻辑 -> 灰度判断 -> 返回。

最初保持了最原始的代码,没有设置任何缓存,这在过去是满足需求的,因为缓存是由网关层的反向代理帮我们在 Header 里加上的,包括了 s-max-age 和 max-age。

然后开压:wrk -c20 -t20 -d1m

20 threads and 20 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency   195.58ms   73.94ms 592.81ms   68.39%
    Req/Sec     5.59      2.83    20.00     63.45%
  6155 requests in 1.00m, 3.94MB read
Requests/sec:    102.40
Transfer/sec:     67.19KB

最初我们以为是压得连接数太少,增大了之后 QPS 不增反降,跟做网关的大佬交流了一下之后知道了以下两点:

  1. QPS 并不是压多少有多少,还要看服务器的处理能力
  2. CPU 和内存都只用了百分之十几,说明瓶颈不在这里,很有可能是 IO

而我们的语句中 IO 的部分只有网络 IO 的数据库操作,于是我们为数据库操作加上了一层 LRU 缓存,大致是这样的:

let result
if (cache(query) is not empty) {
  result = cache(query)
} else {
  result = await db(query)
}

cache.set(result)

当然,这只是一段伪代码。

然后我们再压了一次:

  2 threads and 400 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency   337.68ms  485.34ms   2.00s    84.04%
    Req/Sec    79.44     31.82   323.00     81.98%
  9237 requests in 1.00m, 5.92MB read
  Socket errors: connect 0, read 0, write 0, timeout 4952
Requests/sec:    153.71
Transfer/sec:    100.92KB

性能提升很有限(实际上上面这段是有问题的,读者朋友先思考一下,之后再说),这个时候压测的同学忍不住了,开始和我一起 Review 代码,我们又开始怀疑了连接池的大小。

我们将连接池的最大连接数扩大了十倍,接着压,提升依旧不明显。

然后我们进行了本地调试,开始打日志之后发现 SQL 语句在有缓存的情况下依旧进行了重复请求,这个时候才回想到因为我们没有合并请求(也不好合并请求,因为 querystring 不同),所以并发的数据进来如果没有处理完,其实此时还没有存到 cache,别的请求会接着查询,直到第一波查询完毕,严重的影响了 QPS。

所以我们把查询的 Promise 缓存,而不是请求的结果。(实际上之前做过这种操作的项目,但是一时没想起来)

let resultPromise
if (cache(query) is not empty) {
  resultPromise = cache(query)
} else {
  resultPromise = db(query)
}

cache.set(resultPromise)
result = await resultPromise

压测:

  2 threads and 400 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency   206.88ms  305.24ms   2.39s    89.53%
    Req/Sec     1.46k   580.86     2.65k    71.35%
  28138 requests in 10.10s, 18.00MB read
Requests/sec:   2785.71
Transfer/sec:      1.78MB

并且监控中 CPU 和内存占用也比以前更低了(不过当然没有 QPS 明显拉),系统能在同时处理 1500% 的请求了。

当然,为了避免缓存永远被使用,我们这里设置了五分钟的超时缓存,并且仅当没有缓存时才执行 cache.set(因为 set 会刷新缓存时间计数器),也就是说,每五分钟才请求一次,这样的频率相信也是我们的用户(业务方)能接受的范围内。

如果您觉得文章不错,可以通过赞助支持我

标签: 知识

仅有一条评论

  1. 鬼柒

    压测时设置的QPS如果大于实际吞吐的QPS太多,那么服务器会积压很多请求到队列中,一定程度又会反过来影响实际QPS。所以会有不增反减的情况。如果非JS语言的话,缓存那块地方一般是加个读写锁,不过JS 的Promise 这种缓存写法更有趣

添加新评论