在图片、音视频和压缩包等数据处理场景中,单个对象可能占用较多内存。本文介绍如何使用 Lance-Ray 按批次处理并写入 Lance,避免在应用中先收集全部处理结果。
处理场景 | 建议方式 |
|---|---|
大对象处理后得到较小的特征、摘要或元数据 | lance-ray write_lance |
处理结果仍然很大,不适合进入 Ray Object Store | lance-ray LanceFragmentWriter |
不需要分布式并行,单机资源足以完成计算 | PyLance |
write_lance 是 Ray Dataset 写入 Lance 的高层接口。例如,Worker 可以逐个读取视频并提取特征,释放不再使用的视频缓冲区,只把较小的特征和元数据交给后续写入流程。
以下示例使用只包含 source_uri 字符串列的 Ray Dataset 作为输入,source_uris 和 sources 也用于后续示例。请将示例中的读取和处理逻辑替换为实际业务逻辑。TARGET_URI 和 STORAGE_OPTIONS 分别表示目标数据集路径和存储配置。
import ray # 替换为实际的源对象 URI。 source_uris = ["tos://source-bucket/object-1", "tos://source-bucket/object-2"] sources = ray.data.from_items([{"source_uri": uri} for uri in source_uris])
本例每批处理 32 个源对象引用。
import lance_ray as lr import pyarrow as pa OUTPUT_SCHEMA = pa.schema( { "source_uri": pa.string(), "object_size": pa.int64(), "summary": pa.string(), } ) def process_large_objects(batch: pa.Table) -> pa.Table: rows = [] for source_uri in batch["source_uri"].to_pylist(): # large_object = read_large_object(source_uri) # object_size, summary = process(large_object) object_size, summary = 0, "result" rows.append( { "source_uri": source_uri, "object_size": object_size, "summary": summary, } ) return pa.Table.from_pylist(rows, schema=OUTPUT_SCHEMA) results = sources.map_batches( process_large_objects, batch_format="pyarrow", batch_size=32, concurrency=4, ) lr.write_lance( results, uri=TARGET_URI, schema=OUTPUT_SCHEMA, mode="overwrite", min_rows_per_file=1024, storage_options=STORAGE_OPTIONS, concurrency=4, )
参数 | 作用 |
|---|---|
| 控制一次处理函数调用接收的输入行数;实际 batch 可以更小。 |
| 控制处理任务的最大并发数。 |
| 控制写入任务的最大并发数。 |
| 控制写入任务合并输入的目标行数。调大可减少小 Fragment,通常生成更大的文件,同时可能增加写入等待时间。 |
当处理结果本身仍然很大时,将其作为中间 Ray Dataset 在任务间传递,会增加 Ray Object Store 的序列化、内存和 spill 压力。LanceFragmentWriter 可以把转换与写入放在同一个 Worker 内,避免将大结果作为 map_batches 的输出返回给 Ray。
LanceFragmentWriter 在同一个 Worker 内直接写入这些结果,向 Ray 返回 Fragment 元数据。LanceFragmentCommitter 汇总写入结果,由 Driver 提交新版本。本例一次最多接收 32 个 URI,在生成器中逐个处理,每次产出一行结果。
import lance import pyarrow as pa from lance_ray.datasink import LanceFragmentCommitter from lance_ray.fragment import LanceFragmentWriter OUTPUT_SCHEMA = pa.schema( { "source_uri": pa.string(), "payload": pa.large_binary(), } ) def process_large_objects(batch: pa.Table): for source_uri in batch["source_uri"].to_pylist(): # large_object = read_large_object(source_uri) # payload = process(large_object) payload = b"result" # 大对象占位值 yield pa.Table.from_pylist( [{"source_uri": source_uri, "payload": payload}], schema=OUTPUT_SCHEMA, ) # 先创建空表,再追加处理结果。 lance.write_dataset( pa.Table.from_pylist([], schema=OUTPUT_SCHEMA), TARGET_URI, mode="create", storage_options=STORAGE_OPTIONS, ) fragment_metadata = sources.map_batches( LanceFragmentWriter( TARGET_URI, schema=OUTPUT_SCHEMA, transform=process_large_objects, storage_options=STORAGE_OPTIONS, ), batch_format="pyarrow", batch_size=32, concurrency=2, ) fragment_metadata.write_datasink( LanceFragmentCommitter( TARGET_URI, mode="append", storage_options=STORAGE_OPTIONS, ), )
单机处理时,可以将生成器产出的批次通过 RecordBatchReader 写入 Lance。
import lance import pyarrow as pa OUTPUT_SCHEMA = pa.schema( { "source_uri": pa.string(), "payload": pa.large_binary(), } ) def batches(): for source_uri in source_uris: # large_object = read_large_object(source_uri) # payload = process(large_object) payload = b"result" yield pa.RecordBatch.from_pylist( [{"source_uri": source_uri, "payload": payload}], schema=OUTPUT_SCHEMA, ) lance.write_dataset( pa.RecordBatchReader.from_batches(OUTPUT_SCHEMA, batches()), TARGET_URI, mode="overwrite", storage_options=STORAGE_OPTIONS, )