Java/Python 替代存储过程跑批?

使用存储过程跑批一直是常态,存储过程将 SQL 过程化可以满足相对复杂的跑批场景,同时在数据库内运行(数据不出库)性能相对较好。不过,存储过程的缺点也很多。编辑调试困难,缺乏有效的开发环境;移植性不好,很难适应数据源变化的需要;扩展性也比较差,性能不足时只能纵向升级硬件,无法横向扩容,容量上限低;大量依赖存储过程还会导致数据库压力过大,由于扩容难,又无法更换数据源(库)经常会导致跑批过程很慢,随着业务的不断增加压力愈发明显。

基于存储过程的这些缺点(还有些 AP 型数据库对存储过程支持不好甚至不支持),现在很多场景也开始使用 Java/Python 完成跑批任务。使用高级语言更加灵活,一些复杂的多步运算(如有序计算)用 SQL 实现难以实现,在 Java/Python 中反倒更简单;而且高级语言具备更强的移植性,跑批计算逻辑都在数据库外,极易适应数据源变化;通过硬编码理论上可以编写并行及分布式程序,可以适应任意容量的跑批需要。

不过,使用 Java/Python 跑批也有一些显著缺点。

Java/Python 跑批的缺点

大数据计算能力差

Java 和 Python 都提供了一些计算类库,借助 Stream 和 Pandas 可以较方便地完成一些基本计算。但二者几乎都只提供了内存计算方案,没有针对的游标计算机制,大数据计算能力差。

Java 实现超出内存的计算需要采用迂回的方式,比如排序运算才需要先将大规模数据分割成多个小文件,然后对每个小文件进行排序,最后将所有小文件合并成一个有序的大文件。这样可以避免将所有数据加载到内存中进行排序,从而实现超出内存的排序。过程描述虽然简单,但实现起来并不容易。

还有分组运算。数据库通常会提供直接可用的哈希分组方案,因此在使用数据库进行哈希分组计算时,可以直接使用数据库提供的功能,性能通常不会有问题。Java 虽然提供了 HashMap,但仅针对内存,当数据量超过内存限制时,外存的 Hash 分组实现会复杂不少了,这对很多应用程序员难度太高,结果常常会借助外部排序等简单方式来完成分组计算,难度降低但性能也会受到影响。

Python 在内存计算时也不错,基础函数(groupby,merge 等)性能都挺好,尤其是纯数字的矩阵运算,更是它所擅长的。但 Python 和 Java 存在同样的问题,面对超出内存的计算通常需要分段读取数据,并自行编写代码完成计算任务。然而,像哈希算法这样的复杂算法实现难度较高,很多程序员并不熟悉,只能采用简单的算法,如上文提到的借助外部排序来完成计算任务。缺乏外存计算类型的支持,导致实现难度高性能差。

结果,很多时候还需要借助数据库完成计算,用 Java/Python 指挥数据库执行 SQL,又回到了以数据库为主的道路,压力也集中在数据库端,并不能为数据库减轻多少压力,原来存储过程的不利影响几乎都仍然存在。

并行困难

跑批业务涉及的数据规模很大,并行计算可以有效利用多 CPU 核的能力加速计算过程。Java 本身提供了多线程机制,可以将任务分解成多个子任务交给多个线程并发执行。在 Java7 以后还提供了 Fork/Join 框架,递归地将一个大任务拆分为多个小任务,然后将这些小任务分配给多个线程并发执行。Java8 引入的 Stream API 进一步提供了并行流(parallel stream)的支持,可用于将一个大的数据集拆分为多个小的数据集,并行处理这些小数据集,最终将各个小数据集的结果合并得到最终结果,相当于 Fork/Join 框架的延伸和简化。

不过,虽然提供但使用起来并不容易。多线程编程不仅要考虑线程间任务调度、线程通信以及算法设计等多种情况,还有如何进行有效数据分段为多线程均衡地提供数据,事先将数据拆分到多个文件是一种方式,但效率很低且灵活性很差,如果直接使用数据库等数据源又无法进行游标分段,最后的结果仍然是性能不高。

Python 相对 Java 就更差一些,相当于就没有并行的能力。Python 的并行是伪并行,对于 CPU 来说就是串行,甚至比串行还慢,难以充分利用现代 CPU 多核的优势。在 Cpython 解释器 (Python 语言的主流解释器) 中,有一个全局解释锁(Global Interpreter Lock),执行 Python 代码时,先要得到这个锁,意味着即使是多核 CPU 在同一时段也只可能有一个线程在执行代码,多线程只能交替执行。而多线程涉及到上下文切换、锁机制处理等复杂事务,结果不快反慢。

Python 无法在进程内使用简单的多线程并行机制,很多程序员只能采用复杂的多进程并行,进程本身的开销和管理复杂得多,并行程度无法和多线程相提并论,加上进程间的通信也很复杂,有时只好不直接通信,用文件系统来传递汇总结果,这又导致性能大幅下降。

没有通行的高性能私有存储机制

大数据的运算性能很大程度上还和数据 IO 相关,如果数据的 IO 成本太高,运算再快也没用。高效的 IO 经常依赖于专门优化的存储方案,但遗憾的是,Java 和 Python 都没有应用较广泛的高效存储方案,一般会使用文本文件或数据库存储数据,数据库的接口效率性能很差,文本虽然好一点,但又会在数据类型解析上消耗过多时间导致性能仍然不高。

那自己开发一种存储,把数据写成二进制的是不是就可以了?也没那么简单。虽然这解决了数据类型解析的问题,但如果不做好编码,其占用的空间有可能比文本更大,减少了数据类型解析时间,又多了硬盘读写时间,性能提升可能很有限。一个高效存储要综合考虑压缩编码、列存甚至索引等一切提升效率的手段,实现起来并不容易。

如果数据源本身就是文本或数据库,这没办法改变,忍受低速 IO 也就罢了,但很多复杂运算(比如大数据排序)过程中需要中间结果落地,理论上这些读写性能应该是可控的,却因为缺少高效存储方案,也只能选择低效的文本或数据库,导致整体性能低下。还有些运算需要用到大量历史数据,如果都从文本或数据库读取,往往会出现 IO 时间远远高于计算时间的尴尬局面。

SPL 跑批

esProc SPL(Structured Process Language)是专门面向结构化数据计算的开源程序语言,可以替代存储过程完成跑批,并可以有效解决 Java/Python 跑批过程中面临的各类问题。

SPL 提供了简洁易用的开发环境,编辑调试功能齐全,编码方便程度更胜于 Java 和 Python;SPL 在数据库外运行,数据库更换不需要更改 SPL 计算脚本,解决存储过程的移植性问题;SPL 针对大数据计算提供了游标计算方式,可以轻松完成超出内存容量的数据处理,中间数据不落地性能更高;同时在游标上还可以实施并行计算,进一步提升计算性能;SPL 还专门为高性能计算设计了高效的二进制文件存储,配合 SPL 内置的高性能算法可以充分保障计算性能。

游标机制

SPL 为了解决大数据计算问题,除了内存计算外,特别提供了游标机制来实施外存计算。并且,外存游标的计算与内存基本一致,比如:


A

1

D:\data\orders.txt

2

=file(A1).cursor@t()

3

=A2.groups(area;sum(amount):amount)

A2 创建了一个文件(文本)游标,下面的分组汇总时就不会一次性把数据全部加载到内存,而是分批计算从而完成大数据分组汇总的目标。而且,除了 f.cursor()建立外存游标与内存计算 f.import() 有区别外,其他步骤完全一样,可以有效降低使用门槛。

不过,外存游标计算与内存计算还有一些差异,内存支持随机小量访问,而外存(硬盘)只能顺序批量访问,游标也只能顺序遍历数据,遍历一次游标就失效了,如果还想进行其他计算需要再创建游标,相当于要遍历多次。而在跑批业务中会针对同一份数据进行多步运算,如果每步运算都要重新遍历成本就会高很多。SPL 为此提供了延迟游标,只将计算标记绑定在游标上并不执行真正计算,直到后面有取数动作(结果集函数)才会计算,这样遍历一次数据就可以完成多步计算。比如先过滤再分组汇总:


A

1

D:\data\orders.txt

2

=file(A1).cursor@t()

3

=A2.select(amount>50)

4

=A3.groups(area;sum(amount):amount)

这里的 A3 就是延迟游标,执行到 A3 时只记录计算标记,直到 A4 取数时才会先过滤再分组汇总。

事情还没完。如果针对大数据的计算不是多步,而是多个同类型怎么办?比如,既要按照 area 分组汇总 amount,又要按照 product 分组汇总 amount,即同时会有多个取数(结果集函数)动作。这样的话延迟游标也不管用了,那么只能再创建游标遍历一次吗?

不必。 SPL 提供了遍历复用机制,在游标之上再创建同步管道就能实现一次遍历完成多种计算的效果。比如:


A

B

1

=file(“orders.txt”).cursor@t(product,area,amount)


2

cursor A1

=A2.groups(area;max(amount))

3

cursor

=A3.groups(product;sum(amount))

4

cursor

=A4.select(amount>=50).total(count(1))

游标创建后,用 cursor 语句为其创建管道,然后在其上设置运算。可以创建多个管道,后面语句不再写游标参数则表示会复用同一个游标。

有了游标、延迟游标和遍历复用(管道)这些机制后,SPL 就可以轻松应对大数据计算了。SPL 目前提供了多种外存计算函数,可以满足几乎所有大数据计算需求。拥有这点就已经远胜于 Java 和 Python 了。

并行计算

大数据计算性能很关键,我们知道并行计算可以有效提升计算效率。SPL 除了提供延迟游标和遍历复用可以保证一定性能外,还提供了多线程并行来加速计算,使用起来也很简单。


A

B

1

=file(“orders.txt”)


2

fork to(4)

=A1.cursor@t(area,amount;A2:4)

3


return B2.groups(area;sum(amount):amount)

4

=A2.conj().groups(area;sum(amount))


fork 语句将启动多个线程并行执行自己的代码块,线程的数量由 fork 后参数的数量决定,fork 同时会将这些参数依次分配给每个线程。当所有线程执行完毕之后,每个线程的计算结果将被收集起来拼到一起,供进一步运算使用。

相对 Java 和 Python,SPL 使用 fork 来启动多线程实施并行计算已经很方便了,但代码仍然略显繁琐,特别是对于很常见的单数据表的统计,而且还要注意再次汇总线程返回结果时可能要改变函数(从 count 变 sum)。SPL 提供了更简单的多路游标语法,可以直接生成并行游标。


A

1

=file(“orders.txt”)

2

=A1.cursor@tm(area,amount;4)

3

=A2.groups(area;sum(amount):amount)

使用 @m 选项即可创建多路并行游标,然后和之前的单路游标使用方法一样,SPL 会自动处理并行以及将结果再汇总。

多路游标上也可以实现遍历复用:


A

B

1

=file("orders.txt").cursor@tm(area,amount;4)


2

cursor A1

=A2.groups(area;sum(amount):amount)

3

cursor

=A3.groups(product;sum(amount):amount)

有了简单易用的并行计算就可以充分发挥多 CPU 的能力来充分保障计算性能,缩短跑批时间。

高性能存储

前面我们说了,基于数据库计算会很慢(IO 成本高),文本虽然好一些,通过上面的游标和并行技术也可以提升一定计算效率,但文本的解析效率仍然很低,这就需要自有的高性能存储来进一步保障计算性能。

为此,SPL 设计了自有格式的二进制文件存储,集编码、压缩、列存、索引、分段等多种机制于一身的高效文件格式。

目前 SPL 提供了两种高性能文件类型:集文件和组表。集文件采用了压缩技术(占用空间更小读取更快),存储了数据类型(无需解析数据类型读取更快),支持可追加数据的倍增分段机制,利用分段策略很容易实现并行计算,保证计算性能。组表支持列式存储,在参与计算的列数(字段)较少时会有巨大优势。组表上还实现了索引,同时支持倍增分段,这样不仅能享受到列存的优势,也更容易并行提升计算性能。

在跑批业务中,经常会涉及大量的历史冷数据,有了高性能存储,我们就可以把这些数据迁移到文件中以获得更高计算性能。即使有时数据无法从数据库迁出,使用 SPL 高性能存储用来写缓存或中间计算结果也很有意义。而且冷数据通常也可以复制一份到文件存储中用于高效计算。

有了高性能存储的保障,计算效率可以进一步提升。不仅如此,SPL 基于自有存储还可以实现一些其他提升性能的机制,如游标前过滤技术。

通常外存过滤需要先读出一条记录再判断条件是否满足,如果不满足就丢弃。这种简单的办法会产生一些浪费动作,因为要读出整条记录才能判断,如果能够先判断后读入就会更高效。SPL 实现了这种游标前过滤机制,创建游标时可以附加一个过滤条件,SPL 会先只读出用于计算条件的字段值,如果条件不成立就放弃到下一步,条件成立才再继续读出其它需要的字段并创建这条记录。


A

1

=file("persons.ctx").open()

2

=A1.cursor(sex,age;age>=18).groups(sex;avg(age))

和文本文件类似,SPL 存储(集文件和组表)也都支持分段并行读取计算,使用也很简单:


A

1

=file(“orders.ctx”).open().cursor@m(;;4)

2

=A1.groups(area;sum(amount):amount)

SPL 存储还使用了特别设计的倍增分段技术进行数据分段,更进一步 SPL 组表还支持同步分段,当关联的两个表都很大时通过同步分段就可以使用并行的方式进行连接运算,提升计算性能。

基于高性能存储 SPL 还提供了很多高性能技术,如索引、单边分堆、有序归并、附表等多种机制和算法。有了高性能存储的配合,SPL 的大数据计算性能往往会比一般数据库快数倍到数十倍,比 Java 和 Python 就更有优势了。

多说一点,SPL 独立于数据库运行,可以完全享受 Java/Python 带来的架构优势。SPL 还具备良好的集成性和开放性,集成性使得 SPL 可以嵌入应用内运行,开放性使得 SPL 可以对接多样性数据源并进行混合计算。现代跑批任务有时会涉及多种数据源,SPL 良好的开放性可以直接基于这些数据源进行混算而不必事先统一存储(如入库),更加方便。

使用 SPL 跑批已经在很多企业实际应用,效果非常明显。以下是部分实际案例:

开源 SPL 提速银行贷款协议跑批 10+ 倍
开源 SPL 提速银行贷款跑批任务 150+ 倍
开源 SPL 优化保险公司跑批从 2 小时到 17 分钟
SPL 提速天体聚类任务 2000 倍