一个高可伸缩的游戏服务器架构
原文连接:http://blog.gotocoding.com/archives/827
设计完socket通讯协议后,就面临着服务器架构设计了。我希望他是一个去中心化且具有高可伸缩性的集群架构。
水平扩展是高可伸缩的首要条件,因此,在设计之初就必须考虑好水平扩展考方案。事实上这一部分几乎花了我1整个月的时间来设计,在此期间我重写了3版才总算确定下来我认为可用的方案。
第一版设计方案如下:
将服务器分为3类,分别是GateServer, LoginServer, LogicServer。
GateServer管理客户端链接,数据包的加密、解密、广播、转发等与业务逻辑无关的操作。当压力过大时可通过部署多个实例来水平扩展。
LoginServer处理游戏帐号认证,为客户端分配一合适的GateServer(可能是负载最轻),为客户端与GateServer连接分配临时密钥等操作。
客户端通过连接LoginServer分配的GateServer来进行游戏。如果需要限制玩家单人登陆(同一个帐号同时只能有一个socket来管理), 则只能部署一个,如果压力过大,可做登陆排队处理。
LogicServer是游戏业务逻辑服务器,可根据业务类再行分类。每个业务类型服务器可单独部署一份。
每一个LogicServer在启动时向GateServer建立一条socket连接。并把自己可处理的协议ID发送给GateServer进行注册。
当GateServer收到客户端协议ID后,根据LogicServer注册信息来将不同的协议内容转发给不同的LogicServer服务器处理。
LogicServer接收GameServer转发来的协议后,将处理结果发回源GateServer,再由GateServer处理后发回给客户端。
LogicServer之间根据业务模型的需求直接进行互联。
例如:有一个RoleServer(LogicServer类型)进程和一个SceneServer(LogicServer类型)进程,如果SceneServer在业务逻辑中需要RoleServer提供一些支持。那么SceneServer直接对RoleServer进行连接并请求,不需要任何中心服务器结点。
很容易发现,这个架构的瓶颈一定是在LogicServer的定位上。假如单个RoleServer不足以承载足够多的人,而RoleServer内部的逻辑又交互很密切,RoleServer所承载的最大人数将是整个架构的所能承载的最大人数。这严重制约了整个架构的伸缩性。
因此,想要提高整个架构的伸缩性,就必须要让”同一业务类型服务器”可以部署多个实例。
在第二版的设计中,LogicServer向GateServer注册协议ID时,顺便通知GateServer其本身是否有可能会被布署多份实例。
GateServer在向LogicServer转发协议时根据其是否’可能会被被部署’来做不同的处理。如果此LogicServer是可能会部署多份的,则用hash(uid)的值来确定将此协议内容转发到具体哪一个LogicServer服务器。
事情往往没有看上去那么美好,在实现SceneServer时,发现上述规则并不适用。因为SceneServer如果部署多个实例,一定是按地图区域划分的,与hash(uid)没有必然联系。如果要对SceneServer进行正确的消息转发就必须要新增一种LogicServer的子类型。
同样,如果新增某个业务逻辑服务器需要另外一种转发逻辑,就需要同时修改GateServer的转发逻辑。这与框架与业务逻辑解耦的初衷不符。
看起来已经不可能完全保证,在业务逻辑变动的情况下,完全不修改GateServer的代码了。因此在第三次实现中,我把转发逻辑独立出来,交由业务逻辑处理。
在GateServer中增加了一个元素Agent。框架本身不提供Agent的实现,只提供Agent类的接口。具体的Agent由业务逻辑实现。
在每一个连接到来时,GateServer为其分配一个Agent对象。当客户端消息到来后,GateServer将其交由对应的的Agent对象处理,GateServer不再负责具体的转发逻辑。由此来将业务逻辑代码彻底剥离开来。
假设整个集群部署如下:
有一个GateServer, 两种不同的业务类型的LogicServer。每种LogicServer分别部署N份实例
+———————–+ +————-+ +————-+
| | | | | |
| Gate | | | | |
| | | | | |
| +——-+ +——-+ | | | | |
| | | | | | | | | |
| | Agent | | Agent | | | | | |
| | | | | | | | | |
| +——-+ +——-+ | | LogicServer | | LogicServer |
| | | | | |
| +——-+ +——-+ | | Role x N | | Scene x N |
| | | | | | | | | |
| | Agent | | Agent | | | | | |
| | | | | | | | | |
| +——-+ +——-+ | | | | |
| | | | | |
+———————–+ +————-+ +————-+
那么从业务逻辑层面来看,其实就相当于每一个Agent对象分别包含了一个Role Server实例和一个Scene Server实现。如下图:
+—————————–+
| |
| Agent |
| |
| +———————-+ |
| | | |
| | Role x 1 | |
| | | |
| +———————-+ |
| +———————-+ |
| | | |
| | Scene x 1 | |
| | | |
| +———————-+ |
| |
+—————————–+
整个集群一种可能的工作流程是这样的:
帐号认证过程(LoginServer部分):
客户端连接LoginServer进行认证,LoginServer首先检查客户端认证信息是否合法。
如果合法接着检查此帐号是否已经在线,如果在线,则找到此帐号在线的GateServer,并向其发着kick命令。
GateServer向LoginServer回应kick成功
LoginServer为当前帐号分配GateServer,向GateServer请求为此uid生成一个合法token。
LoginServer将GateServer的IP和Port及token返回给客户端
游戏过程(GateServer部分):
客户端拿着从LoginServer获取到的ip和token去连接GateServer并进行认证。
GateServer收到新的客户端连接就为其新建一个Agent对象,此后便将此连接所有消息都效由Agent对象处理。
GateServer收到LogicServer发来的消息后,根据此消息所属的uid找到对应的Agent来处理,然后把消息交由Agent来处理。
游戏过程(Agent部分):
- 收到GateServer传递过来的由客户端发来的消息,找到其对应的服务器类型,然后根据此服务器类型需要的转发逻辑来转发到相应的LogicServer中去处理。
- 收到GateServer传递过来的由LogicServer发来的消息,将其转发给对应的客户端连接
上述过程只是一个整体上的过程,有很多细节都没有详述。比如GateServer可能把消息解密再传递给Agent处理, LoginServer与GateServer可能还需要交换密钥等。
BTW, 处理多连接绝对不是一件容易的事,在第三版方案确定好,又重写了两次才终于把逻辑理顺。