游戏行业有一种比较特殊的业务机制需求,叫「跨服」。
这种机制,常见于分区分服类游戏(与之对应的是一些全区全服类游戏,比如COC)。
为了便于非游戏行业的同学理解,小说君抽象一下问题。
现在假设我们部署了两组业务,简化的架构图如下:
两个特点:
不同业务的数据独立,相互隔离。
用户可以访问不同的业务,数据无法共享。
那所谓「跨服」,其实就是不同组业务的各种服务可以产生交互。像这样:
图中的「Hub」,只是一个抽象出来的概念。
实践中,我们当然也可以让不同组业务之间做直连,就可以省去「Hub」这么个抽象。
但是,随着业务复杂度增加,这样的做法可维护性越来越差。
因此,我们通常会在架构中引入「Hub」。
可以自己实现一套,也可以借助成熟的消息中间件,还可以用云服务提供商提供的基础设施。
三种方案孰优孰劣,小说君就不再在本文做探讨。现在假设我们自己实现了一套「Hub」。
设计过程按下不表,我们直接看架构图:
每组业务有一个local message hub,承担着各组业务内部各服务之间的消息转发职责。
同时,不同业务的local message hub互联成网,构成一组stateful service。
背景介绍完毕,我们聚焦下要讨论的问题。
现在有这样一组stateful service。
两个特点:
服务的每个实例,都会暴露各自的location,比如host/port。
服务实例的数量和location会动态变化。(比如下线、迁移等)
接下来,我们就可以引入本篇文章的主题了——服务发现。
何谓服务发现?
在这个例子中,不同组业务的部署时间一定不是同时的,而且先后顺序也不是确定的,因此每组业务的Hub上线时间是动态的,location也是动态的。
当我们部署了一组业务,该组业务的Hub上线时,其他已经上线的Hub如何获取到这个信息,以后新上线的Hub如何追溯到这个信息,所依赖的机制,就是服务发现(Service Discovery)。
接下来看看如何实现。
首先我们来看一种最直观的实现方案。
这种方案的核心原则是定义这组stateful service中的静态节点,比如说一定会最先启动的Hub0。然后每组后续启动的业务都配置有Hub0的信息。
如此一来,服务发现的流程如下:
最新启动的Hubk,主动连接定点Hub0。
连接成功向Hub0登录,拿到当前所有已登录Hub,S。
Hubk向S发起连接。
Hub0向S推送Hubk的信息。
S中各Hub收到推送后可以向Hubk发起连接(Hub网两两之间有两条物理连接),也可以仅更新本地维护的集群状态(Hub网两两之间有一条物理连接)。
方案实现的比较简单朴素,问题也很多。
最大的问题有两个:
Hub0成了集群中的单点。
随着Hub网络规模增加,Hub之间的非业务通信量会越来越大(登录、通知、心跳等协议)。
我们先看第一个问题。
单点问题(single point of failure)在我们这个示例系统中有这样几点影响:
解决单点问题有两种思路。
第一个是比较通用的,引入第三方的高可用data store,比如之前小说君的基于redis构建高可用数据服务一文中所用的zookeeper,其他的选择还有etcd和Consul。
这样,我们可以放心地把这个高可用data store作为静态点,每个Hub的地位是平等的,启动了向data store注册自己,同时查询现有Hub列表,再构建成网。
引入第三方组件,可以高效、优雅地解决问题。
但是也有坏处,比如会增加部署、运维成本,增加网络拓扑结构复杂度等等。
那如果我们由于各种各样的原因,无法采用这种方案的话,只有转而看第二种解决问题的思路。
在最朴素的实现方案中,Hub0与Hubk的地位是不对等的,Hubk想加入集群,必须找Hub0注册。
而在第一种方案(第三方高可用组件)中,Hubk想加入集群,只需要找第三方高可用组件注册。因此Hub0和Hubk的地位变得平等了。
那既然第二种方案不能依赖第三方高可用组件,我们就必须让每个Hub都有注册的能力,但是同一时刻只有一个Hub提供注册服务。
这个事实——目前哪个Hub提供注册服务,可以抽象为分布式系统中的一个状态量。
所以思路就很清楚了,我们可以引入分布式一致性协议来解决单点问题。
做法也就两种,一是把现成的协议实现集成到项目中,二是自己按paper实现一遍。
首先挑一个分布式一致性协议,接下来我们就以raft为例。
raft对这类需求是相当友好的,一方面是协议简单易实现,另一方面是模型易理解(log+leader election)。
raft把分布式系统要维护一致性的状态,用复制状态机(replicated state machine)描述:
图来自raft extended。
log => status的转换不难理解。
假设我们集成了raft,现在集群的启动流程和后续的服务发现流程就变成了这样:
定义一组静态的种子Hub,后续部署业务的Hub都需要先找这组Hub尝试连接。
启动种子Hub。根据raft确定的选主规则,这组Hub中会竞争出一个合法Leader。
新启动一组业务以及对应的Hubk,向种子Hub发起连接。
这样,单点问题就成功地通过高可用化解决了。
至于第二个问题,业务数据同步的通信量会随节点数量增加而指数级增加,其实跟本文的主题并不是特别相关。
解决方案也有很多种,比较简单的就是引入类似gossip之类的协议,平滑同步数据量。
详细内容等有机会再写一篇专门聊聊这个话题。
服务发现的实现,其实并不仅仅是「引入分布式一致性协议」这么简单,还有很多种业务特定的corner case需要考虑。
而且,更多时候,业务对可用性的需求其实并不太强烈,一开始可以用最简单的单点模型run起来,当然,不扩展不意味着不能扩展,否则就是丢下来的一大笔技术债。
个人订阅号:gamedev101「说给开发游戏的你」,聊聊服务端,聊聊游戏开发。