第一章:Go量化开发中滑点暴增的本质归因与系统性风险图谱
滑点并非孤立的交易偏差现象,而是Go量化系统在高并发、低延迟、多源异步协同场景下,多个底层机制耦合失稳的外在表征。其暴增往往标志着系统性风险已突破临界阈值,需从执行引擎、市场数据流、订单生命周期及基础设施四维穿透分析。
执行引擎的时序撕裂
Go协程调度的非确定性与交易所API响应延迟共同导致“下单—确认”时间窗口不可控。当time.AfterFunc用于超时撤单但未结合context.WithTimeout做通道级阻断时,可能触发重复提交或幽灵订单。正确实践应统一使用带取消语义的HTTP客户端:
ctx, cancel := context.WithTimeout(context.Background(), 300*time.Millisecond)
defer cancel()
req, _ := http.NewRequestWithContext(ctx, "POST", url, body)
resp, err := client.Do(req) // 若超时,Do()立即返回err=ctx.Err()
市场数据流的瞬态失真
Level2行情解析若依赖无锁环形缓冲区(如ringbuf.RingBuffer)但未校验序列号连续性,将导致价格快照跳变。实盘必须对每帧SeqNum执行单调递增校验,丢弃乱序包并触发重连:
| 风险类型 | 表现特征 | Go检测逻辑 |
|---|---|---|
| 序列号回绕 | SeqNum从65535突降至100 | if seq < lastSeq && lastSeq > 65000 |
| 时间戳倒流 | recvTime < prevRecvTime |
if t.Before(prevT) { log.Warn("time warp") } |
订单状态机的竞态裸奔
多个goroutine并发调用order.UpdateStatus()而未加sync/atomic保护,易造成状态覆盖(如PartiallyFilled被误覆写为New)。必须采用CAS原子更新:
type Order struct {
status uint32 // 0=New, 1=Partial, 2=Filled, 3=Cancelled
}
func (o *Order) TrySetStatus(expect, new uint32) bool {
return atomic.CompareAndSwapUint32(&o.status, expect, new)
}
基础设施层的隐性瓶颈
Linux内核net.core.somaxconn默认值(128)在高频策略集群中极易触发TCP半连接队列溢出,导致行情推送延迟骤升。须在容器启动时显式调优:
sysctl -w net.core.somaxconn=65535
sysctl -w net.ipv4.tcp_tw_reuse=1
第二章:goroutine死锁的十二种典型模式及其量化场景复现
2.1 通道阻塞型死锁:订单簿更新与撮合引擎间的双向等待闭环
当订单簿服务通过 chan<- Order 向撮合引擎推送新订单,而撮合引擎又需通过 sync.RWMutex 读取同一订单簿快照时,双向依赖即刻形成闭环。
数据同步机制
- 订单簿更新 goroutine 持有写锁并尝试发送至满载 channel
- 撮合引擎 goroutine 在
select { case <-orderChan: ... }中阻塞,同时试图获取读锁 - 双方均无法推进,channel 缓冲区耗尽 + 锁竞争 → 阻塞型死锁
// 撮合引擎中典型阻塞点(简化)
func (m *Matcher) run() {
for {
select {
case order := <-m.orderChan: // 若 chan 已满且写端未释放锁,则永久阻塞
m.book.RLock() // 但 RLock 被写锁占用 → 死锁触发
// ...
m.book.RUnlock()
}
}
}
m.orderChan 为无缓冲或小缓冲 channel;m.book 是 sync.RWMutex 包装的订单簿结构。RLock() 在写锁持有期间不可获取,而写端因 channel 阻塞无法释放锁。
| 角色 | 持有资源 | 等待资源 | 风险点 |
|---|---|---|---|
| 订单簿更新 | book.RWMutex 写锁 |
orderChan 可写空间 |
channel 满载 |
| 撮合引擎 | — | book.RWMutex 读锁 + orderChan 消息 |
读锁被占、chan 无数据 |
graph TD
A[订单簿更新] -->|尝试写入| B[orderChan]
B -->|已满| C[阻塞]
C --> D[无法释放 write lock]
E[撮合引擎] -->|尝试读取| F[book.RWMutex]
F -->|write lock held| G[阻塞]
G -->|无法消费| B
2.2 WaitGroup误用型死锁:多周期因子计算协程组的计数器悬空陷阱
数据同步机制
sync.WaitGroup 要求 Add() 与 Done() 严格配对,但在动态启停协程场景中极易因 Add() 调用时机不当导致计数器悬空——即 Wait() 永远阻塞。
典型误用代码
func calcFactorsAsync(nums []int, wg *sync.WaitGroup) {
for _, n := range nums {
wg.Add(1) // ❌ 错误:若 nums 为空,Add(0) 不触发,但后续无 Done()
go func(x int) {
defer wg.Done()
// 因子计算逻辑...
}(n)
}
}
逻辑分析:当
nums为空时,wg.Add(1)零次执行,wg.counter保持初始 0;随后调用wg.Wait()立即返回,看似“成功”,实则漏等所有协程。若后续有依赖该 WaitGroup 的同步点(如关闭 channel),将引发竞态或 panic。
正确模式对比
| 场景 | Add() 位置 | 安全性 |
|---|---|---|
| 静态协程数 | 循环外预设 Add(n) |
✅ |
| 动态协程(零值容错) | 循环内 Add(1) + if len(nums)==0 { return } |
✅ |
| 协程启动失败路径 | Add(1) 后立即 defer Done() 或 recover |
✅ |
graph TD
A[启动协程前] --> B{nums 长度 > 0?}
B -->|是| C[Add 1 → 启动 goroutine]
B -->|否| D[跳过 Add → Wait 立即返回]
C --> E[goroutine 执行完毕 → Done]
D --> F[主流程误判“全部完成”]
2.3 Context取消链断裂:实盘风控熔断信号在跨服务goroutine间丢失路径
熔断信号丢失的典型场景
当风控服务通过 context.WithCancel 发起熔断,下游订单、清算 goroutine 若未显式继承该 context,取消信号即中断。
数据同步机制
风控决策需原子同步至多服务,但常见错误是:
// ❌ 错误:新建独立 context,切断传播链
go func() {
ctx := context.Background() // 丢失上游 cancel signal
processOrder(ctx, order)
}()
// ✅ 正确:显式传递并派生
go func(parentCtx context.Context) {
ctx, _ := context.WithTimeout(parentCtx, 5*time.Second)
processOrder(ctx, order)
}(riskCtx) // 来自风控主 goroutine
逻辑分析:context.Background() 创建无父节点的根 context,无法响应上游 cancel();而 parentCtx 携带取消链,WithTimeout 会自动注册子 canceler 到父链中。
关键传播约束
| 环节 | 是否继承 riskCtx | 信号可达性 |
|---|---|---|
| 订单服务 | ✅ | 是 |
| 清算服务 | ❌(用 Background) | 否 |
| 日志审计协程 | ✅(WithValue) | 是 |
graph TD
A[风控主 Goroutine] -->|riskCtx.cancel()| B[订单服务]
A --> C[清算服务]
C -->|无 context 传递| D[信号丢失]
2.4 Mutex嵌套竞争型死锁:行情快照缓存与策略状态机共享锁的持有顺序颠倒
死锁触发场景
当行情快照缓存(SnapshotCache)与策略状态机(StrategySM)共用两把互斥锁 mu_cache 和 mu_sm,但线程A按 mu_cache → mu_sm、线程B按 mu_sm → mu_cache 顺序加锁时,即构成典型嵌套竞争型死锁。
关键代码片段
// 线程A:先更新快照,再检查状态机合法性
func (c *SnapshotCache) UpdateAndValidate(s *Snapshot) {
c.mu_cache.Lock() // ✅ 持有 mu_cache
defer c.mu_cache.Unlock()
c.data[s.Symbol] = s
c.sm.Validate(s) // ⚠️ 尝试获取 mu_sm(此时B可能已持 mu_sm)
}
// 线程B:先切换状态,再刷新快照视图
func (s *StrategySM) Transition(state State) {
s.mu_sm.Lock() // ✅ 持有 mu_sm
defer s.mu_sm.Unlock()
s.curr = state
s.cache.RefreshView() // ⚠️ 尝试获取 mu_cache(此时A可能已持 mu_cache)
}
逻辑分析:
UpdateAndValidate在持有mu_cache后调用Validate(内部需mu_sm),而Transition在持有mu_sm后调用RefreshView(内部需mu_cache)。二者形成环形等待链。参数s *Snapshot和state State本身无竞态,但锁持有路径不可重入。
预防方案对比
| 方案 | 可行性 | 风险 |
|---|---|---|
全局统一锁顺序(如始终先 mu_sm 后 mu_cache) |
✅ 高 | 需重构所有调用点,易遗漏 |
锁升级为读写锁(RWMutex)分层保护 |
✅ 中 | 写操作仍需双锁,顺序问题未根除 |
| 引入锁超时 + 退避重试 | ❌ 低 | 行情系统对延迟敏感,超时会丢帧 |
根本解决流程
graph TD
A[识别锁依赖图] --> B[提取所有锁获取路径]
B --> C[拓扑排序确定全局偏序]
C --> D[静态检查违反偏序的调用]
D --> E[强制重构为单向锁流]
2.5 select default非阻塞误判:高频做市策略中tick级超时判定引发的goroutine雪崩
问题根源:default分支的“伪非阻塞”陷阱
在tick级响应要求select { default: … }模拟超时,却忽略其零延迟立即执行特性——它不等待通道就绪,也不参与调度公平性。
典型误用代码
func processTick(tick *Tick) {
select {
case out <- tick:
return
default:
// ❌ 错误:此处非“超时”,而是“立即失败”
go fallbackHandler(tick) // 每次tick都启新goroutine!
}
}
逻辑分析:
default永不阻塞,当out通道短暂拥塞(如下游处理抖动),该分支恒成立。参数tick为栈逃逸对象,fallbackHandler持有其引用,导致goroutine无法被GC回收。
雪崩链式反应
- 每秒万级tick → 每秒万级goroutine泄漏
- GC压力指数上升 → STW时间延长 → 更多tick积压 →
default触发更频繁
正确模式对比
| 方案 | 超时精度 | Goroutine生命周期 | 适用场景 |
|---|---|---|---|
select { default: } |
无超时语义 | 每次触发新建,不可控 | ❌ 禁用 |
select { case <-time.After(50*μs): } |
μs级可控 | 复用定时器,自动清理 | ✅ 推荐 |
context.WithTimeout |
纳秒级可组合 | 上下文取消即终止 | ✅ 生产首选 |
修复后核心逻辑
func processTick(ctx context.Context, tick *Tick) error {
select {
case out <- tick:
return nil
case <-time.After(50 * time.Microsecond):
return ErrTickTimeout // 显式错误,非goroutine泄漏
case <-ctx.Done():
return ctx.Err()
}
}
参数说明:
50μs是做市系统最大容忍延迟阈值;ctx由上层ticker循环注入,确保超时与取消信号统一管理。
第三章:内存泄漏驱动的延迟累积与滑点放大机制
3.1 持久化channel未关闭导致的goroutine与buffer双重泄漏
当 channel 被长期持有却未显式关闭,且持续有 goroutine 阻塞在 <-ch 或 ch <- 上时,将同时引发 goroutine 泄漏(无法被调度退出)和 buffer 内存泄漏(底层环形缓冲区持续占用堆内存)。
数据同步机制中的典型误用
func startSyncer(dataCh <-chan int) {
go func() {
for val := range dataCh { // 若 dataCh 永不关闭,此 goroutine 永不退出
process(val)
}
}()
}
逻辑分析:
for range ch会阻塞等待下个元素或 channel 关闭;若上游忘记调用close(dataCh),该 goroutine 将永久休眠,其栈帧与 channel 的buf(若为 buffered channel)均无法被 GC 回收。
泄漏影响对比
| 现象 | 触发条件 | GC 可回收性 |
|---|---|---|
| goroutine 泄漏 | 阻塞在未关闭 channel 的 recv/send | ❌ |
| buffer 内存泄漏 | buffered channel 未关闭 + 有 pending 元素 | ❌(buf 指针被 hchan 持有) |
graph TD
A[启动 sync goroutine] --> B{dataCh 是否关闭?}
B -- 否 --> C[永久阻塞于 range]
B -- 是 --> D[正常退出并释放资源]
C --> E[goroutine + buf 持续驻留]
3.2 sync.Pool误配型泄漏:Tick结构体复用池在多交易所适配器中的生命周期错配
数据同步机制
多个交易所适配器(Binance、OKX、Bybit)共用一个全局 sync.Pool[*Tick],但各交易所 Tick 的字段语义与内存布局存在细微差异:
type Tick struct {
Exchange string // 各适配器写入不同值
Price float64
Volume float64
// ⚠️ 部分适配器额外扩展了未导出字段(如 OKX 的 timestampNs int64)
}
逻辑分析:
sync.Pool不校验类型一致性,Get()返回的*Tick可能残留前一交易所写入的Exchange字符串或未初始化的扩展字段,导致后续json.Marshal()携带脏数据或 panic。
生命周期错配根源
- 适配器 A 在 goroutine 中
Put()一个含timestampNs的Tick - 适配器 B 调用
Get()复用该对象,但其代码未初始化timestampNs→ 读取为随机栈值 - Go GC 无法回收因
sync.Pool引用而长期驻留的脏对象
| 问题维度 | 表现 |
|---|---|
| 内存泄漏 | Pool 中堆积大量 stale Tick |
| 数据污染 | Price/Volume 被旧值覆盖 |
| 并发不安全 | 多 goroutine 竞争复用同一实例 |
graph TD
A[适配器A Put Tick] --> B[Pool 存储]
C[适配器B Get Tick] --> B
B --> D[字段未重置→脏读]
3.3 HTTP client transport复用不足:REST API轮询器中TLS连接池耗尽引发的请求排队恶化
问题现象
轮询器在高并发(>200 QPS)下出现平均延迟陡增(P95 > 3s),net/http.DefaultTransport 的 IdleConnTimeout 默认值(30s)与 TLS 握手开销共同导致连接复用率低于40%。
核心瓶颈分析
// 错误示例:未定制Transport,复用能力受限
client := &http.Client{
Timeout: 10 * time.Second,
} // ❌ 默认MaxIdleConns=100, MaxIdleConnsPerHost=100 → 不足
该配置未适配轮询场景:单主机高频请求下,MaxIdleConnsPerHost=100 被快速占满;TLS握手未启用 session resumption,新建连接占比达65%。
优化配置对比
| 参数 | 默认值 | 推荐值 | 效果 |
|---|---|---|---|
MaxIdleConnsPerHost |
100 | 500 | 提升单域名连接复用容量 |
TLSClientConfig.SessionTicketsDisabled |
false | true | 启用 ticket 复用,降低握手耗时30% |
连接生命周期流程
graph TD
A[发起HTTP请求] --> B{Transport有空闲TLS连接?}
B -->|是| C[复用连接+发送]
B -->|否| D[新建TCP+完整TLS握手]
D --> E[加入idle pool]
C --> F[响应返回]
E --> F
第四章:pprof诊断体系在量化低延迟系统中的定制化落地
4.1 火焰图采样策略调优:-gcflags=”-m”与-gcpolicy=hybrid在回测/实盘环境的差异化配置
火焰图精度高度依赖 GC 行为可观测性。回测环境需深度内联分析,实盘则须抑制日志开销。
回测环境:启用编译期逃逸分析与混合 GC 策略
go build -gcflags="-m -m" -gcpolicy=hybrid -o backtest ./cmd/backtest
-m -m 输出两级逃逸分析(函数级 + 变量级),暴露栈上分配瓶颈;-gcpolicy=hybrid 启用分代+区域混合回收,使火焰图中 runtime.mallocgc 栈帧更贴近真实分配热点。
实盘环境:静默编译提示,保留 GC 策略一致性
go build -gcflags="-m=2" -gcpolicy=hybrid -ldflags="-s -w" -o trading ./cmd/trading
-m=2 仅报告关键逃逸决策(非冗余日志),避免干扰 perf record 采样时序;-s -w 剥离符号表,缩小二进制体积,提升 perf script 解析效率。
| 场景 | -gcflags | -gcpolicy | 目标 |
|---|---|---|---|
| 回测 | -m -m |
hybrid |
定位虚假堆分配 |
| 实盘 | -m=2 |
hybrid |
平衡可观测性与吞吐稳定性 |
graph TD
A[Go 编译] --> B{-gcflags}
B --> C["-m -m:回测深度诊断"]
B --> D["-m=2:实盘轻量提示"]
A --> E{-gcpolicy}
E --> F[hybrid:统一内存视图]
F --> G[火焰图中 GC 栈深度一致]
4.2 goroutine dump语义解析模板:从runtime.Stack输出精准定位阻塞点与泄漏源头
runtime.Stack 输出的 goroutine dump 是诊断阻塞与泄漏的原始金矿,但需结构化解析才能释放其价值。
核心字段语义映射
goroutine N [status]→ 状态(runnable/syscall/IO wait/semacquire)直接指示调度卡点created by ...→ 追溯启动源头,识别长期存活 goroutineselect,chan receive,semacquire→ 典型阻塞信号
阻塞模式识别表
| 状态片段 | 含义 | 关联风险 |
|---|---|---|
semacquire |
竞争锁或 channel send | 死锁、资源未释放 |
chan receive |
无缓冲 channel 等待接收 | 发送方缺失或逻辑遗漏 |
select (no cases) |
select{} 永久挂起 |
误用空 select 导致泄漏 |
buf := make([]byte, 1024*1024)
n := runtime.Stack(buf, true) // true: all goroutines; n: actual bytes written
dump := string(buf[:n])
runtime.Stack(buf, true)获取全量 goroutine 快照;buf需足够大(建议 ≥1MB),否则截断导致关键帧丢失;true参数不可省略,否则仅捕获当前 goroutine。
自动化解析流程
graph TD
A[获取 Stack 输出] --> B{按 “goroutine \\d+” 分块}
B --> C[提取状态 & 栈顶函数]
C --> D[匹配阻塞模式正则]
D --> E[聚合相同阻塞原因的 goroutine ID]
4.3 heap profile时空切片分析法:基于pprof –base识别因子计算链中持续增长的slice引用链
传统 heap profile 难以定位跨时间窗口持续累积的 slice 引用泄漏。pprof --base 提供差分能力,但需结合时空切片(time-sliced delta profiling)才能暴露隐性增长链。
核心分析流程
- 采集 T₁、T₂ 两个时间点的 heap profile(
-memprofile=heap_T1.prof/heap_T2.prof) - 执行差分:
go tool pprof --base heap_T1.prof heap_T2.prof - 按
--unit=MB --focus=slice过滤并排序增长最显著的调用路径
示例命令与输出解析
# 生成带采样精度的时间切片 profile(每5s采集一次,共60s)
GODEBUG=gctrace=1 go run -gcflags="-m" main.go 2>&1 | \
grep -E "(slice|alloc)" | head -20
此命令不直接生成 profile,而是辅助验证 slice 分配热点;真实分析需依赖
runtime.MemProfileRate=1+pprof.WriteHeapProfile定时快照。
差分结果关键字段含义
| 字段 | 含义 | 示例值 |
|---|---|---|
inuse_space |
当前驻留内存增量 | +12.4 MB |
flat |
该函数直接分配量 | +8.2 MB |
cum |
该函数及其下游累计分配 | +12.4 MB |
graph TD
A[heap_T1.prof] -->|pprof --base| B[delta profile]
C[heap_T2.prof] --> B
B --> D[按 alloc_space 排序]
D --> E[定位 top3 slice 创建者]
E --> F[追溯 runtime.growslice 调用链]
4.4 trace profile低延迟校准:通过go tool trace标记关键路径(如OrderSend→AckReceive)并量化goroutine调度抖动
标记关键路径
使用 runtime/trace API 显式标注业务关键链路,例如订单发送与确认接收之间的时间窗口:
import "runtime/trace"
func OrderSend(orderID string) {
trace.WithRegion(context.Background(), "OrderSend", func() {
// 发送逻辑...
AckReceive(orderID) // 关键依赖
})
}
func AckReceive(orderID string) {
trace.WithRegion(context.Background(), "AckReceive", func() {
// 确认处理...
})
}
该代码在 go tool trace 中生成可识别的用户事件区域,WithRegion 自动绑定 goroutine 生命周期,支持跨 goroutine 关联(需共享 context)。
量化调度抖动
运行时采集后,用 go tool trace 分析 SCHED 视图中 Goroutine Schedule Delay 指标:
| 指标 | 含义 | 健康阈值 |
|---|---|---|
| Max Schedule Delay | 单次调度最大等待时长 | |
| P99 Schedule Delay | 99% 调度延迟上限 | |
| Goroutines Blocked | 阻塞态 goroutine 数量 | ≈ 0 |
调度延迟归因流程
graph TD
A[OrderSend 开始] --> B{进入 runtime.schedule}
B --> C[就绪队列等待]
C --> D[被 M 抢占或休眠唤醒延迟]
D --> E[AckReceive 执行]
第五章:构建抗滑点的Go量化运行时防护体系
在高频量化交易系统中,运行时异常往往以“滑点”形式暴露——订单延迟、价格跳变、撮合失败等现象背后,常是Go运行时资源争用、GC停顿、网络抖动或监控盲区导致的瞬态故障。本章基于某实盘CTA策略引擎(日均处理120万笔委托、峰值QPS 8400)的防护升级实践,阐述如何构建具备滑点感知与自动抑制能力的Go运行时防护体系。
滑点敏感型指标埋点设计
不再依赖传统CPU/Mem基础监控,而是注入三类滑点关联指标:order_latency_p99_ms(从下单到交易所确认的端到端P99延迟)、price_drift_bp(下单价与成交价偏差基点)、gc_pause_ms_5s_avg(每5秒内GC STW平均时长)。所有指标通过expvar暴露,并由Prometheus每2秒抓取,避免监控采样间隔掩盖毫秒级抖动。
运行时自适应限流熔断
采用双环路控制:外环基于price_drift_bp > 15bp持续3次触发熔断,暂停新订单;内环实时计算runtime.ReadMemStats()中的PauseNs序列,当最近10次GC暂停总和超过8ms时,动态降低goroutine并发数(通过semaphore.NewWeighted(int64(100 - pause_weight))实现权重调节)。以下为关键熔断逻辑片段:
func (e *Engine) checkSlippageGuard() {
if drift, _ := getAvgDrift(); drift > 15.0 && driftCounter.Inc() >= 3 {
e.orderChan = nil // 熔断通道
log.Warn("slippage guard activated: price_drift_bp=%.1f", drift)
}
}
网络层滑点隔离机制
使用net.Dialer定制TCP连接池,对不同交易所API启用独立连接组,并配置差异化超时: |
交易所 | 连接超时 | 读超时 | 写超时 | 滑点容忍阈值 |
|---|---|---|---|---|---|
| CTP | 300ms | 800ms | 500ms | ±8bp | |
| CFFEX | 200ms | 600ms | 400ms | ±5bp | |
| DCE | 400ms | 1200ms | 600ms | ±12bp |
当单次请求耗时超过对应读超时×1.8时,自动标记该连接为“高滑点风险”,后续请求绕过该连接并触发重连。
GC行为实时干预
通过debug.SetGCPercent(-1)禁用自动GC后,采用手动触发策略:当runtime.MemStats.Alloc增量超32MB/秒且PauseTotalNs上升斜率>500ns/ms时,立即调用runtime.GC()并记录trace。此机制使CTP网关节点GC停顿从均值12.7ms降至3.2ms,滑点事件下降63%。
策略执行沙箱化隔离
每个策略实例运行于独立*exec.Cmd启动的轻量沙箱进程(非容器),通过Unix Domain Socket通信。沙箱内强制设置GOMAXPROCS=2、GODEBUG=madvdontneed=1,并监听SIGUSR1信号执行runtime/debug.FreeOSMemory()。某次行情剧烈波动期间,主进程因内存碎片卡顿,而沙箱策略仍保持12ms内完成信号计算与下单。
滑点根因回溯流水线
所有订单生成时打上trace_id,经Kafka写入ClickHouse,关联字段包括:order_time_unix, exchange_ts, local_ts, gc_pause_ns, mem_alloc_kb, goroutines_count。通过如下SQL可定位滑点根因:
SELECT
toStartOfMinute(order_time) as minute,
avg(price_drift_bp) as avg_drift,
max(gc_pause_ns)/1e6 as max_pause_ms,
countIf(goroutines_count > 1200) as high_goroutines_cnt
FROM quant_orders
WHERE order_time > now() - INTERVAL 1 HOUR
GROUP BY minute
HAVING avg_drift > 10
ORDER BY minute DESC
LIMIT 20
防护效果验证数据
上线后连续30个交易日统计显示:单日最大滑点由平均23.6bp降至6.1bp;因GC引发的订单延迟>50ms事件归零;网络层滑点相关错误日志下降91.7%;沙箱策略实例崩溃率从0.023次/千次执行降至0.0004次/千次执行。
