前面介绍了 heapdump 和 memwatch-next 的用法,但在实际使用时并不那么方便,我们总不能一直盯着服务器的状况,在发现内存持续增长并超过心里的阈值时,再手动去触发 Core Dump 吧?在大多数情况下发现问题时,就已经错过了现场。所以,我们可能需要 cpu-memory-monitor。顾名思义,这个模块可以用来监控 CPU 和 Memory 的使用情况,并可以根据配置策略自动 dump CPU 的使用情况(cpuprofile)和内存快照(heapsnapshot)。

2.4.1 使用 cpu-memory-monitor

我们先来看看如何使用 cpu-memory-monitor,其实很简单,只需在进程启动的入口文件中引入以下代码:

  1. require('cpu-memory-monitor')({
  2. cpu: {
  3. interval: 1000,
  4. duration: 30000,
  5. threshold: 60,
  6. profileDir: '/tmp',
  7. counter: 3,
  8. limiter: [5, 'hour']
  9. }
  10. })

上述代码的作用是:每 1000ms(interval)检查一次 CPU 的使用情况,如果发现连续 3(counter)次 CPU 使用率大于 60%(threshold),则 dump 30000ms(duration) CPU 的使用情况,生成 cpu-${process.pid}-${Date.now()}.cpuprofile/tmp(profileDir) 目录下,1(limiter[1]) 小时最多 dump 5(limiter[0]) 次。

以上是自动 dump CPU 使用情况的策略。dump Memory 使用情况的策略同理:

  1. require('cpu-memory-monitor')({
  2. memory: {
  3. interval: 1000,
  4. threshold: '1.2gb',
  5. profileDir: '/tmp',
  6. counter: 3,
  7. limiter: [3, 'hour']
  8. }
  9. })

上述代码的作用是:每 1000ms(interval) 检查一次 Memory 的使用情况,如果发现连续 3(counter) 次 Memory 大于 1.2gb(threshold),则 dump 一次 Memory,生成 memory-${process.pid}-${Date.now()}.heapsnapshot/tmp(profileDir) 目录下,1(limiter[1]) 小时最多 dump 3(limiter[0]) 次。

注意:memory 的配置没有 duration 参数,因为 Memroy 的 dump 只是某一时刻的,而不是一段时间的。

那聪明的你肯定会问了:能不能将 cpu 和 memory 配置一块使用?比如:

  1. require('cpu-memory-monitor')({
  2. cpu: {
  3. interval: 1000,
  4. duration: 30000,
  5. threshold: 60,
  6. ...
  7. },
  8. memory: {
  9. interval: 10000,
  10. threshold: '1.2gb',
  11. ...
  12. }
  13. })

答案是:可以,但不要这么做。因为这样做可能会出现这种情况:内存高了且达到设定的阈值 -> 触发 Memory Dump/GC -> 导致 CPU 使用率高且达到设定的阈值 -> 触发 CPU Dump -> 导致堆积的请求越来越多(比如内存中堆积了很多 SQL 查询)-> 触发 Memory Dump -> 导致雪崩。

通常情况下,只使用其中一种就可以了。

2.4.2 源码解读

cpu-memory-monitor 的源代码不过百余行,大体逻辑如下:

  1. ...
  2. const processing = {
  3. cpu: false,
  4. memory: false
  5. }
  6. const counter = {
  7. cpu: 0,
  8. memory: 0
  9. }
  10. function dumpCpu(cpuProfileDir, cpuDuration) { ... }
  11. function dumpMemory(memProfileDir) { ... }
  12. module.exports = function cpuMemoryMonitor(options = {}) {
  13. ...
  14. if (options.cpu) {
  15. const cpuTimer = setInterval(() => {
  16. if (processing.cpu) {
  17. return
  18. }
  19. pusage.stat(process.pid, (err, stat) => {
  20. if (err) {
  21. clearInterval(cpuTimer)
  22. return
  23. }
  24. if (stat.cpu > cpuThreshold) {
  25. counter.cpu += 1
  26. if (counter.cpu >= cpuCounter) {
  27. memLimiter.removeTokens(1, (limiterErr, remaining) => {
  28. if (limiterErr) {
  29. return
  30. }
  31. if (remaining > -1) {
  32. dumpCpu(cpuProfileDir, cpuDuration)
  33. counter.cpu = 0
  34. }
  35. })
  36. } else {
  37. counter.cpu = 0
  38. }
  39. }
  40. })
  41. }, cpuInterval)
  42. }
  43. if (options.memory) {
  44. ...
  45. memwatch.on('leak', () => {
  46. dumpMemory(...)
  47. })
  48. }
  49. }

可以看出:cpu-memory-monitor 没有用到什么新鲜的东西,还是之前讲解过的 v8-profiler、heapdump、memwatch-next 的组合使用而已。

有以下几点需要注意:

  1. 只有传入了 cpu 或者 memory 的配置,才会去监听相应的 CPU 或者 Memory。
  2. 在传入 memory 配置时,用了 memwatch-next 额外监听了 leak 事件,也会 dump Memory,格式是 leak-memory-${process.pid}-${Date.now()}.heapsnapshot
  3. 顶部引入了 heapdump,所以即使没有 memory 配置,也可以通过 kill -USR2 <PID> 手动触发 Memory Dump。

2.4.3 参考链接

上一节:2.3 memwatch-next

下一节:3.1 Promise