1.子请求变为协程
案例:
用户需求调用批量接口mget(50Key)要求性能高,吞吐高,响应快。
问题:
由于最早用的Nginx
Subrequest来做批量接口请求的处理,性能一直不高,CPU利用率也不高,QPS提不起来。通过火焰图观察分析子请求开销比较大。
解决方案:
子请求效率较低,因为它需要重新从Server Rewrite开始走一遍Request处理的PHASE。并且子请求共享父请求的内存池,子请求同时并发度过大,导致内存较高。
协程轻量级的线程,占用内存少。经过调研和测试,单机一两百万个协程是没有问题的,
并且性能也很高。
优化前:
a)用户请求mget(k1,k2)到Proxy
b)Proxy根据k1,k2分别发起子请求subrequest1,subrequest2
c)子请求根据key计算slotid,然后去缓存路由表查找节点
d)子请求请求Redis Cluster的相关节点,然后响应返回给Proxy
e)Proxy会合并所有的子请求返回的结果,然后进行解析包装返回给用户
优化后:
a)用户请求mget(k1,k2)到Proxy
b)Proxy根据k1,k2分别计算slotid,然后去缓存路由表查找节点
c)Proxy发起多个协程coroutine1, coroutine2并发的请求Redis Cluster的相关节点
d)Proxy会合并多个协程返回的结果,然后进行解析包装返回给用户
2.合并相同槽,批量执行指令,减少网络开销
案例:
用户需求调用批量接口mget(50key)要求性能高,吞吐高,响应快。
问题:
经过上面协程的方式进行优化后,发现批量接口性能还是提升不够高。通过火焰图观察分析网络开销比较大。
解决方案:
因为在Redis
Cluster中,批量执行的key必须在同一个slotid。所以,我们可以合并相同slotid的key做为一次请求。然后利用Pipeline/Lua+EVALSHA批量执行命令来减少网络开销,提高性能。
优化前:
a)用户请求mget(k1,k2,k3,k4)到Proxy。
b)Proxy会解析请求串,然后计算k1,k2,k3,k4所对应的slotid。
c)Proxy会根据slotid去路由缓存中找到后端服务器的节点,并发的发起多个请求到后端服务器。
d)后端服务器返回结果给Proxy,然后Proxy进行解析获取key对应的value。
e)Proxy把key,value对应数据包装返回给用户。
优化后:
a)用户请求mget(k1,k2,k3,k4)到Proxy。
b)Proxy会解析请求串,然后计算k1,k2,k3,k4所对应的slotid,然后把相同的slotid进行合并为一次Pipeline请求。
c)Proxy会根据slotid去路由缓存中找到后端服务器的节点,并发的发起多个请求到后端服务器。
d)后端服务器返回结果给Proxy,然后Proxy进行解析获取key对应的value。
e)Proxy把key,value对应数据包装返回给用户。
3.对后端并发度的控制
案例:
当用户调用批量接口请求mset,如果k数量几百个甚至几千个时,会导致Proxy瞬间同时发起几百甚至几千个协程同时去访问后端服务器Redis Cluster。
问题:
RedisCluster同时并发请求的协程过多,会导致连接数瞬间会很大,甚至超过上限,CPU,连接数忽高忽低,对集群造成不稳定。
解决方案:
单个批量请求对后端适当控制并发度进行分组并发请求,反向有利于性能提升,避免超过Redis Cluster连接数,同时Redis Cluster波动也会小很多,更加的平滑。
优化前:
a)用户请求批量接口mset(200个key)。(这里先忽略合并相同槽的逻辑)
b)Proxy会解析这200个key,会同时发起200个协程请求并发的去请求Redis Cluster。
c)Proxy等待所有协程请求完成,然后合并所有协程请求的响应结果,进行解析,包装返回给用户。
优化后:
a)用户请求批量接口mset(200个key)。(这里先忽略合并相同槽的逻辑)
b)Proxy会解析这200个key,进行分组。100个key为一组,分批次进行并发请求。
c)Proxy先同时发起第一组100个协程(coroutine1, coroutine100)请求并发的去请求Redis Cluster。
d)Proxy等待所有协程请求完成,然后合并所有协程请求的响应结果。
e)Proxy然后同时发起第二组100个协程(coroutine101, coroutine200)请求并发的去请求Redis Cluster。
f)Proxy等待所有协程请求完成,然后合并所有协程请求的响应结果。
g)Proxy把所有协程响应的结果进行解析,包装,返回给用户。
4.单Work分散到多Work
案例:
当用户调用批量接口请求mset,如果k数量几百个甚至几千个时,会导致Proxy瞬间同时发起几百甚至几千个协程同时去访问后端服务器Redis Cluster。
问题:
由于Nginx的框架模型是单进程单线程,所以Proxy发起的协程都会在一个Work上,这样如果发起的协程请求过多就会导致单Work CPU打满,导致Nginx的每个Work CPU使用率非常不均,内存持续暴涨的情况。(nginx的内存池只能提前释放大块,不会提前释放小块)
解决方案:
增加一层缓冲层代理,把请求的数据进行拆分为多份,然后每份发起请求,控制并发度,在转发给Proxy层,避免单个较大的批量请求打满单Work,从而达到分散多Work,达到Nginx多个Wrok CPU使用率均衡。
优化前:
a)用户请求批量接口mset(200个key)。(这里先忽略合并相同槽的逻辑)
b)Proxy会解析这200个key,会同时发起200个协程请求并发的去请求Redis Cluster。
c)Proxy等待所有协程请求完成,然后合并所有协程请求的响应结果,进行解析,包装返回给用户。
优化后:
a)用户请求批量接口mset(200个key)。(这里先忽略合并相同槽的key的逻辑)
b)Proxy会解析这200个key,然后进行拆分分组以此来控制并发度。
c)Proxy会根据划分好的组进行一组一组的发起请求。
d)Proxy等待所有请求完成,然后合并所有协程请求的响应结果,进行解析,包装返回给用户。
总结,经过上面一系列优化,我们可以来看看针对批量接口mset(50个k/v)性能对比图,Nginx Proxy的响应时间比Java版本的响应时间快了5倍多。
Java版本:
Nginx版本: