关于数据库:实时数据引擎系列二-批流一体的数据

7次阅读

共计 4134 个字符,预计需要花费 11 分钟才能阅读完成。

前言

在上文 (https://segmentfault.com/a/11…) 咱们提到了 通过数据库日志 获取陈腐的数据, 在对数据的意识里, 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 操作转换问题

在实现这些标准化之后, 来自数十个数据库的数据就变成了对立规整的流, 四四方方排好队, 期待引擎下一步的解析与计算

留一个小问题

流计算引擎的实时计算, 个别是计算的哪些内容呢?

关注咱们 (https://tapdata.net), 关注我, 带给你最新的实时计算引擎的思考, 我是来自 TAPDATA 的一名低调的码农

关注 Tapdata 微信公众号, 带给你最新的实时计算引擎的思考。本文作者为 tapdata 技术合伙人 肖贝贝,更多技术博客:https://tapdata.net/blog.html

正文完
 0