Apache Arrow:驱动列式分析性能和连接性的提升[译]

本文永久链接 – https://tonybai.com/2023/07/01/arrow-columnar-analytics

本文翻译自Voltron Data公司CTO Wes McKinney的文章《Apache Arrow: Driving Columnar Analytics Performance and Connectivity》。这篇文章回顾了现代大数据分析遇到的问题、Arrow项目的起源、生态发展以及对未来的展望。

以下是正文部分。


引言

自MapReduce以来,大数据已经走了很长一段路。Jeffrey Dean和Sanjay Ghemawat在2004年发表于Google的论文催生了Apache Hadoop开源项目,以及一系列其他新项目,这些项目是因大量开发人员有捕获,存储和处理非常大的数据集的需求而创建的。


图:大数据演进的3V(Volume、Variety和Velocity)

虽然像Hadoop这样的第一个MapReduce框架能够处理大型数据集,但它们是为了大规模弹性(通过将每个处理步骤的结果写回分布式存储)而设计的,而并未过多考虑性能。Apache Spark于2010年首次发布,因其基于容错分布式内存处理的新架构而脱颖而出。Spark的核心是用Scala实现的,Scala是Java虚拟机(JVM)的编程语言。Spark为其他编程语言提供了binding实现,例如 C# .NET、Java、Python (PySpark) 和 R(SparkR 和 sparklyr),这有助于Spark在众多编程语言开发者社区的普及使用。


图:数据处理生态系统演进

在过去的十年中,像Python和R这样的解释式编程语言已经不再局限于其在科学计算和学术统计中的利基市场,一跃发展成为现代数据科学,商业分析和AI的主流工具。这些编程语言完全主导了“笔记本电脑规模”的数据处理工作。像Hadoop和Spark这样的大规模数据处理框架为Python等解释型语言提供了编程接口,但与JVM上运行的“本机”接口相比,使用这些语言绑定的性能和资源利用率通常都很差。

解释型语言在使用主流大数据系统时所付出的性能损失主要源于数据互操作性问题。为了将数据从Java应用程序的核心运行时传递给用户的自定义Python函数(“用户定义函数”或“UDF”),必须将数据转换为可以Python所接受的格式,然后再转换为内置的Python对象,如列表、字典或基于数组的对象,如pandas DataFrames。更糟糕的是,许多框架,包括Spark和Hadoop,最初只为用户定义函数提供“一次一值”的执行模型,其中NumPy或pandas等工具则提供了“一次一数组”的执行模型,以避免Python解释器的开销。数据转换和解释器的双重昂贵开销使得Python基于大数据框架进行大规模数据处理变得愈加不现实。

Apache Spark通过引入Spark DataFrames来改善与Python的一些语言互操作性问题,Spark DataFrames是Spark SQL的一种新的类似pandas的API,它无需在Spark运行时和Python之间传输数据。不幸的是,任何需要使用Python的数据科学或机器学习库的应用程序都不走运。这给数据科学家和数据工程师带来了一个艰难的选择:用Python更快地开发,以换取更慢、更昂贵的工作负载,或者用Scala或Java重写关键工作负载。

Apache Arrow项目的起源

Apache Arrow的起源故事有点像微积分的创建:各自独立的开源开发人员团体在2010年代中期的同一时间都有过“尤里卡时刻”(译注:据说阿基米德洗澡时福至心灵,想出了如何测量皇冠体积的方法,因而惊喜地叫出了一声:“Eureka!”)。

2014年底,我加入了Cloudera,开始与分别由Marcel Kornacker和Todd Lipcon领导的Apache Impala和Apache Kudu团队密切合作。我们对在大规模分布式存储和数据处理引擎之上为Python程序员(特别是pandas用户)构建直观和快速的开发人员体验上有一致的兴趣。当时的一个突出的问题是缺乏标准化的、高速的面向列的“数据协议”,以便在引擎和编程语言之间高效地传输数据。我们不想为我们的这个事情创建自定义数据格式,也不想使用像Google的Protocol Buffers或Apache Thrift这样的数据序列化技术,因为这些技术引入了过多的计算开销。我们开始设计一种新的列式数据格式,但我们知道,如果它是一个主要由Cloudera领导的项目,那么在大数据开源项目的高度政治化氛围中,它可能会有失败的风险。

与此同时,Julien Le Dem和Jacques Nadeau,分别是Apache Parquet文件格式和Apache Drill查询引擎的共同创建者,他们正在探索一种方法,将Drill用于查询执行的内存列格式转变为独立的开源项目。这种数据格式被用作Dremio的基础,Dremio是一个基于SQL的开源数据湖引擎,使用它可以使得云中不同存储和数据处理系统之间更快,更高效的进行连接。

值得庆幸的是,Julien、Marcel和Todd在几年前就已经合作设计了Parquet文件格式,所以我们取得了联系并决定共同解决问题,而不是启动单独的、几乎肯定不会兼容的项目。我们举行了一系列快速的面对面会议(现在来看,在2022年那几乎是不可想象的!),我们开始招募其他开源大数据领导者加入我们创建一个新项目,包括 Julian Hyde(Apache Calcite)、Reynold Xin (Apache Spark)、Michael Stack (Apache HBase)等等。

2016年,在将Apache Arrow作为Apache软件基金会的顶级项目推出后,我们一直致力于使Arrow成为需要快速移动和处理数据的数据分析系统的首选项目。从那时起,该项目已成为高效的内存中列式分析和低开销数据传输的事实标准,它支持10多种编程语言。除了提供内存数据格式和互操作性协议外,我们还创建了一个功能全面的模块化计算库工具箱,为下一代分析计算系统打下坚实的基础

在启动Arrow项目仅一年后,与Two Sigma的我的新同事以及IBM的合作者的合作,让我们能够加速PySpark与Arrow的使用,在某些情况下实现了10-100倍的性能提升,并显著改善了将Python和pandas与Apache Spark一起使用的体验。看到我们对更快、更具互操作性的未来的愿景开始逐步实现,这真是令人兴奋。

2018年,我与RStudio和Two Sigma合作成立了Ursa Labs,作为一个非营利性行业联盟,其使命是使Arrow成为下一代数据科学工具的强大计算基础。我参与Arrow的工作,除了解决数据互操作性问题外,还旨在解决现代硬件上的内存管理和内存计算效率问题。我們很幸运地获得了NVIDIA、Intel、G-Research、Bloomberg、ODSC和OneSixtyTwo Technologies的额外赞助。

经过4年多的Apache Arrow开发,我们清楚地认识到,要促使Arrow下一阶段的增长和对企业的影响,仅通过行业赞助还不够,还需要获得更大的资本投资才行。于是在2020年底,我们决定将Ursa Labs团队从RStudio(为Ursa Labs提供了大部分资金和运营支持)中剥离出来,组建一家营利性公司Ursa Computing,并在2020年底筹集了一轮风险投资。不久之后,在2021年初,我们有机会与Arrow上的GPU分析、BlazingSQL和RAPIDS领导层的创新者联手,组建了一家统一的Arrow原生(Arrow-native)计算公司Voltron Data。Ursa Labs已成为Voltron Data Labs,Voltron Data内部的一个团队,其持续的使命是发展和支持Arrow生态系统,同时维护Apache Way的开放和透明的治理模型。

Apache Arrow项目的增长

如今,Arrow开发人员社区已发展到700多人,其中67人拥有提交权限。我们以创建跨语言开放标准和构建模块化软件组件为动力,以降低系统复杂性,同时提高性能和效率。我们一直在考虑将该项目视为一个软件开发工具包,旨在使开发人员能够释放Apache Arrow内存格式的好处,并解决随之而来的一阶和二阶问题(例如从云存储中读取Parquet文件,然后进行一些内存分析处理)。如果没有一个可信的、“自带电池”的软件堆栈来构建支持Arrow的计算应用程序来配合它,Arrow的列式格式本质上只能作为一种替代文件格式。

最近,在将Arrow列式格式和协议稳定用于生产用途后,社区一直专注于提供快速的Arrow原生计算组件。这项工作在C++和Rust社区中最为活跃。使用这些语言的查询引擎项目(DataFusion for Rust 和尚未命名的C++子项目),您可以轻松地将嵌入式Arrow原生列式数据处理特性添加到您的应用程序中。这可能包括您可能使用SQL或数据帧(dataframe)库(如 pandas 或 dplyr)表示的工作负载。新的高性能数据帧库(如Polars)从一开始就被构建为Arrow原生。在Voltron Data,我们正在积极努力使这些功能无缝地提供给Python和R程序员。

让这些项目采用Arrow数据互操作性协议的一个令人信服的理由是,与任何其他使用Arrow的项目可以实现简单快速的连接。早期采用者出于信任并收获了巨大的回报。现在,任何可以读写Arrow的项目都可以通过一个快速路径连接到数据帧库(如 pandas 和 R)和许多机器学习系统(PyTorchTensorFlowHugging Face)。

Arrow的贡献者通过与其他开源项目的密切合作,扩展了项目的能力。最近,与DuckDB实验室合作,使用DuckDB作为嵌入式执行引擎实现了无缝查询。R或Python现在能够使用DuckDB无缝查询其Arrow数据,可以使用类似数据帧的API(如dplyr)或SQL。此集成是经由Arrow的C数据接口实现的。

使数据服务和分布式系统更容易使用Arrow的二进制格式是推动Arrow被更广泛接纳的一个重要工作。由于将Arrow协议与一些通用数据服务框架(如 gRPC 或 Apache Thrift)联合最佳使用需要一些中间件代码,因此社区开发了Flight,这是一个用于Arrow原生数据服务的开发者框架和客户端-服务器协议。Flight提供了用于实现服务器和客户端逻辑的高级库,同时使用行业标准gRPC库进行内部通信。通过在客户端和服务器中使用通用内存格式来消除不必要的数据序列化,用户可以实现以前在独立于语言的协议中无法想象的数据吞吐级别(在某些情况下每秒几千兆字节)。Flight库现在在许多Arrow语言库(C++、Python、R、Java、Rust、Go)中可用,未来肯定会添加更多语言。

数据库是最普遍使用的数据服务之一,ODBC和JDBC等标准数据库接口根本上是为实现互操作性和兼容性而设计,而不是为了速度。因此,Flight带来了两全其美的可能性:互操作性而又不影响性能。但是,作为开发者框架和协议的Flight没有任何关于SQL数据库工作方式的内置概念,包括用户会话、执行查询的生命周期或预处理语句等内容。还有一个风险是,每个数据库系统实现其Flight服务器的方式略有不同,因此用户必须使用不同的Flight客户端来访问每种数据库。为了解决这些问题,包括SQL数据库的客户端/服务器标准化以及与ODBC和JDBC相似的高级功能,Arrow创建了一个称为Flight SQL的Flight应用程序扩展。现在,数据库开发人员可以实现一个通用的Flight SQL服务器,用户将能够使用标准的Flight SQL客户端访问任何启用Flight SQL的数据库。


来源:https://www.dremio.com/subsurface/arrow-flight-sql-a-universal-jdbc-driver

Apache Arrow生态系统的发展和采用

Arrow项目及其生态系统的发展得益于其早期采纳者的成功。总的来说,Arrow已经成为Python用户与以Parquet等文件格式存储的数据集进行交互的标准工具。如上所述,在项目早期,我们与Spark社区合作,使用Arrow更快地将数据传输到pandas来加速PySpark。在这些早期成功案例之后,许多其他项目都采用了Arrow来实现更快的互操作性和内存处理,并删除了以前的定制解决方案。

通过采用Arrow进行数据传输,Streamlit能够删除自定义代码,同时大幅提高应用程序性能。Streamlit的传统序列化框架基于Protocol Buffers,用于将表格数据从Python后端发送到JavaScript前端。通过将自定义序列化程序替换为Arrow,Streamlit的性能提高了15倍,并且能够通过使用现成的解决方案来简化其代码库。


来源: https://blog.streamlit.io/content/images/2021/07/legacy-vs-arrow-2-1.png#shadow

Dremio是从头开始就以Apache Arrow为核心构建的系统。Dremio由Jacques Nadeau共同创立,是一个用于数据湖的分布式查询引擎。Dremio开发了一种基于LLVM的即时表达式编译器,称为Gandiva(现在是Arrow项目的一部分),它可以针对Arrow列式内存的操作生成高效的机器代码。与在JVM中执行的解释表达式相比,这可实现更快的性能。

最近,Databricks发布了Cloud Fetch connector,用于将商业智能工具(如Tableau或Power BI)与存储在云中的数据连接起来。过去,从传统数据仓库检索数据的速度受到了在单个线程上从单个SQL端点提取数据的速度的限制。这限制了交互式数据探索工具的有用性。Cloud Fetch 使用Arrow wire协议从云存储并行流式传输数据,与传统方法相比,性能提高了12倍。

这些只是使用Arrow项目的某些部分来加速数据移动或在内存中处理数据的项目的几个示例。随着越来越多的项目启用Arrow,用户将获得复合效率的优势。例如,在Snowflake实现以Arrow格式从其系统中检索数据后,他们的Python和JDBC客户端的数据检索速度提高了5倍。这不仅使Snowflake查询运行得更快,而且使得与Snowflake集成的产品运行得更快。例如,人工智能驱动的分析平台Tellius能够使用Arrow将他们与Snowflake的集成速度提高3倍,相比于之前的实现。

社区

Apache Arrow的受欢迎程度正在不断增长。事实上,Arrow的Python库PyArrow在2022年1月的下载量为4600w次,这一数字比2021年10月份创造的之前的记录增加了近800w次。我们预计,随着越来越多的项目采用Arrow作为依赖项,这一趋势将继续下去。


资料来源:https://pypistats.org/,沃尔创数据

Arrow为数据传输、对二进制文件(如 Parquet)的高速访问以及快速发展的计算引擎提供了坚实的基础。这需要多年的工作和一个庞大的社区才能实现。在过去的6年里,Arrow开发者社区得到了相当大的发展:自2016年首次发布以来,已有676名独立的开发人员为该项目做出了贡献,其中105名贡献者参与了Arrow 7.0.0版本的开发。

与Apache软件基金会中的所有项目一样,我们遵循Apache Way,这是一个开放透明的开源项目治理框架。项目讨论和决策必须在公开场合进行,例如在邮件列表或GitHub上。贡献者以个人身份参与,而不是作为他们工作的公司的代表。通过公开开展所有项目业务,我们可以保持包容和专业的氛围,欢迎来自世界各地的贡献者的不同观点。Apache Way重视多种贡献:回答用户问题、分类错误报告和编写文档与提出拉取请求一样重要。Arrow项目主要的开发人员邮件列表是dev@arrow.apache.org。

在项目中持续工作一段时间后,贡献者可以通过项目管理委员会(PMC)的投票被提升为“提交者”(对项目git存储库具有写入权限)。表现出致力于发展和指导项目社区的提交者以后可能会被提升加入PMC。PMC成员是项目指导委员会,对项目中的发布和其他重大决策具有约束力的投票权。目前Arrow项目有67个提交者和38个PMC 成员。

未来

随着Arrow开发者社区的发展,项目范围也在扩大。该项目始于六年前,旨在设计一个独立于语言的标准来表示面向列的数据,以及一个二进制协议,用于在应用程序之间移动数据。从那时起,该项目稳步发展,提供了一个自带电池的开发工具箱,以简化构建涉及处理大型数据集的高性能分析应用程序。我们预计Arrow将成为下一代大数据系统的关键组成部分。

我们期望开放标准和接口方面的工作能够继续团结和简化分析计算生态系统。我们参与了Substrait,这是一个新的开源框架,提供标准化的中间查询语言(低于SQL级别),将前端用户界面(如SQL或data frame库)与后端分析计算引擎连接起来。Substrait由Arrow项目联合创始人Jacques Nadeau创立,并且发展迅速。我们认为,有了这个新项目提供的执行引擎支持,编程语言接口与分析性计算将更容易发展。

加入我们!

发展Apache Arrow项目是我们Voltron Data使命的重要组成部分!我们期待继续与社区合作,推动生态系统向前发展。您可以订阅我们的新闻通讯以随时了解情况,并考虑在Twitter上关注我们@voltrondata以获取更多新闻。您还可以探索Voltron Data Enterprise Support订阅选项,这个订阅列表旨在帮助在Apache Arrow生态系统中工作的开发人员和公司。


“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群!高品质首发Go技术文章,“三天”首发阅读权,每年两期Go语言发展现状分析,每天提前1小时阅读到新鲜的Gopher日报,网课、技术专栏、图书内容前瞻,六小时内必答保证等满足你关于Go语言生态的所有需求!2023年,Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码,关注代码质量并深入理解Go核心技术,并继续加强与星友的互动。欢迎大家加入!

img{512x368}
img{512x368}

img{512x368}
img{512x368}

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

Gopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily

我的联系方式:

  • 微博(暂不可用):https://weibo.com/bigwhite20xx
  • 微博2:https://weibo.com/u/6484441286
  • 博客:tonybai.com
  • github: https://github.com/bigwhite

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

Go语言开发者的Apache Arrow使用指南:内存管理

本文永久链接 – https://tonybai.com/2023/06/30/a-guide-of-using-apache-arrow-for-gopher-part2

如果你看了上一篇《Go语言开发者的Apache Arrow使用指南:数据类型》中的诸多Go操作arrow的代码示例,你很可能会被代码中大量使用的Retain和Release方法搞晕。不光大家有这样的感觉,我也有同样的feeling:Go是GC语言,为什么还要借助另外一套Retain和Release来进行内存管理呢

在这一篇文章中,我们就来探索一下这个问题的答案,并看看如何使用Retain和Release,顺便再了解一下Apache Arrow的Go实现原理。

注:本文的内容基于Apache Arrow Go v13版本(go.mod中go version为v13)的代码。

1. Go Arrow实现中的builder模式

看过第一篇文章中的代码的童鞋可能发现了,无论是Primitive array type还是嵌套类型的诸如List array type,其array的创建套路都是这样的:

  • 首先创建对应类型的Builder,比如array.Int32Builder;
  • 然后,向Builder实例中append值;
  • 最后,通过Builder的NewArray方法获得目标Array的实例,比如array.Int32。

据说这个builder模式是参考了Arrow的C++实现。这里将Go的builder模式中各个类型之间的关系以下面这幅示意图的形式呈现一下:

当然这幅图也大概可以作为Go Arrow实现的原理图。

从图中,我们可以看到:

  • Arrow go提供了Builder、Array、ArrayData接口作为抽象,在这些接口中都包含了用作内存引用计数管理的Retain和Release方法;
  • array包提供了Builder接口的一个默认实现builder类型,所有的XXXBuilder都组(内)合(嵌)了这个类型,这个类型实现了Retain方法,Release方法需要XXXBuilder自行实现。
  • array包提供了Array接口的一个默认实现array类型,所有的array type(比如array.Int32)都组(内)合(嵌)了这个array类型。该类型实现了Retain和Release方法。
// github.com/apache/arrow/go/arrow/array/array.go
type array struct {
    refCount        int64
    data            *Data
    nullBitmapBytes []byte
}

// Retain increases the reference count by 1.
// Retain may be called simultaneously from multiple goroutines.
func (a *array) Retain() {
    atomic.AddInt64(&a.refCount, 1)
}

// Release decreases the reference count by 1.
// Release may be called simultaneously from multiple goroutines.
// When the reference count goes to zero, the memory is freed.
func (a *array) Release() {
    debug.Assert(atomic.LoadInt64(&a.refCount) > 0, "too many releases")

    if atomic.AddInt64(&a.refCount, -1) == 0 {
        a.data.Release()
        a.data, a.nullBitmapBytes = nil, nil
    }
}

下面以Int64 array type为例:

// github.com/apache/arrow/go/arrow/array/numeric.gen.go 

// A type which represents an immutable sequence of int64 values.
type Int64 struct {
    array // “继承”了array的Retain和Release方法。
    values []int64
}
  • 通过XXXBuilder类型的NewArray方法可以获得该Builder对应的Array type实例,比如:调用Int32Builder的NewArray可获得一个Int32 array type的实例。一个array type实例对应的数据是逻辑上immutable的,一旦创建便不能改变。
  • 通过Array接口的Data方法可以得到该array type的底层数据layout实现(arrow.ArrayData接口的实现),包括child data。
  • arrow包定义了所有的数据类型对应的ID值和string串,这个与arrow.DataType接口放在了一个源文件中。
  • 另外要注意,XXXBuilder的实例是“一次性”的,一旦调用NewArray方法返回一个array type实例,该XXXBuilder就会被reset。如果再次调用其NewArray方法,只能得到一个空的array type实例。你可以重用该Builder,只需向该Builder实例重新append值即可(见下面示例):
// reuse_string_builder.go

func main() {
    bldr := array.NewStringBuilder(memory.DefaultAllocator)
    defer bldr.Release()
    bldr.AppendValues([]string{"hello", "apache arrow"}, nil)
    arr := bldr.NewArray()
    defer arr.Release()
    bitmaps := arr.NullBitmapBytes()
    fmt.Println(hex.Dump(bitmaps))
    bufs := arr.Data().Buffers()
    for _, buf := range bufs {
        fmt.Println(hex.Dump(buf.Buf()))
    }
    fmt.Println(arr)

    // reuse the builder
    bldr.AppendValues([]string{"happy birthday", "leo messi"}, nil)
    arr1 := bldr.NewArray()
    defer arr1.Release()
    bitmaps1 := arr1.NullBitmapBytes()
    fmt.Println(hex.Dump(bitmaps1))
    bufs1 := arr1.Data().Buffers()
    for _, buf := range bufs1 {
        if buf != nil {
            fmt.Println(hex.Dump(buf.Buf()))
        }
    }
    fmt.Println(arr1)
}

输出上面示例运行结果:

$go run reuse_string_builder.go
00000000  03                                                |.|

00000000  03 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000010  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000020  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000030  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|

00000000  00 00 00 00 05 00 00 00  11 00 00 00 00 00 00 00  |................|
00000010  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000020  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000030  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|

00000000  68 65 6c 6c 6f 61 70 61  63 68 65 20 61 72 72 6f  |helloapache arro|
00000010  77 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |w...............|
00000020  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000030  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|

["hello" "apache arrow"]
00000000  03                                                |.|

00000000  03 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000010  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000020  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000030  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|

00000000  00 00 00 00 0e 00 00 00  17 00 00 00 00 00 00 00  |................|
00000010  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000020  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000030  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|

00000000  68 61 70 70 79 20 62 69  72 74 68 64 61 79 6c 65  |happy birthdayle|
00000010  6f 20 6d 65 73 73 69 00  00 00 00 00 00 00 00 00  |o messi.........|
00000020  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000030  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|

["happy birthday" "leo messi"]

想必到这里,大家对Arrow的Go实现原理有了一个大概的认知了。接下来,我们再来看Go arrow实现的内存引用计数管理。

2. Go Arrow实现的内存引用计数管理

在上面图中,我们看到Go Arrow实现的几个主要接口Builder、Array、ArrayData都包含了Release和Retain方法,也就是说实现了这些接口的类型都支持采用引用计数方法(Reference Counting)进行内存的跟踪和管理。Retain方法的语义是引用计数加1,而Release方法则是引用计数减1。由于采用了原子操作对引用计数进行加减,因此这两个方法是并发安全的。当引用计数减到0时,该引用计数对应的内存块就可以被释放掉了。

Go Arrow实现的主页上对引用计数的使用场景和规则做了如下说明:

  • 如果你被传递了一个对象并希望获得它的所有权(ownership),你必须调用Retain方法。当你不再需要该对象时,你必须调用对应的Release方法。”获得所有权”意味着你希望在当前函数调用的范围之外访问该对象。
  • 你通过名称以New或Copy开头的函数创建的任何对象,或者在通过channel接收对象时,你都将拥有所有权。因此,一旦你不再需要这个对象,你必须调用Release。
  • 如果你通过一个channel发送一个对象,你必须在发送之前调用Retain,因为接收者将拥有该对象。接收者有义务在以后不再需要该对象时调用Release。

有了这个说明后,我们对于Retain和Release的使用场景基本做到心里有谱了。但还有一个问题亟待解决,那就是:Go是GC语言,为何还要在GC之上加上一套引用计数呢

这个问题我在这个issue中找到了答案。一个Go arrow实现的commiter在回答issue时提到:“理论上,如果你知道你使用的是默认的Go分配器,你实际上不必在你的消费者(指的是Arrow Go包 API的使用者)代码中调用Retain/Release,可以直接让Go垃圾回收器管理一切。我们只需要确保我们在库内调用Retain/Release,这样如果消费者使用非Go GC分配器,我们就可以确保他们不会出现内存泄漏”。

下面是默认的Go分配器的实现代码:

package memory

// DefaultAllocator is a default implementation of Allocator and can be used anywhere
// an Allocator is required.
//
// DefaultAllocator is safe to use from multiple goroutines.
var DefaultAllocator Allocator = NewGoAllocator()

type GoAllocator struct{}

func NewGoAllocator() *GoAllocator { return &GoAllocator{} }

func (a *GoAllocator) Allocate(size int) []byte {
    buf := make([]byte, size+alignment) // padding for 64-byte alignment
    addr := int(addressOf(buf))
    next := roundUpToMultipleOf64(addr)
    if addr != next {
        shift := next - addr
        return buf[shift : size+shift : size+shift]
    }
    return buf[:size:size]
}

func (a *GoAllocator) Reallocate(size int, b []byte) []byte {
    if size == len(b) {
        return b
    }

    newBuf := a.Allocate(size)
    copy(newBuf, b)
    return newBuf
}

func (a *GoAllocator) Free(b []byte) {}

我们看到默认的Allocator只是分配一个原生切片,并且切片的底层内存块要保证64-byte对齐。

但为什么Retain和Release依然存在且需要调用呢?这位commiter给出了他理解的几点原因:

  • 允许用户控制buffer和内部数据何时被设置为nil,以便在可能的情况下提前标记为可被垃圾收集;
  • 如果用户愿意,允许正确使用不依赖Go垃圾收集器的分配器(比如mallocator实现,它使用malloc/free来管理C内存而不是使用Go垃圾收集来管理);
  • 虽然用户可以通过SetFinalizer来使用Finalizer进行内存释放,但一般来说,我们建议最好有一个显式的释放动作,而不是依赖finalizer,因为没有实际保证finalizer会运行。此外,finalizer只在GC期间运行,这意味着如果你的分配器正在分配C内存或其他东西,而Go内存一直很低,那么你有可能在任何finalizer运行以实际调用Free之前,就被分配了大量的C内存,从而耗尽了你的内存。

基于这些原因,Go Arrow实现保留了Retain和Release,虽然有上门的一些场景使用方法,但这两个方法的加入一定程度上增加了Go Arrow API使用的门槛。并且在重度使用Go Arrow实现的程序中,大家务必对程序做稳定性长测试验证,以确保memory没有leak。

3. 如何实现ZeroCopy的内存数据共享

《In-Memory Analytics with Apache Arrow》一书在第二章中提到了采用Arrow实现zerocopy的内存数据共享的原理,这里将其称为“切片(slice)原理”,用书中的例子简单描述就是这样的:假设你想对一个有数十亿行的非常大的数据集进行一些分析操作。提高这种操作性能的一个常见方法是对行的子集进行并行操作,即仅通过对数组和数据缓冲区进行切分,而不需要复制底层数据。这样你操作的每个批次都不是一个副本–它只是数据的一个视图。书中还给出了如下示意图:

右侧切片列中的每个切片的虚线表示它们只是各自列中的数据子集的视图,每个切片都可以安全地进行并行操作。

array type是逻辑上immutable的,底层data buffer一旦建立后,便可以通过切片的方式来以zerocopy方式做内存数据共享,极大提高了数据操作的性能。

4. 小结

本文介绍了Go arrow实现的主要结构以及实现模式:builder模式,并结合Go arrow官方资料说明了采用引用计数进行内存管理的原因与使用方法,最后介绍了Arrow实现ZeroCopy的内存数据共享的原理。这些将为后续继续深入学习Arrow高级数据类型/结构奠定良好的基础。

注:本文涉及的源代码在这里可以下载。


“Gopher部落”知识星球旨在打造一个精品Go学习和进阶社群!高品质首发Go技术文章,“三天”首发阅读权,每年两期Go语言发展现状分析,每天提前1小时阅读到新鲜的Gopher日报,网课、技术专栏、图书内容前瞻,六小时内必答保证等满足你关于Go语言生态的所有需求!2023年,Gopher部落将进一步聚焦于如何编写雅、地道、可读、可测试的Go代码,关注代码质量并深入理解Go核心技术,并继续加强与星友的互动。欢迎大家加入!

img{512x368}
img{512x368}

img{512x368}
img{512x368}

著名云主机服务厂商DigitalOcean发布最新的主机计划,入门级Droplet配置升级为:1 core CPU、1G内存、25G高速SSD,价格5$/月。有使用DigitalOcean需求的朋友,可以打开这个链接地址:https://m.do.co/c/bff6eed92687 开启你的DO主机之路。

Gopher Daily(Gopher每日新闻)归档仓库 – https://github.com/bigwhite/gopherdaily

我的联系方式:

  • 微博(暂不可用):https://weibo.com/bigwhite20xx
  • 微博2:https://weibo.com/u/6484441286
  • 博客:tonybai.com
  • github: https://github.com/bigwhite

商务合作方式:撰稿、出书、培训、在线课程、合伙创业、咨询、广告合作。

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 Go语言编程指南
商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats