本文作者:付力力,神策数据联合创始人&技术vp
付力力毕业于北京理工大学软件工程专业,2008年至2013年期间历任百度新产品研发部、网页搜索部、基础架构部工程师。2013年9月年至2014年8月担任豌豆荚数据部门资深研发工程师。2014年9月至2015年4月担任黄金钱包技术合伙人。2018年8月,荣登“2018福布斯中国30岁以下精英榜”。
2015年9月神策数据正式发布了神策分析1.0版本,在随后的3年里,我们的产品研发团队一直在不断地进行版本迭代,到目前为止一共发布了12个大版本。
相比于最初的1.0版本,现在的神策分析无论是在产品体验还是在底层架构上都已经发生了很大的变化:
从最初只能使用3个单薄的基础分析功能,到现在支持10个分析模型联合构建的场景化分析能力;从最初只能支持每天数万日活的小app,到现在可以轻松应对一天产生数百亿的数据量巨型app。
而另一方面,3年内,神策分析里也有很多地方没有改变:
例如,从第一版的设计里就确定了模型的event-user,该模型现在依然是整个神策分析里最基础和重要的概念。
在这篇文章里,我给大家介绍神策分析最近在底层架构上一些比较大的设计改进,同时也会分享我们在这些架构设计中关于“变”与“不变”的思考。
一、从sql查询引擎到用户行为分析引擎
我们之前在很多场合都对神策分析的底层架构做过详细的介绍,这个架构的主要特点之一是:
神策所有的分析结果都是从明细数据实时查询得出,而不是基于大多数分析系统所使用的预计算技术,之所以这么设计,因为我们希望系统数据分析能力的上限在于数据本身。
换句话说,我们期望只要是从已经采集的数据里可以分析得到的结论,神策都希望可以帮助我们的客户很容易的实现。
从结果看来,这种架构设计的好处是非常显著的:
它大大简化了整个系统的数据流,我们不需要为不同的分析模型来维护复杂的聚合表,并在数据回溯的时候保持这些数据之间的一致性(大多数类似的数据系统里要么抛弃数据回溯的能力,要么放弃数据一致性)。
受益于这种架构,我们在很短的时间内推出众多灵活的分析模型,并且这些分析模型之间可以通过分群等方式来进行自由的组合查询。
同时,配合我们开发的查询缓存机制,这套架构也可以在报表等相对固定的数据分析需求上得到比较好的使用体验。
当然,这种设计的另外一个结果是,神策分析很明确地抛弃了对高qps查询需求的直接支持(例如不应该尝试在商品详情页里直接从神策分析获取这个商品本周的销量)。
不过,整体上我们认为,牺牲一个非必要的特性来换取数倍的分析灵活性以及一个简单可维护的架构,是一个非常划算的选择。
在这套架构里,impala作为我们使用的数据查询引擎,可以说是一个非常核心的模块。
在最初的设计选型上我们选择impala,一方面是因为impala已经是一个相对比较成熟的mpp架构的查询引擎,而且对sql有着比较良好的支持。另外一方面则是因为我们的研发团队在impala的使用和二次开发上有着比较多的经验。
其中,是否支持sql是一个很重要的选型依据。虽然sql是一种有着几十年历史、至今也没有太多变化的古老工具,但是到目前为止它依然是对表格数据进行操作的最佳选择,在易用性和灵活性之间做到了比较好的平衡。
更重要的是,我们当初经过简单的调研发现,只使用sql就可以很好的实现一个用户行为分析系统的大部分需求,除此之外,还可以通过udf/udaf/udanf等增加扩展能力,则几乎可以满足所有常见需求。
事实上,在神策分析比较早期的版本里,所有的分析模型都是用标准sql直接实现的。
随着我们产品功能的增加,我们为了满足越来越复杂的分析模型和更高的性能指标,也对impala做了很多改造。
不过,在这个过程中,sql自身的描述能力和impala执行架构的局限性也逐渐暴露出来,例如我们很难像spark的dag模型一样来灵活的控制sql的查询计划,导致一些复杂查询的性能不佳,以及在一些组合分析的场景下没有办法很容易的复用查询的中间结果。
因此,我们开始基于impala构建一个全新的查询引擎。通过对已有的各种分析模型计算过程的理解,我们发现它们几乎都可以被抽象为如下的计算过程:
1.筛选出特定时间范围内的特定event数据,如果查询还涉及到user/item,那么还需要再次进行join操作,最终得到:list;
2.对list按照event中的用户id进行shuffle,并按时间排序,最终得到每个用户id的有序event序列:(user id,list);
3.对(user id,list)中的每个userid的list应用具体的分析模型规则,例如漏斗、留存等,得出每个用户id的中间计算结果,如下:(user id,intermediateresult);
4.对(user id,intermediateresult)进行最后一次聚合,得到最终的结果。
不难看出,上述计算过程中最核心的难点在于如何快速的得到(user id,list),这中间可能涉及重排序和大数据量的shuffle等操作。对于需要join user/item表的查询,join本身的性能也可能会成为瓶颈。
我们基于impala原有的执行框架,在底层存储和查询逻辑上做了一系列的优化,最终实现的分析引擎相比于原有的方式在复杂查询的执行性能上有10x的提升,同时由于开发方式的简化,也直接加速了我们对各种复杂分析模型的迭代速度。
在后续的文章中,我们会详细介绍这个面向用户行为分析的查询引擎的具体优化细节。
二、模型扩展:从event-user到event-item-user
在神策分析最初的设计阶段,我们就确定了以event-user为核心的逻辑数据模型,可以说,event-user模型是整个神策分析架构的基础。
3年以来,神策分析在数百家不同行业的客户的实践结果也充分证明了这个模型的适应能力。
所有的数据模型本质上都是对现实世界的抽象,而在抽象之后必然会损失一些对现实世界的还原能力。
所以event-user模型虽然在电商、金融、在线教育、互联网娱乐、企业服务等不同的行业上都发挥了很好的价值,但是随着客户需求的不断深入,尤其是在和具体行业业务的深入融合中,我们也逐渐发现了这个模型的一些缺点。
例如在event-user模型中,出于性能和可解释性等各方面的考虑,event是被设计为不可变的。从逻辑上看似乎没有问题,因为event代表的是历史上已经发生过的事件,一般来说不应该需要进行更新。
但是,在实际的应用过程中,并不一定是这么理想的状态。
例如,在很多客户进行埋点采集的过程中,他们会发现某些event在最初的阶段并不能很容易的采集到完整的数据。
比如一个电商客户,在客户端app里采集“商品加入购物车”事件时,只能采集到商品的id、名称等基本信息,而对于后续分析需要的更多维度,例如商品的分类、促销的活动信息等等,则不一定能很容易的采集到(通常这些信息都是客户端在业务中没有使用到的,如果想要采集,则需要对服务端api、客户端内部的信息传递都做比较大的修改)。
又或者是等到真正需要分析的时候,才发现当初的采集是不完备的,这个时候想再把历史数据补上就是一件非常困难的事情。
还有另外一种比较常见的场景。某个在线教育的app中会有很多和课程相关的事件,例如对课程的浏览、购买、学习等,而关于课程的一些基本信息中会有许多是不断变化的,如课程的分类、定价等等。
在event里记录的,应当是event发生的时刻这个课程的状态,例如一个购买课程的事件,我们可以记录下来当时课程的分类、价格属性,作为event的一部分。而课程的分类、定价后续可能会随着业务的需要随时调整,如果业务方希望按照最新的(或者某个特定阶段的)课程分类或者定价来分析用户的历史行为,则是一个难以完成的需求。
从技术上来看,解决上述问题的方案并不复杂。很多熟悉数据仓库的朋友可能会发现,这些其实是在传统数据仓库里比较典型的维度表的问题,可以使用经典的雪花模型或者星型模型来轻松解决。
但是,我们并不希望引入这么复杂的模型,毕竟神策分析的设计目标并不是一个通用的数据仓库。虽然灵活性是神策分析最核心的设计目标之一,但也是建立在”用户行为分析”这个目标的基础之上的。
我们期望的一个理想方式是:对数据模型增加一点有限的复杂性,但是可以给整个系统带来十倍甚至百倍的灵活性提升。
为了满足上述需求,我们在新版的神策分析中对event-user模型进行了扩展,引入了item的概念。这里的所谓item,在严格意义上是指一个和用户行为相关联的实体,可能是一个商品、一个视频剧集、一部小说等等。
如果不严格约束的话,理论上它也可以存储其它任意的扩展维度信息。
在具体的技术实现上,我们允许客户定义多个不同的item实体,例如电商有商品、配送点等不同的实体。
在使用前,客户要定义这些实体,并且把这些实体的数据通sdk发送到神策分析系统中,自动建立起一个或者多个item表。然后,出于不同性能要求和业务需求的考虑,对于item表的使用我们提供了不同的两种方式。
第一种方式,客户在进行event埋点时,可以选择要进行关联导入的item信息。
例如有一个“商品加入购物车”的事件,这个事件里只采集了“商品id”,但是同时因为我们事先已经定义好了“商品item”,那么通过“商品id”则直接可以先把event和“商品item”进行关联,再把“商品item”的某一些属性作为event的一部分进行直接导入。
使用这种方式,可以在最大程度满足业务分析的情况下简化客户端对数据采集的工作,同时在查询性能方面也不会有任何下降。
第二种方式,更类似于传统数据仓库的维度表。
我们在埋点时不做任何变动,而是在需要进行查询的时候,把item表加入进来。
这种方式会有更好的灵活性,因为可以在event发生之后对数据进行扩展,也可以支持随时使用最新的item数据进行分析,但是另外一方面,这么做并不能很好的保留事件发生当时的某些状态,而且由于需要在查询的时候进行实时的数据join,也不可避免的会降低查询性能。
在把event-user模型扩展为event-item-user模型之后,神策分析对复杂业务场景有了更好的支持,无论是在埋点工作的简化还是在分析能力的提升上都有非常直接的帮助。
后续我们也将继续在简化item数据的接入和使用上做出更多的改进。
三、用户分群的进化
从2年前的神策分析1.4版本开始,我们引入了用户分群功能。从架构层面,我们主要做了两件事情:一是把分群的概念引入了我们的数据模型中,二是提供灵活、便利的定义分群规则的方式。
对于第一点,我们把分群看作是用户属性的一部分,只不过这个属性是根据用户已有的行为特征计算出来的,是一个衍生属性。所以在数据模型上,分群其实是对event-user模型中user部分的一个扩展。
当然,在物理存储上,由于分群具有频繁更新、整体删除等特点,因此并不会直接和原有的用户属性信息存储在一起,而是采用独立存储的方式。
对于第二点,一方面,我们提供了一套描述规则,允许客户直接从ui上定义比较复杂的分群:在某段时间做过某个event几次,或者完成了某个连续的event序列等。
更重要的是,我们把所有已有分析模型的用户列表功能都看作为是分群规则定义的一部分,这种方式使得客户可以很容易的把各个分析模型的结果进行组合,产生1+1>2的效果。
整体上来看,神策分析1.4在引入分群的概念之后,架构上几乎没有做任何大的改动,就可以让所有的分群和普通的用户属性一样在任何的分析模型里直接使用。
这个也是完全得益于前文提到的实时分析架构,以及具有良好扩展能力的event-user模型。
随着客户对神策分析的使用场景越来越复杂,我们的客户对分群功能也提出了更多的需求。
一个比较显著的问题是:现在的神策的每个分群只能保存一个最新的结果,而不能查看历史的状态。
比如在一个电商产品里,我们可以很容易的建立一个”日购买金额>=300″的用户分群,但是这个分群每天都会自动刷新,并且会丢掉前一天的状态。
如果我们想分析这个用户分群在时间轴上的变化趋势,或者考虑一个更复杂的场景,想分析“日购买金额>=300”的这个用户群体在当天购买的商品品类的分布情况,用现在的分群功能都是没办法直接实现的。
为了实现上述功能,我们在即将发布的1.13版本也对用户分群功能做了一次大的改进。
首先在数据模型上,我们扩展了分群的模型定义,加入了时间维度。即每个分群不只是代表这个分群的群体在某一时刻的状态,而是可以保存每天、每周等不同时间点下的状态。
其次,我们也进一步增强了分群的描述能力,除了增强了在ui上进行定义的功能之外,还允许用户直接上传分群好的结果(例如某个线下活动的用户列表),或者是从一个sql结果导出成一个分群,避免让分群的能力受限于已有的规则定义。
另外,在分群的计算执行层面,我们也不再使用独立的mapreduce程序来进行,而是复用了上面提到的基于impala的用户行为分析引擎。
因为分群的过程,其实也是一个很典型的用户行为分析的计算逻辑,这样就很自然的把整个神策系统内对于用户行为的分析都统一到了一个计算模块上来完成。
四、更精确的用户标识体系
如何准确地标识用户一直是用户行为数据系统中的一大难题。在过去的3年里,我们在客户端sdk、服务端架构、数据接入的解决方案支持上做了持续的优化,解决了很多普遍的问题。
传统的网站或者app分析工具,通常以cookie或设备号作为用户(其实是设备)的标识,同时这些分析工具大部分也并不支持跨端的分析,所以关于用户标识导致的各类问题并不突出。
但是在今天的用户行为分析场景中,准确的跨端标识用户变成了一个非常迫切的需求。尤其是在微信生态的情况下,一个自然人用户在app、小程序、h5、公众号之间反复跳转,完成一系列行为是非常常见的场景,如果不能做到准确的标识用户,很多数据分析的需求将会无法准确完成。
在神策分析1.13版本之前,为了解决跨端标识用户的问题,我们提供了有限度的多设备用户关联体系。
这里的“有限”主要体现在一个注册用户在未登录状态下只能跟一个设备进行绑定。很显然,在很多场景下这种关联并不能很好的满足需求。
最典型的场景是,如果一个老用户更换了新的设备,那么他在这个新设备上未登录状态下的操作将会被识别为一个全新的用户,从而对某些分析结果的准确性产生影响。
因此,我们在最近的1.13版本提供了一个注册用户跟任意多个设备进行关联的机制。在这个新的机制下,一个注册用户可以使用多个设备进行登录,并且他在这些设备上注册/登录前后的行为都会被准确的识别到同一个用户身上,从而能在神策分析里更准确的还原一个用户的行为序列。
当然,这个在新的关联机制也并不是提供无限的灵活性。考虑这样一个场景:一个设备先后被多个注册用户登录使用,那这个设备上产生的匿名行为(即非登录状态下产生的行为)只会被关联到第一个在这个设备上登录的注册用户。
虽然在技术上我们也可以很容易的实现用户和设备之间的多重绑定,但是考虑到实际的应用场景并不常见,而且提供这种机制之后一定会给客户带来的更多理解上的复杂性,我们还是决定把新的关联机制限定在一个注册用户多个设备的场景下。
全新的用户标识体系虽然可以更准确地标识用户,但是同时也会引入一个新的问题:允许一个注册用户和多个设备进行关联,会导致历史数据的分析结果是不断变化的。我们可以看一个具体的例子,假设一个用户x进行了一系列操作:
7月1日之前在设备a上注册、登录并使用app
7月2日开始在设备b上使用app
7月5日在设备b上使用之前的帐号进行登录,并继续使用
我们可以看到,在7月5日之前,神策分析并不知道使用设备b跟设备a背后都是用户x在操作,也就是说在这之前计算用户数都会是2,同时在计算留存、漏斗等数据时也都会当作两个不同的用户。
而一旦到7月5日用户x登录了,神策分析可以知道之前的行为其实都是同一个人x产生的,那么这个时候再看7月5日之前的用户数也会变成了1。
这种数据的变化在某些场景下可能会变得更加难以理解,我们假设一个比较极端的情况,如果上面的用户x是在一年之后才在设备b上进行登录,那么这一年内设备b所产生的行为是否都应该视作用户x产生的?现实情况下可能是,也可能不是,只凭借这些信息很难做出准确的判断。
本质上,新的用户标识体系是实现了对历史数据的修正,同时由于神策分析又是一个完全基于明细数据进行实时查询的分析系统,因此数据分析的结果跟着发生变化也是很自然的事情。
正如我们在上文的event-user模型扩展中提到的,虽然event代表的是已经发生的事件,但是依然会有一些信息在event发生的当时是无法得到的。
比如在上面的例子中,7月2日当天我们并不知道在设备b上使用的也是用户x,只能在3天之后再对这个数据进行修正。我们在一定程度上破坏了event的不变性,但是也带来了更高的数据准确性。
不过,除了技术上的难点,历史数据的变化还会给数据的可解释性造成比较大的影响:很多人都会对昨天甚至更早的数据报表会发生变化产生困惑。
因此,如何在提高数据准确性的同时降低客户对数据的理解难度,会是我们后面的重点方向。
关于神策数据
神策数据(https://www.sensorsdata.cn),是一家专业的大数据分析服务公司,致力于帮助客户实现数据驱动。公司推出深度用户行为分析平台神策分析(sensors analytics),支持私有化部署、基础数据采集与建模,并作为paas平台支持二次开发;同时推出基于行为数据的客户全生命周期分析平台神策客景(sensors journey),创造性将用户行为数据融入客户全生命周期的管理与分析,实现客群健康度分析,流失预警等重要价值,并应用到企业服务、工具软件等多个领域。
此外,还提供大数据相关咨询和完整解决方案。神策数据积累了中国银联、中国电信、百度视频、百联、万达、小米、中邮消费金融、广发证券、中原银行、百信银行、聚美优品、中商惠民、纷享销客、keep、36氪、中青旅、太平洋保险、平安寿险、链家、四川航空等500余家付费企业用户的服务和客户成功经验,为客户全面提供指标梳理、数据模型搭建等专业的咨询、实施、和技术支持服务。希望更深入了解神策数据或有数据驱动相关问题,请拨打4006509827电话咨询,会有专业的工作人员为您解答。