第四章:软件架构优化

做市系统的延迟优化,说白了就是跟时间赛跑。硬件加速能帮你把纳秒级的时间抠出来,但软件架构要是拖后腿,再快的硬件也白搭。这一章我重点聊聊三个方向:事件驱动架构、无锁队列设计、内存池与对象池。这些都是我在实际项目中反复踩坑、反复优化的经验。

4.1 事件驱动架构:别让CPU空转

做市系统里,行情来了、订单成交了、风控触发了……这些事件随时可能发生。传统的轮询方式,CPU得不停地问「有事吗?有事吗?」——这效率太低了。

我个人习惯用事件驱动架构。说白了,就是让系统「被动响应」:事件来了,才干活;没事件,就歇着。这样CPU利用率高,延迟也低。

核心思路:事件源(行情、订单、风控)→ 事件分发器 → 事件处理器(策略、风控、日志)

我在项目中遇到过一个问题:事件分发器成了瓶颈。所有事件都走同一个队列,结果行情密集时,订单事件被堵在后面。后来我改成多优先级队列——行情事件走高速通道,订单事件走普通通道。嗯,效果立竿见影。

行情事件 订单事件 风控事件 事件分发器 多优先级队列 策略引擎 风控模块 日志模块 订单管理 事件驱动架构示意图

4.2 无锁队列设计:别让锁成为绊脚石

做市系统里,多个线程同时读写队列是家常便饭。用锁保护队列?简单,但代价高。锁会导致线程阻塞、上下文切换,延迟一下子就上去了。

我建议用无锁队列。它的核心思想是利用CAS(Compare-And-Swap)原子操作,让多个线程同时操作队列而不冲突。

小技巧:无锁队列的典型实现是「环形缓冲区」。生产者写尾部,消费者读头部,只要保证读写指针不重叠就行。

我曾经在一个项目里,用锁队列时延迟在5微秒左右。换成无锁队列后,直接降到500纳秒。你想想看,差了10倍!

// 无锁队列核心代码示例(伪代码)
struct LockFreeQueue {
    atomic<uint32_t> head;  // 读指针
    atomic<uint32_t> tail;  // 写指针
    T buffer[MAX_SIZE];

    bool push(const T& item) {
        uint32_t t = tail.load();
        uint32_t next_t = (t + 1) % MAX_SIZE;
        if (next_t == head.load()) return false; // 队列满
        buffer[t] = item;
        tail.store(next_t);
        return true;
    }

    bool pop(T& item) {
        uint32_t h = head.load();
        if (h == tail.load()) return false; // 队列空
        item = buffer[h];
        head.store((h + 1) % MAX_SIZE);
        return true;
    }
};

注意:无锁队列不是万能的。如果队列经常满或空,CAS自旋会浪费CPU。我建议配合「批量处理」使用——一次push/pop多个元素,减少CAS次数。

4.3 内存池与对象池:告别动态分配

做市系统里,行情数据、订单对象、事件对象……这些对象频繁创建和销毁。如果用new/delete或malloc/free,内存碎片和分配延迟会让你崩溃。

我的做法是:预分配一大块内存,自己管理。这就是内存池和对象池。

特性 内存池 对象池
管理粒度 原始内存块 预构造的对象
适用场景 大小固定的数据包 频繁创建/销毁的对象
延迟优势 分配时间O(1) 避免构造/析构开销
典型实现 空闲链表 对象回收队列

我记得有一次,系统里每笔订单都要new一个对象,结果延迟抖动特别大。换成对象池后,延迟稳定在1微秒以内。嗯,这就是预分配的好处。

避坑指南:我曾经犯过一个错——对象池里的对象没有重置状态。第二次使用时,残留数据导致订单价格算错了。后来我强制要求:对象出池前必须调用reset()方法。

// 对象池核心代码示例
template<typename T>
class ObjectPool {
    std::vector<T*> pool;
    std::atomic<uint32_t> index;

public:
    ObjectPool(size_t size) : index(0) {
        for (size_t i = 0; i < size; ++i) {
            pool.push_back(new T());
        }
    }

    T* acquire() {
        uint32_t idx = index.fetch_add(1);
        if (idx >= pool.size()) return nullptr; // 池耗尽
        pool[idx]->reset(); // 重置状态
        return pool[idx];
    }

    void release(T* obj) {
        // 对象池不真正释放,只是标记可用
        // 实际项目中可以用回收队列
    }
};

4.4 三者如何协同?

事件驱动架构是骨架,无锁队列是血管,内存池是细胞。它们配合起来,才能构建一个低延迟的做市系统。

  • 事件驱动:决定什么时候干活
  • 无锁队列:决定怎么传递数据
  • 内存池:决定怎么分配资源

我个人习惯的流程是:行情事件通过无锁队列进入策略引擎,策略引擎从对象池获取订单对象,计算后通过另一个无锁队列发送到交易所接口。整个过程没有锁、没有动态分配,延迟可控。

最后一个小建议:优化软件架构时,别一上来就追求极致。先用性能分析工具(比如perf、火焰图)找到瓶颈,再针对性地优化。我曾经花了两周优化一个非关键路径,结果发现主路径的锁才是罪魁祸首——白忙活了。

专注资料整理