第四章:软件架构优化
做市系统的延迟优化,说白了就是跟时间赛跑。硬件加速能帮你把纳秒级的时间抠出来,但软件架构要是拖后腿,再快的硬件也白搭。这一章我重点聊聊三个方向:事件驱动架构、无锁队列设计、内存池与对象池。这些都是我在实际项目中反复踩坑、反复优化的经验。
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、火焰图)找到瓶颈,再针对性地优化。我曾经花了两周优化一个非关键路径,结果发现主路径的锁才是罪魁祸首——白忙活了。