Article / 文章中心

优化托管于阿里云函数计算的Node.js应用 - 以Parse为例

发布时间:2022-02-22 点击数:591

上文介绍了怎么快速搬迁Parse到阿里云函数核算,可是这只是一个跑起来的比如,还有一些问题需求咱们优化。本文会介绍常见的优化点和办法,从办法来看适用于一切Serverless渠道的应用。

Serverless的缺陷

没有任何技能形状是完美的,Serverless供给了杰出的可伸缩性和并发性,供给了细粒度的资源分配,优化了成本,相对的也有难以调试等缺点。

这些问题是Serverless这种技能形状本身形成的,并不是阿里云函数核算独有的。不同的云厂商能够经过周边建设来补偿一些问题,比如阿里云函数核算的日志和监控相对比较完善,Serverless Devs工具处理了一部分调试问题。

用更传统的观点来了解Serverless的本质,能够看作扩容缩容战略极端激进的集群,而每个函数都是布置在这一个一个机器上罢了。云厂商的机器特别迷你,计价单位颗粒小。而缩容战略能够将为0,扩容战略能够近乎无限大,缩容战略是固定,不能够自定义。

那么对于一个随时可能创建随时可能被毁掉的机器,布置于其间的服务要面临两个方面的问题

  • 服务毁掉
  • 服务发动

服务毁掉时内存、文件体系的数据都丢失了。服务发动的时分需求一些必要的初始化,需求发动程序。

咱们先看下毁掉引起的耐久化问题。

耐久化改善

Parse是支撑文件上传的,存储文件的FileAdapter是能够自定义的。

一般来说对于文件需求,能够直接运用阿里云目标存储OSS,一般挑选标准型就能够了。


aliyun-oss-price.png

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。


aliyun-function-model.png


初始化只会进行一次,preFreeze和preStop便是退出前的Hook,这三处也是同样的计费。


因为Parse也涉及到数据库衔接,所以能够将数据库衔接部分移动到initialize中。

除了生命周期上一般来说还有一些挑选


提升内存分配:函数核算能够自行配置内存,对于部分应用(特别是有初始化扫描等)加大内存能够改善发动速度


调整结构或者渠道:对于NodeJs而言,新版别普遍都有性能上的优化,选用尽可能新的NodeJs版别也能够加快发动。假如实在对时刻很灵敏,可能要考虑Rust等发动速度更友好的语言。


在发动函数中初始化更多的共享资源:这个其实不能处理第一次冷发动的时刻,可是能够让每次call的耗时更少。


减缩包大小:对于不必要的三方库优先移除,也能够运用更精简的版别进行替换。


定时激活:这个最早在AWS Lambda上广泛运用,其实本质上是保存一个常驻实例,可是依靠的云厂商的机制。比如AWS Lambda大约30-40分钟收回之前的活跃实例。这样只需求一个定时触发器就能够进行激活操作。这个办法在一切Serverless渠道都能够运用。可是需求正确处理来自HTTP触发器和Event触发器的逻辑。