Apache Geaflow推理框架Geaflow-infer 解析系列(二)整体架构设计

第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
   │             │ 读取新数据

无锁算法的核心

  1. 使用 Unsafe.putOrderedLong() 写指针(带内存屏障)
  2. 使用 Unsafe.getVolatile() 读指针(带内存屏障)
  3. 避免使用 synchronizedLock
  4. 支持多个 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 倍

参考资源

相关推荐
鹏北海2 小时前
多标签页登录状态同步:一个简单而有效的解决方案
前端·面试·架构
Xの哲學2 小时前
Linux 分区表深度技术剖析
linux·网络·算法·架构·边缘计算
TracyCoder1233 小时前
微服务概念理解学习笔记
学习·微服务·架构
小璞3 小时前
六、React 并发模式:让应用"感觉"更快的架构智慧
前端·react.js·架构
ALex_zry3 小时前
高并发系统渐进式改造技术调研报告:策略、架构与实战
java·运维·架构
木易 士心4 小时前
WebSocket 与 MQTT 在即时通讯中的深度对比与架构选型指南
websocket·网络协议·架构
Tadas-Gao4 小时前
Spring Boot 4.0架构革新:构建更精简、更安全、更高效的Java应用
java·spring boot·分布式·微服务·云原生·架构·系统架构
BG8EQB5 小时前
开发者的存储救赎计划:从SQL到云原生的架构演进
sql·云原生·架构
Leinwin5 小时前
微软发布全新一代 Arm 架构云原生处理器 Cobalt 200
arm开发·microsoft·架构