当前位置:Gxlcms > 数据库问题 > Node.js——Node+Mongodb 架构常见性能问题 (转)

Node.js——Node+Mongodb 架构常见性能问题 (转)

时间:2021-07-01 10:21:17 帮助过:34人阅读

转自: https://zhuanlan.zhihu.com/p/56010506

Node+Mongodb 架构常见性能问题总结

简介

目前的我们的一个项目,后端使用 node+mongodb+redis 搭建,已运行 2 年,目前日 pv 在 100W 左右。

配置:

两台阿里云 ECS (2 vCPU 4 GB ) 一个阿里云 mongodb。(4核8G,节点数,三节点)

此文由近两年来实际血泪经验,无教科书式说教。

常见现象1:Web 服务超时,node 服务内存占用高。Mongodb CPU ,IOPS 高

正常情况下单个 node 服务占用内存在 100-200M 左右,此时内存可能涨到500-600M以上,Mongodb CPU 超过90%。web 服务失去响应。

技术图片

 

问题原因:

node 每增加一个回调/promise 异步任务,都会创建 一个microtask到执行队列,由于太多的microtask等待处理完成,新的microtask 在任务队列的尾端,得不到处理,web QPS 也因此迅速下降,造成 web 服务内存占用高。这种情况一般是后端 Mongodb 处理不及时拖累 node 服务,这是最常见的性能问题。

调试方法:

登录 mongo 执行执行执行 db.currentOp(),查看是否有执行慢的的任务。如:

  1. <code class="language-text">mgset-2286:PRIMARY> db.currentOp()
  2. {
  3. "inprog" : [
  4. {
  5. "desc" : "conn33170850",
  6. "threadId" : "47003141687040",
  7. "connectionId" : 33170850,
  8. "client" : "10.24.141.149:49804",
  9. "active" : true,
  10. "opid" : -1695682558,
  11. "secs_running" : 0,
  12. "microsecs_running" : NumberLong(9),
  13. "op" : "command",
  14. "ns" : "admin.$cmd",
  15. "query" : {
  16. "currentOp" : 1
  17. },
  18. "numYields" : 0,
  19. "locks" : {
  20. },
  21. "waitingForLock" : false,
  22. "lockStats" : {
  23. }
  24. },
  25. {
  26. "desc" : "conn33150143",
  27. "threadId" : "47003176310528",
  28. "connectionId" : 33150143,
  29. "client" : "10.24.141.149:55818",
  30. "active" : true,
  31. "opid" : -1695713531,
  32. "secs_running" : 148,
  33. "microsecs_running" : NumberLong(148290542),
  34. "op" : "remove",
  35. "ns" : "mydb.bhd_record",
  36. "query" : {
  37. "recType" : 9,
  38. "content.id" : ObjectId("5a044ed48162b041725e3435")
  39. },
  40. "numYields" : 283277,
  41. "locks" : {
  42. "Global" : "w",
  43. "Database" : "w",
  44. "Collection" : "w"
  45. },
  46. "waitingForLock" : false,
  47. "lockStats" : {
  48. "Global" : {
  49. "acquireCount" : {
  50. "r" : NumberLong(283278),
  51. "w" : NumberLong(283278)
  52. }
  53. },
  54. "Database" : {
  55. "acquireCount" : {
  56. "w" : NumberLong(283278)
  57. }
  58. },
  59. "Collection" : {
  60. "acquireCount" : {
  61. "w" : NumberLong(283278)
  62. }
  63. }
  64. }
  65. }
  66. ],
  67. "ok" : 1</code>

}

secs_running ,microsecs_running 反映了某些执行语句执行时间,如上问题上mydb.bhd_record 查询时字段未能命中索引,查询需要全表扫描。
使用db.currentOp 可以检查出常见的索引问题,以及错误的执行了 MapReduce 函数导致的性能问题。

优化策略:

  1. 优化索引:如上可能需要对 recType 和 content.id 字段加索引,Monogo 里建议更多使用复合索引,复合索引的排序顺序是:粗->细,如recType 是对表的粗分,假如表有1KW 条数据,recType 有 10 种, content.id 是唯一的,那么复合索引的排列顺序是,recType -> content.id。
  2. 尽量避免在 生产环境上使用 MapReduce,MapReduce 是服务器级全局读写锁,aggregate 可以一定程度代替 mapreduce 。
  3. 使用如 Redis 缓存将 必须使用 mapreduce或慢查询结果缓存。

在实际生产环境上,服务器挂掉两次是因为 Redis 服务器配置错误,缓存失效慢查询被并发。

常见现象2:Web 服务超时,node 服务内存略高,Mongodb CPU,IOPS 都正常

如果在此时多配 node 服务负载均衡会有一定的效果。或者把 node 服务重启,速度会马上提升,但很快就不行了。

问题原因:

这种情况下数据库并没有太大压力,但依然响应慢。
这个问题依然要回到 node 的执行机制, node 服务里很多执行时间比较久的microtask没有完成,造成任务队列迅速累积,由于代码逻辑流程问题,但不一定是因为数据库响应不及时。
如一个订单复杂的处理流程,要去不同的服务接口较验或者每个数据库操作也很短,但等待有多个步骤完成,整个流程累计执行时间较长。

优化策略:

将执行时间久的任务,放到任务队列组件中执行。node 任务队列组件推荐 Kue,Bull, Kue 和 Bull 都使用 Redis 做后端。

  1. <code class="language-text">var kue = require(‘kue‘)
  2. , queue = kue.createQueue();
  3. queue.process(‘email‘, function(job, done){
  4. email(job.data.to, done);
  5. });
  6. function email(address, done) {
  7. if(!isValidEmail(address)) {
  8. //done(‘invalid to address‘) is possible but discouraged
  9. return done(new Error(‘invalid to address‘));
  10. }
  11. // email send stuff...
  12. done();
  13. }</code>

总之,你的 node web 服务应该执行短快的任务,否则大量并发会使性能迅速下降。

常见现象3:大的循环下,处理速度越来越慢。

  1. <code class="language-text">var a = 0, b = 10000000;
  2. function numbers() {
  3. while (a < b) {
  4. console.log("Number " + a++);
  5. }
  6. }
  7. numbers();</code>

这是一个比较 tricky 的问题,遍历打印一个 1000W 的 数,最后内存暴涨,越来越慢,最后:

  1. <code class="language-text">FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory
  2. 1: node::Abort() [/Users/haozes/.nvm/versions/node/v8.1.4/bin/node]</code>

问题原因:

node 是单线程执行,在这个同步代码中,GC 没有机会运行。这种情况在我经常写脚本遍历整个数据库的时候也经常发生。

优化方法:

  1. <code class="language-text">var a = 0, b = 10000000;
  2. function numbers() {
  3. var i = 0;
  4. while (a < b && i++ < 100) {
  5. console.log("Number " + a++);
  6. }
  7. if (a < b) setImmediate(numbers);
  8. }
  9. numbers();</code>

将函数执行放在 event loop 尾部执行,让主线程喘息一下。
这样写代码还是很拗口,我在遍历数据库时经常这样做:

  1. <code class="language-text">var fork = require(‘child_process‘).fork;
  2. // 同时允许几个子线程处理
  3. let MAX_CHILD = 2;
  4. let childProcCount = 0;
  5. var progress_count = 0;
  6. // 在子进程里处理
  7. function forkChild(guid) {
  8. var child = fork(‘./tool/user_stats/child‘,[guid]);
  9. child.on(‘message‘, (msg) => {
  10. console.log(‘ msg:‘,childProcCount, msg);
  11. });
  12. child.on("close", function () {
  13. childProcCount--;
  14. });
  15. }
  16. async function main() {
  17. console.log("start,from:",progress_count);
  18. while(true){ // 遍历所有用户,直到完成
  19. if(childProcCount < MAX_CHILD){
  20. var users = null;
  21. var collection = await getCollection(‘user‘);
  22. if((users = await collection.find({}).skip(progress_count).limit(1).toArray()).length < 1){
  23. break
  24. }
  25. console.log(">>> ",progress_count);
  26. // 丢到子线程处理
  27. forkChild(users[0].guid);
  28. childProcCount++;
  29. progress_count++;
  30. } else {
  31. // 等待子线程处理完再下一个
  32. await sleep(100);
  33. }
  34. }
  35. console.log("All data processed!");
  36. }
  37. main();</code>

其它:

Mongodb 读写分离及注意事项

在配置读写分离后,假如是主库写,从库读后,容易造成从库读的不一致性。注意majority 属性:

  1. <code class="language-text">recordCollection.insert(rec, {writeConcern: {w: "majority"}})</code>

Node.js——Node+Mongodb 架构常见性能问题 (转)

标签:rip   ogre   nvm   man   while   cal   extern   不一致   function   

人气教程排行