关于社交游戏中的同步服务器(长连接服务器)
博客好久没打理了,今天有时间,把在公司wiki上写的一些东西移植一下吧
我们的游戏至今已经上线第四个同步玩法了
有点想法,也有些疑惑,在这里发一下,欢迎各位tx各抒己见
准确点说,这里所谓的“同步服务器”其实包括两个部分的内容,第一是说明通信方式是长连接,第二是服务器不具有动态扩容的能力,也就是说想增加用户数不能通过增加机器的方式来做
当然,同步服务器按道理说只包含第一部分的内容,但在这里算是一个特殊的语境吧
1.区别(主要区别于普通的web服务器)
a.通信模式很显然的,不多说了
b.冲突的解决方式
(这里的特殊语境,是指做游戏常用的数据库:腾讯的cmem,redis)
现在我所知道的冲突解决方案有这么几种
一致性缓存锁:这种锁的劣势是没有普通锁的notify机制,也就是说,必须事先加锁,锁失败了只能退出,一些在逻辑中间的事务性操作用这种方式就不能实现了
一致性缓存cas:cas是cpu操作原语,也就是compareandswap,通常在一致性缓存中是基于乐观锁的方式(对比版本号)来实现的
redis作者曾说了这种方式性能是非常不好的,但是如果我们在程序逻辑做一些优化,也未尝不可
普通锁:这种锁也不好:很不优雅,必须将逻辑框在一个个的tryfinally中,而且容易出错,而且一旦在逻辑中作为一个常态出现了,那么就不得不把整个逻辑当做了一个巨大的耦合体(除非规范定得很好,能将锁分离,但大部分情况下貌似很难,起码我做不到)
cpucas:比较推荐这种方式,优点很明显,风险低,效率高,缺点也很明显,通常cas只能用于解决单个变量的冲突,一旦涉及到了多个变量,代码就很难写了
同步服务器比较偏向于使用后两种,而普通的web则一般只会用前两种
c.思维方式
这方面其实我一直都没想透
比如通常我们写功能都习惯于使用请求响应模式,但显然这种模式用户体验是偏差的
实际应用中,同步服务器其实能给予更好的用户体验,也就是由服务器去驱动一些事件,注意,这里说的是事件
也就是说服务端给前端返回的不是状态了,而是事件,去驱动前端
虽然写了第四个了,但是由于种种原因,一直都逃离不开请求响应模式,主要不知道前端应该怎么改变它的架构
只是在局部做了一些服务器驱动的功能
2.协议
(指的是flash做客户端,java做服务器端的特殊场景)
由于个人经验也不是太足,这里只讨论两种协议,protobuf跟amf,而也只考虑跟flash进行交互
之前仔细斟酌过amf好还是protobuf好的问题
首先先做一个对比
数据大小:
protobuf无疑是最小的,除了数据,它只多了一个类型编号,跟一个位置索引,当然,该压缩的都压缩了
amf:amf对数据也会做压缩,而对string还会进行重用(这点有可能如果传输大量的类似的文本信息,amf的压缩比会更高),编码方式跟protobuf差别不大
但是有一点很致命,它会将对象的类型信息加进去,包括变量名等信息,也就是说,如果是数组,可能会好一点,如果是每个对象只用一次,传n个对象,无疑这部分的数据量是很大的
效率:
由于游戏服务器基本都是出流量远远高于入流量,这里只考虑服务器向客户端发的情况
protobuf用的方式是生成代码,而amf是反射,虽然没做具体的测试,但无疑,差距应该是很大的,由于是同步服务器一般给前端发的都是短消息,频度高
易用性:
flash默认兼容amf协议,从通道中可以直接简单地解析出对象,非常方便,protobuf则需要编写文件,生成代码,重新编译,数据转换,等等
但是否amf真的完胜了?
经过与我们的首席架构师跟前端的讨论,其实不是这样的
同步服务器一般需要比普通的web更严谨的编码风格,在协议上也是这样的
protobuf定义了前后端交互的协议,非常明确,再也不需要前端debug,查看这样的方式,也就是说,即使我们用amf,我们也需要一份这样的协议,而且是纯手工写的(如一样的dto),个人觉得,会比protobuf产生的更麻烦
当然,为了使用protobuf,还需要一些额外的编码,但这些是一次性的
一比较之下,显然protobuf更好,更优秀,但如果是非常简单的应用,用amf也未尝不可
再补充一点
最近发现前端非常依赖于数据,归根到底其实是amf惹的祸,因为amf返回给前端的是一个动态对象,所以前端想要知道对象里面的值,都习惯性的debug,所以每次,都必须得后端写完,提供数据,前端才能开始做一些事情
这样的前后端合作模式显然是有问题的
相对而言,protobuf会给他们生成一个类文件,对象里面的变量都是确定的,能一定程度上的改变前端这个坏习惯
3.业务架构
现在4个同步项目的业务架构,都沿用了三国里面的面向服务的架构方式,也就是service,bo,这样的方式,在现在所写的4种业务中,这样的方式挺好用的
大体的区别是:
service后面默认都添加了一个参数:“连接上下文”
增加了两个配置文件
把返回的map去掉了,返回数据通过“连接上下文”发送
多了一种叫task的东西,跟bo同一层次,是一种主动运行的事件(这到底在我们业务模型里面应该是一个什么东西呢?之前考虑过让他居于服务层,让他去调用服务,但貌似也不好,因为task调用的逻辑与前端调用的逻辑没有任何地方可重用的,而且会破坏endpoint调用service的方式)
现在有一个问题是:这样的架构如果用于复杂的游戏,例如mmo,能不能用,可用性怎么样?
我们知道,像我们社交游戏的业务,都有一个特点,层次简单,比如过:用户,下一层是兵营,将领,客栈……都处于同一层次上,就没有更深入的层次了
然而复杂的游戏,显然会有很多层,比如说之前写的同步战棋战场,整个服务器有多个战场房间,战场包含着小据点,据点中又包含用户,用户带着将领,都是一层嵌套一层的,这就会我们拿数据的时候,每一个service都需要一层一层地找到数据,然后操作,我们看到的也许就是多个大bo,我们写方法可能都是Abo.doSomething,然后Abo又在方法内调Bbo,这样其实是挺不好的
还有一点,越复杂的业务,task的逻辑就会越多,最后task会越来越多,越来越乱
跟我们的首席架构师多次讨论过这个问题,虽然看法不一致,但我还是觉得,这种业务模型应该还是适用的,起码在有新的方式之前,个人感觉还是利大于弊的
4.异常
在同步服务器中,异常的控制很关键,必须考虑所有可能的情况,定义异常,并捕获,比如之前我在写cas避免冲突的时候,考虑到必须限制cas的次数,否则会造成数据库压力,但是当cas连续失败多次之后,怎么办呢?
以前是直接抛出一个异常,就不管了
但显然,在同步服务器中,这是有严重问题的
我们之所以用同步的方式,除了数据及时性,有一个很重要的部分:通常我们需要写的业务存在着大量的数据竞争。显然用非同步服务器来解决冲突是很困难,并且很不靠谱的一件事,它往往只适合数据分离的业务。
基于这一点,我们如果也像之前一样,抛出一个异常,那很可能会引起数据的不一致,然后是迅速地连锁反应,蔓延,相对而言,我们普通的业务对数据一致性的要求低很多,因为影响的最多是1,2个用户而已
5.同步模型
同步1v1,3v3,老-虎-机用的方式都是房间内单线程,将同步相关的操作封装在了线程池内
国战用的则是锁,所有都是显式加锁,写得很麻烦,而且不好看
总的来说,能不用锁最好还是不用的好,房间内单线程模型,既不用考虑可见性,也不用考虑冲突,而且可以没有限制地使用jdk的集合类,相对起来编码简单很多,房间内操作在锁的层次基本没有任何的耦合,而且不容易错,之前也考虑过,如果写复杂的游戏,也许我们可以把所有场景都抽象成一个个房间,这样,我们只需要考虑房间之间的切换就可以了,然后其他的都是房间内操作,这样的话,我们大部分业务都不需要考虑同步问题了
6.服务器组
我们知道,单服,又不可扩容,对游戏的限制是很大的,于是,有了服务器组的实现(现在做的新游戏原计划是做多区多服的,全部请求都用长连接实现(最后改变了初衷))
所以我们一个区,其实会需求一个服务器组,按业务去划分,比如说:场景1服务器,场景2服务器,交易服务器,战斗服务器,用户状态服务器,等
这其实会有个很关键的需求:网关服务器
既然用了java,肯定希望要用就用全套的,网关服务器也不会考虑用c的实现了
大家都说java比c慢,但慢多少?大家都没有个准确的说法,网上的文章看了几篇,不是测试错误就是检测方法有问题,没有可参照性
按之前在做消息服务器时,与我们的架构师tx做的c版本的比较
c版本:单线程,cpu100%,极限大概是32000qps
java版本:4核多线程,cpu60%-70%,极限大概是68000qps
如果按这个看的话,其实没有太大的区别(个人认为,如果每组能撑10w同时在线,那改为撑7,8w,也不是什么大不了的事情,也就是说性能如果没达到150%,可以忽略不计)
后来也试着在内网做了下压力测试,可是没把服务器压死,先把公司的路由器压死了,后来就没测了
出于这个计划,仔细研究了下mina,netty(还sb兮兮去看了下的游戏开源服务器darkstar,想去看看别人怎么设计网关服务器的,谁知道就一个分布式的业务框架,跟游戏没一毛钱的关系)
发现一个问题,在decoder的时候,mina都会将byte[]从directbuff拷贝一遍到heapbuff中(而且这块代码是写死的,很难动)
如果我们用netty或者mina做这个网关服务器,无疑,我们的做法大部分应该是将byte[]从入口拷贝到出口去,然后发送,这样就会涉及到byte[]的2次拷贝,相当于netty和mina中做的大量的细节优化,都被这简单的两次拷贝给抹杀了
当试着去修改netty的源码时,资料片改为了用类似普通社交的玩法实现,这个计划就不了了之了
之后也去问了下别的公司的开发者(java的),都是说现在这浮躁的网页游戏行业很少有服务器组的实现了,都是单服务器,撑3,5千人就差不多了
c++的代码很多这样的源码,像传奇,魔兽世界,等等,java难道做不了么?
7.时间问题
在同步服务器中,时间是个很恶心的东西,因为客户端是有渲染时间的,但后端没有时间的概念,后端是按事件去触发,去响应的,
举个简单的例子,一个简单的限制,A从x点移动到y点,时间,距离,后端其实是需要检测的,不然就各种外挂,但是如果前端每走一步又跟后端请求一遍,玩家会疯掉的
还有像资料片中的连招的释放,你必须在固定时间内按键,才算正确,又还有国战中我们需要按各自战斗时间决定排队顺序(谁跟谁打,什么时候打)
关于移动网上有的是各式各样的算法,这里就不罗嗦了
说一下在国战中和在资料片中是怎么解决这个问题的
(待续)