3、数据采集层架构设计:多源异构数据接入
做市业务里,数据采集层是整个监控系统的「眼睛」和「耳朵」。
我见过不少团队,花了大把精力做告警规则、做可视化大屏,结果数据采集层一塌糊涂——行情延迟几毫秒、订单漏报、成交时间戳对不上。说白了,上层做得再花哨,底层数据不准,一切都是白搭。
这一节,我们聊聊数据采集层的核心设计思路。
3.1 多源异构数据接入
做市系统要接入的数据源,少说也有四五个:
- 行情数据:来自交易所的深度快照、逐笔成交、Tick 级更新
- 订单数据:自研交易系统内部的订单状态变更(新单、部分成交、完全成交、撤单)
- 成交数据:交易所返回的成交回报,包含成交价、量、时间
- 风控数据:资金余额、持仓限制、净头寸、风险指标
每种数据的格式、频率、传输协议都不一样。行情可能是 UDP 组播,订单是内部 RPC,风控是数据库轮询。怎么统一接入?
我个人习惯的做法是:抽象一个「数据源适配器」层。
// 伪代码:数据源适配器接口
interface DataSourceAdapter {
fun connect(): Boolean
fun subscribe(topic: String): Flow<DataEvent>
fun disconnect()
}
// 行情适配器实现
class MarketDataAdapter : DataSourceAdapter {
override fun subscribe(topic: String): Flow<DataEvent> {
// 连接交易所行情网关,解析二进制协议
// 转换为统一的事件格式
}
}
// 订单适配器实现
class OrderDataAdapter : DataSourceAdapter {
override fun subscribe(topic: String): Flow<DataEvent> {
// 监听内部订单总线,提取订单状态变更
}
}
每个适配器只负责一件事:把原始数据转换成统一的 DataEvent 结构。这样上层处理逻辑就不用关心数据从哪来、怎么来的。
核心原则:数据接入层只做「格式转换」和「透传」,不做任何业务逻辑判断。业务逻辑交给上层处理层。
3.2 高吞吐日志采集方案
做市系统每秒产生的日志量有多大?我举个例子:
一个中等规模的做市商,同时做 50 个币对,每个币对每秒产生 10 次行情更新、5 次订单变更、3 次成交回报。算下来每秒至少 900 条日志。如果遇到极端行情,这个数字能翻 10 倍。
传统的日志采集方案(比如直接写文件 + Filebeat)在这种场景下很容易出问题。我遇到过 Filebeat 追不上日志写入速度,导致日志堆积、内存爆掉的情况。
我的建议是:分层缓冲 + 异步批量写入。
| 层级 | 组件 | 作用 |
|---|---|---|
| 第一层 | 内存环形缓冲区(RingBuffer) | 零分配、无锁写入,扛住瞬时高峰 |
| 第二层 | 本地磁盘队列(如 Kafka 或 RocksDB) | 持久化缓冲,防止进程崩溃丢数据 |
| 第三层 | 远程采集代理(如 Fluentd/Logstash) | 批量压缩、传输到中央存储 |
具体实现上,我推荐用 LMAX Disruptor 做第一层环形缓冲区。它的吞吐量能达到每秒百万级,延迟在微秒级别。第二层用 Kafka 的本地日志存储,既当缓冲又当持久化。
避坑指南:我曾经把日志直接写到 Kafka 主题里,结果 Kafka 客户端在高并发下频繁触发 GC,导致整个应用卡顿。后来改成「先写本地文件,再异步发送到 Kafka」,问题就解决了。
3.3 时间戳精度保障
时间戳是做市监控里最容易被忽视、却又最关键的一环。
你想想看:行情延迟 1 毫秒,订单执行时间差 2 毫秒,风控指标算出来就完全不对了。更严重的是,如果不同数据源的时间戳基准不一致,你根本没法做关联分析。
我遇到过最坑的一次:行情服务器用的是 NTP 同步,订单服务器用的是 PTP 同步,两者差了 50 毫秒。排查了整整两天才发现是时间戳的问题。
怎么保障时间戳精度?我总结了三个要点:
- 统一时间源:所有服务器必须使用同一个时间同步协议。做市业务建议用 PTP(精确时间协议),精度能达到微秒级。如果条件不允许,至少用 NTP 并配置多个可靠的时间源。
- 打点时机要统一:数据采集时,时间戳应该在「数据到达采集层」的那一刻打上,而不是在「数据被处理完」之后。很多团队犯的错误是:在日志写入磁盘时才打时间戳,这已经晚了。
- 记录原始时间戳:除了采集时间戳,还要保留数据源自带的原始时间戳。比如交易所行情里自带的交易所时间。这样即使采集时间有偏差,也能通过原始时间戳做校准。
// 时间戳打点示例
data class DataEvent(
val source: String, // 数据来源:行情/订单/成交/风控
val rawTimestamp: Long, // 数据源自带的原始时间戳(交易所时间)
val collectTimestamp: Long, // 采集层打点时间戳(服务器本地时间)
val payload: ByteArray // 原始数据负载
)
特别注意:不要在采集层做时间戳的「修正」或「对齐」。比如你觉得行情时间比本地时间慢了 10 毫秒,就手动加 10 毫秒。这种操作非常危险,因为时间偏差可能是动态变化的。正确的做法是:保留原始时间戳,在上层分析时再做校准。
3.4 整体架构图
下面这张图展示了数据采集层的整体架构。我习惯用「管道 + 过滤器」的模式来组织:
这张图里,我特意把「时间戳打点位置」标在了最下面。嗯,这里要注意:时间戳不是在数据源层打的,也不是在缓冲层打的,而是在适配器层——数据刚进入采集系统的那一刻。这样才能最大程度减少延迟引入的误差。
总结一句话:数据采集层设计得好不好,就看三点——能不能接进来、能不能扛得住、时间戳准不准。这三点做到了,上层监控系统才能放心地做分析和告警。
公众号:蓝海资料掘金营,微信deep3321