数据仓库的性能问题及解决之道
随着数据量不断增长和业务复杂度逐渐攀升,数据处理效率面临巨大挑战。最典型的表现是面向分析型场景的数据仓库性能问题越来越突出,压力大、性能低,查询时间长甚至查不出来,跑批跑不完造成生产事故等问题时有发生。当数据仓库出现性能问题时便不能很好服务业务了。
传统数据仓库的性能解决方案
集群,也就是采用分布式技术,依赖扩展硬件来提升性能,是最常见的手段。将一个大的任务拆分到各个集群节点上同时计算自然可以获得比单机更好的性能,即使不进行分布式计算而简单分担并发任务也可以减轻单节点的计算压力。集群解决性能问题的思路简单粗暴,只要数据仓库支持集群并且任务能够拆分就可以通过堆硬件来解决性能问题,虽然可能做不到线性提升但基本都会有效果。
集群的缺点在于高成本。现在的大数据时代大家言必提集群,经常不管单机性能是不是得到充分发挥,反正只要能集群往里扩容就行了,集群似乎是很多人眼中的“万能药”。但我们要注意,集群需要更多硬件支撑成本自然高,集群运维也需要投入更多资源。另外有些复杂多步骤计算任务由于无法拆分压根就没法用集群,比如大多数的多步骤大数据量跑批任务还只能用单机(单体数据库存储过程)完成。集群虽好,但并不万能,即使财大气粗架设集群也并不能解决所有性能问题。
对于一些耗时较长的查询还可以采用预计算,采用空间换时间的办法将要用到的数据事先加工好,这样就可以将计算复杂度降到 O(1) 大幅提升效率。预计算同样可以解决很多性能问题,通过预汇总将要用到的数据事先加工好,用空间换时间,对多维分析场景尤其有效。
预计算的问题在于灵活性太差。我们仍然以多维分析场景为例,虽然理论上可以将所有维度组合都预计算好(这样就可以满足所有查询需求),但真这样做会发现不现实,这需要天量的存储空间才能满足。所以只能通过梳理业务进行部分预计算,这就大大限制了查询范围降低了灵活性。
其实即使能全部预计算,仍然解决不了诸如非常规聚合(如算中位数、方差)、组合聚合(如算月平均销售额)、条件测度(如算交易金额大于 100 元以上的订单销售额合计)、时间段汇总(自由选择时间段内的汇总)等情况。实际业务中的查询需求五花八门,灵活性极强,预汇总只能解决其中的一部分甚至仅仅一小部分问题,要更大范围、更高效率地满足多样的在线查询需求还需要更有效的计算手段。
更有效的手段是优化引擎,让数据仓库在同样的硬件资源下跑出更好的性能。这是许多厂商的工作重点,有大量工程性手段已为业界熟知,比如提供列存、向量化执行、编码压缩、内存利用等(集群也可以算是一种工程手段),通过这些技术在一定数据规模内可以提升几倍的计算性能,在某些场景下足够用。但工程些手段并不能改变计算的复杂度,在数据量大和复杂度特别大的场景时,性能提升仍然常常不能满足需求。
优化引擎更有效的手段是算法层面上的(复杂度层面的提升),一个优秀的数据仓库优化引擎可以猜出一个查询语句的真正目标,从而采用更高效的算法执行(而不以字面表达的逻辑去执行),算法层面的改善经常可以获得更高的性能提升。当前大部分数据仓库仍以 SQL 作为主要查询语言,基于 SQL 的优化已经做得足够优秀。但由于 SQL 描述能力的局限性,复杂查询会采取非常迂回的方法,一旦 SQL 复杂度上来优化引擎就很难发挥作用了(猜不出目标只能按照字面表达去执行,性能就不会提升),即优化引擎仅对简单查询有效。
举个例子,TopN 运算时:
SELECT TOP 10 x FROM T ORDER BY x DESC
大部分数据仓库都会优化,不会真排序。但是改成组内 TopN 以后:
select * from
(select y,*,row_number() over (partition by y order by x desc) rn from T)
where rn<=10
复杂度虽然没有提升很多,但优化引擎会犯晕,猜不出这句 SQL 的真正目的,只能按照 SQL 表达的意思进行大排序而性能低下。所以,有些场景我们会事先把数据加工成宽表,这样就可以简化查询从而发挥优化引擎的作用。虽然会付出很多代价,但为了有效利用优化引擎有时也只能不得已而为之。
当前几乎所有数据仓库技术都在竞争 SQL 能力,提供更完善的 SQL 支持、提供更强的优化能力、支持更大规模的集群等等,虽然这可以最大限度地“讨好”受众很广的 SQL 使用者,但在面向复杂计算场景时使用前面的方法往往不够有效,性能问题仍然存在。再在 SQL 的基础上努力(工程上的优化)获得的效用也并不高,并不能从根本上解决问题。而这类问题在实际业务中并不少见,下面是几个例子。
复杂有序计算。比如用户行为转换漏斗分析:用户页面浏览商品、搜索、加购物车、下单、付款等多个有序事件,现在要统计每个步骤的用户流失率,就需要遵循多个事件在指定时间窗口内完成、按指定次序发生才有效的原则来实现。这使用 SQL 就很难实现了,需要借助多个子查询(与步骤数量一致)和反复关联完成,有些数据仓库甚至不能执行这种复杂语句,即使能执行,性能也很低,更难以优化。
多步骤大数据量跑批。复杂的跑批任务使用 SQL 效果也不好,经常需要在存储过程中借助游标逐步读取数据处理,但游标性能很低又无法并行,最后不仅资源消耗大性能也低。同时,几十步运算在存储过程中需要几千行代码,过程中会伴随中间结果反复落地,性能很差,月末年终数据量大任务多的时候就会出现在规定时间内跑不完的情况。
大数据上多指标计算。很多行业都有指标计算的需要,比如银行的贷款业务中就包括多级分类维度、多种担保类型,再加上客户种类、放款方式、币种、分支机构、日期、客户年龄段、学历等指标会衍生出极其庞大的指标数量,汇总这些指标时要基于大量的明细数据完成,计算时会涉及大表关联、条件过滤、分组汇总、去重计数等多种混合运算,灵活、量大、计算复杂,同时伴随高并发导致使用 SQL 来做很吃力,预计算不灵活,实时算又太慢。
这些问题用 SQL 很难解决,于是扩展 SQL 能力成为继集群、预计算和优化引擎外的第四方案。现在很多数据仓库支持用户自定义函数(UDF)来扩展计算能力,用户可以根据实际需求编写 UDF 以满足自身的需要。但 UDF 的开发难度较高,这对使用者的能力有很高要求。更重要的是 UDF 仍然无法解决数据仓库的计算性能问题,因为仍然受限于数据库的存储,无法根据计算特点设计更高效的数据存储(组织)形式,很多高性能算法就无法实施,自然无法获得高性能。
因此,要解决这些问题就需要采用非 SQL 的方案手段,在数据库外由程序员控制执行逻辑,以便更好地采用低复杂度算法和充分利用工程手段。
于是我们看到诸如 Spark 等大数据计算引擎应运而生。Spark 提供的是一个分布式计算框架,本意还是希望通过大规模集群来满足算力的需要,由于基于全内存的设计对于超出内存的计算不够友好,而且 RDD 采用的 immutable 机制,在每个计算步骤后都会复制出新的 RDD,造成内存和 CPU 的大量占用和浪费,性能很低,工程手段利用的并不够好。此外,Spark 的计算类库也不够丰富,缺少足够的高性能算法,很难实现“低复杂度算法”的目标。加之 Scala 的使用难度很大,导致面对前面提到的那些复杂计算问题编码难度极高,既不好写性能也不高,这可能也是 Spark 又要拥抱 SQL 的原因之一。
传统数据仓库不行,外部编程(Spark)又难又慢,那还有什么选择?
从前面的内容我们不难得出这样的结论,要解决数据仓库的性能问题确实需要独立于 SQL 的计算体系(像 Spark),但这个计算体系要具备既简单又快的特点。描述复杂计算逻辑不能像 Spark 那么复杂,甚至要比 SQL 更简单;在计算性能上不能仅靠集群能力,还要提供丰富的高性能算法和工程能力从而能够充分利用硬件资源将单机性能发挥到极致。既有快速描述低复杂度算法的能力,又具备足够多的工程手段。同时,如果在部署运维方面还很方便就更理想了。
esProc SPL 的解决之道
esProc SPL 是一个专门处理(半)结构化数据的计算引擎,与当前数据仓库的能力一致。但与传统 SQL 型数据仓库不同,esProc 没有继续采用关系代数而是设计了全新的计算体系,在此基础上提供了 SPL(Structured Process Language)语法。SPL 相对 SQL 提供了更多的数据类型和运算、更丰富的计算类库,描述能力更强,在过程计算的加持下可以按照自然思维编写算法,不必绕,也更低代码,可以很好应对前面场景中的多步骤复杂计算,相对其他硬编码以及 SQL 的实现方式更简单。
在性能方面,esProc SPL 提供了很多“更低复杂度”的高性能算法来保证计算性能。我们知道,软件改变不了硬件的性能,想要在同等硬件条件下获得更高的计算性能只能设计更低复杂度的算法让计算机少执行一些基本运算,这样自然就变快了。但是算法不仅要想出来,还要能实现,写得越简单越好,所以说写得简单和跑得快其实是一回事。
SPL 提供的部分高性能算法,其中很多都是 SPL 的独创发明
当然,高性能算法还离不开良好的数据组织,即数据存储。像有序归并、单边分堆都要求数据有序才能实施。但是数据库的存储相对封闭,外界无法干预,无法根据计算特征设计存储。基于这样的原因,SPL 提供了自有的二进制文件存储,将数据存储在库外的文件系统中,以便充分利用列存、有序、压缩、并行分段等数据存储优势,实现根据计算特性来灵活组织数据,充分发挥高性能算法效力。
除了这些高性能算法,esProc 还提供了众多工程手段来提升计算性能,列存、压缩编码、大内存以及向量式计算等等。如前所述,这些工程手段虽然无法改变计算的复杂度,但使用后经常能获得数倍的性能提升,再叠加 SPL 内置的众多低复杂度算法,性能提升一两个数量级是常态。
前面说过,基于非 SQL 体系要获得高性能需要由程序员控制执行逻辑,采用低复杂度算法,并且充分利用工程手段。SPL 理论体系的不同带来了描述能力强的效果,编码简单不必绕;丰富的高性能算法库及相应存储机制可以直接使用,实现采用低复杂度算法的同时充分工程优化手段的目标,达到既简单又快的效果。
像 TopN 在 SPL 中被看成普通的聚合运算,无论对全集和分组都是一样的,都不需要大排序,这样就实现了“采用更低复杂度算法”的目标,从而获得了高性能。
我们再来看一下前面提到的电商漏斗例子实现,来感受 SQL 和 SPL 的不同。
SQL 实现:
with e1 as (
select uid,1 as step1,min(etime) as t1
from event
where etime>= to_date('2021-01-10') and etime<to_date('2021-01-25')
and eventtype='eventtype1' and …
group by 1),
e2 as (
select uid,1 as step2,min(e1.t1) as t1,min(e2.etime) as t2
from event as e2
inner join e1 on e2.uid = e1.uid
where e2.etime>= to_date('2021-01-10') and e2.etime<to_date('2021-01-25')
and e2.etime > t1 and e2.etime < t1 + 7
and eventtype='eventtype2' and …
group by 1),
e3 as (
select uid,1 as step3,min(e2.t1) as t1,min(e3.etime) as t3
from event as e3
inner join e2 on e3.uid = e2.uid
where e3.etime>= to_date('2021-01-10') and e3.etime<to_date('2021-01-25')
and e3.etime > t2 and e3.etime < t1 + 7
and eventtype='eventtype3' and …
group by 1)
select
sum(step1) as step1,
sum(step2) as step2,
sum(step3) as step3
from
e1
left join e2 on e1.uid = e2.uid
left join e3 on e2.uid = e3.uid
SPL 实现:
A |
|
1 |
=["etype1","etype2","etype3"] |
2 |
=file("event.ctx").open() |
3 |
=A2.cursor(id,etime,etype;etime>=date("2021-01-10") && etime<date("2021-01-25") && A1.contain(etype) && …) |
4 |
=A3.group(uid).(~.sort(etime)) |
5 |
=A4.new(~.select@1(etype==A1(1)):first,~:all).select(first) |
6 |
=A5.(A1.(t=if(#==1,t1=first.etime,if(t,all.select@1(etype==A1.~ && etime>t && etime<t1+7).etime, null)))) |
7 |
=A6.groups(;count(~(1)):STEP1,count(~(2)):STEP2,count(~(3)):STEP3) |
SPL 在有序计算的支持下实现代码更加简短,可以根据自然思维分步(过程化支持)编写代码,而且这段代码可以处理任意步骤的漏斗分析(这里是 3 步,更多步只需要改变参数即可),相对 SQL 每增加一步漏斗就要增加一个子查询显然更有优势,这就是 SPL 的简单带来的效果。
性能上,这个例子其实是实际案例的简化版(原 SQL 有近 200 行),用户使用 Snowflake 的 Medium 服务器(相当于 4*8=32 核)3 分钟没有跑出来;而 esProc SPL 代码在一个 12 核 1.7G 的低端服务器上仅用不到10 秒就跑出来了,这是 SPL 高性能算法和相应工程手段造就的高性能。
有了这些机制以后,esProc SPL 就可以充分利用硬件资源,将单机性能发挥到极致,不仅原来很多单机性能问题可以得到有效解决,甚至很多原来使用集群的计算现在也可以用单机搞定(可能更快),达到单机顶级群的效果。当然,单机有极限,SPL 也提供了分布式集群功能,当单机性能无论如何也无法满足需要时可以通过集群横向扩展算力。这也是 SPL 的高性能计算理念:先把单机性能提升到极致,不够用再集群。
当然,任何技术都有不足,SPL 也不例外。SQL 经过几十年的积累发展,很多数据库都拥有很强的优化引擎。对于适合用 SQL 完成的简单场景运算,可以将普通程序员写出来的慢语句优化出较好的性能,从这个意义上讲,对程序员的要求相对较低。某些场景(比如多维分析)已经被优化多年,某些 SQL 引擎也可以跑出相当好的极致性能。相比之下,SPL 没有做多少自动优化的功能,要跑出高性能,几乎全靠程序员写出低复杂度的代码。程序员需要经过一定的训练来熟悉 SPL 的理念和库函数,会多一个上手的门槛,不过获得数量级的性能提升和成倍的成本下降,这些付出通常也还是值得的。