pfan123 / Articles

经验文章
169 stars 25 forks source link

Node服务性能监控 #102

Open pfan123 opened 3 years ago

pfan123 commented 3 years ago

目前许多业务开始往前端进行迁移,BFF(backends for frontends)的概念很多团队已开始逐渐尝试。让后端专注于提供统一的数据模型,将业务逻辑迁移到基于 Node 的 BFF 层中,让前端提供 API 接口,节省前后端联调成本,促使后端提供的 RPC 或者 HTTP 接口更加通用,减少修改后端工程,加快开发的效率。

但在 BFF 架构中,非常依赖 Node 端的稳定性,一旦 Node 端发生错误导致阻塞,造成很严重的后果,所以 Node 端的性能监控越来越有必要。

可以结合一些传统平台比如 sentryzabbix 可以帮助构建稳定的前端实时监控部署环境。

Node 系统操作

OS 模块获取系统信息

// http://nodejs.cn/api/os.html
const os = require("os")
os.tmpdir()  // 返回操作系统的默认临时文件夹
os.endianness()  // 返回 CPU 的字节序,可能的是 "BE" 或 "LE"
os.hostname()  // 返回操作系统的主机名
os.type()  // 返回操作系统名
os.platform()  // 返回编译时的操作系统名
os.arch()  // 返回操作系统 CPU 架构,可能的值有 "x64"、"arm" 和 "ia32"
os.release()  // 返回操作系统的发行版本
os.uptime()  // 返回操作系统运行的时间,以秒为单位
os.loadavg()  // 返回一个包含 1、5、15 分钟平均负载的数组, os.loadavg()可以获取系统的CPU使用率
os.totalmem()  // 返回系统内存总量,单位为字节,RAM
os.freemem()  // 返回操作系统空闲内存量,单位是字节
os.cpus()  // 返回一个对象数组,包含所安装的每个 CPU/内核的信息
os.networkInterfaces()  // 获得网络接口列表

一个 CPU,多个内核,CPU 内核有两个概念:

  • CPU核心数 NumberOfCores
  • CPU线程数 NumberOfLogicalProcessors

CPU 内核常会被分成两个线程是一种超线程技术,就是串代码,操作系统会认为一个线程也是一个内核,有点欺骗操作系统的感觉

Node V8 模块

返回V8堆空间,就是组成 V8 堆的片段的统计信息。通过 V8的 GetHeapSpaceStatistics 函数获得的堆空间的顺序或者堆空间的可用信息都不能保证可以作为可靠的统计信息,因为但 V8 的版本发生改变时它们也可能改变。

详见node v8 模块

如果要查看 V8 的 JavaScript 字节码,可以使用在命令行参数中添加 --print-bytecode 运行 D8 或Node.js(8.3 或更高版本)来打印。对于 Chrome,请从命令行启动 Chrome,使用 --js-flags="--print-bytecode",请参考 Run Chromium with flags。

进程内存查看

在 Node.js 环境里提供了 process.memoryUsage 方法用来查看当前进程内存使用情况,单位为字节

// 调整内存限制大小

node --max-nex-space-size=1024 app.js // 单位为KB node --max-old-space-size=2000 app.js // 单位为MB

process.cpuUsage() // 查看 CPU 使用情况

Node 性能

Node 优势适用于处理高并发、 I/O密集的场景,不适用于 CPU 密集的场景。我们了解服务性能要分两方面看:一方面是 I/O 性能,一方面是 CPU 计算性能。而 Node 安身立命的就是 I/O 爆表,事件驱动的特性使得 Node 的 I/O 十分卓越。

Node.js 软肋之 CPU 密集型任务

CPU 密集型任务计算性能的确是 Node 的软肋,跟 Java/C# 自然是不能比,但是 web 开发大多数情况下要命的是 I/O,且 Node 的性能比 Java/C# 差,但并不比其他语言差比 Ruby/Python 还是快出很多倍,而且可以调用 C/C++ 模块来处理 CPU 密集型任务。总而言之 Node 在 I/O 有其卓越的方面,CPU 密集型任务是 Node 的软肋但不致命。

CPU 密集: 涉及大量的逻辑计算,如图形计算、压缩、解压、加密、解密等 I/O 密集: 文件操作,网络操作,数据库操作等

性能指标

服务器的性能瓶颈通常为以下几个:

tips 知识:在 Linux 服务器上可以使用 top 命令常用的性能分析工具,能够实时显示系统中各个进程的资源占用状况,类似于 Windows 的任务管理器。top是一个动态显示过程,即可以通过用户按键来不断刷新当前状态。

不同场景下 Node 性能考虑指标,可能不尽兴相同。如 Node用于前端 SSR,CPU和网络为主要的性能瓶颈指标;如使用 Node 来进行数据持久化相关密集运算,I/O、磁盘、CPU占用负载率会很高。

目前来看,很少团队会用 Node 作为业务数据的支撑(涉及到CPU 密集型任务),而通常会用 Node 做 BFF 层,因此大多数场景下,CPU、内存以及网络可以说是 Node 的主要性能瓶颈。

CPU 指标

CPU 使用率与CPU 负载,这两个从一定程度上都可以反映一台机器的繁忙程度。

CPU 使用率是运行的程序占用的 CPU 资源,表示机器在某个时间点的运行程序的情况。使用率越高,说明机器在这个时间上运行了很多程序,反之较少。使用率的高低与 CPU 强弱有直接关系。

CPU利用率(CPU utilization),量化CPU时间占用状况,我们一般表示为:CPU利用率 = 1 - 空闲CPU时间(idle time) / CPU总的执行时间, 或 CPU利用率 = CPU执行非系统空闲进程的时间 / CPU总的执行时间

const os = require('os');
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));

class OSUtils {
  constructor() {
    this.cpuUsageMSDefault = 1000; // CPU 利用率默认时间段
  }

  /**
   * 获取某时间段 CPU 利用率
   * @param { Number } Options.ms [时间段,默认是 1000ms,即 1 秒钟]
   * @param { Boolean } Options.percentage [true(以百分比结果返回)|false] 
   * @returns { Promise }
   */
  async getCPUUsage(options={}) {
    const that = this;
    let { cpuUsageMS, percentage } = options;
    cpuUsageMS = cpuUsageMS || that.cpuUsageMSDefault;
    const t1 = that._getCPUInfo(); // t1 时间点 CPU 信息

    await sleep(cpuUsageMS);

    const t2 = that._getCPUInfo(); // t2 时间点 CPU 信息
    const idle = t2.idle - t1.idle;
    const total = t2.total - t1.total;
    let usage = 1 - idle / total;

    if (percentage) usage = (usage * 100.0).toFixed(2) + "%";

    return usage;
  }

  /**
   * 获取 CPU 瞬时时间信息
   * @returns { Object } CPU 信息
   * user <number> CPU 在用户模式下花费的毫秒数。
   * nice <number> CPU 在良好模式下花费的毫秒数。
   * sys <number> CPU 在系统模式下花费的毫秒数。
   * idle <number> CPU 在空闲模式下花费的毫秒数。
   * irq <number> CPU 在中断请求模式下花费的毫秒数。
   */
  _getCPUInfo() {
    const cpus = os.cpus();
    let user = 0, nice = 0, sys = 0, idle = 0, irq = 0, total = 0;

    for (let cpu in cpus) {
      const times = cpus[cpu].times;
      user += times.user;
      nice += times.nice;
      sys += times.sys;
      idle += times.idle;
      irq += times.irq;
    }

    total += user + nice + sys + idle + irq;

    return {
      user,
      sys,
      idle,
      total,
    }
  }
}

const cpuUsage = await osUtils.getCPUUsage({ percentage: true });
console.log('CPU 利用率:', cpuUsage) // CPU 利用率: 13.72%

CPU的负载(loadavg)很好理解,指某段时间内占用 CPU 时间的进程和等待 CPU 时间的进程数为平均负载(load average),这里等待CPU 时间的进程是指等待被唤醒的进程,不包括处于wait状态进程。

const os = require('os');
// CPU线程数
const length = os.cpus().length;
// 单核CPU的平均负载,返回一个包含 1、5、15 分钟平均负载的数组
os.loadavg().map(load => load / length);

内存指标

内存是一个非常容易量化的指标。 内存占用率是评判一个系统的内存瓶颈的常见指标。 对于Node来说,内部内存堆栈的使用状态也是一个可以量化的指标。

const os = require('os');
// 查看当前 Node 进程内存使用情况
const { rss, heapUsed, heapTotal } = process.memoryUsage();
// 获取系统空闲内存
const systemFree = os.freemem();
// 获取系统总内存
const systemTotal = os.totalmem();

module.exports = {
  memory: () => {
    return {
      system: 1 - systemFree / systemTotal,  // 系统内存占用率
      heap: heapUsed / headTotal,   // 当前 Node 进程内存占用率
      node: rss / systemTotal,         // 当前 Node 进程内存占用系统内存的比例
    }
  }
}

磁盘指标

Node 无法直接获取系统磁盘信息,这里我们可以借助 child_process 子进程,执行 Linux 查看磁盘空间 df 命令来进行获取。

const exec = require('child_process').exec

function diskMetrics () {
  return new Promise((resolve, reject)=> {
    exec('df -k', (error, stdout, stderr) => {

      let total = 0;
      let used = 0;
      let free = 0;

      if (error) {
        reject({});
        return;
      }

      let lines = stdout.split("\n");

      let str_disk_info = lines[1].replace( /[\s\n\r]+/g,' ');

      let disk_info = str_disk_info.split(' ');

      total = Math.ceil((disk_info[1] * 1024)/ Math.pow(1024,2));
      used = Math.ceil(disk_info[2] * 1024 / Math.pow(1024,2)) ;
      free = Math.ceil(disk_info[3] * 1024 / Math.pow(1024,2)) ;

      resolve({
        total,
        free,
        used,
      })
    })
  })
}

I/O 指标

IO实际上是计算机用语,也写作I/O,指输入/输出(Input/Output)。硬盘IO就是指对字节的读取速度,即硬盘的读写能力。服务器硬盘IO的性能也是服务器稳定性需要考虑的问题,IO瓶颈往往是我们可能会忽略的地方(我们常会看top、free、netstat等等,但经常会忽略IO的负载情况)。

// 每隔1s查询一次 共查询10次
iostat 1 10
// 查看磁盘
iostat -d 1 10

其他指标

可以通过 autocannonwrkloadtestsiege 等工具获取相关压测指标, 如:

{   
  "transactions":                      0,
    "availability":                 0.00,
    "elapsed_time":                 0.01,
    "data_transferred":             0.00,
    "response_time":                0.00,
    "transaction_rate":             0.00,
    "throughput":                   0.00,
    "concurrency":                  0.00,
    "successful_transactions":             0,
    "failed_transactions":                50,
    "longest_transaction":              0.00,
    "shortest_transaction":             0.00
}

日志监控

日志对于 Web 开发的重要性毋庸置疑,对应用的运行状态监控、问题排查等都有非常重要的意义。比较推荐的方案有:

重点看下 egg-logger 框架内置了强大的企业级日志支持,包含主要特性:

对于前端 Node.js 应用来说,根据项目的框架和部署架构,可选择不同的日志切割方案,例如:

  • 基于 pm2 部署的 Node.js 应用,可采用 pm2-logrotate 实现日志切割。
  • 基于 Egg.js 框架的 Node.js 应用,可采用 egg-logrotator 实现日志切割。

推荐工具

appmetrics-dash 工具

节点应用程序指标仪表板(appmetrics-dash)提供了一个非常易于使用的基于 Web 的仪表板,以显示正在运行的 Node.js 应用程序的性能指标。

appmetrics-dash 依赖 appmetrics

Other Resource

QPS、TPS、并发用户数、吞吐量关系

Node.js 应用故障排查手册

NodeJS中被忽略的内存

深入 Nodejs 源码探究 CPU 信息的获取与利用率计算

appmetrics-dash

v8-gc-log-parser

os-utils

sentry 跨端实时监控平台

Prometheus 监控系统

App Metrics

Grafana度量分析与可视化套件

Easy-Monitor 2.0

Node.js环境性能监控

BFF 知识

node相比传统服务端技术栈差在哪里?

node.js应用高并发高性能本质

使用 Node.js 的优势和劣势都有哪些?

SSR VS CSR ,一次讲个通透

容器监控实践—K8S常用指标分析

Memeye 是一个轻量级的 NodeJS 进程监控工具

node-usage 跨端获取内存信息

prometheus-book

如何处理 Node.js 中出现的未捕获异常?

Node 报警设置

了解 V8 的字节码

NodeJS运维: 从 0 开始 Prometheus + Grafana 业务性能指标监控