分片需要解决两个问题:
针对第一个问题,解决方案通常有三:
需求决定解决方案,对于游戏服务端来说,后两者的成本太高,而且增加了很多不确定的复杂性,因此现阶段这两种方案并不是合适的选择。比如gossip protocol,redis cluster现在都不算是release,确实不太适合游戏服务端。而且,游戏服务端毕竟不是web服务,通常是可以在设计阶段确定每个分片的容量上限的,也不需要太复杂的机制支持。
但是第一种方案的缺点也很明显,做不到动态增容减容,而且无法高可用。但是如果稍加改造,就足以满足需求了。
在谈具体的改造措施之前,先看之前提出的第二个问题。
第二个问题实际上是从另一种维度看分片,解决方案很多,但是如果从对架构的影响上来看,大概分为两种:
第一种方案的缺点显而易见,在整个架构中增加了额外的间接层,pipeline中增加了一趟round-trip。如果是像twemproxy或者Codis这种支持高可用的还好,但是github上随便一翻还能找到特别多的没法做到高可用的proxy-based方案,无缘无故多个单点,这样就完全搞不明白sharding的意义何在了。
第二种方案的缺点就是集群状态发生变化的时候没法即时通知到dbClient。
第一种方案,我们其实可以直接pass掉了。因为这种方案本质上还是更适合web开发的。web开发部门众多,开发数据服务的部门有可能和业务部门相去甚远,因此需要统一的转发代理服务。但是游戏开发不一样,数据服务逻辑服务都是一帮人开发的,没什么增加额外中间层的必要。
那么,看起来只能选择第二种方案了。
将presharding与client sharding结合起来后,现在我们的改造成果是:数据服务是全局的,redis可以开多个实例,不相干的数据需要到不同的shard上存取,dbClient掌握这个映射关系。
引入新的问题目前的方案只能满足游戏对数据服务的基本需求。
大部分采用redis的游戏团队,一般最终会选定这个方案作为自己的数据服务。后续的扩展其实对他们来说不是不可以做,但是可能有维护上的复杂性与不确定性。今天这篇文章,我就继续对数据服务做扩展,后面的内容权当抛砖引玉。
现在的这个方案存在两个问题:
针对第一个问题,处理方式跟proxy-based采用的处理方式没太大区别,由于目前的数据服务方案比较简单,采用一致性哈希即可。或者采用一种比较简单的两段映射,第一段是静态的固定哈希,第二段是动态的可配置map。前者通过算法,后者通过map配置维护的方式,都能最小化影响到的key集合。
而对于第二个问题,实际上就是上一节末提到的数据服务可用性问题。
4.2.2 可用性方案 定义问题
讨论数据服务的可用性之前,我们首先看redis的可用性。
对于redis来说,可用性的本质是什么?其实就是redis实例挂掉之后可以有后备节点顶上。
redis通过两种机制支持这一点。
还是由于CAP原则,redis的replication方案采用的是一种一致性较弱的active-passive方案。也就是master自身维护log,将log向其他slave同步,master挂掉有可能导致部分log丢失,client写完master即可收到成功返回,是一种异步replication。
这个机制只能解决节点数据冗余的问题,redis要具有可用性就还得解决redis实例挂掉让备胎自动顶上的问题,毕竟由人肉去监控master状态再人肉切换是不现实的。 因此还需要第二种机制。
redis基于自带的这两种机制,已经能够实现一定程度的可用性。那么接下来,我们来看数据服务如何高可用。
数据服务具有可用性的本质是什么?除了能实现redis可用性的需求——redis实例数据冗余、故障自动切换之外,还需要将切换的消息通知到每个dbClient。
由于是redis sentinel负责主从切换,因此最自然的想法就是问sentinel请求当前节点主从连接信息。但是redis sentinel本身也是redis实例,数量也是动态的,redis sentinel的连接信息不仅在配置上成了一个难题,动态更新时也会有各种问题。而且,redis sentinel本质上是整个服务端的static parts(要像dbClient提供服务),但是却依赖于redis的启动,并不是特别优雅。另一方面,dbClient要想问redis sentinel要到当前连接信息,只能依赖其内置的pub-sub机制。redis的pub-sub只是一个简单的消息分发,没有消息持久化,因此需要轮询式的请求连接信息模型。