本文版权归作者所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
转载自夜明的孤行灯
本文链接地址: https://www.huangyunkun.com/2022/02/22/enhance-parse-in-serverless/
本文首发于阿里云开发者社区:https://developer.aliyun.com/article/871751,修改后发布在本博客
上文介绍了如何快速迁移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。

由于Parse也涉及到数据库连接,所以可以将数据库连接部分移动到initialize中。
除了生命周期上一般来说还有一些选择
提升内存分配:函数计算可以自行配置内存,对于部分应用(特别是有初始化扫描等)加大内存可以改进启动速度
调整框架或者平台:对于NodeJs而言,新版本普遍都有性能上的优化,选用尽可能新的NodeJs版本也可以加速启动。如果实在对时间很敏感,可能要考虑Rust等启动速度更友好的语言。
在启动函数中初始化更多的共享资源:这个其实不能解决第一次冷启动的时间,但是可以让每次call的耗时更少。
缩减包大小:对于不必要的三方库优先移除,也可以使用更精简的版本进行替换。
定时激活:这个最早在AWS Lambda上广泛使用,其实本质上是保留一个常驻实例,但是依赖的云厂商的机制。比如AWS Lambda大约30-40分钟回收之前的活跃实例。这样只需要一个定时触发器就可以进行激活操作。这个方法在所有Serverless平台都可以使用。但是需要正确处理来自HTTP触发器和Event触发器的逻辑。
参考
https://github.com/ali-sdk/ali-oss
本文版权归作者所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
转载自夜明的孤行灯
本文链接地址: https://www.huangyunkun.com/2022/02/22/enhance-parse-in-serverless/