专栏名称: 说给开发游戏的你
游戏开发原创文章分享,游戏圈面试指南,游戏开发问题探讨。内容不限于Unity/UE开发,图形学基础,AI,服务端架构,面试题解惑等等。现猪厂、前鹅厂码农个人维护,诚意分享,欢迎关注。
目录
相关文章推荐
独立出海联合体  ·  Switch销量突破1.46亿份,传二代主机 ... ·  2 天前  
独立出海联合体  ·  Switch销量突破1.46亿份,传二代主机 ... ·  2 天前  
黑马程序员  ·  揭秘英雄联盟游戏背后的关键技术! ·  4 天前  
黑马程序员  ·  揭秘英雄联盟游戏背后的关键技术! ·  4 天前  
51好读  ›  专栏  ›  说给开发游戏的你

从「跨服」到服务发现

说给开发游戏的你  · 公众号  · 游戏开发  · 2017-04-10 20:44

正文

游戏行业有一种比较特殊的业务机制需求,叫「跨服」。

这种机制,常见于分区分服类游戏(与之对应的是一些全区全服类游戏,比如COC)。


为了便于非游戏行业的同学理解,小说君抽象一下问题。


现在假设我们部署了两组业务,简化的架构图如下:

两个特点:

  1. 不同业务的数据独立,相互隔离。

  2. 用户可以访问不同的业务,数据无法共享。


那所谓「跨服」,其实就是不同组业务的各种服务可以产生交互。像这样:

图中的「Hub」,只是一个抽象出来的概念。

实践中,我们当然也可以让不同组业务之间做直连,就可以省去「Hub」这么个抽象。

但是,随着业务复杂度增加,这样的做法可维护性越来越差。


因此,我们通常会在架构中引入「Hub」。

可以自己实现一套,也可以借助成熟的消息中间件,还可以用云服务提供商提供的基础设施。


三种方案孰优孰劣,小说君就不再在本文做探讨。现在假设我们自己实现了一套「Hub」。


设计过程按下不表,我们直接看架构图:

每组业务有一个local message hub,承担着各组业务内部各服务之间的消息转发职责。

同时,不同业务的local message hub互联成网,构成一组stateful service。




背景介绍完毕,我们聚焦下要讨论的问题。

现在有这样一组stateful service。

两个特点:

  1. 服务的每个实例,都会暴露各自的location,比如host/port。

  2. 服务实例的数量和location会动态变化。(比如下线、迁移等)


接下来,我们就可以引入本篇文章的主题了——服务发现。




何谓服务发现?


在这个例子中,不同组业务的部署时间一定不是同时的,而且先后顺序也不是确定的,因此每组业务的Hub上线时间是动态的,location也是动态的。


当我们部署了一组业务,该组业务的Hub上线时,其他已经上线的Hub如何获取到这个信息,以后新上线的Hub如何追溯到这个信息,所依赖的机制,就是服务发现(Service Discovery)。


接下来看看如何实现。




首先我们来看一种最直观的实现方案。


这种方案的核心原则是定义这组stateful service中的静态节点,比如说一定会最先启动的Hub0。然后每组后续启动的业务都配置有Hub0的信息。


如此一来,服务发现的流程如下:

  1. 最新启动的Hubk,主动连接定点Hub0。

  2. 连接成功向Hub0登录,拿到当前所有已登录Hub,S。

  3. Hubk向S发起连接。

  4. Hub0向S推送Hubk的信息。


S中各Hub收到推送后可以向Hubk发起连接(Hub网两两之间有两条物理连接),也可以仅更新本地维护的集群状态(Hub网两两之间有一条物理连接)。


方案实现的比较简单朴素,问题也很多。


最大的问题有两个:

  1. Hub0成了集群中的单点。

  2. 随着Hub网络规模增加,Hub之间的非业务通信量会越来越大(登录、通知、心跳等协议)。




我们先看第一个问题。

单点问题(single point of failure)在我们这个示例系统中有这样几点影响:

  • 假如单点挂掉,在重启之前,新业务无法与已启动的业务建立联系。

  • 每组业务启动时要指定静态的host/port,供各自的Hub连接单点。


解决单点问题有两种思路。

第一个是比较通用的,引入第三方的高可用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,现在集群的启动流程和后续的服务发现流程就变成了这样:

  1. 定义一组静态的种子Hub,后续部署业务的Hub都需要先找这组Hub尝试连接。

  2. 启动种子Hub。根据raft确定的选主规则,这组Hub中会竞争出一个合法Leader。

  3. 新启动一组业务以及对应的Hubk,向种子Hub发起连接。

    • 如果连上的Hub是Follower,就直接能拿到当前Leader的location,再连接。

    • 如果连上的Hub是Leader,登录,拿到当前活跃的Hub集合。当然,向Leader报告的同时,Leader 还要利用raft的joint consensus机制,运行时修改cluster membership(增加了一个节点)。


这样,单点问题就成功地通过高可用化解决了。




至于第二个问题,业务数据同步的通信量会随节点数量增加而指数级增加,其实跟本文的主题并不是特别相关。


解决方案也有很多种,比较简单的就是引入类似gossip之类的协议,平滑同步数据量。

详细内容等有机会再写一篇专门聊聊这个话题。




服务发现的实现,其实并不仅仅是「引入分布式一致性协议」这么简单,还有很多种业务特定的corner case需要考虑。


而且,更多时候,业务对可用性的需求其实并不太强烈,一开始可以用最简单的单点模型run起来,当然,不扩展不意味着不能扩展,否则就是丢下来的一大笔技术债。





个人订阅号:gamedev101「说给开发游戏的你」,聊聊服务端,聊聊游戏开发。