其实第一次创业在技术上还是有不少历史问题的,所以在第二次创业就趁机做了很多弥补,下面详细说一下。
一.运行环境升级
直接列在这里
centos 6.5 x64 -> centos 7.5 x64
mysql 5.5 -> mysql 5.7
redis 2 -> redis 4
python 2.7 -> python 3.7
django 1.6 -> django 2.1
protobuf 2 -> protobuf 3
老版本有很多的问题,比如mysql5.5的性能问题,redis的dump性能问题,python2的中文处理问题,django1.x系列无法自动migrate,protobuf 2的default混乱问题。
所以当把所有这些升级结束后,真是说不出的酸爽啊。
当然,代价也蛮大,比如基本之前所有的python库都做了一遍python3的兼容。
二.架构设计升级
I. 注册登录设计
以QQ登录举例,之前的方案是客户端拿到用户openid和token之后,直接连接gateway进行验证。
这有几个很大的缺点:
- 因为gateway是同步的(因为异步会增加复杂度),而第一次token验证是要经过腾讯的webapi,时间不可控,所以不得不请求转发出去或者通过celery做异步处理,导致代码逻辑非常复杂。当被DDOS时,也极容易被针对。
- 没有游戏自己的统一user token,外部验证和内部逻辑没有分离,每次接入新的账户SDK都很复杂
但其实这是完全可以解决的。
客户端拿到用户的openid和token之后,连接游戏的webserver进行验证,我们web server可以通过gevent做成异步的,性能极高。
验证通过之后,创建游戏内部账号,并返回用户的游戏内部账号的userid和usertoken。
之后客户端再使用获得的userid和usertoken,连接gateway进行登录,这里的登录就一定是只和游戏内部相关的了,所以不用担心同步的问题。
II. 进入游戏房间设计
在之前的棋牌游戏中,我当时为了简单,采用了大厅和游戏服公用一个tcp连接的方案。
这个设计的好处是用户在变更房间时十分迅速,因为不需要等待建立新的tcp连接。
但是缺点也比较明显,就是分布式和负载均衡,容灾等只能做在gateway的后面了。
当然,后来我们也确实做到了,但是总归这并不是一个特别好的设计。
原因如下:
- 即使大厅和房间不共用一个tcp连接,也可以实现快速的房间变更。即只需要房间服务器本身的gateway是无状态的即可。
- 大厅服务器和房间服务器的重要级别不一样。
DDOS在攻击的时候,打死大厅服务器顶多影响新用户注册/登录,而不会影响已经在游戏中的玩家。但是攻击房间服务器会直接影响玩家体验。
所以一般黑客是更希望攻击房间服务器的。而我们把大厅服务器和房间服务器合在一起,其实是变相方便了攻击者。
当然,新方案也带来一定的成本,即客户端要比之前麻烦一点。
但这也是我现在越来越倾向的观点:
必要的时候,可以要让客户端复杂一点。
举个例子: 之前在处理换房的时候,我们的做法是,客户端发送一个换房请求,服务器收到请求后,需要先查到玩家所属的房间,之后执行退房,再执行进房。
但这导致了服务器的逻辑极其复杂,而且万一中途出现bug,客户端所处的状态也要跟着变化。
而我现在的做法将会是:
客户端发送一个退房请求,并且带着自己所在的房间ID,服务器收到请求后,会以收到的房间ID去判断房间与用户的关系,如果存在绑定关系,则进行退房操作,并告知客户端成功。
客户端手动退房成功之后,向服务器发送进房请求,服务器处理即可。
可以看到,新的方案虽然客户端稍微麻烦了一点,但是整个逻辑和容错能力都大大增强。
III. 房间服务器的设计
因为之前的德州棋牌是由下筹码的概念的,而对于筹码是绝对不能丢失的。
所以这也是德州的房间和王者荣耀的房间最大的区别。
即,房间数据不可丢失。
所以当时我们的做法是,把整个房间的数据都放进来redis里面,每次有请求的时候就从redis读取出来,处理完了之后再写回去。
这样确实极大的保证了数据的安全,从而确保我们每次发布服务器都可以直接重启而不影响线上服务。
但这也不是没有代价的:
-
多进程访问统一redis的房间数据导致需要分布式锁的问题。
这个问题在访问量小的时候可以忽略不记,但是访问量大了之后会极大的降低性能。
因为redis默认实现的分布式锁不是排队的,即很有可能一个进程申请了很久的锁,但运气不好就是拿不到。
所以后来,我们特意为这种情况改了server架构,每个房间数据只会有一个进程进行修改。
当然,读取是可以不用锁的,所以问题还算能OK。
但是,即使如此,后来单个进程的处理能力还是成为了瓶颈,毕竟我们是用python做的,cpu密集型的处理本来就满,而棋牌游戏又有很多需要计算概率的部分,所以后来问题还是蛮多。 -
redis本身的使用错误
redis的定位原本就是缓存服务器,但是我们这样用就是假设了redis的数据不能丢失的。
这导致我们付出了极大的代价,比如一开始我们做双机热备+冷备,而为了冷备的实时性,我们设置成60秒内有一次写就要备份一次。导致机器的cpu和磁盘io常见飙高。
后来我们实在对于redis的运维感觉力不从心,就切到了阿里云的托管redis上。但是托管redis也只是在热备上做的比较好,冷备基本上一天才一次。
真要是出现热备也挂掉的情况下,冷备派不上一点用处。。
而这次我们的多人竞技游戏更像王者荣耀的房间,数据丢了就丢了,大不了再来一局。
所以我们把所有的房间数据都放到了进程内存里,如果真的运气很差,进程崩了,那也只能当作数据丢失处理了。
当然,这并不代表我们做不了服务器热更新。
最简单的做法就是部分切换:
- 临时申请一批服务器,部署新代码,并开始分配新版本的游戏任务。
- 已经在运行中的老房间服务器,不要停止。但是一旦游戏结束,不再分配新的游戏任务,进入空闲状态。
- 等所有老房间服务器都进入空闲状态后,统一升级新代码重启,开始分配游戏任务。
- 第一步里部署的房间服务器不再分配新的游戏任务,并在全部进入空闲状态后,进行回收。
上面的方案基本是没有问题的。
另外,说回到德州不能丢筹码的问题,其实用新的方案也不是不能解决的,即进入房间时的兑换筹码,并不是真的扣除了玩家身上的金币,而仅仅只是锁住了。
等到玩家从房间退出的时候,在筹码兑换回金币的同时,将锁定的金币才进行真实的扣除。
这样,即使真的房间数据丢失,服务器在检测到异常的锁定金币后,会回退给玩家,那么玩家也只是相当于没赢没输。
也可以再安全一点,即每局结束后都进行一次牌桌数据的落地保存,这样数据丢失也只会丢失一局的数据。 当然,部分的玩家依然会有意见,尤其是那些赢了大钱的人。
但是只要代码足够健壮的话,房间进程挂掉和redis挂掉的概率也没什么太大区别吧。
好了,时间有限,先写到这里。
lizhaochao on #
上k8s哦
Reply