【IT168 专稿】本文根据胡嘉伟老师在2018年5月12日【第九届中国数据库技术大会】现场演讲内容整理而成。
讲师简介:
胡嘉伟,爱奇艺高级工程师。2016年毕业于上海交通大学并加入爱奇艺分布式实时计算团队, 工作期间,作为核心开发人员, 开发实现了Babel这一公司级的统一大数据平台, 所主导开发的Streaming SQL极大地降低了实时任务的开发门槛, 在公司重要业务得到了广泛应用. 除了大数据平台建设之外, 还主力参与爱奇艺用户播放体验实时监控与播放故障根因分析项目。
摘要:
提供正版、高清、流畅的视频播放服务始终是爱奇艺所追求的目标, 除了播放体系本身的建设之外, 爱奇艺也立足于用户,从用户视角对爱奇艺播放时的播放故障、 卡顿等指标进行实时分析,以提供立体的、多维度的实时数据监控。
分享大纲:
1、用户播放体验
2、挑战与架构
3、大数据平台
4、数据挖掘与分析
5、总结 & 展望
正文:
1、用户播放体验
本文主要介绍爱奇艺如何使用大数据及数据挖掘技术提升用户播放体验,此处的播放体验指用户在观看视频过程中对播放服务的体验,而不是对播放内容的体验。目前,我们使用的是客观服务质量指标对用户播放体验进行描述,比如,卡顿比故障率等。为了能够提供优质的播放服务,爱奇艺在播放体系建设方面投入了大量资源,比如,建立庞大的CDN网络。但是,视频播放是一个比较复杂的过程,它会调用非常多后台服务且调用链较长,同时,用户播放环境非常复杂,它会受到网络环境、本地硬件环境以及客户端环境的影响,任何一个环节发生问题都有可能导致用户播放体验下降。
过去,我们主要通过客服收集用户反馈信息感知服务体验下降,当反馈量累积到一定程度时,客服人员会把信息提交给技术人员,这个过程通常会耗费几个小时。当技术人员接收到客服反馈信息后,他们通常会分析日志数据或者报表数据以进行测试排障,通常,客服提供的信息非常有限,我们在思考如何更迅速地提供更丰富的信息让技术人员快速定位和解决问题。在这样的背景下,我们启动了用户播放体验实时监控及故障根因分析项目。
2、挑战与架构
在项目开发过程中,我们主要面临以下几大难点:一是播放服务环境复杂,依赖子模块较多且调用链很长,任何一个环节出现问题都会导致用户播放体验下降,我们需要设计能够全面描述用户播放体验的指标。
二是播放日志数据量庞大,日新增数据量在5T以上且数据维度众多,公共维度就有30多维,不同场景还存在特有维度。此外,很多指标需要进行复杂联合查询才能计算,我们需要支撑这样一个大数据复杂计算场景。
三是指标会随时间而变化,这些指标有明显的以天为周期的波峰波谷特征。此外,指标本身也是持续变化的,一些特定事件的发生也可能带动指标变化。在这种情况下,如果采用传统的设定阈值方式进行监控,只能对较粗粒度指标进行监控,无法对大量指标进行监控。而且需要持续调整阈值,这意味着需要耗费大量人力。
对于上述问题,我们认为顶层设计非常重要,从数据准备到数据投递,再到数据计算,都需要进行设计规划。为了能够多维度全方位对播放服务质量与用户播放体验进行表征,我们从用户网络环境、用户播放状况、播放子模块信息等角度入手,对20多个维度条件下的多指标进行计算,以此来描述用户播放情况。
针对第二大难点,我们使用大数据技术获得足够的计算能力,以下为大家介绍爱奇艺内部的两大平台:大数据开发平台和日志数据收集平台。依托这两大平台,我们可以进行数据挖掘,对实时指标进行异常检测和根因分析。
上图为爱奇艺播放数据整体架构,数据从客户端的播放内核投递到后台日志收集集群,日志收集集群在收集到日志后会有两条链路对数据进行处理:离线链路和实时链路。在离线链路里,我们最终处理完的数据会保存到Hive,或者Impala+Parquet存储结构中,离线链路的数据特征可靠稳定,缺点是延迟较大。在实时链路中,我们通过Spark或者Flink计算实时采集到的日志数据,最终保存到Impala+Kudu存储结构中。
至于为什么离线数据存到Impala+Parquet,而实时数据存到Impala+Kudu,是因为Kudu支持即插即查,底层可自维护数据存储,没有小文件困扰,比较适合实时数据保存;相对于Kudu,Parquet的批量查询性能高一个数量级,离线需要保留长期历史数据,将这些数据存到Parquet里可提升查询性能。
为了理解用户的播放行为及播放情况,我们做了大量离线报表和实时报表,开发了报表数据查询引擎,查询时会按需对离线报表及实时报表数据进行查询,并对查询结果进行融合。同时,对于实时数据进行异常检测与根因分析。
此外,我们开发了AD-HOC查询引擎,主要解决报表数据的维度膨胀问题。通过AD-HOC查询引擎对维度自由组合的数据进行特别支持,满足业务对细粒度数据查询的需求。
上图为爱奇艺该技术选型中使用的开源工具,Kafka集群经验证可较平稳地支撑千万级QPS实时数据量,我们基于Spark进行了很多开发,自研StreamingSQL以降低实时计算任务的开发门槛;对于爱奇艺内部的实时计算任务,我们已经不建议业务使用Storm,建议使用Spark或Flink;比如,安全风控这种对实时性要求较高的业务,基本都在使用Flink;使用Impala的原因是能够支持复杂查询,也可无缝支持Kudu。
基于上述开源工具,我们开发了大数据平台Babel,提升了开发、托管以及运维任务的效率;日志收集平台Venus,主要作用是采集日志数据并提供给下游计算使用。
3、大数据平台
首先介绍大数据开发平台Babel,Babel主要包括四大模块:任务开发、任务运维、元数据中心以及数据交换。任务开发模块,Babel支持对批处理以及实时任务的开发,开发模式有两种:SQL模式和Jar模式,SQL模式就是用户通过编写SQL制定任务,直接在网页提交任务运行。其中,批处理SQL运算底层使用Hive,实时计算SQL底层使用的是我们自研的StreamingSQL。对于批处理任务,我们还有一个工作流的概念,主要作用是指定各批处理任务之间的依赖及调度关系,以此来维护任务。
任务运维模块,Babel的主要功能是当任务失败时进行重试。此外,需要对实时任务状态进行监控,比如,发生延迟或者重启事件需要通知到用户进行处理,不然会影响到下游计算。元数据中心模块,主要对数据源注册、发布及检索,进行数据权限以及血缘依赖管理。数据交换模块,实现各数据源之间的互通。
接下来主要介绍StreamingSQL,StreamingSQL主要的使用场景是编写SQL,通过编写SQL描述实时ETL及实时报表计算,支持的时间模式有处理时间以及事件时间两种,目前支持的输出数据有Hive、MySQL和Kudu,主要是在SparkSQL基础上对SQL语法进行扩展,引入新的关键词,比如定义流表、维度表、临时表、结果表以及自定义函数,维度表指静态表,可用于与流数据做Join ,临时表用来定义计算的中间状态,结果表用来定义输出数据源信息。我们现在支持两种输出模式:Append 模式和Upsert 模式。Append模式支持所有输出数据源;由于输出数据更新操作最终会push down到数据源里进行操作,因此,如果数据源本身支持该操作,那么Upsert 模式也支持。
以上是打印数据延迟示例,首先创建一个流表,定义数据来源于哪个Kafka以及如何解码;其次,定义中间表把数据里的TimeStamp字段从毫秒值转化成秒值;然后,创建结果表定义;最后,计算结果会在控制台进行打印,将计算逻辑插入结果表。运行该任务时,我们可以在控制台看到这个微批次里数据的最大值、最小值、平均值以及分位数等。
以上是线上实例,在实际使用中光写业务逻辑是不够的,还要定义环境变量、资源使用、权限以及参数等信息,对于复杂计算,我们可能还需要引用自定义函数。
以上是脱敏后的实际业务逻辑,可以看出原来需要大量代码实现的逻辑,现在就像使用Hive一样写个SQL就可以了。
接下来介绍日志收集平台 Venus,Venus的主要作用是采集日志数据提供给下游计算使用,分为实时链路与离线链路。目前,支持的实时链路数据量已达到千万级QPS,上图为其整体流程示意图。在二级数据上,无论是Kafka还是Hive都属于结构化数据。在实时链路上,最终的二级Kafka数据延迟基本在秒级,正常情况是一到两秒,有时会上升到六到七秒。
4、数据挖掘与分析
在具体的项目开发中,数据挖掘与分析主要分为以下四部分:数据计算、异常检测、根因分析以及Ad-hoc查询。数据计算层面,本文只介绍与实时计算相关部分,我个人认为实时计算对数据质量进行监控非常重要,尤其是目前最终的计算结果需要传到下游进行异常检测,如果数据本身存在问题,就会导致下游系统受到影响,最终检测结果可能就是错误的。为了避免这种情况发生,我们需要对细粒度指标监控疑似异常点,对疑似异常点进行根因分析,归并相同原因导致的故障报警,根据不同的异常特性补充对应的详细维度信息。最后,我们提供Ad-hoc 查询功能,可以对细粒度数据进行查询。
在实时数据计算部分,我们经过了一次架构演变,由原来的二级Kafka数据先通过StreamingSQL落盘到Hive,再以批次形式进行计算获得最终报表数据的架构改为Lambda架构,最终数据分为实时部分以及离线部分,实时数据链路中使用Flink做了一次ETL,中间数据由Kafka进行中转。
如果将上述两个方案进行对比,原方案的架构非常简单且节省自身计算资源,因为离线计算与实时计算共享同一份数据源。但是,该方案存在一个致命的问题,它有可能存在数据丢失的风险,如果落盘Hive的程序发生不可恢复的故障,很可能会产生数据丢失。此外,该方案的数据延迟也比较高,整套流程走下来可能需要七至八分钟。
新方案采用Lambda架构保证最终的数据一致性,并且由于全程都是实时计算,所以计算延迟比较低,但是新架构的整个系统更加复杂,计算链路、数据查询也变得比较复杂。
改用新架构之后,我们也遇到了一些新的问题,一是实时计算中的反刷量问题,刷量就是指恶意日志投递。实际上并没有故障发生,但恶意投递的故障日志数据导致最终计算获得的故障相关指标上升,这种由于刷量导致的报警就很致命,业务根据报警信息排障也是白白浪费时间。因此,我们的数据一定要做反刷量处理。
在过去,假设数据落盘到Hive后以五分钟的粒度对数据进行分析,然后把其中的刷量数据过滤掉再进行计算,那么,最终结果就是进行过反刷量的结果。改为实时计算之后,如果还是按照原来的思路就会遇到一个问题,当你识别出某些数据是刷量数据时,这条数据很可能已经被下游计算统计到最终指标结果里了,我们的解决方案是使用Flink做一层ETL,使用滑动窗口对数据进行分析,把刷量数据特征找出来之后存到外部数据源,与此同时,我们在Flink内部对数据进行了回流,让实际数据延迟30秒之后再发到下游计算任务,这样我们就人为制造了整个数据链路里的时间差,当下游业务接收到这些数据时,我们根据外部数据源内的刷量数据特征,就能够进行正确的反刷量过滤。
我们遇到的另一个问题是,在实际计算中,必须要用事件时间处理,最终的结果才有意义,才能够避免数据乱序等问题。但是,这样就要求判断数据到达情况,因为计算出来的指标要直接触发后续的异常检测逻辑,如果数据只是部分到达就触发了异常检测,那么这种数据属于脏数据,会导致检测结果出现问题,比较简单的方法是直接通过watermark 判断数据有没有完全到达,但这种方式很不灵活,如果watermark设置的值太大,数据延迟其实并没有那么大,你也必须等待那么长的时间才能触发后续计算;反之,如果你的watermark设置较小,那么高峰时的数据延迟增长会很大,这可能导致大量时间段的数据不可用,这种情况也是无法接受的,所以设置一个合理的watermark值非常困难,应该如何解决呢?
因为底层实际运行的是Spark微批次模型,所以我们可以拿到微批次数据延迟情况。根据微批次数据延迟情况和watermark分析决定是否要触发后续计算。当数据延迟较小时,我们可以根据微批次里的数据延迟情况及历史数据里的延迟情况来判断,如果当前时间段内的数据已经完全到达,就可以直接触发后续计算,而不用等待watermark到期再计算。 如果数据延迟较大,我们会设置watermark的最大等待时间,当数据延迟超过最大等待时间,对应时间段的数据被标记为不可用。整个流计算的每一个环节都会对延时情况进行监控。
第三个问题是使用Lambda架构之后,数据查询变得更加复杂。上图为数据查询示例,在实际的数据查询中,我们需要根据检索的时间范围对离线和实时数据进行融合,然后对最终结果进行反馈。数据到齐监控是指如果只有部分实时数据到达,肯定不可以参与后续计算。在上述示意图中,橘色部分表示数据延迟过大或者任务发生重启,由于无法保证数据完全正确,因此我们会对数据进行标记,通知数据查询方,这个时间段内的数据是不可信的。
接下来是异常检测及根因分析模块,这部分与业务耦合较强。除了实时指标需要异常检测,我们也曾尝试过人工设定固定阈值的方法,以此来进行监控。但是,我们很快就发现这些指标的特性一直在变化,这些阈值也要跟着不断调整,这个过程非常耗费人力,而且也没有办法对太多指标进行监控,只能对少量指标监控。之后,我们尝试使用机器学习的方法进行异常检测,我们尝试过很多方法,总结来说主要有以下两大套路:
一是假设数据服从某种单峰分布,但我们的数据实际上并不是单峰分布,很可能是多峰分布,因此这种方法并不适用。二是基于预测进行异常检测,主要是通过计算某一种约束条件下的期望来做预测,根据期望值与实际值的偏差判断是否有异常。然而,我们更希望能够计算不确定性的值,通过不确定性的值的大小进行判断。如果希望基于预测的异常检测达到更好的效果,需要对指标进行预处理,将指标映射为平稳序列,但是这种做法会耗费大量人力。
针对我们的业务场景,我们的第一步是引入时域特征,模型能够捕获一天中不同时间段的曲线特征,并且通过周期性地训练模型,使用历史数据训练模型不断更新参数,模型参数是不断动态调整的。
第二步是计算曲线波动以及偏移特征,主要使用概率分布估计以及CUSUM,这两个量作为基础特征用于后续的异常检测逻辑中。
第三步是消除异常数据影响,如果我们已经判断出某个时刻的数据有问题,那么在后续检测中就要把该时刻的数据取消,我们用Holt-Winters对该时间点的数据进行拟合,避免负面影响。
第四步也是最重要的一点,我们引入了视觉特征做模式识别。因为很多机器学习模型检测出的异常,人工查看数据时会觉得不够显著,认为不是异常。我们通过模式识别主要使用第二步计算得到的特征值作为输入检测出人们在视觉上能够认可的异常曲线。最终我们达到了对3000多个指标进行实时监控,整体延迟控制在两分钟左右。
在检测出疑似异常点之后,我们需要做根因分析,我们的根因分析主要基于专家系统,分为合并、归因及补充三步,由于异常点检测是对细粒度下的指标进行检测,那么就会有这样一个情况,比如,某运营商某地域服务发生问题导致产生多个疑似异常点,这些异常点的特征是运营商条件一样,但是具体省份信息不一样,那么,我们根据这两个信息对疑似异常点进行合并,把由于某运营商某地域故障导致的异常合并为一个异常点,在合并之后进行归因,寻找具体原因,我们会在实时原始数据里查询对应条件下增量最大的故障码,根据故障码判断导致问题发生的真实原因。最后根据故障条件与故障码,从实时原始数据中查询聚合业务特定的信息,加入最终的故障报告中,并将报告下发给对应的业务人员。
此外,我们也提供Ad-hoc查询,主要解决数据多维膨胀问题,目前我们能够支持20多个维度任意组合查询,并且可以对结果进行可视化,对查询结果进行分析和对比,帮助业务人员更好得分析数据。
5、总结 & 展望
总结来看,顶层设计以及合理设定有意义的指标是最重要的。如果这些指标本身意义不大,那么后面的计算也无法产出比较有用的信息。此外,可靠的平台也很重要,提升了开发效率,保障了任务稳定运行。我们在大数据实时计算部分主要使用了 Kafka、Flink、StreamingSQL以及Impala等组件,通过基于机器学习的异常检测减少人工干预,设计了基于专家系统的根因分析并开发了数据查询引擎。
未来,我们将增强StreamingSQL 功能,使其能够支持输出到Kafka以及ES这样的数据源;调研Kafka1.0以上版本,希望能够真正做到at-least-once;增强数据质量监控,希望达到对数据里面具体值分布情况的监控;考虑不同业务特点的异常检测,让异常检测对业务更加敏感;将故障日志与业务其他日志进行关联,获得更加完善的故障报告。