[踩坑] A 股开盘把 Python 搞挂了,怒切 Go 重写行情网关 (附 pprof 分析 + 源码) - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
推荐学习书目
Learn Python the Hard Way
Python Sites
PyPI - Python Package Index
http://diveintopython.org/toc/index.html
Pocoo
值得关注的项目
PyPy
Celery
Jinja2
Read the Docs
gevent
pyenv
virtualenv
Stackless Python
Beautiful Soup
结巴中文分词
Green Unicorn
Sentry
Shovel
Pyflakes
pytest
Python 编程
pep8 Checker
Styles
PEP 8
Google Python Style Guide
Code Style from The Hitchhiker's Guide
Howiee
V2EX    Python

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

  •  
  •   Howiee 1 月 21 日 5096 次点击
    事故现场
    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: % 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 是试用版的,别拿去跑大资金实盘哈,被限流了别找我)
    50 条回复    2026-01-30 15:40:55 +08:00
    zoharSoul
        1
    zoharSoul  
       1 月 21 日
    难点在于量化吧
    这种优化的场景还是很简单的, 不管是 go 还是什么的都 ok
    balckcloud37
        2
    balckcloud37  
       1 月 21 日
    其实只是受不了 gc 的话,disable gc 再手动 gc 就好了

    另外如果项目里没有 circular ref ,直接不 gc 也行
    encro
        3
    encro  
       1 月 21 日
    a 股 ctp 接口哪家好用,需要什么开通条件呢。
    shyrock2026
        4
    shyrock2026  
       1 月 21 日
    这标志性小图标。。。不是 AI 写的?
    JimLee0921
        5
    JimLee0921  
       1 月 21 日   8
    这不是 AI 写的我把头剁了
    k9982874
        6
    k9982874  
       1 月 21 日 via Android   5
    没人吐槽 3 秒几千条数据卡 json 解析?
    用的共享 cpu ,512m 的玩具机吗?
    即使是 python 也说不过去吧,只能说是一坨
    Anonono data-uid=
        7
    Anonono  
       1 月 21 日
    没必要上来就喷一坨吧,感谢 lz 分享
    v1
        8
    v1  
       1 月 21 日
    @k9982874 我用 j1900 曾经跑过行情网关,虽然是 c++手搓的,但是完全不可能卡 json 解析,更何况是 python 标准库
    bigtan
        9
    bigtan  
       1 月 21 日
    缓存对齐的环形缓冲区,这个基本上都这么干
    Sawyerhou
        10
    Sawyerhou  
       1 月 21 日
    挺有趣的帖子,感谢分享 :)
    adgfr32
        11
    adgfr32  
       1 月 21 日 via Android
    如果数据模型不复杂,可以先多进程试试,不过如果历史代码不多,直接换 golang 是个明智选择。
    ClericPy
        12
    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
        13
    aloxaf  
       1 月 22 日
    我觉得继续用 Python 优化解决这个问题,会是个更有趣的分享
    SDYY
        14
    SDYY  
       1 月 22 日
    python 我一般都用 orjson
    ZMQ 是 ZeroMQ 吧
    lixuda
        15
    lixuda  
       1 月 22 日
    如果策略慢怎么办,用 go 或 rust 来代替吗
    thtznet
        16
    thtznet  
       1 月 22 日
    Python 交易策略有没有可能共享一下?不是不相信楼主,就是想开开眼界。
    xgdgsc
        17
    xgdgsc  
       1 月 22 日 via Android
    不如 Julia
    zhangli2946
        18
    zhangli2946  
       1 月 22 日
    #go 程序员开年最好的娱乐帖子
    DioBrandoo
        19
    DioBrandoo  
       1 月 22 日
    搞笑,用自己能力问题在这里蹭语言流量
    Huelse
        20
    Huelse  
       1 月 22 日
    想知道用 Python3.14 去掉 GIL 后能否有所改善
    loongkim
        21
    loongkim  
       1 月 22 日
    emm ,对对对...
    DefoliationM
        22
    DefoliationM  
       1 月 22 日 via Android
    Python 底层库都是 c ,比 go 性能好。
    fkdtz
        23
    fkdtz  
       1 月 22 日   1
    你是会起标题的
    如果机房欠电费停电了,是否可以说,国家电网把我服务器干挂了
    i0error
        24
    i0error  
       1 月 22 日
    复制可跑、立竿见影,味太冲了
    namonai
        25
    namonai  
       1 月 22 日
    用 python 接实时行情,不是你在做梦就是我在做梦
    sheeta
        26
    sheeta  
       1 月 22 日
    我也是 python 接的全市场 5000 多家 3s 的行情,我的咋没挂。我是 python -> kafka -> flink
    harlen
        27
    harlen  
       1 月 22 日
    并发高的解决途径是限流吧。 把流量控制在你服务器最大能承受的压力上,你换了 go ,下次来个 go 不能承受的最大并发压力一样要崩。应用没崩溃。基础设施都要崩,比如 一个高并发 打到数据库上。 你数据没用 连接池限流。数据库一样会崩溃
    Howiee
        28
    Howiee  
    OP
       1 月 22 日
    @balckcloud37 是的,这个点后面也复盘过,
    如果继续用 Python ,disable gc + 手动 gc 确实是一个方向。
    当时主要是为了尽快止血,选了拆服务这条路。
    Howiee
        29
    Howiee  
    OP
       1 月 22 日
    @ClericPy 这个总结太真实了,基本命中当时的困境。
    特别是 CPU 密集函数在协程里的不可控性,这点踩过坑之后才有体感。
    julyclyde
        30
    julyclyde  
       1 月 22 日
    不懂量化

    json 在这里是什么情况?上游给的行情数据是 json 格式吗??
    encro
        31
    encro  
       1 月 22 日
    @sheeta

    数据库采用哪个?
    sanebow
        32
    sanebow  
       1 月 22 日 via iPhone
    tickdb 软广?
    sanebow
        33
    sanebow  
       1 月 22 日 via iPhone
    Tickdb 软广?
    freemoon
        34
    freemoon  
       1 月 22 日
    本站有 python 大拿,再等等
    song135711
        35
    song135711  
       1 月 22 日
    至少两点可以改进的
    1. 上线前要测试。
    2. 伪高并发的情况,用消息队列做异步处理。现在就算 golang 可以抗住,以后量增加了,还要改 rust 吗
    sheeta
        36
    sheeta  
       1 月 22 日
    @encro pg ,不过 tick 数据我没有存的
    Howiee
        37
    Howiee  
    OP
       1 月 22 日
    @julyclyde 是的,接入层这边拿到的是已经归一化后的 JSON 。
    上游原始行情并不一定是 JSON ,但为了多市场统一和下游解耦,中间会做一次协议转换。
    这里卡住的点也不在 JSON 本身,而在负载形态:
    A 股这类行情很多是 snapshot 型推送,表面看是 3 秒一批,但实际上会在很短的时间窗口内把一批数据集中推完。
    在 asyncio 的单 event loop 场景下,JSON 解码和对象创建是 CPU 密集的,一旦和这种脉冲叠加,就容易放大循环执行中的耗时,表现出来就是队列堆积和端到端延时飙升。
    balckcloud37
        38
    balckcloud37  
       1 月 22 日
    @k9982874 python gc 在对象特别多的时候确实会卡很久,json 解析本身性能不是问题,但是解析几个 json 就要 gc 一次然后卡几百毫秒就会出问题了
    assassinkyo
        39
    assassinkyo  
       1 月 22 日
    直接发股票代码吧,你写的这些我不爱看。 @_@
    latifrons
        40
    latifrons  
       1 月 22 日
    A 股开盘 + 非农数据公布,你逗我……
    lxxzml
        41
    lxxzml  
       1 月 22 日
    数据源怎么接?
    zhaoahui
        42
    zhaoahui  
       1 月 22 日   1
    聚合源( TickDB ) 软广
    julyclyde
        43
    julyclyde  
       1 月 22 日
    @Howiee json 的浓度有点低啊……
    coefu
        44
    coefu  
       1 月 23 日
    @sheeta #26 请教老哥,行情源是怎么搞到的?花钱买?
    coefu
        45
    coefu  
       1 月 23 日
    @zhaoahui 整篇最有价值的就是数据源的来源。。。属实是软广了。
    sheeta
        46
    sheeta  
       1 月 23 日   1
    @coefu 找个支持 qmt 的券商开个 miniqmt, 3s 的 tick 都是免费使用的
    coefu
        47
    coefu  
       1 月 23 日
    @sheeta #46 太感谢铁汁了。
    stark4
        48
    stark4  
       1 月 24 日
    没学过一天编程,但经过 ai 建议后我是用 orjson 解决的 ws 的风暴的,现在 500k/s 的处理速度,我主要做的是期权量化
    chenfengrugao
        49
    chenfengrugao  
       1 月 27 日
    @stark4 看 AI 程可以快速上手了。有靠的源?
    pyKane
        50
    pyKane  
       1 月 30 日
    3 秒几千条 JSON 就搞挂了,听起来就不靠谱。
    你真的是用了 Asyncio ?
    json.loads 是占点 CPU 但也没你说的这么惨。
    另外 ujson.loads 你换这个也可以,性能更好。
    但你的问题看起来像是你在用 AI 写的坑不少。

    我这一个项目每秒也有上万并发,API 也是 JSON 数据,async/await 很丝滑,对于占 CPU 的地方要学会适当 await asyncio.sleep(0) 让出一些 CPU 给其它 await
    关于     帮助文档     自助推广系统     博客     API     FAQ     Solana     1765 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 30ms UTC 10:07 PVG 18:07 LAX 02:07 JFK 05:07
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86