用 Go 语言写游戏服务器也有一个多月了,也能够明显的感受到两者的区别。这篇文章就是想具体的聊聊其中的区别。当然,在了解区别之间,我们先简单的了解一下 Go 语言本身。
1. Go 语言的特点
Go 语言跟其他的语言例如 Java 比起来,算得上一门很年轻的语言。Go 语言是由 Robert Griesemer、Rob Pike 和 Ken Thompson 于 2007 年在 Google 开发。并于 2009 年正式发布。
Go 语言的设计理念围绕着简洁这两个字,认为少即是多。如果你熟悉 Java,用 Java 那一套语法命名跟 Go 做对比,可以很明显的体会到这种感觉。
Go 的特点可以简单的概括成以下几个点。
1.1 静态类型和编译型
首先 Go 是静态类型,静态类型就是编译时就知道每一个变量的类型,得益于此,在编译的阶段就能够发现很多问题。而如果是动态语言,例如 JavaScript,有些问题直到运行时才能发现。
Go 是编译型语言,看到编译型大家脑子里可能会想到另外一个词解释型。两者的区别从字面上来理解其实已经可以看出来,我用一个简单的例子来类比一下。
- 编译型 去餐馆吃饭,点了菜之后,饭店会等所有的菜做好了再上
- 解释型 去餐馆吃饭,点了菜之后,陆陆续续的边吃边上
1.2 跨平台
顾名思义,你写的 Go 源码在所有的系统都能够运行。
这点其实很好理解,例如 Java 的口号是 ”Write once, run anywhere”。我们都知道 Java 是编译型的语言,但是 Java 在编译的时候生成的是字节码,这个字节码与当前的操作系统无关,与 CPU 也无关。
这种字节码必须依赖 Java 虚拟机才能运行,而虚拟机会将操作系统和 CPU 之间的差异与用户屏蔽。对于编程的人来说这个过程其实无感知的。而对 Java 来说,语言本身的跨平台并不能代表代码可以跨平台。
Go 的跨平台从某种方面来说,与 Java 类型,我们需要安装与当前操作系统相对应版本的 Go。编译出来的可执行文件会根据操作系统的不同而有所不同。
1.3 自动垃圾回收
与 JVM 一样,Go 在运行时的内存管理(GC)由 Go 语言本身来管理,不需要程序员的参与,但是我们可以干预。
1.4 原生的并发编程
何为原生?我们都知道,在 Java 中如果要实现并发,需要外部的类库支持(Thread),而 Go 不需要从外部再引入任何依赖。支持使用关键字 go
即可。而且 Java 中是通过共享内存进行通信的,熟悉 Go 的应该都看过一句话“不要通过共享内存来通信,而应该通过通信来共享内存”
1.5 完善的构建工具
从获取、编译、测试、安装、运行和分析等一系列流程都有自己的内置工具。例如获取可以使用 go get
命令来下载更新指定的代码包,并且对它们进行编译和安装,可以使用 go build
对源码进行编译,用go run
命令来运行 Go 的程序,用 go fmt
来快速格式化代码,统一代码风格。
1.6 多范式编程
目前主流的编程范式有命令式编程、函数式编程和我们最熟悉的面向对象编程。在编写 Go 的代码的时候,我们可以选择使用面向对象的方法,也可以使用函数式编程的思想,相互结合,相辅相成。
例如,在 Go 里面也可以用接口来描述行为,也可以使用纯函数来避免出现副作用。因此,多范式编程就是指这个语言支持多种编程范式的。
1.7 代码风格强统一
使用 Go 的内置工具 go fmt
即可快速的将代码格式化成官方统一的标准,以此来达到代码风格统一的目的。甚至可以用 golangci-lint 来检测你的语法跟内置的标准语法是否有冲突,完全可以将这个检测工具挂在 git 的钩子上,以此来达到强制的代码风格统一的目的。
1.8 活跃的社区
还有一个很重要的特点是,国内的 Go 的社区十分的活跃,这对于 Go 在国内的普及起到了很大的作用。
2. 用 Go 的优势
先说一下我对 Go 语言的看法,我认为 Go 在服务器这块是非常有优势的。以后如果有高并发的应用场景,那么大概率这个服务就是用 Go 写的。不知道大家有没有发现,摩尔定律正在失效。近十年内,硬件的原始处理能力都没有太大的提升。显然,一味的增加晶体管的数量已经不是解决问题最好的方法。
NASA 前不久发布到官网然后又迅速删掉的文章透露了,Google 可能已经实现了量子霸权,通俗一点说就是拥有超越所有传统计算机的计算能力。而放置更多的晶体管的代价也越来越高,所以现在厂商都在向处理器中添加更多的内核来提升性能。
就像大家熟悉的 Java,虽然 Java 本身支持多线程,但是在 Java 上使用多线程编程代码算是比较昂贵的。在 Java 中创建一个新的线程就会消耗接近 1M 左右的内存。假如你真的需要支持运行上千个线程,那么服务很可能运行着就 OOM 了。除了内存消耗外,还会存在由于支持多线程带来的并发和死锁等问题。
而 Go 中,使用协程来代替线程。而且一个协程所消耗的内存比线程少了很多倍。同样的物理设备限制,你可能只能启动最多几千个线程,而协程能够启动上百万个。而且不同的 Goroutine 可以通过信 channel 进行安全的通信。
3. 游戏服务器和 Web 服务器的区别
有些对游戏服务器的介绍可能会说,游戏服务器是一个需要长期运行的程序,然后怎么怎么样。我个人认为 Web 服务器一样的需要长期运行,也需要响应不定点不定时来自用户的请求。两者从宏观上来看其实没有本质的区别。同时 Web 服务器也会对于稳定性和性能有要求,游戏服一般分为大小服,我们这里都按照小服举例子。
3.1 状态
首先要提到的就是状态。可能你会听说过一个概念,游戏服务器是有状态的,而 Web 服务器是无状态的。什么意思呢?Web 服务器的数据流大多直接会到数据库中。而游戏服务器的数据流首先会到内存中,然后定期的写入数据库(落地)。
换句话说,游戏服务器本身的数据与数据库中的数据在运行期间会存在一个数据不一致的窗口。如果此时游戏服务器宕机了,那么就会造成数据首先到的内存数据与数据库存的数据不一致。
而 Web 服务器则不会有这样的问题,Web 所有的数据状态都会落地,而且可以针对操作加上事务,不用担心因为操作失败而引入脏数据。正因为有了状态的约束,游戏服务器就会很慎重的使用内存、CPU。以求在资源有限的情况下,最大化的提高的承载量,并且降低服务延迟。当然,Web 服务器会为了降低某个接口的响应时间而去做对应的优化。
3.2 扩容
在 Web 服务器中,如果你不能评估一个服务所面临的压力,又不想因为瞬时的热点访问导致服务直接不可用的话,完全可以设置成自动扩容,因为每个服务只是单纯的接收请求,然后处理请求、返回结果,不会将数据保存在服务器的内存中。要有数据存到内存,那也是在 Redis 中。而 Redis 数据丢失对数据的一致性基本没有影响。
但是在游戏服务器这边很难做到像 Web 那样灵活。首先,数据的流向不是数据库,而是内存。
举个很简单的例子,玩家的主城被攻打着火了,如果有了自动扩容,很有可能在落地的窗口内,玩家再请求一次,请求到了另一个实例。主城又没有着火了。因为数据都会先存在内存中。
再举一个例子,玩家氪金买了一个礼包。然后退出游戏,落地窗口内再次上线没了。这就不是单纯的数据问题了,玩家这是花了真金白银买的道具,突然就没了,一两个还好处理,如果多个玩家都出现这样的问题,那这就属于严重的线上事故了。修复数据的工作量十分的大。
所以,对于一个游戏服务器,所能使用的内存和 CPU 的资源是非常有限的,不像 Web 服务器可以不用花很大的代价做到横向扩展。这也就是为什么游戏服务器会十分十分的注重代码的性能以及稳定性。
3.3 稳定
就像上面说的例子,如果游戏服务器运行中出了 BUG,导致服务直接不可用,或者说通过这个 BUG 刷到了大量的道具,将是一个非常严重的线上事故。
而对于 Web 服务器来说,如果是管理系统之类的,有可能会有脏数据值得一提的是,脏数据对于 Web 来说,排查起来也是一件很头疼的事情。如果没有脏数据,只是服务暂且不可用,而且如果用的是微服务架构,重启服务的代价是相对来说比较小的,只有正在重启的服务的业务是不可用的,其余的部分则可以正常的访问。
而对于游戏服务器来说,服务器重启影响的是全服的玩家。玩家在停服期间,甚至连游戏都进不了,特别的影响玩家体验。而且,如果停服之前服务器的数据落地出现了问题,服务重启之后会将数据从数据库 load 到内存中,此时同样会造成数据不一致的问题。
3.4 性能
从我的经验来看,在做 Web 服务器的时候,没有为了减少 GC 的压力,为了少占用内存去做过多的优化。当然这是因为项目本身的体量不大,如果 QPS 很高的话,Web 服务器同样很需要注重性能,只不过游戏服务器需要一直特别注意这个方面。
不过在 Web,如果访问量很大的话导致单个服务不能扛住压力,大部分人首先想到的解决方案应该就是搞多个实例,毕竟可以做到很轻松的横向扩展。
在游戏服务器里,会把服务器的资源看的相当的宝贵。例如,能不落地的字段就绝对不要落地,某个字段的值可以通过已知的条件算出来的,就尽量不要定义在代码里。不过这也要看具体情况权衡运算量和调用的频率。因为上线之后,如果遇到了数据不一致,维护的数据越少,修复数据的难度就越小。
3.5 严谨
这一点上来说,我认为是两者都很关注的一个重点。只不过,在游戏服务器的某些情况中,如果服务器抛出异常或者 panic。其造成的后果会被游戏特殊的环境放大。
例如,召回你的在外部队失败了,那么部队就会一直在外面且不可用。这跟在浏览器中点一个按钮没有反应比起来,影响相对较小。而且使用微服务架构,在修复问题之后可以以很低的成本来重启对应的服务,而游戏服务器中还要修复一次数据。
再举一个很极端的例子,点击商店,玩家要准备氪金了。但是却发现进不了商店,也可能不能获取商品列表。这些会直接影响到游戏的体验,甚至收入。
而对于 Web 来说,服务器的稳定性同样很重要。不然根据业务的不同,造成后果的严重性也有可能不同。影响了用户体验,就会直接影响到产品的口碑。
3.6 数据传输格式
熟悉 Web 的都知道,数据传输格式是 JSON。而在游戏服务器中是 Protobuf,是由 Google 开发的数据传输格式,与 JSON 类似。Protobuf 是二进制的,二进制数据量会比 JSON 更小一点。而且,如果传输的字段是空值,就不会被传输。而 JSON 如果是空值,一样的也会被传输。
无论是在什么样的环境中,举个例子,Node.js 和 Java 中,Protobuf 的性能表现都比 JSON 好。在 Java 中,Protobuf 甚至要比 JSON 快了接近 80%。如果 Java 的服务之间通信有了性能瓶颈,可以考虑服务之间使用 RPC 来通信。
但是凡事都具有两面性。Protobuf 的缺点仍然存在:
- 文档较少
- 社区与 JSON 的对比起来
- 可读性没有 JSON 好
4 总结
以上就是这两个月以来,总结的两者的区别。只是从大体上做了一个对比,并没有具体深入细节。细节的话有可能会在以后单独的来介绍。
往期文章:
- Go 中使用 Seed 得到重复随机数的问题
- 从 Web 转到游戏之后
- go 源码解析 -Println 的故事
- 用 go-module 作为包管理器搭建 go 的 web 服务器
- WebAssembly 完全入门——了解 wasm 的前世今身
- 小强开饭店 - 从单体应用到微服务
相关:
- 微信公众号:SH 的全栈笔记(或直接在添加公众号界面搜索微信号 LunhaoHu)