摘要:这里有点像的主从同步一样,拿到内存的最后版本后还有新过来的写操作进入和队列,先把历史版本推给客户端,再把之后的写操作一次推给客户端。
以下为演讲实录:本文是野狗科技联合创始人&架构师谢乔在ArchSummit 北京2015全球架构师峰会上进行的《基于数据同步云服务架构实践》的演讲实录,主要分为三个方面:野狗的数据同步理念,数据同步的架构演进,数据同步的细节问题。
野狗官博:https://blog.wilddog.com/
野狗官网:https://www.wilddog.com/
公众订阅号:wilddogbaas
可能大家在实际的应用场景中不使用数据同步的业务模式,但是我是想跟大家分享我们在演进过程中一些问题的解决思路,希望能对大家有所帮助。
今天的演讲内容主要分三个议题:
野狗的数据同步理念
数据同步的架构演进
数据同步的细节问题
野狗的数据同步理念首先从云端这块儿开始讲起,我们的数据存储是个Schema-free的形式,树形的数据库像一颗Json树,更像前端工程师们用的数据结构,它能把原来的关系型数据通过一些关联查询形成聚合型的数据,比如blog,里面有标题、回复等内容,就相当于把数据重新聚合,这样数据之间的关系就更直观了,方便大家快速的设计比较好的数据结构,完美的与url结合,每条数据都通过url来唯一定位,每个path作为一个key,就成为了key-value的数据结构。
经典的云服务是这样的:现提供一个API,然后有其他的auth接入,云端有存储,有用户管理,有hosting功能,还有周边的一些工具,客户端通过rest api这种方式与云端进行交互来开发你的业务模型。
而野狗除了这一部分以外,还有一个富客户端的SDK,本地也做了存储,当本地数据发生变化的时候会通过一个事件来通知用户,然后用户进行修改。
具体来讲,是客户端与服务端建立一个长连接,来完成数据同步,当同步完成之后产生数据变化,就可以完成业务逻辑的实现。如果我们把模型再抽象一点,就像一个主从的同步,客户端作为从,和云端进行副本级的同步过程。
也可以有另外一种同步方式,大家的服务可以与野狗云进行实时同步。比如说,你的服务端进行了一次数据修改,同步到云端,云端把这个修改同步给关注这个数据的客户端。
数据实现同步的基本模型是这样的:
开始有一个初始化的慢同步,可以做全量的同步或者条件同步,比如这个例子,客户端A进行了条件同步,同步到本地产生了一个本地副本,客户端B通过全同步拉取到本地形成一个本地副本。当客户端A修改后,产生了新的数据,我们把它叫增量同步,数据会push到云端。然后本地使用best-effort模式,客户端先成功触发事件,然后再同步到云端,云端再同步到其他的客户端,实现最终一致性。
这个过程很像op log的过程,也是基于长连接的,如果每次连接发生了异常,这里会重新连接进行一次初始化慢同步过程。这也是我们所做的数据同步和消息推送的根本区别,原因是,消息推送要保证每个消息顺序到达,而且不丢失,数据同步则是在性能上的提升,只关心最终的数据状态。一旦发生异常,客户端重新连入到云端以后,不会把之前过程中的op log都传过去,只需要重新进行一次初始化操作,让两端进行同步恢复就可以了。
数据同步的架构演进刚才讲的业务方面的内容可能比较枯燥,接下来就是我们技术架构的演进过程。
首先看一下我们技术架构的特点,跟其他传统业务不太一样,属于写多读少。因为读只需要读一次到客户端以后,读客户端的副本就可以了,而且一些修改操作直接修改客户端本地,再由终端同步到云端,剩下的操作大部分都是写操作。写同步当然是越实时越好,但问题就是读的性能肯定会有一些延迟,后面会详细讲解。
我们实现的是最终一致性,因为这不是强一致性的架构,很多客户端可以关注同一个数据节点的变化。因为我们采用最终一致性,所以会导致多个客户端可以同时进行写操作,就必然会产生写冲突的问题,所以并行写冲突的问题也要解决。
实时性是我们的特点,这里暂时不详细说。
最后一个是幂等操作。
这是0.1版本的架构框图,这个主要面向我们的初期用户,用来验证我们产品是否被用户认可。这个架构由一个接入层组成,用来维护和客户端的长连接,如果有一个请求过来,会产生数据操作到数据处理,数据处理直接写Mysql。
Mysql这块儿直接用了主从同步的模式来保留一定的可用性,然后再进行数据推送。数据推送的时候,先从Redis集群中进行lookup操作,这个操作的目的是寻找要修改的数据节点被哪些终端所关注,然后再进行push操作。
这里的数据采用了物化路径存储,也就是说,如果存的是/a/b/c的数据,实际上是存/a一条/a/b一条,/a/b/c一条。
业务得到认可之后,需要对早期用户有一个性能的保证,所以就有了这个0.2版本的架构框图,把之前的Mysql改成了mongodb。使用mongodb的原因是可以动态创建数据库,把用户的数据在APP级别进行隔离,这样不会互相影响。同时,mongodb也带来了读写性能的提升。
同时我们采用了副本集多活,利用mongodb自己的副本集主挂了之后自动切从的方案。
机枪换导弹的意思是之前是一次一次对数据库进行操作,现在我们做了批量的操作和合并的push。之前的操作一个push会影响多个数据节点发生变化,会一条一条的推给关注的终端,现在可以做一个合并的push。
当我们的产品进入bate版测试之后就需要面向广大的公测用户了,我们逐渐要面对的就是写压力了。因为mongodb的写操作对于同一个数据表是锁表的,所以写是一个串行的性能问题,所以我们这里加了一个写缓冲队列,这是大家都会想到的解决方案。
我们这里使用了kafka。一条数据来了之后,由生产者进入kafka,然后由消费者把kafka的数据拿出来进行批量消费,最后内存生成一个操作树的缓存,再批量写入mongodb。这块儿更类似Nagle算法,达到一定的操作量或者达到一定的超时时间后,就同步到Mysql数据库。
可能大家有过加写缓冲的经验,这时候肯定会面临读性能下降的问题。因为这时候我们在读到mongodb的时候是一个已经过时的数据快照,有一些操作还暂存在kafka,写缓存队列中,所以必须要解决这个读不一致的问题。当读操作来的时候,先从mongodb中读取到快照,然后再记录你当前执行到哪,一共有哪些操作还未执行。读取完之后,在内存进行一个回放操作,拿到的就是比较新的快照版本了。
但是这里还有一个问题,在操作的过程中,还会有新的写操作过的内容,就算回放完,也是过期的版本。这里有点像redis的主从同步一样,拿到内存的最后版本后还有新过来的写操作进入push和wait队列,先把历史版本推给客户端,再把之后的写操作一次推给客户端。最后在客户端进行计算达到的就是最终一致性,用户拿到的就是最新的数据版本。
在beta版发布一段时间之后,服务器的负载是很平稳的上升,延迟是10、11、12ms,每周是这样一种递增。但是突然有一天我们发现延迟暴增到上百ms,甚至到700ms,我们开始各种排查。但是查过之后,kafka、mongodb等等,都一切正常,最后才查到原来是因为push这里需要查一次redis造成的。也就是说,我们在redis中存的是路径Key,路径下面是有哪些客户端节点关注了这个key,所以这里要进行一次模糊匹配查询,当一个实例的redis数据量到达20w、30w条的时候,如果用模糊查询性能会非常低,延迟会达到几百ms。所以我们这里采用了临时方案,用mongodb来代替redis,用mongodb加它的索引来提升模糊查询的性能。
这里也为我们敲了个警钟,我们需要做性能监控,才能真正的面对用户。后来我们就基于flume做了一套自己的性能监控。Flume可以统计日志,还有对每一个系统延迟的调用,以及异常报警,都写入flume,再做一个flume的后台处理。
我们在设计架构的时候,总是把我们的关注点放在最容易发生问题的位置,而往往有时候虽然你解决了这块儿的问题,但是由于总量上来了,还会影响一些原来不关注的地方出现问题,完全出乎意料。
数据同步的细节问题刚才是简单架构框图的介绍,现在是我们数据同步面临的一些细节的介绍。
两个客户端同时修改本地的副本,需要考虑到数据的静态一致性,同时还要考虑到写隔离的问题。对于这个问题其实有两个解决方案:一是中心化锁机制;另外一个是进程间协商机制。但是锁机制会有单点故障问题。所以我们做了一个分布式树形锁机制。不过这里有一些需要注意的问题:1、tryLock和release 需要2次的交互;2、需要注意注册Lock的有效期;3、要等待Lock超时;4、最好使用动态hash;5、连接异常时退化。
还有一些性能问题,因为每个App都有一个树形锁,所以是单进程就算你进行了这种操作,在理论上是会有一个吞吐量的上限的。任何操作都要先去尝试先获得锁,这个操作其实是一个浪费的操作。主要性能的点有两个:一个是单次push sync量比较大,可以导致阻塞。另外一个就是异步push sync。
因为以上这些原因,一个恶心的架构就诞生了。主要因为缩减了write操作的过程,还有要保证云端与客户端的一致性。整个系统就会太过于复杂,不确定因素太多。
但是我们做技术不能意淫。在真实的应用场景中,有同一客户端场景和不同客户端场景。但是两者所占的比例是不一样的。不同客户端的写冲突有0.3%,同一客户端写冲突有4.1%。所以说,其实冲突的概率是非常小的。用上面那种方式就会有种“杀鸡焉用宰牛刀”的感觉。
所以,我们提出了一个理念:让上帝的归上帝,野狗的归野狗。具体到实施上就是让用户进行可配置化,主要有四种方式:1、默认不启用;2、减少不必要的开销;3、降低锁粒度;4、由appld hash改进为path hash。在这里技术的同学就要注意了,有些问题其实不需要多么厉害的架构,如果能在业务层面进行解决,就尽量将问题在业务层面解决,不要做特别复杂的架构去解决一些虚无缥缈的问题。
要解决这些问题,主要还是依赖写时的树形锁,达到顺序push的效果。如果没有这个操作,就会出现客户端数据不一致的问题,所以push顺序很重要,一定要一致。
主要是需要保证同一客户端的顺序性。以“太空站”这个游戏为例。飞机走着走着回发生回退的现象,造成这个现象的原因,是因为客户端在进行写处理的时候是进行并行处理的。这个问题很好解决,可以按照客户端ID散列到每一个数据处理的进程上,在数据处理进程内部达到一个数据写一致的效果。进程内的锁也要实现顺序性,所以目标又变成了解决write的性能。
第四个问题就是最终一致性的问题,刚才我们说的都是云端和被同步客户端之间的问题。
但是这块儿还会产生的问题模型是客户端A在本地先做修改,由1修改成2,将2同步到云端以后,云端也修改成2,云端再push到其他的客户端,对这个数据有关注的,也会修改成2,这样就解决了最终一致性的问题。
看似很完美,但还是有漏洞。
刚才所做的这一切,只能保证云端和被同步的客户端的数据是一致的,但是这种情况由于客户端可以都先对本地进行修改,客户端A修改成2,客户端B修改成3,在推送到云端的过程中,A进行的修改会写入,B进行的修改也会写入。最后执行的时候如果在云端执行的时候是以某种顺序推送过来的,假设云端最后生成的是2那就是说,云端和左侧是一致的,就会与另一侧的节点产生不一致。
也就是说,由于并行写,最后会有一个客户端产生不一致的问题。
这里我们也没有用到一些复杂的算法,用了一个push给自己的模型来化解这个问题,达到最终的一致性。在并行写和推送的时候仍然推送给自己,由于推送的过程是串行的,只有推送完前面的一次,才会推送对这个节点的下一次改变操作。这个推送完毕以后,因为是TCP的,所以会按顺序推送过去,那就可以认为,在这个推送过程中,所有终端都达到了一致性。
会产生的问题大家也可以看到就是可能会出现,数据由2修改成3,再修改成2。在这里我们需要对一致性问题和性能做一个取舍,当然还是选择为了达到实时,所以采用这种比较弱的最终一致性方案。
最后一个问题,是一个原子性问题,因为我们是幂等操作,所以不会支持if then,i ++的操作。我们在这里用了一个自旋锁的CAS机制,在本地拉到数据之后做一个hash,这个hash和要修改的值做一个复合操作一起发到云端,而云端也对这个数据进行一个hash,如果两个hash是一致的,那才能认为可以操作,才能覆盖。如果不一致的话,重新从云端再次同步一些数据到本地产生一些副本,进行上一步的操作,直到成功为止。不过我们也有一个重试次数,现在的设置是20次。
今天的演讲就到这里了,谢谢大家。
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/11710.html
摘要:用小程序云开发将博客小程序常用功能一网打尽本文介绍博客小程序的详情页的功能按钮如何实现,具体包括评论点赞收藏和海报功能,这里记录下整个实现过程和实际编码中的一些坑。考虑到小程序本身的大小限制,使用的方式是最佳的。 用小程序·云开发将博客小程序常用功能一网打尽 本文介绍mini博客小程序的详情页的功能按钮如何实现,具体包括评论、点赞、收藏和海报功能,这里记录下整个实现过程和实际编码中的一...
摘要:用小程序云开发将博客小程序常用功能一网打尽本文介绍博客小程序的详情页的功能按钮如何实现,具体包括评论点赞收藏和海报功能,这里记录下整个实现过程和实际编码中的一些坑。考虑到小程序本身的大小限制,使用的方式是最佳的。 用小程序·云开发将博客小程序常用功能一网打尽 本文介绍mini博客小程序的详情页的功能按钮如何实现,具体包括评论、点赞、收藏和海报功能,这里记录下整个实现过程和实际编码中的一...
摘要:前面给大家讲过一个借助小程序云开发实现微信支付的,但是那个操作稍微有点繁琐,并且还会经常出现问题,今天就给大家讲一个简单的,并且借助官方支付实现小程序支付功能。只需要一个简单的云函数,就可以轻松的实现微信小程序支付功能。 前面给大家讲过一个借助小程序云开发实现微信支付的,但是那个操作稍微有点繁琐,并且还会经常出现问题,今天就给大家讲一个简单的,并且借助官方支付api实现小程序支付功能。...
摘要:笔者最近涉猎了小程序相关的知识,于是利用周末时间开发了一款类似于同事的小程序,深度体验了小程序云开发模式提供的云函数数据库存储三大能力。 笔者最近涉猎了小程序相关的知识,于是利用周末时间开发了一款类似于同事的小程序,深度体验了小程序云开发模式提供的云函数、数据库、存储三大能力。关于云开发,可参考文档:小程序·云开发。 个人感觉云开发带来的最大好处是鉴权流程的简化和对后端的弱化,所以像笔...
摘要:七调用云函数发送邮件我们在文件里写一个按钮,当点击这个按钮时就发送邮件。到这里我们就完整的实现了微信小程序云开发使用云函数发送邮件的功能了。 先看效果图: showImg(https://segmentfault.com/img/remote/1460000020151412); 通过上面的日志,可以看出我们是158开头的邮箱给250开头的邮箱发送邮件,下面是成功接收到的邮件。 sho...
阅读 4510·2021-09-22 14:57
阅读 554·2019-08-30 15:56
阅读 2657·2019-08-30 15:53
阅读 2233·2019-08-29 14:15
阅读 1684·2019-08-28 17:54
阅读 553·2019-08-26 13:37
阅读 3471·2019-08-26 10:57
阅读 1040·2019-08-26 10:32