阿里云Serverless技术、产品选型理由
从发展历程中,我们也可以看出,以函数为起点,Serverless 的技术和产品逐步趋于多元化。多种形态相互补充,为解决更复杂、更多样化的业务场景提供支撑,我们通常会把场景分为三类:
- 函数计算(FaaS):面向函数,用户只需关注函数层级的代码,用于解决轻量型、无状态、有时效的任务;>
- Serverless 应用托管:面向应用,用户只需要更关注应用本身,与微服务结合,它融合应用治理、可观测,降低了新应用的构建成本、老应用的适配改造成本;
- Serverelss 容器服务:面向容器,在不改变当前 Kubernetes 的前提下,由于不再需要关注节点,大大提升了前期资源的准备过程,降低了维护成本,使得应用的全生命周期管理更高效。
随着 Serverless 的内涵持续外延,计算的 Serverless 化要求 BaaS 服务也具备与之配套的托管能力和极致弹性,全链路完整支撑真正 Serverless 化的应用构建。
——《Serverless 进阶实战课》极客时间
Serverless 产品概念:
Servless解决方案图:
目前Serverless技术绝大部分都是基于Knative来进行构建的。Knative是什么:
Knative is a platform-agnostic solution for running serverless deployments.
Knative 是一种用于运行无服务器部署的与平台无关的解决方案。
运行Serverless函数会自动生成一个容器:
When you build or run a function, an Open Container Initiative (OCI) format container image is generated automatically for you, and is stored in a container registry. Each time you update your code and then run or deploy it, the container image is also updated.
当您构建或运行函数时,会自动为您生成开放容器计划 (OCI) 格式的容器映像,并将其存储在容器注册表中。每次更新代码然后运行或部署它时,容器映像也会更新。
也就是无论时面向函数、应用、容器的Serverless服务,实际上底层的实现都是通过将代码打包成OCI镜像,然后用Knative容器来运行该镜像的。
容器与虚拟机的差异:
容器允许应用组件共享单个操作系统实例的资源,就像虚拟机 (VM) 允许应用共享单个物理计算机的资源一样。 容器比虚拟机更小、资源效率更高,更适合敏捷和 DevOps 开发方法的增量发布周期,容器已成为现代云原生应用事实上的计算单元。
再且由于实际上Serverless的运行单位是容器,再加上阿里云支持自定义容器镜像,所以可以将Rust、AI、GPU等非阿里云内置支持语言打包成容器镜像以后,进行Serverless部署。
Serverless预留资源
TableStore当业务稳定以后,需要将纯按量计费提升为预留吞吐量按量计费,这样可以节省成本。
访问量稳定以后,可以通过预留函数实例来避免冷启动导致请求延迟
详情:配置预留实例
Serverless应用开发
前提:
- 因为事件函数不能响应HTTP请求,不能处理HTTP触发器的参数,所以函数类型只能选择Web函数(可自定义容器镜像)。
- 再进一步,想要提交代码以后自动构建并部署函数,并对函数版本进行管理,就使用“应用”
创建Serverless应用
应用创建:选择模板(Express,轻量级nodejs,必要功能齐全,可通过中间件扩展)
因为应用最终还是要变成函数执行,所以创建应用会带一个函数,该函数在后续也不能删除。
开发和部署
创建出来的模板仓库中,有README.md文档,里面有如何本地开发并部署上云的。(但是windows下执行s deploy会导致异常,因为s.yaml中的命令是unix的。)
所以直接在WebIDE开发,然后保存提交部署可以解决部署问题(但是测试速度慢,得等1分钟部署好)
添加依赖的方式
nodejs可以直接在package.json中添加依赖。
通过层添加依赖,是一个可以在多个函数中共用依赖的方法。将公共依赖或者工具类放到层中,让函数依赖层。
WebIDE终端安装依赖的方式无效,并不会实际作用到package.json中。
Serverless应用集成TableStore
客户端配置
根据推荐的方式,将拥有TableStore权限的RAM账号,AccessKey ID和Secret设置为环境变量,在程序中读取环境变量,会更安全。
但是每次应用部署时就会覆盖函数的配置,包括环境变量。所以环境变量目前只写在了s.yaml中,和硬编码在代码里差不多。
Serverless函数的日志设置也是,每次应用部署都会被应用覆盖,导致没开启,这个还得解决。
Express请求中等待TableStore请求响应再返回
因为Express的Route Handler有同步和异步两种形式。
TableStore的Client请求时,既接受callback回调函数,也会在同一个方法调用时返回一个Promise,以用来支持async语法(官方示例没有提到,翻看源码才看到)。
如果是直抄默认示例的话,那么同步的Route Handler加上Client的回调函数,会导致在Client的回调函数还没有执行完的时候,Express请求响应就结束了。或者如果把response的输出放在了callback回调函数中的话,就会导致Route Handler执行完了,但是没有返回响应。
所以正确的做法是,使用async修饰Route Handler,书写成异步路由处理器;再在Route Handler方法中使用await等待TableStore的Client调用的Promise结果resolve或者catch。这样才能在Route Handler处理完成时,既拿到TableStore的处理结果,也能将处理结果在响应中返回。
条件查询
因为SQL查询需要额外按量收费,所以优先使用TableStore提供的API。
条件查询文档:过滤器 | TableStore 阿里云文档
function getRowWithCompositeCondition() {
//设置过滤器,当col1 = 表格存储且col5 = 123456789时,返回该行。
var condition = new TableStore.CompositeCondition(TableStore.LogicalOperator.AND);
condition.addSubCondition(new TableStore.SingleColumnCondition('col1', '表格存储', TableStore.ComparatorType.EQUAL));
condition.addSubCondition(new TableStore.SingleColumnCondition('col5', Long.fromNumber(123456789), TableStore.ComparatorType.EQUAL));
params.columnFilter = condition;
client.getRow(params, function (err, data) {
if (err) {
console.log('error:', err);
return;
}
console.log('success:', data);
});
}
条件更新
样例代码:
var TableStore = require('../index.js');
var Long = TableStore.Long;
var client = require('./client');
var params = {
tableName: "sampleTable",
primaryKey: [{ 'gid': Long.fromNumber(20013) }, { 'uid': Long.fromNumber(20013) }],
updateOfAttributeColumns: [{ 'PUT': [{ 'col1': 'test6' }] }]
};
/*
条件更新使用说明:
1、期望行是否存在
RowExistenceExpectation.IGNORE 表示不管此行是否已经存在,都会插入新数据,如果之前有会被覆盖。
RowExistenceExpectation.EXPECT_EXIST 表示只有此行存在时,才会插入新数据,此时,原有数据也会被覆盖。
RowExistenceExpectation.EXPECT_NOT_EXIST 表示只有此行不存在时,才会插入数据,否则不执行。
2、条件组合
TableStore.SingleColumnCondition 只有一个条件的时候使用
TableStore.CompositeCondition 有多个条件的时候使用
*/
//示例:指定条件 期望行存在,并且name=john,addr=china
var condition = new TableStore.CompositeCondition(TableStore.LogicalOperator.AND);
condition.addSubCondition(new TableStore.SingleColumnCondition('name', 'john', TableStore.ComparatorType.EQUAL));
condition.addSubCondition(new TableStore.SingleColumnCondition('addr', 'china', TableStore.ComparatorType.EQUAL));
params.condition = new TableStore.Condition(TableStore.RowExistenceExpectation.EXPECT_EXIST, condition);
client.updateRow(params,
function (err, data) {
if (err) {
console.log('error:', err);
return;
}
console.log('success:', data);
});