【FFI】 实战——数据中台场景下的 FFI 应用
数据中台场景下,不同技术栈的系统怎么用 FFI 共享同一套数据基础设施。
问题:为什么不能用微服务
公司数据基础设施是 Java 写的——Hudi 数仓、消息中间件、存储引擎,全是 JVM 生态。上游系统是 Python 训练引擎、Go 推理服务。让 Python 读 Hudi 表,最简单的方案是起一个 Java 微服务做代理:
Python → HTTP → Java 代理 → Hudi 原生 API
能跑,但不该跑。问题不在功能,在开销:每读一批 Parquet 文件,数据要经过 Java 侧反序列化 → JSON 序列化 → HTTP 传输 → Python 侧 JSON 反序列化。原始数据 200MB,序列化后膨胀到 350MB,再加上网络往返,单次查询慢了 10 倍。数据中台的链路本来就长,再加一层序列化开销是雪上加霜。
直接重写?不现实。Hudi 的 merge-on-read、compaction、timeline 一致性逻辑是几万行 Java,换语言重写的成本远超收益。
方案:FFI 桥接,同进程共享
让 Python 在同一个进程里调 Java 的 Hudi 原生 API。数据不离开进程,没有网络序列化,没有 HTTP 往返。
实现路径是 PyO3 包一层 Rust → Java Panama:
Python → PyO3(Rust 胶水) → Panama(Java FFI) → Hudi Java SDK
Rust 是中间的胶水层。原因:
- Python 不能直接调 Java。ctypes 只能调 C ABI,Java 不暴露 C ABI。PyO3 让 Python 调 Rust,Rust 再通过 Panama 调 Java,串联起来。
- Rust 管内存边界。Java 返回的
MemorySegment指针是 native 内存,Python 拿到裸指针。Rust 在中间用Arc包一层,确保两边都不提前释放。 - 类型转换编在编译期。Rust 的
#[pyfunction]宏自动生成 Python C API,Panama 的 jextract 从.h生成绑定,中间 Rust 负责类型映射——i64↔PyLong↔Java long,不需要手写。
经验:什么时候该用 FFI
做了两个项目后,判断标准很清楚:
适合 FFI 的场景:
- 数据基础设施共享。 一套 Hudi/中间件逻辑,多语言客户端都要调。FFI 一次桥接,多语言复用。PyO3 编译出来的
.so,Python 能import,Node.js 也能require。 - 性能敏感的热路径。 微服务的序列化成本在批处理里可以忽略(每秒几千条),在实时推理里不能(每条几毫秒)。FFI 省掉的是每次调用的序列化开销。
- 已有成熟的底层库。 Hudi、Kafka、Parquet 都有成熟的 Java 实现,重写不是选项。FFI 是唯一能在不重写的前提下拿到原生性能的方案。
不适合 FFI 的场景:
- 简单 CRUD。 一个查询走 200ms 网络,序列化占 5ms,FFI 省下来的 5ms 没有意义。微服务够用。
- 底层库不稳定。 FFI 意味着你的代码依赖对方语言的 API 语义。如果 Java 侧还在频繁改接口,维护成本远超微服务。
- 团队没有 Rust 人。 FFI 的胶水层是 Rust 写的,内存管理的坑需要有人能看懂
unsafe块和 allocator 语义。不值得为了省几百毫秒招一个人。
核心判断
FFI 不是银弹。它的价值在于:当不同技术栈的系统必须共享同一套数据基础设施时,FFI 让你不用在"重写"和"加网络开销"之间二选一。 但前提是底层库稳定、团队懂内存管理、性能收益值得维护成本。三个条件缺一个,微服务是更安全的选择。
寒蝉 Hancic

