上一节末提到过,要想最小化数据迁移成本可以采用两段映射或一致性哈希。这时还有另一种可以扩展的思路,如果采用两段映射,那么我们可以动态下发第二段的配置数据;如果采用一致性哈希,那么我们可以动态下发分片的连接信息。这其中的动态,就可以基于新的符合Phial规范的服务来做。而这个通知机制,就非常适合采用Phial中的Notify pattern实现。而且redis sentinel的实现难度比较低,我们完全可以以较低的成本实现一个扩展性更强,定制性更强,还能额外支持分片服务的部分在线数据迁移机制的服务。
同时,有一部分我在这篇文章里也没提过,那就是落地服务所依赖的mysql的可用性保障机制。相比于再开一个额外的mysql高可用组件,倒不如整合到同样的一个数据服务监控服务中。
这个监控服务就是watcher。由于原理类似,接下来的讨论就不再涉及对mysql的监控部分,只针对redis的。
watcher解决了什么问题?解决这些问题,watcher的职责就已经达成,我们的数据服务也就更加健壮,可用程度更高。
引入新的问题 但是,如果我们引入了新的服务,那就引入了新的不确定性。如果引入这个服务的同时还要保证数据服务具有可用性,那我们就还得保证这个服务本身是可用的。
先简单介绍一下redis sentinel的可用性是如何做到的。同时监控同一组主从的sentinel可以有多个,master挂掉的时候,这些sentinel会根据一种raft算法的工业级实现选举出leader,算法流程也不是特别复杂,至少比paxos简单多了。所有sentinel都是follower,判断出master客观下线的sentinel会升级成candidate同时向其他follower拉票,所有follower同一epoch内只能投给第一个向自己拉票的candidate。在具体表现中,通常一两个epoch就能保证形成多数派,选出leader。有了leader,后面再对redis做SLAVEOF的时候就容易多了。
如果想用watcher取代sentinel,最复杂的实现细节可能就是这部分逻辑了。
这部分逻辑说白了就是要在分布式系统中维护一个一致状态,举个例子,可以将“谁是leader”这个概念当作一个状态量,由分布式系统中的身份相等的几个节点共同维护,既然谁都有可能修改这个变量,那究竟谁的修改才奏效呢?
幸好,针对这种常见的问题情景,我们有现成的基础设施抽象可以解决。
这种基础设施就是分布式系统的协调器组件(coordinator),老牌的有zookeeper(zab),新一点的有etcd(raft)。这种组件通常没有重复开发的必要,像paxos这种算法理解起来都得老半天,实现起来的细节数量级更是难以想象。因此很多现成的开源项目都是依赖这两者实现高可用的,比如codis就是用的zk。
zk解决了什么问题?
就我们的游戏服务端需求来说,zk可以用来选leader,还可以用来维护dbClient的配置数据——dbClient直接去找zk要数据就行了。
zk的具体原理我就不再介绍了,具体的可以参考lamport的paxos paper,没时间没精力的话搜一下看看zk实现原理的博客就行了。
简单介绍下如何基于zk实现leader election。zk提供了一个类似于os文件系统的目录结构,目录结构上的每个节点都有类型的概念同时可以存储一些数据。zk还提供了一次性触发的watch机制。leader election就是基于这几点概念实现的。
假设有某个目录节点/election,watcher1启动的时候在这个节点下面创建一个子节点,节点类型是临时顺序节点,也就是说这个节点会随创建者挂掉而挂掉,顺序的意思就是会在节点的名字后面加个数字后缀,唯一标识这个节点在/election的子节点中的id。
一个简单的方案是我们可以每个watcher都watch /election的所有子节点,然后看自己的id是否是最小的,如果是就说明自己是leader,然后告诉应用层自己是leader,让应用层进行后续操作就行了。但是这样会产生惊群效应,因为一个子节点删除,每个watcher都会收到通知,但是至多一个watcher会从follower变为leader。
优化一些的方案是每个节点都关注比自己小一个排位的节点。这样如果id最小的节点挂掉之后,id次小的节点会收到通知然后了解到自己成为了leader,避免了惊群效应。
还有一点需要注意的是,临时顺序节点的临时性体现在一次session而不是一次连接的终止。例如watcher1每次申请节点都叫watcher1,第一次它申请成功的节点全名假设是watcher10002(后面的是zk自动加的序列号),然后下线,watcher10002节点还会存在一段时间,如果这段时间内watcher1再上线,再尝试创建watcher1就会失败,然后之前的节点过一会儿就因为session超时而销毁,这样就相当于这个watcher1消失了。解决方案有两个,可以创建节点前先显式delete一次,也可以通过其他机制保证每次创建节点的名字不同,比如guid。
至于配置下发,就更简单了。配置变更时直接更新节点数据,就能借助zk通知到关注的dbClient,这种事件通知机制相比于轮询请求sentinel要配置数据的机制更加优雅。
我在实现中将zk作为路由协议的一种整合进了Phial规范,这样基于zk的消息通知可以直接走Phial的RPC协议。
有兴趣的同学可以看下我实现的zkAdaptor,leader election的功能作为zkAdaptor的特殊API,watcherService会直接调用。而配置下发直接走了RPC协议,集成在统一的Phial.RPC规范中。zkAdaptor仅支持Phial.RPC中的Notify pattern。
watcher的实现在这里。
5.总结目前形成的架构以及能做什么