Nodejs-应用故障排查手册-雪崩型内存泄漏问题

摘要: 还有一些问题场景下下应用的内存泄漏非常严重和迅速,甚至于在我们的告警系统感知之前就已经造成应用的 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 的很大的字符串即可。  ...

April 23, 2019 · 2 min · jiezi

小程序循环require之坑

循环require在JavaScript中,模块之间可能出现相互引用的情况,例如现在有三个模块,他们之间的相互引用关系如下,大致的引用关系可以表示为 A -> B -> C -> A,要完成模块A,它依赖于模块C,但是模块C反过来又依赖于模块A,此时就出现了循环require。// a.jsconst B = require(’./b.js’);console.log(‘B in A’, B);const A = { name: ‘A’, childName: B.name,};module.exports = A;// b.jsconst C = require(’./c.js’);console.log(‘C in B’, C);const B = { name: ‘B’, childName: C.name,}module.exports = B;// c.jsconst A = require(’./a.js’);console.log(‘A in C’, A);const C = { name: ‘C’, childName: A.name,};module.exports = C;那JS引擎会一直循环require下去吗?答案是不会的,如果我们以a.js为入口执行程序,C在引用A时,a.js已经执行,不会再重新执行a.js,因此c.js获得的A对象是一个空对象(因为a.js还没执行完成)。2. 小程序中的坑在正常情况下,JS引擎是可以解析循环require的情形的。但是在一些低版本的小程序中,居然出现程序一直循环require的情况,最终导致栈溢出而报错,实在是天坑。那如何解决呢,很遗憾,目前并未找到完美的方法来解决,只能找到程序中的循环require的代码,并进行修改。为了快速定位程序中的循环引用,写了一段NodeJs检测代码来检测进行检测。const fs = require(‘fs’);const path = require(‘path’);const fileCache = {};const requireLink = [];if (process.argv.length !== 3) { console.log(please run as: node ${__filename.split(path.sep).pop()} file/to/track); return;}const filePath = process.argv[2];const absFilePath = getFullFilePath(filePath);if (absFilePath) { resolveRequires(absFilePath, 0);} else { console.error(‘file not exist:’, filePath);}/** * 递归函数,解析文件的依赖 * @param {String} file 引用文件的路径 * @param {Number} level 文件所在的引用层级 /function resolveRequires(file, level) { requireLink[level] = file; for (let i = 0; i < level; i ++) { if (requireLink[i] === file) { console.log(’*** require circle detected ’); console.log(requireLink.slice(0, level + 1)); console.log(); return; } } const requireFiles = getRequireFiles(file); requireFiles.forEach(file => resolveRequires(file, level + 1));}/ * 获取文件依赖的文件 * @param {String} filePath 引用文件的路径 /function getRequireFiles(filePath) { if (!fileCache[filePath]) { try { const fileBuffer = fs.readFileSync(filePath); fileCache[filePath] = fileBuffer.toString(); } catch(err) { console.log(‘read file failed’, filePath); return []; } } const fileContent = fileCache[filePath]; // 引入模块的几种形式 const requirePattern = /require\s([’"])/g; const importPattern1 = /import\s+.?\s+from\s+[’"]/g; const importPattern2 = /import\s+[’"]/g; const requireFilePaths = []; const baseDir = path.dirname(filePath); let match = null; while ((match = requirePattern.exec(fileContent)) !== null) { requireFilePaths.push(match[1]); } while ((match = importPattern1.exec(fileContent)) !== null) { requireFilePaths.push(match[1]); } while ((match = importPattern2.exec(fileContent)) !== null) { requireFilePaths.push(match[1]); } return requireFilePaths.map(fp => getFullFilePath(fp, baseDir)).filter(fp => !!fp);}/* * 获取文件的完整绝对路径 * @param {String} filePath 文件路径 * @param {String} baseDir 文件路径的相对路径 */function getFullFilePath(filePath, baseDir) { if (baseDir) { filePath = path.resolve(baseDir, filePath); } else { filePath = path.resolve(filePath); } if (fs.existsSync(filePath)) { const stat = fs.statSync(filePath); if (stat.isDirectory() && fs.existsSync(path.join(filePath, ‘index.js’))) { return path.join(filePath, ‘index.js’); } else if (stat.isFile()){ return filePath; } } else if (fs.existsSync(filePath + ‘.js’)) { return filePath + ‘.js’; } return ‘’;}

March 7, 2019 · 2 min · jiezi

webpack -- require和import机制

欢迎访问我的个人博客:http://www.xiaolongwu.cn前言虽然我们很多人每天都在写项目,require或者import写的爽得很,但还是有很大一部分人不清楚它背后的运行原理和所谓的规则机制。开始我们基于webpack开发,就拿基本的vue项目来举例子吧假如我们项目中要用到vue或者express框架,我们的代码就这样写import Vue from ‘vue’//或者var Vue = require(‘vue’)然后我们就能在下面轻松的用Vue这个变量,感觉很愉悦,但是你想过我们是怎么拿到Vue这个东西的吗?我们写的import或者require这行代码道理干了啥?首先,import是es2015的模块引入规范,而require是commonjs的模块引入规范;webpack支持es2015,commonjs,AMD等规范;工作机制前提是你在做web开发,试图用webpack或者rollup打包你的项目;首先会从本地的node_modules文件夹中找到vue文件夹,看是否存在package.json文件;如果找到了package.json,就会先找module字段,然后读取对应的路径下的文件,查找到此结束;如果没找到module字段,就会找main字段,然后读取对应的路径下的文件,查找到此结束;如果没有main字段,就会在vue文件夹下找index.js文件,然后读取文件,查找到此结束;如果以上都没找到就会返回异常,扔出not find异常如果不存在package.json就会找index.js文件,然后读取文件,查找到此结束;如果还没有就会抛出异常;简单说一下module字段说到module字段就不得不说一个和webpack很像的模块打包工具—rollup,rollup是一个轻量级的打包工具,一般被用来打包模块或者库,可以根据需要将模块打包为es,commonjs,AMD,CMD,UMD,IIFE等规范的模块;而webpack一般被用来打包应用程序;rollup提出了module这个字段,其原因是一般主流的模块或者库都是commonjs规范的模块,而es2015的模块规范才是js的未来,才应该是主流;所以,一般的package.json中的module对应的模块为es模块,而main对应的为commonjs模块,webpack和rollup都会默认优先读取module字段;github资源地址:webpack–require和import机制.md我的CSDN博客地址:https://blog.csdn.net/wxl1555如果您对我的博客内容有疑惑或质疑的地方,请在下方评论区留言,或邮件给我,共同学习进步。邮箱:wuxiaolong802@163.com

January 11, 2019 · 1 min · jiezi

理解import、require、export、module.export

理解import、require、export、module.exportES6的模块设计模块设计的思想是尽量静态化,使得编译的时候就可以确定模块的一来关系,以及输入和输出的变量。CommonJS和AMD都只能在运行时确定这些东西,commonJS模块就是对象,输入时需要查找对象属性// CommonJS模块let { stat, exists, readFile } = require(‘fs’);// 等同于let _fs = require(‘fs’);let stat = _fs.stat;let exists = _fs.exists;let readfile = _fs.readfile;nodeJS 中模块化使用的就是CommonJS的规范,实质就是整体加载fs模块,生成fs_对象,在对象上读取属性和方法,这种加载方式是“运行时加载”ES6 模块ES6 模块不是对象,而是通过export命令显式指定输出的代码,再通过import命令输入。// ES6模块import { stat, exists, readFile } from ‘fs’;上面代码的实质是从fs模块加载 3 个方法,其他方法不加载。这种加载称为“编译时加载”或者静态加载,即 ES6 可以在编译时就完成模块加载,效率要比 CommonJS 模块的加载方式高。export命令一个模块就是一个独立的文件。该文件内部的所有变量,外部无法获取。如果你希望外部能够读取模块内部的某个变量,就必须使用export关键字输出该变量。// profile.jsexport var firstName = ‘Michael’;export var lastName = ‘Jackson’;export var year = 1958;// profile.jsvar firstName = ‘Michael’;var lastName = ‘Jackson’;var year = 1958;export {firstName, lastName, year};export的语法,对外导出接口,在接口名与模块内部变量之间,建立了一一对应的关系。// 写法一export var m = 1;// 写法二var m = 1;export {m};// 写法三var n = 1;export {n as m};import命令注意,import命令具有提升效果,会提升到整个模块的头部,首先执行。目前阶段,通过 Babel 转码,CommonJS 模块的require命令和 ES6 模块的import命令,可以写在同一个模块里面,但是最好不要这样做。因为import在静态解析阶段执行,所以它是一个模块之中最早执行的。下面的代码可能不会得到预期结果。require(‘core-js/modules/es6.symbol’);require(‘core-js/modules/es6.promise’);import React from ‘React’;export default 命令export default命令用于指定模块的默认输出。显然,一个模块只能有一个默认输出,因此export default命令只能使用一次。所以,import命令后面才不用加大括号,因为只可能唯一对应export default命令。本质上,export default就是输出一个叫做default的变量或方法,然后系统允许你为它取任意名字。所以,下面的写法是有效的。// modules.jsfunction add(x, y) { return x * y;}export {add as default};// 等同于// export default add;// app.jsimport { default as foo } from ‘modules’;// 等同于// import foo from ‘modules’;// 正确export var a = 1;// 正确var a = 1;export default a;// 错误export default var a = 1;CommonJS规范每个文件就是一个模块,有自己的作用域。在一个文件里面定义的变量、函数、类,都是私有的,对其他文件不可见。CommonJS规范规定,每个模块内部,module变量代表当前模块。这个变量是一个对象,它的exports属性(即module.exports)是对外的接口。加载某个模块,其实是加载该模块的module.exports属性。var x = 5;var addX = function (value) { return value + x;};module.exports.x = x;module.exports.addX = addX;// 使用var example = require(’./example.js’);console.log(example.x); // 5console.log(example.addX(1)); // 6CommonJS模块的特点所有代码都运行在模块作用域,不会污染全局作用域。模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存。模块加载的顺序,按照其在代码中出现的顺序。export var foo = ‘bar’;setTimeout(() => foo = ‘baz’, 500);ES6 模块化上面代码输出变量foo,值为bar,500 毫秒之后变成baz。这一点与 CommonJS 规范完全不同。CommonJS 模块输出的是值的缓存,不存在动态更新module对象Node内部提供一个Module构建函数。所有模块都是Module的实例module.id 模块的识别符,通常是带有绝对路径的模块文件名。module.filename 模块的文件名,带有绝对路径。module.loaded 返回一个布尔值,表示模块是否已经完成加载。module.parent 返回一个对象,表示调用该模块的模块。module.children 返回一个数组,表示该模块要用到的其他模块。module.exports 表示模块对外输出的值。module.exports属性module.exports属性表示当前模块对外输出的接口,其他文件加载该模块,实际上就是读取module.exports变量。为了方便,Node为每个模块提供一个exports变量,指向module.exports。这等同在每个模块头部,有一行这样的命令。var exports = module.exports;造成的结果是,在对外输出模块接口时,可以向exports对象添加方法。exports.area = function (r) { return Math.PI * r * r;};exports.circumference = function (r) { return 2 * Math.PI * r;};注意,不能直接将exports变量指向一个值,因为这样等于切断了exports与module.exports的联系。exports.hello = function() { return ‘hello’;};module.exports = ‘Hello world’;面代码中,hello函数是无法对外输出的,因为module.exports被重新赋值了。这意味着,如果一个模块的对外接口,就是一个单一的值,不能使用exports输出,只能使用module.exports输出。目录的加载规则和node中模块加载规则一致通常,我们会把相关的文件会放在一个目录里面,便于组织。这时,最好为该目录设置一个入口文件,让require方法可以通过这个入口文件,加载整个目录。在目录中放置一个package.json文件,并且将入口文件写入main字段。下面是一个例子。// package.json{ “name” : “some-library”, “main” : “./lib/some-library.js” }require发现参数字符串指向一个目录以后,会自动查看该目录的package.json文件,然后加载main字段指定的入口文件。如果package.json文件没有main字段,或者根本就没有package.json文件,则会加载该目录下的index.js文件或index.node文件。参考CommonJS规范Module 的语法 ...

December 29, 2018 · 2 min · jiezi

30 行 Javascript 代码搞定智能家居系统

本文首发于『阿里云 IoT 开发者社区』,更多精彩物联网内容欢迎前往浏览。智能家居可谓是今年物联网的热门领域,通过智能单品和智能音箱,人们已然把『智能』两个字变成了生活的理所应当。搭建云上之家除了买买买,还能 DIY。依托阿里云物联网平台,我们用 30 行代码来搞定一套智能家居解决方案。常见的智能家居解决方案包括了设备端、上云、应用端三大部分,更广的还涉及大数据及人工智能。传统的物联网开发非常强调流程性,即设备端、云、应用端三个步骤需要依次进行。而今天,依托于阿里云物联网平台的『物模型』基础,物联网开发的两端可以齐头并进,节省大量的人力物力成本。齐头并进显然很诱人,但是能否再更进一步,一人 Handle 全部开发呢?答案是 YES!目前,有大量互联网开发者由于缺乏嵌入式开发能力,如C/C++语言基础,止步于物联网蓝海的大门。通过阿里云 IoT 提供的 TinyEngine 引擎,可以快速使用 Javascript 进行设备端开发,完美解决这部分开发者的心头大患。而针对不熟悉前后端开发的嵌入式开发者,阿里云物联网平台一样提供了『可视化搭建应用』等快速上手的功能,零代码实现应用开发,大大减轻学习负担。下面我们就使用阿里云物联网开发平台的 TinyEngine 引擎和可视化搭建功能,30 行代码快速开发一个由灯和温湿度计组成的智能家居系统。一、开通服务首先,申请阿里云账号,并开通登陆 Link Develop 一站式开发平台:https://linkdevelop.aliyun.com。之后,新建项目(项目名任意)—— 设备开发 —— 新增产品 —— 所属分类按需选择『灯』或『温湿度计』,通讯方式选择 WiFi ,数据格式选择Alink —— 完成。完成后选择『设备开发』标签页 —— 新增调试设备,记录下设备三元组。二、设备开发打开嵌入式 Javascript 在线工作台(没错,开发环境都不用搭建),创建新项目。替换 index.js代码:1. 灯var deviceShadow = require(‘deviceShadow’);var ledHandle = GPIO.open(“led1”);deviceShadow.bindDevID({ productKey: “”, deviceName: “”, deviceSecret: “”});function main(err){ if(err){ console.log(“连接平台失败”); }else{ console.log(“主程序开始”); deviceShadow.addDevSetPropertyNotify(“LightSwitch”, function (lightStatus) { GPIO.write(ledHandle, 1-lightStatus); }); var mainLoop = setInterval(function () { var ledStatus = GPIO.read(ledHandle); deviceShadow.postProperty(“LightSwitch”, 1-ledStatus); }, 2000); }}deviceShadow.start(main);2. 温湿度计var deviceShadow = require(‘deviceShadow’);var shtc1 = require(‘shtc1’);var handle = new shtc1(‘shtc1’);var ledHandle = GPIO.open(“led”);deviceShadow.bindDevID({ productKey: “a17vi82MmxP”, deviceName: “0001”, deviceSecret: “tYUngSMqYeDxODgtX3DNKkQ7920I3t4T”});function main(err) { if (err) { console.log(“连接平台失败”); } else { console.log(“主程序开始”); var mainLoop = setInterval(function () { var val = handle.getTempHumi(); console.log(‘shtc1:temp=’ + val[0] + ’ humi:’ + val[1]); deviceShadow.postProperty(“CurrentTemperature”, val[0]); deviceShadow.postProperty(“RelativeHumidity”, val[1]); }, 2000); }}deviceShadow.start(main);将设备连接至电脑,点击『连接』并『运行』,设备启动后会自动加载并运行index.js这个文件,同时上报数据至阿里云物联网平台。三、应用开发既然是系统,没有应用可不行,我们利用可视化搭建功能 0 代码快速完成一个应用,只需依次拖入仪表盘和开关组件,替换图片,绑定设备即可完成全部操作。齐活,短短 30 行代码搭建出的端到端智能家居系统就完成了,保存发布后就可以分享给他人访问了。*配合的 TinyEngine 引擎和可视化搭建,开发者无需学习新的编程语言,即可无缝快速切入物联网开发,也彰显了阿里云物联网平台的包容性和独创性。各位开发者,赶紧丢掉犹豫,上手试试吧!https://linkdevelop.aliyun.com本文作者:cxlwill阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

December 11, 2018 · 1 min · jiezi