第23章 性能监控:系统资源监控(CPU/内存/网络)、交易延迟监控、吞吐量分析

做市系统跑起来之后,最怕什么?

怕它突然挂了,怕它延迟飙升,怕它吞吐量上不去。

我见过太多团队,代码写得漂亮,策略逻辑也牛,结果上线第一天就被CPU打满,订单堵在队列里出不去。嗯,这种场面,经历过一次就再也不想经历了。

所以这一章,我们来聊聊性能监控。说白了,就是给你的系统装上各种仪表盘,随时知道它「身体」怎么样。

23.1 系统资源监控:CPU、内存、网络

系统资源是交易系统的「硬件底座」。CPU不够,计算就慢;内存不够,数据就丢;网络不稳,订单就飞了。

23.1.1 CPU监控

CPU使用率是第一个要盯的指标。我个人习惯把CPU监控分成三层:

  • 整体使用率:看系统总负载,超过80%就要警惕
  • 核心分布:做市系统通常是多线程的,要看看是不是所有核都均匀跑,还是某个核被某个线程打满了
  • 上下文切换:这个容易被忽略。上下文切换太高,说明线程数设置不合理,或者锁竞争严重

实战经验:我在项目中遇到过CPU使用率只有30%,但延迟却很高的情况。查了半天,发现是某个线程在做大量无意义的轮询,把CPU时间片都浪费了。后来改成事件驱动,延迟直接降了一半。

监控CPU,我推荐用tophtop做实时查看,用Prometheus + node_exporter做长期采集。代码层面,可以这样获取CPU信息:

// Go语言示例:获取当前进程CPU使用率
import (
    "github.com/shirou/gopsutil/cpu"
    "time"
)

func getCPUUsage() float64 {
    percent, _ := cpu.Percent(time.Second, false)
    return percent[0]
}

23.1.2 内存监控

做市系统对内存的要求很苛刻。你想想看,订单簿、持仓数据、历史行情,哪个不是吃内存的大户?

内存监控要关注这几个点:

  • 物理内存使用率:别让系统开始用swap,那延迟就完蛋了
  • 堆内存 vs 栈内存:Java系系统要特别关注GC频率,GC一次可能卡几十毫秒
  • 内存泄漏:这个最要命。我见过一个系统跑了三天,内存从4G涨到32G,最后OOM挂了

避坑指南:我曾经在监控内存时只看了总使用率,没看GC情况。结果系统内存占用一直稳定在60%,但每30秒一次Full GC,每次卡顿200ms。后来加上GC监控才发现问题。记住:内存稳定不代表没问题,GC频率才是关键。

内存监控的常用命令:

# 查看内存使用
free -h

# 查看进程内存详情
top -p <pid>

# Java系系统看GC
jstat -gcutil <pid> 1000

23.1.3 网络监控

网络是做市系统的生命线。订单从你的系统发到交易所,中间经过多少跳?延迟多少?丢包率多少?这些都得盯着。

网络监控的核心指标:

指标 说明 警戒线
带宽使用率 网络接口的吞吐量 > 70% 需扩容
TCP重传率 数据包重传比例 > 0.1% 需排查
连接数 与交易所的TCP连接数量 接近上限时预警
网络延迟 ping或专线延迟 > 1ms 需优化

小技巧:我个人习惯在交易服务器上跑一个ping到交易所的脚本,每秒钟记录一次延迟。一旦发现延迟抖动超过0.5ms,立刻告警。别小看这0.5ms,在高频做市里,可能就是几百万的盈亏。

23.2 交易延迟监控

延迟是做市系统的核心指标。没有之一。

为什么?因为做市赚的就是买卖价差,你比别人慢1ms,订单就被别人抢走了。所以延迟监控必须做到「端到端」。

23.2.1 延迟的四个阶段

我把交易延迟拆成四个阶段来监控:

  1. 行情接收延迟:从交易所发出行情,到你的系统收到,这中间的耗时
  2. 策略计算延迟:收到行情后,策略引擎计算新报价的时间
  3. 订单发送延迟:从策略发出订单,到订单到达交易所的时间
  4. 成交回报延迟:从交易所撮合完成,到你的系统收到成交确认的时间

每个阶段都要打上时间戳。我习惯用纳秒级精度:

// 延迟打点示例
func onMarketData(tick MarketData) {
    t1 := time.Now().UnixNano()  // 收到行情
    
    // 策略计算
    order := strategy.calculate(tick)
    t2 := time.Now().UnixNano()  // 计算完成
    
    // 发送订单
    sendOrder(order)
    t3 := time.Now().UnixNano()  // 发送完成
    
    // 记录延迟
    metrics.Observe("latency.calc", float64(t2-t1)/1e6)  // 计算延迟(ms)
    metrics.Observe("latency.send", float64(t3-t2)/1e6)  // 发送延迟(ms)
}

23.2.2 延迟的统计方式

只看平均延迟是不够的。你想想看,平均延迟1ms,但P99延迟100ms,那你的系统在1%的时间里是「瘫痪」的。

我建议至少监控这几个分位值:

  • P50:典型延迟,看系统日常表现
  • P99:99%的请求都在这个延迟以内,看系统稳定性
  • P99.9:极端情况,看系统有没有「毛刺」
  • 最大值:最差情况,看系统有没有「死锁」或「卡顿」

个人经验:我曾经遇到一个系统,P50延迟只有0.5ms,但P99.9延迟高达500ms。查了三天,发现是某个定时任务每5分钟跑一次全量数据同步,把CPU打满了。后来把同步任务改成增量模式,P99.9直接降到2ms。所以,只看平均值真的会骗人。

23.3 吞吐量分析

吞吐量,说白了就是你的系统「能扛多少活」。做市系统的吞吐量通常用「每秒处理订单数」来衡量。

23.3.1 吞吐量的三个维度

我习惯从三个维度来分析吞吐量:

维度 指标 说明
行情吞吐 tick/s 每秒能处理多少行情数据
订单吞吐 order/s 每秒能发送多少订单
成交吞吐 trade/s 每秒能处理多少成交回报

这三个维度要分开监控。因为行情吞吐高不代表订单吞吐也高,反之亦然。

23.3.2 吞吐量的瓶颈分析

当吞吐量上不去时,怎么找瓶颈?我有一套「三板斧」:

  1. 看CPU:如果CPU没跑满,说明瓶颈在IO(磁盘或网络)
  2. 看锁:如果CPU跑满了但吞吐量上不去,大概率是锁竞争
  3. 看队列:如果队列长度一直在增长,说明消费速度跟不上生产速度

实战技巧:我个人习惯在代码里埋一些「计数器」,比如每处理1000笔订单就打印一次吞吐量。这样不用依赖外部监控系统,也能快速定位问题。代码大概长这样:

// 吞吐量计数器
var orderCount int64
var lastTime time.Time

func onOrderSent() {
    atomic.AddInt64(&orderCount, 1)
    
    // 每1000笔打印一次
    if orderCount%1000 == 0 {
        elapsed := time.Since(lastTime).Seconds()
        throughput := 1000 / elapsed
        log.Printf("订单吞吐量: %.0f order/s", throughput)
        lastTime = time.Now()
    }
}

23.4 性能监控的整体架构

说了这么多,我们来画一张图,看看性能监控的整体架构是什么样的。

做市系统性能监控架构图 数据采集层 系统资源采集 延迟打点采集 吞吐量计数器 日志采集 数据存储层 时序数据库 日志系统 告警规则 可视化与告警层 Grafana看板 实时告警 日报/周报 API 行动层:优化 / 扩容 / 降级

这张图展示了我常用的四层监控架构。从数据采集到存储,再到可视化和行动,每一层都有明确的职责。

23.5 监控系统的落地建议

最后,给几个落地建议:

  • 先做核心指标:别一上来就想监控100个指标。先盯CPU、内存、延迟P99、吞吐量这四个,跑稳了再加
  • 告警要有分级:P99延迟超过10ms发邮件,超过50ms发短信,超过100ms打电话。别让运维半夜被不重要的告警吵醒
  • 保留历史数据:我习惯保留至少30天的监控数据。这样出问题时可以回溯,看看是不是「昨天改了个配置」导致的
  • 监控系统本身也要监控:嗯,这听起来有点套娃,但确实重要。监控系统挂了,你都不知道系统出了什么问题

最后提醒一句:性能监控不是「一次性工程」。系统在变,行情在变,交易量在变,你的监控指标也要跟着变。定期复盘,看看哪些指标已经没用了,哪些指标需要新增。只有这样,监控系统才能真正帮你「守住底线」。


无相订单流研究社 微信Lucian808555