2024 探索云开发及其应用

Chanx 4555 字 16 分钟阅读

原来这就是云开发! 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)
}

继续进行应用开发,可能遇到一些非结构化的数据或者说大文件的存储,数据库不能满足诉求了

这时候需要「文件存储」来解决这个问题,平台也把存储能力集成到 SDK 里

// 官方文档示例
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 laf next 重构计划说明 · labring/laf · Discussion #352 laf product key & roadmap · labring/laf · Discussion #354

从个人使用和理解的角度出发,对云开发平台使用到的核心能力进行简单梳理。

怎么从零开始跑通一个最简单云开发平台流程呢?

首先,核心能力分为云函数、数据库、文件存储三个方面

云函数

云函数,首先是一个函数,写在函数内的逻辑会被执行,实际上就是一段 ESM 代码

编写的函数需要默认导出,平台后续会导入并调用

import cloud from '@lafjs/cloud'
 
export default async function (ctx: FunctionContext) {
  console.log('Hello World')
  return { data: 'hi, laf' }
}

平台能力注入

将各种实例封装成 SDK 引入到每一个云函数文件里,实现开箱即用,即@lafjs/cloud

// 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加载并执行

// 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)
}

三方依赖

环境初始化后,执行命令安装 NPM 包;运行时通过FunctionModule加载依赖并执行

// 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://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
}

函数调用

每一个函数对应着发布上线后的一个接口,有唯一的调用地址,/:name即是函数名称

请求调用的上下文,比如请求的 body 等,即ctx: FunctionContext

// 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)
})
// 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,
      })
    }
}
 
 
// 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

  • 比较自由,不用设置固定的数据结构

  • 在水平扩展上也有比较好的优势,便于做弹性伸缩

文件存储

一般是对象存储方案,比如开源的 MinIOhttps://github.com/minio/minio)

对象存储是以对象的形式存储和组织数据,每个对象通常包括数据、元数据和唯一的标识符,该标识符用于在存储系统中检索对象。相比传统的文件系统或块存储,对象存储的优势:

  • 更灵活: 采用扁平的命名空间,不受目录结构限制;文件用元数据来描述

  • 可伸缩:采用水平扩展的架构,允许通过添加更多的存储节点来增加存储容量和性能

  • 高度可用:分布式存储,数据通常会被冗余存储在多个节点或多个数据中心

小结

从上面的简单介绍来看,核心能力的选用都离不开「弹性伸缩」,根据资源使用情况进行实时调度扩容,提高整个系统的吞吐量,这也算是云计算的一个特点之一。

应用案例

云开发 x 开放接口 = 任意组合 = 无限可能

对接飞书开放平台

MoLNb1uohoxblox2iNicfNebnxh

定时发送技术文章

通过接口、页面解析等方式对信息源做过滤和聚合,调用飞书消息相关的接口,定时将数据发送到飞书上

RLuhbStRso3fOdxSjurcEXkfnbd

MPjcbPAWzoR8pnxYsjrc06GinNI

云文档周报管理

  • 基于周报模版文档每周定时创建新的周报文档,并给团队成员授权文档编辑权限;

  • 自动将新周报文档归档到指定知识库指定节点;

  • 通过群机器人发送群消息,通知所有人更新周报内容

KKL8bN0elo0r1JxSQnpcjeOJnvc

MnBLbqPaNoR81uxIrcNcczNln1d

可视化数据后台

解放社畜~基于飞书API实现next.js网站内容自动生成实践 - 开发者广场

人事维护飞书表格,基于飞书表格 API 自动化生成招聘网页内容(飞书表格 -> Markdown -> Next.js)

ATPlbV1RBoLDRQxUbwzc3b54nce

对接 OpenAI 实现 AI 应用

Openai 提供的Node SDK:https://github.com/openai/openai-node

Chat GPT 应用

Laf函数示例:https://laf.dev/market/templates/64c8b75c644db038c51d174e

实践案例:用 Laf 云开发搭建一个 ChatGPT Web 演示网页

Tzc8b2ioHozaEgxpdyec1nZSnAb

ChatMind

官网地址:https://chatmind.tech/

通过与 AI 对话,快速创建和完善思维导图,该产品后续被 Xmind 收购。(据说是大学生基于 Laf 平台编写的项目

IWINb672moBvyqxG702cdCdtnPc

对接 Gitlab 实现自动化工作流

结合 Gitlab 的 Open API 或者 Webhook 实现自动化工作流

发挥想象力:

  • 主分支合入/上线群通知提醒

  • 自动切出版本分支/打tag

  • 自动生成changelog文档

  • GPT + Merge Diff = 自动代码审查

按下 K 进行搜索