游戏服务器架构的一些思考
背景
想写这篇文章挺久了,但是一直发现自己的技术视野好像并不是很广,所以一直没有动手,之前在公司内部写了一次后续我司的新项目的架构设计,发现自己的技术视野还是有所提升的,所以就想着写一篇关于游戏服务器架构的文章,来总结一下自己的一些想法。同时也希望可以验证一下自己的想法。
接触游戏服务器开发的时间不算短,也不是很长,从21年1月份入职呸喽到写这篇文章24年12月末,堪堪过去了将近4年。相比起其他重度游戏来说,我司的业务和服务器架构跟互联网业务其实差别不大,甚至相对我个人来说,我司的业务更像是一个互联网业务,只不过是在游戏行业而已。
场景
- 对于我司现在的业务而言,可以理解为一个全区服的游戏,也就是说,当前所有的玩家都在一个业务服务器上进行游戏。并不会去做分服的操作。
接下来我会先画一个简单的架构图,然后再对每个部分进行详细的分析。
架构说明和设计
系统整体架构图
- 这是系统整体的架构图。从图里可以看到的是,我们的用户其实分为3类:
- 游戏客户端
- 管理后台
- 以及部分对三方开放的OpenAPI接口
这三类用户的请求都会经过我们的连接层,然后再根据不同的请求路径,转发到不同的服务上。
游戏客户端会建立长链接,然后通过长链接对战服务进行对战,同时也会通过长链接 Comet 服务进行消息推送。
游戏客户端的http请求,管理后台和OpenAPI接口则会通过API Gateway服务进行转发。
连接层
这一层主要是负责处理用户链接,作为用户请求的入口。
这一层里在图里有一个比较奇怪的服务,长链接对战服务。它既是属于连接层,也是属于游戏逻辑层。这一部分我会单独拿出来讲。
我们这里先讲一下我们的连接层的功能和作用:
- 首先
长链接 Comet
服务,主要负责托管用户的长链接和用户非业务性的心跳检测,断线重连等功能。并且这一层还会负责服务器通知消息的推送。 - 其次是
API Gateway
服务,这一部分负责对外界的 http 请求进行转发,同时也会负责一些简单的鉴权和限流的功能。- 这一服务主要面向的对象就是 client 的 http 请求和admin以及OpenAPI的请求。
- 会通过对不同请求路径的判断,将请求转发到不同的服务上。
- 最后就是
长链接对战服务
,这一部分是我们的游戏逻辑层,但是也算是连接层的一部分。因为为了保证游戏的实时性,我们会将用户的对战逻辑放在这一层,这样可以保证用户的对战逻辑不会受到多次网络转发的影响。- 这一层我们会做的事情就是:房间管理,用户对战的逻辑。
- 因为需要直接跟客户端进行长链接(TCP,KCP,ws)通信,所以这一层同时也算是连接层。
服务层
这一层基本都是无状态服务,可以随意扩容,也可以随意缩容。
- 服务如何切分这一类的东西就不详细说了,因为这一部分仁者见仁智者见智,不同的业务会有不同的切分方式。说了也没什么意义。
- 要记住一点:这一层的服务都是无状态的,可以随意扩容,也可以随意缩容。
- 这一层的每个服务都会有自己的数据库,这样可以保证服务之间的数据不会相互影响。基本会公用一个消息队列,用于服务之间的消息广播。
- 同步的消息调用我们是直接用的grpc。通过自己的服务发现机制,可以直接调用到对应的服务。
数据层
这一层主要是负责数据的存储和读取。
- 数据库我们会使用PgSQL,Redis,MongoDB等数据库。
- 每个服务根据自己的业务需要,也会进行分库分表的操作。
- 有一些业务导致需要进行搜索的操作,我们会使用ElasticSearch来进行搜索。以及一些需要进行数据分析的操作,我们会使用ClickHouse来进行数据分析。
- 如果是游戏配置的话,我们会有一套自己的配置方案。这一部分会单独拿出来讲。
- 静态配置最终会存在OSS中,动态配置会由每个服务自己进行管理(后台处理)。
基建层
这一层主要是负责基础设施的搭建和维护。
- 基本就是一些监控,日志,告警,以及一些基础设施的搭建(在这里不是重点,所以不详细说了)。
- 对了,给大家推荐个GO 的可观测工具 阿里云开源的 Golang Agent,业务可以无侵入性的接入,可以帮助我们更好的监控我们的业务。
小总结
- 这个架构其实在分层上并没有什么特别的地方,只是在连接层上多了一个长链接对战服务。
- 接下来我会结合实际的业务场景,来讲一下我的一些设计思路。
大厅服务设计
这是我个人最喜欢的一个服务设计。个人认为,非常优雅的设计。
并且该设计也适用大部分的IM类业务。
场景说明
- 说是大厅服务,其实就是服务器推送的一个链接层服务。
- 大厅服务主要是负责用户状态的保活以及服务端的消息推送。
- 长链接只作为服务端的消息推送,不做业务逻辑。
- 例如:新邮箱,新活动,新公告等消息推送。
- 用户状态保活:其实就是用户的心跳检测和断线重连。
- 用户的其他主动请求,例如:用户的对战请求,用户的好友请求等,我们这里一律使用http请求。发送到API Gateway服务,然后再转发到对应的服务上。
先来看一下图
有过IM类业务开发经验的同学,应该对这个图有所了解。跟一个开源项目(goim)的架构设计有点类似。该架构思路也是从开源项目中学习到的。
该架构主要是为了解决长连接有状态服务导致的服务扩展性受限的问题。通过将用户状态和长链接维持分离,可以保证服务的扩展性。
连接流程:
- 用户登录后获取token,以及所能连接的服务器列表。
- 客户端通过检测延迟,选择一个最优的服务器进行连接。
- 用户连接到Comet服务,Comet服务会将用户的连接信息存储到Redis(会话服务)中,会返回一个链接ID给客户端(用来识别断线重连)。
- 客户端会定时发送心跳包,心跳包会更新用户的状态信息。
消息推送流程:
- 业务服务在需要推送消息时,会将消息发送给Logic服务。
- Logic 服务会将消息发送给消息队列。消息包含转发类型(广播/私发),接受者信息(所在的服务器,链接ID),消息内容
- Job 服务会从消息队列中获取消息,然后根据消息的接收者,将消息发送给对应的Comet服务。
优点:
- 把用户状态和长链接维持分离,保证了服务的扩展性。
- 通过后台编写,可以实现Comet扩容和流量控制。甚至可以通过协议定制,实现Comet的热更新。
缺点
- 这套流程主要是为了服务端的消息推送,对于用户主动请求的处理,还是需要通过http请求来处理。
- 所以在网络资源上会有一些浪费。
对战服务设计
对战服务是我们的游戏逻辑层,也是我们的连接层的一部分。整体架构逻辑上跟大厅服务有点类似,但是实现上有所不同。因为他不需要考虑纯粹的横向扩容。
场景说明
- 对战逻辑总共包括:匹配服务,房间(对战)服务。
- 流程基本是用户创建房间,邀请好友,开始匹配,匹配成功后开始对战。
- 房间服务基本就是一个长链接的对战服务,负责用户的对战逻辑。
看个图先
- 这个服务是跟大厅服务有点类似的,但是实现上有所不同。
- 客户端通过请求创建房间,匹配服务会按照现在各个房间服务的负载情况,选择对应的服务实例进行创建房间。
- 如果是匹配逻辑,那么匹配服务会处理匹配逻辑,用户进入匹配状态,等待匹配成功。匹配成功的消息由匹配服务通过之前的大厅服务进行消息推送。
- 在Redis中会有一些房间的状态信息:例如房间是否开放加入,房间的人数等。
按权负载均衡
- 对于长链接服务而言,负载均衡并不想无状态请求一样简单。因为长链接服务是有状态的,所以我们需要按权负载均衡。
- 我们将房间业务(长链接)的一些指标,例如:房间的人数,房间的负载等信息,通过一个权重算法,计算出一个权重值,然后根据这个权重值,返回一个服务实例。
- 这样可以保证服务实例的负载均衡。不会导致某个服务实例的负载过高。
服务间通讯
在跨服务调用时,根据不同的业务场景,我们会有不同的调用方式。同步调用和异步通知。
- 同步调用:主要是在需要立即返回结果的场景下使用。例如:用户登录,用户注册,获取用户信息等。
- 异步通知:主要是在不需要立即返回结果的场景下使用。例如:用户的对战结果,用户的消息推送,用户的好友请求等。
同步调用
强一致性的场景下,我们会使用同步调用。我们会使用grpc来进行同步调用。通过服务发现机制,可以直接调用到对应的服务。进行对应服务的调用操作。
异步通知
异步通知主要是通过消息队列来实现的。我们目前是使用的NSQ作为服务内的消息队列。
举个例子:用户的游玩结算逻辑,基本就是一个异步通知的场景。游玩结束后,会将游玩结果发送到消息队列中,然后对应的排行榜服务,任务服务等会从消息队列中获取消息。排行榜服务会根据游玩结果,更新用户的排行榜信息。任务服务会根据游玩结果,更新用户的任务信息。
总结
- 架构设计没有银弹,只有适合自己业务的架构。
- 本文还有挺多东西没有详细提到的,例如分库分表,服务发现,服务治理等等。
- 感觉详写出来,需要扩展的东西还是挺多的,但是这里只是一个简单的架构设计,希望可以一起讨论一下。