Apache Geaflow推理框架Geaflow-infer 解析系列(六)共享内存架构

第6章:共享内存架构

章节导读

如果说环境初始化是"准备舞台",那么共享内存就是"舞台的核心"。Java 和 Python 进程间的高性能通信完全依赖于 DataExchangeQueue 的精妙设计。

本章将深入讲解:

  • mmap 内存映射的原理
  • 环形缓冲区的设计
  • 无锁算法的实现
  • Unsafe 底层操作

通过本章,你将理解为什么 geaflow-infer 能达到微秒级延迟。

核心设计

markdown 复制代码
问题: 如何在毫秒级延迟和通用性之间权衡?

答案: DataExchangeQueue
  ✓ 以微秒级延迟换取本机限制
  ✓ 以复杂的无锁实现换取高性能
  ✓ 以 Unsafe 危险性换取性能优势
  ✓ 这些权衡在推理场景中是值得的

最终结果:
  - 延迟: 微秒级 (vs RPC 毫秒级)
  - 吞吐: Gbps 级别
  - 复杂度: 高,但被隔离在这个模块
  - 可维护性: 通过接口隔离,易于理解

6.1 DataExchangeQueue 核心设计

设计目标

markdown 复制代码
问题: Java 和 Python 进程如何高效传输大量数据?

候选方案对比:
  方案 1: 文件系统
    ├─ 优点: 简单、可靠
    └─ 缺点: 延迟高 (毫秒级)、吞吐量受限

  方案 2: 网络 Socket
    ├─ 优点: 支持远程
    └─ 缺点: 延迟高 (毫秒级)、有网络开销

  方案 3: 消息队列 (如 Redis)
    ├─ 优点: 功能丰富
    └─ 缺点: 延迟中等 (微秒级)、有序列化开销

  方案 4: 共享内存 (DataExchangeQueue)
    ├─ 优点: 延迟极低 (微秒级)、零拷贝、吞吐量高
    └─ 缺点: 仅限本机、需要复杂的并发控制

GeaFlow-Infer 的选择: 方案 4
原因: 推理通常在本机,对延迟敏感,需要高吞吐量

内存映射 (mmap) 的基础

scss 复制代码
┌──────────────────────────────────────────────────┐
│  传统的进程间通信 (IPC)                           │
├──────────────────────────────────────────────────┤
│ Java 进程              系统内核              Python 进程│
│    │                    │                         │
│    ├─ 写数据            │                         │
│    │  (系统调用)        │                         │
│    │   ↓                │                         │
│    │  [数据在内核缓冲] ←─→ [数据在内核缓冲]        │
│    │   ↓                │   ↓                     │
│    │  (系统调用)        │ (系统调用)              │
│    │                    │   ↓                     │
│    │                    │ 读数据                  │
│    │                    │   ↓                     │
│ 缺点: 多次数据拷贝,延迟高              结果进程接收│
└──────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────┐
│  基于 mmap 的共享内存通信                        │
├──────────────────────────────────────────────────┤
│ Java 进程              Linux 内核              Python 进程│
│    │                      │                        │
│    │ 1. mmap() 系统调用   │                        │
│    ├─→ 请求内存映射       │                        │
│    │      ↓               │                        │
│    │ 2. 返回虚拟地址      │                        │
│    │      ↓               │   3. mmap() 系统调用    │
│    │  获得虚拟地址 A ─────→ ←─ 获得虚拟地址 B      │
│    │  (不同地址,           │   (不同地址,           │
│    │   指向同一物理内存)    │   指向同一物理内存)    │
│    │                      │                        │
│    │ 4. 直接写内存        │                        │
│    ├─ *(void*)A = data   │ ← 直接读内存            │
│    │                      │    data = *(void*)B   │
│    │  (无系统调用,无拷贝!)│   (无系统调用,无拷贝!) │
│    │                      │                        │
│ 优点: 零拷贝、延迟微秒级、吞吐量高                    │
└──────────────────────────────────────────────────┘

内存映射的工作原理

ini 复制代码
Step 1: 创建共享内存文件

  Linux 文件系统
  /dev/shm/geaflow_queue_input
  (大小: 1MB, 内容: 全 0)

Step 2: Java 进程映射文件

  RandomAccessFile raf = new RandomAccessFile(file, "rw");
  FileChannel channel = raf.getChannel();
  MappedByteBuffer mbb = channel.map(
      MapMode.READ_WRITE, 0, file.length());
  
  结果:
    Java 虚拟地址空间: 0x1000 - 0x2000
    ↓
    页表 (Page Table)
    ↓
    物理内存: Frame 100-200
    ↓
    /dev/shm/geaflow_queue_input

Step 3: Python 进程映射文件

  import mmap
  with open('/dev/shm/geaflow_queue_input', 'r+b') as f:
      mm = mmap.mmap(f.fileno(), 0)
      # 或直接使用 mm[offset:offset+length]
  
  结果:
    Python 虚拟地址空间: 0x5000 - 0x6000
    ↓
    页表 (Page Table)
    ↓
    物理内存: Frame 100-200 (同一份!)

Step 4: 数据交换 (零拷贝!)

  Java:                  Python:
  mbb.put(index, data)  ← 直接写内存
                         data = mm[index]
                         (直接读取, 看到 Java 写入的值)

6.2 无锁队列实现原理

Ring Buffer 设计

ini 复制代码
┌────────────────────────────────────────┐
│    环形缓冲区 (Ring Buffer)             │
│    大小: 2^N = 1MB (对齐)              │
├────────────────────────────────────────┤
│                                        │
│  [写入数据1]                           │
│  [写入数据2]      ← 写指针指向这里      │
│  [写入数据3]                           │
│  [写入数据4]                           │
│  ...                                   │
│  [读取完毕的数据]  ← 读指针指向这里      │
│  [可写入的空间]                         │
│                                        │
│ 大小=2^20, 掩码=0xFFFFF                │
│ 指针值: 0 ~ 2^30 (不清零)             │
│ 实际地址: ptr & 掩码                   │
│                                        │
└────────────────────────────────────────┘

示例:
  queue size = 2^10 = 1024 bytes
  mask = 0x3FF = 1023
  
  write_ptr = 100,  实际地址 = 100 & 0x3FF = 100
  write_ptr = 1100, 实际地址 = 1100 & 0x3FF = 76  (绕一圈)
  write_ptr = 2100, 实际地址 = 2100 & 0x3FF = 52  (绕两圈)
  
  优点: 无需清零指针,&mask 自动处理环绕

无锁并发的关键

arduino 复制代码
问题: 多个 Writer 和 Reader 同时访问队列

┌──────────────────────────────────────┐
│  Writer Thread 1    Writer Thread 2   │
│       │                    │          │
│       └────→ Queue ←───────┘          │
│              │                        │
│              │                        │
│       ┌──────┘                        │
│       ↓                               │
│   Reader Thread                       │
│                                       │
│  不能用 Lock (synchronized)           │
│  原因:                                 │
│   - 太慢 (原子操作之间可能被中断)     │
│   - Java 锁只保证 JVM 内同步          │
│   - Python 的读取无法被保护          │
│                                       │
│  解决方案: 无锁算法                    │
│   - 使用原子操作 (CAS)                │
│   - 使用内存屏障                      │
│   - 利用 CPU 指令级别的原子性         │
└──────────────────────────────────────┘

内存屏障的作用

ini 复制代码
情景: Writer 写数据,Reader 读数据

没有内存屏障:
  Writer                      Reader
    │                          │
    ├─ write data[0]          │
    ├─ write data[1]          │
    ├─ write_ptr = 2 ✗        │
    │                         ├─ read_ptr ✓
    │ (reorder!)              ├─ read data[0]
    │ 实际执行顺序:           ├─ read data[1]
    │ 1. write_ptr = 2        │
    │ 2. write data[0]        │ 问题: Reader 看到
    │ 3. write data[1]        │ write_ptr=2 但数据还没写!

使用内存屏障:
  Writer                      Reader
    │                          │
    ├─ write data[0]          │
    ├─ write data[1]          │
    ├─ [内存屏障]             │
    │  ~~forcedFlush~~        │
    ├─ write_ptr = 2 ✓        │
    │                         ├─ [内存屏障]
    │                         ├─ read_ptr ✓
    │  顺序保证:              ├─ read data[0]
    │  1. write data[0]       ├─ read data[1]
    │  2. write data[1]       │
    │  3. 屏障完成            │ ✓ 正确: Reader 先等待
    │  4. write_ptr = 2       │       再读数据

Java Unsafe 中的方法:
  putOrderedLong()   ← Writer 使用,写屏障
  getVolatileLong()  ← Reader 使用,读屏障

6.3 内存布局与地址管理

共享内存的完整布局

scss 复制代码
┌─────────────────────────────────────────────────────┐
│   DataExchangeQueue 的内存布局                      │
├─────────────────────────────────────────────────────┤
│ [Cache Line 64字节]                                 │
│  ┌─────────────────────────────────┐               │
│  │ startPointAddress (8 bytes)      │ ← 标记开始   │
│  │ [填充到 Cache Line 边界]         │              │
│  └─────────────────────────────────┘               │
│                                                     │
│ [Cache Line 64字节]                                 │
│  ┌─────────────────────────────────┐               │
│  │ capacityAddress (8 bytes)        │ ← 队列大小   │
│  │ outputAddress (8 bytes)          │ ← 写指针    │
│  │ [填充到 Cache Line 边界]         │              │
│  └─────────────────────────────────┘               │
│                                                     │
│ [Cache Line 64字节]                                 │
│  ┌─────────────────────────────────┐               │
│  │ inputNextAddress (8 bytes)       │ ← 下次读    │
│  │ outputNextAddress (8 bytes)      │ ← 下次写    │
│  │ [填充到 Cache Line 边界]         │              │
│  └─────────────────────────────────┘               │
│                                                     │
│ [Cache Line 64字节]                                 │
│  ┌─────────────────────────────────┐               │
│  │ endPointAddress (8 bytes)        │ ← 标记结尾   │
│  │ [填充到 Cache Line 边界]         │              │
│  └─────────────────────────────────┘               │
│                                                     │
│ [Cache Line 64字节]                                 │
│  ┌─────────────────────────────────┐               │
│  │ barrierAddress (8 bytes)         │ ← 屏障      │
│  │ [填充到 Cache Line 边界]         │              │
│  └─────────────────────────────────┘               │
│                                                     │
│ [Cache Line 64字节]                                 │
│  ┌─────────────────────────────────┐               │
│  │ currentBufferAddress (8 bytes)   │ ← 当前缓冲   │
│  │ [填充到 Cache Line 边界]         │              │
│  └─────────────────────────────────┘               │
│                                                     │
│ [环形数据缓冲区] (实际大小: 通常 1MB)              │
│  ┌─────────────────────────────────────────────┐  │
│  │ [数据1] [数据2] [数据3] ... [数据N]         │  │
│  │ ^ read_ptr                  ^ write_ptr      │  │
│  └─────────────────────────────────────────────┘  │
│                                                     │
└─────────────────────────────────────────────────────┘

设计亮点:
  1. Cache Line 对齐 (64 字节)
     原因: 避免 False Sharing
     什么是 False Sharing?
       Writer Thread 在 CPU 0 上修改 write_ptr
       Reader Thread 在 CPU 1 上读 read_ptr
       如果两个指针在同一 Cache Line,
       CPU 0 的缓存失效会导致 CPU 1 的缓存也失效 (通过 Cache Coherency)
       使用 Cache Line 对齐,确保指针在不同 Line,避免这个开销
  
  2. 指针不清零
     原因: 简化逻辑
     write_ptr 可以不断增长: 0 → 1MB → 2MB → ...
     实际地址: ptr & mask (自动绕回)
     不需要检查"是否需要清零"
  
  3. 多个缓冲区地址
     原因: 支持多生产者-多消费者
     outputAddress: 下一个要写入的位置 (Writer 使用)
     inputNextAddress: 下一个要读取的位置 (Reader 使用)

地址计算

java 复制代码
public final class DataExchangeQueue {
    
    // 初始化时计算各地址
    public DataExchangeQueue(String mapKey, int capacity, 
        boolean reset) {
        
        // 1. 获取内存映射
        this.memoryMapper = new MemoryMapper(mapKey, bufferCapacity);
        this.mapAddress = memoryMapper.getMapAddress();
        
        // 2. 对齐到 Cache Line
        this.initialRawAddress = Pow2.align(mapAddress, 
            PortableJvmInfo.CACHE_LINE_SIZE);  // 通常 64 字节
        
        // 3. 计算各个字段的地址
        this.startPointAddress = initialRawAddress;
        this.capacityAddress = startPointAddress 
            + PortableJvmInfo.CACHE_LINE_SIZE;
        this.outputAddress = startPointAddress 
            + 2L * PortableJvmInfo.CACHE_LINE_SIZE;
        this.inputNextAddress = outputAddress + 8;  // 同一 cache line
        this.outputNextAddress = startPointAddress + 8;  // 另一 line
        this.endPointAddress = outputAddress 
            + PortableJvmInfo.CACHE_LINE_SIZE;
        this.barrierAddress = endPointAddress + 8;
        this.currentBufferAddress = barrierAddress + 8;
        
        // 4. 初始化队列
        if (reset) {
            reset();
        }
    }
    
    /**
     * 初始化队列状态
     */
    private void reset() {
        UNSAFE.putOrderedLong(null, startPointAddress, 0);
        UNSAFE.putOrderedLong(null, capacityAddress, queueCapacity);
        UNSAFE.putOrderedLong(null, outputAddress, 0);
        UNSAFE.putOrderedLong(null, inputNextAddress, 0);
        UNSAFE.putOrderedLong(null, outputNextAddress, 0);
    }
}

6.4 Unsafe 底层操作详解

Unsafe 是什么?

objectivec 复制代码
sun.misc.Unsafe 是 Java 提供的一个"后门" API
  ├─ 直接操纵内存 (绕过 GC, 安全检查)
  ├─ 提供原子操作 (CAS, volatile)
  ├─ 访问任意对象的任意字段
  └─ 原本用于 JVM 内部实现,现在被开发者广泛使用

Unsafe 的危险性:
  ⚠️  可能导致 JVM 崩溃
  ⚠️  破坏 Java 的内存安全
  ⚠️  代码难以维护
  ⚠️  Java 9+ 开始限制使用

但是,对于 geaflow-infer:
  ✓ 需要微秒级延迟,无法使用 Java Lock
  ✓ 需要跨进程通信,Unsafe 提供底层能力
  ✓ 开发者理解风险,谨慎使用

核心 Unsafe 操作

java 复制代码
// 1. 获取 Unsafe 实例 (反射)
Field field = sun.misc.Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
Unsafe UNSAFE = (Unsafe) field.get(null);

// 2. 写操作 (带内存屏障)
UNSAFE.putOrderedLong(object, offset, value);
// 相当于: *(long*)(object + offset) = value
// 带屏障: 确保写操作后的所有内存操作完成

// 3. 读操作 (带内存屏障)
long value = UNSAFE.getVolatileLong(object, offset);
// 相当于: return *(long*)(object + offset)
// 带屏障: 确保读操作前的所有内存操作完成

// 4. 写字节
UNSAFE.putByte(address, value);
UNSAFE.putByte(object, offset, value);

// 5. 读字节
byte value = UNSAFE.getByte(address);
byte value = UNSAFE.getByte(object, offset);

// 6. 比较并交换 (CAS - 无锁的基础)
boolean success = UNSAFE.compareAndSwapLong(
    object,       // 对象
    offset,       // 字段偏移
    expect,       // 期望值
    update        // 新值
);
// 原子操作: 如果 *(object+offset) == expect,
//          则设置为 update,返回 true
//          否则返回 false

实际使用示例

java 复制代码
// 来自 DataExchangeQueue 的实际代码

public class DataExchangeQueue {
    
    private static final Unsafe UNSAFE;
    private static final long OUTPUT_ADDRESS_OFFSET;
    private static final long INPUT_NEXT_ADDRESS_OFFSET;
    
    static {
        try {
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            UNSAFE = (Unsafe) field.get(null);
            
            // 计算字段偏移 (用于后续 putOrderedLong/getVolatileLong)
            OUTPUT_ADDRESS_OFFSET = UNSAFE.objectFieldOffset(
                DataExchangeQueue.class.getDeclaredField("outputAddress")
            );
            INPUT_NEXT_ADDRESS_OFFSET = UNSAFE.objectFieldOffset(
                DataExchangeQueue.class.getDeclaredField("inputNextAddress")
            );
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
    
    /**
     * 写入数据
     */
    public void put(byte[] data) {
        long writeIndex = UNSAFE.getVolatileLong(this, 
            OUTPUT_ADDRESS_OFFSET);
        long nextIndex = writeIndex + data.length;
        
        // 检查是否越界
        if ((nextIndex & ~mask) != (writeIndex & ~mask)) {
            throw new BufferFullException();
        }
        
        // 写数据到共享内存
        for (int i = 0; i < data.length; i++) {
            long address = initialRawAddress + (writeIndex + i) & mask;
            UNSAFE.putByte(address, data[i]);
        }
        
        // 更新写指针 (带屏障,其他进程能看到)
        UNSAFE.putOrderedLong(this, OUTPUT_ADDRESS_OFFSET, nextIndex);
    }
    
    /**
     * 读取数据
     */
    public byte[] get() {
        // 读写指针 (带屏障)
        long writeIndex = UNSAFE.getVolatileLong(this, 
            OUTPUT_ADDRESS_OFFSET);
        long readIndex = UNSAFE.getVolatileLong(this, 
            INPUT_NEXT_ADDRESS_OFFSET);
        
        if (writeIndex == readIndex) {
            return null;  // 无数据
        }
        
        // 获取数据长度 (存储在数据前 4 字节)
        long lenAddress = initialRawAddress + (readIndex & mask);
        int len = UNSAFE.getInt(lenAddress);
        
        // 读数据
        byte[] data = new byte[len];
        for (int i = 0; i < len; i++) {
            long address = initialRawAddress + 
                ((readIndex + i + 4) & mask);
            data[i] = UNSAFE.getByte(address);
        }
        
        // 更新读指针 (带屏障)
        UNSAFE.putOrderedLong(this, INPUT_NEXT_ADDRESS_OFFSET,
            readIndex + len + 4);
        
        return data;
    }
}

Unsafe 的性能特性

erlang 复制代码
操作延迟对比:

操作               延迟 (纳秒)
─────────────────────────────
普通字段访问         1-10 ns
Unsafe.getByte()    5-20 ns
Unsafe.getLong()    10-30 ns
getVolatileLong()   20-50 ns (带内存屏障)
putOrderedLong()    20-50 ns (带内存屏障)
synchronized Lock   100-500 ns
Lock (java.util.concurrent) 200-1000 ns

结论: 即使加上内存屏障,Unsafe 操作仍然比 Lock 快 10 倍以上

参考资源

相关推荐
期待のcode3 小时前
MyBatisX插件
java·数据库·后端·mybatis·springboot
华仔啊6 小时前
这 10 个 MySQL 高级用法,让你的代码又快又好看
后端·mysql
码事漫谈7 小时前
国产时序数据库崛起:金仓凭什么在复杂场景中碾压InfluxDB
后端
上进小菜猪7 小时前
当时序数据不再“只是时间”:金仓数据库如何在复杂场景中拉开与 InfluxDB 的差距
后端
盖世英雄酱581368 小时前
springboot 项目 从jdk 8 升级到jdk21 会面临哪些问题
java·后端
程序猿DD8 小时前
JUnit 5 中的 @ClassTemplate 实战指南
java·后端
Victor3569 小时前
Netty(14)如何处理Netty中的异常和错误?
后端
Victor3569 小时前
Netty(13)Netty中的事件和回调机制
后端
码事漫谈10 小时前
VS Code 1.107 更新:多智能体协同与开发体验升级
后端