优化托管于阿里云函数计算的Node.js应用 - 以Parse为例
上文介绍了怎么快速搬迁Parse到阿里云函数核算,可是这只是一个跑起来的比如,还有一些问题需求咱们优化。本文会介绍常见的优化点和办法,从办法来看适用于一切Serverless渠道的应用。
Serverless的缺陷
没有任何技能形状是完美的,Serverless供给了杰出的可伸缩性和并发性,供给了细粒度的资源分配,优化了成本,相对的也有难以调试等缺点。
这些问题是Serverless这种技能形状本身形成的,并不是阿里云函数核算独有的。不同的云厂商能够经过周边建设来补偿一些问题,比如阿里云函数核算的日志和监控相对比较完善,Serverless Devs工具处理了一部分调试问题。
用更传统的观点来了解Serverless的本质,能够看作扩容缩容战略极端激进的集群,而每个函数都是布置在这一个一个机器上罢了。云厂商的机器特别迷你,计价单位颗粒小。而缩容战略能够将为0,扩容战略能够近乎无限大,缩容战略是固定,不能够自定义。
那么对于一个随时可能创建随时可能被毁掉的机器,布置于其间的服务要面临两个方面的问题
- 服务毁掉
- 服务发动
服务毁掉时内存、文件体系的数据都丢失了。服务发动的时分需求一些必要的初始化,需求发动程序。
咱们先看下毁掉引起的耐久化问题。
耐久化改善
Parse是支撑文件上传的,存储文件的FileAdapter是能够自定义的。
一般来说对于文件需求,能够直接运用阿里云目标存储OSS,一般挑选标准型就能够了。
Parse官方不支撑阿里云OSS,理论上能够运用parse-server-s3-adapter,可是我之前没有配置过,能够完全能够自定义,直接运用OSS官方的SDK就行了。
'use strict'; var OSS = require('ali-oss').Wrapper; const DEFAULT_OSS_REGION = "oss-cn-hangzhou"; function requiredOrFromEnvironment(options, key, env) { options[key] = options[key] || process.env[env]; if (!options[key]) { throw `OSSAdapter requires option '${key}' or env. variable ${env}`; } return options; } function fromEnvironmentOrDefault(options, key, env, defaultValue) { options[key] = options[key] || process.env[env] || defaultValue; return options; } function optionsFromArguments(args) { let options = {}; let accessKeyOrOptions = args[0]; if (typeof accessKeyOrOptions == 'string') { options.accessKey = accessKeyOrOptions; options.secretKey = args[1]; options.bucket = args[2]; let otherOptions = args[3]; if (otherOptions) { options.bucketPrefix = otherOptions.bucketPrefix; options.region = otherOptions.region; options.directAccess = otherOptions.directAccess; options.baseUrl = otherOptions.baseUrl; options.baseUrlDirect = otherOptions.baseUrlDirect; } } else { options = accessKeyOrOptions || {}; } options = requiredOrFromEnvironment(options, 'accessKey', 'OSS_ACCESS_KEY'); options = requiredOrFromEnvironment(options, 'secretKey', 'OSS_SECRET_KEY'); options = requiredOrFromEnvironment(options, 'bucket', 'OSS_BUCKET'); options = fromEnvironmentOrDefault(options, 'bucketPrefix', 'OSS_BUCKET_PREFIX', ''); options = fromEnvironmentOrDefault(options, 'region', 'OSS_REGION', DEFAULT_OSS_REGION); options = fromEnvironmentOrDefault(options, 'directAccess', 'OSS_DIRECT_ACCESS', false); options = fromEnvironmentOrDefault(options, 'baseUrl', 'OSS_BASE_URL', null); options = fromEnvironmentOrDefault(options, 'baseUrlDirect', 'OSS_BASE_URL_DIRECT', false); return options; } function OSSAdapter() { var options = optionsFromArguments(arguments); this._region = options.region; this._bucket = options.bucket; this._bucketPrefix = options.bucketPrefix; this._directAccess = options.directAccess; this._baseUrl = options.baseUrl; this._baseUrlDirect = options.baseUrlDirect; let ossOptions = { accessKeyId: options.accessKey, accessKeySecret: options.secretKey, bucket: this._bucket, region: this._region }; this._ossClient = new OSS(ossOptions); this._ossClient.listBuckets().then((val) => { var bucket = val.buckets.filter((bucket) => { return bucket.name === this._bucket }).pop(); this._hasBucket = !!bucket; }); } OSSAdapter.prototype.createBucket = function () { if (this._hasBucket) { return Promise.resolve(); } else { return this._ossClient.putBucket(this._bucket, this._region).then(() => { this._hasBucket = true; if (this._directAccess) { return this._ossClient.putBucketACL(this._bucket, this._region, 'public-read'); } return Promise.resolve(); }).then(() => { return this._ossClient.useBucket(this._bucket, this._region); }); } }; OSSAdapter.prototype.createFile = function (filename, data, contentType) { let options = {}; if (contentType) { options.headers = {'Content-Type': contentType} } return this.createBucket().then(() => { return this._ossClient.put(this._bucketPrefix + filename, new Buffer(data), options); }); }; OSSAdapter.prototype.deleteFile = function (filename) { return this.createBucket().then(() => { return this._ossClient.delete(this._bucketPrefix + filename); }); }; OSSAdapter.prototype.getFileData = function (filename) { return this.createBucket().then(() => { return this._ossClient.get(this._bucketPrefix + filename).then((val) => { return Promise.resolve(val.content); }).catch((err) => { return Promise.reject(err); }); }); }; OSSAdapter.prototype.getFileLocation = function (config, filename) { var url = this._ossClient.signatureUrl(this._bucketPrefix + filename); url = url.replace(/^http:/, "https:"); return url; }; module.exports = OSSAdapter; module.exports.default = OSSAdapter;
这个是我正在用的adapter,能够参阅运用。特别是getFileLocation,要根据自己情况运用。
Parse还有一个缓存,一般默许运用本地环境,可是考虑到Serverless的特性,这一部分还是要耐久化用于加快。官方供给的RedisCacheAdapter能够直接运用。Redis集群要求不是很高,最好复用已有的,单独运用成本有点高。
发动改善
Serverless函数的生命周期问题一直是搬迁的阻止,比较明显的是异步恳求丢失、高雅下线困难。阿里云函数核算对于模型有必定扩展,额定供给了一些Hook。
初始化只会进行一次,preFreeze和preStop便是退出前的Hook,这三处也是同样的计费。
因为Parse也涉及到数据库衔接,所以能够将数据库衔接部分移动到initialize中。
除了生命周期上一般来说还有一些挑选
提升内存分配:函数核算能够自行配置内存,对于部分应用(特别是有初始化扫描等)加大内存能够改善发动速度
调整结构或者渠道:对于NodeJs而言,新版别普遍都有性能上的优化,选用尽可能新的NodeJs版别也能够加快发动。假如实在对时刻很灵敏,可能要考虑Rust等发动速度更友好的语言。
在发动函数中初始化更多的共享资源:这个其实不能处理第一次冷发动的时刻,可是能够让每次call的耗时更少。
减缩包大小:对于不必要的三方库优先移除,也能够运用更精简的版别进行替换。
定时激活:这个最早在AWS Lambda上广泛运用,其实本质上是保存一个常驻实例,可是依靠的云厂商的机制。比如AWS Lambda大约30-40分钟收回之前的活跃实例。这样只需求一个定时触发器就能够进行激活操作。这个办法在一切Serverless渠道都能够运用。可是需求正确处理来自HTTP触发器和Event触发器的逻辑。