Daly的游戏人生

服务器程序常见bug总结

   最近整理了过去一年发生过的bug,包含跟其他项目组程序朋友交流的例子, 都是大家发生过的真实营运事故。
   游戏服务器程序,很多bug的原因都是共通的。抽象出了以下10点启示, 作为checklist, 写下来以后写程序review时自检:

1. 安全边界问题
     对于有界限的东西(数值,buffer空间,队列或一切对象容器),一定要考虑越界判断。
     启示:用snprint, strncpy等限制长度.  永远都要考虑超过边界的情况
               数值加法和乘法:考虑上限溢出; 
               减法:考虑负数; 除法,判断分母

2. 输入参数非法
    case1: 扣钱逻辑,减去一个负数,变成了加钱。  
    case2: int型大负数相加,负溢出变成大正数
    启示:test case要全覆盖输入参数范围, 处理各种可能的情况

3. 上下文改变错误
     共享变量/全局变量被外部改变,这似乎很常见,而且有时很隐蔽。在异步回调的情况下更常见。

     check A变量 
     call func_B()
     ....
     A变量被func_B改变了, 但继续信任A变量check的结果。

    启示:白盒复查代码时,注意检查调用后的变化。
               减少共享变量和全局变量的使用
               外部接口调用后,注意共享变量的更新和恢复
     启示:在最接近执行的地方,检查上下文变量。不信任调用者,如果效率不关键,多一遍冗余检查没有坏处

4. 执行中断
     动态脚本抛异常,或者引擎层面的EINTR中断信号,都有可能中断代码执行,需要考虑函数的重入性问题。
     启示:要检查一致性,有些逻辑不允许多次被执行(比如发奖励),需要有状态变量确保只执行1次(避免出刷bug)
     推广到异步环境(多线程,多进程,各种回调),事务的中断也有一个重入性问题,解决方法也只有一个:用一个唯一可辨认的状态变量,保证某些逻辑不会被多次执行(比如购物应用中,用唯一订单号来识别,状态改变是一次性的,当逻辑运行多次,也不会重复加物品,或者重复扣钱了)

5. 终止条件问题--死循环
     case: 异步环境中,RPC远程调用,调用成环,逻辑一直不结束。
     启示:while或递归的终止条件,逻辑全覆盖检查,避免死循环。较深层次的互相调用,要注意是否出现了递归,是否有可能死循环。

6. 关联数据操作的不一致
     例子:Employee对象有company变量, Company中有employee变量,
          如果操作改变其中一方,而另一方没有改变,则造成数据不一致。
      (数据库表可以指定constrain, 关联表删除, 但代码变量中需要程序员自己实现)
       双向引用的数据一致性问题,要特别注意。
       为什么要双向引用?为了查找效率,而避免遍历其中一方.
       这个问题本质是数据一致性问题,编程中遇到的很多bug也归结到这个问题,比如野指针,就是因为数据结构相互引用的操作不一致造成的。
       处理这个问题,个人经验是,他们的attach,detach操作尽可能在同一个模块,不要分散在多个地方随意修改,所有修改都集中在同一级接口做。

       同理适用于new, delete, malloc, free这些分配,释放,都集中在同一层的接口/模块文件中做,debug起来也容易;非常反感在一个地方new, 然后不知道哪个模块去delete, 很容易泄漏或者野指针, 无论如何,想办法传递这些指针,一直传到分配他所在的模块文件中释放,而且new和delete的接口代码要靠近,方便查找问题。

7. 涉及多玩家,防止笔误传错参数
     经典错误: foreach(uid in team) some_func(usernum, xxx)   
     经典错误:有usernum和target两个对象,调用函数搞混了。review时要仔细检查

8. 特殊分支忘了return
     异常判断等if分支忘了return。导致逻辑继续往下走。这属于笔误问题,测试期间未必能留意的到。

9. 异步返回没清变量
    对于异步操作,如果在返回时清变量,这时如果不能保证把变量清掉(比如期间玩家下线无法离线修改该变量),就会出刷。
    启示:对于已奖励标记,一定要保证各种情况下领奖后能正确记录。

10. 瞬爆容量上限
     case1:  网络待发送队列,因为瞬间大量请求,塞满抛异常,导致流程受影响。
     case2:  大量连接请求,listen的accept没有规定单次读事件的accept,用了while(true), 导致爆机
                在listen fd的读事件回调中, 通常会accept所有新的连接请求,如果用while(true)而不设一个上限,就有可能被攻击(想象一下客户端也用一个死循环来做connect)。
                一方面要限制单次接受的socket次数, 另外各个状态要有超时机制,踢掉不寻常的连接,以防被攻击占尽资源。

     case3: 异步情况下,要限制操作者连续频繁的操作。(比如在请求入口处增加最少时间间隔限制,避免玩家狂点,形成雪崩效应)
                (同时要考虑用户体验,不要让玩家死等,可以做一个提示跳转,或者等候的动画)

参考资料:
附上最近看的一篇文章
<Writing-reliable-online-game-services> 作者曾是魔兽争霸和星际争霸,battle.net的开发者,
里面讲的point也是游戏里经常遇到的可靠性问题。
http://www.codeofhonor.com/blog/wp-content/uploads/2012/04/Patrick-Wyatt-Writing-reliable-online-game-services.pdf



posted on 2012-11-30 14:14 Daly 阅读(2304) 评论(5)  编辑 收藏 引用 所属分类: 游戏开发

评论

# re: 服务器程序常见bug总结 2012-12-01 23:11 Jcilz

有共鸣,总结得很实际,顶。  回复  更多评论   

# re: 服务器程序常见bug总结 2012-12-04 16:39 谭军

listen的accept没有规定单次读事件的accept,用了while(true), 导致爆机,
这个是什么意思啊?  回复  更多评论   

# re: 服务器程序常见bug总结 2012-12-07 12:31 Daly

@谭军
一般用epoll等event驱动的典型写法是,listen的fd有读事件,那么在回调函数里就会accept一个新socket, 如果是 while(true) { accept() }不断接受新连接, 就有可能被攻击。假如管理socket的容器没有加上限,就会爆满。  回复  更多评论   

# re: 服务器程序常见bug总结 2013-05-08 14:11 egmkang

那是因为你listen设置的backlog值太大了吧  回复  更多评论   

# re: 服务器程序常见bug总结 2013-10-08 12:14 coderchen

@egmkang

listen设置的backlog是第一次和第三次握手的两个队列之和吧。我感觉博主说的对,如果有个客户端不停的connect,服务器while中accept就悲剧了。  回复  更多评论   


只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理