4、数据采集架构设计:采集层、处理层、存储层三层架构、数据流管道设计、高可用与容错机制
聊到高频行情数据采集,很多人第一反应就是“快”。没错,延迟确实重要。但我在这个行业摸爬滚打这么多年,发现真正让系统稳定跑下去的,其实是架构。
你想想看,行情数据每秒几万笔甚至几十万笔涌进来。如果架构设计得不好,哪怕单机性能再强,早晚也会出问题。我个人习惯把整个采集系统拆成三层:采集层、处理层、存储层。每一层各司其职,互不干扰。
4.1 三层架构:各层职责与边界
先说说这三层到底干什么。
- 采集层:负责从交易所网关接收原始行情。说白了就是“接数据”。这一层只做一件事——把网络上的二进制流收下来,丢进内存队列。不做任何加工。
- 处理层:负责清洗、校验、重组、计算。比如把乱序的tick排好序,把缺失的字段补上,或者算个中间价。这一层是“加工车间”。
- 存储层:负责把处理好的数据持久化。可以是时序数据库,也可以是文件系统。这一层只关心“怎么写最快、怎么读最方便”。
我在项目中遇到过一种情况:有人把数据清洗逻辑直接写在采集层里。结果行情爆发时,采集线程卡在清洗上,导致丢包。嗯,这里要注意——每一层只做自己的事,千万别越界。
核心原则:采集层不处理,处理层不存储,存储层不采集。各层之间通过队列或管道解耦。
4.2 数据流管道设计
三层之间怎么传数据?这就是管道设计要解决的问题。
我个人比较喜欢用无锁环形队列做层间通信。为什么?因为高频场景下,加锁的成本太高了。你想想看,每秒几十万次入队出队,如果每次都要抢锁,CPU时间全耗在等待上了。
下面是一个简化版的管道设计图,我用SVG画了一下核心流程:
实际代码中,管道设计大概长这样:
// 伪代码:采集层 -> 处理层 的无锁队列传递
struct TickData {
uint64_t timestamp;
double price;
uint64_t volume;
char symbol[8];
};
// 无锁环形队列(简化版)
LockFreeQueue<TickData, 65536> raw_queue;
// 采集线程
void collector_thread() {
while (running) {
TickData tick = recv_from_exchange();
raw_queue.push(tick); // 入队,不阻塞
}
}
// 处理线程
void processor_thread() {
while (running) {
TickData tick;
if (raw_queue.pop(tick)) { // 出队
process_tick(tick); // 清洗、计算
send_to_storage(tick);
}
}
}
经验之谈:队列大小要设得够大。我曾经因为队列设小了,行情爆发时直接写爆,数据全丢了。建议至少设到65536个槽位,每个槽位存一个tick。
4.3 高可用与容错机制
做高频交易最怕什么?怕系统挂了,行情断了,然后你还在傻傻地等数据。容错不是锦上添花,是保命用的。
我总结了几条必须做的容错策略:
- 采集层双链路热备:同时连接两个交易所网关。主链路断了,备用链路秒级接管。注意,两个链路的数据要能去重,不然同一笔tick处理两次就麻烦了。
- 处理层进程级主备:主进程挂了,备用进程立刻顶上。状态怎么同步?通过共享内存或者Redis保存最近1000笔tick的校验和。备机启动时先校验,再继续处理。
- 存储层多副本:数据同时写到两个不同的磁盘,或者两个不同的节点。我用的是Raft协议做副本同步,强一致性,但代价是写入延迟会高一点点。如果对延迟极度敏感,可以改成异步复制。
- 全链路心跳检测:每层之间每秒发一次心跳。连续3次没收到,就触发告警并自动切换。
避坑指南:我曾经遇到过一种情况——主备切换时,备用进程启动后发现自己落后了500毫秒的数据。这500毫秒的行情如果丢了,策略可能做出错误决策。后来我加了一个“追赶模式”:备用进程先快速回放最近1秒的日志,追上进度后再切为主。
下面是一个容错切换的伪代码示例:
// 主备切换逻辑
void health_check() {
if (missed_heartbeat_count >= 3) {
// 主节点挂了,触发切换
if (role == BACKUP) {
// 先追赶数据
catch_up_from_log();
// 再切换角色
role = PRIMARY;
start_processing();
}
}
}
// 数据追赶:从本地日志回放
void catch_up_from_log() {
uint64_t last_seq = get_last_processed_seq();
LogEntry entry;
while (log_reader.read_next(&entry)) {
if (entry.seq > last_seq) {
process_tick(entry.tick);
}
}
}
4.4 架构总结
说了这么多,其实核心就几句话:
| 层级 | 核心职责 | 容错方式 | 性能关键点 |
|---|---|---|---|
| 采集层 | 接收原始行情 | 双链路热备 | 零拷贝、低延迟网络 |
| 处理层 | 清洗、校验、计算 | 进程级主备切换 | 无锁队列、批量处理 |
| 存储层 | 持久化存储 | 多副本 + 异步复制 | 顺序写入、列式存储 |
说白了,架构设计就是“把复杂问题拆简单”。每一层只做一件事,层与层之间用管道解耦。容错不是事后补救,而是设计时就考虑进去。嗯,这套架构我在多个项目里验证过,每秒处理50万笔tick没什么压力。
一句话总结:采集层接得住,处理层算得快,存储层存得稳,容错机制兜得住底。这就是一套靠谱的高频行情采集架构。