3 零漏单数据同步
我们已经有了API网关以及可靠的消息服务,但是对外提供服务时,用户在订单数据获取中常常因为经验不足和代码缺陷导致延迟和漏单的现象,于是我们对外提供数据同步的服务。
传统的数据同步技术一般是基于数据库的主备复制完成的。在简单的业务场景下这种方法是可行的,并且已经很多数据库都自带了同步工具。但是在业务复杂度较高或者数据是对外同步的场景下,传统的数据同步工具就很难满足灵活性、安全性的要求了,基于数据的同步技术无法契合复杂的业务场景。
双11场景下,数据同步的流量是平常的数十倍,在峰值期间是百倍,而数据同步机器资源不可能逐年成倍增加。保证数据同步写入的平稳的关键在于流量调控及变更合并。
3.1 分布式数据一致性保证
在数据同步服务中,我们使用了消息 + 对账任务双重保障机制,消息保障数据同步的实时性,对账任务保障数据同步一致性。以订单数据同步为例,订单在创建及变更过程中都会产生该订单的消息,消息中夹带着订单号。接受到该消息后,对短时间内同一订单的消息做合并,数据同步客户端会拿消息中的订单号请求订单详情,然后写入DB。消息处理过程保证了订单在创建或者发生了任意变更之后都能在极短的延迟下更新到用户的DB中。
对账任务调度体系会同步运行。初始化时每个用户都会生成一个或同步任务,每个任务具有自己的唯一ID。数据同步客户端存活时每30秒发出一次心跳数据,针对同一分组任务的机器的心跳信息将会进行汇总排序,排序结果一般使用IP顺序。每台客户端在获取需执行的同步任务列表时,将会根据自身机器在存活机器总和x中的顺序y,取得任务ID%x=y-1的任务列表作为当前客户端的执行任务。执行同步任务时,会从订单中心取出在过去一段时间内发生过变更的订单列表及变更时间,并与用户DB中的订单进行一一对比,如果发现订单不存在或者与存储的订单变更时间不一致,则对DB中的数据进行更新。
图8 数据同步服务架构
3.2 资源动态调配与隔离
在双11场景下如何保证数据同步的高可用,资源调配是重点。最先面临的问题是,如果每台机器都是幂等的对应全体用户,那么光是这些用户身后的DB连接数消耗就是很大问题;其次,在淘宝的生态下,卖家用户存在热点,一个热点卖家的订单量可能会是一个普通卖家的数万倍,如果用户之间直接共享机器资源,那么大流量用户将会占用几乎全部的机器资源,小流量用户的数据同步实效会受到很大的影响。
为了解决以上问题,我们引入了分组隔离。数据同步机器自身是一个超大集群,在此之上,我们将机器和用户进行了逻辑集群的划分,同一逻辑集群的机器只服务同一个逻辑集群的用户。在划分逻辑集群时,我们将热点用户从用户池中取出,划分到一批热点用户专属集群中。分组隔离解决了DB连接数的问题,在此场景下固定的用户只会有固定的一批机器为他服务,只需要对这批机器分配连接数即可,而另一个好处是,我们可以进行指定逻辑集群的资源倾斜保障大促场景下重点用户的数据同步体验。
数据同步服务大集群的机器来源于三个机房,在划分逻辑集群时,每个逻辑分组集群都是至少由两个以上机房的机器组成,在单个机房宕机的场景下,逻辑集群还会有存活机器,此时消息和任务都会向存活的机器列表进行重新分配,保证该逻辑集群所服务的用户不受影响。在机器发生宕机或者单个逻辑集群的压力增大时,调度程序将会检测到这一情况并且对冗余及空闲机器再次进行逻辑集群划分,以保证数据同步的正常运行。在集群压力降低或宕机机器恢复一段时间后,调度程序会自动将二次划分的机器回收,或用于其他压力较大的集群。
图9 机器宕机与重分配
3.3 通用数据存储模型
订单上存储的数据结构随着业务的发展也在频繁的发生的变化,进行订单数据的同步,需要在上游结构发生变化时,避免对数据同步服务产生影响,同时兼顾用户的读取需求。对此我们设计了应对结构易变数据的大字段存储模型。在订单数据的存储模型中,我们将订单号、卖家昵称、更新时间等需要被当做查询/索引条件的字段抽出独立字段存储,将整个的订单数据结构当成json串存入一个大字段中。
图10 订单同步数据存储结构
这样的好处是通过大字段存储做到对上游业务的变化无感知,同时,为了在进行增量数据同步时避免对大字段中的订单详情进行对比,在进行数据同步写入的同时将当前数据的hashcode记录存储,这样就将订单数据对比转换成了hashcode与modified时间对比,提高了更新效率。
3.4 如何降低数据写入开销
在双11场景下,数据同步的瓶颈一般不在淘宝内部服务,而在外部用户的DB性能上。数据同步是以消息的方式保证实时性。在处理非创建消息的时候,我们会使用直接update + modified时间判断的更新方式,替换传统的先select进行判断之后再进行update的做法。这一优化降低了90%的DB访问量。
传统写法:
SELECT * FROM jdp_tb_trade WHERE tid = #tid#; UPDATE jdp_tb_trade SET jdp_response = #jdpResponse#, jdp_ modified = now() WHERE tid = #tid#
优化写法:
UPDATE jdp_tb_trade SET jdp_response = #jdpResponse#, jdp_ modified = now() WHERE tid = #tid# AND modified < #modified#
订单数据存在明显的时间段分布不均的现象,在白天订单成交量较高,对DB的访问量增大,此时不适合做频繁的删除。采用逻辑删除的方式批量更新失效数据,在晚上零点后交易低峰的时候再批量对数据错峰删除,可以有效提升数据同步体验。