有赞亿级订单同步的探索与实践

2019-04-25 17:01:34 浏览数 (1)

文 | 王启 on 订单

一、引子

有赞是提供商家 SAAS 服务,随着越来越多的商家使用有赞,搜索或详情的需求也日益增长,针对需求及场景,之前提到过的订单管理架构演变及 AKF 架构等在这两篇文章里已经有所体现,而这些数据的查询来自于不同的 NoSQL,怎么同步这些非实时存储系统将是一个很有趣的事情。

1.1 同步现状

当前有赞订单同步流程及业务现状如图所示,采用了 ES HBase(tip1)架构体系去解决搜索和详情的需求,利用 canal(tip2)将数据库变更写入到 mq 中,然后利用同步系统来解决相关数据同步问题,而后续下文中将叙述有赞订单同步面临的问题及应对方案。

二、同步

2.1 实现同步基础 - 单表同步

2.1.1 乱序问题

单表同步如图所示:

业务场景中,在这任何一个序号链路中,如果并发出现同一主键的两条消息,同时在 Nosql 中没有做版本控制,都有可能造成消息乱序问题,而相反,如果通过顺序解析 binlog 的同时,为每条 statement sql 执行的结果分配一个顺序 SeqNo,该 SeqNo 保证有序(tip3),再通过 Nosql 中控制乐观锁就可以解决顺序性问题。

2.1.2 HBase 同步

Hbase 同步相对来说比较简单,Hbase 内部拥有 timestamp 协助控制每个 qualify 的版本,只要让 timestamp 传入上文所说的顺序 SeqNo,那么就可以保证每个字段读取出来的数据是最终一致性的。

2.1.3 ES 同步

ES 同步针对单表场景可以通过 index 的操作来进行写入,index 可以采用 exteneral 版本也称作外部版本号来进行控制,同样适用上述的 SeqNo 来做乐观锁去解决该问题。

2.1.4 同步过程图

2.2 实现同步进阶 - 多表同步

2.2.1 乱序问题

多表同步基于单表同步的基础上做相关的同步,如图所示:

当数据库的两张表(不关心是否在一个实例上,如果不在,还更惨,SeqNo 还不一定能够保证有序)触发了更新操作,假设 t1 生成 binlog 的 SeqNo 小于 t2 生成binlog 的 SeqNo,若 t1 这条消息因序号链路中的网络抖动或其它原因造成消费晚于 t2,也就是 t2 的 binlogSeq 先写入 Nosql 中,那么就会造成一个 t1 的数据无法写入到 Nosql 中。

2.2.2 HBase同步

其实上述多表同步乱序的问题并不是绝对的,针对 Hbase 这种自带列版本号的将会自动处理或丢弃低版本数据,同时针对这种情况,设计成每个 table 表中的字段都会列入到 Hbase 中。举个例子,针对订单的情形存入订单主表和订单商品表

场景1:1

针对订单主表,我们写入的数据以订单号做 hash,然后以 hash 值:订单号作为主键降低热点问题,同时定义单 column family,qualifiy 格式为 表:字段 value为对应的 value 值,timestamp 为 SeqNo,如图所示:

场景1:n

针对订单商品表,我们写入的数据同样以订单号生成相应的 rowkey,同时定义单 column family,qualifiy 格式为 表:字段:对应记录的 id 值 value 为对应的 value 值,timestamp 为 SeqNo,如图所示:

通过上述写入,能够针对具体到某个字段都有对应的 timestamp 值的更新,为后期写入更新数据能够更新到具体字段级别。

2.2.3 ES同步

针对上面的同步乱序问题,ES 没有 HBase 这种列版本号,ES 只有 doc 级别的 version,如果上述真的出现 SeqNo2>SeqNo1,且 SeqNo2 早于 SeqNo1 写入到 ES 中,则就会出现 SeqNo1 的内容无法写入,也就会造成顺序不一致的情况。那如何去解决这个多表同步问题呢?

既然会乱序,那让它有序就好了,数据保证有序不就能够解决这个事情嘛,让整个链路有序也就代表 canal 消费 binlog 数据保证有序且丢到 MQ 中有序,MQ 然后保证顺序投递到 Sync 消费处理程序中,通过消费一条消息然后 ack 告诉 MQ 是否成功,已达到保证所有数据全部有序(若多线程或多机器处理 MQ 中的多个分区都是会存在问题)。如图所示:

如此只要保证 t1 表的数据和 t2 表中的数据在 ES 不互相关联,每个数据写入的时候按照 update 方式写入(如果不存在需要做一次 create 操作),这样就能保证所有数据按照顺序执行。

2.3 配置化同步

上文已经讲到数据同步是由一个数据源(这个数据源可以来自于 MQ、Mysql 等)同步到另外一个数据源(Mysql、ES、HBase、Alert 等),也就是一个管道的过程。借鉴了一下 logstash 官网,同样处理流程分为 input、filter、output 组件,这些流程称之为 task 任务,如图所示:

通过这些组件,抽象化出每个组件都有对应的配置,由这些配置来进行初始化组件,驱动组件去执行流程。简单来说,只需要在页面中配置一些组件,无需开发任何一行代码就能实现同步任务。如图所示:

通过一系列的配置,就能配置出一个任务,针对业务逻辑,可以采用动态语言 groovy 来实行脚本化处理(复杂业务场景可以通过 UDF 函数来做支持),针对 mqinput 拿到的字段然后经过处理,经过过滤 filter 等,可以直接拿到相关的数据进行组装,然后配置化的写入到 ES 中,无需开发任何一行 java 代码即可实现流程自动配置化,针对复杂的需求也能够高效率支持。配置界面如图所示:

2.3.1 性能瓶颈

上述就能解决 ES 多表同步的问题,但是同样会存在一些问题:

  • 性能瓶颈问题
  • 失败堆积问题

性能瓶颈:比如写入量超级大的场景情况下,而 Sync 消费程序只能针对 MQ 中的分区(kafka 的 partition 概念)消费,每个分区只能有一个线程去执行,消费速率与消费分区成正比,与消费 RT 成反比,尤其是大促场景下就会造成数据消费不过来,数据堆积严重问题。失败堆积:因为是顺序消费,只要某个分区的某条消息消费失败,后续消息就会全部堆积,造成数据延迟率超高。所以建议用顺序队列的场景除非是业务量没有性能瓶颈的情况下可以采取使用,而怎么去解决顺序队列或者去掉顺序队列呢?用顺序队列无非就是保证有序,因为 ES 没有 HBase 的字段级别版本号,目前订单采用的是用 HBase 做一层中间处理层,解决该问题,如图所示:

通过借助 HBase 字段级别版本号帮助每个表保证表内部字段有序,同时 put 写入完数据之后,通过额外字段 version 做 increment 操作,当这两个写入动作完成之后立马 get 操作拿到 HBase 的数据写入到 ES 中,无论并发程度如何,最终至少有一次的 get 请求拿到的版本 version 字段是最大的,用该 version 作为 ES 的外部版本号解决 ES 版本号问题。用此方案会有好处:

  • HBase 协助管理内部字段版本,同时根据内部操作,协助 ES 拿到对应的版本,且数据能拿到最新数据;
  • 去掉了顺序队列,HBase 具有良好的吞吐,相对于顺序队列拥有更大的吞吐量;
  • 横向拓展增大消费速率;
  • ES 可以采用 index 操作,性能更好。

当然也有弊端:HBase 存在抖动的情况,以及主备切换问题。

因为存在抖动或者准备切换问题,会造成数据不一致,我们该怎么去解决这个事情呢?

2.4 未来扩展

目前订单同步是通过加载配置文件形式来做的,也就是横向拓展的机器都会去加载同一份配置文件,各个任务通过异常解耦,理论上不会有影响,但是会存在加载任务的重要度的问题。 举个例子:

  • 我需要一台机器临时去消费数据解决线上问题;
  • 有个量级很大但又不是很重要的任务,想不影响其他任务的进行;
  • 要做对比,增量延迟对比或全量对比数据,但又不希望影响其他数据;
  • 查询日志需要所有机器查看查询(当然,公司有内部日志系统,可直接上去查看) 如此,可以让同步系统无状态化,每个任务的配置加载有任务配置平台来进行配置,指定相关的机器做相关的处理,扩容也可以动态申请扩容,如图所示,可以自由分配机器处理不同的任务。

三、一致性保障

上文讲了有赞在处理订单的时候怎么讲数据同步到 ES 或 HBase,数据来源于 binlog,写入到 MQ,也就是说处理的来源来自于 MQ。简单一句话来讲:我们不生产消息,我们是消息的搬运工。“搬运工”的角色可以做一些事情,同样有赞在处理数据对比也是如此,这章讲讲“搬运工”可以做什么:

3.1 数据对比

上述一般情况下不会出问题,那如果出问题了怎么办,需要做数据对比,而数据来源就是我们刚刚抛弃的顺序队列,顺序队列有个缺点就是堆积,同样我们也可以利用堆积的特性,让其第一条消息堆积十分钟,那么后续消息基本上也会堆积十分钟,然后就可以消费这个消息进行数据拉取,拿到最新的数据进行数据对比,如图所示:

通过对比结果发送到 alert 中,就可以知道哪些数据不一致,频率多少,这也是一种同步(mq->filter->alert)!

3.2 全量对比/数据刷入

上述我们讲到数据同步到 Nosql 中,但是只是讲了增量的一个过程,涉及到历史数据,就需要对历史数据进行迁移,同样,这也是一种数据同步,后面将会出相关博文怎么去做全量数据同步。

四、Tips

Tip-1:为什么采用 ES HBase 处理搜索和详情?

一般情况下,公司达到一定规模,有类似全文检索的需求或者高频 key:value 的时候,大家会推荐 ES HBase 的架构体系去完成搜索和详情的需求,而现实中,绝大多数情况下生产环境不会将数据直接写入到 ES 或者 Hbase,大家都会优先写入数据库,不进行双写的操作是因为增加链路影响业务。当然 Hbase 可能还好一点,ES 本身就是非实时查询系统(为什么是非实时,有兴趣的可以去看看ES读写流程),这种情况下也造就了 ES 和 HBase 的一个准实时系统。针对业务来说,准实时是可以满足相关需求的,比如商家搜索订单并不要求实时。

Tip-2:为什么有赞选 canal 解析 binlog,而不是采用业务消息进行数据同步?

  • 数据表被更改,比如修数据情况,业务消息不会触发,导致无法写入到对应的 Nosql 中造成。数据不一致
  • 顺序性问题无法得到相关保障;
  • 业务消息并不能拿到所有相关的数据进行写入到 nosql 中。

Tip-3:SeqNo 实现方式,为什么不用 binlogoffset?

因为 cana 实例与 mysql 实例是 1:N(推荐 1:1),而大部分业务场景同一种数据一般会落在同一个实例上,canal 就可以通过该台实例的时间与每秒处理的个数相结合。如:timestamp*10000 counter ,而不用 binlogoffset 的原因是 mysql 的实例挂了话,binlogoffset 可能会乱序。

五、结语

有赞交易订单管理承接了亿级流量的同步任务,面临着众多的需求挑战,从最开始的mysql到如今的产品化的同步任务,从单表同步到多表同步,从单索引到多索引,从增量到全量,都有不同的解决之道,如今新兴搜索中台更是承接亿级搜索和同步流量,如有兴趣,欢迎加入,我们一起共同探讨。

0 人点赞