前言

为什么我们要将数据从 MySQL 实时同步到 ES ,本质是什么?相对于数据去规范化的其他几种方案,数据迁移同步方式存在以下几个优点,也是其成为目前业界主流方式的原因:

  • 稳定性好:迁移同步对主数据库的操作主要是进行数据和日志的顺序读取,同时并发小,对主数据库稳定性影响较小(较多的下游订阅可能在网络层面存在影响,一般用消息解决)。另外日志(Binlog/WAL/Redo 等)可重放特质,让下游丢数据的可能性大大减小(处理好幂等的情况下)
  • 业务解耦:一般而言主数据库更多承载事务型操作,而下游数据系统承载运营等层面的业务, 典型如电商的买家侧和卖家侧业务
  • 业务侵入小:数据迁移同步对业务无侵入,双端对接标准数据库(源),可以便利地找到开源、商业、云等各个方向的成熟解决方案或产品
  • 业务适配性好:某些数据迁移同步产品能够嵌入业务逻辑,让下游获取到更加贴近业务的数据,从而让数据服务更加有效和便捷

前置知识:数据库去规范化

数据库规范化(Database normalization)是指关系型数据库中通过一系列数据库范式来减少数据冗余、增强数据一致性的策略。Database Normalization 在带来我们看得见的好处同时(利于事务操作性能、存储成本降低),伴随数据规模扩大、并发度提高、复杂度上升,弊端也慢慢展现,这时候 Database Denormalization 能够一定程度满足这些挑战,总体思路是通过一系列降低写入性能的操作例如更多的数据冗余、数据分组等来提升数据库读取的性能。

去规范化的时机

数据去规范化动机多样,当出现因数据复杂操作影响系统稳定性、业务响应/并发要求不满足等都是触发因素:

  • 业务稳定性问题:面向 C 端的互联网应用特征是并发量较高,SQL 偏向点查点写,相对简单,但是沉淀下来的数据(比如订单、支付等) 需要做运营往往涉及传统企业级应用对于数据库的操作特征,大范围数据栅查、表关联、排序等实时操作,以及满足报表/BI 等更加复杂的数据库需求。通过去规范化和负载分离是较合理的选择。

  • 复杂查询性能问题:企业级应用例如 ERP、CRM、BOSS 或者其他一些企业运营系统,经常涉及表关联、聚合、多维筛选、排序等操作,并常常带来性能问题。通过去规范化的一些方式,如下文提到的数据冗余和预计算方式,显著改善性能。

去规范化的几种实现方式

假设有如下三张表,学生、班级和教师。需求是:已知学生的学号,需要查询当前学生的班主任是名字。

使用规范化数据查询,是一个 3 表联查操作,而在数据库中,大体分三步:

  1. 通过学生学号获取学生信息,得到班级编号
  2. 通过班级编号获取班级信息,得到班主任工号
  3. 通过班主任工号得到教师信息,得到班主任的名字

如果在数据量较大,有一定并发要求,并且涉及更多表关联时候,这种计算就不能满足需求,这个时候去规范化的优化方式就登场了。

image-20210525185355159

列级处理——主查询表冗余字段

通过在主表冗余计算好的数据,可避免频繁重复计算数据。如下场景适合在主数据表内冗余数据:

  • 应用系统需要经常获取计算好的数据
  • 冗余的原始数据不经常变化

在学生表冗余班主任的名字信息,表的设计变为如下:

image-20210525191316634

这时候查询就只有一步了:

  1. 根据序号获取学生信息,同时也得到了其班主任名字的信息

优点:方法较为简单易懂,容易实现。

缺点:侵入业务逻辑,拖慢业务代码性能的同时,长期迭代所产生的变化可能会有稳定性风险。

表级处理——宽表预构建/Cube 预构建

表级处理主要操作就是构建宽表,或者构建数据立方体(Data Cube)。构建好的宽表包含了用户查询时需要的所有维度、度量信息。以上面学生查找班主任的问题为例,构建的宽表结构如下。

表级处理常见实现方式包括 应用多写数据库自身实现的物化视图数据迁移同步

image-20210525191344855

应用多写

在主数据相同数据库内创建宽表,应用写入数据的时候同时也向宽表写入数据(事务保证一致性),复杂查询即可从该表进行。

优点:实现简单、低成本

缺点:对主数据库造成更大的读写压力,外加业务改造成本。

RDBMS 物化视图

Oracle、SqlServer 等数据库物化视图方案,通过数据冗余与预计算减少 join、聚合,从而提升查询性能。例如,在 Oracle 上完成学生查找班主任这个查询,可以构建一张“学生管理表”的物化视图,查询请求直接请求物化视图即可得到查询结果,避免 join ,显著改善该 SQL 执行效率。

优点:数据库引擎自身支持,使用成本较低

缺点:RDBMS 实现的方式有自己的局限性,比如生成物化视图的数据需要做一些业务紧相关变换就无法满足,或者某些数据库并没有完整实现该能力(物化视图在 2000 年左右是数据库学术圈研究的重点)。

数据迁移同步 ⭐️

借助数据同步工具,准实时将主数据表数据组织变换(包括按照业务逻辑变换)形成普通表或大宽表,写入第三方存储引擎(如 OLAP 存储引擎或者搜索系统)。复杂查询直接在预构建好的表上或者 cube 上执行,从而达到良好的性能。数据迁移工具的选择较多,总体上按照其侧重点,可以分为如下几类:

  • 大数据类:为大数据产品流入数据提供服务,因为大数据产品本身特点,侧重批量定时的迁移,实时同步一般需要用特别的方法,往往和业务特征紧耦合。常见的数据迁移同步工具有 sqoopdatax
  • 流计算类:为自身流计算框架生态服务,侧重计算,迁移同步更多是类似数据连接器的角色,代表的产品如 Flink
  • 消息类:为自身消息产品生态服务,如丰富的 kafka connectordebezium
  • 数据库类:数据库厂家一般都会提供原厂工具,典型如 Oracle 的 GoldenGate
  • 云厂商类:云厂商提供的数据迁移同步工具,主要侧重自身云上数据库生态产品之间的互融互通和将线下自建数据库的数据上云,例如阿里云 DTS, 腾讯云 DTS , AWS 的 DMS
  • 专业数据迁移同步工具: 包括部分开源产品或第三方独立公司提供的数据迁移同步工具,例如 canalstreamsetsmaxwell、cloudcanal、striimfivetran ,以及老牌数据集成厂商 InformaticaQlik 等所提供的产品

优点:

  • 主库更稳定:异步化解耦业务系统事务查询和复杂查询,避免复杂查询对主数据库产生影响
  • 易运维、链路稳定:数据迁移同步链路有标准化产品支撑,和主业务系统、主库读写解耦。整体架构上职责清晰,易于维护和问题追踪

缺点:需要对纷繁多样的数据迁移同步工具、承载复杂查询数据库产品选型,对技术能力有一定要求

数据迁移同步模型

订阅消费

优点:

  • 堆积能力:由于引入了消息队列,所以整个链路是具备变更数据的堆积能力的。假设变更数据消费的比较慢,MySQL 本地较老的 binlog 文件由于磁盘空间的不足而被删除时,消息队列中的数据仍然存在,数据同步仍然可以正常进行
  • 数据分发能力:引入消息队列后可以支持多方订阅。如果下游多个应用都依赖源端的变更数据,可以订阅同一份 topic 即可
  • 数据加工能力:由于变更数据是由下游消费者订阅,因此订阅后可以灵活的做一些数据加工。例如从外部调用微服务接口或者反查一些数据来做数据加工都是比较方便的

缺点:

  • 运维成本相对较高:包含了较多的组件和应用,运维保障相对复杂。
  • 稳定性风险较高:一环出问题会导致整个数据同步链路的稳定性受到影响。而且排查和定位问题也会比较困难。

image-20210517155636453

端到端直连

优点:

  • 低延迟:端到端的直接同步,链路较短,延迟低
  • 稳定性好:链路组件少,出问题概率较低,定位排查均比较容易。适合对数据精确性高的严苛场景。
  • 功能拓展性强:对端写入消息系统,模拟订阅模式,可扩展性强
  • 运维部署简单:链路组件少,部署运维更简单

image-20210517155816368

如何选择迁移同步模型

如果没有众多的下游数据订阅,建议采用直连模式。数据同步链路往往置于在线业务中,随着业务规模以及重要性逐渐加大,链路 稳定性 更为重要些。另外 端到端模式 只要支持对端数据源为消息中间件,可立刻实现订阅模式,数据加工能力在某些数据迁移同步产品上可通过上传业务代码运行的方式解决。

数据架构在满足业务需求的同时,简洁清晰能够让系统更加易于维护和排查,当遇到链路每天同步几千万条上亿条数据、偶发丢几条需要排查,或同步链路卡住不同步等情况,端到端方式往往体现出相当大的优势。

MySQL 到 ES 数据实时同步存在的问题

MySQL 关联表在 ES 上的设计

关系型数据库库中的表 join 关系在 ES 可以用几种数据类型来表达,包括 objectednestedjoin 三种。

objected

object 类型可以存储嵌套结构.

优点

  • 表示主 field 和 object 内部 field 之间的一对多关系,支持 doc 的 join 查询。由于所有查询时依赖的关联数据也都在一个文档内,避免了 ES 内部的 join,查询效率较高

缺点

  • 一对多关系只能保留一层,多于一层的会被打平,会丢失嵌套 field 内部的关联关系。下面的例子中,第一幅图看到写入 ES 的是一条订单数据,其中 producets 这个 field 是 object 类型,其中包含了多个产品的记录。

🤔 举个例子:

case1:不使用nested的时候,ES中数组的扁平化处理

# 当采用 objected 字段存储 products 信息时,原本存储的信息如下:
"order_id" : 123,
"products" : [

  {
     "price" : 10,
     "sku" : "SKU_10",
  },
  {
     "price" : 20,
     "sku" : "SKU_20",
  }
]
#  ES 中存储的样子为:
# 可以看到在 ES 的存储中,products 中每个字段的值都已经被打平处理。如果我们查询订单 ID  123,价格 price  10,SKU  “SKU_20 的文档,我们同样可以搜索到结果,但这样显然就丢失了其内部之间的关系了。
{

  "order_id":            [ 123 ],
  "products.price":    [ 10, 20],
  "products.sku": [ SKU_10, SKU_20 ],

}

case2:

image-20210525162119050

nested

nested 类型可以存储嵌套结构,表示一对多关系,是 object 类型的拓展。

优点

  • 不会出现 object 的缺点,整个嵌套关系是完整维护的,子文档内部的关联关系保存是完整的
  • 关联数据通过实现上自然关联到主文档上,搜索时性能较好(相对于 join 类型)

缺点

  • 一个 nested field 只能属于一个主文档
  • 在 nested 类型中,子文档和主文档之间是强绑定,主文档更新的时候会强制更新子文档在写多读少的场景,性能开销较大
  • child 文档的查询必须通过父文档再找到子文档
  • 子文档频繁修改的话会影响别的子文档和父文档,因为本质上在 lucence 实现上是父文档下的冗余存储

join

join 类型可以配置父子文档,通过父子文档来实现一对多的能力,一个索引只能建一个。相比 nested 类型,该类型更加灵活。父子文档之间通过 parentId 来关联,实际实现上他们就是独立的文档。

优点

  • 子文档更新不影响父文档和其他子文档
  • 一个子文档可以单独搜索
  • 一个文档在作为子文档时可以自己选择属于哪个父文档。通过 relation 可以指定不同的 join 列

缺点

  • 需要建个全局序数,用于服务于父子文档的关联关系,这个会影响搜索性能

join 和 nested 类型比较

  • join 适合写多读少场景,更加适合关注索引性能的场景。这意味着更新的生效会更快,但是搜索时的开销也相对大些
  • nested 适合读多写少的场景,更加关注搜索的性能

MySQL 到 ES 实时数据同步实现去规范化

在了解 ES 的一些关键类型之后,我们就可以描述通过数据同步去规范化的几种实现方式。

主表冗余数据 ✖️

业务侧将一些查询时需要的关系数据提前冗余在源表的一个字段中。例如序列化成 json 存储在源表的一个冗余字段内,利用数据同步工具写入对端 ES 的 join/nested 类型字段。例如我们有订单表和商品表如下图所示。假设我们的搜索需求是,给定一个订单 ID,同时将这个订单的订单明细以及所有包含的多件商品的明细全部搜索出来。

image-20210525182536729

如果采用这种列级处理模式,我们在订单表新增一个冗余列,然后将商品表的所有明细信息,按照 kv 组织成 json 写入冗余列即可,如下图所示。对端 ES 的 mapping 结构按照如下方式定义。数据同步工具直接将该包含关联表数据的订单表直接同步到对端 ES ,即可在 ES 上搜索符合我们需求的数据。

image-20210525182550214

{
  "mappings": {
    "_doc": {
      "properties": {
        "order_id": {
          "type": "long"
        },
        "order_price": {
          "type": "long"
        },
        "product_count": {
          "type": "long"
        },
        "discount": {
          "type": "long"
        },
        "product_info": {
          "type": "nested"
        },
      }
    }
  }
}

优点:

  • 处理模式能应对各种一对多的关联关系,对数据同步工具的功能要求低,配置简单,只需要支持单表同步到 ES 即可。

缺点:

  • 索引、搜索性能非最佳:提供给 ES 的不是预构建好的宽表数据。例如例子中,订单关联的商品信息,全部存储在主表的一个 object/nested/join 字段内,这种实现方式会有索引、搜索性能方面的额外开销,不是性能最佳的实现方式
  • 业务系统侵入:业务系统写主数据的时候需要额外写入信息
  • 主数据库表冗余过多数据:关系型数据库的表冗余了过多其他表的信息,可能存在存储和性能开销

总结:不太推荐该方式

多表订阅合并预构建宽表数据

数据同步工具同时订阅搜索时依赖的所有表,先到的数据先进到 ES,没有数据过来的字段为空。以上面提到的订单和商品表的例子来说,即同时同步订单表和商品表到对端索引。对端索引的 mapping 定义如下所示,包含订单和商品表的所有字段,定义的索引是一张宽表。流计算中多流汇聚配合时间窗口 join 多表的方式与该种方式有异曲同工之处。

优点

  • 数据同步工具配置同步任务较为简单,无业务入侵,不耦合业务系统逻辑
  • 对数据同步工具要求低,除了同步以外,不需要其他额外的功能特性
  • 基于预构建宽表的方式在 ES 上也有较好的索引和查询性能。
  • 同步链路不会因为宽表某些列缺失数据阻塞整个数据链路的同步(是否有该优点取决于数据同步工具本身设计,如果引入时间窗口,则同步链路会因为等待列数据影响同步时效性)。

缺点:

  • 基于事实表主动触发式的方式来进行宽表的构建。源端订阅的表,如果更新很少或者从来不更新产生 binlog,则对端的文档中的列值可能一直不完整,导致时效性会比较差。搜索的时候有一些列的数据会缺少

总结:

  • 适合构成宽表的事实表数据写入有事务保证一起落盘的场景,这样可以避免对端 ES 搜索到不完整的数据。
  • 适合构建宽表不需要业务加工处理的场景,构建宽表只是单纯的将多张表的列拼接在一起,形成宽表。
{
  "mappings": {
    "_doc": {
      "properties": {
        "order_id": {
          "type": "long"
        },
        "order_price": {
          "type": "long"
        },
        "product_count": {
          "type": "long"
        },
        "discount": {
          "type": "long"
        },
        "product_id": {
          "type": "long"
        },
        "product_unit_price": {
          "type": "long"
        },
        "product_name": {
          "type": "text"
        },
      }
    }
  }
}

同步过程回查预构建

数据同步工具订阅的表称为主表。数据同步过程中,反查数据库查询的表称为从表。利用数据同步工具自身的能力,在订阅主表期间,自动通过回查的方式,填补宽表中的列,形成完整的宽表行数据。对端 ES 的 mapping 定义例子与“多表订阅合并预构建宽表数据”中的保持相同。

优点

  • 基于反查的方式构建宽表灵活性好,可以在生成宽表前基于主表的数据对从表数据做一些轻度的数据加工
  • 一条主表的数据,通过反查生成宽表行,可以配合数据加工生成多条宽表行数据
  • 基于反查的方式可以比较轻松的实现跨实例的 join ,从而生成宽表行(相对好实现,具体要看数据同步工具本身是否支持)
  • 基于宽表预构建的方式在 ES 上有较好的索引、查询性能。

缺点

  • 反查时数据可能没有准备好,导致数据缺失(这里具体的影响取决于数据同步工具本身设计,可以引入时间窗口配合超时等待,也可以没有数据时直接同步到对端)
  • 需要数据同步工具在数据反查、数据加工方面进行支持

总结:

  • 对于构建宽表涉及数据加工的场景,该方式比较适合。
  • 由于该方式的回查机制、预构建前数据加工的能力支持,能力上是“多表订阅合并预构建宽表数据”这种方式的超集。如果有比较好的数据同步工具支持,这种方式是比较推荐的。

数据迁移同步工具选型

数据迁移同步工具的选择比较多样,下表仅从 MySQL 同步 ES 这个场景下,以下是一些数据同步工具的对比,用户可以根据自己的实际需要选取适合自己的产品。

特性\产品 Canal DTS CloudCanal
是否支持自建ES
ES对端版本支持丰富度 中支持ES6和ES7 高支持ES5,ES6和ES7 中支持ES6和ES7
嵌套类型支持 join/nested/object object nested/object
join支持方式 基于join父子文档&反查 基于宽表预构建&反查
是否支持结构迁移
是否支持全量迁移
是否支持增量迁移
数据过滤能力 中仅全量可添加where条件 高全增量阶段where条件 高全增量阶段where条件
是否支持时区转换
同步限流能力
任务编辑能力
数据源支持丰富度
架构模式 订阅消费模式需先写入消息队列 直连模式 直连模式
监控指标丰富度 中性能指标监控 中性能指标监控 高性能指标、资源指标监控
报警能力 针对延迟、异常的电话报警 针对延迟、异常的钉钉、短信、邮件报警
任务可视化创建&配置&管理能力
是否开源
是否免费 是社区版、SAAS版免费
是否支持独立输出 否依赖云平台整体输出
是否支持SAAS化使用

Canal

Canal使用:以MySQL实时同步数据到Elasticsearch为例

canal主要用途是对MySQL数据库增量日志进行解析,提供增量数据的订阅和消费,简单说就是可以对MySQL的增量数据进行实时同步,支持同步到MySQL、Elasticsearch、HBase等数据存储中去。

canal会模拟MySQL主库和从库的交互协议,从而伪装成MySQL的从库,然后向MySQL主库发送dump协议,MySQL主库收到dump请求会向canal推送binlog,canal通过解析binlog将数据同步到其他存储中去。

image-20210525193323135


参考:

canal官方文档

MySQL 数据实时同步到 Elasticsearch 的技术方案选型和思考

MySQL如何实时同步数据到ES?