天天数百亿用户行为数据,美团点评怎么实现秒级转化分析?

背景

用户行为分析是数据分析中很是重要的一项内容,在统计活跃用户,分析留存和转化率,改进产品体验、推进用户增加等领域有重要做用。美团点评天天收集的用户行为日志达到数百亿条,如何在海量数据集上实现对用户行为的快速灵活分析,成为一个巨大的挑战。为此,咱们提出并实现了一套面向海量数据的用户行为分析解决方案,将单次分析的耗时从小时级下降到秒级,极大的改善了分析体验,提高了分析人员的工做效率。算法

本文以有序漏斗的需求为例,详细介绍了问题分析和思路设计,以及工程实现和优化的全过程。本文根据2017年12月ArchSummit北京站演讲整理而成,略有删改。安全

问题分析

下图描述了转化率分析中一个常见场景,对访问路径“首页-搜索-菜品-下单-支付”作分析,统计按照顺序访问每层节点的用户数,获得访问过程的转化率。bash

统计上有一些维度约束,好比日期,时间窗口(整个访问过程在规定时间内完成,不然统计无效),城市或操做系统等,所以这也是一个典型的OLAP分析需求。此外,每一个访问节点可能还有埋点属性,好比搜索页上的关键词属性,支付页的价格属性等。从结果上看,用户数是逐层收敛的,在可视化上构成了一个漏斗的形状,所以这一类需求又称之为“有序漏斗”。网络

问题

问题

这类分析一般是基于用户行为的日志表上进行的,其中每行数据记录了某个用户的一次事件的相关信息,包括发生时间、用户ID、事件类型以及相关属性和维度信息等。如今业界流行的一般有两种解决思路。数据结构

  1. 基于Join的SQL架构

    select count (distinct t1.id1), count (distinct t2.id2), count (distinct t3.id3) 
    from (select uuid id1, timestamp ts1 from data where timestamp >= 1510329600 and timestamp < 1510416000 and page = '首页') t1
    left join
    (select uuid id2, timestamp ts2 from data where timestamp >= 1510329600 and timestamp < 1510416000 and page = '搜索' and keyword = '中餐') t2
    on t1.id1 = t2.id2 and t1.ts1 < t2.ts2 and t2.ts2 - t1.ts1 < 3600
    left join
    (select uuid id3, timestamp ts3 from data where timestamp >= 1510329600 and timestamp < 1510416000 and page = '菜品') t3
    on t1.id1 = t3.id3 and t2.ts2 < t3.ts3 and t1.ts1 < t3.ts3 and t3.ts3 - t1.ts1 < 3600
    复制代码
  2. 基于UDAF(User Defined Aggregate Function)的SQL并发

select
funnel(timestamp, 3600, '首页') stage0,
funnel(timestamp, 3600, '首页', '搜索', keyword = '中餐') stage1, funnel(timestamp, 3600, '首页', '搜索', '菜品') stage2
from data
where timestamp >= 1510329600 and timestamp < 1510416000 group by uuid
复制代码

对于第一种解法,最大的问题是须要作大量join操做,并且关联条件除了ID的等值链接以外,还有时间戳的非等值链接。当数据规模不大时,这种用法没有什么问题。但随着数据规模愈来愈大,在几百亿的数据集上作join操做的代价很是高,甚至已经不可行。框架

第二种解法有了改进,经过聚合的方式避免了join操做,改成对聚合后的数据经过UDAF作数据匹配。这种解法的问题是没有足够的筛选手段,这意味着几亿用户对应的几亿条数据都须要遍历筛选,在性能上也难以接受。运维

那么这个问题的难点在哪里?为何上述两个解法在实际应用中变得愈来愈不可行?主要问题有这么几点。分布式

  1. 事件匹配有序列关系。若是没有序列关系就很是容易,经过集合的交集并集运算便可。
  2. 时间窗口约束。这意味着事件匹配的时候还有最大长度的约束,因此匹配算法的复杂度会进一步提高。
  3. 属性和维度的需求。埋点SDK提供给各个业务线,每一个页面具体埋什么内容,彻底由业务决定,并且取值是彻底开放的,所以目前属性基数已经达到了百万量级。同时还有几十个维度用于筛选,有些维度的基数也很高。
  4. 数据规模。目前天天收集到的用户行为日志有几百亿条,对资源和效率都是很大的挑战。

基于上述难点和实际需求的分析,能够总结出几个实际困难,称之为“坏消息”。

  1. 漏斗定义彻底随机。不一样分析需求对应的漏斗定义彻底不一样,包括具体包含哪些事件,这些事件的顺序等,这意味着彻底的预计算是不可能的。
  2. 附加OLAP需求。除了路径匹配以外,还须要知足属性和维度上一些OLAP的上卷下钻的需求。
  3. 规模和性能的矛盾。一方面有几百亿条数据的超大规模,另外一方面又追求秒级响应的交互式分析效率,这是一个很是激烈的矛盾冲突。

另外一方面,仍是可以从问题的分析中获得一些“好消息”, 这些也是在设计和优化中能够利用的点。

  1. 计算需求很是单一。这个需求最终须要的就是去重计数的结果,这意味着不须要一个大而全的数据引擎,在设计上有很大的优化空间。
  2. 并发需求不高。漏斗分析这类需求通常由运营或者产品同窗手动提交,查询结果用于辅助决策,所以并发度不会很高,这样能够在一次查询时充分调动整个集群的资源。
  3. 数据不可变。所谓日志即事实,用户行为的日志一旦收集进来,除非bug等缘由通常不会再更新,基于此能够考虑一些索引类的手段来加速查询。
  4. 实际业务特色。最后是对实际业务观察得出的结论,整个漏斗收敛很是快,好比首页是几千万甚至上亿的结果,到了最下层节点可能只有几千,所以能够考虑一些快速过滤的方法来下降查询计算和数据IO的压力。

若是用一句话总结这个问题的核心本质,那就是“多维分析序列匹配基础上的去重计数”。具体来讲,最终结果就是每层节点符合条件的UUID有多少个,也就是去重后的计数值。这里UUID要符合两个条件,一是符合维度的筛选,二是事件序列能匹配漏斗的定义。去重计数是相对好解的问题,那么问题的重点就是若是快速有效的作维度筛选和序列匹配。

算法设计

下图是部分行为日志的数据,前面已经提到,直接在这样的数据上作维度筛选和序列匹配都是很困难的,所以考虑如何对数据作预处理,以提升执行效率。

数据1

数据1

很天然的想法是基于UUID作聚合,根据时间排序,这也是前面提到的UDAF的思路,以下图所示。这里的问题是没有过滤的手段,每一个UUID都须要遍历,成本很高。

数据2

数据2

再进一步,为了更快更方便的作过滤,考虑把维度和属性抽出来构成Key,把对应的UUID和时间戳组织起来构成value。若是有搜索引擎经验的话,很容易看出来这很是像倒排的思路。

数据3

数据3

这个数据结构仍是存在问题。好比说要拿到某个Key对应的UUID列表时,须要遍历全部的value才能够。再好比作时间序列的匹配,这里的时间戳信息被打散了,实际处理起来更困难。所以还能够在此基础上再优化。

能够看到优化后的Key内容保持不变,value被拆成了UUID集合和时间戳序列集合这两部分,这样的好处有两点:一是能够作快速的UUID筛选,经过Key对应的UUID集合运算就能够达成;二是在作时间序列匹配时,对于匹配算法和IO效率都是很友好的,由于时间戳是统一连续存放的,在处理时很方便。

数据4

数据4

基于上述的思路,最终的索引格式以下图所示。这里每一个色块对应了一个索引的block,其中包括三部份内容,一是属性名和取值;二是对应的UUID集合,数据经过bitmap格式存储,在快速筛选时效率很高;三是每一个UUID对应的时间戳序列,用于序列匹配,在存储时使用差值或变长编码等一些编码压缩手段提升存储效率。

索引格式

索引格式

在实际应用中,一般会同时指定多个属性或维度条件,经过AND或OR的条件组织起来。这在处理时也很简单,经过语法分析能够把查询条件转为一颗表达树,树上的叶子节点对应的是单个索引数据,非叶子节点就是AND或OR类型的索引,经过并集或交集的思路作集合筛选和序列匹配便可。

上面解决的是维度筛选的问题,另外一个序列匹配的问题相对简单不少。基于上述的数据格式,读取UUID对应的每一个事件的时间戳序列,检查是否能按照顺序匹配便可。须要注意的是,因为存在最大时间窗口的限制,匹配算法中须要考虑回溯的状况,下图展现了一个具体的例子。在第一次匹配过程当中,因为第一层节点的起始时间戳为100,而且时间窗口为10,因此第二层节点的时间戳101符合要求,但第三层节点的时间戳112超过了最大截止时间戳110,所以只能匹配两层节点,但经过回溯以后,第二次能够完整的匹配三层节点。

匹配算法

匹配算法

经过上述的讨论和设计,完整的算法以下图所示。其中的核心要点是先经过UUID集合作快速的过滤,再对过滤后的UUID分别作时间戳的匹配,同时上一层节点输出也做为下一层节点的输入,由此达到快速过滤的目的。

算法设计

算法设计

工程实现和优化

有了明确的算法思路,接下来再看看工程如何落地。

首先明确的是须要一个分布式的服务,主要包括接口服务、计算框架和文件系统三部分。其中接口服务用于接收查询请求,分析请求并生成实际的查询逻辑;计算框架用于分布式的执行查询逻辑;文件系统存储实际的索引数据,用于响应具体的查询。

这里简单谈一下架构选型的方法论,主要有四点:简单、成熟、可控、可调。

1.简单。无论是架构设计,仍是逻辑复杂度和运维成本,都但愿尽量简单。这样的系统能够快速落地,也比较容易掌控。 2.成熟。评估一个系统是否成熟有不少方面,好比社区是否活跃,项目是否有明确的发展规划并能持续落地推动?再好比业界有没有足够多的成功案例,实际应用效果如何?一个成熟的系统在落地时的问题相对较少,出现问题也能参考其它案例比较容易的解决,从而很大程度上下降了总体系统的风险。 3.可控。若是一个系统持续保持黑盒的状态,那只能是被动的使用,出了问题也很难解决。反之如今有不少的开源项目,能够拿到完整的代码,这样就能够有更强的掌控力,无论是问题的定位解决,仍是修改、定制、优化等,都更容易实现。 4.可调。一个设计良好的系统,在架构上必定是分层和模块化的,且有合理的抽象。在这样的架构下,针对其中一些逻辑作进一步定制或替换时就比较方便,不须要对代码作大范围的改动,下降了改形成本和出错几率。

基于上述的选型思路,服务的三个核心架构分别选择了Spring,Spark和Alluxio。其中Spring的应用很是普遍,在实际案例和文档上都很是丰富,很容易落地实现;Spark自己是一个很是优秀的分布式计算框架,目前团队对Spark有很强的掌控力,调优经验也很丰富,这样只须要专一在计算逻辑的开发便可;Alluxio相对HDFS或HBase来讲更加轻量,同时支持包括内存在内的多层异构存储,这些特性可能会在后续优化中获得利用。

在具体的部署方式上,Spring Server单独启动,Spark和Alluxio都采用Standalone模式,且两个服务的slave节点在物理机上共同部署。Spring进程中经过SparkContext维持一个Spark长做业,这样接到查询请求后能够快速提交逻辑,避免了申请节点资源和启动Executor的时间开销。

架构概览

架构概览

上述架构经过对数据的合理分区和资源的并发利用,能够实现一个查询请求在几分钟内完成。相对原来的几个小时有了很大改观,但仍是不能知足交互式分析的需求,所以还须要作进一步的优化。

  1. 本地化调度。存储和计算分离的架构中这是常见的一种优化手段。如下图为例,某个节点上task读取的数据在另外节点上,这样就产生了跨机器的访问,在并发度很大时对网络IO带来了很大压力。若是经过本地化调度,把计算调度到数据的同一节点上执行,就能够避免这个问题。实现本地化调度的前提是有包含数据位置信息的元数据,以及计算框架的支持,这两点在Alluxio和Spark中都很容易作到。

优化1

优化1

  1. 内存映射。常规实现中,数据须要从磁盘拷贝到JVM的内存中,这会带来两个问题。一是拷贝的时间很长,几百MB的数据对CPU时间的占用很是可观;二是JVM的内存压力很大,带来GC等一系列的问题。经过mmap等内存映射的方式,数据能够直接读取,不须要再进JVM,这样就很好的解决了上述的两个问题。

优化2

优化2

  1. Unsafe调用。因为大部分的数据经过ByteBuffer访问,这里带来的额外开销对最终性能也有很大影响。Java lib中的ByteBuffer访问接口是很是安全的,但安全也意味着低效,一次访问会有不少次的边界检查,并且多层函数的调用也有不少额外开销。若是访问逻辑相对简单,对数据边界控制颇有信心的状况下,能够直接调用native方法,绕过上述的一系列额外检查和函数调用。这种用法在不少系统中也被普遍采用,好比Presto和Spark都有相似的优化方法。

优化3

优化3

下图是对上述优化过程的对比展现。请注意纵轴是对数轴,也就是说图中每格表明了一个数据级的优化。从图中能够看到,常规的UDAF方案一次查询须要花几千秒的时间,通过索引结构的设计、本地化调度、内存映射和Unsafe调用的优化过程以后,一次查询只须要几秒的时间,优化了3~4个数据级,彻底达到了交互式分析的需求。

优化对比

优化对比

这里想多谈几句对这个优化结果的见解。主流的大数据生态系统都是基于JVM系语言开发的,包括Hadoop生态的Java,Spark的Scala等等。因为JVM执行机制带来的不可避免的性能损失,如今也有一些基于C++或其它语言开发的系统,有人宣称在性能上有几倍甚至几十倍的提高。这种尝试固然很好,但从上面的优化过程来看,整个系统主要是经过更高效的数据结构和更合理的系统架构达到了3个数量级的性能提高,语言特性只是在最后一步优化中有必定效果,在总体占比中并很少。

有一句鸡汤说“以大多数人的努力程度而言,根本没有到拼天赋的地步”,套用在这里就是“以大多数系统的架构设计而言,根本没有到拼语言性能的地步”。语言自己不是门槛,代码你们都会写,但整个系统的架构是否合理,数据结构是否足够高效,这些设计依赖的是对问题本质的理解和工程上的权衡,这才是更考量设计能力和经验的地方。

总结

上述方案目前在美团点评内部已经实际落地,稳定运行超过半年以上。天天的数据有几百亿条,活跃用户达到了上亿的量级,埋点属性超过了百万,日均查询量几百次,单次查询的TP95时间小于5秒,彻底可以知足交互式分析的预期。

效果总结

效果总结

整个方案从业务需求的实际理解和深刻分析出发,抽象出了维度筛选、序列匹配和去重计数三个核心问题,针对每一个问题都给出了合理高效的解决方案,其中结合实际数据特色对数据结构的优化是方案的最大亮点。在方案的实际工程落地和优化过程当中,秉持“简单、成熟、可控、可调”的选型原则,快速落地实现了高效架构,经过一系列的优化手段和技巧,最终达成了3~4个数量级的性能提高。

相关文章
相关标签/搜索