1.webp

前语

在上文xie.infoq.cn/article/62a… 咱们提到了 通过数据库日志 获取新鲜的数据, 在对数据的认识里, TAPDATA 引擎的规划和一些其他的流结构不太相同, 他的方针笼统里没有批数据和流数据的区别, 数据只有一种, 被命名为 Record, 数据来历只有一种, 命名为 DataSource, 而数据流阶段也只有一种, 被命名为 DataStage

在笼统上数据去除了批与流的差异, 在悉数的核算流程里也不会有差异, 根据这个理念规划的结构才是真正批流一体的结构

所以问题来了, 应该规划一个什么样的数据结构, 来表达批流一体的数据呢?

规划一个结构

首先要处理的是批数据与流数据的一致性表达, 咱们把已经存在的批数据认为是新写入的流数据, 就完成了概念上的一致

而流数据包括了包括了 写入, 更新, 与 删去, 是批数据的超集

接下来从 0 开端, 一步步来看一下这份数据结构里应该包括哪些内容

先给出一份示例结构, 然后对照看下面的解说会明晰许多

{   "op": "u", // 一个更新操作   "ts": 1465491461815, // 操作时刻   "offset": "123456", // 操作的位移   "before": {          // 更新之前的值     "_id": 12345,     "uid": 12345,     "name": "tapdata",   },   "after": {          // 更新之后的值     "_id": 12345,     "uid": 12345,     "name": "tap",     "nick": "dfs",        },   "patch": {         // 更新操作的内容     "$set": {       "name": "tap",       "nick": "dfs",     }   },   "key": {         // 记载仅有标识条件, 如果没有, 可认为 {}     "_id": 12345,   },   "source": {      // 数据源的特点     "connector": "mongodb",     "name": "fulfillment",     "snapshot": true,     "db": "system",     "table": "user",   } }

仿制代码

新鲜的值

最清楚明了需求包括的内容, 关于写入, 指的是写入的值, 关于更新, 指的是更新之后的值, 关于删去, 用 {} 表达

这儿的值的 key 用 after 来表明

陈腐的值

指的是改变之前的数据, 关于数据库的主从同步来说, 出于数据一致性的意图, 陈腐的值并不重要, 可是在进行流核算的时分, 因为需求进行增量实时核算, 改变前的值变得不可或缺

举例说明一下, 考虑咱们关于一份数据的某个字段进行一个 求和 操作, 根据流核算的规划, 求和必然是能够增量核算, 而不是每次更新对悉数的存量数据做一次核算, 咱们只需求每次将字段变化的值进行相加, 就能得到完好的实时的成果, 而这个过程中变化的值, 需求用新鲜的值减去陈腐的值

这儿的值咱们用 before 表明

操作类型

对应于 写入/更新/删去 的符号

从某种意义上来说, 这个符号并不是必须的, 咱们能够从前面两个新旧值 a b 得到, 有 a 无 b 的就是写入, 有 a 有 b 的就是更新, 无 a 有 b 的就是删去, 可是冗余存储一份会让数据在感官上十分明晰

操作类型的字段用 op 来表明

操作内容

用来描绘这次操作详细做了什么改变, 更多是用于 更新 操作, 在一些场景, 比方数据的实时同步上, 能够削减一些额定的负担

这个值能够通过 新旧值 的差获取, 独自记载也是为了提高记载自身的可读性

操作内容的字段用 patch 里表明

仅有符号

用来描绘操作对应的是哪条记载, 能够用来对数据进行精准辨认

大多数状况下, 这儿的符号是主键, 在没有主键的状况下, 能够用仅有索引替代, 如果都没有, 符号需求退化为 悉数的陈腐的值

仅有符号用 key 来表明

结构

当时数据 schema 的描绘

关于结构, 有两种比较通用的做法, 一种是将结构与数据放在一同, 这样做的好处是每个内容都是自解析的, 不需求额定存储结构, 不好处是额定占用了很多的存储空间, 因为比较数据的改变, 结构的改变往往是少数的, 每个数据都带结构存储对资源是一种浪费

另一种规划是将结构, 与结构的改变独自规划一个事情进行告诉, 这样的规划节省了资源, 可是在进行数据实时处理的过程中, 结构需求确保每条数据需求与数据自身的结构一一对应, 带来了额定的工作量

在这儿 TAPDATA 的挑选仍是从场景出发, 挑选将结构改变独自寄存, 成为 DDL 事情, 不在数据流里展示, 结构的结构与数据的结构完全一致, 只是在 kv 的内容上, 变成对字段的描绘

用 type, 值为 ddl, 或者是 dml 里表明是数据描绘仍是结构描绘

时刻

操作产生的时刻, 因为对人来说, 时刻是十分直观的特点, 在回退消费和定位数据点等场景下十分方便, 咱们用 ts 来表明, 一般的精度在 ms 等级

位移

与时刻相似, 记载操作产生的序号, 时刻的好处在于人可读, 不好处在于不准确, 一般时刻的精度在 ms 等级, 而每 ms 能够产生许多事情, 为了准确认位一个事情, 咱们需求一个仅有位移, 这儿用 offset 表明, 一个确认的数据源, 和一个确认的位移, 能够表达一个确认的数据流

来历

用来描绘这个数据所属的数据源的信息, 类型, 姓名, 库/表, 是产生在全量阶段, 仍是增量阶段(稍候我会解说为什么需求一个这样的区别), 咱们作为数据源的方针, 有时分需求通过一个操作获取比较多空间的数据, 在这儿增加一个特点区别, 也有利于后续的数据处理

这儿用 source 字段表明, 大概的特点有:

"source": {      "connector": "mongodb",     "name": "fulfillment",     "ts": 1558965508000,     "snapshot": false,     "db": "inventory",     "table": "customers", }

仿制代码

完成上的问题

实时数据的结构规划能够做得比较完善, 可是完成起来会有各种各样的问题, 之前讲过一些, 这儿从更细节的角度做一些总结

值的缺失

将结构按操作分组, 对完好的流核算结构的需求来说, 写入操作应该包括 after 值, 更新操作应该包括 before/after 值, 删去操作应该包括 before 值

可是因为数据库的日志规划是为同步预备的, 只需求确保现有日志应用之后, 方针的数据能够达到一致的状况就能够, 不一定会包括悉数的字段, 而通过流核算之后, 完好的数据不一定会被保存, 这个会形成引擎自身无法获取完好的数据流

举 MongoDB 的比方, 知名的 CDC 结构 debezium 是如此解说的:

In MongoDB’s oplog, update events do not contain the before or after states of the changed document. Consequently, it is not possible for a Debezium connector to provide this information. However, a Debezium connector provides a document’s starting state in create and read events. Downstream consumers of the stream can reconstruct document state by keeping the latest state for each document and comparing the state in a new event with the saved state. Debezium connector’s are not able to keep this state.

因为数据库自身的日志里不包括这些要害信息, 关于 日志 的消费方来说, 想要补全是很困难的, debezium 的原文是 it is not possible for a Debezium connector to provide this information

抛开 CDC 结构的束缚, 从流核算结构的角度来看, 只需结构在同步的时分, 能把之前的值保存下来, 在产生更新的时分把数据吐出去, 就能得到完好的前后值了

不一致的数据类型

数据获取时分, 需求在平台进行各种处理, 而不同的数据源子数据类型上有各自的规范, 在进行触及多个源数据交互的时分, 会遇到无法辨认的问题

比方来自 Oracle 的 9 位精度时刻, 和 来自 MongoDB 的 3 位精度时刻都在表达时刻, 可是两者同步做 JOIN 的时分, 直接比对会出现永久无法匹配的状况

因此关于数据, 完成一个完好一致的数据类型, 关于后续的流处理是十分要害的

不一致的结构类型

不同的数据库结构差异或许十分大, 举几个比方:

  1. 命名空间层级: 部分数据库只有单层空间, 比方 ES 的索引, 部分数据库或许存在三层空间, 比方 Oracle 的库, 表, schema
  2. 表定义: 部分数据库是强结构表, 比方大部分的 SQL 数据库, 部分数据库是动态弱结构表, 比方 ES 的动态 mapping, 部分数据库无结构, 比方 MongoDB, 部分数据库是 KV, 比方 Redis
  3. 索引结构差异大: 有些数据库只支撑 B 树索引, 有些支撑 地理位置, 全文, 或者图索引

关于数据结构的不同, 完成完全相同的笼统是十分困难的, 可是完成一个边界明晰的支撑规模是可行的

TAPDATA 的处理方案

针对批流一体数据格式, TAPDATA 在完成数据流出的时分, 已经针对不同的数据源完成了一致规整, 关于 MYSQL 相似的数据库, 因为 ROW LOG 包括了完好的字段, 能够直接转化解析, 关于其他的不包括完好数据的数据库, 进行了 内存+外存缓存 构建完好数据流的方案, 简略装备, 规整全自动

针对数据类型的问题, TAPDATA 的结构笼统了多种平台规范的数据类型, 数据源在 读/写 数据时均对此完成了适配, 而且保存了通用数据类型的扩展接口, 处理了异构数据类型的相互通信问题

在结构改变上, 相同完成了结构改变的一致转化, 比方针关于 MYSQL 的删去字段, 在 MongoDB 里会转化为对全表的 UNSET 字段操作, 处理了异构数据源之间的 DDL 操作转化问题

在完成这些规范化之后, 来自数十个数据库的数据就变成了一致规整的流, 四四方方排好队, 等待引擎下一步的解析与核算

留一个小问题

流核算引擎的实时核算, 一般是核算的哪些内容呢?

重视咱们(tapdata.net), 重视我, 带给你最新的实时核算引擎的考虑, 我是来自 TAPDATA 的一名低调的码农

重视 Tapdata 微信公众号, 带给你最新的实时核算引擎的考虑。本文作者 Tapdata 技能合伙人肖贝贝,更多技能博客:tapdata.net/blog.html