问题第一次出现。基于node.js开发的业务系统,对外提供dubbo服务,为第三方提供缓存查询,设置多个业务数据,聚合运行结果。当QPS达到800时(两台虚拟机,每台4Core8G4node进程),监控平台出现大量slowrt警告,平均接口响应达到60+ms,请求告警率达到80%+。为了找出导致服务吞吐量低的罪魁祸首,业务人员查看了请求日志中的所有查询缓存操作,结果显示每个请求的查询缓存时间在50-100ms之间波动。查询redis-server的监控数据,发现server端没有慢查询,整个监控区间server端的处理时间徘徊在40us左右,所以是redis处理能力不足的原因-服务器被排除;通过登录内网机器连续测试到对应redis服务器机器的端到端延迟,发现内网的带宽、延迟和抖动都足够正常,不是原因的问题。因此,错误原因定位在调用redis客户端的业务代码和redis客户端的I/O性能上。本文提到的node-redis客户端使用的是基于node-redis包的二方包,所以排错也是基于node-redis模块。瓶颈在哪里为了在本地模拟线上环境的并发,可以做一个不太严谨的测试:async()=>{letdd=Date.now()letarr=[]for(leti=0;i<200;i++){arr.push(newPromise((res,rej)=>{lethrtime=process.hrtime();client.send_command('get',['key'],function(e,r){letdiff=process.hrtime(hrtime);letcost=(diff[0]*NS_PER_SEC+diff[1])/1000000;console.log(`final:${cost}ms`)res();});}));}awaitPromise.all(arr)console.log('ops/sec:',200*1000/(Date.now()-dd),Date.now()-dd);}会找到每一个的rtrequest会比上一个请求大](https://si.geilicdn.com/viewm...上一个请求的rt已经达到了257ms!虽然节点单进程像示例代码一样并发执行200个get请求非常少见且愚蠢(示例代码的优化在下一节中介绍),但是对于这个例子,必须找到请求延迟增加的原因。继续分析,redis客户端采用单连接模式,底层采用非阻塞网络I/O。socket.recv()是通过在节点级别监听socket的数据事件完成的,所以先分析redis-client的读性能如何:](https://si.geilicdn.com/viewm...上图中每条日志的含义分别表示:-dataeventstriggertimes:socket数据事件触发的次数-dataeventstartfrompreventevent:dataeventTimeintervalfromlasttrigger-dataeventsexectime(ms):这个事件处理函数的执行时间35ms,这个现象会在后续的请求中重现,所以这会导致200个并发的查询请求,每个请求的rt会相应增加,有的响应会间隔30ms。出自上表面上,问题是redis-server发送的响应不是一个数据块,而是多个数据块,导致触发socket的数据事件过多,数据事件抖动过大,导致响应之间有30ms的突变(数据事件不能同时触发两次,下一次数据事件只能在每次数据事件处理函数执行完毕后触发);当然也可能跟socket写(也就是发送req)有关,比如缓存请求等,为了继续探索,监听和socket写相关接口_write(),记录下距离上次的间隔每次向socket写入数据时write:](https://si.geilicdn.com/viewm...可以看出,当使用redis-client发送请求时,write方式不是瓶颈.用同样的方法监听socket的push()(该方法触发了socket的data事件),发现socket的数据到达间隔波动很大:](https://si.geilicdn.com/viewm...所以redis-client并发请求造成的大响应rt抖动与单连接下响应数据到达本地的时刻有关,可能与底层libuv的缓存策略有关(作者没有重复Probe下来)。](https://si.geilicdn.com/viewm...在node实例中,使用单连接与redis服务器通信,高并发下,会有一个队列等待响应,可能会有是一个responsert雪崩效应(如上demo所示),所以需要尽量减少或者缓存client请求的个数,分批发送调优1.pipeline(涉及写入方式和时序)2.script对于pipeline方式,redisserver是默认支持的,通俗的说,pipeline可以把一系列的请求组合起来一次发送,一次性得到这些请求对应的结果,所以这种方式可以有效减少响应次数,从而减少socket触发数据事件的次数,尽快获取响应体。](https://si.geilicdn.com/viewm...需要强调的是,在node中,通过底层socket的_writev一次发送多个redis命令。_writev也称为聚合Write,支持通过一次系统调用将多条数据在不同的buffer中写入到目标流中,因此性能比每次将单个数据写入单个buffer中要好很多。在node的Writeable对象中,有cork和uncork方法,通过这两个方法可以将多条数据缓存在node写流中,通过_writev一次发送。得到_writev的数据结构后,redis根据resp协议解析出命令集,缓存到队列中,直到收到exec命令,批量执行命令集,所有命令执行结果为转换成数组返回给redis客户端。这样一写一读就可以实现高性能的I/O。async()=>{letdd=Date.now()letbatch=awaitclient.batch();for(leti=0;i<200;i++){batch.get('vdWeex_com.koudai.weidian.buyer_1');}让rt=awaitbatch.exec();process.exit();}对于脚本方法,由redis客户端传入脚本命令,在服务端执行脚本逻辑,批量执行命令,返回结果。也是写一次读一次。收获1.nodesocket默认使用writevset写入2.非依赖的批量请求使用pipeline3.eval脚本解决依赖的批量请求4.redis的高性能体现在服务器的处理能力上,但是瓶颈经常出现出现在客户端,所以客户端是增强I/O能力,并发并行多客户端是高并发解决方案
