提升node.js中使用redis的性能遇到的问题及解决方法


Posted in Javascript onOctober 30, 2018

问题初现

某基于node.js开发的业务系统向外提供了一个dubbo服务,提供向第三方缓存查询、设置多项业务数据并聚合操作结果。在QPS达到800时(两台虚拟机,每台机器4Core8G4node进程),在监控平台上出现了非常多的slow rt警告,平均接口响应达到60+ms,请求报警率达到80%+。

为找到造成该服务吞吐量过低的罪魁祸首,业务人员在请求日志中打点了所有查询缓存的操作,结果显示每个请求查询缓存耗时在50-100ms之间跳动。查询了redis-server的监控数据发现,不存在server端的慢查询,在整个监控区间内服务端处理时间在40us徘徊,因此排除了redis-server的处理能力不足原因;

通过登录内网机器进行不断测试到对应redis server机器的端到端时延发现内部局域网的带宽、时延与抖动足够正常,都不是造成该问题的原因。

因此,错误原因定位到了调用redis client的业务代码以及redis client的I/O性能。

本文中提到的node redis client采用的基于node-redis封装的二方包,因此问题排查也基于node-redis这个模块。

瓶颈在哪

为了在本地模拟线上环境的并发,可以做一个不是很严谨的测试:

async ()=>{
  let dd = Date.now()
  let arr = []
  for(let i=0;i<200;i++){
    arr.push(new Promise((res,rej)=>{
      let hrtime = process.hrtime();
      client.send_command('get',['key'], function(e,r) {
      let diff = process.hrtime(hrtime);
      let cost = (diff[0] * NS_PER_SEC + diff[1])/1000000;
      console.log(`final: ${cost} ms`)
      res();
      });
    }));
  }
  await Promise.all(arr)
  console.log('ops/sec:',200*1000/(Date.now() - dd),Date.now() - dd);
}

会发现每个请求的rt都会比前一个请求来的大

提升node.js中使用redis的性能遇到的问题及解决方法

 最后一个请求的rt竟然达到了257 ms!虽然在node单进程像示例代码那样并发执行200次get请求是非常少见而且愚蠢的(关于示例代码的优化在在下节讲述),但是针对这个示例必须找到请求delay增加的原因。

 为此继续分析,redis client采用的是单连接模式,底层采用的非阻塞网络I/O,socket.recv()在node层面是通过监听socket的data事件完成的,因此先分析redis-client读性能如何:

提升node.js中使用redis的性能遇到的问题及解决方法

上图每段日志的含义分别表示:

- data events trigger times: socket data事件触发的次数
- data event start from prevent event: data事件距离上次触发的时间间隔
- data events exec time(ms): 本次事件处理函数执行时间

 上图只是截取了最初的请求日志,发现当第6次触发data事件时,竟然距离上次触发事件隔了35ms,在随后的请求中会复

现这种现象,因此这也就导致了在并发200次查询请求时,每个请求的rt都会随之增大,并且有些响应之间间隔了30ms。

从表象看造成问题在于redis-server发送的响应不是一个数据块,而是多个数据块导致触发socket的data事件过多,而且data事件抖动过大导致响应之间存在30ms的突变(data事件是无法同时触发两次的,每次data事件处理函数执行完后才能继续触发下一个data事件);当然也有可能和socket写入(即发送req)有关,如缓存请求等。为了继续探查,监控与socket写入相关的接口 **_write()**,记录每次写入socket的数据时距离上一次写入的间隔:

提升node.js中使用redis的性能遇到的问题及解决方法

可见,在使用redis-client发送请求时,write方法也不是瓶颈。

采用同样方法,对socket的push()(该方法触发socket的data事件)进行监控,发现socket的数据到达间隔抖动非常大:

提升node.js中使用redis的性能遇到的问题及解决方法

 因此,造成redis-client并发请求下响应rt抖动较大的情况与单连接下响应数据到达本地的时刻有关,具体可能与底层libuv的缓存策略有关(笔者并未再往下探查)。

提升node.js中使用redis的性能遇到的问题及解决方法

在一个node实例中通过一个单连接与redis server通信,在高并发下会出现排队等待响应的情况,并且有可能会出现响应rt雪崩效应(如上文demo所示),因此需要尽可能减少或缓存客户端的请求数量,进行批量发送。

调优

1. pipeline(涉及到写模式及时序)
2. script

对于pipeline方式,redis server是默认支持的。通俗点说,pipeline可以合并一系列请求一次发送,并将这些请求对应的结果一次性拿到。因此这种方式可以有效减少响应次数,从而减少socket触发data事件的次数,尽可能快的拿到响应体。

提升node.js中使用redis的性能遇到的问题及解决方法

 需要强调的是,在node中,是通过底层socket的**_writev**实现一次发送多条redis命令的,_writev又叫做聚合写,它支持将不同缓冲区的多条数据通过一次系统调用写入目标流,因此性能上比每次写单个缓冲区的单个数据来的好得多。在node的Writeable对象中,有cork和uncork方法,通过这两个方法可以在node write stream中缓存多条数据,通过_writev一次性发送。

关于 _writev的数据结构

redis在拿到数据后,根据resp协议解析出命令集合缓存在队列中,直到收到exec命令,开始批量执行命令集,并将所有命令执行的结果转换为数组返回给redis client。这样就可以通过一次写、一次读实现高性能I/O。

async ()=>{
  let dd = Date.now()
  let batch = await client.batch();
  for(let i=0;i<200;i++){
    batch.get('vdWeex_com.koudai.weidian.buyer_1');
  }
  let rt = await batch.exec();
  process.exit();
}

而对于script方法,则是由redis client传入script命令,在server端执行script逻辑,批量执行命令,并返回结果。同样是一次写、一次读。

收获

1. node socket默认采用writev 集合写
2. 无依赖批量请求采用pipeline
3. eval script解决有依赖批量请求
4. redis高性能体现在服务端处理能力,但瓶颈往往出现在客户端,因此增强客户端I/O能力与并发并行多客户端才是高并发解决方案

Javascript 相关文章推荐
javascript 打印页面代码
Mar 24 Javascript
js string 转 int 注意的问题小结
Aug 15 Javascript
网页下载文件期间如何防止用户对网页进行其他操作
Jun 27 Javascript
jQuery中next()方法用法实例
Jan 07 Javascript
jQuery简单实现隐藏以及显示特效
Feb 26 Javascript
常用jQuery代码分享
Jul 14 Javascript
JavaScript仿支付宝密码输入框
Dec 29 Javascript
Vue系列:通过vue-router如何传递参数示例
Jan 16 Javascript
a标签置灰不可点击的实现方法
Feb 06 Javascript
基于iScroll实现下拉刷新和上滑加载效果
Jul 18 Javascript
Vue2.0 实现歌手列表滚动及右侧快速入口功能
Aug 08 Javascript
一定要知道的 25 个 Vue 技巧
Nov 02 Vue.js
小程序云开发部署攻略(图文教程)
Oct 30 #Javascript
傻瓜式解读koa中间件处理模块koa-compose的使用
Oct 30 #Javascript
微信小程序实现单选功能
Oct 30 #Javascript
基于vue2.0实现仿百度前端分页效果附实现代码
Oct 30 #Javascript
小程序实现多选框功能
Oct 30 #Javascript
vue-cli项目配置多环境的详细操作过程
Oct 30 #Javascript
详解微信小程序中组件通讯
Oct 30 #Javascript
You might like
咖啡界又出新概念,无需咖啡豆的分子咖啡
2021/03/03 咖啡文化
如何使用php绘制在图片上的正余弦曲线
2013/06/08 PHP
Mootools 图片展示插件(lightbox,ImageMenu)收集集合
2010/05/21 Javascript
JS复制到剪贴板示例代码
2013/10/30 Javascript
使用script的src实现跨域和类似ajax效果
2014/11/10 Javascript
jQuery中ready事件用法实例
2015/01/19 Javascript
JavaScript自定义数组排序方法
2015/02/12 Javascript
jQuery实现contains方法不区分大小写的方法
2015/02/13 Javascript
使用JavaScript和CSS实现文本隔行换色的方法
2015/11/04 Javascript
项目实践一图片上传之form表单还是base64前端图片压缩(前端图片压缩)
2016/07/28 Javascript
基于JavaScript实现跳转提示页面
2016/09/24 Javascript
jQuery实现弹窗居中效果类似alert()
2017/02/27 Javascript
jQuery查找dom的几种方法效率详解
2017/05/17 jQuery
angular学习之动态创建表单的方法
2018/12/07 Javascript
使用JQuery自动完成插件Auto Complete详解
2019/06/18 jQuery
微信小程序 自定义弹窗实现过程(附代码)
2019/12/05 Javascript
微信小程序学习总结(四)事件与冒泡实例分析
2020/06/04 Javascript
理解生产者消费者模型及在Python编程中的运用实例
2016/06/26 Python
人工智能最火编程语言 Python大战Java!
2017/11/13 Python
Python基于多线程实现ping扫描功能示例
2018/07/23 Python
Python根据文件名批量转移图片的方法
2018/10/21 Python
在PyCharm中批量查找及替换的方法
2019/01/20 Python
python代码编写计算器小程序
2020/03/30 Python
python代码实现逻辑回归logistic原理
2019/08/07 Python
解决Python中报错TypeError: must be str, not bytes问题
2020/04/07 Python
美国运动鞋和服装网上商店:YCMC
2018/09/15 全球购物
免税水晶:Duty Free Crystal
2019/05/13 全球购物
软件测试笔试题
2012/10/25 面试题
中英文自我评价常用句型
2013/12/19 职场文书
教师个人考察材料
2014/12/16 职场文书
小学生五一劳动节演讲稿
2015/03/18 职场文书
2015年秘书个人工作总结
2015/04/25 职场文书
房屋维修申请报告
2015/05/18 职场文书
2016年国庆节67周年活动总结
2016/04/01 职场文书
利用Python实现模拟登录知乎
2022/05/25 Python
MySQL主从切换的超详细步骤
2022/06/28 MySQL