第2章:整体架构设计
章节导读
本章将从顶层设计的角度,详细阐述 geaflow-infer 的三层架构 、核心组件关系 和Java-Python 进程间通信机制。
通过本章,你将深刻理解:
- 为什么 要这样分层设计
- 如何 在各层之间协调和通信
- 核心的权衡 是什么(性能 vs 可维护性)
2.1 三层架构设计
总体架构设计理念
geaflow-infer 采用了三层分层架构:
scss
┌────────────────────────────────────────────────────────────┐
│ 应用层 (Application Layer) │
│ InferContext │
│ 用户通过 InferContext 调用 infer() 进行推理 │
└──────────────────┬─────────────────────────────────────────┘
│
│ 统一接口(What)
▼
┌────────────────────────────────────────────────────────────┐
│ 逻辑层 (Logic Layer) │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ 环境管理 任务运行 数据交换 序列化 │ │
│ │ InferEnv* InferTask* DataExchange* Pickle* │ │
│ └──────────────────────────────────────────────────────┘ │
│ - 初始化虚拟环境、启动Python进程、协调数据流 │
│ - 处理错误、管理状态、记录日志 │
└──────────────────┬─────────────────────────────────────────┘
│
│ 系统调用、进程管理、内存操作(How)
▼
┌────────────────────────────────────────────────────────────┐
│ 基础层 (Infrastructure Layer) │
│ ┌──────────────┐ ┌───────────────┐ ┌──────────────┐ │
│ │ ProcessBuilder│ │ File I/O │ │ Unsafe + mmap│ │
│ │ (进程) │ │ (文件) │ │ (内存) │ │
│ └──────────────┘ └───────────────┘ └──────────────┘ │
│ 与操作系统交互,提供底层能力 │
└────────────────────────────────────────────────────────────┘
架构的设计哲学
1. 分离关注点(Separation of Concerns)
markdown
┌────────────────────────────────────────────────────┐
│ 环境管理层 │
│ - 只关心虚拟环境和依赖的准备 │
│ - 不关心具体的推理逻辑 │
└────────────────────────────────────────────────────┘
↓
┌────────────────────────────────────────────────────┐
│ 任务运行层 │
│ - 只关心进程的启动和生命周期 │
│ - 不关心数据如何序列化 │
└────────────────────────────────────────────────────┘
↓
┌────────────────────────────────────────────────────┐
│ 数据交换层 │
│ - 只关心高效传输数据 │
│ - 不关心数据的业务含义 │
└────────────────────────────────────────────────────┘
↓
┌────────────────────────────────────────────────────┐
│ 序列化层 │
│ - 只关心对象<->字节流的转换 │
│ - 不关心数据来自哪里 │
└────────────────────────────────────────────────────┘
好处:
- 每一层可以独立开发和测试
- 修改某一层的实现不会影响其他层
- 代码易于理解和维护
2. 接口导向设计(Interface-Oriented Design)
java
// 任务运行接口(隐藏实现细节)
public interface InferTaskRun {
void run(List<String> script);
void stop();
}
// 数据桥接接口(支持多种实现)
public interface IDataBridge<OUT> extends Closeable {
boolean write(Object... obj) throws IOException;
OUT read() throws IOException;
}
// 编解码接口(支持扩展)
public interface IEncoder {
byte[] encode(Object obj);
}
好处:
- 支持多种实现(如 HTTP 桥接、WebSocket 桥接)
- 便于单元测试(Mock 实现)
- 降低组件间的耦合度
3. 单例 + 懒加载(Singleton + Lazy Initialization)
css
┌─────────────────────────────────────────┐
│ 首次调用 InferContext 时: │
│ 1. 创建 InferEnvironmentManager 单例 │
│ 2. 初始化虚拟环境(异步,后台进行) │
│ 3. 立即返回,不阻塞用户 │
└─────────────────────────────────────────┘
↓
┌─────────────────────────────────────────┐
│ 调用 infer() 时: │
│ 1. 检查虚拟环境是否就绪 │
│ 2. 如果就绪,立即启动推理 │
│ 3. 如果未就绪,等待虚拟环境初始化 │
└─────────────────────────────────────────┘
好处:
- 减少内存占用(仅创建一个虚拟环境)
- 加快应用启动速度(虚拟环境初始化异步进行)
- 避免重复初始化的开销
2.2 核心组件关系
完整流程时序图
css
时间轴
│
├─ T0: 应用启动
│ │
│ └─→ InferContext.build(config)
│ │
│ ├─→ InferEnvironmentManager.buildInferEnvironmentManager(config)
│ │ │ (单例)
│ │ └─→ 创建虚拟环境(后台异步)
│ │
│ ├─→ DataExchangeContext.init()
│ │ │
│ │ ├─→ DataExchangeQueue("input", ...)
│ │ └─→ DataExchangeQueue("output", ...)
│ │
│ └─→ InferTaskRunImpl(environmentContext)
│
├─ T1-T100ms: 虚拟环境初始化中...
│ │
│ ├─→ InferDependencyManager.buildInferRuntimeFiles()
│ ├─→ 执行 install-infer-env.sh (Conda)
│ └─→ 写入 _finish 标记文件
│
├─ T100ms: 首次推理调用
│ │
│ └─→ InferContext.infer(features...)
│ │
│ ├─→ [检查虚拟环境状态]
│ │ 如果未就绪,等待...
│ │
│ ├─→ InferTaskRunImpl.run([python, infer_server.py, ...])
│ │ │
│ │ └─→ ProcessBuilder.start()
│ │ │
│ │ ├─→ ProcessLoggerManager.startLogging()
│ │ │ │ (后台线程捕获输出)
│ │ │ └─→ Slf4j 日志
│ │ │
│ │ └─→ Python 进程启动
│ │ │
│ │ └─→ infer_server.py
│ │ │
│ │ ├─→ 连接共享内存队列
│ │ ├─→ 加载用户 Transform 类
│ │ └─→ 等待推理请求...
│ │
│ ├─→ InferDataBridgeImpl.write(features)
│ │ │
│ │ ├─→ Pickler.encode(features)
│ │ │ │ Java对象 → Pickle字节流
│ │ │ └─→ OpCode生成
│ │ │
│ │ ├─→ InferDataWriter.write(bytes)
│ │ │ │
│ │ │ └─→ DataExchangeQueue("input").put(bytes)
│ │ │ │ (无锁,Unsafe操作)
│ │ │ └─→ 内存映射文件
│ │ │
│ │ └─→ 触发 Python 进程读取
│ │
│ ├─ [Python进程执行]
│ │ │
│ │ ├─→ 从 Queue("input") 读取字节流
│ │ ├─→ Unpickle 解序列化
│ │ ├─→ 执行用户 Transform 类
│ │ └─→ 写入结果到 Queue("output")
│ │
│ ├─→ InferDataBridgeImpl.read()
│ │ │
│ │ ├─→ InferDataReader.read()
│ │ │ │
│ │ │ └─→ DataExchangeQueue("output").get(bytes)
│ │ │ │ (无锁,轮询)
│ │ │ └─→ 内存映射文件
│ │ │
│ │ ├─→ Unpickler.decode(bytes)
│ │ │ │ Pickle字节流 → Java对象
│ │ │ └─→ 对象构造
│ │ │
│ │ └─→ 返回结果对象
│ │
│ └─→ 返回给用户
│
├─ T200ms-N: 后续推理调用
│ │
│ └─→ [复用 Python 进程,不需要重新初始化]
│ Python 进程持续监听队列...
│
└─ 应用关闭
│
└─→ InferContext.close()
│
├─→ InferTaskRunImpl.stop()
│ │
│ └─→ process.destroyForcibly()
│ │ 杀死 Python 进程
│ └─→ ProcessLoggerManager 停止日志捕获
│
└─→ DataExchangeQueue.close()
│
└─→ 释放内存映射
组件依赖关系图
scss
┌─────────────────────────────────────┐
│ InferContext (用户入口) │
│ + infer(features...) │
│ + close() │
└──────────────────┬──────────────────┘
│
┌─────────┼─────────┬──────────┐
│ │ │ │
▼ ▼ ▼ ▼
┌────────┐ ┌─────────┐ ┌───────┐ ┌──────────┐
│InferEnv│ │InferTask│ │IDataB │ │InferEnv │
│Manager │ │RunImpl │ │ridge │ │Context │
│ (单例) │ │ │ │ │ │ │
└────────┘ └─────────┘ └───────┘ └──────────┘
│ │ │
│ │ │ 持有
│ ┌────┴─────────┘
│ │
│ ▼
│ ┌──────────────────────┐
│ │ProcessLoggerManager │
│ │ (后台日志线程) │
│ └──────────────────────┘
│
▼
┌─────────────────────────┐
│InferDependencyManager │
│ (文件和脚本管理) │
└────────┬────────────────┘
│
▼
┌──────────────────────┐
│DataExchangeContext │
│ (两个共享内存队列) │
│ - Input Queue │
│ - Output Queue │
└────────┬─────────────┘
│
┌────┴────┐
▼ ▼
┌─────────┐ ┌──────────┐
│Pickler │ │Unpickler │
│(序列化) │ │(反序列化) │
└─────────┘ └──────────┘
关键数据结构关系
yaml
┌──────────────────────────────────────┐
│ InferContext (门面) │
│ - environmentManager: InferEnviron │
│ - dataContext: DataExchangeContext │
│ - taskRun: InferTaskRunImpl │
│ - dataBridge: IDataBridge<OUT> │
│ - encoder/decoder: I** │
└──────────────────────────────────────┘
┌──────────────────────────────────────┐
│ DataExchangeContext (数据上下文) │
│ - receiveQueue: DataExchangeQueue │
│ - sendQueue: DataExchangeQueue │
│ (双向通信,A->B 和 B->A) │
└──────────────────────────────────────┘
┌──────────────────────────────────────┐
│ DataExchangeQueue (共享内存) │
│ - mapAddress: long │
│ - outputAddress: long │
│ - inputNextAddress: long │
│ - [内存布局详见第6章] │
└──────────────────────────────────────┘
┌──────────────────────────────────────┐
│ InferTaskRunImpl (进程管理) │
│ - inferTask: Process │
│ - inferTaskStatus: InferTaskStatus │
│ - environmentContext: InferEnv... │
│ - virtualEnvPath: String │
└──────────────────────────────────────┘
2.3 Java-Python 进程间通信机制
通信模式选择
geaflow-infer 采用了共享内存 + 无锁队列的通信模式,而不是其他可选方案:
markdown
┌─────────────────────────────────────────────────────────────┐
│ 可选的 IPC (Inter-Process Communication) 方案 │
├─────────────────────────────────────────────────────────────┤
│ 方案 │ 延迟 │ 吞吐 │ 复杂度 │ 跨网络 │
├───────────────┼──────────┼────────┼────────┼──────────────┤
│ HTTP/REST │ 毫秒级 │ 中等 │ 低 │ 支持 │
│ Socket/RPC │ 毫秒级 │ 中等 │ 中等 │ 支持 │
│ Named Pipe │ 微秒级 │ 中等 │ 中等 │ 不支持 │
│ Shared Memory │ 微秒级 │ 高 │ 高 │ 不支持 │
│ (geaflow-infer) │ │ │ │
└─────────────────────────────────────────────────────────────┘
GeaFlow-Infer 的选择: 共享内存(同机实现)
原因:
1. 延迟敏感: 推理任务通常要求低延迟
2. 吞吐量大: 图计算可能产生大量推理请求
3. 本地通信: Java 和 Python 通常在同一台机器
4. 零拷贝: 共享内存避免数据复制
通信流程详解
Phase 1: 初始化(Init Phase)
scss
Java进程 Python进程
│ │
│ 1. 创建 2 个共享内存队列 │
├─→ Queue("input_${id}") │
├─→ Queue("output_${id}") │
│ │
│ 2. 记录 Queue ID │
│ 作为环境变量 │
│ │
│ 3. 启动 Python 进程 │
├───────────────────────────────→ 启动
│ │
│ │ 4. 读取环境变量
│ ├─→ input_queue_shm_id
│ ├─→ output_queue_shm_id
│ │
│ │ 5. 连接共享内存
│ ├─→ mmap.mmap(input_queue_shm_id)
│ ├─→ mmap.mmap(output_queue_shm_id)
│ │
│ │ 6. 加载模型,就绪
│ 7. 检测 Python 进程就绪 ←────────┤
│ │
核心数据结构:
java
// Java 侧:环境变量配置
Map<String, String> env = processBuilder.environment();
env.put("input_queue_shm_id", "input_12345"); // 队列 ID
env.put("output_queue_shm_id", "output_12345");
env.put("PYTHONPATH", "/path/to/infer");
env.put("LD_LIBRARY_PATH", "/path/to/lib");
env.put("--tfClassName", "my.custom.Transform");
// Python 侧:读取环境变量
input_queue_id = os.environ['input_queue_shm_id']
output_queue_id = os.environ['output_queue_shm_id']
input_queue = DataExchangeQueue(input_queue_id)
output_queue = DataExchangeQueue(output_queue_id)
Phase 2: 数据传输(Data Transfer Phase)
scss
Java进程 共享内存队列 Python进程
│ │ │
│ 1. 序列化输入数据 │ │
├──> Pickler.encode() │ │
│ features → bytes │ │
│ │ │
│ 2. 写入 input 队列 │ │
├──> DataExchangeQueue │ │
│ .put(bytes) │ │
│ [内存映射] │ │
│ │ notify │
│ ├──────────────────────────→ 读取 input 队列
│ │ │
│ │ │ 3. 反序列化
│ │ ├──> Unpickle
│ │ │ bytes → objects
│ │ │
│ │ │ 4. 执行推理
│ │ ├──> user_transform(obj)
│ │ │
│ │ │ 5. 序列化结果
│ │ ├──> Pickler.encode()
│ │ │ result → bytes
│ │ │
│ │ │ 6. 写入 output 队列
│ │ ←─────────────────────────┤
│ │ [内存映射] │
│ │ notify │
│ 7. 轮询 output 队列 │ │
├──> DataExchangeQueue │ │
│ .get(bytes) │ │
│ │ │
│ 8. 反序列化结果 │ │
├──> Unpickler.decode() │ │
│ bytes → objects │ │
│ │ │
│ 9. 返回结果给用户 │ │
↓ │ │
内存映射与无锁设计
内存映射(mmap)的工作原理
scss
┌─────────────────────────────────────┐
│ Linux 文件系统 │
│ /dev/shm/geaflow_infer_queue_${id} │
│ (共享内存文件,大小:通常 1MB-100MB) │
└─────────────────────────────────────┘
│ │
┌────┘ └────┐
│ │
▼ ▼
┌───────────────┐ ┌──────────────┐
│ Java进程页表 │ │ Python进程页表│
│ 虚拟地址空间 │ │ 虚拟地址空间 │
│ 0x1000-0x2000 ─────────→ 0x5000-0x6000│
│ (不同地址) │ (不同地址) │
│ ↓ │ ↓ │
│ 同一份物理内存 │ 同一份物理内存 │
│ (Page Frame 123-456) │(Page Frame │
│ │ 123-456) │
└───────────────┘ └──────────────┘
关键特性:
1. 虚拟地址不同,但指向同一物理内存
2. 修改内存的一方,另一方立即可见
3. 不需要显式的数据复制(零拷贝)
无锁队列的并发机制
ini
┌──────────────────────────────────────────────┐
│ 共享内存布局 │
├──────────────────────────────────────────────┤
│ [元数据区] │
│ - outputAddress (8 bytes) ─> 写指针 │
│ - inputNextAddress (8 bytes) ─> 读指针 │
│ - [缓存行对齐,避免 False Sharing] │
├──────────────────────────────────────────────┤
│ [数据区] │
│ [缓冲区 0] [缓冲区 1] [缓冲区 2] ... │
│ Ring Buffer 结构,大小为 2^n 对齐 │
└──────────────────────────────────────────────┘
时间轴:
Java: Python:
│ │
├─ T0: │
│ write_ptr = 0
│ 写入数据 │
│ ├─ T0.5:
│ │ read_ptr 还是 0
│ │ (看不到新数据)
│ │
├─ T1: │
│ write_ptr = 256
│ 发出内存屏障 │
│ ├─ T2:
│ │ 内存屏障保证顺序
│ │ read_ptr = 256
│ │ 读取新数据
无锁算法的核心:
- 使用
Unsafe.putOrderedLong()写指针(带内存屏障) - 使用
Unsafe.getVolatile()读指针(带内存屏障) - 避免使用
synchronized和Lock - 支持多个 reader 和多个 writer 的并发
java
// 伪代码:写操作
public void write(byte[] data) {
// 检查可用空间
long writeIndex = getWriteIndex();
long nextIndex = (writeIndex + data.length) & mask;
if (!canWrite(nextIndex)) {
throw new BufferFullException();
}
// 写数据(普通写)
for (int i = 0; i < data.length; i++) {
UNSAFE.putByte(data[i]);
}
// 更新指针(带内存屏障)
UNSAFE.putOrderedLong(this, WRITE_PTR_OFFSET, nextIndex);
}
// 伪代码:读操作
public byte[] read() {
// 读写指针(带内存屏障)
long writeIndex = UNSAFE.getVolatileLong(this, WRITE_PTR_OFFSET);
long readIndex = getReadIndex();
if (writeIndex == readIndex) {
return null; // 无数据可读
}
// 读数据(普通读)
byte[] result = new byte[getDataLength()];
for (int i = 0; i < result.length; i++) {
result[i] = UNSAFE.getByte(address + readIndex + i);
}
// 更新指针(带内存屏障)
UNSAFE.putOrderedLong(this, READ_PTR_OFFSET,
(readIndex + result.length) & mask);
return result;
}
通信性能分析
markdown
延迟分解(End-to-End Latency):
总延迟 = Java序列化 + 内存写入 + 上下文切换 + Python反序列化
+ 推理执行 + 结果序列化 + 内存读取 + Java反序列化
≈ 1μs (序列化) + 0.1μs (mmap 写) + ~100μs (上下文切换)
+ 1μs (反序列化) + 推理时间 + ...
吞吐量分析:
- 环形缓冲区大小: 通常 1MB - 100MB
- 单次推理数据: 通常 KB 级别
- 吞吐量: 可以达到 Gbps 级别(理论上)
对比 HTTP/RPC:
HTTP: 毫秒级 (ms) = 1000μs
共享内存: 微秒级 (μs)
性能提升: 1000 倍