写了一个卡牌游戏框架
内容提要
作者开发了一个卡牌游戏框架,源于2018年面试时对合服方案的思考。框架将进程分为agent和gameplay,通过共享数据库和etcd管理节点,支持跨服玩法和动态迁移。核心是逻辑单元调度,类似K8s,解决跨服数据一致性和资源回滚问题,并支持热更新。
延伸解读
从合服到跨服的架构演进
作者从2018年面试中提到的Tag合服方案出发,结合自身经历,发现本服玩法扩展为跨服玩法时,因网络层和数据库差异导致代码难以复用。因此提出将本服玩法视为跨服的特殊情况,通过共享数据实现跨服,从而避免重复开发。这一思路也自然支持合服,体现了架构设计的前瞻性。
进程模型与连接管理
框架将进程分为agent、gameplay和social三类,分别处理玩家私有数据、交互玩法和社交关系,并限制同类进程不互联以避免连接爆炸。但后来发现限制过于僵化,改为允许互联,并采用惰性连接和连接仲裁机制解决连接管理问题。这种设计借鉴了Erlang的进程抽象,提高了灵活性。
调度引擎与热更新能力
框架借鉴K8s的调度思想,将业务单元抽象为逻辑单元,通过cluster.call实现跨进程调用,并支持动态迁移和热更新。热更新通过暂存消息、落地数据、加载新代码实现,但需注意正在处理的rpc.call和timer函数,框架提供了包装函数确保安全切换。
跨服一致性与资源回滚
针对跨服玩法中资源扣除后操作失败的回滚问题,框架利用共享DB集群提供user_mq抽象,先扣资源并记录数量,失败时通过消息队列返还。这种方式避免了传统回滚的缺陷,也防止了刷资源漏洞,适用于活动结算、充值发货等场景。
Q&A
这个卡牌游戏框架的起源是什么?
框架起源于2018年面试时,面试官提到的合服方案:只需打一个Tag即可完成合服。作者当时不以为然,但后来在工作中遇到跨服玩法开发困难,重新思考后意识到该方案的价值,并在此基础上发展出这个框架。
这个框架的核心设计思路是什么?
核心是将进程分为agent和gameplay两类,agent处理玩家私有数据,gameplay处理交互性玩法。所有进程共享一个数据库集群,通过etcd管理节点,支持跨服玩法和动态迁移。逻辑单元通过cluster.call进行通信,框架根据进程位置决定走网络还是本地调用。
为什么选择共享数据库而不是每个服独立数据库?
共享数据库可以实现存算分离,降低成本并提高可维护性。同时,它使得跨服数据一致性和资源回滚问题更容易解决,因为所有进程可以直接访问同一份数据。
框架如何解决跨服玩法中资源扣除和回滚的问题?
框架提供了user_mq抽象。在玩家请求时先扣除资源,并将扣除数量带到gameplay,如果操作失败,通过user_mq将资源返还给玩家。这样可以避免玩家吃亏或刷资源的漏洞。
框架如何实现热更新和动态迁移?
热更新和动态迁移的逻辑相同:先暂存未处理的消息,将当前数据落地,在当前或新进程新建逻辑单元并加载数据,然后继续处理消息。也可以直接丢弃消息让发起方重试。
框架中etcd承担哪些职责?
etcd承担三个职责:配置逻辑单元部署在哪个进程节点上;配置战区(即玩家消息路由到哪个逻辑单元);节点间相互发现对方的监听地址。所有etcd操作通过controlplane代理,必要时使用两阶段提交保证一致性。
框架如何解决进程间连接爆炸的问题?
最初限制同种进程不互联,但后来放弃该约束,采用惰性连接:首次调用rpc.call时才建立连接。这样可以通过调整业务部署来减少连接数,并实现唯一连接,类似Erlang的仲裁机制。
框架中agent和gameplay的区别是什么?
agent处理玩家私有数据,如抽卡、养成,可分配到任意进程;gameplay处理交互性玩法,以战区组织,一个战区关联一个或多个游戏服。agent和gameplay通过cluster.call通信。