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

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

    json 在这里是什么情况?上游给的行情数据是 json 格式吗??
    encro
        31
    encro  
       13 小时 23 分钟前
    @sheeta

    数据库采用哪个?
    sanebow
        32
    sanebow  
       13 小时 15 分钟前 via iPhone
    tickdb 软广?
    sanebow
        33
    sanebow  
       13 小时 14 分钟前 via iPhone
    Tickdb 软广?
    lasuar
        34
    lasuar  
       13 小时 6 分钟前
    本站有 python 大拿,再等等
    song135711
        35
    song135711  
       12 小时 46 分钟前
    至少两点可以改进的
    1. 上线前要测试。
    2. 伪高并发的情况,用消息队列做异步处理。现在就算 golang 可以抗住,以后量增加了,还要改 rust 吗
    sheeta
        36
    sheeta  
       12 小时 44 分钟前
    @encro pg ,不过 tick 数据我没有存的
    Howiee
        37
    Howiee  
    OP
       12 小时 39 分钟前
    @julyclyde 是的,接入层这边拿到的是已经归一化后的 JSON 。
    上游原始行情并不一定是 JSON ,但为了多市场统一和下游解耦,中间会做一次协议转换。
    这里卡住的点也不在 JSON 本身,而在负载形态:
    A 股这类行情很多是 snapshot 型推送,表面看是 3 秒一批,但实际上会在很短的时间窗口内把一批数据集中推完。
    在 asyncio 的单 event loop 场景下,JSON 解码和对象创建是 CPU 密集的,一旦和这种脉冲叠加,就容易放大循环执行中的耗时,表现出来就是队列堆积和端到端延时飙升。
    balckcloud37
        38
    balckcloud37  
       12 小时 32 分钟前
    @k9982874 python gc 在对象特别多的时候确实会卡很久,json 解析本身性能不是问题,但是解析几个 json 就要 gc 一次然后卡几百毫秒就会出问题了
    assassinkyo
        39
    assassinkyo  
       12 小时 21 分钟前
    直接发股票代码吧,你写的这些我不爱看。 @_@
    latifrons
        40
    latifrons  
       11 小时 39 分钟前
    A 股开盘 + 非农数据公布,你逗我……
    lxxzml
        41
    lxxzml  
       10 小时 1 分钟前
    数据源怎么接?
    zhaoahui
        42
    zhaoahui  
       9 小时 42 分钟前
    聚合源( TickDB ) 软广
    julyclyde
        43
    julyclyde  
       6 小时 57 分钟前
    @Howiee json 的浓度有点低啊……
    关于   ·   帮助文档   ·   自助推广系统   ·   博客   ·   API   ·   FAQ   ·   Solana   ·   1165 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 31ms · UTC 18:13 · PVG 02:13 · LAX 10:13 · JFK 13:13
    ♥ Do have faith in what you're doing.