第一章:Go sync.Pool误用导致连接池雪崩:耗子哥绘制的Pool对象生命周期状态机图谱
sync.Pool 并非通用对象缓存,而是专为短期、可丢弃、高分配频率的临时对象设计。当开发者将其错误用于管理长生命周期资源(如数据库连接、HTTP客户端连接)时,会触发“假性健康—突发雪崩”的典型故障模式:Pool 在 GC 周期被清空,而业务流量未衰减,导致所有 goroutine 同步新建连接,瞬间压垮下游服务。
耗子哥在 GopherCon 分享中提出的 Pool 状态机图谱,精准刻画了三个核心状态:
Idle:对象被 Put 进 Pool 后处于待复用状态,但不保证存活至下次 GetStale:经历至少一次 GC 后未被复用的对象,将被 runtime 无条件回收Evicted:对象已被 GC 回收,对应内存地址失效,再次 Get 将返回 nil 或新分配对象
常见误用模式包括:
- ❌ 将 sql.DB 或 http.Client 实例存入 Pool(它们本身已是连接池,且需 Close/Shutdown)
- ❌ 在 HTTP handler 中 Put 一个带活跃 net.Conn 的结构体(连接可能已关闭,复用导致 write: broken pipe)
- ❌ 依赖 Pool 保活连接以规避建连开销(违背 Pool “无状态临时缓冲”语义)
正确实践应严格限定对象边界:
// ✅ 正确:缓存 byte.Buffer(无外部依赖、零值可重用)
var bufferPool = sync.Pool{
New: func() interface{} {
return new(bytes.Buffer) // New 必须返回可立即安全使用的对象
},
}
func handleRequest(w http.ResponseWriter, r *http.Request) {
buf := bufferPool.Get().(*bytes.Buffer)
buf.Reset() // 关键:重置内部状态,避免残留数据污染
defer bufferPool.Put(buf) // 必须在函数退出前 Put,不可跨 goroutine 持有
}
关键原则:Put 的对象必须是当前 goroutine 完全拥有、无外部引用、且复用前可安全 Reset 的临时载体。 一旦违反,Pool 不再是性能加速器,而成为隐蔽的雪崩扳机。
第二章:sync.Pool底层机制与状态机本质
2.1 Pool对象的初始化与本地P缓存绑定原理
Pool对象在首次调用 sync.Pool.Get() 时触发惰性初始化,其核心在于将 runtime.P(调度器本地处理器)与 poolLocal 实例静态绑定。
初始化时机与结构分配
- 每个
P对应唯一poolLocal,存储于全局poolLocalPool(类型为sync.Pool)中 poolLocal包含私有缓存private和共享队列shared(*[]interface{},需原子操作)
本地P绑定机制
func (p *Pool) getSlow() interface{} {
size := atomic.LoadUintptr(&poolLocalSize)
l := p.local[uintptr(procid)%size] // 基于P ID哈希定位local实例
return l.private // 优先返回P专属private字段
}
procid由getg().m.p.ptr().id获取,确保每个 Goroutine 在固定 P 上始终访问同一poolLocal;private无锁直取,shared则需atomic.Load/Store保护。
| 字段 | 访问方式 | 线程安全 | 典型场景 |
|---|---|---|---|
private |
直接读写 | 是(仅本P) | 高频单次Get/Put |
shared |
原子操作 | 是 | 跨P借用(GC后) |
graph TD
A[Goroutine调用Get] --> B{P已绑定local?}
B -->|否| C[从poolLocalPool.New创建poolLocal]
B -->|是| D[读取l.private]
D --> E[非空?]
E -->|是| F[返回并置nil]
E -->|否| G[尝试pop shared]
2.2 Get/put操作在M、P、G协同下的状态跃迁路径
G的调度状态跃迁触发点
get/put操作并非直接修改G状态,而是通过runtime·park()与runtime·ready()间接驱动G在_Grunnable ↔ _Gwaiting间切换。
M-P-G协作关键路径
get阻塞时:G →_Gwaiting→ 入P本地队列或全局队列put就绪时:唤醒G →_Grunnable→ 被M窃取或抢占执行
// runtime/chan.go 简化片段
func chansend(c *hchan, ep unsafe.Pointer, block bool) bool {
// ... 省略锁逻辑
if c.qcount < c.dataqsiz { // 缓冲区有空位
typedmemmove(c.elemtype, qp, ep)
c.qcount++
return true
}
// 否则:G入sudog链表,状态设为_Gwaiting
}
该代码中c.qcount决定是否绕过阻塞;sudog绑定G与channel,是状态跃迁的载体。参数block控制是否允许挂起——false时立即返回false,不触发状态变更。
状态跃迁对照表
| 操作 | G原状态 | 触发动作 | 新状态 |
|---|---|---|---|
| get(空chan) | _Grunning | park() + en-queue | _Gwaiting |
| put(满chan) | _Grunning | park() + sudog入队 | _Gwaiting |
| 唤醒响应 | _Gwaiting | ready() | _Grunnable |
graph TD
A[G._Grunning] -->|chansend on full| B[G._Gwaiting]
B -->|recv wakes| C[G._Grunnable]
C -->|schedule by M| D[M executes G]
2.3 GC触发时poolCleanup对victim与poolLocal的原子切换实践
数据同步机制
GC触发时,poolCleanup需确保victim(待回收旧本地池)与poolLocal(新分配槽位)的零竞争切换。核心依赖atomic.SwapPointer实现无锁原子替换。
// 原子切换 victim → poolLocal
old := atomic.SwapPointer(&p.local, unsafe.Pointer(&newLocal))
victim := (*[64]poolLocal)(old)
p.local为*poolLocal指针,指向当前活跃本地池数组;SwapPointer返回旧地址并原子更新为&newLocal;victim被强制转换为固定长度数组,供后续批量清理。
切换状态对比
| 阶段 | p.local 指向 |
victim 状态 |
|---|---|---|
| 切换前 | 旧 poolLocal | 未定义(需显式捕获) |
SwapPointer后 |
新 poolLocal | 旧数组地址(已隔离) |
执行流程
graph TD
A[GC开始] --> B[调用 poolCleanup]
B --> C[atomic.SwapPointer 更新 p.local]
C --> D[将返回值转为 victim 数组]
D --> E[遍历 victim 中每个 poolLocal 清空私有队列]
- 切换后,新 goroutine 将绑定至
newLocal; victim仅被当前清理协程独占访问,避免读写冲突。
2.4 状态机中“未初始化→活跃→被GC标记→归零重置”四阶段实测验证
为验证状态机生命周期的严格性,我们在 StatefulResource 类中嵌入原子状态变量与 GC 钩子:
public enum ResourceState {
UNINITIALIZED, ACTIVE, MARKED_FOR_GC, ZEROED
}
// state 受 volatile 保护,ensureStateTransition() 校验跃迁合法性(如禁止跳过 MARKED_FOR_GC 直接归零)
逻辑分析:volatile 保证跨线程可见性;ensureStateTransition() 内部通过 CAS 检查前驱状态,参数 from 和 to 构成有向边约束。
四阶段跃迁约束表
| 当前状态 | 允许跃迁至 | 禁止原因 |
|---|---|---|
| UNINITIALIZED | ACTIVE | 未初始化不可直接标记GC |
| ACTIVE | MARKED_FOR_GC | 必须经显式回收触发 |
| MARKED_FOR_GC | ZEROED | 归零是唯一安全终态 |
状态流转图
graph TD
A[UNINITIALIZED] --> B[ACTIVE]
B --> C[MARKED_FOR_GC]
C --> D[ZEROED]
2.5 基于pprof+debug.ReadGCStats还原真实Pool生命周期轨迹
Go 的 sync.Pool 生命周期难以直接观测——它既不暴露内部状态,也不提供创建/销毁钩子。但可通过双源信号交叉验证:运行时采样(pprof)与垃圾回收统计(debug.ReadGCStats)协同还原对象驻留轨迹。
GC 统计锚定内存驻留窗口
调用 debug.ReadGCStats 获取各次 GC 的 PauseEnd 时间戳与 NumGC,结合 runtime.ReadMemStats 中的 Mallocs/Frees 差值,可定位 Pool 对象被回收的 GC 轮次:
var s runtime.GCStats
debug.ReadGCStats(&s)
fmt.Printf("Last GC: %v, Total: %d\n", s.PauseEnd[0], s.NumGC) // PauseEnd[0] 是最新一次GC结束纳秒时间戳
PauseEnd是单调递增的时间戳(纳秒),可用于对齐 pprof profile 的time.Now()采样点;NumGC指示已发生 GC 次数,辅助判断 Pool Put/Get 是否跨越 GC 边界。
pprof CPU/heap profile 关联对象归属
启用 GODEBUG=gctrace=1 并采集 runtime/pprof 的 heap profile,通过 --inuse_space 或 --alloc_space 视角识别 Pool 缓存对象的分配堆栈。
| 指标 | 含义 | 用于推断 |
|---|---|---|
sync.Pool.getSlow 调用频次 |
表明 Miss 率高,可能因 GC 清空或竞争激烈 | Pool 失效周期短 |
runtime.mallocgc 栈中含 sync.(*Pool).pin |
对象正被 Pool pin 住(即处于 Get 后、Put 前) | 实时驻留状态 |
生命周期重建逻辑
graph TD
A[New object allocated] --> B{Put to Pool?}
B -->|Yes| C[Object marked 'cached']
B -->|No| D[Immediate GC candidate]
C --> E[Next GC: cleared if unused]
E --> F[ReadGCStats.NumGC increment → timestamp window]
F --> G[pprof heap allocs between GCs → Pool reuse count]
第三章:典型误用场景与雪崩根因分析
3.1 将非零值对象Put回Pool引发的内存语义污染实战复现
当 sync.Pool 的 Put 方法接收已使用且未重置字段的非零值对象时,后续 Get 可能返回携带脏状态的对象,导致隐式状态泄露。
复现场景代码
var bufPool = sync.Pool{
New: func() interface{} { return new(bytes.Buffer) },
}
func badReuse() {
buf := bufPool.Get().(*bytes.Buffer)
buf.WriteString("hello") // 写入数据 → 字段非零
bufPool.Put(buf) // ❌ 错误:未清空即归还
next := bufPool.Get().(*bytes.Buffer)
fmt.Println(next.String()) // 输出 "hello" —— 语义污染!
}
逻辑分析:
bytes.Buffer的write操作修改内部buf []byte和off int;Put未调用Reset(),导致Get返回带残留数据的实例。关键参数:buf的len(buf.buf)和buf.off均非零。
污染传播路径
graph TD
A[Put 非零对象] --> B[Pool 缓存脏实例]
B --> C[Get 返回未重置对象]
C --> D[业务逻辑误读残留字段]
正确实践对照表
| 操作 | 是否安全 | 原因 |
|---|---|---|
buf.Reset() 后 Put |
✅ | 归零 buf, off, cap |
直接 Put(buf) |
❌ | 保留 buf 中所有字段值 |
3.2 在goroutine泄漏场景下Pool本地队列无限膨胀的压测证据
复现泄漏的基准压测脚本
以下代码模拟持续创建 goroutine 但永不释放 sync.Pool 对象:
var leakyPool = sync.Pool{
New: func() interface{} { return make([]byte, 0, 1024) },
}
func leakyWorker(id int) {
for i := 0; i < 1e4; i++ {
buf := leakyPool.Get().([]byte)
buf = append(buf, "leak"...)
// ❌ 忘记 Put 回池中 → goroutine 泄漏 + 本地队列持续累积
runtime.Gosched()
}
}
// 启动 1000 个长期存活 goroutine(不 await)
for i := 0; i < 1000; i++ {
go leakyWorker(i)
}
逻辑分析:
sync.Pool的本地队列(poolLocal.private+poolLocal.shared)在无Put调用时,对象永不回收;每个 P 绑定的poolLocal的shared切片会随Get频次指数扩容(grow触发append),且无 GC 清理机制。
关键观测指标对比(压测 60s 后)
| 指标 | 正常场景 | 泄漏场景 |
|---|---|---|
runtime.ReadMemStats().Mallocs |
~2.1e5 | ~8.9e6 |
平均 poolLocal.shared 长度 |
0–3 | >12,000(单 P) |
| Goroutine 数量 | ~10 | >1020 |
内存增长路径
graph TD
A[goroutine 创建] --> B[调用 Get]
B --> C{Put 被跳过?}
C -->|是| D[对象滞留本地队列]
C -->|否| E[正常复用]
D --> F[shared 切片扩容]
F --> G[内存持续占用+GC 不扫描]
3.3 HTTP长连接池混用sync.Pool导致fd泄漏与TIME_WAIT激增案例
问题现象
线上服务在QPS上升后出现netstat -an | grep TIME_WAIT | wc -l持续突破6万,同时lsof -p $PID | wc -l显示打开文件数缓慢增长,重启后回落——典型fd泄漏+连接复用失效。
根本原因
错误地将*http.Transport实例放入sync.Pool,而Transport内部的idleConn map持有未关闭的net.Conn,且Put()不触发CloseIdleConnections():
var transportPool = sync.Pool{
New: func() interface{} {
return &http.Transport{ // ❌ 错误:Transport非无状态对象
MaxIdleConns: 100,
MaxIdleConnsPerHost: 100,
}
},
}
sync.Pool.Put()仅回收对象指针,不调用任何清理逻辑;Transport.idleConn中存活的连接仍持有socket fd,且keep-alive超时后进入TIME_WAIT,但因无引用无法被GC或主动关闭。
修复方案对比
| 方案 | 是否解决fd泄漏 | 是否降低TIME_WAIT | 备注 |
|---|---|---|---|
移除sync.Pool,全局复用单例Transport |
✅ | ✅ | 推荐,符合HTTP/1.1连接复用语义 |
Put()前手动调用CloseIdleConnections() |
✅ | ⚠️(延迟生效) | 需确保无并发RoundTrip调用 |
改用&http.Client{}池化(仍需谨慎) |
❌(Client不持连接) | ❌(未解决底层Transport问题) | 无效 |
正确实践
只池化无状态、可重置对象(如预分配的bytes.Buffer),HTTP连接管理交由http.Transport自身生命周期控制。
第四章:高可靠连接池重构方案与工程落地
4.1 基于channel+worker goroutine的无状态连接复用模型实现
该模型摒弃连接池绑定,将连接抽象为可安全复用的“资源令牌”,通过 channel 统一调度,由固定 worker goroutine 池执行 I/O。
核心组件职责
connChan:chan *Conn,生产者(业务协程)投递空闲连接,消费者(worker)从中获取workerPool: 固定数量 goroutine,循环阻塞读取connChan,执行读写/心跳/回收逻辑idleTimeout: 连接空闲超时后自动关闭,保障资源新鲜度
连接复用流程(mermaid)
graph TD
A[业务协程] -->|Put conn to channel| B(connChan)
B --> C{Worker Goroutine}
C --> D[校验健康状态]
D -->|OK| E[执行请求]
D -->|Failed| F[关闭并丢弃]
E --> G[归还 conn 到 connChan]
示例:worker 处理循环
func startWorker(connChan <-chan *Conn, done chan struct{}) {
for {
select {
case conn := <-connChan:
if conn.IsHealthy() {
conn.ServeRequest() // 同步处理,不阻塞 channel 接收
go func(c *Conn) { c.ReturnToPool() }(conn) // 异步归还
} else {
conn.Close()
}
case <-done:
return
}
}
}
connChan 是无缓冲 channel,天然限流;ReturnToPool() 内部调用 connChan <- conn 实现归还;done 用于优雅退出。所有连接操作无共享状态,完全无锁。
4.2 使用unsafe.Pointer绕过反射开销构建零拷贝Pool适配层
Go 标准库 sync.Pool 虽高效,但泛型缺失前常依赖 interface{} 存储,触发逃逸与反射类型检查。为消除运行时开销,可结合 unsafe.Pointer 构建类型固定、零分配的 Pool 适配层。
核心思路
- 预分配连续内存块(如
[]byte) - 用
unsafe.Pointer直接转换为具体结构体指针 - 手动管理生命周期,规避 GC 扫描与类型断言
内存布局示意图
graph TD
A[Pool.Get] --> B[从 slab 分配 uintptr]
B --> C[unsafe.Pointer → *T]
C --> D[直接构造/复用对象]
关键代码片段
func (p *zeroCopyPool[T]) Get() *T {
ptr := p.slabs.alloc() // 返回 uintptr
return (*T)(unsafe.Pointer(ptr)) // 零成本类型绑定
}
ptr 是预对齐的内存地址;(*T)(unsafe.Pointer(ptr)) 绕过 reflect.TypeOf 和 interface{} 拆箱,避免反射调用与接口字典查找——实测降低 Get() 延迟 40%+(基准测试:100ns → 60ns)。
| 对比维度 | interface{} Pool | unsafe.Pointer Pool |
|---|---|---|
| 类型检查开销 | ✅ 反射调用 | ❌ 编译期确定 |
| 内存分配次数 | 每次 Get/put | 预分配,仅复用 |
| GC 扫描压力 | 高(含指针字段) | 可设为 no-pointers |
4.3 结合context.WithTimeout实现连接获取超时与自动驱逐策略
在高并发连接池场景中,单纯依赖 sync.Pool 无法解决连接获取阻塞问题。context.WithTimeout 提供了优雅的超时控制能力。
超时获取连接的核心逻辑
func (p *ConnPool) Get(ctx context.Context) (*Conn, error) {
// 使用 WithTimeout 包裹原始上下文,限定获取连接的最大等待时间
timeoutCtx, cancel := context.WithTimeout(ctx, p.getTimeout)
defer cancel()
select {
case conn := <-p.connCh:
return conn, nil
case <-timeoutCtx.Done():
return nil, fmt.Errorf("failed to get connection: %w", timeoutCtx.Err())
}
}
逻辑分析:
context.WithTimeout创建子上下文,当超过p.getTimeout(如500ms)时触发Done()。select避免永久阻塞;cancel()防止 Goroutine 泄漏。参数p.getTimeout应独立配置,区别于连接空闲超时。
自动驱逐策略协同机制
- 连接空闲超时由后台 goroutine 定期扫描
time.Since(lastUsed) > idleTimeout - 获取超时失败后,触发
p.metrics.RecordGetTimeout() - 驱逐阈值动态调整:当连续3次超时,临时降低最大空闲连接数(防雪崩)
| 策略维度 | 作用目标 | 依赖机制 |
|---|---|---|
| 获取超时 | 防止调用方卡死 | context.WithTimeout |
| 空闲驱逐 | 回收无效连接 | time.Timer + 扫描队列 |
| 负载感知驱逐 | 动态缩容 | 超时率+QPS滑动窗口 |
graph TD
A[调用 Get] --> B{context.Done?}
B -- Yes --> C[返回 timeout 错误]
B -- No --> D[从 connCh 取连接]
D --> E[更新 lastUsed 时间]
E --> F[返回有效连接]
4.4 在gRPC client interceptor中嵌入Pool健康度指标上报模块
在高并发微服务场景下,连接池(如 grpc-go 的 ClientConn 底层连接池)的健康状态直接影响调用成功率与延迟。将健康度指标(空闲连接数、最大连接数、创建/关闭速率、等待超时次数)实时注入 client interceptor,可实现零侵入式可观测性增强。
指标采集点设计
- 在
UnaryClientInterceptor的before阶段读取ClientConn.GetState()与自定义poolStats - 在
after阶段捕获失败原因并更新failedByReason计数器
上报逻辑实现
func poolHealthReporter(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
stats := getPoolStats(cc) // 从 cc 连接池扩展字段提取
metrics.PoolIdleConnections.WithLabelValues(method).Set(float64(stats.Idle))
metrics.PoolWaitDurationSeconds.WithLabelValues(method).Observe(stats.AvgWaitSec)
return invoker(ctx, method, req, reply, cc, opts...)
}
getPoolStats(cc) 通过反射访问 cc.dopts.channelzParentID 关联的 internal.TransportMonitor 实例;AvgWaitSec 来自 transport.WaitTime 原子计数器,精度为毫秒级浮点值。
关键指标维度表
| 指标名 | 类型 | 标签 | 说明 |
|---|---|---|---|
pool_idle_connections |
Gauge | method |
当前空闲连接数 |
pool_wait_duration_seconds |
Histogram | method |
请求等待连接的耗时分布 |
graph TD
A[Interceptor Entry] --> B{Is Pool Metric Enabled?}
B -->|Yes| C[Read poolStats from cc]
B -->|No| D[Skip reporting]
C --> E[Update Prometheus metrics]
E --> F[Invoke downstream]
第五章:从状态机到系统韧性——写给每一位Go基础设施工程师
在高并发微服务架构中,状态一致性从来不是“可选项”,而是基础设施工程师每天必须直面的硬约束。我们曾在线上遭遇过一个典型故障:支付网关在分布式事务超时后,将同一笔订单重复投递至下游账务服务,导致资金双扣。根因并非网络分区,而是状态机设计缺失显式终态校验与幂等跃迁约束。
状态机不是UML草图,而是运行时契约
我们用 go:generate 结合 statemachine 库自动生成状态跃迁校验代码。例如定义订单状态机:
type OrderState string
const (
StateCreated OrderState = "created"
StatePaid OrderState = "paid"
StateShipped OrderState = "shipped"
StateRefunded OrderState = "refunded"
)
// 自动生成的跃迁规则表(部分)
var ValidTransitions = map[OrderState][]OrderState{
StateCreated: {StatePaid, StateRefunded},
StatePaid: {StateShipped, StateRefunded},
StateShipped: {StateRefunded},
StateRefunded: {},
}
幂等性必须绑定状态版本号而非业务ID
在 Kafka 消费者中,我们弃用单纯 order_id 做去重键,改为 (order_id, expected_state_version) 复合键。当消费者收到 order_id=1001, state=paid, version=3 时,先查 Redis 中当前 order:1001:state_version 是否为 2;若匹配,则原子执行 SET order:1001:state_version 3 EX 86400 NX,成功后才更新业务状态。该机制使幂等窗口从“单次消费”扩展为“状态版本窗口”。
超时处理需区分三类状态跃迁
| 超时类型 | 状态跃迁示例 | 补偿动作 | 触发条件 |
|---|---|---|---|
| 预期内未完成 | created → paid(30s未回) | 发起对账查询+人工介入工单 | 超时且无任何下游回调记录 |
| 部分成功 | paid → shipped(物流API返回503) | 重试+降级为异步轮询物流单号 | 收到HTTP 200但响应体含error字段 |
| 终态冲突 | shipped → refunded(但已发货) | 拒绝跃迁并返回409 Conflict | 当前DB中shipped_at非空 |
熔断器嵌入状态机生命周期
我们改造了 hystrix-go,使其在 BeforeTransition 钩子中动态读取当前服务健康度指标。当 payment-service 的 P99 延迟 > 2s 且错误率 > 5%,自动阻断所有 created → paid 跃迁,并返回 503 Service Unavailable 附带 Retry-After: 30。该策略避免了雪崩式状态卡顿。
日志必须携带状态跃迁上下文
每条日志强制注入 state_transition_id(UUIDv4)、from_state、to_state、transition_duration_ms 字段。ELK 中通过 state_transition_id 可串联完整链路:从 Kafka 消息接收、DB 更新、第三方调用、到最终事件发布。某次排查发现 7.3% 的 paid → shipped 跃迁耗时异常,根源是物流接口 TLS 握手在特定 OpenSSL 版本下存在 2.1s 固定延迟。
监控指标直接映射状态跃迁失败原因
使用 Prometheus 自定义指标:
# 统计各跃迁路径失败率(按错误码维度)
sum(rate(state_transition_failure_total{job="order-svc"}[1h])) by (from_state, to_state, error_code)
当 created→paid 的 error_code="PAYMENT_TIMEOUT" 突增时,告警自动关联支付渠道熔断状态与 Redis 连接池饱和度。
测试必须覆盖非法跃迁与并发冲突
编写 TestStateTransitionRace:启动 100 个 goroutine 并发尝试将同一订单从 created 跃迁至 paid 和 refunded,验证最终状态必为 paid 或 refunded 之一,且 DB 中 version 字段严格单调递增。该测试在 CI 中捕获出 GORM 的 SelectForUpdate 在 PostgreSQL 12 下未正确加锁的 bug。
灰度发布需同步演进状态机定义
新版本服务上线前,先将新版状态机 JSON Schema 推送至 Consul KV,旧版服务读取后拒绝执行未知跃迁(如 shipped → canceled)。灰度期间双版本共存,状态跃迁语义始终保持向后兼容。
状态机不是教科书里的抽象模型,它是每一行 if err != nil 后面必须校验的 currentState == expected,是每个 UPDATE ... WHERE version = ? 语句里不容妥协的乐观锁,更是凌晨三点告警电话响起时,你打开 Grafana 查看 state_transition_duration_seconds_bucket 分位图的第一眼依据。
