处理网络上的Parquet数据时,最常见的做法是先把文件下载到本地磁盘,再用pandas或Spark读取成行式对象,最后通过JSON或pickle序列化后发送给对端。这条链路的问题在于Parquet本身就是按列存储、按列压缩的,可一旦被读成逐行对象,列内连续的数据布局就被打散,原本高效的批量操作退化成一条条记录转换。Arrow的出现正好解决了这个矛盾——它既能直接读取Parquet的列式数据,又提供了进程间和网络间传输的二进制协议,让数据从磁盘到网卡始终维持列式内存布局。

Arrow项目包含多个组件:核心的C++库、Python的pyarrow、Go的go-arrow、Java等语言绑定,以及专门用于网络传输的Arrow Flight RPC框架。在处理Parquet数据时,pyarrow.parquet模块能直接读取文件为RecordBatch或Table,这些对象本身就是Arrow列式内存格式。相比pandas的DataFrame,Arrow Table在列方向上内存连续,且支持零拷贝切片。当你需要把这个Table发送到另一台机器时,不需要先转成行式字典列表,直接用Arrow IPC流格式(也称作Feather或Arrow Streaming)序列化即可,接收方再用Arrow API还原,整个过程中没有逐行循环和Python对象装箱开销。
Parquet列式结构决定了网络传输的优化路径
Parquet文件内部由多个Row Group组成,每个Row Group包含若干Column Chunk,每个Column Chunk又按Page分块存储。列式存储意味着某一列的所有值在物理磁盘上是连续排列的,并且可以独立压缩。如果一个查询只需要三列中的两列,Parquet读取器可以只读取对应的Column Chunk,跳过其余列,这就是列裁剪。对于网络传输来说,这一特性非常关键:发送方可以先执行列裁剪,把不需要的列在读取阶段就丢弃,从而减少需要发送的字节数。传统行式读取方案通常会把整行全部读出来,即便最后只用到其中几个字段,数据量和序列化开销都白白浪费。
利用pyarrow读取Parquet时,可以通过columns参数指定需要的列。比如一个Parquet文件包含user_id、event_type、event_time、payload四列,但下游只关心user_id和event_time,就可以这样写:
import pyarrow.parquet as pq
table = pq.read_table(
"events.parquet",
columns=["user_id", "event_time"]
)
这段代码在底层调用Parquet C++库时,会只扫描对应列的Column Chunk,磁盘读取量和内存占用都明显降低。如果把裁剪后的Table直接通过IPC流发送给远端,那么发送的数据量就是裁剪后的列数据,而不是原始文件大小。除了列裁剪,Parquet还支持谓词下推,也就是把过滤条件推给Parquet读取器,利用文件中保存的列统计信息(最小值、最大值、空值计数)跳过整个Row Group。例如只取event_time大于某个时间戳的记录:
import pyarrow as pa
import pyarrow.parquet as pq
filters = [
("event_time", ">", pa.scalar("2024-01-01 00:00:00", type=pa.timestamp("ns")))
]
table = pq.read_table("events.parquet", filters=filters)
这里演示了谓词下推的用法,Parquet读取器会检查每个Row Group的元数据中event_time列的最小值和最大值,如果整个Row Group都不满足条件,就直接跳过不读取。对于网络场景,这相当于在源头就过滤掉了无关数据,避免它们进入网卡。Arrow的Python接口完全暴露了这些底层能力,而且调用开销极低,因为过滤逻辑是在C++层执行的。
用Arrow IPC流式格式替代JSON序列化
把Arrow Table通过常规网络接口发送时,有些开发者会先调用to_pandas()转成DataFrame,再用to_json()输出JSON字符串。JSON是一种文本格式,每条记录都要重复字段名,数字和布尔值都转成文本,二进制数据还需要Base64编码,体积膨胀非常明显。Arrow IPC流格式则是一种紧凑的二进制格式,它保留了列式布局,并且内置了Schema信息,接收方可以直接还原出Table。在pyarrow中,可以使用ipc模块进行序列化:
import pyarrow as pa import pyarrow.ipc as ipc # 假设table是已经读取好的Arrow Table sink = pa.BufferOutputStream() writer = ipc.new_stream(sink, table.schema) writer.write_table(table) writer.close() binary_data = sink.getvalue().to_pybytes()
这段代码把Table写进一个内存缓冲区,得到一段二进制字节串。这个字节串可以直接通过socket、HTTP请求体或消息队列发送。接收方反序列化同样简单:
import pyarrow as pa import pyarrow.ipc as ipc # binary_data是从网络收到的字节串 reader = ipc.open_stream(binary_data) table = reader.read_all()
对比JSON,Arrow IPC流格式通常能减少50%到80%的数据量,具体取决于字段类型和数据的重复程度。更重要的是,它省去了字符解析和数值转换的CPU时间。在实际测试中,读取一个包含100万行、10个数值列的Parquet文件,转成JSON字符串后大小约为180MB,而Arrow IPC流只需要约80MB,传输时间在网络带宽有限时差异显著。而且Arrow IPC流支持增量读取,如果发送方逐批写出多个RecordBatch,接收方可以边接收边处理,不需要等待全部数据到达,这对大文件流式传输非常有用。
Arrow Flight:为列式数据设计的原生网络传输框架
虽然手动使用IPC流格式已经比JSON高效,但要在生产环境中处理认证、负载均衡、并发控制等问题,自己实现一套完整的传输协议成本不低。Arrow Flight就是专门解决这个问题的RPC框架,它基于gRPC构建,但传输的数据载荷是Arrow IPC格式。Flight服务器可以暴露Parquet文件读取接口,客户端调用时直接拿到Arrow Table,内部自动完成数据的列式传输。Python中的Flight服务端实现如下:
import pyarrow as pa
import pyarrow.flight as flight
class ParquetFlightServer(flight.FlightServerBase):
def do_get(self, context, ticket):
# ticket里可以携带文件路径和过滤条件
table = pa.parquet.read_table(ticket.ticket.decode("utf-8"))
return flight.RecordBatchStream(table)
server = ParquetFlightServer("grpc://0.0.0.0:8815")
server.serve()
客户端连接后,直接发送ticket请求远程文件:
import pyarrow.flight as flight
client = flight.FlightClient("grpc://localhost:8815")
reader = client.do_get(flight.Ticket("events.parquet"))
table = reader.read_all()
这里的do_get方法返回的是一个FlightStreamReader,它本质上是Arrow IPC流,但底层走gRPC的双向流,支持背压和分块传输。服务端不是先把整个Table一次性序列化到内存再发送,而是可以按RecordBatch逐个写出,每个批次独立传输,非常适合大文件或者需要实时处理第一批数据的场景。Flight还支持do_put上传、do_action管理操作,以及通过gRPC的TLS实现加密传输。对于跨语言环境,Flight客户端和服务端可以在Python、C++、Java、Go等语言间互通,因为Arrow IPC格式是语言无关的。
在Go语言中处理Parquet和Flight也有成熟方案。go-arrow库提供了parquet读取和Flight客户端能力。一个典型的Go客户端拉取Flight数据的代码片段如下:
package main
import (
"context"
"fmt"
"log"
"github.com/apache/arrow/go/v17/arrow/flight"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
func main() {
conn, err := grpc.Dial("localhost:8815", grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
log.Fatal(err)
}
client, err := flight.NewClientWithMiddleware("localhost:8815", nil, nil)
if err != nil {
log.Fatal(err)
}
stream, err := client.DoGet(context.Background(), &flight.Ticket{Ticket: []byte("events.parquet")})
if err != nil {
log.Fatal(err)
}
for stream.Next() {
record := stream.Record()
fmt.Printf("批次行数: %d, 列数: %d\n", record.NumRows(), record.NumCols())
record.Release()
}
}
这段Go代码建立到Flight服务的连接,通过DoGet获取数据流,然后循环读取每个RecordBatch。注意Go的Arrow库要求手动管理内存,每个Record在使用后需要Release。虽然代码比Python繁琐,但性能更高,适合构建高性能网关或数据转发服务。如果服务端保存的是Parquet文件,Go也可以直接读取本地Parquet并通过Flight对外提供,或者作为中间层把上游Flight流转发给下游。
总体来看,使用Arrow包处理网络Parquet数据,核心收益来自避免列式到行式的反复转换。选择IPC流格式做轻量级传输,或者采用Arrow Flight构建完整的RPC服务,都能在不同复杂度需求下获得比传统JSON方案更好的吞吐和更低的延迟。实际落地时建议先评估文件大小和网络带宽,如果只是偶尔传输小文件,直接读取后序列化为IPC流即可;如果是持续的高并发服务,Arrow Flight配合列裁剪、谓词下推可以显著降低网络和磁盘负担。

