[推荐] 一个 Node.js 技术选型案例:使用 CARMEN 作为卡牌手游技术栈

[推荐] 一个 Node.js 技术选型案例:使用 CARMEN 作为卡牌手游技术栈_第1张图片

作者 @超人张宝胜 ,原文地址:https://zhuanlan.zhihu.com/p/103724412,如需转载请联系作者授权。

前言

本文介绍了在资金、人员、时间上全面告急、云服务提供商不确定、需求混乱且易变的情况下面对肮脏且 I/O 密集的业务场景的一种行之有效的手游技术栈,内容以新老项目经验教训、技术选型与架构设计思路为主,无大篇幅粘贴代码片段。该技术栈在压力测试与线上实际表现均远优于对比方案,且拥有更佳的可维护性。

本文重要更新记录

2020年1月27日晚上追加当前项目架构图一张。

概念

本文所讨论的“卡牌”游戏,不是斗地主、同花顺等等棋牌游戏,也不是集换式卡牌游戏,而是可以“抽老婆”的 gacha games 及其变种。由于此类游戏立绘精(kě)美(lū),战斗大多非强实时甚至可挂机,并且付费深度深不可测,这几年以来一直长盛不衰。由于此类游戏的战斗多为回合制、采用行动条的类回合制、带QTE的类回合制、挂机等等,普遍认为较之MOBA、吃鸡类的游戏的技术含量低,因此鲜有相关特别是服务器端的技术心得分享。

本文标题的 CARMEN ,指的是

  • Cocos Creator,著名 2D 游戏引擎

  • Angular

  • Redis

  • MongoDB

  • Electron

  • Nest

[推荐] 一个 Node.js 技术选型案例:使用 CARMEN 作为卡牌手游技术栈_第2张图片
神偷卡门·圣地亚哥

不被看好的 Node.js

如今(2020年初)在Node.js、前端以外的技术圈,Node.js 仿佛成为了千人踩、万人捶的存在,以下言论均来自知乎

  • Node.js 只是一个让前端开发人员快速入门后端的东西而已。

  • Node.js 做做小服务和中间件差不多。

  • Node.js 这种残废只是前端工具而已啊。

  • Node.js(和其他语言/框架) 就不配放在这比较。

  • Node.js 真正的优势是给做纯前端JS的人打开一扇后台的门。

  • 如果你没有非常雄厚的资本创业,别用 Node.js ,这东西就是阿里忽悠坑人的!!创业公司能有多余的钱去弄几个子资深后端专门去研究 Node 几年重新造轮子??阿里那帮犊子能整天鼓吹 Node 的优势,是因为他们能有一大堆冗余开发资源去解决 Node 不成熟的一堆劣势。他们晋升需要造轮子,顺带装下B~ ……

同时,在 Web Framework Benchmarks 中,Node.js 也在几乎所有的项目落败。Actix、Swoole、Vert.x 等等大批明星框架长江后浪推前浪,唯有 Node.js 是永恒的沙滩看客。

如今 Node.js 在前端工具链、SSR、中间层、FaaS 以及玩具项目还有相当重要的地位,而传统的后端开发中却很少再听人提起。就连著名的 Node.js 游戏服务器框架 Pomelo 也有数年没有重大更新了。这是否是 Node.js 在服务器端的最终宿命呢?

上一个项目

先分享我们在上个卡牌手游项目中的真实经历。

该项目核心战斗类似韩国手游《魔灵召唤》,且部分玩法要求支持跨服对抗。来自国内著名互联网公司的高级架构师带领服务器端团队,使用了如下图的架构:

[推荐] 一个 Node.js 技术选型案例:使用 CARMEN 作为卡牌手游技术栈_第3张图片 除了 game servers 的数量不定,其它服务器的数量均是正式版本
  • 基于 JDK 1.8 与 Spring Framework 4 。

  • 使用 DubboX(Dubbo 2.x 官方停止维护后的当当修改版 )提供服务自动注册与发现、RPC 调用。

  • 使用 Redis 作为 数据库的缓存层,以及分布式锁等。

  • 使用 Spark 存储日志及统计每日运营数据。

如此,可以由多台服务器作为可伸缩的无状态服务提供者。非游戏行业的朋友们可能觉得,这套架构至少应该没有什么明显缺陷,“能用”甚至“好用”。而时至今日,国内卡牌手游的服务器使用类似的分布式架构,甚至 Redis 作为关系型数据库的内存缓存的情况也不多。那么可以想象一下,这样的架构用在我们的卡牌手游中,是不是砍瓜切菜,游刃有余,秒杀友商?再不济至少也该是四平八稳、波澜不惊吧?想想都有点小兴奋呢。

[推荐] 一个 Node.js 技术选型案例:使用 CARMEN 作为卡牌手游技术栈_第4张图片
这个笑容…… 算了,还是你们来守护吧。

奇怪的是,服务器端团队偷偷架设了不公开、不受控的代码仓库,并且从一开始就抵触压力测试,时任客户端技术负责人的笔者反复提出要进行压力测试后,服务器技术负责人直接告状到 CEO 处,称笔者“入侵他人技术领域”。运维甚至直接翻脸,当众指责笔者为何在内网开发服进行压力测试,“要测去线上测去”(???)。服务器技术负责人在此期间还将此项目技术栈在 QCon 等技术会议上分享,成为了明星讲师;并与他人合作出版了分布式相关技术书籍若干,京东分类排名第一,成为博文视点年度优秀作者;最终入职一线互联网企业,从此走上人生巅峰。整个服务器端团队连同运维随后集体跑路,就在游戏即将正式推广上线前夕,果实唾手可得的时刻。这么优秀的人,这么优秀的架构,为什么不愿亲身体会上线成功的喜悦呢?

在告知最终结果之前,我们先了解一下这个游戏项目的一些特点。

  • 游戏内部分 PvE 玩法(如类似 rogue-like 和探索解* 谜类的玩法)逻辑较复杂。

  • 战斗使用行动条,计算涉及的数值与逻辑较复杂。

  • 通讯频率高,消息体大。

  • 相当比例的消息与全服共享的数据相关,如公告、排行榜。

  • 部分消息需要服务器端主动推给客户端。

  • 需求较不明确且改动频繁。

  • 游戏运营需求比游戏本身基础功能更多更复杂,且严重侵入游戏基础功能逻辑,还占去了后期表现、逻辑开发的大头。

  • 需要能够配置复杂运营需求的管理后台( Admin )。

关于运营需求的复杂性,笔者可以举一个例子:

  • 玩家消耗资源增加了某个英雄的属性点。

  • 产生了两条日志分别记录资源消耗与属性点变更。

  • 由于属性达到特定的门槛了,原来对该玩家关闭的功能开启了。

  • 由于属性达到特定的门槛了,在全服的英雄排行榜上增加了该玩家的记录。

  • 玩家所有的包含此英雄的防守阵容都变得更强了。

  • 由于属性达到了特定的门槛了,触发了玩家部分英雄的“情缘”(也被称为“羁绊”等,可近似认为就是套装加成)生效,玩家的其它十几个英雄也变得更强了。

  • 玩家的英雄总战斗力也变更了,顺带更新了总战斗力排行榜上的名次。

  • 由于在排行榜上到达了特定的名次,全服的玩家都收到了一条广播,该广播将以夸张的文字刺激其他玩家消费。由于玩家今日消耗了这个资源,玩家的每日达标任务也完成了一项。

  • 由于正好是春节活动期间,玩家消耗资源和增加属性也使得两项活动达到了新阶段,使得玩家收到了一封游戏,内含奖励道具的附件;同时解锁特定资源礼包的购买资格。

  • 由于属性更强、“情缘”生效、消耗的资源的历史总量达到了特定值,该玩家还完成了三项成就。

  • 没过几秒刚好跨天了,昨日达标任务的奖励也该从邮件发放了。

  • ……

以上例子并无刻意夸张。而在实际场景下,如果此刻同时生效的活动较多,比如上例子更复杂的情况也比比皆是(有兴趣的同学可以参考《明日方舟》的活动,该游戏的活动已算是比较良心且不太复杂的了)。而作为需求的提出者,运营与策划其实也都无法哪怕粗略描述这么一个属性提升的操作具体影响了什么。这就是笔者所说的“脏”业务。

回到主题。遮遮掩掩、扭扭捏捏、拖泥带水且极不愉快的交接后,多达6人的服务器端团队两年半的成果终见天日:

  • Redis 缓存直接粗暴地加在 DAO 层的单条插入和单条更新接口上,绝大多数情况下只允许通过 ID 同步查询和更新,且更新时不允许使用事务。出于“分布式”的目的,没有 Redis 以外的其它缓存机制。

  • 只做 CRUD 有关的业务,而且开发效率极低。上线前中重度玩法和战斗逻辑完全没有实现,连最起码的校验都没有,相关数据要求客户端自行计算再全量同步回服务器。

  • 涉及到装备、卡牌等系统的批量操作,产生大量的循环内同步更新数据库操作,附带大量同步 Redis 操作和日志操作,一次请求经常直接导致近万次的同步 I/O 。

  • 超时等原因导致的请求重发,导致数据错误的情况大量出现,交易丢单、财产丢失、排行榜错乱问题频出。

  • 只接受 http 作为唯一的基础通讯协议,理由是不愿维护连接,甚至连长轮询(long polling)都不愿支持。团队甚至声称由于服务器处在负载均衡之后,无法获得客户端IP地址[1]。

  • 无论是静态还是动态数据,都只走统一的消息协议。再加上大量本应推送的消息只能让客户端采用频繁查询的方式来替代,最终的产品有着高达数兆的单次通讯消息包体与极高频的通讯,体验极差。

  • 四核心至强的物理服务器,不用40000、不用4000,只要4的压测并发队列就足以使其瘫痪。在未瘫痪时,响应时间也经常达到数秒。

  • 网关服务器的配置竟然比游戏服务器的配置高很多。

  • 三台 ZooKeeper 服务器并非配置成一台主力,两台备份,而是… 任意一台挂了,网关与游戏服务器之前的通讯就彻底挂了。嗯,三倍的配置,三倍的风险。

  • 使用人人/千橡祖传十余年的“非常先进的 jsp”(原话如此)开发的 Admin,成千上万行的超大 jsp 文件比比皆是,团队中 Admin 相应岗位的人来了又去,无人愿维护。而且 Admin 在4核心8GB内存的主机上,只要有两三个用户同时在线操作就大概率 404 ,所以登录 Admin 需要在群里吼一声。

  • 简单的日留存和日付费数据竟然无法实时计算,需要次日凌晨执行超长时长的定时任务,而且此任务经常跑着跑着就自己挂了。

  • 开发环境搭建复杂,离职者自己都甚至耗费了一天时间在给交接者搭建环境上,而且直至成功跑起来时,离职者也完全不明白怎么就能行了。

  • ……

[推荐] 一个 Node.js 技术选型案例:使用 CARMEN 作为卡牌手游技术栈_第5张图片 开服首日在线用户,来自 TalkingData

最终在正式上线当天,我们使用了云服务商单账户下能购买的服务器上限(64台4核心8GB主机,其中约40多台作为 game server),以及高配的 MySQL 和 Redis 实例,然而开服仅10分钟就在3000名用户的访问下彻底瘫痪,接下来就是长时间的丢单处理、修复与填坑噩梦,同时还需要垫付每个月数万元的服务器费用。在此期间,笔者除了发现了上文列举的问题,还定位到数据库连接池、线程池、DubboX 等多处存在的问题。最终笔者退掉大部分云服务器,将所谓网关、游戏服等合并为一,移除所有远程调用和 Redis 缓存,使用 Guava Cache 作为本地缓存,使得游戏服务器终于有了正常的表现。然而已造成的损失是无法挽回的,发行渠道与玩家也再没有信心重新接纳。没多久游戏彻底关闭,团队3年的心血全部成为少数人进入一线互联网公司的跳板,余下的只有一地鸡毛和欠下的巨额债务。

新的项目及选型标准

当原先的工作室破产解散,客户端技术团队剩余的小伙伴们开始了新的项目,但仅3人的人数,却面临着更大的挑战:

  • 游戏风格、核心玩法和战斗有着显著不同,新项目各方面需要从零开始。

  • 技术团队允许扩充1-2人,但预算上不允许招聘富有经验的人员,且不允许为 Admin 招聘前端。

  • 部分玩法需要配套的配置编辑器,带 GUI 。

  • 可用于开发和上线的云服务预算极低。

  • 云服务商不固定,主要取决于发行渠道偏好;但是由于掌趣科技与腾讯的关系,开发服务器与中国大陆的线上服务器供应商确定只有腾讯云这一选择。

  • 工期远短于上一个项目,两个月内出核心玩法 Demo,再过三个月上线包含主要玩法的测试版。

好在现有团队成员在 C / C++ 与脚本编程( Lua )上都有较多的项目经验,可将在计算密集型业务中使用原生代码作为保底。因此针对游戏与团队特点,我们在制定了如下的选型标准:

  • 客户端、服务器、Admin及周边工具的大部分业务代码使用统一的编程语言。

  • 希望兼有脚本开发的快捷和静态类型语言的可维护性。

  • 客户端代码可以较低代价移植至常见浏览器平台与原生平台,在原生平台必须支持热更新。

  • 低心智负担的异步 I/O,及尽可能低的 I/O 次数。

  • 开发、调试、部署便利。

  • 通讯协议可根据业务灵活选择选择。

  • 数据传输必须做到动静分离。

  • 服务器端程序支持水平扩展,且同时具备低下限与高上限:既能以每月30元人民币的云服务器(含主机与数据库)预算应对日常开发与千人日活规模的小范围封闭测试,又能在正式推广上线时横向扩展为高性能模式。

  • 现代化的 Admin ,支持响应式布局,以便在移动端应急处理线上状况;必须做到前后端分离,以在必要时将前端部分整体重构替换。

  • 新人可快速上手产出,且不易造成较大破坏。

最终选型

冗繁的测试、对比与验证过程就不大篇幅粘贴在此,最终的选型也在标题和开篇就已表明。其中比较值得分享的部分就在下文阐述。

编程语言:TypeScript

其实放宽一些要求的话,客户端与服务器可满足需求的编程语言有 C#、Lua 与 TypeScript。

团队在多年使用 Lua 的经验中已深知非静态类型语言在调试与维护中的诸多缺点,本次调研一致认为不再优先考虑 Lua。

而与 TS 同为 Anders Hejlsberg 的亲儿子,C# 还可用于编辑器等的开发,这样也仅有 Admin 需要其它编程语言。但是考虑到客户端的热更新,如使用支持 C# 的游戏引擎 Unity ,则要么在业务代码中使用 Lua ,要么采用掌趣科技的 ILRuntime 热更新方案。前者由于语言关系已不考虑,而后者在使用中带来的限制与不便也使其在与 TS 方案的比较中没有显著优势。这里插一句,依笔者愚见,ILRuntime 本身仍有巨大的改进空间,在此不作展开。

而让团队下定决心选择 TS 的最主要原因并非上文所述。与 Ryan Dahl 当时选择了 JS 和 V8 的原因相似:

在他快绝望的时候,V8 引擎来了。V8 满足他关于高性能Web服务器的想象:没有历史包袱,没有同步 I/O。不会出现一个同步 I/O 导致事件循环性能急剧降低的情况。V8 性能足够好,远远比 Python、Ruby 等其他脚本语言的引擎快。

没有同步 I/O!没有同步 I/O!没有同步 I/O!(基本上 。)这点完美符合了之前所制定的新人“不易造成较大破坏”的选型要求。

客户端引擎:Cocos Creator

编程语言一旦确定为 TS,客户端引擎的选择瞬间缩小到一个极小的范围:Unreal Engine 4 加 Unreal.js、Egret、Layabox、Cocos Creator。2018年中时的 Unreal.js 还无法支持 iOS,首先被排除。接下来就是三大 2D 引擎之间的选择了。从在线文档、UI 动画编辑等多因素考虑,外加团队当时已有8年的 cocos2d-x 的使用经验,最后对 Cocos Creator 的选择其实是毫无悬念的。

通信协议:多协议混合

笔者对人人/千橡祖传代码及其维护者坚持不懈只使用 http (不高于1.1) 的做法早已深恶痛绝,但绝非一味地排斥 http。笔者之前提到,公共的、静态或伪静态的数据,其实可不必夹在日常的业务通讯消息中。因此这部分数据可以使用 http,并使用 CDN 来加速访问并控制其生命周期。

而动态数据,可能读者认为笔者会直接选择 WebSocket 吧。然而笔者之前强调,“云服务商不确定”,这点其实对协议选择有着很大影响。笔者日常深入使用的云服务有 AWS、阿里云、腾讯云、金山云、UCloud 等等,这里能非常明确地告诉大家,大部分云服务商不能提供廉价的 WS 加速方案,有且只有 AWS,在2018年下半年让 Cloudfront 支持了 WS[2] 。在云服务成本极为有限的情况下,无论是多地主机,还是天价加速,这都是团队无法负担的。置之不理,又对海外发行有着极大的负面影响。笔者必须考虑使用 http 的可行性。

好在云服务商都能直接或者间接提供基于 http 的廉价加速方案,比如阿里云的全站加速。其它的云服务商可使用0秒过期的 CDN 来曲线救国。

这里点名批评腾讯云,你家的那个天价加速,上线这么长时间,到底有没有人真正测试过?能连上自家的负载均衡吗?连你自家技术支持都推荐使用 CDN 来取代加速。而你家的负载均衡,新建实例也时常出现连不上同区域同子网的主机的情况。CDN就更别说了,这几年改版过几次了吧,有什么问题心里应该清楚。

为此我们重新梳理了业务中的动态数据。大部分的功能走 http 是没什么问题的;实时战斗部分是一定需要长链接的,但是可让客户端单向发送数据,服务器端校验,因此可容忍稍高一些的延时;部分消息,如竞技场防守、支付结果等,需要服务器推送回客户端的,不仅对延时容忍度较高,而且完全可以走 发布/订阅 模式。

最终我们确定了如下的多协议混合方案:

  • 公共数据、静态与伪静态数据:http,CDN 根据业务配置1秒到1年的缓存失效时间。

  • 大部分个人业务数据:http,CDN 的失效时间为0秒,纯粹为了 CDN 的加速效果。

  • 需要服务器端推送的业务数据:MQTT over WebSocket。

  • 实时性较高的数据:WebSocket。

服务器端框架:Nest

虽然是面向手游服务器选型,但是由于通讯协议的特点,Web 服务器框架也是完全可以的。另一方面说,Web 服务器框架本身提供的基础功能也不是重点考虑。虽然笔者长期使用 Egg.js,但我们更期望框架对 TS 原生支持,并对代码有着更多约束。在2018年中这个时间点,Midway 还未诞生, @小爝 的 Daruk 大乳鸽 也未开源(而且笔者个人也无法接受这两个框架的装饰器的首字母小写风格,强迫症)。事实上也只有 ThinkJS 和当时尚无人知晓的 Nest 可供选择。最终选择 Nest 的原因如下:

  • 语言:完美支持 TS,其自身也是以 TS 编写。

  • 性能:可选的 FastifyAdapter ,而 Fastify 在 JSON 的序列化上可达原生方法的性能的两倍(而且加上 yarnpkg 和 Nest,一共有三个 的Logo了,看来猫猫在这个生态圈里还是很受欢迎的)。

  • 可伸缩:内建多种传输器的微服务,同时可被配置为 http 服务器与微服务同时服务的混合应用(Hybrid Application)。

  • 可维护性:Nest 深受 Spring Framework 和 Angular 的启发,组件容器、依赖注入、AOP、中间件、管道、守卫、拦截器……良好的设计配合 TS 能够避免“重构火葬场”的状况。

数据库:MongoDB 和 Redis

处于需求快速变化的考虑以及服务器端框架本身能提供的约束,选择 MongoDB 基本是毫无悬念的;Cosmos DB 等基本就别想了,发行方和费用都不允许。

本项目还同时使用了 Redis ,但这次和上个项目不同, Redis 不再是作为关系型数据库的内存缓存的二等公民,而是作为排名数据、短期失效数据(如各类限时活动、限时任务等)的持久化存储,成为了与 MongoDB 平起平坐的一等公民。

Admin 前端框架:Angular

与服务器端框架选型类似,虽然笔者也有 Vue.js 的使用经验,但开箱即用的全家桶和更强的约束,对着文档撸一遍即可上手开发的优秀官方教程,以及类似 Nest 的风格(当然其实是 Nest 学习自 Angular),Admin 的技术选型基本没花太多时间。

周边编辑器与工具链:Electron + Angular

前端框架确定后,这一点就自然而然了。Angular 与 Electron 的集成很快,一些小坑网上也早有解决方案。这里顺带说一句,其实 Cocos Creator 的编辑器也是基于 Electron 的。

包管理器与锁版本策略

考虑2018年时的 npm 的诸多不足,我们一开始就确定使用 yarn 作为整个技术栈的包管理器,并作出如下规范:

提交 yarn.lock 文件至版本控制系统。定期更新依赖,遇到问题的依赖在短时间内锁版本。

Nest 深度定制与改造

我也实在不是谦虚,我一个 web 服务器框架怎么就拿来开发游戏服务器了呢?”

仅靠 Nest 的开箱状态来承担游戏服务器的用途显然是难以满足需求的。我们在开发过程中对其进行了大刀阔斧的改造。

[推荐] 一个 Node.js 技术选型案例:使用 CARMEN 作为卡牌手游技术栈_第6张图片 当前项目架构图,正式环境典型配置

可配置的角色(Roles)

同一个工程,根据配置可以成为以下任一角色或任何的角色组合。

  • 监视者:管理所有其它角色的生命周期管理,以及版本检查、更新、代码编译、重启

  • 游戏 http 网关

  • WebSocket 网关

  • Admin http 服务器

  • MQTT broker

  • 微服务提供者:承担大部分的业务逻辑处理

  • 战斗服务器:实时战斗相关的逻辑

  • 定时任务服务器:处理排行榜、活动、统计等定时任务

  • 邮件服务器:向运营或运维人员发送数据日报、预警等邮件

  • JSON Schema Generator

  • ……

在低配置硬件服务器(1C、512M)上或者本地调试阶段,全角色模式的单进程无疑有着诸多的好处。

扬长避短

尽快可被配置为万能的瑞士军刀,事实上我们在生产环境里只启用了部分角色,毕竟术业有专攻,我们专注于 I/O 密集型的业务即可:

  • 使用 NATS 作为为服务器微服务 transporter 。

  • 使用云服务商提供的负载均衡。

  • 使用 CDN 配置 https 证书、gzip 压缩。

  • 使用 NGINX 作为静态资源服务器。

  • 使用 EMQ X 作为 MQTT broker。非要用 Node.js 的实现是没有必要的,可参见 @小爝 的 mqtt 各种 broker 如何选择? 。

不依赖于 Redis 的粘性会话(Sticky Sessions)

大部分的 Node.js 的服务器端框架在多进程模式下都支持粘性会话,但是通常仅用于 WebSocket,且依赖于 Redis 的发布/订阅,增加了 I/O 次数。而我们的服务器,一个会话在其生命周期内,只会在一个服务器端进程内处理业务逻辑。在多进程的模式下,多个 Hybrid App 实例监听不同的端口;同时我们通过云服务的负载均衡的配置,使用路径或端口绑定到特定的 Hybrid App 实现了粘性会话,如此粘性会话可以作用于 http 和 WebSocket,且无需全程依赖 Redis,没有额外的 I/O 开销。更重要的是,由于会话永远在一个进程内执行,分布式锁不再需要,而各种数据上的不一致也由此解决。

极细化的缓存策略

先前的项目在 DAO 层使用一刀切的 Redis 缓存,除了显著造成巨大的 I/O 压力外,同时还造成了:

  • 事务不可用。

  • 非主键查询或条件查询无法使用缓存。

  • 数据极易出现不一致的情况。

而现在,我们有多种的缓存可供选择与组合:

  • LRU(Least Recently Used)Cache,可加上最大条数和失效时间等限制。

  • 带有 Max Age 缓存策略的定时失效缓存。

  • TypeORM 自带的缓存。

  • Nest 提供的 http 请求缓存。

  • CDN。

我们根据业务精细定制了每个 Entity、每个查询(包括带条件的查询)的缓存策略,保证各个业务都有一到多级的缓存,以降低 I/O 次数并降低数据库负担。例如服务器列表便是多级缓存的典型应用,每个服务器进程最短20秒,最长60秒才访问一次数据库,在实时性与性能之间取得了很好的平衡。

FastifyAdapter 与 JSON 序列化

Fastify 的重要特色就是超越原生 JSON 序列化的速度。为了能充分用上该特性,我们需要:

  • 让 Nest 使用内置的 FastifyAdapter 取代默认的 ExpressAdapter。

  • 为协议消息体定义生成 JSON Schema 。

  • 角色被配置为 JSON Schema Generator 的实例在启动时将扫描代码目录下所有的 *.schema.ts,为其它角色提前生成好 JSON Schema。

MessagePack 的应用

其实出于空间与序列化和反序列化性能考量,Redis 中存储的大部分数据,以及大部分的游戏通信消息,都是采用 MessagePack 来编码的。Gzip 压缩后的大小也令人满意,而且 MessagePack 对各个编程语言的支持都很好,上个项目 Lua 客户端和 Java 服务器也是用其编码的(不过当时经常被迫在 MessagePack 消息中加上 JSON 文本字段,而且 JSON 文本字段里还可能有二次编码过的 JSON 文本,无奈)。

不以 Cluster 的形式启用多进程

避免不必要的 Master 与 Worker 的进程间通讯。

Nest 的不足

就算不考虑部分程序员对类 Spring/Angular 风格的排斥,这个年轻的框架也有很多不足。以下观点均基于 5.x 版本,当前的 6.x 版本已作出不少改进。

官方文档的编排顺序有待优化

在通读文档全文之前就急匆匆上手的话,很容易遇到困难和疑惑,但其实相应的解释在文档的犄角旮旯里。

文档假定用户已对 Express 有一定的了解,而且部分文档完全不适用于使用 FasifyAdapter 时的情况 "/foo/bar" 和 "/foo/bar/" 。全局中间件、模块中间件、Express 中间件、Fastify 中间件的注册姿势各不相同。信任代理。获取客户端IP地址。监听非本机请求。认证是几乎所有的新手都会询问的部分 除非用户使用和范例完全一样的认证方式。

定时任务和线上部署

国产框架 Think.js 与 Egg.js 都包含了定时任务功能和线上部署的详细说明。虽然为 Nest 加入定时任务很简单,线上部署也可以自由选择 PM2 或其它方案,但是这些方面的缺失,还是给很多新手困扰。

最终成果

截止笔者下笔时,服务器端工程已成为近80万行(不包含依赖,不包含 Admin 部分)、1000多个依赖的超大 Node.js 工程。按照普遍的观点,回调地狱、npm、糟糕的可维护性等等都早已将工程变成了屎坑,更别提还有游戏本身的脏业务加持。但是实际的结果令人欣慰:

  • 响应速度和服务器承载能力均大幅提高,同条件压测时的结果可以说是上千倍的提升(这点真不必吹牛,毕竟上一个项目那么差 )。

  • 客户端、服务器、Admin、工具等部分代码共享,减少了不少开发量。

  • 得益于 async/await 和 RxJS ,完全没有遇到任何回调地狱的问题。

  • yarn install 完全不需要提心吊胆,唯一一次 yarn upgrade 后锁依赖版本也仅仅是因为 TypeORM 自身对 MongoDB 的支持尚属于实验特性,有点小问题,而且很快就修复了(话说我们连实验特性都敢在生产环境使用,更可见我们对 yarn 的信心)。

  • Admin 较祖传代码便利许多,移动端也可正常访问。统计数据更不用等凌晨恼人的任务,随时可查阅实时数据。

  • 在完全干净的系统(Windows、macOS)上,5分钟时间就可搭建好全套开发环境。

  • 可放手让新人参与到业务开发中来,新人只需照猫画虎就可完成部分业务的开发。比较需要注意的是 Promise.all 的使用和缓存策略的制定,需要主管时常 review 或提供思路,但即使如此,最坏情况下新人也没有较大破坏性,多 await 两三次并不会有什么过于明显的负面效果。

  • 不暴露代码的情况下,策划与美术可部分参与到项目中来,使用工具链生成配置代码及优化过的图片资源。

可以说,我们得以在之前项目几分之一的人力、几分之一的工期完成较之更复杂的需求,同时服务器的性能反而得到了大幅的提升,并且代码是易于维护的。

[推荐] 一个 Node.js 技术选型案例:使用 CARMEN 作为卡牌手游技术栈_第7张图片 Admin [推荐] 一个 Node.js 技术选型案例:使用 CARMEN 作为卡牌手游技术栈_第8张图片 地图编辑器

不足之处

技术栈不可能完美无瑕毫无缺点,CARMEN 也是。除了是针对特定游戏类型定制之外,最大的问题就是,粘性会话与细化的缓存策略,导致了大量的负载均衡与 CDN 规则,远超正常项目,而且不同云服务商的配置方法还不一样,故通常运维不愿意协助配置。好在每个发行渠道只需要配置一次,而且上个项目中运维做出了伙同服务器端团队绑架项目的行为,笔者这辈子不可能再信任运维,所以相关规则的配置一直是由笔者亲自进行。

回顾

笔者曾提到,曾经(至少是前几年)同类游戏中,使用分布式架构与 Redis 的情况不多,原因又是什么呢?

  • 分区分服机制使得单个游戏逻辑服的用户量是可控的,单个物理服通常可以支承数个游戏逻辑服。

  • 服务器通常选择大内存的配置,以内存作为本地缓存,而数据库操作一定是读写分离的,甚至有独立进程专门负责写入。

看似陈旧无新意的架构,却是业务场景下的可靠选择。而盲目求新,未经起码的验证就直接套用所谓“先进架构”,就算有“工业流水线语言和框架”加持,不要说做得好不好,性能高不高,连本应完成的业务功能都没完成,连起码的正常运行都做不到,这时候再谈什么语言框架的优势,又有什么意义呢?

那么回到开头,通过这两个项目的介绍与对比,我们可以知道,至少在需求复杂且易变、重 I/O 的业务场景下,Node.js 有着非常值得肯定的优势。也就是在传统的服务器开发方向,Node.js 应有一席之地。

“哪有什么岁月静好,不过是有人在为你擦腚背锅。”

共勉

很多朋友习惯于争论语言、框架、中间件的优劣,并以使用过其中的强者而沾沾自喜。但人绝不会因为用了什么语言、框架、中间件就能秒变牛逼,语言、框架、中间件也不会因为多了个用户而脸上增光。

同时笔者也觉得,在 Node.js 之外,也有很多东西是有必要关注的。

  • 至少一门 native language:有 native language 保底,即使在不得不以脚本语言面对计算密集的业务也不虚。会 native language 的技术选型是技术选型,不会 native language 的技术选型那叫被技术绑架。

  • 国内外主流云服务:了解各家云服务的优劣势并善用云服务,能少做很多无用功,也能为技术选型扬长避短。

  • 良好的语文、数学、英语基础。别人文档看不懂,写的文档也没人看得懂,调 API 或者 CRUD 以外的需求都做不了。你说你是工程师吗,你见过水平相当于九年义务教育都没完成的工程师吗。

人外有人,天外有天,每当产生了一种“我很牛逼”的错觉时,请打开这篇回答:

如何看待哔哩哔哩的开源 HTML5 播放器内核 flv.js作者月薪不足5000?

想想作者谦谦(艾特不到)是在什么样的年龄,什么样的薪资,以及2016年时 MSE 相关的资料有多么贫瘠时实现了 flv.js,以一己之力改变了中国大陆大部分视频网站 PC 端 Web 播放器,再想想自己现在在做的事,自己将来能做的事。

感谢

这些年的游戏开发历程,笔者查阅了知乎大量的回答与文章,得到了很多有参考价值的信息,在此对有印象的几位表示感谢,排名无先后,名单有遗漏。



@王哲 @i5ting @汪志成 @justjavac @朴灵 @小爝 @龙泉寺扫地僧 @方应杭 @圆胖肿 @左华栋 …… 太多太多了。

其中有的朋友与笔者的观点差异极大甚至完全相反,每次看到都想暴打一顿,甚至被笔者多次拉黑,但是冷静下来后还是能从对方的观点里提取出值得参考的部分。

CARMEN

感谢读者对笔者的啰哩啰嗦的宽容,祝愿大家新春快乐,身体安康。

参考

  1. ^X-Forwarded-For https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/X-Forwarded-For

  2. ^Amazon CloudFront 发布针对 WebSocket 协议的支持 https://aws.amazon.com/cn/about-aws/whats-new/2018/11/amazon-cloudfront-announces-support-for-the-websocket-protocol/

编辑注

  • 由于微信限制,非微信外链无法跳转,也可文末 “阅读原文” 查看。

❤️爱心三连击

1.看到这里了就点个在看支持下吧,你的「点赞,在看」是我创作的动力。
2.关注公众号程序员成长指北,回复「1」加入高级前端交流群!「在这里有好多 前端 开发者,会讨论 前端 Node 知识,互相学习」!
3.也可添加微信【ikoala520】,一起成长。


“在看转发”是最大的支持

你可能感兴趣的:(游戏,中间件,数据库,powerdesigner,hashtable)