乐趣区

关于大数据:如何实现一款毫秒级实时数据分析引擎

业务背景

随着 Shopee 业务一直扩张,为了更加理解用户对产品的行为反馈,更好地决策产品个性,各团队外部涌现出大量数据分析的需要。例如:客户端用户行为剖析(如跳转行为、页面留存等),业务外围指标剖析(购买量、购买品类),甚至于 A/B Test 的后果数据分析,都须要一套数据体系来撑持。而通过传统离线数据产出未然不能满足实时经营、流动投放、异样问题发现等需要。

为了反对这些实时数据分析能力,咱们团队开发了 Boussole——多维数据实时剖析零碎,旨在通过低成本的形式撑持海量多维数据实时剖析。本文将详细描述零碎中的实时剖析查问引擎 Boussole Engine 作为多维数据分析的外围一环,是如何通过对引擎的设计撑持毫秒级实时数据分析后果返回。

1. 介绍

Boussole 作为多维分析平台,与大多数实时剖析零碎有相似的数据流向。从数据源拉取数据并通过前置荡涤,通过用户在平台中定义的指标和维度以及汇聚形式实时聚合后,将产生的后果数据落入长久化存储,用户通过平台前端配置的相干视图及 Dashboard 实时观测这些最新汇聚出的数据后果。

整个零碎的外围在于如何能在海量数据上报时提供疾速的查问能力。通过获取数据后的预汇聚解决流程,让引擎能在指定维度下疾速返回查问后果,但这样带来了额定的存储开销。而通过引擎实现的二次汇聚能力,可能在局部维度不命中预汇聚规定时也能以较快速度查问到后果,从而缩小了存储开销。零碎提供了较大的灵活性来让用户感知并管制查问速度和存储开销之间的取舍。

咱们在整个数据流中的每个阶段都投入了不少的设计精力,来应答海量数据带来的压力,本文仅就其中外围的数据查问引擎来介绍设计思路和具体架构。团队外部启动时面临的首要问题是如何设计一种前后端查问数据和交互的协定,使用户能不便地在前端通过本人的需要查问多维数据。咱们在初期调研了一些支流时序数据分析产品,它们次要分为以下几类:

  • 类 SQL 的时序数据查问形式,次要有 TimescalesDB[1] 和基于 InfluxQL 实现的 InfluxDB[2],外围思路是通过 SQL 的形式将维度筛选、维度汇聚、指标间运算和工夫过滤等规范的时序数据操作通过 SQL 形容并将后果返回给用户。
  • 通过 JSON 自定义查问 Schema,次要有 OpenTSD[3]KairosDB[4],客户端须要查问的指标和维度明确指定在 JSON 字段中,服务端将查问的时序数据后果按要求返回。
  • 自定义语言实现的查问,次要有 Promethues 的 PromQL[5] 和基于 Flux[6] 实现的 InfluxDB,它们各自都有一套独立的查问语法定义,并且能较好地反对筛选、指标计算和维度汇聚。

在选型上,咱们最终应用了 PromQL 来作为前后端查问协定,外围起因是它的性能和易用性及业界的应用宽泛水平。作为一种表现形式良好的时序数据查询语言,它能满足在前端查问时维度筛选、汇聚和指标计算的所有需要。并且,它的表现形式简略,在有简单的汇聚需要(多维度复合指标运算、时序子查问等)时能通过自定义查问能力剖析现有数据,相比于 SQL 的简单表述和 OpenTSDB 过于简略的查问性能,PromQL 更合乎需要。

要想做到实时剖析查问,在我的项目初期就应该对将来能达到的成果有明确布局。咱们心愿不管有多少原始数据上报,在查问响应速度方面都能达到毫秒级,下文将详细描述咱们是如何设计零碎并达到这一指标的。

2. 存储模型

在理解如何实现查问流程前,先介绍一下 Boussole 底层的多维时序数据存储模型。对于多维时序数据的存储,业界大部分实现都是相似的,外围思路是将多维数据细化到粒度最小的单个维度转化为 KV 格局,再通过保留单维度与多维度之间的映射关系,从而将多维时序数据映射在长久存储中。

这里以温度为一个指标举例,阐明零碎外部如何解决多维时序数据:每个城市都有一个温度采集站,会定时收集此地的温度数据,将数据上报至气象局。并且,因为温度垂直递加的关系,采温站并不会只采集一个高度的数据,而是一批高度的数据。这些数据是不同的,通常状况下在对流层中海拔越高气温越低。这样,温度随工夫、高度、地区的变动就造成了一组多维时序数据。

如上图所示,采集好的多维数据降维后转换成 KV 格局,不便落地在后端的长久存储中,这样做的益处是不管有多少维度,最终存储的格局是雷同的。

依照这个思路,其实可能选型的具体存储引擎有很多,思考到运维老本和社区的成熟度,最终咱们选用 HBase 作为后端存储工具。引擎底层为了适配不同的存储类型,实现了一个存储适配层,使得零碎能够在 Redis、Memcache、RocksDB、TiKV 等相似存储作为后端时疾速对接,这种做法参考了 Promethues-Remote-Storage-Integrations[7]

但以这种数据模型存储,是很难查问的。如果不加以解决,多维分析指定维度进行查问时,须要扫描整个以 Temperature 为前缀的所有数据,挑选出用户指定维度后再进行过滤。如果原数据维度组合有很多,这样做的 IO 开销会十分大。为了减速查问过程,零碎会对原始数据做预聚合操作。并且为了实现用户在理论应用中维度筛选的便捷性,零碎在汇聚时会将某个维度下存在哪些具体维度保留下来,不便后续的筛选聚合剖析操作。

2.1 指标的预汇聚

预汇聚的次要目标是当用户以某个维度做聚合操作时能够间接返回数据而不须要做二次计算。用上述采温站的例子来说,此时用户想看到全地区高度为 5 的平均温度,或想看到北京市所有高度的平均温度。若想放慢这些数据的返回速度,预聚合是十分要害的一个步骤,它决定了查问引擎在执行时的具体方法。如果要减速这两个条件查问,预汇聚须要的配置及成果如下图所示:

预汇聚产生的一个问题就是存储放大,这种放大成果会随着维度值的数量和具体的预汇聚规定而发生变化(一个维度个数为 N 的原始数据,如果开启全排列,减速所有条件下的查问,存储会放大为原来的 2^N),抉择预汇聚的维度组合须要用户基于其具体应用场景的了解;在数据接入时评估数据模型,也须要对具体分析场景有事后理解。后续的章节将会详述零碎理论应用中是如何通过预汇聚和二次汇聚穿插应用来均衡存储和查问速度带来的影响。

2.2 指标的存储

当然这也不是数据在存储中的最终保留模式,落地存储时还需对这些数据做一些转换。零碎在汇聚逻辑最终产生数据后果后会将 Metric 和 Tags 的局部通过 FNV64a 进行 Hash,对工夫戳进行 uint32 编码,值作为 float64 保留,具体落盘的 KV 格局如下:

在指标存储时系统对指标和维度明细进行了 Hash,次要是为了保障 Metric 表中 Key 是定长的,这样在 Range 提取时序曲线过程中不会出问题,避免其余脏数据混入。其次是因为有些数据的维度个数可能很多,导致 Key 较长,影响对存储量的评估。

2.3 维度的存储

零碎将维度存下来是因为在前端查问时,用户须要用到维度筛选和维度过滤性能。在理论的存储系统中,每个维度值是一个没有 Value 的 KV 对,因为只用到了 Key 这个属性来筛选和去重。理论应用时,用户只须要晓得指标、维度和剖析工夫区间,就能够获取这段时间存在的维度值列表。

这里有一个细节,存入维度表时的工夫和指标上报的工夫不一样了,存入维度的工夫比维度理论呈现工夫早了一些(例如图中存入工夫就比理论呈现工夫少了 10 秒)。其实在存储 Tag 的过程中,零碎会强制将 Tag 的工夫左对齐到每个整点小时。这么做是因为在时序剖析场景中,用户不关怀某个维度值在某个工夫点是否呈现,取而代之的是一段时间内,这个维度下有哪些维度值,通过事后对齐到小时节俭了大量的存储空间(一小时内反复呈现的维度值不会被写入,假如某个维度值在一小时内都稳固呈现,没有断流,预聚合工夫粒度为十秒,大概能节俭 (3600-10)/3600 ≈ 99.72% 的存储空间)。

采纳上述整点对齐的存储形式也引入了新的问题,在查问某维度下具体的维度值时可能会混入一些脏数据。如上图所示,存储落地时会把这两个 Location 维度值同时存储在 10:00-11:00 这个区间内。此时,如果用户想查问 10:30-11:00 期间的数据,Locaiton = Beijing 这个维度值会被扫描进去,然而理论状况是这段时间并不存在这个数据点。前面的章节将详细描述如何解决掉这些脏维度,并且使它们不在数据查问时返回。

3. 剖析查问流程

时序数据的查问流程概括来说是用户输出一个 Query,零碎返回一系列带标签的曲线组合。通常用户不仅会查看在存储里的原始汇聚信息,也会对这些信息做上卷、筛选聚合、运算等一系列操作,最终失去本人想要的数据后果,整个查问引擎的工作流程都是围绕这些性能开展的。

在零碎中一次查问次要经验以下几个阶段:首先是 PromQL 的 Parser 和 Optimizer,这里间接应用了开源 MetricsQL,相比 Promethues 原生的 PromQL,它具备更多的拓展能力,不便当前在查问过程中的各种定制化拓展。晚期的 Boussole 版本在拿到解析器生成的 AST 后就间接开始数据获取和数据加工流程,首要的工作就是数据抽取,此时须要晓得存储里具体哪些曲线是一次查问所须要的。

具体哪些细化的维度时序须要从存储中抽取进去,取决于用户在前端进行的维度筛选和维度开展。这里的维度筛选对应到上述温度采集的例子中,具体的应用场景是只查看地位为北京的数据,或查看高度不等于 200 的所有数据。维度筛选通常来说是比较复杂的,明确且固定值的维度筛选能够在数据获取时少查一次存储(不须要确定这个 Key 是否存在,间接可能拼接完 Metric 表中的 Key),除此之外诸如大于、不等于、正则匹配等各种非确定性查问都须要再次获取全量维度值来逐个进行匹配,命中的维度值须要退出待抽取指标数据的维度列表中。

例如用户在发动查问时指定了筛选条件 location=(Beijing||Shanghai),height!=200,在筛选待抽取数据列表时整个流程如上图,最初失去的待抽取指标数据维度列表就是须要在底层存储查问的具体曲线。

维度汇聚也影响着须要拉取的数据集的大小。在多维时序剖析中,用户查问到的后果往往不止一条曲线,而是在某个维度下钻或上卷的后果,或是某几个维度下钻或上卷的后果。并且,维度汇聚和维度抉择会产生肯定的关系。如果汇聚和筛选作用在同一个维度中,那么筛选的优先级是比汇聚高的,这时须要先排除用户筛选掉的维度后再汇聚数据才会产生正确后果。

筹备好待抽取指标数据列表后,须要解决的就是聚合逻辑以及指标间运算。实质上来说这些操作都是对一批带标签的曲线汇合进行数学运算。但因为曲线带上了标签,所以一些解决逻辑变得有些简单。比方在聚合逻辑中,依照一个维度下钻并对其余所有维度取 Max 操作,最终,除了此维度以外其余维度都不会保留下来,曲线的标签产生了变动。在指标间运算过程中,只有雷同标签的曲线才会参加计算。例如计算以 URL 维度开展的成功率,须要用胜利数除以总数,只有维度完全相同时,曲线逐点计算才有意义。不过在指标与实数计算的过程中,实数会疏忽标签,与所有维度标签一起计算,计算作用于每条指标曲线中,所以能够认为实数计算时是带有任意标签的。

在理论场景中做到以上的剖析查问性能其实曾经满足了绝大部分需要,但在能力拓展上仍留有很大空间,比方:须要反对一些特定的时序解决逻辑时会自定义时序处理函数,并在前端提醒这些可用的函数用法。

在下表中咱们将简述 MetricsQL 和 FLux 的区别。如果最后选用 Flux 作为前后端的查问协定,能够在发动查问时让用户自定义这些函数,在发动时间接提交。尽管有较高的自由度,但最后选型时咱们并没有应用 Flux,外围起因是它是一种新的查询语言,了解并学习须要破费较高老本。并且,未经优化的 Flux 语句可能会导致额定的资源耗费,这些 Query 提交至后盾解决时,零碎须要在资源限度和超时管制上做一些额定工作,能力保障执行性能和稳定性合乎预期。

查询语言 拓展能力 学习老本 资源耗费管制
MetricsQL 较弱。须要在语言层面定义新性能。 较低。PromQL 语法简略。2014 年公布后广泛应用在监控畛域,能接触到的材料较多。 可控水平高。能严格控制每个执行打算中的计算量和数据量。
Flux 强。反对自定义函数,有大量包能够援用。 较高。自定义了一套相似 Lambda 的流解决语言,2018 年公布后应用在 InfluxDB 上运行。 可控水平低。引入自定义函数无奈准确控制数据在集群中理论的资源耗费。

4. 查问条件与预汇聚规定

Boussole 在窗口汇聚时并不会将所选维度的所有组合都进行预汇聚计算,在配置数据源时会让用户抉择一些事后须要查问的维度组合进行预汇聚,从而在查问时可能疾速返回后果。事后设定维度组合进行汇聚计算 是预汇聚统计里罕用的一种形式,它在查问速度和存储大小之间做出了肯定均衡。存储空间有余时,适当缩小预汇聚的维度组合数,能缩小存储开销。相同,如果开启全副维度组合的预汇聚,可能使用户在任意维度下自由组合查问并且放弃疾速的响应工夫。如下图中预汇聚的后果:

在上图中,命中预汇聚规定时,如果用户查问条件 A=1,C=3 下 MetricX 的和值,存储会间接返回 12,但如果命中没有命中预汇聚的查问,例如用户这时查问 D=12 下 Metric 的和值或 A=1 下 Metric 的和值,都是无奈通过现有存储间接返回的,所以引擎必须要实现二次汇聚,通过现有汇聚好的数据进行二次加工失去用户想要查问的后果。

其实这里的实现思路比较简单:抉择一个预汇聚后果中绝对于指标查问维度最匹配的汇聚后果进行二次汇聚,例如用户想查问 A=1 下的值,通过组合 [A,B] 汇聚后果间接能够取出三条数据,并将这三条数据合并失去后果 Sum(MetricX){A=1}=40。但这个后果并不是最优的,因为通过组合 [A,C] 只须要两条数据就能汇聚出雷同后果。所以这里定义的最优匹配其实是为了汇聚指标后果所须要获取最小数据量的预汇聚汇合。当然为了保障用户的每个查问都是有后果的,零碎设计在预汇聚时必须开启一个全副维度的组合(如例子中的 [A,B,C,D]),这样不管用户须要查问任何子维度集,都会是这个选集的子集。

通用化一些,用户须要查问维度集 X 的汇聚后果,此时有预汇聚维度集列表 YL=[Y1,Y2,……Yn],零碎须要先判断 X∈YL,如果成立则间接去底层查问后果数据,不须要二次汇聚。如果不存在,则须要逐个计算 YL 中所有成员与 X 的差集 DYL=[DY1,DY2……DYn],如果这个后果存在且非空,逐个在维度表中查问这些维度下的维度值个数,选取乘积最小的一组差集,并追回导出它的 Yx,这个预汇聚组合 Yx 就是查问维度集 X 的最优的二次汇聚数据起源。

在理论生产中从 X => Yx 的关系推导损耗是比拟大的,外围耗时次要是破费在计算某一维度下的值个数有多少(对应存储的 RowCount 操作),为了减速后续雷同维度组合的二次汇聚查问,引擎会把这种对应关系缓存下来以备后用。在缓存的生存工夫抉择上,咱们采纳了与 TCP 慢启动机制相似的策略,如果缓存过期后下次的推导后果没有发生变化,则阐明这个指标的维度数目绝对稳固,零碎会翻倍此缓存工夫,避免频繁计算汇聚关系导致的额定性能开销。

5. 抽样和清空

Boussole 目前提供给用户可选的汇聚最小工夫粒度为 10s,受限于所领有的存储资源的大小,零碎将存储的最长保留期限设定为一年半,日常应用时用户常常会查问近一个月的数据来察看数据稳定,这是一个很常见的需要,而如此细粒度的数据在做用户展现时也有不小的压力。

这种压力来自两方面,一个是前端渲染给浏览器带来的压力,另外是查问的后果申请数据很大,一般客户带宽传输就须要较长时间期待。以一个二十条曲线汇聚统计图为例,假如汇聚粒度为 10s,查问近 60 天的数据。前端共须要渲染的点个数为 10,368,000 个,如果以纯二进制数据在 Web 中传输,疏忽维度信息和申请头尾,一个 uint32 类型工夫戳 4Byte 和一个 float64 类型的值 8Byte,整个包大概须要 118.65MB,开启 Delta-of-Delta 压缩后须要 15.1MB 的传输大小。这个体积的返回如果须要用户在发动申请后 350ms 返回,就算疏忽服务端的解决工夫,用户须要 345M 的带宽能力保障响应工夫达标。

在查看长期趋势图时,用户不关怀是否每个点都能展现,这时用户理论察看的是曲线的稳定及大体趋势。在查看趋势时,如果某个细节呈现了异样,用户通常会对这个工夫区间放大,察看区间中某些异样点的具体值,这时须要对这些数据点进行明确返回。所以区间工夫长意味着须要疏忽部分细节,工夫短则要全量展现。为了均衡这两者之间的关系,须要管制单条曲线能显示点的个数,在后端做抽样逻辑解决,无论查问的工夫多长,放弃抽样的输入后果大小即可。

在理论生产中,系统配置的抽样准则是保留 3840 个点,起因是这个数字是目前的显示设施横向分辨率值的广泛大小,能够让前端渲染出图在一个 4K 显示器全屏展现而不失真,尽可能利用设备的显示劣势展现每一个数据点。以 3840 来预估方才的例子,60 天的曲线数据开启压缩后大概为 117.9KB,不仅减速了传输,放慢了端上的渲染速度,同时也升高了服务端进口带宽的压力。

6. Distributed PromQL Executor 架构设计

上线一段时间后,随着业务上报的维度组合数变多,咱们通过对系统性能和资源进行监控,发现了一些乏味的景象:

  • 某些查问节点的资源应用会因为一个简单 Query 忽然升高。因为每个 Query 在查问节点中都是独自解决的,在动辄几万甚至上十万维度的汇聚,波及到子查问和多指标间计算时,单个节点的资源耗费会飙升。
  • 一次查问会向存储发送大量拉取申请,导致内核 TCP 缓存队列缓存阻塞。因为每个 Query 在维度筛选和汇聚后须要查问的根底数据可能会达到上万至十万条,每条曲线都会波及对存储进行一次区间扫描,短时间内大量 RPC 申请间接影响了查问的响应工夫。

第一个问题是零碎的隐患,查问资源无奈平均分配,在整体利用率不高的状况下偶然单节点疾速打满,使得零碎的下限不稳固。第二个问题则更重大,影响了单个申请的响应工夫,并且机器可能因为 TCP 内核阻塞影响其余查问申请,呈现雪崩景象。

为了解决这些问题,Boussole Engine 参考了 CockroachDB(CRDB) Distributed SQL 的设计思路,实现了一个简略的 Distributed PromQL Executor。CRDB 的分布式 SQL 实现比较复杂,它采纳查问和存储节点绑定的形式,能将适宜的执行打算挪动到间隔存储更近的节点执行。只管实现上因为架构的不同存在一些差别,不过解决问题的思路是雷同的,都会将查问申请转化为分布式解决打算,将单个查问绑定到集群中的多个节点上,由收到原始申请的节点通过最初的一系列解决(后续会提到抽样及清空逻辑)返回给客户端。

例如一个计算 URL 可用性的简略表达式,它用到了简略的指标间运算,须要拉取两个指标来进行除法运算,最初通过聚合函数在 URL 维度上聚合曲线,具体的执行打算如下图所示:

因为数据获取波及的操作对单机网络可能造成的影响,在引擎设计时让某些步骤强制调配到其余节点执行,而有些简略的过滤和汇总在以后节点计算,具体的决策取决于零碎在执行时评估要计算的数据量。

启动分布式查问之后,资源飙升的景象在集群中有所缓解,各个执行节点的资源应用也趋于均匀,集群内节点资源利用率日内最大差别由 896.28% 降落到 171.86%。但正如预期,因为分布式执行造成了额定的网络通信,导致整体执行工夫变长。咱们统计了一周内用户的查问状况后发现,原来均匀一次的查问,额定减少了 2.2 次 RPC 拜访,因为节点之间的数据挪动在两端编解码的额定开销,导致整体查问工夫均匀减少了 31.9ms。

实际上对于简略查问做分布式解决的确是存在额定开销的,咱们做查问分布式解决的初衷是为了均衡资源,但一些简略 Query 并不会引起性能资源的额定耗费。相同,启用分布式查问后耗时减少了。为此须要寻找一个开启分布式查问的临界点,将简略查问和简单汇聚区别解决,做到开销与收益的均衡。这个临界点是基于查问细粒度曲线的个数和时长决定的,总体上这也反映了须要查问数据集的包大小。具体的值如何设定,是依据集群所能容忍的资源不均衡度决定的,理论生产中大多数用户查问的简略 Query 都在 144,000 个点的数据体积之内,所以零碎将这个值定为是否开启分布式查问的条件。

为了应答第二个问题,咱们首先在机器层面进行了调整,启动了网卡多队列,并且增大了 TCP 缓冲区大小等参数。但这些调整并不能间接解决问题,实质上只是在网络层面抛出异样和期待时长方面做肯定的周旋。基本的解决办法是引入了 HBase Coprocessor 来将大量申请组合成单个申请,并在 Coprocessor 中启用了 Delta-of-Delta[8] 时序压缩算法,在理论生产中对 90 万条一小时的时序曲线进行压缩测试,Delta-of-Delta 能够实现 13.1% 的压缩比,节约大量传输带宽。

7. 将来瞻望

作为一套落地理论利用场景中的查问剖析引擎,Boussole Engine 仍处于起步阶段,有很多须要打磨和优化的细节,同时也有大量的遗留工作须要实现。现阶段的成绩一部分是对开源产品的参考,一部分是业界相干畛域通用解决方案的落地,还有一部分是团队外部在理论应用时发现问题的修复补充及优化。其实方向和指标是十分明确的,咱们心愿它可能在反对更多功能个性的状况下 blazing fast and low cost。

随着业务的倒退,一劳永逸的数据量及每天高频次实时剖析需要对整个零碎的设计和迭代都带来了不小的考验。与此同时团队外部也总结了许多时序数据查询处理的教训,基于理论场景中呈现的问题进行针对性优化,让它成为业务和用户真正感觉好用的产品,这也使得平台在业务外部被宽泛应用。将来咱们还会持续优化引擎速度,进步跨节点数据传输效率,剖析反馈学习用户预聚合维度减速查问,尝试新的时序存储形式和模型,降低成本且晋升查问效率。

参考资料

[1] TimescaleDB: PostgreSQL for time‑series https://www.timescale.com/

[2] InfluxDB: Open Source Time Series, Analytics Database. https://www.influxdata.com/

[3] OpenTSDB: A Distributed, Scalable Monitoring System http://opentsdb.net/

[4] KariosDB: fast distributed scalable time series database https://kairosdb.github.io/

[5] PromQL: Prometheus Query Language Querying basics | Prometheus

[6] Flux: InfluxData’s functional data scripting language Started with Flux

[7] Promethues: Storage-Integrations

[8] T. Pelkonen et al., “Gorilla: A fast scalable in-memory time series database”, Proc. VLDB Endowment, vol. 8, no. 12, pp. 1816-1827, 2015.

本文作者

Zhuo,后端开发工程师。次要从事实时多维时序数据存储及剖析相干工作,来自 SeaMoney Data 团队。

本文首发于微信公众号“Shopee 技术团队”。

退出移动版