[踩坑] A 股开盘把 Python 搞挂了,怒切 Go 重写行情网关 (附 pprof 分析 + 源码)

1 月 21 日
 Howiee
💥 事故现场
LZ 所在的量化小厂,早期基础设施全是 Python (Asyncio) 一把梭。 跑美股( US )的时候相安无事,毕竟 Tick 流是均匀的。 上周策略组说要加 A 股 (CN) 和 外汇 (FX) 做宏观对冲,我就按老套路接了数据源。

结果上线第一天 9:30 就炸了。 监控报警 CPU 100%,接着就是 TCP Recv-Q 堆积,最后直接断连。 策略端收到行情的时候,黄花菜都凉了(延迟 > 500ms )。

🔍 排查过程 (Post-Mortem)
被 Leader 骂完后,挂了 py-spy 看火焰图,发现两个大坑:

Snapshot 脉冲:A 股跟美股不一样,它是 3 秒一次的全市场快照。几千只股票的数据在同一毫秒涌进来,瞬间流量是平时的几十倍。

GIL + GC 混合双打:

json.loads 是 CPU 密集型,把 GIL 锁死了,网络线程根本抢不到 CPU 读数据。

短时间生成大量 dict 对象,触发 Python 频繁 GC ,Stop-the-world 。

🛠️ 架构重构 (Python -> Go)
为了保住饭碗,连夜决定把 Feed Handler 层剥离出来用 Go 重写。 目标很明确:扛住 A 股脉冲,把数据洗干净,再喂给 Python 策略。

架构逻辑:WebSocket (Unified API) -> Go Channel (Buffer) -> Worker Pool (Sonic Decode) -> Shm/ZMQ

为什么用 Go ?

Goroutine:几 KB 开销,随开随用。

Channel:天然的队列,做 Buffer 抗脉冲神器。

Sonic:字节开源的 JSON 库,带 SIMD 加速,比标准库快 2-3 倍(这个是关键)。

💻 Show me the code
为了解决 协议异构( A 股 CTP 、美股 FIX 、外汇 MT4 ),我接了个聚合源( TickDB ),把全市场数据洗成了统一的 JSON 。这样 Go 这边只用维护一个 Struct 。

以下是脱敏后的核心代码,复制可跑(需 go get 依赖)。
package main

import (
"fmt"
"log"
"runtime"
"time"

"github.com/bytedance/sonic" // 字节的库,解析速度吊打 encoding/json
"github.com/gorilla/websocket"
)

// 防爬虫/防风控,URL 拆一下
const (
Host = "api.tickdb.ai"
Path = "/v1/realtime"
// Key 是薅的试用版,大家拿去压测没问题
Key = "?api_key=YOUR_V2EX_KEY"
)

// 内存对齐优化:把同类型字段放一起
type MarketTick struct {
Cmd string `json:"cmd"`
Data struct {
Symbol string `json:"symbol"`
LastPrice string `json:"last_price"` // 价格统一 string ,下游处理精度
Volume string `json:"volume_24h"`
Timestamp int64 `json:"timestamp"` // 8 byte
Market string `json:"market"` // CN/US/HK/FX
} `json:"data"`
}

func main() {
// 1. 跑满多核,别浪费 AWS 的 CPU
runtime.GOMAXPROCS(runtime.NumCPU())

url := "wss://" + Host + Path + Key
conn, _, err := websocket.DefaultDialer.Dial(url, nil)
if err != nil {
log.Fatal("Dial err:", err)
}
defer conn.Close()

// 2. 订阅指令
// 重点测试:A 股(脉冲) + 贵金属(高频) + 美股/港股
subMsg := `{
"cmd": "subscribe",
"data": {
"channel": "ticker",
"symbols": [
"600519.SH", "000001.SZ", // A 股:茅台、平安 (9:30 压力源)
"XAUUSD", "USDJPY", // 外汇:黄金、日元 (高频源)
"NVDA.US", "AAPL.US", // 美股:英伟达
"00700.HK", "09988.HK", // 港股:腾讯
"BTCUSDT" // Crypto:拿来跑 7x24h 稳定性的
]
}
}`
if err := conn.WriteMessage(websocket.TextMessage, []byte(subMsg)); err != nil {
log.Fatal("Sub err:", err)
}
fmt.Println(">>> Go Engine Started...")

// 3. Ring Buffer
// 关键点:8192 的缓冲,专门为了吃下 A 股的瞬间脉冲
dataChan := make(chan []byte, 8192)

// 4. Worker Pool
// 经验值:CPU 核数 * 2
workerNum := runtime.NumCPU() * 2
for i := 0; i < workerNum; i++ {
go worker(i, dataChan)
}

// 5. Producer Loop (IO Bound)
// 只管读,读到就扔 Channel ,绝对不阻塞
for {
_, msg, err := conn.ReadMessage()
if err != nil {
log.Println("Read err:", err)
break
}
dataChan <- msg
}
}

// Consumer (CPU Bound)
func worker(id int, ch <-chan []byte) {
var tick MarketTick
for msg := range ch {
// 用 Sonic 解析,性能起飞
if err := sonic.Unmarshal(msg, &tick); err != nil {
continue
}

if tick.Cmd == "ticker" {
// 简单的监控:全链路延迟
latency := time.Now().UnixMilli() - tick.Data.Timestamp

// 抽样打印
if id == 0 {
fmt.Printf("[%s] %-8s | Price: %s | Lat: %d ms\n",
tick.Data.Market, tick.Data.Symbol, tick.Data.LastPrice, latency)
}
}
}
}

📊 Benchmark (实测数据)
环境:AWS c5.xlarge (4C 8G),订阅 500 个活跃 Symbol 。 复现了 9:30 A 股开盘 + 非农数据公布 的混合场景。
指标,Python (Asyncio),Go (Sonic + Channel),评价
P99 Latency,480ms+,< 4ms,简直是降维打击
Max Jitter,1.2s (GC Stop),15ms,终于不丢包了
CPU Usage,98% (单核打满),18% (多核均衡),机器都不怎么转
Mem,800MB,60MB,省下来的内存可以多跑个回测

📝 几点心得
术业有专攻:Python 做策略逻辑开发是无敌的,但这种 I/O + CPU 混合密集型的接入层,还是交给 Go/Rust 吧,别头铁。

别造轮子:之前想自己写 CTP 和 FIX 的解析器,写了一周只想跑路。后来切到 TickDB 这种 Unified API ,把脏活外包出去,瞬间清爽了。

Sonic 是神器:如果你的 Go 程序瓶颈在 JSON ,无脑换 bytedance/sonic ,立竿见影。

代码大家随便拿去改,希望能帮到同样被 Python 延迟折磨的兄弟。 (Key 是试用版的,别拿去跑大资金实盘哈,被限流了别找我)
6317 次点击
所在节点    Python
51 条回复
zoharSoul
1 月 21 日
难点在于量化吧
这种优化的场景还是很简单的, 不管是 go 还是什么的都 ok
balckcloud37
1 月 21 日
其实只是受不了 gc 的话,disable gc 再手动 gc 就好了

另外如果项目里没有 circular ref ,直接不 gc 也行
encro
1 月 21 日
a 股 ctp 接口哪家好用,需要什么开通条件呢。
shyrock2026
1 月 21 日
这标志性小图标。。。不是 AI 写的?
JimLee0921
1 月 21 日
这不是 AI 写的我把头剁了
k9982874
1 月 21 日
没人吐槽 3 秒几千条数据卡 json 解析?
用的共享 cpu ,512m 的玩具机吗?
即使是 python 也说不过去吧,只能说是一坨
Anonono
1 月 21 日
没必要上来就喷一坨吧,感谢 lz 分享
v1
1 月 21 日
@k9982874 我用 j1900 曾经跑过行情网关,虽然是 c++手搓的,但是完全不可能卡 json 解析,更何况是 python 标准库
bigtan
1 月 21 日
缓存对齐的环形缓冲区,这个基本上都这么干
Sawyerhou
1 月 21 日
挺有趣的帖子,感谢分享 :)
adgfr32
1 月 21 日
如果数据模型不复杂,可以先多进程试试,不过如果历史代码不多,直接换 golang 是个明智选择。
ClericPy
1 月 21 日
曾经也遇到过,CPU 密集型把协程卡死出现过 3 次,两次是 selectolax 、lxml 的解析十几万字符的 HTML ,一次也是类似你的情况解析十几 MB 的大 JSON (特么的有人把一大堆图片做 base64 放 JSON 里了)。最后 hadoop 直接超时杀死还没看到报错

python 在有些场景确实体现了并发上的先天不足:

1. 多线程不能利用多核,所以有些时候要自己开进程池,明明是无副作用的纯函数却要共享个 GIL 。子解释器希望有用但不太期待

2. to_thread 能让协程不至于因为一个 CPU 特别忙的任务一直 hang 在那。

但是现在非常尴尬的一个地方是,我也不知道哪个函数是敏捷的小函数还是 CPU 秘籍的大函数

在协程里的一个困境就是:

1. 同步函数无脑放 to_thread ,对于特别多的小函数开销很浪费。
2. 为了计算密集型的放 ProcessExecutor 里,子进程也很费事。

现在的协程,只能尽最大可能保证全程协程,不耦合太多同步函数进来

PS:当年 hang 死的问题,现在看书知道 asyncio 开 debug 模式就行了,然后在公司里 langchain 的一个项目日志里,几百条阻塞 warning 日志。。。。。。
aloxaf
1 月 22 日
我觉得继续用 Python 优化解决这个问题,会是个更有趣的分享
SDYY
1 月 22 日
python 我一般都用 orjson
ZMQ 是 ZeroMQ 吧
lixuda
1 月 22 日
如果策略慢怎么办,用 go 或 rust 来代替吗
thtznet
1 月 22 日
Python 交易策略有没有可能共享一下?不是不相信楼主,就是想开开眼界。
xgdgsc
1 月 22 日
不如 Julia
zhangli2946
1 月 22 日
#go 程序员开年最好的娱乐帖子
DioBrandoo
1 月 22 日
搞笑,用自己能力问题在这里蹭语言流量
Huelse
1 月 22 日
想知道用 Python3.14 去掉 GIL 后能否有所改善

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/1187370

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX