在线状态与心跳维护--暂时先写着,过段时间补充点代码

 
 
 

状态的描述:

  状态显示用于只管了解服务器或者客户端当前处于哪个执行流程中。如WEB业务调度系统中,需要了解每个业务调度的执行状态时需要状态;在C/S系统中,C端的业务状态或者配置状态需要在S端一一展示。
      
 
C/S架构中的状态描述:
 
     C/S架构中的状态分为两部分:一部分为客户端上报的客户端当前的状态,一部分是客户端状态转化为服务器的显示状态。
 
    前者直接由客户端根据实际状态上报给服务端;后者可能需要根据前者上报的状态信息做转换,也可能不用做转换直接拿来使用。一般而言,推荐客户端上报的状态定义与服务器显示的状态定义一致,这样在项目理解和处理上回方便很多。
    如:
        客户端定义第一位=1,表示在线;=0表示离线。
        服务器端定义第一位=1,表示在线;=0表示离线。
 
   另外一种情形就是需要做状态变换。状态变换根据实际需要又分为以下情况:
      1、客户端的状态需要延时显示。如:客户端将报警状态上报给服务器后,服务器就算在下一刻收到了客户端处于非报警状态,也要将该状态保持显示1小时。
      2、客户端的状态与服务器状态显示定义不一样。
        客户端定义第一位=1,表示在线;=0表示离线。
        服务器端定义第二位=1,表示在线;=0表示离线。
      3、多个客户端的状态合并,才能得到一个服务器状态。
        客户端的第一位与第二位都是1,才能得到服务器的某个状态位为1
   
 
常见的状态:
 
    在线/离线,命令执行中/命令未执行,软件升级/软件未升级,插件变更/插件未变更,等等。实际中使用到的状态往往与项目有关。
 
 
C/S 架构中的状态显示:
  
  客户端状态实现:客户端心跳维护
  服务器状态实现:服务器自己的心跳维护(定时任务实现),服务器自己模拟一个心跳
 
 
 

状态维护过程

    某项C/S目中的状态维护,基本以下面为思路实现。
 
    
 
状态实现与心跳
              1、客户端的心跳用于记录客户端当前的运行状态,客户端将自己的状态上报给服务器后,服务器对这些状态做保存。
              2、服务器的心跳用于实时刷新处理客户端的状态,并生成具有服务器自己风格的展示。一般将服务器的状态与客户端的状态定义成一样最佳,这样需要做的改动最少。单独实现一个服务器心跳是因为客户端是存在掉线的可能,只有服务器主动刷新才能发现掉线的客户端。而将服务器与客户端的状态定义成一样的状态,可以避免状态转换等操作。
    3、某些特殊业务,可能无法上报自己的状态。如客户端升级时,客户端可能程序死亡了。
 

从代码维护的角度来分析:

    由图上可知,将状态维护交由统一的心跳处理后,代码的维护难度会简单很多,因为如果将不同状态拆分到依赖不同的业务,一旦其中的任何一个状态出错了,都需要到不同的业务代码层中寻找对应的状态处理,这种寻找代价太大。也许你会认为就算是由客户端上报状态数据,客户端也需要针对不同业务生成不同的状态,然后上交给服务器,为什么服务器不做这类操作,这就是一和N的问题了。如果由服务器来维护这个过程,则有N个客户端就需要维护N个状态,其中任何一个出问题都需要服务器去处理,相反如果是客户端却只是针对一个客户端机器去处理,相对而言代码复杂度下降。

              另外,如果本(server)业务要做数据转发,对于上报类数据基本是无状态的,大多是以入库为主,而状态数据是以更新为主,尤其某些状态的变更会绑定一些其它内存信息,在转发时不能保证转发目标有这类内存数据,无形中增加了数据转发业务的难度。

    如:服务器的客户端其它业务2在转发时包含了状态,而它的转发目标现在需要读出客户端原来的状态,以便对比是否需要做update操作,但是转发目标本身没有客户端原来的状态数据,这就造成了状态维护的难度。

    业务的专一性很重要,减少代码之间的交叉性,对于后期代码的维护和性能提升是很有必要性。

 

 
 
 

服务器状态维护:

 

1、客户端的(在线)状态维护依靠心跳包维护。

接口的专一性

            实际应用中,类如登录操作,上报操作,等其他指令操作也可以用于判断用户在线,但最好统一交给心跳,这样维护简单,逻辑清晰,出问题时直接找心跳业务。
 
 状态业务的统一性
 
         无论实际处理过程中会遇到多少种业务状态,如:在线,离线,变更,升级,某业务处理中,等等;全部统一交给心跳业务处理,这样可以将后期的维护成本控制在最低,也能最小化变更成本。
   
    如果实际有特殊要求,根据需求做。但需要警示的是,不能增加太多的额外处理业务,否则上图的浅蓝部分,就会变的很长。
 
 

2、在线状态的维护依靠心跳包和定时检查。

            首先客户端的心跳包来了,服务端直接设置内存标志为在线,记录在线时间。客户端重复来的时候,只需要刷新内存上的在线时间即可。这里没有谈论关于状态入数据库的问题,这个得考虑到实际的产品需求,如果状态更改比较频繁会造成数据库更新压力过大,如果更新不频繁当然可以直接入库。
 
            其次当所有客户端掉线时,靠定时刷新(即服务器心跳)来维护。服务端定时检查超出timeout(当前时间-n*心跳时间间隔)的客户端,并将它删除掉。
 
            注意,依靠定时来刷新(服务器心跳)是一个不错的方案,如果服务端一直有客户端的话,可以通过客户端来驱动刷新掉线,比如设置一个redis 排它锁(setnx),每隔一定时间主动触发掉线检查,这样就不需要定时任务了,但实际应用中往往还是依靠服务器心跳来实现,因为锁是一种抢夺资源,客户端每次都去抢夺锁,判断临界条件,造成的资源消耗过大。
 
 
在线状态与客户端在线时间的刷新。
    如果服务器需要记录客户端从离线变为在线的时间,则在线状态刷新与其它状态刷新之间稍微有了一点区别。在不记录客户端从离线变为在线的时间时,客户端在线状态与普通状态的处理逻辑是一样的,但是如果需要记录这个时间则就多了一个时间记录,时间记录与状态记录是不一样性质定义,如果这个时间与状态在一个表里则可以用做一个SQL语句,如果这个时间记录与在线状态不在一张数据表里,则就有两条SQL语句。
 
    下图描述了以上的分析流程,只是没有将客户端从离线变为在线的SQL刷新记录进去。
 

 

 


 

 

 

3、服务端维护所有客户端在线状态的实现逻辑

        1、依靠redis的ZSET,KEY 和SCORE。score记录时间,KEY记录客户端唯一ID。注意大集合的删除操作,和大集合的迭代访问操作。
 
        2、参考的ZSET的实现,即skiplist,使用跳跃表(已经排序)维护时间,记录唯一  ID。
                有些问题,跳跃表可以根据时间查询,无法做uid唯一。比如key唯一
                使用字典来维护uid与时间的关系,并保证uid的唯一性
 
        3、
 
 

4、心跳维持在线状态的基本原理

        1、客户端每来一次心跳,记录客户端的心跳时间戳,如果是ZSET集合,还可以同时根据分值删除多个心跳间隔前的记录。也就是客户端心跳来的时候,确认客户端UID已经记录,即已经在线,此时刷新心跳时间。在心跳来的时候,  REDIS的有序集合,可以根据分数(心跳到达的时间戳)做删除,也就是可以删除掉线的客户端。这样每个客户端心跳时,对于新的在线客户端(不在集合中的客户端)有在线状态变化,对于已经掉线的客户端则是离线状态变化。
            这个步骤,我们称呼为,客户端维持心跳的在线与离线变化。
        2、由于上面的步骤是依赖REDIS内存结构的,如果redis掉线,或者redis奔溃重新起来时,redis结构就不可靠了。或者由于在上面一步中,我们只在某个客户端上线,或者下线时做数据库记录,但是记录失败了,这样就会出现不可靠的情况。此时引入定时刷新功能,它会在每一个时间周期内,对所有心跳集合的数据,进行访问,将数据的状态同步到数据库中,实际使用时除了在线状态外,还可以引入在线时间,这样就保证了在线绝对可靠,在线时间也可以用于选择每天在线的客户端,或者昨天在线今天不在线的客户端。
              这一步的操作属于服务器单独维护的操作,它是客户端状态在服务器端的维护处理。
    该步骤的另一个好处是,多数据转发时,如果出现某次数据丢失了,可以在定时刷新时重新获取状态数据。
 

5、心跳请求与高并发

        对客户端而言,服务器维护的客户端在线状态与客户端是没有关系的,客户端上报心跳时,只需要在心跳里记录好自己的信息即可。如果还需要获取服务器给客户端的信息,客户端从服务器拿走信息就可以。
        综合上面的业务分析有:心跳状态维护是服务器的工作,客户端并不关心,因此在上报心跳状态后,可以触发一个异步任务在后台维护状态,客户端立即拿走属于自己的信息。这样就通过异步任务(或者写队列),可以加速心跳的运行,增加并发速度。
 
参考下图
 
    

 

   如上所示:客户端发生心跳请求后,直接从内存取走(实时回复)服务器准备给客户端下发的数据,客户端的逻辑处理就完成了;客户端通过心跳上报给服务器的状态,服务器需要维护的状态。还有客户端通过心跳推送给服务端的数据,服务器需要处理的数据。等等。都是服务器需要处理的业务,可以直接推送到队列中。

  通过这样的拆分,即保证了IO请求的高并发性,也保证了数据处理的可靠性。


                                                                  

6、状态刷新之间的互斥性质

        在需要记录离线状态的代码中,需要知道离线状态刷新与其它状态是个互斥的行为。一旦记录了离线行为,则不能刷新在线状态,或者其它的状态行为。
        这类状态大多是服务器自己管理的状态。如客户端报告了自己处于警示状态,服务器需要保持一天的时间,因为服务器只是在警示的时候报了一次或者几次警示状态,所以这个时候需要服务器来保持一天的时间。但是这类状态遇上了客户端离线状态,就很有意思了,有的产品可能离线不影响其他状态的维持,但是有的产品就影响了,所以需要根据实际处理。
        
 

7、状态码的继承性质

        比如某个程序,同时有资产更新状态,升级状态,在线状态。如果这些状态都是记录在一起的,则需要注意将所有状态以位的形式合并成一个值;或者一次性判断完所有的状态,做好处理,然后写到数据库中,不能每执行一个状态就执行一次SQL,对数据库而言压力太大。
  流程如下所示:
    

 

 

        这类状态大多是客户端自己管理的状态,只是状态推送到服务器后,服务器需要针对个别状态做特殊处理,或者不处理。
 

8、接口的专一化与状态处理

 
        在一个系统中,客户端与服务器之间的接口有很多,但是需要注意的是,专业的接口来维护专业的状态。如客户端与服务器之间有登录和心跳,还有数据上报,甚至更多。依照常理来说,登录,心跳,数据上报都表示客户端状态应该是在线的,因为两者之间的数据是通达的。但是在实际应用中,如果按照这种模式来维护,会造成后期维护压力太大,如登录,心跳,上报中都要判断;这样不如让心跳来专门维护在线,这样一旦出了问题只需要查心跳,后期维护方便,同样服务器在维护状态时,做的处理最少。因为心跳是频繁的,因此这样是可以的,甚至可以将客户端的状态全部交由客户端维护。
 
 

9、状态显示的时序性

  某些系统会对状态处理的流程要求比较严格,比如有要求记录每一个处理过程,不得遗漏任何一个过程。有些系统并不要求记录所有的状态,只要能够表示当前的状态即可,至于之前的状态转瞬即过的话,并不要求能够追踪。系统的这些特性导致了状态处理上的一些细微差别。
 
  这里先不讨论上面说的这些业务实现,因为其中一部分在项目中没有具体实现过,这里只针对遇到的问题进行说明。
 
业务处理上的时间差导致的状态显示异常。
  某业务在做状态显示时,原本应该是正常显示该业务状态,却因为后台为了做批量的SQL更新导致了旧状态将新状态给替换了。批量处理大致有两个实现,数据量级达到上限;或者时间间隔上限到了。无论是哪种处理方式都无法避免的会遇到业务上的状态突然变更,如正常运行的的某个业务,很快就处理完毕了,于是后端就收到了关于这个业务的两个状态,正在运行状态和已经结束运行状态。此时如果做批量处理,且没有做严格的状态时间顺序性处理,就会出现不可预料的问题,出现类似以下的语句:
 
  UPDATE task_status SET  status = CASE uid  WHEN 'haidian0' THEN 1  WHEN 'haidian0' THEN 0  END  ,  online_status = CASE uid  WHEN 'haidian0' THEN 1  WHEN 'haidian0' THEN 0  END,  monitor_status = CASE uid  WHEN 'haidian0' THEN 1  WHEN 'haidian0' THEN 0  END   where uid in ( 'haidian0' , 'haidian0' )

 

  虽然以上语句在update时是正确的,但无疑增大了理解难度和业务上的梳理难度。还有就是SQL组装时,如果没有按照时间点顺序实现,则会是致命的错误。
 
 
批量更新导致的状态问题
 
        这里有一个坑:   
                update ....   uid  in (  uid_a,  uid_a  )
        在一些结构没有处理好的代码中,可能会出现类似上面的SQL语句,同一个uid,在批量更新mysql时,执行了两次update(姑且这么叫),则后面的会覆盖的前面的更新,导致另一个状态没有更新。
       深入挖掘上面的问题,他是因为尝试做多个状态的合并操作导致的,因为批量操作有一个触发的条件,一般是数量上限或者时间上限,在触发这个阈值前,同一个客户端连续多次发生了状态变更导致了批量update时,in的部分同一个uid出现了多次,这里需要根据时间出现的先后顺序,当同一个客户端又一次出现状态数据时,应该将早前记录的数据值删除。
 

10、状态变更的有限性

  客户端的状态不能无限制的刷新,这样会造成后端数据库的高压力。比如有些人为及时获取客户端的在线状态,只要客户端有心跳就会去刷新一次客户端在线状态,他们认为这样做的话可以一劳永逸的获取到所有客户端的变更信息,但遗憾的是,这是一个错误的做法。当客户端少时,机器性能有盈余,所以看不出来,但是一旦客户端数量成规模了,则会造成数据库的IO压力上升。正确的做法是将客户端的状态存储在内存中,然后根据客户端上报的状态,做一个状态对比,当状态变更时则放入队列内,以便数据库做状态变更。
  某些项目里可能存在高速变化的状态,遇到这些问题只能依赖内存(redis)对外提供高速访问的能力,如果项目必须提供状态变更的历史则将这些信息慢慢写入数据库中,如果项目不允许数据丢失,则需要做好备份的措施,如选择不会丢失数据的任务队列,或者选择记录日志(类似大数据里的日志),由一个专有程序负责解析日志。
 
 

在线状态引申:

 
        在C/S 类架构中,服务器与客户端之间的维护的状态可能会有很多种。比如在服务器显示客户端是否在线,客户端是否在升级程序,客户端是否在报毒(杀毒软件里),客户端是否在下载补丁等等;这些状态通常而言都是客户端自己维护的,也就是客户端会一直报自己的状态,服务器端只要记录一个对应的客户端内存状态,两者不一致,则往数据库里刷新记录值即可。
        但是实际中,客户端的某些状态可能需要保留一天,比如在某一天客户端仅仅报毒了一次,且报毒这个操作,1次上报结束后,就结束了。也就是说报毒这个操作,如果跟随客户端状态的话,仅仅在其报毒的那几个心跳里会有状态记录,以后就没有了。无法保证再WEB页面里,在当天的时间里一直显示为报毒状态。
        也就是某些状态在上报到服务器后,服务器需要针对该状态做单独的维护工作。这个维护工作就很有意思了,由于该维护工作是服务器自己维护的,因此需要服务器自己定时去检查,是否状态过期。这时就需要依赖定时任务来实现了,即定时去检查,直到过期了。
        如果服务器需要单独维护的状态非常多,就需要为每一个状态做计划任务。反过来想,可以通过生成一个定时心跳,来通知所有的状态任务,让状态任务检查是否到达自己的执行周期(比如多少个服务器心跳后开始执行,每隔多少时间后执行),这样就变成了一个定时心跳负责维持所有状态任务的刷新。
        如下使用celery beat实现服务端心跳,由心跳任务去判断状态逻辑。
 
        这里需要避免一个坑,就是取消服务器心跳,依托客户端心跳 + 全局互斥锁来实现服务器状态刷新。一是客户端数量少时,可能会出现客户端全部掉线,造成服务器状态没法维护。一是如果客户端数量超级多,会造成互斥锁调用那块的条件判断执行次数过于频繁,造成资源的浪费。
 
        如下就是依赖客户端心跳维护服务器端的状态刷新,由于每个客户端心跳都会造成执行这些代码,造成性能浪费。
        
 
 
 
 
 
 
 
 
 
posted @ 2021-11-08 19:16  dos_hello_world  阅读(679)  评论(0)    收藏  举报