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

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

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

    数据库采用哪个?
    sanebow
        32
    sanebow  
       6 分钟前 via iPhone
    tickdb 软广?
    sanebow
        33
    sanebow  
       6 分钟前 via iPhone
    Tickdb 软广?
    关于   ·   帮助文档   ·   自助推广系统   ·   博客   ·   API   ·   FAQ   ·   Solana   ·   3727 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 24ms · UTC 05:04 · PVG 13:04 · LAX 21:04 · JFK 00:04
    ♥ Do have faith in what you're doing.