ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

《萌兽堂》Erlang卡牌游戏服务器源码解析与实战部署

《萌兽堂》Erlang卡牌游戏服务器源码解析与实战部署 简介《萌兽堂》是一款以卡牌对战为主题的网络游戏后端服务器使用 Erlang 编写。这份压缩包提供其完整服务器原始码面向游戏后端开发者和 Erlang 爱好者用于学习高并发、可扩展的分布式服务端架构。包内共 170 个文件体积仅 1.03MB核心是 111 个 .erl 源文件和 20 个 .hrl 头文件同时包含 .bat/.cmd 启动脚本、.config/.emakefile/rebar3 构建配置、.sql 数据库脚本、.c 接口文件等目录结构清晰便于按模块阅读。目前已有 166 人浏览学习。通过研读源码可以掌握 OTP 框架下的进程组织、消息传递、热更新与容错机制理解 Mnesia 数据库的持久化方案以及 Cowboy/TCP 网络通信的接入处理还能看到用户认证、战斗逻辑、匹配算法等游戏业务模块如何组织作者还提供了 nodetool 脚本和 tags 文件方便启动节点与代码跳转是 Erlang 游戏服务器实战中难得的参考素材。 做游戏后端这些年其实很少能看到一套卡牌游戏服务器能把完整原始码放出来。前阵子我拿到《萌兽堂》这套erlang_server花了两周从架构、协议、战斗到部署全部啃了一遍越看越觉得它对中小型卡牌项目非常有参考价值。如果你正打算拿Erlang做游戏服务器或者手上有一套老OTP代码不知道从哪下手这篇文章就是帮你把这条线整明白的。这套原始码不是那种几百万行的大厂框架它更接近一个“能跑的完整项目”网关、登录、大厅、战斗、存档都有进程模型清晰目录也不乱。我会从设计思路、源码怎么读、Linux部署、常见故障排查这几个方向逐层讲结尾再聊一聊我自己的扩展建议。整套内容尽量按照“拿到代码后第一天应该看什么、第一周应该改什么”的节奏来写适合想快速上手Erlang游戏服务器的读者。1. 为什么是Erlang先看《萌兽堂》这类卡牌游戏要满足什么1.1 卡牌游戏的业务形态决定了它不拼帧率拼状态卡牌游戏的玩家行为不是连续的坐标帧同步而是“操作-结算-广播”的离散事件。服务器不需要处理高频率位置同步却需要管理大量长连接和复杂状态账号登录、玩家数据、卡牌配置、战斗中的buff结算。这种场景下普通多线程服务不是不能做但并发模型会比较别扭每个玩家一个线程线程栈开销和锁管理会吃掉很多资源。Erlang的actor模型恰好把每个玩家、每个房间当成一个轻量进程进程之间只有消息传递没有共享内存这决定了后续写并发逻辑时几乎不用考虑死锁。我这里强调“轻量”是因为Erlang进程占内存只有几KB级别一台16G机器跑几十万玩家进程并不夸张。《萌兽堂》这类项目不一定需要那么高并发但用Erlang写卡牌服务器状态管理会清晰很多。1.2 Erlang服务器原始码的典型分层网关、逻辑、数据三件套大部分Erlang游戏服务器原始码都会按OTP Application拆成几个部分gateway负责连接接入和协议解析game逻辑层负责登录、大厅、战斗db负责数据持久化shared放公共定义和工具函数。这种分层让每部分可以独立启停、独立热更也方便运维单独压测。为什么要拆这么细核心是故障隔离。网关崩了不能影响已经进入战斗的玩家数据库队列阻塞时也不能让战斗进程卡死。如果所有代码塞进一个application后期每改一行都提心吊胆。读源码时先从app文件入手就能快速知道系统由哪些服务组成。2. 原始码里的核心模块怎么读又该怎么写2.1 读源码的入口app文件、supervisor树、路由表以《萌兽堂》这种项目为例拿到源码第一天不要急着看战斗。先打开各个.app.src确认有哪些application再找到顶层supervisor的init把进程树画出来然后去找协议路由表。协议路由表一般是一张ets或映射记录协议号到处理模块函数的映射。只要找到它就能顺着一条消息从网关注入到业务处理的完整链路。如果你自己写我建议路由表用纯map存启动时生成不要到处硬编码case。很多老源码喜欢把协议处理都写在同一个.erl文件里几千行往下堆。读的时候先用协议号后缀去搜能省很多时间等理清链路后再考虑要不要拆分模块。2.2 消息协议设计的常见做法卡牌游戏每次操作通过二进制协议传递常见封包格式是“长度协议号消息体”。二进制协议的好处是解析快、包体小也方便客户端跨平台。Erlang写二进制解析本身就是强项模式匹配一段代码就搞定decode(Len:16, Cmd:16, Body:Len/binary) - #{cmd Cmd, body Body, len Len}.这里有个细节长度字段和大端小端必须和客户端约定一致否则解析出来全是乱码。我在实际项目里还见过长度字段没算上自身两个字节导致整个流错位的案例排查了很久。协议联调阶段建议两边各写一个抓包工具把原始二进制打印出来对比。目前很多团队图方便直接用JSON我不反对网关层做JSON转换但战斗服内部通信尽量用原生term或者msgpack别用JSON。压测下来同样数据量JSON解析的CPU占用会是二进制的三到五倍。2.3 卡牌战斗状态机回合、行动、技能结算战斗核心是一个有限状态机。每个房间用一个Erlang进程玩家行为通过cast消息发进去战斗进程内串行处理避免并发修改。状态字段包括回合、行动者、手牌、战场牌、buff列表、结算队列。结算顺序是卡牌游戏最容易出问题的地方进场效果、光环、攻击伤害、死亡结算、回合结束触发等如果直接在玩家操作回调里堆if else后面一定会崩。建议学原始码里的一个做法把结算抽象成队列每个效果是一个数据对象或函数引用按优先级依次执行。常见坑是buff叠加时没区分“刷新持续时间”还是“叠加层数”以及把已经死亡的角色还拉进结算队列。这块没有捷径每个卡牌技能都写测试用例一改动就能回归。2.4 存档与异步落库别在玩家进程里做磁盘IO玩家的卡牌、货币、进度通常是一个很大的属性列表或map。每次操作都直接写数据库肯定不行数据库IO会成为瓶颈。实践中是把所有玩家进程算出来的变更通过erlang:send投递到db_worker进程db_worker再批量写入。这样玩家进程只操作内存ETS数据库队列异步消化。但这里有个容易忽略的点离线存档时要确保玩家进程内的状态完整投递到db_worker并且db_worker处理完再确认否则登出过快会丢档。一个简单方案是登出请求经玩家进程转发给db_workerdb_worker落库后再reply玩家进程收到ack才真正结束进程。我在《萌兽堂》这套代码里也看到类似思路说明这个方案在中小型项目里非常通用。3. Linux服务器部署从安装Erlang到正式上线3.1 编译安装Erlang注意安装包和OTP版本拿到erlang原始码先在本机和Linux服务器安装Erlang。很多老项目用的是OTP 20/21时代不要一上来就装最新OTP 27很多接口改了之后直接编译不过。推荐用kerl管理多个版本或者直接下载对应版本安装包编译。sudo apt install build-essential libncurses5-dev libssl-dev unixodbc-dev wget https://github.com/erlang/otp/releases/download/OTP-21.3.8.17/otp_src_21.3.8.17.tar.gz tar zxf otp_src_21.3.8.17.tar.gz cd otp_src_21.3.8.17 export ERL_TOPpwd ./configure --prefix/usr/local/erlang --with-ssl make -j4 sudo make install依赖方面要特别注意libssl。如果缺这个crypto模块编不出来登录鉴权会有问题。unixodbc也是常见依赖不需要数据库ODBC可以不加。老代码在编译时经常报一些奇怪的include找不到八成是对应依赖的头文件没装用apt-file搜索一下补上就行。3.2 启动参数搭配P、K、S、A别照抄默认值用erl启动带哪些参数很关键。默认P比较小游戏服务器一般要调到50万或100万进程K true启用内核轮询提升网络吞吐A设置异步线程池大小S设置调度器数量一般跟物理核数一致不要盲目开大。给一个典型启动脚本#!/bin/bash export ERL_CRASH_DUMP/data/logs/erl_crash.dump cd /data/server exec /usr/local/erlang/bin/erl \ -name game172.18.0.10 \ -setcookie abc123 \ P 1000000 \ K true \ A 16 \ S 16:16 \ -detached \ -pa ebin deps/*/ebin如果机器只有4核S 16会引发大量调度器切换反而降低性能。pid文件、日志目录要先建好。-detached适合后台启动但调试期建议直接前台跑方便看启动日志。原始码里一般自带启动脚本模板直接改路径和节点名就能用。3.3 部署环境要顺手做好的几件事时区、NTP、内核参数游戏活动时间如果跟服务器本地时间不一致会很麻烦。建议统一使用UTC存储展示层转本地数据库连接串里的时区也要和代码保持一致否则统计报表会差一小时。生产环境配置好chrony或ntpd定期校时。内核参数也要调一调直接写进/etc/sysctl.confnet.ipv4.tcp_tw_reuse 1 net.ipv4.tcp_fin_timeout 30 net.core.somaxconn 4096 net.ipv4.ip_local_port_range 1024 65535 fs.file-max 6553600修改后执行sysctl -p生效。这些参数对高并发TCP长连接的好处是减少TIME_WAIT和提升监听队列。打开文件数也要调ulimit -n 6553600否则连接一多就报emfile服务看起来活着但新连接进不来。如果测试机用KVM虚拟机CPU型号穿透要开好否则Erlang的原子指令性能会受明显影响。3.4 节点发现与热更新的实操节奏多节点部署时用-name指定节点名节点之间通过EPMD端口4369和其他动态端口通信防火墙要放通。cookie必须一致跨机房场景建议用独立内网网段跑节点间通信不要暴露公网否则很容易被别人连上来操作节点。热更新推荐用release方式或者至少用code:load_binary。不要在玩家在线高峰期直接改源码重编译完load容易滚雪球。先测试服验证再分批load。我实际会先rpc:multicall把所有节点调用一次code:get_object_code确认各节点代码版本一致后再切换。这一步很多人忽略结果热更了半天实际只更了一台机器。4. 运行期的常见故障和排查手法4.1 进程堆积与内存增长先看进程数再看二进制堆线上最典型的问题是VM内存持续上涨。先看系统层面ss、top再用erlang:memory()区分total、processes、ets、binary。如果processes内存大可能是进程堆积如果是binary内存大可能是某处term_to_binary或大报文没释放。定位时用recon比较快recon:proc_count(memory, 10).定位到具体进程后用erlang:process_info(Pid, [current_function, message_queue_len, memory])看卡在哪个函数。如果大量进程的message_queue_len都很长说明某个gen_server处理不过来要检查是否有慢日志或死循环。4.2 ETS表泄漏与事务锁等待ETS是Erlang游戏服务器必备的玩家在线数据存放点。但ets:new创建的表如果没设置heir且所有者进程挂了表会直接消失更常见的是表一直存在数据却只insert不清理内存越涨越高。表参数也要选对玩家临时数据用set热点活动排名用ordered_set。如果多读多写打开read_concurrency和write_concurrency效果很明显ets:new(player_cache, [set, public, named_table, {read_concurrency, true}, {write_concurrency, true}]).经验之谈玩家登出时一定要delete对应记录每天零点做一次全表扫描清理孤儿数据。否则三天后内存拉满服务还在跑但新增连接已经开始变慢。4.3 网络连接异常端口不通、连接断开、backlog溢出排查网络问题我的顺序是先看端口有没有监听ss -lntp再看防火墙iptables和云安全组最后看连接数。卡牌游戏频繁断线很多不是代码问题而是心跳超时设置太短或者TCP keepalive没配。建议服务端心跳检测周期设成略大于客户端发送间隔的3倍太短会把正常玩家踢下线。现象可能原因常用排查端口连不上服务未启动 / 防火墙 / 安全组ss -lntp、iptables -L客户端频繁掉线心跳超时、网络抖动tcp_keepalive_time、抓包高并发时卡顿ETS锁竞争、调度器满erlang:statistics(scheduler_wall_time)新连接开不进somaxconn太小ss -lt看drop计数特别是云服务器安全组规则改了不会立即生效需要确认是不是没放行目标端口。之前我排查过一个问题监听正常、本机telnet通但公网访问不了最后发现安全组入方向没加这个最坑。4.4 时区与日志时间乱跳问题很多游戏服务器日志默认按服务器本地时间打印部署到云服务器后时区如果是UTC活动日志和玩家消费记录就对不上。建议日志统一使用UTC时间戳展示层再转本地。数据库datetime字段也要统一时区否则统计报表每天错一小时。另外一个隐藏问题NTP没配好服务器时钟漂移严重活动开启时间就会提前或延后。用ntpq -p看一下同步源状态如果显示不在网段内多半是chrony配置里没放行或上游时间源不通。这套东西看起来小但影响活动运营值得在部署初期花十分钟搞定。5. 一套能跑的卡牌游戏服务器后续怎么扩展5.1 从单服到分服/跨服原版架构通常是单节点跑完所有逻辑玩家多了以后可以把登录服、大厅服、战斗服拆开。登录服只负责账号验证和节点路由战斗服独立进程运行跨服战就通过rpc:call到对应节点再做房间迁移。我个人建议规模没大到一定程度不要一上来就上K8s和跨区复杂架构。像《萌兽堂》这种量级2到4台同可用区云服务器中间放个Redis能扛住不少压力。等真需要跨服了再加服务发现和配置中心一步一步来踩坑成本小很多。5.2 日志与监控如果能早做能省很多事后台服没有监控就像开盲盒。最好从第一天就接上prometheus_ex或类似打点库。我自己用的组合是lager写日志recon做在线诊断prometheus_ex打点grafana看面板。至少要把在线人数、进程数、ETS内存、DB队列长度、CPU占用做成面板。卡牌游戏特别关注数据库队列堆积如果db_worker积压太多玩家存档就会延后登出可能丢数据。这个指标一旦持续增长就要立刻报警处理。不要等玩家投诉再排查那时候已经很被动了。5.3 读这套原始码最后几天我最想留给大家的三句话读源码先找链路不要纠结单个函数。协议格式别轻易改除非你把客户端一起改。每一步改动都要能回滚线上不是实验室。最后补充一个实操技巧开发期直接在VSCode里装SSH Remote插件连到服务器上改代码配合remsh进节点看在线状态比本地改完再传上去效率高很多。服务器远程开发看着是件小事但迭代速度能提上来一大截。这套erlang_server代码本身不复杂把它吃透后面再碰大型OTP项目都会顺手很多。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进