数据集成领域创业感悟2-技术篇

前言

这篇是数据集成领域创业系列的第二篇,主要就是聊聊一些实现一个数据集成软件时,一些技术上的思考和感悟。整体内容会以分散的topic来组织。

数据集成软件的协议设计

过去自己实现的数据集成软件关于协议设计这块的思考不太足够,这导致整体内核层面的统一性不是很高。如果有个良好设计的内核协议,针对数据集成软件内核的开发是大有益处的。例如协议可以约定内核层面需要实现的核心对象、能力、标准是怎样的,这有助于形成一个可扩展、高效、合理抽象的内核。对团队开发协作来说,这也很重要。协议的设计让我们逆向思考实现一个数据集成软件,哪些是必要的、需要提前抽象好的。这点给我重要启示还是因为看了airbyte protocol。我们可以看下airbyte协议的设计,从中学到设计一个数据集成软件必须抽象和提前定义好的能力是哪些。airbyte将协议定义成一系列标准构件及其接口,他们通过进程间通信来协作来实现一个ELT pipeline。
image.png
airbyte将以下内容列到协议中进行设计,这对于我们后续实现自己的数据集成软件协议是有重要借鉴意义的:

  • data store:数据源的统一抽象,包括API、JDBC database、file等等
  • state/checkpoint: 状态管理,其中比较重要的是检查点的抽象。关于检查点这个可以有很多名字,offset、position、checkpoint。
  • source/destination:内核源、目标的抽象。顾名思义,source负责抽取数据,destination负责load数据到data store
  • namespace: 用于隔离数据。例如mysql中是database,postgresql中是schema。
  • message:顶层消息的抽象
  • data types:数据类型

是否支持事务的取舍

可以有事务支持,但是不应该成为默认选项。即使有事务支持,也应该后续作为一个额外开启的选项,而不是作为默认配置。之所以这么说主要是因为:

  • 几乎没有场景:估计也就阿里这种做异地多活,源对端数据库都要读写的场景才需要事务。我们实际市场接触下来这么多用户并没有发现对事务有强诉求的。
  • 影响性能和资源占用:支持事务的最关键一点就在于你不得不hold一整个事务直到transaction end才可以写入对端。当有大事务或者并发比较高的场景下,各种事务hold会占用你大量的资源,为了做保序同一个事务内的变更必须等到事务结束才可以写入。
  • 不方便做批处理优化和内存管理:另外就是支持事务实现时很容易内存中处理消息时以事务为单位进行控制,因为这样简单。但是这么做的后果就是你很难做批处理优化和内存管理。例如下面一个缓行缓冲池。我们封装一个事务的消息是一个Event对象。下图16个slot可以放16个event,但是你根本不知道这边到底有多少“行”的变更。因为一个event代表一个事务,一个事务中又会有很多变更,你根本不知道整个缓冲池有多少行变更以及他们会占用多少内存。批处理调优也无从谈起,当你觉得性能不足时,你增大slot数量很有可能直接导致OOM,因为不清楚每个slot到底会占多少内存,而且slot占用内存大小还随着事务大小动态变化。相反的,针对行的优化是比较好做的,针对一个表,平均每行的大小我们根据数据库统计信息是可以估算出来的。假设每行大小平均1K,如果这边的每个slot只放一行的变更,我可以非常轻松的得出结论:这个缓行缓冲区总共放16行的变更,占据大小约16K。对于用户侧来说,我们可以将这个缓行缓冲区的大小的参数开放给用户或者由程序调控,整个处理的内存开销是可控的,批处理优化调优也是可行的。

image.png

考虑back pressure来提升内存利用率

过去实现的多个数据集成软件中我都用了disruptor这个组件。好处自然不用多少,cache line友好、lock-free、并发。不过其缺点也很明显,就是不支持backpressure,这在很多时候成为数据集成软件占用内存的罪魁祸首。理由也很简单,很多时候数据集成软件transfer消息的性能瓶颈基本都在写入。对端写得慢,这些没能及时写到对端的消息只能驻留在环形缓冲池中导致资源浪费。大部分数据集成软件并不是一个计算密集型的软件,其实可以不用disruptor,不仅重还资源浪费。disruptor还是适合用在CPU-bound的场景中。

映射实现取舍

数据集成软件提供端到端的迁移同步能力,用户往往需要数据对象之间的映射能力。例如源端的库名是A,写到对端映射的库名则是B。针对关系数据库这种映射关系可以在database、schema、table、column四种维度上去映射。

这里其实有两种思路:

  • 基于用户自定义配置:优点是能力强、功能灵活、可以支持用户配置各个层级上的映射。缺点是实现上复杂些、并且映射配置本身空间占用会很大。迁移同步上千个表的时候在网络中传递配置数据的成本就比较高。如果基于自定义配置,可以采用自底向上的映射处理,这样可以屏蔽异构数据源数据对象层级不一致导致的复杂性(例如mysql是两层的结构,db/table,而postgresql是db/schema/table)。
  • 基于规则:基于规则的映射配置空间占用小,实现也简单很多。

从我接触的实际用户场景来看,基于规则的实现方式是比较好的。用户完全需要自定义的映射规则的场景几乎没有,通过提供一些规则足以满足用户的实际需求,同时也能保证内核实现上简单、高效。过去为了实现完全基于用户的自定义配置内核层面其实这块做的比较复杂,而且由于所有数据链路都依赖这个内核,基本上属于一个“巨石内核实现”,不太敢动,实际上是不太合理的。

设计之初就要考虑可测性

对于数据集成软件的可测性重点体现在两个维度:

  • 内核可测性:数据集成软件内核的可测性在设计之初就需要考虑,这可以逆向要求我们设计一个松耦合的内核。这点在实践中很容易被忽略,因为我们总是习惯先实现代码,等写完代码再回头类调整代码支持内核可测性会难得多。对于数据集成软件内核的可测性很重要,原因主要是数据源实在是太多了,他们包括各类RDBMS、file、存储、API等等。如果链路可测性不好,会阻碍我们构建健壮的数据链路。这点国外软件比国内都做的好很多。
  • 端到端集成测试:这个如果在设计之初没准备好,一般后续还是可以跟上的。作为集成测试则从整个产品维度关注功能、数据准确性、性能的情况。

DDL变更实时同步

实际用户使用场景中,对于DDL实时同步主要是新增表的数据订阅需求比较多,针对已经订阅表的DDL实时同步反而诉求不多。在线实时同步的场景中,用户更加喜欢自己掌控DDL执行的节奏来追求更好的可控性和安全性。这个也是合理的,DDL是个风险操作,交给数据集成软件同步自动执行从DBA数据源管理的角度来说是不太合适的。

DDL实时同步其实是个蛮重的特性,因为支持DDL实时同步你得做:

  • ddl parse
  • sql改写
  • 数据集成软件内部元信息更新

现在主流实现方式有两种:

  • druid parser: 国人产品,社区也很活跃,作者也在持续维护,在阿里有应用。性能上据说比antlr好,缺点是可能支持的SQL完整性不如antlr
  • antlr: 这个解析器的好处是有各种数据库完整的语法g4文件,支持的完整度是很高的。缺点可能是性能上相比druid得优化下

DDL作为数据集成领域内一个非常不高频的操作,一定的性能损失是可以接受的,从我的角度来说使用antlr实现,从可掌控性、支持程度来说都是更好的。

源、目标解耦

数据集成软件的源、目标实现解耦是一种比较好的实践。如果源、目标是高度耦合的,内核的复杂性会随着数据源的增加而呈现指数级上升。下图演示了未解耦合解耦后源、对端关系数量的变化。通过引入中间体系我们可以将源对端解耦从而使得整个内核变得更加干净。
image.png

数据schema的处理

数据的schema结构是重要的的元数据,他在数据集成软件中主要应用在:

  • 异构数据源之间的结构迁移:实现该能力需要获取源端的结构信息,从而可以支持SQL改写然后生成适用于对端的新SQL
  • 根据类型信息做value的处理:异构数据源不同类型读写的处理不同,得依据schema中的类型信息做特殊的处理
  • 基于schema信息做性能优化:基于schema结构例如pk、uk等信息做一些性能优化。例如常见的基于pk partition写入的方式可能引发的uk conflict冲突问题,可以根据pk、uk信息对数据重新分组避免冲突提升性能

数据schema的管理的一些心得:

  • 和内核解耦、独立提供服务:schema管理是适合作为独立的服务来抽象的。schema管理和数据集成软件内核耦合在一起是不适合的。内核作为一个负责迁移同步的工作进程经常会涉及重启,在云原生的背景下这种重启会更加普遍。而schema的获取本身是个很重的操作,尤其在涉及的库表十分多的场景下。每次重启内核都需要重新初始化schema会大大拖慢启动速度。和内核解耦作为独立的服务在架构上会更加清晰,而且针对schema服务可以更好的做数据共享、性能优化等。
  • 不要使用JDBC metadata class获取元schema信息: 通过这个类获取schema元数据的话是不太靠谱的,因为这个取决于jdbc driver的实现。不同数据库厂商对于jdbc标准的实现程度都不一样,有些数据库可能完全没对MetaData接口做实现。正确的做法应该是统一通过数据库内置的系统表去获取元信息

设计中间类型系统

对于数据集成软件设计一个适配异构源对端数据源的类型系统是至关重要的,其带来的好处包括:

  • 解耦源对端的读写
  • 内核层面统一的类型提供系统可以进行标准化抽象,代码架构上会更加精简高效

设计一个中间类型系统可以在此基础上实现一个统一的内核读写层,将异构数据源读写差异从内核解耦出去。

数据集成与流计算

年初看了一篇《重新思考流处理与流数据库》也促使我重新审视和思考数据集成软件可以从流计算引擎和流数据库中得到哪些启示。从某种角度来说,数据集成软件和流计算有一定的相似之处,大家都是处理stream data,都有点像个ETL pipeline。然而数据集成关注的主要是关注在读和写上,本质只是个数据流经的管道,没有计算或者很少计算,而流计算本质是个计算引擎。

从我个人判断来看,数据集成软件至少是没必要在内核层面引入重度的计算层的。计算引擎是为计算密集型应用服务的,而数据集成软件显然不是,这个主要是由其职责决定的。数据集成软件核心职责是异构数据源读写以及作为统一管道。数据集成软件不是说不需要任何计算,而是说没必要在内核层面引入一个很重的计算层。数据集成软件也会涉及计算,不过一般都不是很重度的场景,例如数据的裁剪、清洗。专业计算的事情交给专业的人来做就好。

未来流计算引擎会占据极低延迟、事件驱动的场景,而新型数据库随着计算能力不断增强和其天然的易用性,会抢占其他重度计算的场景。数据集成软件本身则当好自身异构数据源桥梁的角色。未来各个数据软件之间虽然仍然会有些交叉,但是互相之间是会协同工作的,并且具备一定的边界性。

数据集成与消息系统

这里的消息系统主要指的是Kafka、RocketMQ这类带存储的消息系统。 消息系统和数据集成软件也有很多相似之处,他们都作为一个数据流经的管道,而其中核心的不同之处在于消息系统更加像一个通用的总线,承担高通量的数据分发,而数据集成这个管道是面向特定领域的。消息系统给数据集成软件带来的启示主要是来源于Kafka这类带存储的消息系统。这让我思考如果数据集成软件引入存储层会怎么样?

从我近些年该领域的沉淀来看,我觉得数据集成软件引入一个松耦合的存储层是必要的。数据集成软件很多时候被设计成一个无状态的管道,本身不做数据的持久化。对于数据集成软件来说,完全放弃存储最大的问题在于失去change data的控制。失去change data的控制产生的典型问题包括:

  • change log丢失:实际应用场景中我们最常碰到的问题就是用户侧自身的change log不存在了,导致的同步阻塞。这种情况一旦发生,基本就得重新执行全量、增量完整的操作来恢复数据。
  • 延迟链路的稳定性不可控:延迟是迁移同步链路不可避免的问题,很多时候这受到资源、源对端数据库读写能力的制约。因为无法掌控change data,这种情况下数据链路是否还能继续稳定工作是受到数据库本身影响的。数据库对于已经过时的change data如何维护和管理数据集成软件是无法控制的。

综合来看,引入存储对于数据集成软件来说是很有意义的。我们可以设计一个和内核松耦合的存储层,用来存储change data,这样可以带来诸多好处。包括:

  • 获取change data控制权:整个数据变更流就在数据集成软件本身的掌控中了。我们可以让用户决定数据保留的策略,数据集成核心的CDC能力也不再受到数据库内部机制的影响,实现更简单链路更稳定。
  • change data可共享:没有数据集成软件层面的存储层意味着没有change data共享,如果一个数据库的change log有很多下游,dump log和解析这个过程会重复执行,对于性能、网络带宽来说都是一种浪费。
  • 针对性的优化:引入存储层给数据集成软件做更多性能优化带来可能。例如从数据共享、缓存等角度都可以开展一些优化工作。