Node.js 应用故障排查手册 —— 雪崩型内存泄漏问题

简介: 还有一些问题场景下下应用的内存泄漏非常严重和迅速,甚至于在我们的告警系统感知之前就已经造成应用的 OOM 了,这时我们来不及或者说根本没办法获取到堆快照,因此就没有办法借助于之前的办法来分析为什么进程会内存泄漏到溢出进而 Crash 的原因了。

楔子

实践篇一中我们也看到了一个比较典型的由于开发者不当使用第三方库,而且在配置信息中携带了三方库本身使用不到的信息,导致了内存泄漏的案例,实际上类似这种相对缓慢的 Node.js 应用内存泄漏问题我们总是可以在合适的机会抓取堆快照进行分析,而且堆快照一般来说确实是分析内存泄漏问题的最佳手段。

但是还有一些问题场景下下应用的内存泄漏非常严重和迅速,甚至于在我们的告警系统感知之前就已经造成应用的 OOM 了,这时我们来不及或者说根本没办法获取到堆快照,因此就没有办法借助于之前的办法来分析为什么进程会内存泄漏到溢出进而 Crash 的原因了。这种问题场景实际上属于线上 Node.js 应用内存问题的一个极端状况,本节将同样从源自真实生产的一个案例来来给大家讲解下如何处理这类极端内存异常。

本书首发在 Github,仓库地址:https://github.com/aliyun-node/Node.js-Troubleshooting-Guide,云栖社区会同步更新。

最小化复现代码

同样我们因为例子的特殊性,我们需要首先给出到大家生产案例的最小化复现代码,建议读者自行运行一番此代码,这样结合起来看下面的排查分析过程会更有收获。最小复现代码还是基于 Egg.js,如下所示:

'use strict';
const Controller = require('egg').Controller;
const fs = require('fs');
const path = require('path');
const util = require('util');
const readFile = util.promisify(fs.readFile);

class DatabaseError extends Error {
  constructor(message, stack, sql) {
    super();
    this.name = 'SequelizeDatabaseError';
    this.message = message;
    this.stack = stack;
    this.sql = sql;
  }
}

class MemoryController extends Controller {
  async oom() {
    const { ctx } = this;
    let bigErrorMessage = await readFile(path.join(__dirname, 'resource/error.txt'));
    bigErrorMessage = bigErrorMessage.toString();
    const error = new DatabaseError(bigErrorMessage, bigErrorMessage, bigErrorMessage);
    ctx.logger.error(error);
    ctx.body = { ok: false };
  }
}

module.exports = MemoryController;

这里我们还需要在 app/controller/ 目录下创建一个 resource 文件夹,并且在这个文件夹中添加一个 error.txt,这个 TXT 内容随意,只要是一个能超过 100M 的很大的字符串即可。 

值得注意的是,其实这里的问题已经在 egg-logger >= 1.7.1 的版本中修复了,所以要复现当时的状况,你还需要在 Demo 的根目录执行以下的三条命令以恢复当时的版本状况:

rm -rf package-lock.json 
rm -rf node_modules/egg/egg-logger
npm install egg-logger@1.7.0

最后使用 npm run dev 启动这个问题最小化复现的 Demo。

感知进程出现问题

这个案例下,实际上我们的线上 Node.js 应用几乎是触发了这个 Bug 后瞬间内存溢出然后 Crash 的,而平台预设的内存阈值告警,实际上是由一个定时上报的逻辑构成,因此存在延时,也导致了这个案例下我们无法像 冗余配置传递引发的内存溢出 问题那样获取到 Node.js 进程级别的内存超过预设阈值的告警。

那么我们如何来感知到这里的错误的呢?这里我们的服务器配置过了 ulimit -c unlimited ,因此 Node.js 应用 Crash 的时候内核会自动生成核心转储文件,而且性能平台目前也支持核心转储文件的生成预警,这一条规则目前也被放入了预设的快速添加告警规则中,可以参考工具篇中 Node.js 性能平台使用指南 - 配置合适的告警 一节,详细的规则内容如下所示:

image.png

这里需要注意的是,核心转储文件告警需要我们在服务器上安装的 Agenthub/Agentx 依赖的 Commandx 模块的版本在 1.5.2 之上(包含),这一块更详细的信息也可以看官方文档 核心转储分析能力 一节。

问题排查过程

I. 分析栈信息

依靠上面提到的平台提供的核心转储文件生成时给出的告警,我们在收到报警短信时登录控制台,可以看到 Coredump 文件列表出现了新生成的核心转储文件,继续参照工具篇中 Node.js 性能平台使用指南 - 核心转储分析 中给出的转储和 AliNode 定制分析的过程,我们可以看到如下的分析结果展示信息:

image.png

同样我们直接展开 JavaScript 栈信息查看应用 Crash 那一刻的栈信息:

image.png

截图中忽略掉了 Native C/C++ 代码的栈信息,这里其实仅仅看 JavaScript 栈信息就能得到结论了,通过翻阅比对出问题的 egg-logger@1.7.0 中 lib/utils.js 的代码内容:

function formatError(err) {
  // ...
  // 这里对 Error 对象的 key 和 value 调用 inspect 方法进行序列化
  const errProperties = Object.keys(err).map(key => inspect(key, err[key])).join('\n');
  // ...
}

// inspect 方法实际上是调用 require('util').inspect 来对错误对象的 value 进行序列化
function inspect(key, value) {
  return `${key}: ${util.inspect(value, { breakLength: Infinity })}`;
}

这样我们就知道了线上 Node.js 应用在 Crash 的那一刻正在使用 require('util').inspect 对某个字符串进行序列化操作。

II. 可疑字符串

那么这个序列化的动作究竟是不是造成进程 Crash 的元凶呢?我们接着来点击 inspect 函数的参数来展开查看这个可疑的字符串的详细信息,如下图所示:

image.png

点击红框中的参数,得到字符串的详情页面链接,如下图所示:

image.png

再次点击这里的 detail 链接,既可在弹出的新页面中看到这个可疑字符串的全部信息:

image.png
这里可以看到,这个正在被 util.inspect 的字符串大小高达 186.94 兆,显然正是在序列化这么大的字符串的时候,造成了线上 Node.js 应用的堆内存雪崩,几乎在瞬间就内存溢出导致 Crash。

值得一提的是,我们还可以点击上图中的 + 号来在当前页面展示更多的问题字符串内容:

image.png

也可以在页面上点击 一键导出 按钮下载问题完整的字符串:

image.png

毕竟对于这样的问题来说,如果能抓到产生问题的元凶参数,起码能更方便地进行本地复现。

III. 修复问题

那么知道了原因,其实修复此问题就比较简单了,Egg-logger 官方是使用 circular-json 来替换掉原生的 util.inspect 序列化动作,并且增加序列化后的字符串最大只保留 10000 个字符的限制,这样就解决这种包含大字符串的错误对象在 Egg-logger 模块中的序列化问题。

结尾

本节的给大家展现了对于线上 Node.js 应用出现瞬间 Crash 问题时的排查思路,而在最小化复现 Demo 对应的那个真实线上故障中,实际上是拼接的 SQL 语句非常大,大小约为 120M,因此首先导致数据库操作失败,接着数据库操作失败后输出的 DatabaseError 对象实例上则原封不动地将问题 SQL 语句设置到属性上,从而导致了 ctx.logger.error(error) 时堆内存的雪崩。


Node.js 性能平台 提供的 核心转储告警 + 在线分析能力 的帮助下,此类无法获取到常规 CPU Profile 和堆快照等信息的进程无故崩溃问题也变得有迹可循了,实际上它作为一种兜底分析手段,在很大程度上提升了开发者真正将 Node.js 运用到服务端生产环境中的信心。

目录
相关文章
|
2天前
|
缓存 移动开发 Android开发
构建高效Android应用:从内存优化到电池寿命
【5月更文挑战第18天】在移动开发领域,一个优秀的Android应用不仅要拥有流畅的用户界面和丰富的功能,更要在设备资源有限的前提下保持高效运行。本文将探讨Android应用开发中关键的性能优化策略,包括内存使用优化、CPU使用减少和电池寿命延长等方面。通过分析常见的性能瓶颈和提供实用的解决方案,帮助开发者打造更高效、更受欢迎的Android应用。
|
5天前
|
存储 监控 JavaScript
使用Node.js构建实时聊天应用的技术指南
【5月更文挑战第12天】本文指导使用Node.js、Express.js和Socket.IO构建实时聊天应用。技术栈包括Node.js作为服务器环境、WebSocket协议、Express.js作为Web框架和Socket.IO处理实时通信。步骤包括项目初始化、安装依赖、搭建服务器、实现实时聊天功能、运行应用以及后续的完善和部署建议。通过这个指南,读者可以学习到创建简单实时聊天应用的基本流程。
|
5天前
|
消息中间件 监控 NoSQL
中间件应用合理配置内存
【5月更文挑战第4天】中间件应用合理配置内存
16 2
中间件应用合理配置内存
|
5天前
|
安全 Java Android开发
构建高效Android应用:采用Kotlin进行内存优化的策略
【5月更文挑战第8天】 在移动开发领域,性能优化一直是开发者关注的焦点。特别是对于Android应用而言,合理管理内存资源是确保应用流畅运行的关键因素之一。近年来,Kotlin作为官方推荐的开发语言,以其简洁、安全和互操作性的特点受到开发者青睐。本文将深入探讨利用Kotlin语言特性,通过具体策略对Android应用的内存使用进行优化,旨在帮助开发者提高应用性能,减少内存消耗,避免常见的内存泄漏问题。
12 0
|
5天前
|
数据采集 JavaScript 数据可视化
Node.js爬虫在租房信息监测与分析中的应用
Node.js爬虫在租房信息监测与分析中的应用
|
5天前
|
缓存 安全 Android开发
构建高效Android应用:采用Kotlin进行内存优化
【5月更文挑战第1天】随着移动设备的普及,用户对应用程序的性能要求越来越高。特别是对于Android开发者来说,理解并优化应用的内存使用是提升性能的关键。本文将探讨使用Kotlin语言在Android开发中实现内存优化的策略和技术。我们将深入分析Kotlin特有的语言特性和工具,以及它们如何帮助开发者减少内存消耗,避免常见的内存泄漏问题,并提高整体应用性能。
|
5天前
|
机器学习/深度学习 自动驾驶 安全
深入理解操作系统内存管理:策略与实现基于深度学习的图像识别技术在自动驾驶系统中的应用
【4月更文挑战第30天】 在现代计算机系统中,操作系统的内存管理是确保系统高效、稳定运行的关键组成部分。本文将深入探讨操作系统中内存管理的多种策略及其实现机制,包括但不限于分页、分段和段页式结合等技术。我们将剖析内存分配的原理,讨论虚拟内存技术的实现以及它如何提供更大的地址空间并允许内存的交换。同时,我们还会涉及内存保护机制,它们是如何防止程序访问未授权的内存区域。最后,文中将对现代操作系统如Linux和Windows中的内存管理实践进行比较分析,以期给读者提供全面而深入的理解和参考。 【4月更文挑战第30天】 随着人工智能技术的飞速发展,深度学习已经
|
5天前
|
安全 网络安全 Android开发
云端防御策略:融合云服务与网络安全的未来构建高效的Android应用:从内存优化到电池寿命
【4月更文挑战第30天】 随着企业加速向云计算环境转移,数据和服务的云端托管成为常态。本文探讨了在动态且复杂的云服务场景下,如何构建和实施有效的网络安全措施来保障信息资产的安全。我们将分析云计算中存在的安全挑战,并展示通过多层次、多维度的安全框架来提升整体防护能力的方法。重点关注包括数据加密、身份认证、访问控制以及威胁检测与响应等关键技术的实践应用,旨在为读者提供一种结合最新技术进展的网络安全策略视角。 【4月更文挑战第30天】 在竞争激烈的移动市场中,Android应用的性能和资源管理已成为区分优秀与平庸的关键因素。本文深入探讨了提升Android应用效率的多个方面,包括内存优化策略、电池
|
5天前
|
算法 安全 Android开发
深入理解操作系统的内存管理机制构建高效Android应用:Kotlin的协程优势
【4月更文挑战第30天】 在现代计算机系统中,操作系统的内存管理是确保系统高效、稳定运行的关键。本文将探讨操作系统内存管理的核心技术,包括内存分配、虚拟内存、分页和分段等概念,以及它们是如何协同工作以提高内存利用率和系统性能的。通过对这些技术的详细分析,我们可以更好地理解操作系统背后的原理,并评估不同内存管理策略对系统行为的影响。 【4月更文挑战第30天】 在移动开发领域,尤其是针对Android平台,性能优化和流畅的用户体验始终是开发者追求的核心目标。随着Kotlin语言的普及,协程作为其在异步编程领域的杀手锏特性,已经逐渐成为提高应用性能和简化代码结构的重要工具。本文将深入探讨Kotli
|
5天前
|
缓存 监控 Android开发
构建高效Android应用:从内存优化到电池续航
【4月更文挑战第30天】 在移动开发领域,性能优化是一个永不过时的话题。对于Android应用而言,实现流畅的用户体验和延长设备电池寿命是至关重要的。本文将深入探讨Android平台特有的内存管理和电池使用策略,并提出一系列切实可行的优化措施。通过智能管理应用的生命周期、合理利用系统资源和调整后台任务执行策略,开发者可以显著提升应用性能并减少能源消耗。文章最后还将讨论如何利用Android Studio内置工具进行性能分析与监控,确保应用在发布前达到最优状态。