因为Nodejs在使用第三方Typeorm库过程中,当要查询很多表,一次查询大量数据是时候会占用高额的内存。当项目已经运行上线一两年,避免大动干戈,一直在想有没有其他办法把消耗资源太大的代码提取出来,就像是给电脑之前的风冷换成高级水冷。
问题: AWS EC2 经常卡死,可知的事情为内存过高,cpu突然飙升,事情经常发生在客户在导出excel时有发生。
分析问题: 导出和查询展示数据使用了同一个api和查询方式,是否为exceljs这个导出插件消耗了过多资源呢。
为了方便监听内存变化,本地并没有使用pm2第三方工具,写了一段简单的代码。
setInterval(()=>{
const used = process.memoryUsage().heapUsed / 1024 / 1024;
console.log(`The memory used: ${Math.round(used * 100) / 100} MB`);
}, 200)
第一次运行,当导出时,内存已经达到2G。
一开始盲目认为是excel导出导致,跟exceljs这个第三方库相关。
所以换了stream导出方式,参考了很多github的提问回答和stackoverflow一些大神的分享方式。
核心是将
const workbook = new ExcelJS.Workbook();
改为了
const workbook = new ExcelJS.stream.xlsx.WorkbookWriter({
stream: response, // nodejs的res
useSharedStrings: false,
});
第二次运行,内存依然没有变化,白白花费了很久的时间,好在对exceljs又十分的深入了一遍。
屏蔽掉所有的代码,只留了typeorm查询的部分,打印sql,发现内存占用很大。既然orm比较复杂查询涉及了十几张表。要是单独抽离一个项目关联关系、entity、DTO、token验证、AUTH管道、加密等等,还有很多业务逻辑,非常复杂,短时间也不现实。
所以想到一个折中方案,只导出sql,然后放到Lambda服务上去做数据处理导出。
一般我们创建AWS Lambda,需要登录aws的线上环境。之前要创建一个Lambda非常复杂,本地创建一个Lambda文件夹,书写完代码,无法本地测试,需要上传zip,然后在Lambda当前函数页面选择解压,测试再运行测试。一个简单的输出“Hello word”,可能需要好几分钟才能实现。再加上国内访问aws的网络,一天下来有效工作很低。
使用VScode安装 [AWS Toolkit for Visual Studio Code] 实现本地调试。
插件实际相当于实现了AWS Command Line Interface一些基本的配置集成到了VScode。使用AWS cli 相当于将所有的aws控制台操作都变成了命令行的方式,我们完全可以自己编写一套shell脚本实现lambda 的创建等操作。
aws lambda create-function --function-name my-function \
--zip-file fileb://function.zip --handler index.handler --runtime nodejs12.x \
--role arn:aws:iam::123456789012:role/lambda-ex
aws lambda invoke --function-name my-function out --log-type Tail
言归正传下面讲一讲AWS Toolkit。
首先创建IAM用户,Create New Access Key以创建一对Access Key ID 及Secret Access Key
AWS SAM CLI Install for MAC OS
Install docker
在vscode 应用里找到AWS Toolkit安装。安装成功之后:
打开Lambda列表右键下载到本地目录,到此我们已经成功了一大半。
假如你下载了SAM CLI, 那么选择本地下载后的Lambda函数,然后点击左侧debug,选择debug。
这里你也可以使用Sam cli 终端命令自己测试一个lambda函数,也非常的cool。
sam init
现在,您将拥有如下文件结构:
sam-app/template.yaml 您可以在这里配置API,例如HTTP方法、URL、要运行的代码的位置等。
HelloWorldFunction:
Type: AWS::Serverless::Function # More info about Function Resource: https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction
Properties:
CodeUri: hello_world/
Handler: app.lambda_handler
Events:
HelloWorld:
Properties:
Path: /hello
Method: get
$ cd sam-app/
$ sam build
$ sam local start-api
访问您的本地API
请注意,第一次运行此操作可能需要一些时间,因为这是在设置API环境时进行的,例如创建映像和运行容器。任何后续呼叫都应该已经很快了。
$ curl http://127.0.0.1:3000/hello
{“message”: “hello world”}
npm install --save-dev typescript @types/aws-lambda @types/node
"scripts": { "tsc-init": "tsc --init", "compile": "tsc" }
npm run tsc-init
import {
APIGatewayProxyEvent,
APIGatewayProxyResult }
from "aws-lambda/trigger/api-gateway-proxy";
export const lambdaHandler = async (
event: APIGatewayProxyEvent
): Promise => {
return {
statusCode: 200,
body: JSON.stringify({'test' : 123})
}
}
npm run compile
sam build
sam local invoke -e events/event.json
await uploadToS3({
Bucket: process.env.AWS_BUCKET_NAME,
Key: `${objectKey}.xlsx`,
ContentType:
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
Body: await excelSheet.workbook.xlsx.writeBuffer()
});
//Get signed url with an expiry date
let downloadURL = await getS3SignedUrl({
Bucket: process.env.AWS_BUCKET_NAME,
Key: `${objectKey}.xlsx`,
Expires: 3600 //this is 60 minutes, change as per your requirements
});
export async function uploadToS3(s3Data: S3.PutObjectRequest) {
try {
return await s3.upload(s3Data).promise();
} catch (error) {
console.log(error);
return error;
}
}
export async function getS3SignedUrl(params: any): Promise {
try {
return s3.getSignedUrl("getObject", {
Bucket: params.Bucket,
Key: params.Key,
Expires: params.Expires,
});
} catch (error) {
console.log(error);
throw error;
}
}