云函数开发 - 接口聚合实践

接口聚合,将端上需要请求多个接口获取数据的方式变更为使用聚合层对端屏蔽多个接口请求和数据组装一次性向端吐出面向渲染的结构数据的过程。

为什么要聚合接口

  • 微保几乎所有的主营业务都在一个单独的小程序上,页面的渲染/逻辑的处理会线性地增加小程序包体积,而在当前的阶段,包体积已接近红线,将数据的获取和组装逻辑迁移到远端聚合层是非常有必要的
  • 微保的业务在承接形态上有小程序/H5/App(开发中),数据的聚合和组装统一到聚合层,大大减少多端间相同逻辑的开发和维护

如何处理接口聚合

业界比较常见的做法大概分为2类,通过网关实现接口聚合(又叫服务编排)和新增接口聚合层。

  • 在网关做接口聚合
    比如国内的悟空API网关,就内置有服务编排的能力
  • 新增接口聚合层

对于稳定的业务而言,冲击现有的网关显然是不太合理的,我们选择使用serverless作为聚合层的载体。主要的原因是希望能将开发者从琐碎的运维工作中解放出来,释放运维资源,专注于业务。

接口聚合的实现

整体的大概流程如下图


接口聚合核心流程.png

围绕上图,逐一介绍实现的细节和注意事项

DSL(领域特定语言)

DSL的目的是描述数据获取任务及任务间的并行/串行关系,简单理解是一份配置文件。
业界在定义DSL的时候,一些团队使用yaml或者json这类静态配置文件,作为任务描述的载体。比如美团技术团队,使用json文件描述请求任务。
静态配置文件能够cover绝大部分的应用场景,对于比较常见的数据获取场景基本够用。但对于下列场景就显得力不从心:

  • 根据被依赖的接口数据状态决策依赖方是否执行
  • 接口级别的降级处理。接口请求失败使用本地默认数据,请求成功使用线上数据
  • 接口数据format
    基于此,使用js脚本作为DSL的载体,示例如下
/* eslint-disable no-console */
import { IContext, PlainObject, RpcFetcherResult } from '@wesure-scf/types';
import { GetProposalListRequest } from './_types/carCoreService';

type ProposalListParams = Pick<GetProposalListRequest, 'userId' | 'statusFilter' | 'needRiskKind'>;

export default (body: PlainObject, context: IContext) => {
    return {
        task1: {
            // xxx
        },
        task2: {
            deps: ['task1'],
            fetcher: (): RpcFetcherResult<ProposalListParams> => {
                return {
                    endpoint: 'rpc/service-xx/GetProposalList',
                    params: {
                        x: 1,
                        y: 2,
                    },
                };
            },
          defaultValue: {},
          cache: {
                ttl: 60 * 1000,
                key: 'cacheKey',
          },
        },
    };
};

因为云函数开发基本ts化,相应的脚本也变更为ts类型。使用ts作为描述文件有下列优势

  • 无缝集成到代码。本身是ts文件,直接引入即可
  • 使用类json的写法(js中的对象)描述任务相关特性
  • 天然支持脚本编写
  • 天然支持类型校验(ts的特性)
Parser

解析器有2个重要的任务

  • 配置校验
  • 生成任务拓扑图
    对复杂的配置进行简化,形式如下
const notDapConfig = {
    a: {},
    b: {
        deps: ['a', 'd'],
    },
    c: {
        deps: ['b'],
    },
    d: {
        deps: ['c'],
    }
};

稍作变化可以快速转化为一个逆临接表的数据结构。其实也很好理解,接口聚合的本质就是把一系列的任务根据它们之间的依赖关系创建成一个有向无环图(DAG)的过程。
同样的,如何校验配置文件就比较清晰了。配置文件的格式可以交给ts的类型系统在编写时即可校验,解析器需要校验的是一些动态的规则。校验列表如下:

  • 是否引入不存在的依赖
  • 是否形成循环依赖

校验依赖是否存在相对简单,只需要判断deps中的依赖id是否在配置文件的属性列表中。
校验循环依赖稍微麻烦点,不过基于上述的讲解,可以转换为是否为DAG的校验。而当前的配置文件,可以轻松转为逆临接表,那么使用拓扑排序的方法就能达到目的。判断的代码可以参考如下:

// 是否为有向无环图
const isDAG = (config: Config): boolean => {
    // ApiConfig天然是一个 逆临接表,是以入度为基准的,deps代表了连向当前节点的其他节点,边的度数都为1
    const map = new Map<string, { deps: string[] }>();
    // 入度为0的队列
    const queue: string[] = [];
    Object.keys(config).map((k) => {
        const deps: string[] = config[k].deps ?? [];
        // 入度为0的放到queue中,其他放到非0的map中,减少运算次数
        if (deps.length <= 0) {
            queue.push(k);
        } else {
            map.set(k, {
                deps,
            });
        }
    });
    // 入度为0的节点还在继续处理,map还不为空
    while(map.size > 0) {
        if (queue.length <= 0) {
            break;
        }
        // 删除入度为0的节点,更新其他节点的入度
        const taskKey = queue.shift();
        map.forEach((v, k) => {
            const { deps } = v;
            const index = deps.findIndex(item => item === taskKey);
            // 入度减1
            if (index >= 0) {
                deps.splice(index, 1);
            }
            // 节点更新后判断是否可以放到入度为0的队列
            if (deps.length <= 0) {
                map.delete(k);
                queue.push(k);
            }
        });
    }
    if (map.size > 0) {
        const kArr: string[] = [...map.keys()];
        const tip = kArr.join(',');
        throw new Error(`${tip}存在循环依赖`);
    }
    return true;
};
Controller

配置解析完成后,接下来需要将其创建为若干个任务单元,并根据配置的拓扑关系和接口的执行状态完成接口聚合。


控制器.png

控制器维护发布/订阅中心,遵循下列原则

  • 使用拓扑排序, 启动入度为0deps为空或空数组)的任务
  • 任务完成后,发布一条以自身id命名的消息,并将任务数据写入上下文
  • 订阅该名为该id消息的任务入度减1deps数组中移除掉该字符串)
  • 控制器判断是否全部任务都完成,否则循环处理上述过程

控制器的本质是一个小型的任务管理系统。

Fetcher

在最初的设计中,所有的任务处理并不是泛型的,默认套上接口请求的外衣。后续的应用中,发现这种设计不仅不优雅,而且不利于扩展。数据的获取广泛来看可能会有多种来源,rpc请求只是其中一种,其他包括本地json文件/yaml文件/计算所得等等。
基于这些场景和扩展地考量,将任务的处理设计为泛型的fetcher。抽象来看,就是一个函数。

export type FetcherFunction<T = PlainObject> = (deps?: T) => Promise<any>
export type RpcFetcherResult<T = PlainObject, U = PlainObject> = {
    endpoint: string,
    params: T,
    headers?: Headers,
    formatReply?: (res: PlainObject) => U,
    defaultValue?: PlainObject | (() => PlainObject),
}
export type RpcFetcherFunction<T = PlainObject, U = PlainObject> = (deps?: PlainObject) => RpcFetcherResult<T, U> 

Fetcher默认提供RpcFecher,对于常用的请求接口获取数据的场景直接使用内置能力,其他场景开发者可以自己扩展fetcher

小结

接口聚合中比较重要的技术点如下

  • DSL(包含DSL的设计)
  • DAG
  • 发布/订阅

抛砖引玉,期望看到更多关于前后端协作的方案探讨。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 162,050评论 4 370
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,538评论 1 306
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 111,673评论 0 254
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,622评论 0 218
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 53,047评论 3 295
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,974评论 1 224
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,129评论 2 317
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,893评论 0 209
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,654评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,828评论 2 254
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,297评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,619评论 3 262
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,326评论 3 243
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,176评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,975评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,118评论 2 285
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,909评论 2 278

推荐阅读更多精彩内容