一次让性能提升 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 不增反降,跟做网关的大佬交流了一下之后知道了以下两点:
- QPS 并不是压多少有多少,还要看服务器的处理能力
- 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 会刷新缓存时间计数器),也就是说,每五分钟才请求一次,这样的频率相信也是我们的用户(业务方)能接受的范围内。
植入部分
如果您觉得文章不错,可以通过赞助支持我。
如果您不希望打赏,也可以通过关闭广告屏蔽插件的形式帮助网站运作。
压测时设置的QPS如果大于实际吞吐的QPS太多,那么服务器会积压很多请求到队列中,一定程度又会反过来影响实际QPS。所以会有不增反减的情况。如果非JS语言的话,缓存那块地方一般是加个读写锁,不过JS 的Promise 这种缓存写法更有趣
这个标题有点UC震惊部的意思啊