日处理20亿数据,实时用户行为服务系统架构实践
2017-04-27 编辑:
作者丨陈清渠
责编丨钱曙光
携程实时用户行为服务作为基础服务,目前普遍应用在多个场景中,比如猜你喜欢(携程的推荐系统)、动态广告、用户画像、浏览历史等等。
以猜你喜欢为例,猜你喜欢为应用内用户提供潜在选项,提高成交效率。旅行是一项综合性的需求,用户往往需要不止一个产品。作为一站式的旅游服务平台,跨业务线的推荐,特别是实时推荐,能实际满足用户的需求,因此在上游提供打通各业务线之间的用户行为数据有很大的必要性。
携程原有的实时用户行为系统存在一些问题,包括:1)数据覆盖不全;2)数据输出没有统一格式,对众多使用方提高了接入成本;3)日志处理模块是Web Service,比较难支持多种数据处理策略和实现方便扩容应对流量洪峰的需求等。
而近几年旅游市场高速增长,数据量越来越大,并且会持续快速增长。有越来越多的使用需求,对系统的实时性,稳定性也提出了更高的要求。总的来说,当前需求对系统的实时性/可用性/性能/扩展性方面都有很高的要求。
一、架构
这样的背景下,我们按照如下结构重新设计了系统:
图1 实时用户行为系统逻辑视图
新的架构下,数据有两种流向,分别是处理流和输出流。
在处理流,行为日志会从客户端(App/Online/H5)上传到服务端的Collector Service。Collector Service将消息发送到分布式队列。数据处理模块由流计算框架完成,从分布式队列读出数据,处理之后把数据写入数据层,由分布式缓存和数据库集群组成。
输出流相对简单,Web Service的后台会从数据层拉取数据,并输出给调用方,有的是内部服务调用,比如推荐系统,也有的是输出到前台,比如浏览历史。系统实现采用的是Java+Kafka+Storm+Redis+MySQL+Tomcat+Spring的技术栈。
Java:目前公司内部Java化的氛围比较浓厚,并且Java有比较成熟的大数据组件
Kafka/Storm:Kafka作为分布式消息队列已经在公司有比较成熟的应用,流计算框架Storm也已经落地,并且有比较好的运维支持环境。
Redis: Redis的HA,SortedSet和过期等特性比较好地满足了系统的需求。
MySQL: 作为基础系统,稳定性和性能也是系统的两大指标,对比NoSQL的主要选项,比如HBase和ElasticSearch,十亿数据级别上MySQL在这两方面有更好的表现,并且经过设计能够有不错的水平扩展能力。
目前系统每天处理20亿左右的数据量,数据从上线到可用的时间在300毫秒左右。查询服务每天服务8000万左右的请求,平均延迟在6毫秒左右。下面从实时性/可用性/性能/部署几个维度来说明系统的设计。
二、实时性
作为一个实时系统,实时性是首要指标。线上系统面对着各种异常情况。例如如下几种情况:
突发流量洪峰,怎么应对;
出现失败数据或故障模块,如何保证失败数据重试并同时保证新数据的处理;
环境问题或bug导致数据积压,如何快速消解;
程序bug,旧数据需要重新处理,如何快速处理同时保证新数据。
系统从设计之初就考虑了上述情况。
首先是用Storm解决了突发流量洪峰的问题。Storm具有如下特性:
图2 Storm特性
作为一个流计算框架,和早期大数据处理的批处理框架有明显区别。批处理框架是执行完一次任务就结束运行,而流处理框架则持续运行,理论上永不停止,并且处理粒度是消息级别,因此只要系统的计算能力足够,就能保证每条消息都能第一时间被发现并处理。
对当前系统来说,通过Storm处理框架,消息能在进入Kafka之后毫秒级别被处理。此外,Storm具有强大的scale out能力。只要通过后台修改Worker数量参数,并重启Topology(Storm的任务名称),可以马上扩展计算能力,方便应对突发的流量洪峰。
对消息的处理Storm支持多种数据保证策略,at least once,at most once,exactly once。对实时用户行为来说,首先是保证数据尽可能少丢失,另外要支持包括重试和降级的多种数据处理策略,并不能发挥exactly once的优势,反而会因为事务支持降低性能,所以实时用户行为系统采用的at least once的策略。这种策略下消息可能会重发,所以程序处理实现了幂等支持。
Storm的发布比较简单,上传更新程序jar包并重启任务即可完成一次发布,遗憾的是没有多版本灰度发布的支持。
图3 Storm架构
在部分情况下数据处理需要重试,比如数据库连接超时,或者无法连接。连接超时可能马上重试就能恢复,但是无法连接一般需要更长时间等待网络或数据库的恢复,这种情况下处理程序不能一直等待,否则会造成数据延迟。实时用户行为系统采用了双队列的设计来解决这个问题。
图4 双队列设计
生产者将行为纪录写入Queue1(主要保持数据新鲜),Worker从Queue1消费新鲜数据。如果发生上述异常数据,则Worker将异常数据写入Queue2(主要保持异常数据)。
这样Worker对Queue1的消费进度不会被异常数据影响,可以保持消费新鲜数据。RetryWorker会监听Queue2,消费异常数据,如果处理还没有成功,则按照一定的策略(如下图)等待或者重新将异常数据写入Queue2。
图5 补偿重试策略
另外,数据发生积压的情况下,可以调整Worker的消费游标,从最新的数据重新开始消费,保证最新数据得到处理。中间未经处理的一段数据则启动backupWorker,指定起止游标,在消费完指定区间的数据之后,backupWorker会自动停止(如下图)。
图6 积压数据消解三、可用性
作为基础服务,对可用性的要求比一般的服务要高得多,因为下游依赖的服务多,一旦出现故障,有可能会引起级联反应影响大量业务。项目从设计上对以下问题做了处理,保障系统的可用性:
系统是否有单点?
DB扩容/维护/故障怎么办?
Redis维护/升级补丁怎么办?
服务万一挂了如何快速恢复?如何尽量不影响下游应用?