构建高性能服务起因

日常开发中,多数通过pm2部署node应用,pm2会默认启动多个进程。
pm2的实现原理是基于Cluster模式的封装。
由于node是单线程,有一个线程出现错误,就会引起整个进程奔溃,也即是整个服务宕机。
node可以通过cluster模块创建多个子进程,利用服务器的多核cpu启动多个子进程,当一个进程出现错误停掉,会调用其他进程,来保障服务的稳定性。

创建web服务

新建app.js,通过http模块创建web服务。

  1. const http = require("http");
  2. const server = http.createServer((req, res) => {
  3. Math.random() > 0.95 ? err() : "random";
  4. res.end("hello world!");
  5. });
  6. const err = ()=>{
  7. console.error("error")
  8. throw new Error("出现错误")
  9. }
  10. // !module.parent在没有require的情况下执行运行下面代码,有被require则运行else
  11. if (!module.parent) {
  12. server.listen(3000, () => {
  13. console.log("listen on port 3000");
  14. });
  15. } else {
  16. module.exports = server;
  17. }

创建web服务,端口3000。
然后通过axios定时循环访问该端口。

创建test.js测试文件

  1. const axios = require("axios");
  2. setInterval(async function () {
  3. const response = await axios.get("http://localhost:3000");
  4. console.log("请求的返回值", response.data);
  5. }, 1000);

node的cluster模块

cluster模块可以创建多个工作线程共享同一个TCP连接。
cluster.fork创建的工作线程可以共享同一个端口,node在底层处理了net网络模块进行判断,如果是cluster创建的线程会进行兼容。
但是child_process模块的fork,无法共享同一端口。

cluster会创建一个主master,根据指定的数量创建多个server app,它们内部通过IPC通道与工作线程之间进行通信,并使用内置的负载均衡处理线程直接的压力,该负载均衡使用了Round-robin算法。
当使用Round-robin调度策略时,master接收所有传入的连接请求,然后将相应的TCP请求处理发送给选中的工作线程。
创建cluster.js

  1. const cluster = require("cluster");
  2. const os = require("os");
  3. // 判断服务器有多少核cpu
  4. const cpuNum = os.cpus().length;
  5. const process = require("process");
  6. const workers = {};
  7. // 初始创建主进程
  8. if (cluster.isMaster) {
  9. for (let i = 0; i < cpuNum; i++) {
  10. // 根据cpu数量,fork出多个子进程
  11. const worker = cluster.fork();
  12. workers[worker.process.pid] = worker;
  13. console.log("工作进程启动", worker.process.pid);
  14. }
  15. // 监听子进程的状态,如果出错退出,则删除该进程pid
  16. cluster.on("exit", (worker, code, signal) => {
  17. console.log("工作进程close", worker.process.pid);
  18. delete workers[worker.process.pid];
  19. worker = cluster.fork();
  20. workers[worker.process.pid] = worker;
  21. });
  22. } else {
  23. // 通过子进行启动端口为3000的服务,cluster.fork的进程可以共享端口
  24. const app = require("./app.js");
  25. app.listen(3000);
  26. }
  27. // SIGTERM表示结束主进程,相当于ctrl+c结束程序,
  28. process.on("SIGTERM", () => {
  29. for (var pid in workers) {
  30. process.kill(pid);
  31. }
  32. // 正常退出,code为0
  33. process.exit(0);
  34. });
  35. // 调用test,通过axios重复不断访问http://localhost:3000/
  36. require("./test.js");

运行node cluster.js
image.png
即使一个进程出现错误,还有其它进程在执行。这样就可以解决node单线程出现错误无法访问的问题,并且还能提高并发数。

使用ab进行压力测试

  1. ab -n2000 -c200 "http://10.21.102.0:3000/"

进行ab测试,需要调整下代码

  1. 注释掉cluster.js的38行,不引用test文件
  2. 注释掉app.js第3行,不抛出错误

image.png

使用PM2启动node服务

PM2是node进程管理工具,可以利用它来简化很多node应用管理的繁琐任务,如性能监控、自动重启、负载均衡等

PM2常用操作命令

  1. $ pm2 start app.js # ⭐️ 启动app.js应用程序
  2. $ pm2 start app.js -i 4 # ⭐️ cluster mode 模式启动4app.js的应用实例
  3. # 4个应用程序会自动进行负载均衡
  4. $ pm2 start app.js --name="api" # 启动应用程序并命名为 "api"
  5. $ pm2 start app.js --watch # 当文件变化时自动重启应用
  6. $ pm2 start script.sh # 启动 bash 脚本
  7. $ pm2 list # 列表 PM2 启动的所有的应用程序
  8. $ pm2 monit # 显示每个应用程序的CPU和内存占用情况
  9. $ pm2 show [app-name] # 显示应用程序的所有信息
  10. $ pm2 logs # 显示所有应用程序的日志
  11. $ pm2 logs [app-name] # 显示指定应用程序的日志
  12. $ pm2 flush
  13. $ pm2 stop all # 停止所有的应用程序
  14. $ pm2 stop 0 # 停止 id 0的指定应用程序
  15. $ pm2 restart all # 重启所有应用
  16. $ pm2 reload all # 重启 cluster mode下的所有应用
  17. $ pm2 gracefulReload all # Graceful reload all apps in cluster mode
  18. $ pm2 delete all # 关闭并删除所有应用
  19. $ pm2 delete 0 # 删除指定应用 id 0
  20. $ pm2 scale api 10 # ⭐️把名字叫api的应用扩展到10个实例
  21. $ pm2 reset [app-name] # 重置重启数量
  22. $ pm2 startup # ⭐️创建开机自启动命令
  23. $ pm2 save # ⭐️保存当前应用列表
  24. $ pm2 resurrect # 重新加载保存的应用列表
  25. $ pm2 update # Save processes, kill PM2 and restore processes
  26. $ pm2 generate # Generate a sample json configuration file

开启http服务

创建server.js文件,通过http模块启动一个web服务

  1. const http = require('http')
  2. http.createServer((req, res) => {
  3. for (let i = 0; i < 1e7; i++) { }
  4. res.writeHead(200)
  5. res.end('hello')
  6. }).listen(3333)

通过PM2启动服务

  1. pm2 start server.js -i 4

-i 指定启动的进程数。如果给的0,PM2会根据cpu的核心数量生成对应的工作线程。
通过PM2启动的node服务不用担心服务停掉,如果一个线程出现问题,其它线程会立即重启。

实时扩展集群

  1. pm2 scale server +2

使用ab工具进行测试

先是不开启多线程,命令行输入node server.js
用ab压测 ab -n 1000 -c 100 http://localhost:3333/,这条测试命令的意思是一共发送1000个请求,每秒100并发
得到如下结果

  1. Server Software:
  2. Server Hostname: localhost
  3. Server Port: 3333
  4. Document Path: /
  5. //第一个成功返回的文档的字节大小
  6. Document Length: 11 bytes
  7. // 并发数
  8. Concurrency Level: 200
  9. // 从建立连接到最后接受完成总时间
  10. Time taken for tests: 6.643 seconds
  11. // 完成请求数
  12. Complete requests: 2000
  13. Failed requests: 0
  14. // 从服务器接收的字节总数
  15. Total transferred: 172000 bytes
  16. // HTML接收字节数,减去了Total transferred中HTTP响应数据中的头信息的长度
  17. HTML transferred: 22000 bytes
  18. // 吞吐率:每秒请求数(总请求数/总时间,相当于LR中的每秒事务数TPS)
  19. Requests per second: 301.07 [#/sec] (mean)
  20. // 用户平均请求等待时间
  21. Time per request: 664.303 [ms] (mean)
  22. // 服务器处理每个请求平均响应时间,mean表示为平均值
  23. Time per request: 3.322 [ms] (mean, across all concurrent requests)
  24. // 这些请求在单位时间内从服务器获取的数据长度
  25. Transfer rate: 25.28 [Kbytes/sec] received
  26. // 连接消耗时间分解
  27. Connection Times (ms)
  28. min mean[+/-sd] median max
  29. 最小值 平均值 标准差 中间值 最大值
  30. Connect: 0 14 128.0 1 1288
  31. Processing: 26 626 173.3 619 1240
  32. Waiting: 15 361 137.7 350 844
  33. Total: 26 641 216.2 621 1929
  34. Percentage of the requests served within a certain time (ms)
  35. // 50%请求完成时间的最大值是621毫秒
  36. 50% 621
  37. 66% 630
  38. 75% 645
  39. 80% 665
  40. // 90%请求完成时间的最大值是914毫秒
  41. 90% 914
  42. 95% 1076
  43. 98% 1240
  44. 99% 1928
  45. // 100%请求完成时间的最大值是1929毫秒(最长请求
  46. 100% 1929 (longest request)

看到服务端的QPS【Requests per second】是301左右
进行扩容

  1. pm2 start server.js -i max
  1. Server Software:
  2. Server Hostname: localhost
  3. Server Port: 3333
  4. Document Path: /
  5. Document Length: 11 bytes
  6. Concurrency Level: 200
  7. Time taken for tests: 1.946 seconds
  8. Complete requests: 2000
  9. Failed requests: 0
  10. Total transferred: 172000 bytes
  11. HTML transferred: 22000 bytes
  12. Requests per second: 1027.97 [#/sec] (mean)
  13. Time per request: 194.559 [ms] (mean)
  14. Time per request: 0.973 [ms] (mean, across all concurrent requests)
  15. Transfer rate: 86.33 [Kbytes/sec] received
  16. Connection Times (ms)
  17. min mean[+/-sd] median max
  18. Connect: 0 6 19.1 0 72
  19. Processing: 33 178 31.4 187 227
  20. Waiting: 17 168 36.3 180 217
  21. Total: 34 184 19.3 187 228
  22. Percentage of the requests served within a certain time (ms)
  23. 50% 187
  24. 66% 191
  25. 75% 195
  26. 80% 197
  27. 90% 202
  28. 95% 206
  29. 98% 213
  30. 99% 217
  31. 100% 228 (longest request)

服务端的QPS1027提升3倍多,启动的8核进程。