美文网首页
线上问题处理总结

线上问题处理总结

作者: fooboo | 来源:发表于2019-10-27 17:21 被阅读0次

    这边有个版本上的其他平台,然后相同的代码出现了一些问题,一份代码运行了快一年了,在好几个平台上跑,并没有出现这样的问题,所以比较奇怪。

    当高负载时,一切较容易暴露出来,这些都需要在开发时考虑的。比如对于C++协程,在一个线程中跑很多个协程,此时一个协程操作线程局部数据到一半,然后因为要请求某些资源会切出去,然后再切另一个协程回来并修改了那份数据,那么当上一个协程回来时,可能数据已经失效,虽然可能还引用着地址,但地址所代表的数据已经释放,这种情况在lua语言中比较常见。当然也并不一定要操作同一份数据,比如用lua说明一种情况:

    local function buyitem(player)
          local checkmoney = reqmoney(player)//这里协程会挂起
          if checkmoney then
                //这里操作player的钱并加物品等
          end
    end
    

    这里因为是协程处理请求,且操作的是player,那么第一个协程请求检查玩家钱的时候,可能因为业务设计要挂起,此时玩家进行了登出处理,那么player就失效了但这里player还引用着地址,所以是有问题的,后续的逻辑处理也没什么意义,并造成其他数据不正确。

    当然可能会有技术手段去同步协程间的临界区,操作同一个玩家数据,本质是要同步,有先后。前面一个操作完成后再继续下一个操作,但这里可能就需要小心处理。其中在游戏侧,对于分布式系统,也是有这样的情况,只是使用了id去代表一个实例比如玩家对象,某个业务进程检查通过后,会把请求转发到其他业务进程,此时转发的请求包括玩家id,会话id等信息,然后响应回来时,通过玩家id获取玩家对象,此时可能获取不到,并进行后续的回滚操作。而线程里的lua协程会引用着地址,所以这里简单点就使用id再获取下玩家对象。

    一般游戏在进行登陆过程中,两边初始化完毕后,会给服务器发prepare/entergame这种消息,表示准备好和进入场景,如果在entergame时,进行抛事件,而监听该事件的各模块要处理一些数据,如果里面有同步请求的要挂起的,那么就会导致报错,完整的登陆流程没有走完,同时请求回来后,资源已经释放,可能也有卡顿情况发生。但这里想说的是,那么多代码,怎么去定位。因为我grep一些相关代码后,发现有很多,并且查看了十几个文件并没有找到明显的问题。这里使用了一种方法,可以快速的定位是哪儿发起了同步调用。包括如何在几十万行代码中定位有问题的代码,总不能一个一个看吧,或者使用网上说的二分查找?

    另外,当新增加业务需求时,比如某个小系统,增加战力数值那种。因为游戏业务中涉及模块太多,不能更新某一个属性导致重新计算一遍战力等加成,因为更新属性很频繁,那导致计算量太大,后来进行特殊处理。在我加另外的增加战力的代码时,发现有些逻辑有点乱。我这边为了减少不必要的该模块的属性总加成,而使用额外的存储,所以需要上线时计算一次缓存,有变化更新该缓存,切场景等操作把这份值带过去。然后当玩家上线时,会先计算总的属性加成,再抛登陆事件,因为第一步时还没先计算我那模块的属性加成,所以返回为零,然后第二步监听到登陆事件再计算该模块,这样少加了属性和战力。应该把属性计算挪到初始化模块,而初始化模块在基础代码中,没有业务代码相关的,所以这里比较奇怪。这里还是使用了存储中的一个字段临时解决这个问题,因为如果改底层代码可能有风险,毕竟已经运行好久了。有些代码块需要排好顺序,哪个先发生哪个后发生,先发生的需要什么数据要提供好,而不是依赖后发生的,否则会拿到不正确的数据。

    按理来说,登陆后,初始化玩家数据,包括其中因为数据兼容等问题修正,然后再派发给各模块,该怎么处理由各模块来,之后进入场景再刷新视野等,当然有些需要等玩家进入场景后再处理。可能当初因为项目工作量及工期原因,有些模块并没有清晰的规划,导致后面加功能越来越多,不太好分离出来,甚至引起微妙的bugs。早期的设计过多考虑弹性也要多注意下。定期保持重构相关代码。

    2019-11-01补:
    这几天解决几个严重的问题,都是底层框架之前没有测试出来的老问题。之前压测没有出现,或者跟特殊操作方式有关,或者在某些关键路径上阻塞导致的有顺序关系的逻辑,所以这些机器人不大好测试功能性问题。

    前几天总结过一次,有个问题没有分析出来,周一解决了然后发现其他问题,只需要手动修改一下让他故意发生阻塞,在不改变原来的逻辑是语义基础上就会复现。

    之前那个问题是一个agent被两个玩家占用。查看前面日志,看上去应该是正常的,然后某个玩家在游戏中,后来断线。过几秒,另外一个玩家登陆占用那个agent。因为不可能在断线的情况下那么快释放agent对象,所以分配的时候不会给另外一个玩家。再说分配的时候是互斥操作(虽然这里后面优化删除同步代码),不会出现问题。然后猜想是不是释放的时候,对同一个agent插入两次到资源池中。因为一条消息是通过协程处理的,而资源池是共享资源,所以释放的时候也要互斥,这里判断逻辑和释放顺序可能造成问题。后来分析释放release的地方,虽然有好几处,复现的过程比较艰辛。最后解决办法在释放的时候设置个标志,然后如果double release时候检查下即可并记录日志。

    然后这里改完后,发现申请和释放也有问题,申请的时候如果阻塞,而释放的时候因为是空的所以跳过,然后申请资源的协程切换回来,导致把agent资源给已经释放的对象,造成资源泄露。这里复现的时候也是需要特殊操作,因为本地基本不会发生阻塞,所以要添加几行代码,然后就出现了和线上一样的问题,最后修复只加了几行代码。

    上面这个问题间接会造成排队登陆的假象,实际服务器上并没有那么多玩家在线。因为一般单个服会限制一定数量的玩家,比如五千,超过后会排队等,并实时更新客户端界面当前自己排在哪个位置。如果因为一些异常情况,导致统计只增不减,哪怕有玩家离开游戏时也是这样,那就有问题了。

    以上几个问题是老问题,奇怪的是一年后的今天才暴露,之前的几个版本已经跑了一年和大半年等。

    最后是lua做到了不停服热更新代码,和代码注入修改某个变量值,不像C/C++这样的,虽然也可以“热更新”,本质还是重启进程,另外也需要架构的支持。

    这里附上lua中同步的实现:

      1 local skynet = require "skynet"
      2 local coroutine = coroutine
      3 local xpcall = xpcall
      4 local traceback = debug.traceback
      5 local table = table
      6 
      7 function skynet.queue()
      8     local current_thread
      9     local ref = 0
     10     local thread_queue = {}
     11 
     12     local function xpcall_ret(ok, ...)
     13         ref = ref - 1
     14         if ref == 0 then
     15             current_thread = table.remove(thread_queue,1)
     16             if current_thread then
     17                 skynet.wakeup(current_thread)
     18             end
     19         end
     20         assert(ok, (...))
     21         return ...
     22     end
     23 
     24     return function(f, ...)
     25         local thread = coroutine.running()
     26         if current_thread and current_thread ~= thread then
     27             table.insert(thread_queue, thread)
     28             skynet.wait()
     29             assert(ref == 0)    -- current_thread == thread
     30         end
     31         current_thread = thread
     32 
     33         ref = ref + 1
     34         return xpcall_ret(xpcall(f, traceback, ...))
     35     end
     36 end
     37 
     38 return skynet.queue
    

    大概就是对某段代码形成临界区,然后如果有其他协程进入则进入等待队列,然后等第一个协程处理完进行唤醒。

    往后,还是会继续学习,提升能力,保持一颗好奇心,多思考。

    相关文章

      网友评论

          本文标题:线上问题处理总结

          本文链接:https://www.haomeiwen.com/subject/yseevctx.html