Arrow 是数据领域非常重要的一个项目,也是很多新兴数据库和大数据项目的基石。事实上,很难用一句话描述它的定位,毕竟它从最开始的内存列式数据格式逐渐演变成一个包含 DataFrame、RPC、SQL 执行引擎的项目。
今天太可研究所(techinstitute)就来盘一盘 Arrow,希望大家看完后能对它有着更清晰的理解。
## Vol.1
Arrow 诞生的时间其实很短,2016 年才从 Apache 基金会毕业,最初的定位是想解决跨系统的数据交换问题,如官网图所示:
![](https://files.mdnice.com/user/54703/c720235f-f3dd-46bb-b64d-7c0f9f70f378.png)
在 2010 年代,围绕着 Hadoop 诞生了一大批计算引擎、NoSQL 数据库,但没有一个产品能解决所有的问题,所以运维一套大数据系统,需要使用若干产品。而这些产品之间其实没有很高效的格式做数据交换,Arrow 是从尝试解决这一问题中诞生的。
要想让大数据算得快,使用列存是最佳选择,Parquet、Avro 是列式的存储格式,而 Arrow 则是面向计算的列式计算格式,Arrow 的格式是面向计算而生,所以其重点要解决的是如何高效的使用内存以及怎么算得快,与列式存储的技术特点很不相同。
Apache Arrow 的列式格式协议包括以下细节:
- 数据邻近性:面向范围查询而优化,数据在内存中的布局是为了实现顺序访问时的高效性能。
- O(1) 随机访问:面向点查的优化,内存布局设计使得可以在常数时间内进行随机访问。
- SIMD :内存布局设计使得数据可以利用 SIMD 和向量化指令进行高效处理。
- Zero copy:减少内存拷贝,提升性能。
- 缓冲区对齐和填充:Apache Arrow 建议实现时在分配内存时进行对齐和填充,以提高内存访问的效率。
## Vol.2
由于 Hadoop 生态都是用 Java 构建的,所以 Arrow 最早使用的语言也是 Java。后来随着 AI 的 Python、科学计算的 R 等语言越来越重要,Arrow 也逐渐有了多语言的实现,当然其他语言的实现大部分都是 C++ 实现的套壳。
一个存储格式有多语言的实现其实不是什么新鲜事,比如 Parquet、JSON、CSV 各个语言都有实现,为什么 Arrow 的跨语言特点会值得一提呢?
原因在于 Arrow 是面向内存的格式,跨语言调用不需要序列化、反序列化可以大幅提升性能同时减少内存开销。举个例子来说 Spark 是支持 Python、Scala、Java、R 多种语言的,Scala 和 Java 都是跑在 JVM 上的,两者之间传输数据没什么问题。如果 Scala 要给 Python 传输数据,必须要 Scala 先把数据按照一定的格式序列化,发送给 Python,Python 再把数据反序列化出来才能使用,这中间就会有序列化、反序列的内存和 cpu 开销。而如果使用 Arrow 则没有这个问题,因为Arrow的格式就是一段内存,这段内存只要从 Scala 传递到 Python,就可以通过 Arrow 的 sdk 使用,无需序列化和反序列化。
这样的优点对于使用多种语言的项目来说非常有吸引力。就拿我参与的数据库项目来说,同时用了 C++ 和 Go,一个查询请求需要在两者之间传输大量数据,如果不用 Arrow 会有大量序列化、反序列化操作,而开大量临时内存也会消耗宝贵的 CPU 资源,使用 Arrow 能显著减少此类问题。但是由于要对现有代码做大量改造,始终没有改造完成,这就是另一个故事了。
## Vol.3
有了以上的特点之后,不难发现另一个重要的应用场景,就是跨服务的 RPC 调用。在分布式系统里 RPC 调用往往是性能瓶颈,而在大数据的分布式计算引擎里需要通过 RPC 传输大量数据,瓶颈问题尤为突出,Arrow 的 Arrow Flight 子项目,就是一个基于 Arrow 格式的 RPC 协议,优点无需赘述,缺点就是对现有系统改动太大。
## Vol.4
曾经 Arrow 有一个 Plasma 子项目,是 Ray 团队贡献的, 希望能提供一个跨进程共享内存的方案。文档中举的例子虽然没有明说是哪个项目的问题,但话里话外都在说 Spark,比如多个 Python 的 worker 进程要做计算,并对结果进行 reduce,这其中会有很多变量的内存要每个进程拷贝一份,这就会有额外的浪费,如果跨进程能够共享一份内存就能提高资源利用率。就拿我就近碰到的例子来说,有一个 Spark 任务需要加载 AI 的 model 进内存,这个 model 有 2G,load 进内存之后又 6G,如果只加载一份的话还好,但是每个 Python 进程都要加载一份,就导致大量的内存被浪费。
Plasma 的初衷是好的,但是很可惜,除了 Ray 自己之外其他项目对 Plasma 兴趣寥寥,最近的 Arrow 版本里已经把 Plasma 删掉了。深入探究一下 Plasma 的失败原因,在于适用的场景太少,而且语言只能用 C++ 和 Python,同时 Plasma 的主要维护者 Ray 也在项目内部对 Plasma 进行了分叉,让社区版本更加缺乏生命力。
## Vol.5
说到基于 Arrow 的计算引擎,至少有三个,一个是 Arrow 内部的 Acero,一个是基于 Arrow rust 语言的 DataFusion,还有一个相对独立的 Polars。这仨项目都提供了 DataFrame 接口,DataFusion 还提供了 SQL 接口。
我曾经试着用过 Acero,不得不说 C++ 的接口实在是太难用了,用的人也不多,估计很快会和 Plasma 一样被废弃掉。DataFusion 社区比较活跃,已经有HuggingFace Dataset、GCP 在使用了,也有尝试将 DataFusion 作为 Spark 的执行引擎提升效率的 Blaze 项目,Blaze 项目目前看来很不活跃,感觉又是一个大厂的 KPI 项目,社区没有真正建立起来。
使用 DataFusion 和 Polars 作为计算引擎,从开发语言上看肯定会比 Spark 自身的计算引擎性能要好,但影响一个计算引擎成败与否不仅仅是开发语言和性能,Spark 生态完善程度和功能齐全程度,短时间内其他计算引擎还远远赶不上。但是 Spark 由于其诞生的时代背景和开发语言的限制,上限已经几乎锁死了,想要进一步提升很难,反倒是用 Arrow + Rust 的新兴项目的未来更有潜力。
## Vol.6
Arrow 诞生的时间不长,但社区发展的很蓬勃,前几年步子迈得特别大各种子项目层出不穷,上文提到的 Plasma、Acero、Flight、Gandiva,甚至 parquet 的 C++ reader 也维护在了 Arrow 项目里,导致整个 Arrow 的代码仓库无比冗杂,有各种语言的 binding,还有各种子项目,有点精装小伙还没成年就已经步履蹒跚的感觉。
好在后来 Arrow 社区开始逐步清退各种无意义的子项目,或者把相对独立的子项目分拆 repo 进行维护,比如 arrow-rs、arrow-datafusion,主 repo 更加关专注。
总体而言,Arrow 从内存列式格式入手,逐步发展出包含 RPC、Dataset、计算引擎的一个完整的生态,同时社区也还在在蓬勃的发展。同时上一代的计算引擎也逐渐走到了瓶颈期,未来大有可为,距离使用 Rust 开发大数据引擎的日子不远了。