背景介绍
当前,大数据的重要性对于任何大中型公司来说都不言而喻 ,飞书深诺也不例外 ,大数据在多个维度,多个环节都帮助公司更好的提升业务效果,为用户赋能,包括:媒体达标,风控违规,广告诊断,素材中台,行业基准等;在大数据的使用场景中,数据的获取是其中特别重要的一环,在很多业务流程中,都涉及到获取数据以支撑下一步的行为和决策,所以如何快速高效且准确的帮助团队获取数据是数据基础设施建设中的重点;我们最初为公司提供数据的方式主要有:数据看板,数据导出,数据同步;但这几种方式还是有一些固有的缺陷,包括:
- 数据输出形式混乱,直接同步到业务库,推送文件发队列通知,导致业务直接访问大数据的底层库
- 效率低下,每个任务都需要单独开发。无法沉淀公司的统一接口平台,复用性和透明度不够
- 定制化程度不够 所以我们一直在思考如何提供一种数据获取方式,其既能帮助用户高效的获取数据,又不失灵活性,同时不需要太多的开发资源投入
设计思路
我们可以先来回顾下一般的数据API的构建流程,其基本上都是硬编码的方式,一般一个API的交付在开发层面需要:
- 封装控制层,服务层,实体层
- 从数据研发那里获取可执行SQL
- 将执行SQL改造成mapper中可用的动态SQL
仔细分析这个过程,会发现其工作可以抽象为:
- 配置:需要编写配置文件,配置数据库连接、MyBatis 映射文件、日志记录等
- 开发:需要编写控制器层、服务层、DAO 层、实体层等代码,处理请求、处理业务逻辑、访问数据库等
- 调试:由于涉及多个层级,调试起来可能比较困难,需要花费更多的时间和精力
继续思考这些工作内容,我们发现如下特点:
- 当获取数据的以列表的形式输出时,返回的结果集结构相对统一与确定
- 数据字段,枚举,类型的定义来自数据中台或更上游(数据源头)
- 业务端对于获取数据的指标字段与聚合,分组方式灵活多变
- 库表由数仓提供,无需服务层设计与灌入
基于这些特点,有哪些工作可以标准化,自动化,哪些是一定需要人工处理?
最后我们发现:
- 出参入参结构大体相同,可以归纳共性来代替定制化的出入参设计
- 人只需要参与核心SQL逻辑,其它层工作规范标准自动化即可
所以可以看出如果想要达成上述目标就要处理两个关键点:精简结构及SQL配置化
精简结构
舍弃出参入参对象的封装,取而代之与业务端约定:
- 入参方式以map形式的key,value对传入
- 出参设计提供两种类型
一种是map:适用于汇总与统计
{
"data": {
"key1": "value1",
"key2": "value2"
}
}
一种是分页列表:适用于明细
{
"totalNum": 2,
"pageNum": 2,
"pageSize": 1,
"data": [
{
"key1": "value1",
"key2": "value2"
},
{
"key1": "value3",
"key2": "value4"
}
]
}
SQL配置化
如何将SQL逻辑设计成可配置,在MyBatis中,SQL脚本无论是写在注解中还是xml文件中,都免除不了发布或加载的动作。SQL脚本的解析要在代理mapper后执行,所以我们的思路是将SQL脚本储存在数据库的配置表中,舍弃mapper的代理动作,将MyBatis中的脚本解析功能拉出来,这样使得SQL逻辑的读取可以随时调整,配置表结构及核心字段如下:
请求路径从request.getRequestURI() 获取,和表中 api_path 字段匹配,拿到SQL脚本。MyBatis SQL脚本 结合入参形成一条可执行SQL,再利用 java.sql.PreparedStatement SQL预备声明 执行这条可执行SQL获取返回结果
带着上面这两个关键需求,我们开始寻找可行的解决方案
选型调研
在寻找解决方案的初期,我们首先把目光投向了外部已有的方案,首先是阿里数据服务。因为数据中心本身就在用Dataworks,而数据服务本身就在Dataworks全家桶中,当时并没有额外的人力资源来开发数据接口,所以数据服务充当的那段时间的应急方案。
DataWorks数据服务
DataWorks是阿里巴巴提供的一种一站式大数据开发与管理平台,它可以帮助企业高效进行数据开发、数据上线、数据管理和数据运维 ,这个平台将数据的采集、清洗、转换、分析和可视化等全过程整合在一起,使数据开发工作更加得心应手,它的优点是:
- 数据服务支持通过可视化配置的向导模式,快速将关系型数据库和NoSQL数据库的表生成API ,无需具备编码能力,即可快速配置一个API
- 为满足高阶用户的个性化查询需求,数据服务为您提供自定义SQL的脚本模式,您可以自行编写API的查询SQL ,在脚本模式下,支持多表关联、复杂查询和聚合函数等功能
缺点有:
- 缺少详细的日志输出,除了入参出参再无其他日志信息
- API接口返回字段在利用到个别数据库时字段无法做到驼峰映射
- 非开源,改造升级被动
DataWorks数据服务的产品模式实际上提供给我们很多参考,但是其中的问题排查,架构适配等问题隐患也是不容忽视的,后来我们目光转移到开源的解决方案上
DBApi
DBApi基于orange(开源动态SQL引擎,类似MyBatis的功能,解析带标签的动态SQL,生成?占位符的SQL和?对应的参数列表) 的面向数仓开发人员的低代码工具 ,其优点是:
- 开箱即用,不需要编程,单机模式不需要依赖其他软件(只需要Java运行环境)
- 支持单机模式、集群模式;支持云原生容器化部署
- 支持动态创建、修改API;动态创建、修改数据源 ,热部署全程无感
- 支持API级别的访问权限控制,支持IP白名单、黑名单控制
但是DBApi也有一些我们当前需求无法接受的缺陷,包括:
- 返回结构比较单一,只提供无界列表,在不明确总数据量级的情况下,每次全量返回数据会有不必要的流量损耗
- 没有做SQL返回中对键的处理,例如PostgreSQL返回的字段名称都是小写的,而公司规范中需要做驼峰式处理
基于此,同时考虑到实现复杂度我们可以控制,且后续也会有不少定制化需求,所以决定走自研道路
技术要点
首先,自研方案有几个需要重点考虑的问题:
- 如何借用MyBatis的能力来处理动态SQL脚本
- 如何处理结果返回
- 如何设置接口路径
- 等
脚本处理
实际上SQL脚本的标签处理本身就是MyBatis现有的,只不过强关联在了mapper文件中。我们所要做的就是把这个处理过程拉取出来,直接执行我们所关注的部分为了便于理解,这里补充一张从SQL脚本至标签处理完成的时序图:
- XMLLanguageDriver 将 SQL脚本和入参传入 XMLScriptBuilder,XMLScriptBuilder 将 SQL脚本传入SqlNode 中
- 将 SqlNode传入 DynamicSqlSource 处理脚本逻辑
- DynamicSqlSource 执行 getBoundSql 方法 ,在 getBoundSql 中SqlNode的apply方法处理DynamicContext
- 这样我们就从DynamicContext中我们可以获取我们需要的2个重要组件: 处理后的入参map和处理后的待传参SQL
获取解析后的入参 & 获取解析后的SQL
//获取解析后的入参
Map<String, Object> bindings = context.getBindings();
//获取解析后的SQL
String sql = context.getSql();
结果处理
个别数据库返回字段不分大小写,如PostgreSQL;配置化平台设计了返回字段映射功能,可以将SQL的返回集字段名称映射成期望字段名称,从而解决特定数据库无法将字段处理大小写的问题
编辑规范
SQL的编辑规范注意和MyBatis一样,SQL里的小于号不要写成<,要写成<
动态路径
动态路径写法参考
//Spring注解
@PostMapping("/api/result/**")
RestResponse<Data> getSinoTypeAPI(@RequestBody Map<String,Object> inputMap);
//或重写HttpServerlet
public abstract class HttpServlet extends GenericServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp);
}
多数据源
多数据源匹配处理,启动时加载读取Apollo在配置好数据源并通过数据源命名来注册bean,数据源通过bean名称与表中dbName匹配的方式找到对应数据源
数据校验
数据产品同学,数据开发同学或测试同学需要实际的执行SQL
配置化平台提供了获取SQL功能,将/sql拼在原接口后面即可获得当前接口执行SQL
应用情况
经过改造后的配置化API可支持:
- 扁平结构的出入参
- 常见数据库,如MySQL、PostgreSQL、ClickHouse等
- 与敏捷开发模式无缝衔接 ,做到高频率的产品发布
最终我们原来99%的读库场景都可以基于配置化API快速支持;应用以来,累计提供API给到10余个服务,上线并稳定运行API共200余条,每条API约节省成本(开发+发布) 0.3人日,新增API经用数仓接口配置化平台应用率达到接近100%;且我们提供了可视化界面供数据开发同学和服务开发同学使用
未来规划
基于配置化API在上述场景中一系列实践与应用,其主要功能基本满足当前的需求,但还是存在不少需要优化的点;同时考虑到后续业务发展方向,该服务后续的迭代方向为:
- 配置化API文档化:基于对配置化API出入参的统一处理,虽然我们可以将API的暴露规范化自动化,但也导致无法借助Swagger等插件自动生成接口文档;后续我们考虑在配置API的时候将一些接口相关信息录入并存储,以便接口文档工具可以基于这些信息自动生成文档
- 业务支持::配置化API舍弃掉了服务层逻辑,导致一些基础的业务需求都落在了SQL逻辑或业务服务上。后续计划归纳出通用前置插件,整合空值筛选、日期判定、数值型范围筛选 、基本聚合函数类型选择 等功能
- 出参结构支持:当前绝大部分数据库的字段类型已经支持了。但是还有个别字段类型的显示是有问题的,类似PostgreSQL数组字段如数组字段(int4[] ,int8[] ,float4[] ,float8[] ,boolean[] ,text[]) ,JSON等
- 接口&表关联统计:从数仓角度来说,数仓开发需要理清接口与应用表之间的关系,完善数据血缘。所以配置化平台会理清数据接口到数据表之间的流动和变化,提供表和对应API之间的双向查询功能
- 非只读操作:当前配置化接口仅应用在读领域,未来规划在业务领域中会做更多'读写操作'和'更新操作'方面的尝试
参考资料
作者
李南多 (飞书深诺技术中心,高级JAVA研发工程师)