state-生命周期管理

系统现状

当client与gateway简历连接后,将会收发消息,但是连接作为一个通道,在互联网上经过不可控的路由节点,很难保证连接的可用性,这里定义可用指的是能够用连接收发消息。

那么连接通常会在什么场景异常断开?所谓异常断开强调的是,连接的断开既不是客户端也不是服务端主动断开的场景。

通常公网长连接要经过运营商,而运营商为了节约成本会周期性将没有消息手法的连接断开,每个地区每个运营商都是不同的,运营商会向客户端和服务端发送fin信号断开链接,所以客户端和服务端都会以为是彼此端口,难以排查。

其次当tcp数据到数据中心时,现代数据中心都会有多层网关,这些网关为了节约自身资源也会周期性清理不活跃的连接,这时也会发送fin信号断开彼此。

再有由于移动互联网场景,客户端通常处于不确定的网络场景中,比如离开室内wifi到达户外使用移动信号,此时手机会连接基站长连接也必然会断开。

当客户端高速移动时,手机移动出漫游区域覆盖范围,进入到其他运营商网关范畴,此时会倒置手机被分配新的ip地址,网络层IP地址发生变化,那么上层的传输层TCP也会断开,此时会导致连接不可靠。

在进入一些若亡场景中时也会倒置连接断开,例如隧道/地铁/山区等等,这种情况俊晖袋中连接断开。

连接断开会机器影响用户的使用体验,轻则影响消息的延迟,重则导致消息错乱,对比所有app会发现在地铁上的时间,只有微信网络是比较好的,这就是弱网场景的一些优化,其中连接可靠性时最重要一环之一。

优化目标

被断开的原因可以总结为两种,第一是被中间代理资源收回,第二种是底层ip的切换。

针对第一种的做法是心跳机制,只要保证在整个链路上周期性的收发一条消息,即可防止资源的回收,但前提是消息收发周期要小于整个链路的最小回收周期。

谁来发送消息,如果是服务端那么每一次都要便利一遍遍所有的socket,这是巨大的耗时,这里需要注意的是,连接创建成功时服务端需要创建一个定时器,当电视时间到达后就要主动断开链接,这是防止客户的因为某些原因没有断开链接,但是也变得不可用无法再收发消息,此时链接占用服务端资源必须回收,否则会造成泄漏问题。

针对第二种现象,ip地址的更新一定会倒置链接断开,此时我们的目标不是防止不断,而是如果链接断开后但是用户无感知,这需要客户在后台进行多次连接重建的请求,当链接断开后,如果客户端没有问题此时他会主动去重新连接,此时服务端需要能够快速重连,由于之前具有的一些链接状态在这里都可以被再次继承,无需重新分配因此链接速度就会增加。

约束条件

  1. 资源成本,尽可能减少服务端资源的损耗是接入层的重要目标,因为长连接接入层需要维护极易改变的状态,这造成了重大的工程技术挑战。
  2. 可靠性,连接应该在各种极端情况下都能以最快速度恢复联通状态。
  3. 低延迟,不能因为维护连接的可靠性久导致收发消息的延迟加大。

技术方案

编解码器基于现在的架构,state server需要实现一个协议解析模块,这个模块用来解析消息协议,然后根据消息的类型进行逻辑处理的路由,协议的解析与IOS的协议分层机制有些类似,gateway只解析len与data,state会解析data中的msg type,然后基于type进一步选择相应的解码器实现将剩余palyload传递给解码器,最后路由到业务层处理业务。

控制信令需要实现 登陆/心跳/重连三个,需要进行协议的设计,整体上会使用pb的序列化,协议的设计要充分考虑扩展性,支持未来做更加复杂的优化策略。

如何尽可能的优化资源成本?

心跳的定时器维护是一个巨大的开销,他将跟持有的长连接数量保持一致水平扩展,如果在某一段时间有大量用户登陆就会倒置定时器的触发点出现潮汐现象,严重情况下会导致系统直接卡死,因为回调协程被直接唤醒,造成调度潮汐,cpu利用率被直接拉满,同时定时器的存储也是一个巨大的内存消耗,go底层使用的是四叉堆来存储定时器,这种设计精度会很高,但是在我们这个场景其实不需要很高的精度,只要资源能够被及时回收的秒级别误差是能够接受的,因此我们其实只需要使用时间轮算法代替原声带定时器算法,时间轮的精度较低但是性能足够好,插入定时器和触发定时器都可以做到O(1)的时间复杂度,单golang原声的定时器是logn的。

登陆时需要做什么?

创建一个定时器,然后冲中解析出did的信息,将connID(等价于fd,fd存在bug,会被复用)+endpoint信息与did进行绑定,形成路由(did作为key,endpoint+connid作为value),这一映射是用来业务层下推消息时使用,因为业务层仅靠感知did信息,第三步将消息下发给业务server,一旦业务server回复ok,就直接回复客户端ack,这里假设业务的处理都是同步的,即使有异步操作也会通过MQ等手段保证可靠执行。

心跳时需要做什么?

当客户端接收到心跳消息时,通过endpoint+connID作为key找到定时器对象,然后将其重制重新定时,此时从心跳中可以解析出一些客户端周期性上报的消息,在这些上报消息中服务端可以做一些统计行为,然后回复客户端ack,代表消息已经收到。(也可以不回复)

重连时需要做什么?

在登陆成功时,服务端回复的ack中会携带此时连接的connID,在连接端口后发送重连信令时将connID发送过来。

在服务端感知到连接断开时,不要立即进行资源的回收,而是启动一个定时器,当这个定时器超时后再回收资源,这段等待时间就是为了等待客户端进行快速重连,进行资源的复用,这有助于故障连接的快读恢复。

如果gateway进程崩溃,state server并没有及时清理资源,此时客户算使用上一个gateway创建的connID来进行重连时,state server本质上同样可以识别并正常工作,因为endpoint+connID并没有变化。

如果state server崩溃,该变量将失去所有的状态,定时器信息,此时将无法正常工作,除非使用分布式缓存。将一些关键信息缓存在其中。