关于现网问题补偿修复的一些想法

Page content

现网发生bug,导致玩家流程卡住,或者物品丢失补偿,都需要在项目开发和设计阶段进行充分的设计和思考。本文介绍一些实际项目中用到的思路和一点思考。

物品补发及补发修复

物品补发常用的方式有:

  1. 邮件系统。 好处是玩家可以不用在线也不影响正在线的玩家。邮件的拉取可以在玩家登录时,也可以在特定的时间,如回到主界面,取决于业务的考量。邮件的领取,提供主动领取和自动领取。自动领取用于做一些不需要玩家感知的事情,如某个用于扭转玩家状态操作指令。缺点是邮件领取本身的可靠性需要权衡,在极端情况下重复领取或者领取完数据未存档导致的异常,后面单独尝试理解下这里的case。

  2. 发货服务。 发货服务位于业务集群,本身可以直接操作玩家数据库,查询、修改玩家数据。发货服务一般设计上设计为无状态服务,对外部而言就当成一次接口调用。无状态服务的缺点是,如果多点写会产生数据竞争,要求平台多个业务之间对相同账号的操作需要收敛到相同的服务。避免多个操作发货业务同时进行,如果都对同一个账户操作,会导致多点写的问题。多点写本身可以在业务层检测到,部分数据库提供乐观锁的服务,修改一次数据,数据版本号会提示,如果发生了同时写,会有一个失败。失败稍后重试,问题就能得到解决。

发货服务,目前在业务侧见过的实现方式大致有两种。

一种是提供直接加载并且修改玩家数据的能力,一旦数据被修改在线玩家的数据保存就会失败,会导致踢线。所以这种发货一般选择在凌晨玩家相对少的场合。这种方式的改良版本是如果玩家在线,直接将消息发送给玩家对应的逻辑服务,由逻辑服务直接操作数据,这样在线玩家的影响就会小很多。缺点是无法保证消息的可靠性,一旦玩家突然下线或者玩家所在的逻辑服宕机消息就丢了。而消息丢失对发货服务而言是一种难以决断的状态,重试还是认为成功都是模棱两可的问题。所以这种改良对于重要的消息来说还是明显不足,对于一些不太重要的消息可以通过这种方式避免对玩家踢线。

第二种是增加间接层。虽然发货服务是无状态的,但是间接层可以收敛到有状态。间接层取个更通俗的名称叫异步消息服务器。这里异步是指不需要立刻被玩家感知到,游戏逻辑服务可以按需拉取。拉取相对于发送问题稍微简化了一点,实际上异步消息服务器逻辑相对较少,主要是收敛数据,避免多点读写的问题,查询和修改,没有特别重的逻辑,改动也比较少,服务的可靠性更高一点。收敛的服务缺点也很明显,天然的单点。一旦机器宕机意味着未存档的数据丢失。

针对异步消息服务器可靠性,降低单点故障,可以简单的支持一下幂等性。本次的消息附带上一个唯一的票据,消息本身可以是多个消息的合包,取决于本次发货是单个还是多个。补充一下,这里讨论的发货,均以单个玩家为单位。异步消息服务器收到消息后,会同时保存该票据和消息,票据严格来说也属于消息的一部分。如果数据成功的存档,宕机或者内部网络中断,导致发货服没有收到回包。这时发货服可以选择一定时间后重试,当第二次异步消息服务器收到该消息时,立马检查到消息已经存档了,可以明确的告诉平台成功了,而且新来的消息不用再次处理。这里有个有意思的边界,如果这个时候玩家过来拉取异步消息服务器,时间不会读到未存档的数据。本质上异步消息服务器是一个无状态的服务。发货消息先到来,进行存档。玩家查询消息后来,会从db直接读取。虽然性能差一点,但是保证了读取到的数据是存档过的。避免玩家消费了未存档的数据,随后在异步消息服务存档前宕机,导致发货服再次发送数据,玩家会消费多次。

异步消息服务器状态讨论

异步消息有无状态,取决于两个重要的因素,一是性能,如果修改和读取都很频繁,无状态相对要差很多。二是承载,有状态需要把状态都缓存在内存中,虽然可以考虑LRU节省不少内存,频繁的淘汰和加载本身也会有问题。

如果在承载足够满足的情况下,读写频率相对较高,此时要用有状态的异步消息服务器,该如何设计?

如果数据正在存档中,读取消息来了,是直接返回数据还是等待存档完返回。等待能保证只消费存档的数据,避免中间消费中间数据导致不一致。

第二种设计是采用版本号的机制。修改一次数据,版本号提升1,这个版本号不需要数据库支持,逻辑层保存即可,作为数据的一部分进行存档。当读取消息到来时,立刻读取最新的数据带着版本号回包。需要在业务层下次读取时,判定下刚读取的数据和历史读到的最大版本号对比,业务层做了兼容。这个设计解决了业务<->异步消息服务 之间的一个即使存档丢失也不会重复消费的问题。

第二种设计对于发货服务而言却有一点问题。发货服务要保证数据能通过重试确认发到异步消息服务器。考虑前面提到过的一个极端情况:

发货消息 -> 异步消息服务器内存更新 -> 业务侧读取内存消息 -> 异步消息服务器宕机 (此时业务已经消费了中间态的消息) -> 发货服务收到回包超时

此时发货服务,再次重试发送消息:

重试的发货消息 -> 异步消息内存更新 -> 异步消息存档(可选)-> 业务侧读取消息

这里有个很麻烦的事情,如果丢失了多个版本的数据,业务侧以消费的版本号可能高于现在异步消息内存中的版本号。

有状态服务带来的问题比较多,异步消息机制本身属于一个非高频操作。采用无状态的服务,实现上更简单一点。这一节主要是想对这种case下的有状态服务推演,以及从消息的产生和消费的渠道保证可靠性分析。

异步消息的时效性

如果希望业务在有异步消息是尽量能感知,可以在异步消息服务存档存档时,发送一个新消息到来的通知给业务,业务触发一次拉取,这样就不会在登录这种感知比较弱的场景。特别是对于修复紧急的问题,又不希望玩家重登。

基于邮件的实现讨论

邮件和离线消息非常接近。邮件一般意味着客户端是有感知的,对于没有异步消息服务器的业务。可以用非展示的邮件,来实现和异步消息一样的功能,属于一点小trick。

代码热更

目前代码热更是一种相对简单的实现,利用服务定时的加载指定的so文件,将指定的函数替换成so中的新函数。修改代码段的属性,将旧函数进入的代码调整为跳转到新函数的指令。约束是对非内联和宏函数才有用,需要函数体本身大于13字节。这种热更流程上更麻烦一点,主要靠开发完成。而前面提到的发货的方式,则只需要运营就可以完成。

总结

采用异步消息服务器的方式,利用发货平台的ticket实现幂等。基于无状态的服务,保证业务侧消费的是存档数据,业务侧保存已消费的数据信息,避免重复消费。发货的方式依赖程序已经支持了特定的消息来放过玩家的某个状态保证流程。热更的方式相对而言解决更复杂的问题,但是操作更麻烦一点。