2024 探索云开发及其应用
本文最后更新于:2024年7月21日 凌晨
原来这就是云开发! Serverless?!
探索云开发及其应用
开始想分享一篇 AI 相关的文章,但是比较尴尬的是很多了解都是在应用层面,理论知识又不够丰富;
后面在写 AI Demo 的时候想到部署这块,恰好在云开发上有些体验,结合个人理解编写本文
云开发
概述
引用腾讯云云开发的介绍
「云开发」是云原生一体化开发环境和工具平台,为开发者提供高可用、自动弹性扩缩的后端云服务,包含计算、存储、托管等 Serverless 化能力,可用于云端一体化开发多种端应用(小程序、公众号、Web 应用等),帮助开发者统一构建和管理后端服务和云资源,避免了应用开发过程中繁琐的服务器搭建及运维,开发者可以专注于业务逻辑的实现,开发门槛更低,效率更高。
优劣势
优势
弹性伸缩: 根据应用负载的变化,自动扩展或缩减计算资源,提高了稳定性,满足并发场景。
无服务器架构(Serverless): 云开发采用无服务器架构,开发者只需关注代码的编写,无需关心服务器的维护和管理,降低了运维成本。Serverless 并非表示没有服务器,而是指开发者无需关心底层的服务器管理。
多环境快速部署: 云开发可以将应用部署在全球范围内的服务器上,提供低延迟和高可用性的服务
性价比高: 按使用量计费,无需支付闲置资源费用
劣势
平台强相关: 使用云开发平台意味着对云服务提供商的依赖,依赖于平台提供的环境和功能进行开发
定制化能力不足: 一些云开发平台为了提供简便性和快速开发,可能限制了一些高度定制的选项,对于一些特殊需求的应用可能不够灵活
数据隐私和合规: 对于一些行业或地区的应用,特别关注数据隐私和合规性。使用云开发平台时,需要确保选择的平台符合相关法规和标准
网络延迟: 尽管云服务提供商通常有全球性的数据中心,但仍然可能存在一些网络延迟,对于某些对延迟非常敏感的应用可能会成为一个问题
应用场景
轻量应用后端: 通过无服务器框架构建灵活的后端服务
事件驱动处理: 处理异步事件,如消息队列、文件上传
定时任务: 周期性执行
选择
云开发平台那么多,选择一个平台需要考虑哪些方面?几个常见的考虑点:
功能多少:是否提供满足业务场景的能力或者说可以定制化开发的能力
迭代速度:平台开发功能是否稳定,直接影响开发者体验
环境稳定:决定线上生产环境是否稳定,直接影响应用稳定性和用户体验
费用核算:付费方式及各种计费资源的价格
通常找到一个大公司背书的云开发平台就能解决以上问题,但受制于多方面的因素影响,还是需要找到更适合当前场景的平台并做好备份迁移等措施。
产品分析
这里列举的主要是云开发平台,部分平台只是函数计算
海外
Google Firebase
官网地址:https://firebase.google.com/
Firebase 是一家实时后端数据库创业公司,它能帮助开发者很快的写出Web端和移动端的应用。自2014年10月Google 收购 Firebase以来,用户可以在更方便地使用Firebase的同时,结合Google的云服务。
虽然Firebase本身是一个应用开发的平台,但一个亮点功能是 Analytics(分析),所以面向的不只是研发人员,还有运营人员,包括一些市场投放人员。
Google Cloud
官网地址:https://cloud.google.com/compute
谷歌提供的云函数计算服务,结合平台提供的其他云服务能力实现应用开发
AWS Lambda
官网地址:https://aws.amazon.com/lambda
亚马逊提供的云函数计算服务,结合平台提供的其他云服务能力实现应用开发
Azure Function
官网地址:https://azure.microsoft.com/en-us/products/functions/
微软提供的云函数计算服务,结合平台提供的其他云服务能力实现应用开发
国内
阿里云 - 云开发(已下线)
云开发平台即将停止对外服务,如果需要云开发相关的功能,您可以根据自己的需求使用阿里云其他云产品提供的同类工具:函数计算应用管理、云效AppStack、SAE应用管理
官网地址:https://workbench.aliyun.com/
云开发平台下线,但是其他云服务可以满足同样的能力,服务对个人和企业可用
字节跳动 - 轻服务(已下线)
轻服务将从2022年5月14日起停止服务创建,2022年6月14日23点59分停止服务并删除所有资源
官网地址:https://qingfuwu.cn/
字节跳动旗下云服务平台「火山引擎」提供函数服务,但服务对象是企业,不面向个人开发者
腾讯云 - 云开发
官网地址:https://cloud.tencent.com/product/tcb
目前应该是国内大厂还在提供云开发平台服务的,从免费到商业化,经历过多轮价格调整。
Aircode(已下线)
官网地址:https://aircode.io/
开发文档地址:https://docs.aircode.io/
开源组织:https://github.com/AirCodeLabs
不知道什么名字的团队在孵化的项目,提供一些的基础能力,还在功能快速迭代期,刚进入商业化不久
提供三个免费项目额度,超出需要购买收费套餐
Laf
官网地址: laf.run (国内版) laf.dev (海外版)
开发文档地址:https://doc.laf.run/zh/
开源组织:https://github.com/labring/laf
国内企业环界云计算孵化的项目,目前进入商业化阶段,新用户赠送一定余额体验
应该是国内除了大厂外做的最好的(或者说云开发不是腾讯云就是Laf?哈哈)
亮点:
代码开源,拥抱社区,函数市场
支持私有化部署
在线协作
支持 Websocket 链接
小结
可以看到上述云服务厂商或平台提供了主要的云能力:函数计算、数据库、存储、CDN
海外厂商基本都只提供单独的云服务能力,没有将服务聚合成云开发平台进行销售
国内大部分厂商都对云开发平台进行下线处理,将用户引流到自家的各种云服务能力
腾讯云考虑继续将服务聚合成云开发平台,笔者猜测原因是:
新的云开发平台收获好评,拥抱开源社区,发展势头强劲
以前比较火的还有「新浪 SAE」 ,不知道什么原因在国内市场渐渐消失。
使用体验
实际上云开发平台功能的使用上基本一致,这里主要拿 **laf 平台 **举例。
30s 完成 helloword 接口的创建、开发、发布,如下图示例。
基于上述路径,我们可以编写「云函数」,实现自己的逻辑,快速完成上线
需要注意的是,每次函数的执行都是在新的环境里,也就是两次函数的状态本身是不共享的,即无状态
但是一些数据需要进行持久存储,这时候需要使用到 「数据库」** **能力
平台本身封装数据库的能力并注入到函数的上下文当中,即下面代码中提供的@lafjs/cloud
// 官方文档示例
import cloud from '@lafjs/cloud'
const db = cloud.mongo.db
export default async function () {
const ret = await db.collection('users').insertOne({
name: '王小波',
age: 44,
books: ['黄金时代', '白银时代', '青铜时代']
})
console.log(ret)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
继续进行应用开发,可能遇到一些非结构化的数据或者说大文件的存储,数据库不能满足诉求了
这时候需要「**文件存储**」来解决这个问题,平台也把存储能力集成到 SDK 里
```javascript
// 官方文档示例
import cloud from '@lafjs/cloud'
export default async function (ctx: FunctionContext) {
// 获取存储桶
const bucket = cloud.storage.bucket('data')
// 写文件,假设是一个大文件
const content = 'hello, laf'
await bucket.writeFile('laf.html', content)
}
云开发三件套组合技一使用,貌似应用后端的雏形就出来了,前端同学可以大声说:**~~后端不是有手就行?**~~
确实,云开发平台大大降低了开发的成本,只关注逻辑即可完成开发,一个人也可以是一支军队
除了上面说的三件套,平台也会提供其他的一些功能:
- 大多数平台都会提供的触发器,满足部分定时任务的场景
- 腾讯云额外提供兼容 Redis 协议的缓存数据库,满足高速缓存的场景
- Laf 平台提供处理 WebSocket 连接的云函数能力
尽管平台提供再全面的功能,但是云开发不是万金油,也会有不能满足的场景,这时候就要考虑其他方案
# 技术分析
> 感兴趣可以了解关于 laf 的一些题外话
> [laf 的发展背景和方向讨论 · labring/laf · Discussion #178](https://github.com/labring/laf/discussions/178)
> [laf next 重构计划说明 · labring/laf · Discussion #352](https://github.com/labring/laf/discussions/352)
> [laf product key & roadmap · labring/laf · Discussion #354](https://github.com/labring/laf/discussions/354)
从个人使用和理解的角度出发,对云开发平台使用到的核心能力进行简单梳理。
怎么从零开始跑通一个最简单云开发平台流程呢?
首先,核心能力分为云函数、数据库、文件存储三个方面
## 云函数
云函数,首先是一个函数,写在函数内的逻辑会被执行,实际上就是一段 ESM 代码
编写的函数需要默认导出,平台后续会导入并调用
```javascript
import cloud from '@lafjs/cloud'
export default async function (ctx: FunctionContext) {
console.log('Hello World')
return { data: 'hi, laf' }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
### 平台能力注入
将各种实例封装成 SDK 引入到每一个云函数文件里,实现开箱即用,即`@lafjs/cloud`
```javascript
// https://github.com/labring/laf/blob/main/runtimes/nodejs/src/support/cloud-sdk.ts
/**
* Create a new Cloud SDK instance
*
* @returns
*/
export function createCloudSdk() {
const cloud: CloudSdkInterface = {
database: () => getDb(DatabaseAgent.accessor),
invoke: invokeInFunction,
shared: _shared_preference,
getToken: getToken,
parseToken: parseToken,
mongo: {
client: DatabaseAgent.client as any,
db: DatabaseAgent.db as any,
},
sockets: WebSocketAgent.clients,
appid: Config.APPID,
get env() {
return process.env
},
storage: null,
}
/**
* Ensure the database is connected, update its Mongo object, otherwise it is null
*/
DatabaseAgent.ready.then(() => {
cloud.mongo.client = DatabaseAgent.client as any
cloud.mongo.db = DatabaseAgent.accessor.db as any
})
return cloud
}
### 云函数相互调用
- 正常编写 import 语句,使用相对路径导入
- 使用SDK的`invoke`方法。运行时会由`FunctionModule`加载并执行
```javascript
// https://github.com/labring/laf/blob/main/runtimes/nodejs/src/support/cloud-sdk.ts
async function invokeInFunction(name: string, ctx?: FunctionContext) {
const mod = FunctionModule.get(name)
const func = mod?.default || mod?.main
if (!func) {
throw new Error(`invoke() failed to get function: ${name}`)
}
ctx = ctx ?? ({} as any)
ctx.__function_name = name
ctx.requestId = ctx.requestId ?? 'invoke'
ctx.method = ctx.method ?? 'call'
return await func(ctx)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
### 三方依赖
环境初始化后,执行命令安装 NPM 包;运行时通过`FunctionModule`加载依赖并执行
```javascript
// https://github.com/labring/laf/blob/main/runtimes/nodejs/src/init.ts
/**
* Install packages
* @param packages
* @returns
*/
export function installPackages() {
const deps = process.env.DEPENDENCIES
if (!deps) {
return
}
const flags = Config.NPM_INSTALL_FLAGS
logger.info('run command: ', `npm install ${deps} ${flags}`)
const r = execSync(`npm install ${deps} ${flags}`)
console.log(r.toString())
}
### 沙箱执行
代码需要在沙箱环境执行,这里可以使用 **Node.js vm 模块**
Laf 函数调用的分析:[开源 Serverless 框架 Laf 性能优化实践](https://forum.laf.run/d/1146)
```javascript
// https://github.com/labring/laf/blob/main/runtimes/nodejs/src/support/engine/module.ts
import * as vm from 'vm'
export class FunctionModule {
protected static cache: Map<string, any> = new Map()
private static customRequire = createRequire(
CUSTOM_DEPENDENCY_NODE_MODULES_PATH,
)
static get(functionName: string): any {
const moduleName = `@/${functionName}`
return this.require(moduleName, [])
}
static require(moduleName: string, fromModule: string[], filename = ''): any {
// 加载依赖的逻辑,处理缓存、循环引用等
// #1 平台封装的sdk
// #2 云函数
// #3 其他模块
}
/**
* Compile function module
*/
static compile(){
...
// #1 包装代码
const wrapped = this.wrap(code)
// #2 创建全局对象上下文
const sandbox = this.buildSandbox(
functionName,
fromModules,
consoleInstance,
)
// #3 通过vm创建script脚本
const script = this.createScript(wrapped, {})
// #4 在沙箱内执行脚本,返回对应模块
return script.runInNewContext(sandbox, options)
}
static deleteCache(): void {
FunctionModule.cache.clear()
}
protected static wrap(code: string): string {
return `
const require = (name) => {
__from_modules.push(__filename)
return __require(name, __from_modules, __filename)
}
${code}
module.exports;
`
}
/**
* Create vm.Script
*/
protected static createScript(){
const script = new vm.Script(code, _options)
return script
}
/**
* Build function module global sandbox
*/
protected static buildSandbox(){
const sandbox: FunctionModuleGlobalContext = {
__filename: functionName,
module: _module,
exports: _module.exports,
console: fConsole,
__require: this.require.bind(this),
Buffer: Buffer,
setImmediate: setImmediate,
clearImmediate: clearImmediate,
Float32Array: Float32Array,
setInterval: setInterval,
clearInterval: clearInterval,
setTimeout: setTimeout,
clearTimeout: clearTimeout,
process: process,
URL: URL,
fetch: globalThis.fetch,
global: null,
__from_modules: [...__from_modules],
ObjectId: ObjectId,
}
sandbox.global = sandbox
return sandbox
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
### 函数调用
每一个函数对应着发布上线后的一个接口,有唯一的调用地址,`/:name`即是函数名称
请求调用的上下文,比如请求的 body 等,即`ctx: FunctionContext`
```javascript
// https://github.com/labring/laf/blob/main/runtimes/nodejs/src/handler/router.ts
import { Router } from 'express'
export const router = Router();
/**
* Invoke cloud function through HTTP request.
* @method *
*/
// router.all('/:name', uploader.any(), handleInvokeFunction)
router.all('*', uploader.any(), (req, res) => {
let func_name = req.path
// remove the leading slash
if (func_name.startsWith('/')) {
func_name = func_name.slice(1)
}
// check length
if (func_name.length > 256) {
return res.status(500).send('function name is too long')
}
req.params['name'] = func_name
handleInvokeFunction(req, res)
})
```javascript
// https://github.com/labring/laf/blob/main/runtimes/nodejs/src/handler/invoke.ts
export async function handleInvokeFunction(req: IRequest, res: Response) {
const name = req.params?.name
const ctx: FunctionContext = {
__function_name: name,
requestId: req.requestId,
query: req.query,
files: req.files as any,
body: req.body,
headers: req.headers,
method: req.method,
auth: req['auth'],
user: req.user,
request: req,
response: res,
}
...
return await invokeFunction(ctx, useInterceptor)
}
async function invokeFunction() {
const name = ctx.__function_name
// 获取函数
let func = FunctionCache.get(name)
if (!func) {
func = FunctionCache.get(DEFAULT_FUNCTION_NAME)
if (!func) {
return ctx.response.status(404).send('Function Not Found')
}
}
try {
const executor = new FunctionExecutor(func)
const result = await executor.invoke(ctx, useInterceptor)
if (result.error) {
logger.error(result.error)
return ctx.response.status(500).send({
error: 'Internal Server Error',
requestId,
})
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
```javascript
// https://github.com/labring/laf/blob/main/runtimes/nodejs/src/support/engine/executor.ts
export class FunctionExecutor {
async invoke(){
try {
const mod = this.getModule()
const main = mod.default || mod.main
// 执行函数
data = await main(context)
return {
data,
time_usage: timeUsage,
}
} catch (error) {
return {
error,
time_usage: timeUsage,
}
}
}
## 数据库
一般使用 **NoSQL** 数据库,比如 **MongoDB**
- 比较自由,不用设置固定的数据结构
- 在水平扩展上也有比较好的优势,便于做弹性伸缩
## 文件存储
一般是**对象存储**方案,比如开源的 **MinIO**(https://github.com/minio/minio)
对象存储是以对象的形式存储和组织数据,每个对象通常包括数据、元数据和唯一的标识符,该标识符用于在存储系统中检索对象。相比传统的文件系统或块存储,对象存储的优势:
- 更灵活: 采用扁平的命名空间,不受目录结构限制;文件用元数据来描述
- 可伸缩:采用水平扩展的架构,允许通过添加更多的存储节点来增加存储容量和性能
- 高度可用:分布式存储,数据通常会被冗余存储在多个节点或多个数据中心
## 小结
从上面的简单介绍来看,核心能力的选用都离不开「弹性伸缩」,根据资源使用情况进行实时调度扩容,提高整个系统的吞吐量,这也算是云计算的一个特点之一。
# 应用案例
> 云开发 x 开放接口 = 任意组合 = 无限可能
## 对接飞书开放平台
![MoLNb1uohoxblox2iNicfNebnxh](https://static.chanx.tech/image/MoLNb1uohoxblox2iNicfNebnxh.jpg)
### 定时发送技术文章
通过接口、页面解析等方式对信息源做过滤和聚合,调用飞书消息相关的接口,定时将数据发送到飞书上
![RLuhbStRso3fOdxSjurcEXkfnbd](https://static.chanx.tech/image/RLuhbStRso3fOdxSjurcEXkfnbd.jpg)
![MPjcbPAWzoR8pnxYsjrc06GinNI](https://static.chanx.tech/image/MPjcbPAWzoR8pnxYsjrc06GinNI.jpg)
### 云文档周报管理
- 基于周报模版文档每周定时创建新的周报文档,并给团队成员授权文档编辑权限;
- 自动将新周报文档归档到指定知识库指定节点;
- 通过群机器人发送群消息,通知所有人更新周报内容
![KKL8bN0elo0r1JxSQnpcjeOJnvc](https://static.chanx.tech/image/KKL8bN0elo0r1JxSQnpcjeOJnvc.jpg)
![MnBLbqPaNoR81uxIrcNcczNln1d](https://static.chanx.tech/image/MnBLbqPaNoR81uxIrcNcczNln1d.jpg)
### 可视化数据后台
> [解放社畜~基于飞书API实现next.js网站内容自动生成实践 - 开发者广场](https://open.feishu.cn/community/articles/7271149634339438594)
人事维护飞书表格,基于飞书表格 API 自动化生成招聘网页内容(飞书表格 -> Markdown -> Next.js)
![ATPlbV1RBoLDRQxUbwzc3b54nce](https://static.chanx.tech/image/ATPlbV1RBoLDRQxUbwzc3b54nce.jpg)
## 对接 OpenAI 实现 AI 应用
> Openai 提供的Node SDK:https://github.com/openai/openai-node
### Chat GPT 应用
Laf函数示例:https://laf.dev/market/templates/64c8b75c644db038c51d174e
实践案例:[用 Laf 云开发搭建一个 ChatGPT Web 演示网页](https://icloudnative.io/posts/build-chatgpt-web-using-laf/)
![Tzc8b2ioHozaEgxpdyec1nZSnAb](https://static.chanx.tech/image/Tzc8b2ioHozaEgxpdyec1nZSnAb.jpg)
### ChatMind
官网地址:https://chatmind.tech/
通过与 AI 对话,快速创建和完善思维导图,该产品后续被 Xmind 收购。(据说是大学生基于 Laf 平台编写的项目
![IWINb672moBvyqxG702cdCdtnPc](https://static.chanx.tech/image/IWINb672moBvyqxG702cdCdtnPc.jpg)
## 对接 Gitlab 实现自动化工作流
结合 Gitlab 的 Open API 或者 Webhook 实现自动化工作流
- Gitlab API:https://docs.gitlab.com/ee/api/api_resources.html
- Webhook:https://docs.gitlab.com/ee/user/project/integrations/webhooks.html
发挥想象力:
- 主分支合入/上线群通知提醒
- 自动切出版本分支/打tag
- 自动生成changelog文档
- GPT + Merge Diff = 自动代码审查