实战经验丨如何避免云服务数据不一样步?

本文是野狗科技联合创始人&架构师谢乔在ArchSummit 北京2015全球架构师峰会上进行的《基于数据同步云服务架构实践》的演讲实录,主要分为三个方面:野狗的数据同步理念,数据同步的架构演进,数据同步的细节问题。
野狗官博:https://blog.wilddog.com/
野狗官网:https://www.wilddog.com/
公众订阅号:wilddogbaas前端

图片描述

如下为演讲实录:

可能你们在实际的应用场景中不使用数据同步的业务模式,可是我是想跟你们分享咱们在演进过程当中一些问题的解决思路,但愿能对你们有所帮助。redis

图片描述

今天的演讲内容主要分三个议题:算法

  • 野狗的数据同步理念sql

  • 数据同步的架构演进mongodb

  • 数据同步的细节问题数据库

幻灯片4

野狗的数据同步理念

首先从云端这块儿开始讲起,咱们的数据存储是个Schema-free的形式,树形的数据库像一颗Json树,更像前端工程师们用的数据结构,它能把原来的关系型数据经过一些关联查询造成聚合型的数据,好比blog,里面有标题、回复等内容,就至关于把数据从新聚合,这样数据之间的关系就更直观了,方便你们快速的设计比较好的数据结构,完美的与url结合,每条数据都经过url来惟必定位,每一个path做为一个key,就成为了key-value的数据结构。api

幻灯片5

经典的云服务是这样的:现提供一个API,而后有其余的auth接入,云端有存储,有用户管理,有hosting功能,还有周边的一些工具,客户端经过rest api这种方式与云端进行交互来开发你的业务模型。缓存

幻灯片6

而野狗除了这一部分之外,还有一个富客户端的SDK,本地也作了存储,当本地数据发生变化的时候会经过一个事件来通知用户,而后用户进行修改。服务器

幻灯片7

具体来说,是客户端与服务端创建一个长链接,来完成数据同步,当同步完成以后产生数据变化,就能够完成业务逻辑的实现。若是咱们把模型再抽象一点,就像一个主从的同步,客户端做为从,和云端进行副本级的同步过程。前端工程师

也能够有另一种同步方式,你们的服务能够与野狗云进行实时同步。好比说,你的服务端进行了一次数据修改,同步到云端,云端把这个修改同步给关注这个数据的客户端。

幻灯片8

数据实现同步的基本模型是这样的:

开始有一个初始化的慢同步,能够作全量的同步或者条件同步,好比这个例子,客户端A进行了条件同步,同步到本地产生了一个本地副本,客户端B经过全同步拉取到本地造成一个本地副本。当客户端A修改后,产生了新的数据,咱们把它叫增量同步,数据会push到云端。而后本地使用best-effort模式,客户端先成功触发事件,而后再同步到云端,云端再同步到其余的客户端,实现最终一致性。

这个过程很像op log的过程,也是基于长链接的,若是每次链接发生了异常,这里会从新链接进行一次初始化慢同步过程。这也是咱们所作的数据同步和消息推送的根本区别,缘由是,消息推送要保证每一个消息顺序到达,并且不丢失,数据同步则是在性能上的提高,只关心最终的数据状态。一旦发生异常,客户端从新连入到云端之后,不会把以前过程当中的op log都传过去,只须要从新进行一次初始化操做,让两端进行同步恢复就能够了。

数据同步的架构演进

幻灯片9

刚才讲的业务方面的内容可能比较枯燥,接下来就是咱们技术架构的演进过程。

首先看一下咱们技术架构的特色,跟其余传统业务不太同样,属于写多读少。由于读只须要读一次到客户端之后,读客户端的副本就能够了,并且一些修改操做直接修改客户端本地,再由终端同步到云端,剩下的操做大部分都是写操做。写同步固然是越实时越好,但问题就是读的性能确定会有一些延迟,后面会详细讲解。

咱们实现的是最终一致性,由于这不是强一致性的架构,不少客户端能够关注同一个数据节点的变化。由于咱们采用最终一致性,因此会致使多个客户端能够同时进行写操做,就必然会产生写冲突的问题,因此并行写冲突的问题也要解决。

实时性是咱们的特色,这里暂时不详细说。

最后一个是幂等操做。

幻灯片11

这是0.1版本的架构框图,这个主要面向咱们的初期用户,用来验证咱们产品是否被用户承认。这个架构由一个接入层组成,用来维护和客户端的长链接,若是有一个请求过来,会产生数据操做到数据处理,数据处理直接写Mysql。

Mysql这块儿直接用了主从同步的模式来保留必定的可用性,而后再进行数据推送。数据推送的时候,先从Redis集群中进行lookup操做,这个操做的目的是寻找要修改的数据节点被哪些终端所关注,而后再进行push操做。

这里的数据采用了物化路径存储,也就是说,若是存的是/a/b/c的数据,其实是存/a一条/a/b一条,/a/b/c一条。

幻灯片12

业务获得承认以后,须要对早期用户有一个性能的保证,因此就有了这个0.2版本的架构框图,把以前的Mysql改为了mongodb。使用mongodb的缘由是能够动态建立数据库,把用户的数据在APP级别进行隔离,这样不会互相影响。同时,mongodb也带来了读写性能的提高。

同时咱们采用了副本集多活,利用mongodb本身的副本集主挂了以后自动切从的方案。

机枪换导弹的意思是以前是一次一次对数据库进行操做,如今咱们作了批量的操做和合并的push。以前的操做一个push会影响多个数据节点发生变化,会一条一条的推给关注的终端,如今能够作一个合并的push。

幻灯片13

当咱们的产品进入bate版测试以后就须要面向广大的公测用户了,咱们逐渐要面对的就是写压力了。由于mongodb的写操做对于同一个数据表是锁表的,因此写是一个串行的性能问题,因此咱们这里加了一个写缓冲队列,这是你们都会想到的解决方案。

咱们这里使用了kafka。一条数据来了以后,由生产者进入kafka,而后由消费者把kafka的数据拿出来进行批量消费,最后内存生成一个操做树的缓存,再批量写入mongodb。这块儿更相似Nagle算法,达到必定的操做量或者达到必定的超时时间后,就同步到Mysql数据库。

可能你们有过加写缓冲的经验,这时候确定会面临读性能降低的问题。由于这时候咱们在读到mongodb的时候是一个已通过时的数据快照,有一些操做还暂存在kafka,写缓存队列中,因此必需要解决这个读不一致的问题。当读操做来的时候,先从mongodb中读取到快照,而后再记录你当前执行到哪,一共有哪些操做还未执行。读取完以后,在内存进行一个回放操做,拿到的就是比较新的快照版本了。

幻灯片14

可是这里还有一个问题,在操做的过程当中,还会有新的写操做过的内容,就算回放完,也是过时的版本。这里有点像redis的主从同步同样,拿到内存的最后版本后还有新过来的写操做进入push和wait队列,先把历史版本推给客户端,再把以后的写操做一次推给客户端。最后在客户端进行计算达到的就是最终一致性,用户拿到的就是最新的数据版本。

幻灯片15

在beta版发布一段时间以后,服务器的负载是很平稳的上升,延迟是十、十一、12ms,每周是这样一种递增。可是忽然有一天咱们发现延迟暴增到上百ms,甚至到700ms,咱们开始各类排查。可是查过以后,kafka、mongodb等等,都一切正常,最后才查到原来是由于push这里须要查一次redis形成的。也就是说,咱们在redis中存的是路径Key,路径下面是有哪些客户端节点关注了这个key,因此这里要进行一次模糊匹配查询,当一个实例的redis数据量到达20w、30w条的时候,若是用模糊查询性能会很是低,延迟会达到几百ms。因此咱们这里采用了临时方案,用mongodb来代替redis,用mongodb加它的索引来提高模糊查询的性能。

幻灯片16

这里也为咱们敲了个警钟,咱们须要作性能监控,才能真正的面对用户。后来咱们就基于flume作了一套本身的性能监控。Flume能够统计日志,还有对每个系统延迟的调用,以及异常报警,都写入flume,再作一个flume的后台处理。

咱们在设计架构的时候,老是把咱们的关注点放在最容易发生问题的位置,而每每有时候虽然你解决了这块儿的问题,可是因为总量上来了,还会影响一些原来不关注的地方出现问题,彻底出乎意料。

数据同步的细节问题

刚才是简单架构框图的介绍,如今是咱们数据同步面临的一些细节的介绍。

幻灯片18

幻灯片19

两个客户端同时修改本地的副本,须要考虑到数据的静态一致性,同时还要考虑到写隔离的问题。对于这个问题其实有两个解决方案:一是中心化锁机制;另一个是进程间协商机制。可是锁机制会有单点故障问题。因此咱们作了一个分布式树形锁机制。不过这里有一些须要注意的问题:一、tryLock和release 须要2次的交互;二、须要注意注册Lock的有效期;三、要等待Lock超时;四、最好使用动态hash;五、链接异常时退化。

幻灯片20

还有一些性能问题,由于每一个App都有一个树形锁,因此是单进程就算你进行了这种操做,在理论上是会有一个吞吐量的上限的。任何操做都要先去尝试先得到锁,这个操做实际上是一个浪费的操做。主要性能的点有两个:一个是单次push sync量比较大,能够致使阻塞。另一个就是异步push sync。

幻灯片21

由于以上这些缘由,一个恶心的架构就诞生了。主要由于缩减了write操做的过程,还有要保证云端与客户端的一致性。整个系统就会太过于复杂,不肯定因素太多。

幻灯片22

可是咱们作技术不能意淫。在真实的应用场景中,有同一客户端场景和不一样客户端场景。可是二者所占的比例是不同的。不一样客户端的写冲突有0.3%,同一客户端写冲突有4.1%。因此说,其实冲突的几率是很是小的。用上面那种方式就会有种“杀鸡焉用宰牛刀”的感受。

幻灯片23

因此,咱们提出了一个理念:让上帝的归上帝,野狗的归野狗。具体到实施上就是让用户进行可配置化,主要有四种方式:一、默认不启用;二、减小没必要要的开销;三、下降锁粒度;四、由appld hash改进为path hash。在这里技术的同窗就要注意了,有些问题其实不须要多么厉害的架构,若是能在业务层面进行解决,就尽可能将问题在业务层面解决,不要作特别复杂的架构去解决一些虚无缥缈的问题。

幻灯片24

要解决这些问题,主要仍是依赖写时的树形锁,达到顺序push的效果。若是没有这个操做,就会出现客户端数据不一致的问题,因此push顺序很重要,必定要一致。

幻灯片25

幻灯片26

主要是须要保证同一客户端的顺序性。以“太空站”这个游戏为例。飞机走着走着回发生回退的现象,形成这个现象的缘由,是由于客户端在进行写处理的时候是进行并行处理的。这个问题很好解决,能够按照客户端ID散列到每个数据处理的进程上,在数据处理进程内部达到一个数据写一致的效果。进程内的锁也要实现顺序性,因此目标又变成了解决write的性能。

幻灯片27

第四个问题就是最终一致性的问题,刚才咱们说的都是云端和被同步客户端之间的问题。

可是这块儿还会产生的问题模型是客户端A在本地先作修改,由1修改为2,将2同步到云端之后,云端也修改为2,云端再push到其余的客户端,对这个数据有关注的,也会修改为2,这样就解决了最终一致性的问题。

看似很完美,但仍是有漏洞。

幻灯片28

刚才所作的这一切,只能保证云端和被同步的客户端的数据是一致的,可是这种状况因为客户端能够都先对本地进行修改,客户端A修改为2,客户端B修改为3,在推送到云端的过程当中,A进行的修改会写入,B进行的修改也会写入。最后执行的时候若是在云端执行的时候是以某种顺序推送过来的,假设云端最后生成的是2那就是说,云端和左侧是一致的,就会与另外一侧的节点产生不一致。

也就是说,因为并行写,最后会有一个客户端产生不一致的问题。

幻灯片29

这里咱们也没有用到一些复杂的算法,用了一个push给本身的模型来化解这个问题,达到最终的一致性。在并行写和推送的时候仍然推送给本身,因为推送的过程是串行的,只有推送完前面的一次,才会推送对这个节点的下一次改变操做。这个推送完毕之后,由于是TCP的,因此会按顺序推送过去,那就能够认为,在这个推送过程当中,全部终端都达到了一致性。

会产生的问题你们也能够看到就是可能会出现,数据由2修改为3,再修改为2。在这里咱们须要对一致性问题和性能作一个取舍,固然仍是选择为了达到实时,因此采用这种比较弱的最终一致性方案。

幻灯片30

最后一个问题,是一个原子性问题,由于咱们是幂等操做,因此不会支持if then,i ++的操做。咱们在这里用了一个自旋锁的CAS机制,在本地拉到数据以后作一个hash,这个hash和要修改的值作一个复合操做一块儿发到云端,而云端也对这个数据进行一个hash,若是两个hash是一致的,那才能认为能够操做,才能覆盖。若是不一致的话,从新从云端再次同步一些数据到本地产生一些副本,进行上一步的操做,直到成功为止。不过咱们也有一个重试次数,如今的设置是20次。

今天的演讲就到这里了,谢谢你们。

相关文章
相关标签/搜索