图4:双队列设计
生产者将行为纪录写入Queue1(主要保持数据新鲜),Worker从Queue1消费新鲜数据。如果发生上述异常数据,则Worker将异常数据写入Queue2(主要保持异常数据)。
这样Worker对Queue1的消费进度不会被异常数据影响,可以保持消费新鲜数据。RetryWorker会监听Queue2,消费异常数据,如果处理还没有成功,则按照一定的策略(如下图)等待或者重新将异常数据写入Queue2。
图5:补偿重试策略
另外,数据发生积压的情况下,可以调整Worker的消费游标,从最新的数据重新开始消费,保证最新数据得到处理。中间未经处理的一段数据则启动backupWorker,指定起止游标,在消费完指定区间的数据之后,backupWorker会自动停止。(如下图)
图6:积压数据消解
三、可用性
作为基础服务,对可用性的要求比一般的服务要高得多,因为下游依赖的服务多,一旦出现故障,有可能会引起级联反应影响大量业务。项目从设计上对以下问题做了处理,保障系统的可用性:
系统是否有单点? DB扩容/维护/故障怎么办? Redis维护/升级补丁怎么办? 服务万一挂了如何快速恢复?如何尽量不影响下游应用?
首先是系统层面上做了全栈集群化。kafka和storm本身比较成熟地支持集群化运维;web服务支持了无状态处理并且通过负载均衡实现集群化;Redis和DB方面携程已经支持主备部署,使用过程中如果主机发生故障,备机会自动接管服务;通过全栈集群化保障系统没有单点。
另外系统在部分模块不可用时通过降级处理保障整个系统的可用性。先看看正常数据处理流程:(如下图)
图7:正常数据流程
在系统正常状态下,storm会从kafka中读取数据,分别写入到redis和mysql中。服务从redis拉取(取不到时从db补偿),输出给客户端。DB降级的情况下,数据流程也随之改变(如下图)
图8:系统降级-DB
当mysql不可用时,通过打开db降级开关,storm会正常写入redis,但不再往mysql写入数据。数据进入reids就可以被查询服务使用,提供给客户端。另外storm会把数据写入一份到kafka的retry队列,在mysql正常服务之后,通过关闭db降级开关,storm会消费retry队列中的数据,从而把数据写入到mysql中。redis和mysql的数据在降级期间会有不一致,但系统恢复正常之后会通过retry保证数据最终的一致性。redis的降级处理也类似(如下图)
图9:系统降级-Redis
唯一有点不同的是Redis的服务能力要远超过MySQL。所以在Redis降级时系统的吞吐能力是下降的。这时我们会监控db压力,如果发现MySQL压力较大,会暂时停止数据的写入,降低MySQL的压力,从而保证查询服务的稳定。
为了降低故障情况下对下游的影响,查询服务通过Netflix的Hystrix组件支持了熔断模式(如下图)。
图10:Circuit Breaker Pattern
在该模式下,一旦服务失败请求在给定时间内超过一个阈值,就会打开熔断开关。在开关开启情况下,服务对后续请求直接返回失败响应,不会再让请求经过业务模块处理,从而避免服务器进一步增加压力引起雪崩,也不会因为响应时间延长拖累调用方。
开关打开之后会开始计时,timeout后会进入Half Open的状态,在该状态下会允许一个请求通过,进入业务处理模块,如果能正常返回则关闭开关,否则继续保持开关打开直到下次timeout。这样业务恢复之后就能正常服务请求。