serverless的理念是即时弹性,用完及走。服务并非长时间运行,这也就意味着像websocket这种长链接的请求模式看起来并不适合serverless,但是否有其他的办法即能满足长连接模式请求,也能够利用serverless本身特性呢?答案是肯定的,从前面的一篇文章我们看到了网关的关键作用,所以这次也是通过网关来解决全双工通信的问题。本次以弹幕场景为例来为大家展开我们是怎么使用serverless架构来实现这个场景的。
应用效果预览
弹幕应用的实用场景比较多,比如运营推广,年会活动等。本次弹幕应用演示的源码均已放至github,大家可以自行修改使用
架构一览
整体架构依然采用dns解析->网关-> oss|fc 。不一样的是分了3个静态资源的工程,函数部分则采用事件驱动和http相结合,并且 api 部分采用tablestore进行数据的持久化。
流程说明
弹幕应用总共由大屏幕, 个人用户,管理员三个客户端,以及一个注册设备的服务和一个api服务。客户端跟服务端的长链接由网关来承载,每次客户端连接到网关的时候,网关都会存储设备编号,并且触发一次注册函数,设备编号存储到tablestore。当用户发起弹幕的时候经网关到api服务,api服务会做一次查询先判断弹幕是否被管制,如果无管制则直接查找当前的大屏幕设备id,并且进行网关的下行调用,网关在发到前端页面,显示数据。如果被管制,则查询在线的管理员设备,将弹幕下行通知到网关,网关发送给管理员前端页面。
数据表设计
equipment(设备)
字段 |
类型 |
说明 |
id |
string |
设备表主键 |
deviceId |
string |
设备id |
docId |
string |
备用字段 |
type |
string |
设备类型(screen|admin) |
barrage(弹幕)
字段 |
类型 |
说明 |
gid |
string |
分区键 |
id |
integer |
主键自增 |
fromId |
string |
弹幕作者id |
fromName |
string |
来源作者名称 |
color |
string |
弹幕颜色 |
fontSize |
string |
弹幕字体大小 |
checkStatus |
integer |
弹幕状态0(未处理)1(审批通过)2(审批未过) |
sendTime |
string |
弹幕发送时间 |
checkTime |
string |
弹幕更新时间 |
message |
string |
弹幕内容 |
interceptor (过滤器)
字段 |
类型 |
说明 |
id |
integer |
主键/分区键 |
status |
integer |
拦截状态0不拦截 1拦截 2拦截加过滤 |
filterWords |
string |
过滤字段 |
准备工作
同前篇《人人都是Serverless架构师之现代化Web应用开发实战》文章一样需要准备域名,以及安装安装好Serverless devs开发者工具,还有下面的产品
这次我们引入了tablestore的数据库记性数据的持久化,同样需要创建好数据库实例备用。
操作步骤
为了更好的展示效果,本次演示使用ServerlessDesktop来给大家演示一下如何2分钟部署一个这样复杂的弹幕应用。你可以根据自身需要选择Serverless Devs Cli 或者 Serverless Desktop对弹幕应用进行初始化和部署构建。
秘钥配置
略
初始化
本次初始化除了将应用模板下载到本地之外,还会帮忙初始化tablestore的表及数据,所以需要预配置几个参数
- 秘钥别名 - 对应你的阿里云账号
- 域名 - 自定义域名
- bucketName - oss的bucket名称
- endpoint - 对应tablestore 实例的公网访问地址
- instance - 对应tablestore 的实例名
预配置参数写好后点击“确定”,接下来的工作就叫给Serverless Devs,他会帮我们初始化弹幕应用的表。
构建部署
初始化之后,我们重新进入配置页面,对项目进行部署。配置信息->全量操作->deploy 点击后其他的就交给Serverless Devs了,他会帮助我们完成 大屏幕,管理后台和玩家的前端部署,注册函数以及api函数的部署以及网关的路由设置和网关的域名绑定
部署效果查看
网关
函数计算
Oss
DNS
此时访问barragego.serverless-developer.com发现访问不同,检查发现是 apigateway 的域名和oss 域名都未绑定成功,我们手动处理一下
接下来再访问barragego.serverless-developer.com 即可看到效果
数据库明细
数据库方面想拿出来说一下,主要本次用的数据库确实比较新,也就是tablestore
数据库配置传递
可以看到,我们在初始化应用的时候是填写了数据库的公网访问地址和实例名称信息的,初始化的时候会把用户的输入配置写入到s.yaml中,这里如果是比较敏感的信息建议从s.yaml提取出来放到.env环境中,并且ignore掉这个文件,减少数据库信息被泄露到代码仓库的风险。
最终devs 会把这两个基本信息放到函数计算的环境变量中然后各运行时可以通过环境变量取到这些值,比如这里是nodejs 的运行环境,则通过process.env.instance获取。
除了实例名称和公网访问地址外数据库的初始化还需要 用户的秘钥信息。鉴于秘钥信息的敏感性比较高,不建议直接把秘钥信息配置到s.yaml里,而是通过给函数服务授权tablestore角色权限,让函数内置临时秘钥信息
函数服务授权配置如下:
函数内获取秘钥信息如下:
数据库初始化
为了减少数据库初始化次数,我们可以在函数的 initializer方法中初始化,当函数未被释放的时候可以直接使用数据库的实例而不必重新连接。这样可以降低请求响应时间。单实例多并发的情况下比较实用。
exports.initializer = (context, callback) => { try { const ak = context.credentials.accessKeyId; const sk = context.credentials.accessKeySecret; const stsToken = context.credentials.securityToken; SAT.init(endpoint, instance, ak, sk, stsToken); internal = { tableClient: SAT, TableStore }; callback(); } catch (err) { callback(err.message); } }
数据库实例初始化之后,我们通过赋值给全局变量来从其他的方法中取得实例,进行后续的操作
CRUD
tablestore 原生的api 去做CRUD操作用户体验不够友好,这里借助tablestore社区提供了一个很好的封装SAT我们用它来做基础的增删改查会非常的方便,代码看起来也非常整洁。
// 单主键查询 const getInterceptor = async (ctx) => { const { tableClient } = ctx.req.requestContext.internal; const res = await tableClient.table('interceptor').get(1, cols = []); return res; } // 查询全部 const getAllEquipment = async (tableClient,TableStore) => { const res = await tableClient.table('equipment').getRange(TableStore.INF_MIN, TableStore.INF_MAX, cols = []) return Object.keys(res).map((key)=> res[key]); } // 双主键(一个分区键,一个自增键)的插入 const addBarrage = async (ctx) => { const { tableClient, TableStore } = ctx.req.requestContext.internal; const { fromId, fromName, color, fontSize = '28px', checkStatus = 0, message } = ctx.request.body; const currentTime = Date.now().toString(); const newData = Object.assign({}, { fromId, fromName, color, fontSize, checkStatus: parseInt(checkStatus), message }, { sendTime: currentTime, checkTime: currentTime }); const res = await tableClient.table('barrage', ['gid', 'id']).put([1, TableStore.PK_AUTO_INCR], newData, c = 'I'); return res; } // 更新 const updateBarrage = async (ctx) => { const { tableClient } = ctx.req.requestContext.internal; const { checkStatus } = ctx.request.body; const { id } = ctx.request.params; const currentTime = Date.now().toString(); const res = await tableClient.table('barrage', ['gid', 'id']).update([1, parseInt(id)], { checkStatus: parseInt(checkStatus), checkTime: currentTime }, c = 'I') return res; } // 条件查询 const getBarrageByCondition = async (ctx) => { const { tableClient, TableStore } = ctx.req.requestContext.internal; const res = await tableClient.table('barrage').search('index', ['checkStatus', 0]) return res; }
当然如果你想做更高级的查询,就需要自己去查阅官网文档了
更多
这个项目本身是对serverless 如何使用websocket的一个展示示例,你可以把他变成任意相近形态的应用,比如聊天室,多人协作平台等。应用本身可以拿过去做二次改进,比如增加点赞,管控部分可以加上管理员的登录注册等。总之你可以根据自身需求定制更高级的功能,相关的源码已经提供出来供大家参考,下个篇章会跟大家聊一聊serverless和低代码的场景,分享一个我们最近做的实践。