# 滴答 FaaS 研发平台小结

## 滴答是什么

去年转岗来到 AE 技术部以后，和团队同事们一起做一个基于 FaaS 的一体化研发平台。首先简单介绍一下开发使用视角的滴答是什么。

### 如何使用

作为开发者，你接到一个需求，做一个网站，比如我们的 F1 项目，重新做一套 AE 购物流程。对于前端来说，我们核心关心三个元素，HTML、接口数据、静态资源

在滴答平台上，一个应用，可以把上面三个事情都搞定了。

* 页面渲染 -- dida-render 渲染源站
* 接口数据 -- FaaS 运行时
* 静态资源(css + js) -- 构建、打包

 ![](https://iling.fun/api/files.get?sig=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ1cGxvYWRzL2MyMzFmODM0LTY3ZGQtNGJjZi1iM2RmLWYxMTQ2NGUxM2Y0MS9kMjEzNjZiZC0xMDRmLTQ1MmYtYmE0Zi0zMTdmMzRhYjIxOGUvaW1hZ2UiLCJ0eXBlIjoiYXR0YWNobWVudCIsImlhdCI6MTc5MDQzODQwMCwiZXhwIjoxNzkwNTI0ODAwfQ.xgh7VSDdD0LG35cRSmwLAOjDu7ufX1I039Zn8UHrLrA " =600x226")

### 解决方案

滴答给的解决方案，大致架构模式如下图所示：

 ![](https://iling.fun/api/files.get?sig=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ1cGxvYWRzL2MyMzFmODM0LTY3ZGQtNGJjZi1iM2RmLWYxMTQ2NGUxM2Y0MS81ZDM0MGVjMC1jOGFmLTQwOTUtYWNmYi1jZTExZGEyMGVlYTUvaW1hZ2UiLCJ0eXBlIjoiYXR0YWNobWVudCIsImlhdCI6MTc5MDQzODQwMCwiZXhwIjoxNzkwNTI0ODAwfQ.c91HNZu735fZgXFSLyVKB3_m3rKG49IsujlUTK7punQ)

整个平台分为三大块功能

* 面向研发本地场景提供配套工具，这一块同时包括前端运行时 sdk
* 滴答平台，页面配置和发布流程
* FaaS 运行时，提供一个公共 sdk ，包装滴答通用逻辑

从开发者视角，我们希望能够做到**模块级别交付、开发**。技术实现上，我们把页面拆分成一个个一体化模块，开发者开发模块业务逻辑，模块组装由滴答来完成。通过这种组织模式，开发者可以分散在不同的团队，各自开发自己的模块，最终在滴答平台上整合成完整的页面功能。

下面主要讲讲我们是如何实现的，我只讲一下 FaaS 运行时体系的演化过程。

## FaaS 运行时

FaaS 运行时链路是直接面向用户的，老的模式全是 Java 应用，我们需要重头开始建设 node 体系。

### 第一步

首先，我们选择把一个 Java 应用 ae-msite 一个接口迁移到新的 FaaS 模式上。我们先分析了现有 Java 接口的大致逻辑分层，大致分为两大块

* 登陆 session 解析、通用限流、爬虫处理逻辑
* 业务数据查询逻辑

下面是 ae-msite 请求处理流程图  ![](https://iling.fun/api/files.get?sig=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ1cGxvYWRzL2MyMzFmODM0LTY3ZGQtNGJjZi1iM2RmLWYxMTQ2NGUxM2Y0MS8zNTViMzcwOS1hNzA3LTQ3MDktOTk3Yi1lYzJjNmI5YjY1NWYvaW1hZ2UiLCJ0eXBlIjoiYXR0YWNobWVudCIsImlhdCI6MTc5MDQzODQwMCwiZXhwIjoxNzkwNTI0ODAwfQ.qw1eBOiPOkCftTawEGpjNTLlKM0NIO78Q1jzwVc0oFM) 第一层实际上是阿里一些公共的中间件能力，同时在 AE 国际化业务场景下，我们会有一些业务定制的公共处理逻辑，比如爬虫处理，国际化站点跳转逻辑。这些逻辑如果要彻底迁移到 Node ，开发成本还是比较大的，所以一期包括未来很长一段时间，这一部分逻辑，我们都是直接由一个 Java 网关应用来承担。

第一期，我们直接在 ae-msite 这个业务系统上新增一个接口，实现一个临时的网关转发请求逻辑。第二层就是业务逻辑，直接用 node 重写即可，最终我们选择的运行时方案是这样的  ![](https://iling.fun/api/files.get?sig=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ1cGxvYWRzL2MyMzFmODM0LTY3ZGQtNGJjZi1iM2RmLWYxMTQ2NGUxM2Y0MS8yZTU0ODg4ZC00ZTE3LTQ0YWYtYTMxNC1hY2JmNDFlZWMyYzMvaW1hZ2UiLCJ0eXBlIjoiYXR0YWNobWVudCIsImlhdCI6MTc5MDQzODQwMCwiZXhwIjoxNzkwNTI0ODAwfQ.o2h4Y3bX-I8YJ1_nU-xyOIQLj-bd8S6Lzn3DFU3NX6w " =440x124")

### 上线

上线以后，我们首先做了一个专门的切流大盘，主要监控新老版本流量、成功率、耗时等数据。  ![image.png](https://iling.fun/api/files.get?sig=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ1cGxvYWRzL2MyMzFmODM0LTY3ZGQtNGJjZi1iM2RmLWYxMTQ2NGUxM2Y0MS85NWY4ODQ1ZC04YTk4LTQ5NjMtYjJjZC0xODQxYTk2ZTBjMzQvaW1hZ2UucG5nIiwidHlwZSI6ImF0dGFjaG1lbnQiLCJpYXQiOjE3OTA0Mzg0MDAsImV4cCI6MTc5MDUyNDgwMH0.-obeaV1PF_zWJLjCEcCwSjmuPmBjx8Oargdc747EsxY) 功能完成以后，最重要的是就是灰度切流控制逐步上线过程，切流过程要精细化，可以随时切回。一期灰度控制逻辑是这样实现的：  ![](https://iling.fun/api/files.get?sig=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ1cGxvYWRzL2MyMzFmODM0LTY3ZGQtNGJjZi1iM2RmLWYxMTQ2NGUxM2Y0MS9lYmViYmU5Yi0zZDA3LTQ3ZGMtYTY3NS1jOTM0ZTQ2NjZlM2EvaW1hZ2UiLCJ0eXBlIjoiYXR0YWNobWVudCIsImlhdCI6MTc5MDQzODQwMCwiZXhwIjoxNzkwNTI0ODAwfQ.QOd3hv8Z1R1Ww_NCenwU-etjLVJ6jxTNk79xAY8AzlA " =440x138") 分流逻辑在 ae-msite 实现，当 cdn 回流的时候，ae-msite 模板渲染代码，首先读取分流配置，如果命中了灰度，渲染新的模板 item2.vm 。新的模板代码会请求新的 `/fetch/function`  接口，新版本接口再读取分流配置，如果命中灰度，请求到 Node 应用，返回新的数据结构。

这里比较关键的是，**前端需要兼容新老数据的渲染，新的模板如果返回老的数据，需要同样能够正常渲染**。

模板是回源到 cdn 的，所以就算 diamond 更新了，模板还是不能马上切回到老版本。另外，如果新版本接口挂了，网关可以直接返回老版本数据，这样可以座一层异常自动兜底。这样的兜底逻辑在技术升级过程中是非常重要，最初我们没有做这个功能，导致线上出现过一个严重的故障。

最后，终于开发完成上线了，线上我们就正常慢慢切流，慢慢放流即可，我们单独搞了一个[文档](https://yuque.antfin.com/tick/agwlw3/tx8ogf)记录整个过程。

### 渲染 dida-render

后来 F1 项目启动，我们需要重新搭建一套类似于 AE 的网站，正好乘此机会把我们新的架构方案用上。我们的顺便搭建了一个渲染源站 dida-render 。新的一期，我们终于完整的实现了我们的运行时框架  ![](https://iling.fun/api/files.get?sig=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ1cGxvYWRzL2MyMzFmODM0LTY3ZGQtNGJjZi1iM2RmLWYxMTQ2NGUxM2Y0MS8zMDg3ZTU1OS01NzIxLTQyMzYtODdhMC02Yjc1NWQxNDdmZDkvaW1hZ2UiLCJ0eXBlIjoiYXR0YWNobWVudCIsImlhdCI6MTc5MDQzODQwMCwiZXhwIjoxNzkwNTI0ODAwfQ.LDSVo9NBnoFHE59OhARV9M3IZhEvpFIPNbsAt9UCIt8 " =400x157") 这一期关键在于 fn-gateway 的建设，有了标准化的网关，我们的 FaaS 应用可以任意接入其他应用了。第一期实际上做的是把单个应用切过来，这个模式是不可复制的。同时我们完成了 dida-render ，实现了一个中心化的渲染源站。

整体架构是这样的，前置网关是 Java 应用，主要做转发，核心渲染逻辑由 FaaS 应用 dida-render 控制  ![](https://iling.fun/api/files.get?sig=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ1cGxvYWRzL2MyMzFmODM0LTY3ZGQtNGJjZi1iM2RmLWYxMTQ2NGUxM2Y0MS8wMWYzMjkzNS1iY2IwLTQ5ZmItYWMyNS1lMDBiM2FmYzlkNWQvaW1hZ2UiLCJ0eXBlIjoiYXR0YWNobWVudCIsImlhdCI6MTc5MDQzODQwMCwiZXhwIjoxNzkwNTI0ODAwfQ.d13QF-R0eOH3PZnWF8dPz0lfmZ7xxCjhKYRFsJFa8wc " =560x296") 具体源站 dida-render 本身，渲染过程分为三个大的步骤：路由、模板加载、模板解析。当然非常重要的这个过程中的一些异常情况处理  ![](https://iling.fun/api/files.get?sig=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ1cGxvYWRzL2MyMzFmODM0LTY3ZGQtNGJjZi1iM2RmLWYxMTQ2NGUxM2Y0MS9mOTE3MDc3Ny0yZTlhLTRhNTYtYjQwNi0xODVlNGNkMTY2M2IvaW1hZ2UiLCJ0eXBlIjoiYXR0YWNobWVudCIsImlhdCI6MTc5MDQzODQwMCwiZXhwIjoxNzkwNTI0ODAwfQ.zcq26hMF30LzaGJKAQE1Bp3OLnzQWXwEIV4rYHm-LQk " =440x292")

### 爬虫逻辑支持

F1 项目完成以后，我们开始马不停蹄的继续第一个 ae-msite 迁移项目。首先把应用新的网关迁移上，并且把模板同时迁移到新的渲染源站上。但这一些都只是一部分流量，在我们的国际业务中，爬虫 SEO 场景是同样重要的场景。

但是爬虫和用户请求有一些不一样，如下图所示，真实用户请求 HTML 和数据是分开的，我们可以通过浏览器 service work 做一些性能优化，做到页面 html 渲染和请求同时请求。

但爬虫无法做类似的优化，目前爬虫请求数据和模板是一并下发的。  ![](https://iling.fun/api/files.get?sig=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ1cGxvYWRzL2MyMzFmODM0LTY3ZGQtNGJjZi1iM2RmLWYxMTQ2NGUxM2Y0MS80ZGE5NjFjMS00Nzk5LTQ4N2EtYmRiZC1hNWZkOTNmOWMwODQvaW1hZ2UiLCJ0eXBlIjoiYXR0YWNobWVudCIsImlhdCI6MTc5MDQzODQwMCwiZXhwIjoxNzkwNTI0ODAwfQ.32Koumw8FsAQLNeFgj9vvdIzv-IoYWa4e-C1SbNCCpA)

ae-msite 详情逻辑要切过来，爬虫肯定要支持，我们研究了几个不同的方案

* 边缘计算，就是在 cdn 节点直接发起模板和数据请求，然后 cdn 做一些数据合并的逻辑
* FaaS 应用做 SSR 渲染，直接返回页面结构 + 数据（或者完整的  HTML）

第一个方案依赖 cdn 节点的计算能力，同时有一些限流缓存的逻辑不是很方便处理，这个方案被我们放弃了。

然后我们开始往 SSR 方向做了，做 SSR 肯定只能用 react 渲染了，毕竟我们的前端都是 react，第一步先在渲染层支持 react 。

#### react 渲染

渲染接入 react ，整体改造成本并不大，主要是考虑到安全性问题。react 本身就是 js ，这会导致开发者可以提交任意代码到源站，源站本身的稳定性无法保障了，任何一个站点都可能把整个应用拖垮。通常一个模板代码是这样的

```typescript
import { DocumentContext, Main, DidaConfig } from '@ali/dida-render/jsx';
export default function Page() {
  const { getAssetsURL } = useContext(DocumentContext);
  return (
    <html>
      <link href={getAssetsURL('index.css')} />
      <DidaConfig />
      <Main />
      <script src={getAssetsURL('index.js')} crossOrigin="anonymous" />
    </html>
  );
}
```

耗时通过 vm 来控制，防止开发者写死循环代码，同时 vm 还可以控制运行上下文，只有一个 require 可用，并且 require 是受限制的 `myRequire` ，这样的逻辑首选在滴答平台上运行，可以保证用户问题在开发阶段暴露

```typescript
function getDoc(input: string) {
  const context = vm.createContext({ exports: {}, require: myRequire });
  const DocumentElement = vm.runInContext(ts.transpileModule(input), context, {
    // 最多运行 50ms
    timeout: 50,
  }) as React.FunctionComponent;
  return DocumentElement;
}
```

当然还可能有运行时死循环，这种情况必须把代码真正执行一次才能暴露了，下面是简化代码

```typescript
function testRunner(input: string) {
  ReactServer.renderToStaticMarkup(
    <DocumentContext.Provider value={context}>
        {getDoc(input)}
    </DocumentContext.Provider>
  )
}

function runIt(input: string) {
  const code = `(\${testRunner.toString()})(input)`;
  const context = vm.createContext({ React, DocumentElement, ctx, input });
  try {
    vm.runInContext(code, context, { timeout: 50 })
  } catch (e) {
    const err = new Error(`jsx 模板渲染异常，请检查模板语法: \${e.message}`);
    throw err;
  }
}
```

#### 去中心化渲染

react 渲染安全问题彻底解决，就是去中心化渲染方案，方案运行流程图如下，HTML 请求，直接从网关转发业务应用，应用执行渲染和数据查询逻辑，最终返回完整 HTML 。  ![](https://iling.fun/api/files.get?sig=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ1cGxvYWRzL2MyMzFmODM0LTY3ZGQtNGJjZi1iM2RmLWYxMTQ2NGUxM2Y0MS8wMzM4ZDE1Yi00NDVjLTRiNjctOGFjZS0yMzViM2EwNmI1ZjUvaW1hZ2UiLCJ0eXBlIjoiYXR0YWNobWVudCIsImlhdCI6MTc5MDQzODQwMCwiZXhwIjoxNzkwNTI0ODAwfQ.2fTJ_-Ka7zxkM7KWVBU_I00iItrwpKssENyhm533-9A " =400x184") dida-render 作为一个 npm 包，集成在已有的 dida-sdk 中。页面 HTML 请求直接转发到业务应用，同时在业务应用中完成模板渲染和数据查询。

## 问题与未来

滴答虽然包装很多通用逻辑，但是对于开发者来说，业务逻辑才是最核心，最复杂的部分。

这些复杂的业务状态，目前我们通过页面方案配置来支持不同状态，配置不同的模块。但是这种拆分也只能是比较粗的粒度，一个大的业务状态下，还是存在非常多流程分支处理。而这些，没有更好的办法，只能开发者编码来处理。

滴答从无到有做到现在，将近一年时间，应该是刚刚开始起步。但做得越久，我们会发现最难做的还是平台本身。平台功能完成并不难，但要做得好用就非常难了。存在大量的细节需要打磨，包括发布流程控制、迭代切换等等。做产品过程中，选择做什么，不做什么是很难的，未来还是需要慢慢打磨平台本身。

---

**Documents**

- [基于实践谈谈软件架构设计](https://iling.fun/s/blog/doc/5z65lqo5a6e6le16lci6lci6l2v5lu25p625p6e6k66k6h-tsGrQnNSNu)
- [rspack 使用体验过程](https://iling.fun/s/blog/doc/rspack-u8S4Mtjkfz)
- [E2E test with Playwright](https://iling.fun/s/blog/doc/e2e-test-with-playwright-JLFKAvWQ0U)
- [Vim everywhere](https://iling.fun/s/blog/doc/vim-everywhere-v6WZyLzGA1)
- [滴答 FaaS 研发平台小结](https://iling.fun/s/blog/doc/faas-XQxhEO3e5r)
- [Cod 服务编排之路](https://iling.fun/s/blog/doc/cod-0CFsOfiX20)
- [Cod 起源和用武之地](https://iling.fun/s/blog/doc/cod-1aAA2rVJZY)
- [2019 年度总结](https://iling.fun/s/blog/doc/2019-RhbTD2SiCn)
- [凤蝶区块历史及其问题排查记录](https://iling.fun/s/blog/doc/5yek6j225yy65z2x5y6g5yy5yk5yw26zeu6aky5o6s5pl6k6w5b2v-WoaaJ9QHpq)
- [如何用单元测试](https://iling.fun/s/blog/doc/5aac5l2v55so5y2v5ywd5rwl6kv-3zwG1cJvTn)
- [小钱袋 Node 应用开发经验总结](https://iling.fun/s/blog/doc/node-QpFfYCuoE0)
- [简单 JS 算数表达式求值算法的实现探索](https://iling.fun/s/blog/doc/js-FXcWkpW2vw)
- [凤蝶可视化编辑实现原理](https://iling.fun/s/blog/doc/5yek6j225yv6keg5yyw57yw6l6r5a6e546w5y6f55cg-ZsBj0CS1ev)
- [凤蝶服务端渲染经验总结二](https://iling.fun/s/blog/doc/5yek6j225pyn5yqh56uv5riy5pt57up6aqm5oc757ut5lqm-a4Kd59d7Td)
- [凤蝶服务端渲染经验总结](https://iling.fun/s/blog/doc/5yek6j225pyn5yqh56uv5riy5pt57up6aqm5oc757ut-GBhTESymbn)
- [Node.js 中 sourcemap 使用问题总结](https://iling.fun/s/blog/doc/nodejs-sourcemap-duex3wDhWt)
- [如何实现一个 mvvm 组件](https://iling.fun/s/blog/doc/mvvm-8TXAVSXe77)
- [webpack 热加载原理探索](https://iling.fun/s/blog/doc/webpack-7Wjn9z1fUB)
- [再谈 vim](https://iling.fun/s/blog/doc/vim-UBf5u57ko7)
- [How to realize velocity template interpreters](https://iling.fun/s/blog/doc/how-to-realize-velocity-template-interpreters-4s1KEznjYR)
- [joycss](https://iling.fun/s/blog/doc/joycss-bGvBvDGF7k)