# Cod 服务编排之路

## 前言

> 本文和一篇文章有一些类似，本文写于 cod 平台诞生后半年，cod 从诞生，达到了稳定程度，理念上有了一些更加完备的设计

Cod 是一个动态 tr 解决方案，简单说就是动态 tr 接口转发，在 tr 动态能力之上，我们加上了逻辑运算、依赖的支持，走向了服务编排这条路。

公司内部已经有很多团队在做服务编排了，大部分都从可视化编排入手，Cod 特色在于，我们做**代码编排**。我们认为，**我们的用户是专业的前端开发，代码是更合理的接口形式**。

下面慢慢介绍这个过程是如何一步步发展而来的。

## 问题

自从有了 BFF ，后端就开开心心的安心写服务，Controller 逻辑都丢给前端了。  ![image.png](https://iling.fun/api/files.get?sig=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ1cGxvYWRzL2MyMzFmODM0LTY3ZGQtNGJjZi1iM2RmLWYxMTQ2NGUxM2Y0MS82NDA4ODA0My1mYTc4LTRlMzUtODlhNC0zMzlkM2MyZGU1YzAvaW1hZ2UucG5nIiwidHlwZSI6ImF0dGFjaG1lbnQiLCJpYXQiOjE3OTA0Mzc1MDAsImV4cCI6MTc5MDUyMzkwMH0.n2JcPd_CiiD8DyeqA3tqvNeq1eH0lyjCqtCbu48xt7E)

这导致我们需要维护发布一个服务端应用。在业务中，有**大量 BFF 接口做的事情模板化的**，入参读取 -> 调用 tr -> 返回数据。

为了解决这个问题，团队砂瀑做了 common.invoke ，通过一个接口，转发到 proxy 的方法。调用方式大致如下

```javascript
await getRPC('common.invoke', {
  method: 'healthUserFriendListFacade.queryRemindSwitch',
  params: { bizType: 'XHB_BIZTYPE' },
});
```

这样简化了很多，以后每次 BFF 接口，只需要新增 proxy 配置，然后发布一下就好了。

但还是会有些麻烦，发布一次本身成本很高，我们就开始思考，**能不能不需要修改代码、发布应用，直接动态调用 tr 接口呢**？答案就是我们的第一版 Cod 。

## 动态 tr 实现

有一个和我们类似的产品陆游，他们也同样实现了 hsf 接口的动态调用。他们主打的是**代码热更新平台**，这体现了我们两者实现的区别，Cod 不做代码更新，我们发布的都是配置，没有动态执行代码的能力。

我们决定坚持一个原则，**代码发布必须通过应用发布流程，可以动态化的只有配置**，这也是符合标准应用开发的基本原则。动态下发代码，安全性和可维护性保障是更加困难的，我们的目标场景是替代 BFF 开发，自然需要面对最专业的前端，最高频的接口场景，**稳定性，安全性和性能上，是必须等同于标准应用开发模式的**。

### 原理

整体上，实现并不复杂，先看看实现流图  ![image.png](https://iling.fun/api/files.get?sig=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ1cGxvYWRzL2MyMzFmODM0LTY3ZGQtNGJjZi1iM2RmLWYxMTQ2NGUxM2Y0MS9iNjU3ZTkyOS03MWU4LTQ1YWUtOTMzNS0xMWJmMzNmOTRhYWIvaW1hZ2UucG5nIiwidHlwZSI6ImF0dGFjaG1lbnQiLCJpYXQiOjE3OTA0Mzc1MDAsImV4cCI6MTc5MDUyMzkwMH0.wOPWhQ5kc1q-VT0hfBcwp0DfnQ_rNhXLb6uMDYeh2Kc) 目前，主要分为两个大的部分

* cod 平台，cod.alibaba-inc.com 。主要管理应用，jar 包，最终完成接口配置下发。通过这个平台，调用 jar2proxy 生成 jar 包类型元数据，最后由开发者负责发布接口配置。数据通过 filesync 同步到 chair 应用的区块上
* 完成配置下发后，第二部分负责执行 tr 动态调用，这一部分，是一个 Chair 插件实现的

### 采坑

动态调用本身并不复杂，框架已经封装的很好了，对于使用者，只需要关注请求元数据，然后就可以调用接口了。有了 jar2proxy 生成的元数据，动态发 tr 请求就可以实现了。很快我们搞定了，和老的方式对比快了很多。

但 tr 动态化以前一直没人做，这条路是不是真的通，有没有大坑都是未知的。我们摸着 API 调用成功了，但底层是否真的支持的很好，我们是没有底的，只能先跑起来在看。后来，真遇到了大坑，我们花了几个月，差一点就被埋在坑里面了。

当时一个新应用接入 Cod ，预发测试 ok ，到线上直接挂了。报一个序列化异常的错误

```shell
Exception caught in invocation. responseStatus: 18
```

排查了好久，然后请教了一下 chair 维护团队成员，他们恰好也在搞 tr 动态化，然后他告诉我他们遇到的坑。

### 根本原因

tr 协议实现最初，是没有考虑过动态化存在的可能的，在调用的很多链路上，为了提高性能加上了很多缓存逻辑。主要包括两部分，ClassMap 复用，入参预编译缓存。

ClassMap 在 Connection 层面会缓存，同时在协议层的入参预编译，会导致 tr 元数据修改了，同时还需要更新相应的缓存。

当时这个问题看起来是无解的，云凤蝶在 OneAPI 中实现，折腾了很久，最终给出一个 hack 的方案：

> 
> 1. 把 [sofa-hessian-node](https://github.com/sofastack/sofa-hessian-node) 的 classMap 覆盖，不使用预编译的缓存
> 2. 每次调用的时候先把上一个版本的实例和对应的 connection 给 destroy 掉

第一个方案解决序列化入参结构缓存的问题，第二个解决 connection 层缓存，导致元数据无法透传问题。彻底解决，当时我们想着依靠 chair 本身的升级，从底层支持动态化。

### 解决与思考

问题明确了，我们觉得不应该使用 OneAPI 那种方式，我们需要应用于 C 端请求，缓存本身为了提高性能，OneAPI 的方案会导致整个应用 tr 调用性能下降，这是对我们和使用者来说，都是不可接受的。

后来风棋把 tr 请求整个链路研究了一遍。给出了一个 hack 的方案，**npm 包覆盖黑魔法**，直接覆盖协议实现的 npm 包，支持动态化调用。反反复复折腾，终于搞出了第一版解决方案。

**但始终如芒在刺，万一底层实现改了，我们不就挂了吗**？而且这样的代码，我们也不好意思拿出去给别人用，只能自己用用了，也许后续只能等待 Chair 底层改造了，问了下零一，说目前还在排期。

最后，我们想明白了，**这个问题对于 Cod 来说是立身之本，不解决 Cod 的基础就倒了**，但对于 OneAPI 或者 Chair 来说，只是一个特性加强，锦上添花的东西。这个问题最需要的是我们自己，如果等待官方的解决，那可能永远等不到，或者半年一年以后去了。既然如此，那么我们想办法自己来搞定吧，然后就开始尝试自己来修复这个问题。

最终，经过反复的修改和讨论，风棋完成对 [sofa-hessian-node](https://github.com/sofastack/sofa-hessian-node/pull/30) 和 tr-client 进行改造，彻底解决协议层实现上无法支持动态 tr 的问题，并且不影响性能。最终代码改动不多，但其实经过几周反复讨论，修改，最终实现了让大家都觉得可行的方案。

现在回头想想，**如果一开始就知道存在这么大的坑，我们可能就直接放弃了，或者等待 chair 团队给终极方案**。但那时候比较尴尬，线上已经用了，牛皮也吹出去了，退无可退，只能硬着头皮上了。最终我们发现，这个问题的解决并没有想象的复杂，改动范围也是可控的。更关键的是，**通过这个过程，我们最终对 tr 协议本身有了深入的认识和理解，对我们的服务本身，也有了真正底气**。

## codql

经过几个月的开发和填坑，底层终于稳定了。然后我们开始思考，如果能够解决更多问题。接口转发是一个场景，但还是有点局限。

Cod 从一开始就做了 simpleql ，一种简化了的 graphql 语法来查询。从这开始就支持了简单的服务编排能力，并且后来我们加上了处理器的设计，支持对数据进行一定的处理。稍微复杂的接口我们也能实现了，当时还尝试过一个非常复杂的逻辑编排

```shell
isSamePerson: Cod(method: IdentityService.isSamePerson) | get(model),
hasInsUser: Cod(method: InsUserQueryFacade.getInsUser) | get(model),
userNo: Cod(\$if: "isSamePerson && hasInsUser", method: InsUserFacade.addUser) | get(data)
_attachment: Empty(\$\$userNo) | assign(\$attachQuery) 
attachment: Cod(method: insUserAuthFacade.uploadCertificateAttachment, params: \$\$_attachment)
```

功能是支持了，但是这个查询语句大家表示看不懂，没有必要，直接写代码多好。

### 存在的基础：简单

最终让我决定放弃 simpleql 是源自一次集体 review ，一个项目搞定之后，我开心的和大家说，看我这次开发，所有接口都是用 cod 搞定了。小组成员纷纷表示这代码看不懂，并且发出灵魂拷问：**这代码以后怎么维护啊**？

看到自我感觉牛逼的查询语法，被大家嫌弃，我当时有些震惊、不解。再后来我仔细思考了很久，我想明白了，对于 Cod 服务编排来说，**做的足够简单是存在的基础条件**，**如果做不到比写代码简单很多，就没有存在的意义**，**直接写代码就够了**。

服务编排说起来高大上，但做到足够简单很难，**矛盾点在于逻辑本身是复杂的，没法做的非常简单，但如果不够简单，开发者何不直接写代码呢？**通过可视化方式来编排逻辑，我认为方向就错了，可视化是给运营、小白用户使用的，对于开发者来说，最简单最直接的还是代码本身。

如何做的简单，我们给的答案就是 codql 。

### 查询语法

最终我们完成一套新的 codql 查询语法，新的语法，**我们只引入了 wait 和 when 两个概念**，其他所有语法直接借用 js 语法，对于开发者来说是基本没有学习成本。下面是一个示例

```javascript
import { cod, tr, rpc } from 'codql';
const query = cod({
  gray: tr('GrayFacade.grayJudgeByBizCode', args),
  config: fengdie('insxhbbff_healthNotice'),
  iopData: rpc('common.insiop.get', iopParam),
});
const data = await fetchRPC('common.query', query);
```

目前，我们支持三个基本的查询类型

* tr: 发起一个 tr 请求，动态 tr 能力支持
* fengdie: 读取区块配置数据
* rpc：请求本地 rpc 方法，这个是自定义函数的支持，有些复杂的逻辑可以通和往常一样开发一个 rpc 接口，然后和 cod 其他查询结合使用

当然，我们还支持更加复杂的查询语句，主要包括

* wait 参数依赖
* when 逻辑依赖

说起来有些复杂，看看具体的例子

```typescript
cod({
  // 调用 tr ，创建一个 topic ，得到一个 topicId
  topicId: Tr('CommonTopicFacade.createCommonTopic', data).get('model'),
  // topicId 作为 belongId 属性，请求创建 post 请求
  post: Tr('PostFacade.createPost', {
    belongId: wait('topicId'),
    ...args1,
  },
});
```

还有前面举的复杂例子，换成 codql ，可读性就好多了。

```typescript
cod({
  isSamePerson: tr('IdentityService.isSamePerson').get('model'),
  hasInsUser: tr('InsUserQueryFacade.getInsUser').get('model'),
  userNo: when('isSamePerson && hasInsUser').tr('InsUserFacade.addUser').get(data)
  attachment: tr('insUserAuthFacade.uploadCertificateAttachment', {
  	userNo: wait('userNo'),
  }),
});
```

### 编排可视化

可视化是有价值的，能够让我们直接看到逻辑过程。后来我们还加上了编排可视化的支持，也就是自动把 codql 语法，转换成为 plantuml 图。比如，前面那个复制的例子，生成的图是这样的  ![](https://iling.fun/api/files.get?sig=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ1cGxvYWRzL2MyMzFmODM0LTY3ZGQtNGJjZi1iM2RmLWYxMTQ2NGUxM2Y0MS85N2FkYzIwYS1jYTVlLTRjMDItODYwZC0xYzQxMjY2MWY5ZjcvaW1hZ2UiLCJ0eXBlIjoiYXR0YWNobWVudCIsImlhdCI6MTc5MDQzNzUwMCwiZXhwIjoxNzkwNTIzOTAwfQ.U3d3Kw813cbwDJ5SGLXzZTTdklNvvsRgr4c80CfFjpk)对于我们来说，可视化是服务编排的结果和辅助手段，有了图，我们可以更方便的理解逻辑流程。

## 总结

经过将近一年时间，我们慢慢摸索出一套 Cod 特色的服务编排解决方案。目前正式对内部使用了 5 个月时间，一共有 8 个 BFF 应用真实使用，主要覆盖了相互宝、保险理赔、保险平台等业务中，**每天接口访问量目前在 1500w 左右**的调用量。

目前，cod 处于功能基本稳定，开始准备对外推广的阶段了。

一路走来，我们坚持着三个基本原则

* 下发数据配置
* 代码编排
* 简单是存在的基础

有了第一个原则，我们才可以在前端编排逻辑，流程上整体会很轻便。我们坚持应该给开发者代码形式的接口，而不是可视化操作，也就是我们所谓的代码编排。最终，通过 codql 和编排可视化，让整个编排过程足够简单，可理解，可维护。

---

**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)