9.0 KiB
可插拔 RPC 传输层
更新时间:2026-07-29
目标
在 Redis RPC 之上加一层抽象,支持快速切换传输后端,按延迟/部署场景选择:
| 传输 | 同机 p50 | 跨机 | 依赖 | 适用场景 |
|---|---|---|---|---|
redis(默认) |
~12ms | ✅ | redis-py | 生产默认,跨机也能用 |
zmq |
~0.2ms | ✅(tcp) | pyzmq | 同机低延迟,主优化目标 |
mysql |
~50ms+ | ✅ | DBUtils + 驱动 | 兼容兜底(Redis/ZMQ 都不可用时) |
shm |
— | ❌ | — | 留接口未实现(需 Python 3.8+) |
切换传输只改一个配置字段 transport,业务代码(handlers / to_jsonable / process_request)零改动。
无 redis 版本(QMT 沙箱拒绝 import redis 时用)
如果 QMT 环境拒绝 import redis(券商白名单拦截),用 bigqmt_no_redis/ 目录下的无 redis 版本:
bigqmt_no_redis/zmq_transport.py— 自包含的 ZMQ transport,内联所有编码函数(decode_text/encode_rpc_request_payload/decode_rpc_request_payload),完全不 import redis_common/redis_rpc,去掉 redis 服务发现(用静态派生端口)bigqmt_no_redis/DRYRUN_no_redis.py— 无 redis 的 DRYRUN 入口,强制transport=zmq+background_threads=True,只加载 zmq transport
用法:QMT 策略编辑器加载 BIGQMT_DRYRUN_NO_REDIS.py(同步到 QMT 目录时用这个文件名),RPC 走纯 ZMQ,零 redis 依赖。
架构
业务层(不变) BigQmtRpcHandlers / process_request / to_jsonable
│ request/response dict (JSON)
┌───────────────▼────────────────┐
│ RpcTransport 抽象接口 │
│ send_request / start_receiving│
│ send_response / stop │
└───┬────────┬─────────┬─────────┘
┌──────────▼┐ ┌────▼───┐ ┌──▼─────┐ ┌──────┐
│ Redis │ │ ZMQ │ │ MySQL │ │ SHM │
│ (默认) │ │ (低延迟)│ │(兼容) │ │(stub)│
└────────────┘ └────────┘ └────────┘ └──────┘
传输层只负责"请求/响应怎么在网络上走",不碰业务语义。抽象接口见
src/bigqmt_signal_trader/transports/base.py:
send_request(request, timeout)— 客户端发请求并阻塞等响应start_receiving(on_request)— 服务端开始接收,每个请求回调on_requestsend_response(request, response)— 服务端回包(路由信息从 request 读)stop()— 释放资源
配置怎么切换
服务端(QMT 进程)
在 bigqmt_signal_trader_local_config.py 的 BIGQMT_REDIS_CONFIG 里设置 transport 字段:
BIGQMT_REDIS_CONFIG = {
"transport": "zmq", # 默认 "redis"。可选: redis/zmq/mysql/shm
"zmq": {
"bind_address": "tcp://127.0.0.1:5560", # Windows 同机使用 TCP 回环
},
"rpc_background_threads": False,
"schedule_adjust": True,
"schedule_adjust_interval": "100nMilliSecond",
}
注意(ZMQ):ZMQ 支持由 QMT 官方
adjust回调排空请求。低延迟实盘建议设置rpc_background_threads=False,并把schedule_adjust_interval设置为100nMilliSecond,避免后台 Python 线程受 QMT 进程 GIL 调度影响。 MySQL/SHM 仍会自动启用后台接收线程。 端口不写时按账号自动派生tcp://127.0.0.1:{15560 + 账号%100}(同机回环)。 Linux 同机也可使用ipc:///tmp/bigqmt_rpc.sock。
transport=mysql 时可额外配置:
BIGQMT_REDIS_CONFIG["mysql"] = {
"driver": "pymysql", # 或 mysql.connector
"host": "127.0.0.1", "port": 3306,
"user": "rpc", "password": "***", "database": "bigqmt_rpc",
"pool_config": {
"mincached": 1, "maxcached": 4,
"maxshared": 3, "maxconnections": 8,
},
}
不指定 transport = "redis" = 完全保持现状。
客户端
BigQmtRpcClient 同样读 transport 字段(从 client config 或环境变量 BIGQMT_RPC_TRANSPORT):
BIGQMT_REDIS_CONFIG = {
"transport": "zmq",
"zmq": {"connect_address": "tcp://127.0.0.1:5560"}, # 指向服务端 bind 地址
# ...
}
或环境变量:export BIGQMT_RPC_TRANSPORT=zmq
各传输说明
Redis(transport: redis,默认)
完全保持原有行为:
- 客户端
RPUSH请求到bigqmt:rpc:queue:{account_id}→BLPOP响应 list - 服务端
brpop取请求 → 三路回包(SETEXkey +RPUSHlist +PUBLISHchannel) - 保留 b64 股票代码混淆编码
现有 14 个测试、所有模板字符串、配置全部不变。
ZMQ(transport: zmq,低延迟)
- 服务端 ROUTER socket bind,客户端 DEALER socket connect
- 用 ZMQ 原生 identity 路由(
reply_*字段忽略) - Windows 用
tcp://127.0.0.1:port(ZMQ 在 Windows 不支持ipc://) - Linux 同机可用
ipc://更快(绕过 TCP 栈) - 实测同机 tcp 回环:p50 = 0.2ms(比 Redis 快 ~60 倍); 在大 QMT 全终端进程内实测 ping p50 ≈ 0.3ms(20 次 0 个 >50ms)。
注意:ZMQ transport 的 stop() 由 ROUTER 接收线程自己关闭 socket(Windows
上跨线程 close socket 会触发 signaler 断言)。
⚠️ 在大 QMT 进程内跑 zmq 的两个必要条件(都已自动处理,勿手动关):
background_threads必须为 True —— ZMQ 的 ROUTER 只有在后台线程里才 起接收循环;否则只 bind 不收包,客户端全部超时。_build_rpc_service已对 非 redis 传输自动置 True,无需在 config 里写。schedule_adjust必须保持开 ——run_time("adjust", interval)是我们注册的 RPC 队列 drain 定时器(adjust不是 QMT 内置回调,QMT 只自动调 init/handlebar; handlebar 里return adjust(...))。deferred 档的交易查询要靠它在主线程执行;关掉就没 有主线程 drain 点。它也是后台线程拿 GIL 窗口的节奏源:schedule_adjust_interval越小 inline 尾延迟越低(500ms→490ms,100ms→热循环100ms 但烧 CPU,200ms 折中)。 详见docs/BIG_QMT_REDIS_RPC.md的「延迟模式」。
MySQL(transport: mysql,兼容兜底)
- 用
requests/responses两张表轮询 - 通过 DBUtils
PooledDB连接池管理连接,避免频繁开关 - 跨驱动:支持 pymysql / mysql.connector / sqlite3(paramstyle 自动适配)
DELETE-then-INSERT写响应,兼容 MySQL 和 sqlite- 延迟较高(~50ms+,受轮询间隔限制),仅作 Redis/ZMQ 不可用时的兜底
连接池配置(pool_config):
"pool_config": {
"mincached": 1, # 空闲连接数
"maxcached": 4, # 最大缓存连接
"maxshared": 3, # 最大共享连接
"maxconnections": 8, # 最大连接数
}
注意:sqlite 连接线程绑定,sqlite 测试需 check_same_thread=False + maxshared=0。
SHM(transport: shm,未实现)
留接口,send_request 会抛 TransportError。Python 3.8+ 的
multiprocessing.shared_memory 或自定义 mmap 环形缓冲区可后续实现。
实测延迟对比(同机)
基准脚本:python bench_transports.py -n 100
redis n=100 min=10.86 p50=12.22 p90=14.77 p99=290.60 avg=24.99 ms
zmq n=100 min=0.15 p50=0.21 p90=0.33 p99=20.62 avg=0.43 ms
切换检查清单
- 服务端和客户端的
transport字段必须一致 - zmq:不写
zmq块时端口按账号自动派生tcp://127.0.0.1:{15560+账号%100}, 两端一致;要跨机或自定义端口时才写connect_address/bind_address - zmq/mysql:
background_threads由_build_rpc_service自动开,不用手动配 - zmq(大 QMT 进程内):保持
schedule_adjust开(默认就是开),否则 adjust 空转占满 GIL 饿死接收线程 → RPC 超时 - mysql:两端连同一个数据库,schema 自动创建
- 切回 redis:删掉
transport字段或设为"redis",无需改其他配置
文件结构
src/bigqmt_signal_trader/transports/
├── __init__.py # 导出 build_transport, RpcTransport
├── base.py # RpcTransport 抽象基类
├── redis_transport.py # Redis 实现(默认,零行为变更)
├── zmq_transport.py # ZMQ ROUTER/DEALER 实现
├── mysql_transport.py # MySQL + DBUtils 连接池
├── shm_transport.py # 共享内存 stub
└── factory.py # build_transport(name, config) 工厂
测试:tests/bigqmt_signal_trader/test_transports.py(9 个测试,含 ZMQ/MySQL 往返)