如何使用 Go 语言写游戏服务器?

之前先后用Erlang,nodejs做过tcp,http的游戏服务器。接触了golang一两个月(纯新手),想在最近的tcp网游项目中使用,但又担心以…
关注者
660
被浏览
125,996
登录后你可以
不限量看优质回答私信答主深度交流精彩内容一键收藏

谢邀,昨天就有看到这问题,因为题太大不知道从何答起,所以就没答,今天易伟前辈邀请,不答不行了。

真有趣团队是从Go 1.0开始使用Go开发游戏服务端的,所以小经验有点,但是我们还处在不断学习摸索的阶段,所以太高深的学问不多,下面我就按题主的问题顺序尝试一个个的回答吧:

# 如何高性能的搭建tcp底层,并且能负载到同时在线N多人

Go自身在特定平台会使用对应平台的io重用方案,比如epoll,kqueue等,所以底层部分效率已经不错了,比起自己用C/C++去封装底层或调用libevent之类的库,优势是Go将事件机制封装成了CSP模式,编程变得方便了,但是需要付出goroutine调度的开销,在游戏项目上实践的经验是调度开销可接受,无需做额外工作来优化。如果自己要在Go里面调用epoll重新封装一个网络层,这样做所能提升的效率和付出的代价对比起来,性价比太低了,不值得这么做。

所以用Go搭建TCP底层是很省事的,主要关注几个点:

1. 尽量减少系统的IO调用次数,比如使用bufio这个包来减少实际IO次数

2. 尽量减少不必要的数据拷贝,比如消息的封包解包过程,细心点设计是可以做到极少的数据拷贝的

3. IO阻塞时的边界情况处理,比如一个请求处理过程中,如果消息回发导致处理过程阻塞,是否会影响到其他后续请求,又或者广播过程中消息发送阻塞,是否应该把阻塞的连接关闭等

我这有个简单的库可以提供参考:

funny/link · GitHub

# 如何架构整个服务器端(包括网络层,缓存层,持久化层,日志层,逻辑分发处理层,通信协议层,以及如何有效部署)

这个议题挺大的,但是题主已经明确罗列出了这些项目层级和模块划分,说明是已经有经验的了。Go语言跟其他语言一样分层分模块,没太大特别之处。

Go在组织游戏项目的时候有一点需要提前预防,就是业务模块间的递归引用。Go从语法上是禁止包递归引用的。但是游戏的业务模块很多,交叉是很平常的事情,所以需要提前设计一个项目结构来防止业务上的交叉碰到Go的语法限制。

具体的代码我就不写在这边了,思路就是通过公共接口的注册来解耦包的引用关系,我这边有个演示项目可以参考一下,并没有什么高深的设计在里面,就是一个脑筋急转弯而已。

funny/go-project-demo · GitHub

缓存层、持久层的实现方式不同团队差异巨大,这边我能分享的经验只有一点,就是尽量不要人工去维护缓存和持久化之间的关系,尽量做成自动的,这样才不会人工引发BUG导致数据损坏。

如果要说具体说法,我们目前是MySQL做持久存储,这样做数据分析和备份什么的都比较方便也比较可靠。缓存则是根据MySQL的结构自动生成代码映射到Go里边的。

Go做大数据量的缓存的时候需要小心GC的负载,如果你的缓存设计是内存吃得多但是对象很少,就不用担心这一点。如果是像我一样一条MySQL数据对应一个Go对象到内存里的,就要小心处理,要嘛做成按需加载的,减少对象数量。要嘛就是干脆用堆外内存来存储缓存数据,这样GC不会有负担。堆外内存的一些技巧我之前网上也有分享过了,原理比较简单,就是用cgo机制让C来分配内存。

通讯层也是各个项目差异很大的部分,我们团队是自己实现一套二进制协议格式,也有团队是用protobuf,也有用json,各式各样都有。这个按个人喜好和传统来做就可以了,差异不会差到哪里去的。

如果做自定义格式的协议,我这有个二进制操作的库可以用用:

funny/binary · GitHub

部署方面其实跟语言无关,单进程的结构都很好运维和部署,多进程都会麻烦一些,所有语言都一样的,这方面我没有太值得分享的经验。

# goroutine间如何高效通信

goroutine就是靠chan通讯了,没什么好办法。如果关心goroutine通讯的各种开销,最好是按自己的应用场景测试看看。

有些场景下chan通讯是不划算的,比如一个简单的map数据获取,可能用锁就可以了。有些场景用chan是必须的,比如做个多人互动功能。

还有就是带buffer的chan和不带buffer的chan的差异,最好通过试验来让自己有个直观认识,除了异步和同步的差异,还会有边界情况的处理差异,比如带buffer的chan阻塞了,在功能设计上需要考虑,否则可能引发严重问题,这个上面其实也讲了。

# 担心go1.5版本及以后的gc问题

如果有这个担心,就最好从项目初期就提前预防,比如从设计上就避免产生大量对象,或者就是前面说的堆外内存分配,或者是通过多进程结构来分散负担。

还有就是提前做好测试,对量级有个心理预期。

游戏已经比实时交易系统好很多了,正常的用Go是不用担心GC延迟导致服务质量不符合需求的,游戏会产生大量对象的地方就是缓存了,这个地方小心设计基本上就没什么问题了。

1.5版本的GC我还没测试过,因为用了堆外内存,现在不怎么关心这个了。。。

# 如何调试程序和快速定位线上问题

调试Go确实有点麻烦,如果要用GDB调试Go,你最好关掉Go的编译优化,否则可能出现调试不了的情况。另外就是靠打印了,所以我们项目里面有这样一个模块:

funny/debug · GitHub

线上问题定位要靠提前留好定位措施来实现,最常用的就是排查死锁和排查内存泄漏,可以参考一下这个模块:

funny/pprof · GitHub

死锁的时候通过lookup goroutine来获取所有goroutine的堆栈跟踪信息,然后排查死锁的原因。

内存泄漏或者效率问题通过cpuprof和memprof来定位问题:

Go语言程序的状态监控

保存cpuprof和memprof的工具函数在 funny/pprof 包里也有。

# 压力测试负载能力

游戏的完整压力测试我没做过,感觉没法做,游戏操作逻辑太复杂了。所以我的测试方式是对逐个可能成为瓶颈的点做benchmark或对算法做benchmark,来估计一个整体的效果。

另外就是开发期间持续监控所有请求的响应时间,我们团队的要求是在小于1毫秒,实际线上平均是30多微秒(不包含IO过程),有这样的响应速度,应该不用担心负载问题,如果有负载问题,会在请求执行时间上暴露出来。

用来监控请求执行时间的模块也在这个包里:

funny/pprof · GitHub

能力范围内只能回答这些了,我最近在研究怎么进一步提高开发和运维的整体效率,所以感觉自己还很多东西不懂,懂的只是一些皮毛的东西,当抛砖引玉了。