休闲回合制游戏架构相关技术实现

前言

以前做过的休闲回合制游戏的用的分层式分布式架构(只介绍分服/区域服架构)

这章主要来聊下这个分层式分布式架构(支持20W CCU,DAU 过100W)

本章 讲述的相关技术实现

1:基本架构

基本的业务服务 都支持水平伸缩

需要就近接入 参数前面2章相关内容, 增加anycast eip, 防攻击 长链接的gateway 前也加高仿LB

全局调度/配置中心 、资源使用情况及有限查询(少量可以Grafana 直接查db,其他参考后面的) 单独列出

日志查询 搜索 检索及落库(需要增加kafka,ES等)

链路日志,一般不需要落库(根据实际业务 决定用不用Jaeger)

角色上线加载及落库过程

2:层次

1>接入层(access layer)

高仿LB

gateway(短链接,长连接)

2> 逻辑层(logic layer)

(1)账号与登录

login

(2)玩家核心业务

hall game match gamemanager

(3)运营与资金

gm server payserver

3> 公共服务层(common service layer)

(1)通用业务支撑

datasync 数据同步服务

msgwriter 消息/日志落库服务

(2)底层中间件

etcd/nacos 服务注册与发现

nacos 配置中心

kafka 消息队列

(3)平台管控服务

Global Scheduler 全局调度中心

Global config center 全局配置中心

服务发现 链路、监控、告警 限流、熔断、降级

4> 数据层(data layer)

redis mysql mongodb

clickhouse

文件(parquet/csv/json) (parquet 大数据文件,其他小数据文件)

2: 两条消息链路

1> 短链接

client<--->LB<--->gateway<--->login

2> 长链接

client<--->gateway<--->hall

client<--->gateway<--->hall<--->match<--->gamemgr<--->game

client<--->gateway<--->game

client<--->gateway<--->game--->hall--->gateway

client<--->gateway<--->hall<--->payserver

3:登录过程

重点:

IP 限流(防暴刷、防攻击)

黑名单 + TTL(超限直接拉黑一段时间)

Redis 全局防重复校验(同一个 token 只让一台网关去第三方)

Redis 缓存结果(有效 / 无效)+ 短 TTL(避免重复验)

1>高仿LB 自带监控

带宽、连接数、QPS、丢包、健康检查状态等都有。

2>gateway (无状态)主要是 做所有流量控制、熔断、限流、黑白名单、鉴权、路由

需要共享的数据可以通过redis 定时拉取/订阅更新

3>login(无状态) 处理登录校验 等待型

失败:给客户端相应消息

成功:选择 一个合适的 gateway 及TOKEN 给客户端(其他account,ACCOUNTID)

4>防攻击

无效token 加入缓存(类试防缓存穿透),单位 时间内 N个TOKEN 请求登录,加入black单,限制登录(N时间)

4: 进入游戏

slot id<---> hallid eg:1<--->hall1 2<--->hall1 3<--->hall1 10000<--->hallN

accountId % 16384 = 槽号 → hallId → ip+port

重复登录,挤号 ,换角色 只需要在同一个hall上处理就可以了,比用userid 方便

槽号 对应 hall, 这个可以用redis 主从+哨兵 (用redis话,最好单独,不用跟任何过期策略的混搭),也可以自己写 更简洁更稳,就是一点,高可用,不需要存档,

故障转移或恢复后 可以由全局调度中心 处理

踢人 挤号 redis 及hall 大厅缓存里各存一份 db也留一份, gateway 产生uuid(「服务类型 + 机器 ID + 时间戳 + 自增」) 关联 socket

key: online:{account_id}

value:

user_id: yyy # 你的游戏角色ID

hall_id: zzz # 大厅服ID

gateway_id: ggg # 网关ID

session_uuid: sss # 踢人用

踢人 通过hall 发消息 到指定gatway 踢掉UUID的socket链接

同时 可以保存玩家数据

挤人 ,如果是同一个角色,只需要 更新gatewayid session_uuid,把当前玩家数据给新链接发一份就行, 不是同一角色,就是踢人,保存数据,拉新角色数据.

5:角色上线加载及落库过程

上线加载 先查redis 没有 查mysql ,查到回写redis

落库过程 都用lua执行保持原子性,先更新redis 及脏标志位,版本号,把uid 扔 set(去重) 及 list(先后顺序),datasync 从list 取uid, 根据脏标志位去redis 查询 user_base,user_task,user_bag 等 拉取并清除脏标志位及set 去掉uid

最后写入库

6:日志查询 搜索 检索及落库

按流程来就可以了,写得很明白了,kafka 一般5个topic(充值,玩家行为,全链路,服务日志,warn/error)

充值 玩家行为 写本地文件 进入es 同时落库

全链路,服务日志,warn/error 写本地文件 并进ES

7:全局调度/配置中心 单独列出

注册与发现 可以基于etcd sdk(eg:go zero),配置中心 可以基于nacos sdk(eg:go zero) 进行开发 最少是主从

调度中心几乎连所有:业务服务 + DB、Redis、Kafka、ETCD 全都管;

配置中心只连业务,不连基础中间件(除了ETCD,NACOS)

ETCD 服务注册与发现

NACOS 配置中心

只作有限日志查询 及prometheus拉取的数据

看服务的资源及业务指标(提供接口供prometheus拉取)

8:全链路tracer

写本地文件->filebeat->kafka->Logstash->es->AlertManager(告警系统)

es->Grafana

1>正常采样

一般1%--5% 采样

2>核心链路

全量采样

3>异常、错误、超时

某节以后 全量采样 主要给AlertManager(告警系统)

单独topic

traceID 64 bit

机器 ID:8 位(0~255)

业务类型:4 位(0~15)

时间戳:34 位(秒级 / 毫秒级都能用几十年)

自增序号:18 位(最多 26 万 / 毫秒)

spanId 用「服务类型 + 机器 ID + 时间戳 + 自增」

一条链路日志,必须有的最小字段

生产可用、排查问题必带的 5 个:

traceId整条链路唯一 ID,网关生成,全程不变。

spanId当前这段调用的 ID,每段新生成。

parentSpanId父调用的 spanId,用来串成调用树。

serviceName / module哪个服务 / 模块打的日志。

timestamp + level + message时间、日志级别、内容。

简单的 可以自行 搞定

复杂的用Jaeger 更稳妥

业务 OpenTelemetry SDK → Jaeger Agent → Jaeger Collector → Kafka → Jaeger Ingester → ES → Jaeger Query/UI

1)采集层(游戏服侧)

游戏后端:接入 Jaeger /OpenTelemetry SDK

每台游戏服起一个 Jaeger Agent(Sidecar / 本机进程)

Agent 做:批量上报、UDP/grpc 接收、限流

2)收集层

集群:Jaeger Collector 3~5 实例

作用:校验、格式化、写入 Kafka

不直接写 ES

3)缓冲层(关键)

Kafka Topic:jaeger-spans

建议分区:12~24 个(看 ES 写入能力)

保留 1~3 天即可

4)入库层

Jaeger 自带 ingester 组件(从 Kafka 读 → 写入 ES)

可多实例水平扩展

5)存储 & 查询

存储:Elasticsearch(6.8/7.x 最稳)

查询:Jaeger Query

UI:Jaeger UI

9:角色数据 redis 手动LRU

角色数据实时同步到redis, 通过datasync 异步 进程 ,多改合一/增量更新 同步到DB

把角色信息 N张表 因为每次更新,只会更新关联的表,所以不能全量存一起

但是角色信息 又是一个整体,要么全在,有么全不在,不能出现部分在的情况

user:base:{uid}

user:bag:{uid}

user:task:{uid}

。。。。。。。

1> 用ttl过期的问题

如果用ttl 过期,要么更新任何一张表,其他表的TTL 也同步更新,不然会出现 部分表过期的问题

2>解决方案

玩家所有模块 key(不过期)

一个 ZSet:

key = user:lru:zset

score = 最后更新时间戳

member = uid

ZADD user:lru:zset 1736947200 10001 自动覆盖

清理为用LUA 删除角色所有的表,必须是原子

(1)时间策略(兜底) 假定ttl 为24小时

只要玩家最后更新时间戳 超过 24 小时(86400 秒)

不管内存够不够,直接清理

目的:防止冷数据永远占内存

(2)内存策略(紧急)

Redis 内存占用 > 70%(70%可以自行调整,这里只是推荐)

不管有没有到 24 小时

直接从 ZSet 拿最老的一批清理

目的:防止 Redis 爆掉

3> 基础数据里增加version 防止倒灌

版本号只有一个,全局唯一

存在基础数据里

任何子模块(背包、任务、成就)改动

→ 都让 version++

全局单版本号,任何改动必自增;

同步只许高版本覆盖低版本,永不回写。

10: match 匹配规则

按星级 比如 1-10 match1* 处理 11-20 match2* 处理 21-30 match3* 处理 以正负2星 匹配4人队伍

星级 范围 按实际情况处理

hall->match set uid list uid

eg:1-10星 直接扔match11 12 *** match1* 最小2个

match11 list len<300 处理 1-299 数据

match12 list >=300 处理 300-599 数据 当数据长度 小于300,空闲,watch match11的健康状态,故障时,接手match11 从1-599 全处理

规则 接手自身前一个点的故障点,match11 match12 match13 match11 环形 逆时针
类试 一致性哈希(环 + 顺时针) + 虚拟节点(每台机器配多个)

match12 故障时 match11 从1-599 全处理

规则 watch 自身 前个的实例及前实例接管的实例

组队

从list 拿出N个 uid ,以第一个作为基点,以lua 方式执行 组对,判定 是否在线,星级等

找到,删除队成员UID,同时删除set 成员ID,通知gamemgr 创建房间 ,返回给hall 房间相关信息,再通知客户端

11:如果觉得有用,麻烦点个赞,加个收藏

相关推荐
SmartBrain2 小时前
FastAPI实战:基于 SQLAlchemy的后端接口开发流程
数据库·架构·fastapi
风痕天际3 小时前
Godot扫雷游戏制作记录2——鼠标交互
游戏·游戏引擎·godot
Tadas-Gao3 小时前
校准之弧与演进之轮:大模型时代的软件工程范式革命
人工智能·深度学习·架构·大模型·llm·软件工程
岱宗夫up3 小时前
FastAPI进阶3:云原生架构与DevOps最佳实践
前端·python·云原生·架构·前端框架·fastapi·devops
老迟聊架构4 小时前
你的程序应该启动多少线程?
后端·架构
Tadas-Gao4 小时前
微服务注册中心选型深度分析:Eureka、Nacos与新一代替代方案
java·分布式·微服务·云原生·eureka·架构·系统架构
无心水4 小时前
【任务调度:数据库锁 + 线程池实战】2、MySQL 8.0+ vs PostgreSQL:SKIP LOCKED 终极对决,谁才是分布式调度的王者?
java·人工智能·后端·面试·架构
RFG20124 小时前
18、Dubbo实例注入:简化微服务架构中的依赖管理【面向初学者】
人工智能·后端·微服务·云原生·架构·tomcat·dubbo
笨蛋不要掉眼泪4 小时前
Spring Cloud Alibaba Sentinel 从入门到实战:微服务稳定性的守护者
分布式·微服务·云原生·架构·sentinel