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(分析),所以面向的不只是研发人员,还有运营人员,包括一些市场投放人员。

JWQcbntmfooUNsxpT23cb1gun7e

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/

云开发平台下线,但是其他云服务可以满足同样的能力,服务对个人和企业可用

QD2ObgxqkoxcW5xmLUBciTqOnAc

字节跳动 - 轻服务(已下线)

轻服务将从2022年5月14日起停止服务创建,2022年6月14日23点59分停止服务并删除所有资源

官网地址:https://qingfuwu.cn/

字节跳动旗下云服务平台「火山引擎」提供函数服务,但服务对象是企业,不面向个人开发者

WHEBb3JkDoohshxOdd9cc6JTnD4

LcO9biKA2onnuSxkvwUcgMYhned

EryobiPP9oDzxBxahapcMniFnVf

腾讯云 - 云开发

官网地址:https://cloud.tencent.com/product/tcb

目前应该是国内大厂还在提供云开发平台服务的,从免费到商业化,经历过多轮价格调整。

DLiybELu7oOO47xDThsc2xpNnig

X7I6b3V6YovhBWx8TVecdzFhnwb

Aircode(已下线)

官网地址:https://aircode.io/

开发文档地址:https://docs.aircode.io/

开源组织:https://github.com/AirCodeLabs

不知道什么名字的团队在孵化的项目,提供一些的基础能力,还在功能快速迭代期,刚进入商业化不久

提供三个免费项目额度,超出需要购买收费套餐

NIGSbN5aYoZ3EOxoduacSjmcnCc

PcigbynyToMAORxjFxLc8t5Xngo

Laf

官网地址: laf.run (国内版) laf.dev (海外版)

开发文档地址:https://doc.laf.run/zh/

开源组织:https://github.com/labring/laf

国内企业环界云计算孵化的项目,目前进入商业化阶段,新用户赠送一定余额体验

应该是国内除了大厂外做的最好的(或者说云开发不是腾讯云就是Laf?哈哈)

亮点:

  • 代码开源,拥抱社区,函数市场

  • 支持私有化部署

  • 在线协作

  • 支持 Websocket 链接

AwXfbU1eXo3ZxKxVBvYcktuInxf

H5YEbStSsoQSVxxdDuVc0PqIncc

小结

可以看到上述云服务厂商或平台提供了主要的云能力:函数计算、数据库、存储、CDN

  • 海外厂商基本都只提供单独的云服务能力,没有将服务聚合成云开发平台进行销售

  • 国内大部分厂商都对云开发平台进行下线处理,将用户引流到自家的各种云服务能力

  • 腾讯云考虑继续将服务聚合成云开发平台,笔者猜测原因是:

  • 新的云开发平台收获好评,拥抱开源社区,发展势头强劲

以前比较火的还有「新浪 SAE」 ,不知道什么原因在国内市场渐渐消失。

IRBXbrit7o5pUFxhJ6EcZvqVnVd

使用体验

实际上云开发平台功能的使用上基本一致,这里主要拿 **laf 平台 **举例。

30s 完成 helloword 接口的创建、开发、发布,如下图示例。

基于上述路径,我们可以编写「云函数」,实现自己的逻辑,快速完成上线

OMoSb7qPDoCk3UxpCq1c5WVFnXd

需要注意的是,每次函数的执行都是在新的环境里,也就是两次函数的状态本身是不共享的,即无状态

但是一些数据需要进行持久存储,这时候需要使用到 「数据库」** **能力

平台本身封装数据库的能力并注入到函数的上下文当中,即下面代码中提供的@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 = 自动代码审查

2024 探索云开发及其应用
https://chanx.tech/2024-serverless/
作者
ischanx
发布于
2024年1月11日
更新于
2024年7月21日
许可协议