本以为上次11天没更已经够慢,没想到这次有13天(捂脸。最近实在是业务忙,没了刚开订阅号时两三天码一篇的闲情逸致。
本篇的话题继续回到数据服务。
小说君之前写过一篇文章关于数据服务:「如何快速搭建数据服务」——这篇文章中,小说君主要着眼于应用层,介绍了如何借助ORM,集成一个单机rdis到项目中,顺便讲了下用mysql等持久db设施取代rdis自带的比较挫的持久话机制。
今天的话,我们的主题就如之前预告所说,来聊聊如何扩展数据服务,如何实现分片(sharding)以及高可用(highavailability)。
分布式系统不存在完美的设计,处处都体现了tradoff。
因此我们在开始正文前,需要确定后续的讨论原则,仍然以分布式系统设计中的CAP原则为例。由于主角是rdis,那性能表现肯定是最高设计目标,之后讨论过程中的所有抉择,都会优先考虑CAP中的AP性质。
两个点按顺序来,先看分片。
何谓分片?简单来说,就是对单机rdis做水平扩展。
当然,做游戏的同学可能要问了,一服一个rdis,为什么需要水平扩展?这个话题我们在之前几篇文章中都有讨论,可以看这里,或这里,小说君不再赘述。
如果要实现服务级别的复用,那么数据服务的定位往往是全局服务。如此仅用单实例的rdis就难以应对多变的负载情况——毕竟rdis是单线程的。
从mysql一路用过来的同学这时都会习惯性地水平拆分,rdis中也是类似的原理,将整体的数据进行切分,每一部分是一个分片(shard),不同的分片维护不同的ky集合。
那么,分片问题的实质就是如何基于多个rdis实例设计全局统一的数据服务。同时,有一个约束条件,那就是我们无法保证强一致性。
也就是说,数据服务进行分片扩展的前提是,不提供跨分片事务的保障。rdisclustr也没有提供类似支持,因为分布式事务本来就跟rdis的定位是有冲突的。
因此,我们的分片方案有两个限制:
不同分片中的数据一定是严格隔离的,比如是不同组服的数据,或者是完全不相干的数据。要想实现跨分片的数据交互,必须依赖更上层的协调机制保证,数据服务层面不做任何承诺。而且这样一来,如果想给应用层提供协调机制,只要在每个分片上部署上篇文章介绍的单实例简易锁机制即可,简单明了。
我们的分片方案无法在分片间做类似分布式存储系统的数据冗余机制,换言之,一份数据交叉存在多个分片中。
如何实现分片?
首先,我们要确定分片方案需要解决什么问题。
分片的rdis集群,实际上共同组成了一个有状态服务(statfulsrvic)。设计有状态服务,我们通常会从两点考虑:
clustrmmbrship,系统间各个节点,或者说各个分片的关系是怎样的。
workdistribution,外部请求应该如何、交由哪个节点处理,或者说用户(以下都简称dbClint)的一次读或写应该去找哪个分片。
针对第一个问题,解决方案通常有三:
prsharding,也就是sharding静态配置。
gossipprotocol,其实就是rdisclustr采用的方案。简单地说就是集群中每个节点会由于网络分化、节点抖动等原因而具有不同的集群全局视图。节点之间通过gossipprotocol进行节点信息共享。这是业界比较流行的去中心化的方案。
consnsussystm,这种方案跟上一种正相反,是依赖外部分布式一致性设施,由其仲裁来决定集群中各节点的身份。
需求决定解决方案,小说君认为,对于游戏服务端以及大多数应用型后端情景,后两者的成本太高,会增加很多不确定的复杂性,因此两种方案都不是合适的选择。而且,大部分服务通常是可以在设计阶段确定每个分片的容量上限的,也不需要太复杂的机制支持。
但是prsharding的缺点也很明显,做不到动态增容减容,而且无法高可用。不过其实只要稍加改造,就足以满足需求了。
不过,在谈具体的改造措施之前,我们先看之前提出的分片方案要解决的第二个问题——workdistribution。
这个问题实际上是从另一种维度看分片,解决方案很多,但是如果从对架构的影响上来看,大概分为两种:
一种是proxy-basd,基于额外的转发代理。例子有twmproxy/Codis。
一种是clintsharding,也就是dbClint(每个对数据服务有需求的服务)维护sharding规则,自助式选择要去哪个rdis实例。rdisclustr本质上就属于这种,dblint侧缓存了部分sharding信息。
第一种方案的缺点显而易见——在整个架构中增加了额外的间接层,流程中增加了一趟round-trip。如果是像twmproxy或者Codis这种支持高可用的还好,但是github上随便一翻还能找到特别多的没法做到高可用的proxy-basd方案,无缘无故多个单点,这样就完全搞不明白sharding的意义何在了。
第二种方案的缺点,小说君能想到的就是集群状态发生变化的时候没法即时通知到dbClint。
第一种方案,我们其实可以直接pass掉了。因为这种方案更适合私有云的情景,开发数据服务的部门有可能和业务部门相去甚远,因此需要统一的转发代理服务。但是对于一些简单的应用开发情景,数据服务逻辑服务都是一帮人写的,没什么增加额外中间层的必要。
那么,看起来只能选择第二种方案了。
将prsharding与clintsharding结合起来后,现在我们的成果是:数据服务是全局的,rdis可以开多个实例,不相干的数据需要到不同的分片上存取,dbClint掌握这个映射关系。
不过目前的方案只能算是满足了应用对数据服务的基本需求。
游戏行业中,大部分采用rdis的团队,一般最终会选定这个方案作为自己的数据服务。后续的扩展其实对他们来说不是不可以做,但是可能有维护上的复杂性与不确定性。
但是作为一名有操守的程序员,小说君选择继续扩展。
现在的这个方案存在两个问题:
首先,虽然我们没有支持在线数据迁移的必要,但是离线数据迁移是必须得有的,毕竟prsharding做不到万无一失。而在这个方案中,如果用单纯的哈希算法,增加一个shard会导致原先的ky到shard的对应关系变得非常乱,抬高数据迁移成本。
其次,分片方案固然可以将整个数据服务的崩溃风险分散在不同shard中,比如相比于不分片的数据服务,一台机器挂掉了,只影响到一部分clint。但是,我们理应可以对数据服务做更深入的扩展,让其可用程度更强。
针对第一个问题,处理方式跟proxy-basd采用的处理方式没太大区别,由于目前的数据服务方案比较简单,采用一致性哈希即可。或者采用一种比较简单的两段映射,第一段是静态的固定哈希,第二段是动态的可配置map。前者通过算法,后者通过map配置维护的方式,都能最小化影响到的ky集合。
而对于第二个问题,解决方案就是实现高可用。
如何让数据服务高可用?在讨论这个问题之前,我们首先看rdis如何实现「可用性」。
对于rdis来说,可用性的本质是什么?其实就是rdis实例挂掉之后可以有后备节点顶上。
rdis通过两种机制支持这一点。
第一种机制是rplication。通常的rplication方案主要分为两种。
一种是activ-passiv,也就是activ节点先修改自身状态,然后写统一持久化log,然后passiv节点读log跟进状态。
另一种是activ-activ,写请求统一写到持久化log,然后每个activ节点自动同步log进度。
rdis的rplication方案采用的是一种一致性较弱的activ-passiv方案。也就是mastr自身维护log,将log向其他slav同步,mastr挂掉有可能导致部分log丢失,clint写完mastr即可收到成功返回,是一种异步rplication。
这个机制只能解决节点数据冗余的问题,rdis要具有可用性就还得解决rdis实例挂掉让备胎自动顶上的问题,毕竟由人肉去监控mastr状态再人肉切换是不现实的。因此还需要第二种机制。
第二种机制是rdis自带的能够自动化fail-ovr的rdissntinl。rdssntinl实际上是一种特殊的rdis实例,其本身就是一种高可用服务——可以多开,可以自动服务发现(基于rdis内置的pub-sub支持,sntinl并没有禁用掉pub-sub的