第一章:Golang调度系统的核心架构与演进脉络
Go 调度器(Goroutine Scheduler)是运行时(runtime)最核心的子系统之一,其设计目标是在用户态高效复用操作系统线程(OS Threads),实现轻量级协程(Goroutine)的并发执行。它摒弃了传统“一对一”或“多对一”线程模型,采用独特的 G-M-P 三层协作模型:G(Goroutine)代表待执行的任务单元;M(Machine)对应绑定 OS 线程的运行上下文;P(Processor)则是调度所需的逻辑处理器资源,承载本地可运行队列(runq)、全局队列(runqge)、定时器、内存分配缓存等关键状态。
调度器的代际演进
早期 Go 1.0 使用 G-M 模型,所有 Goroutine 共享单个全局运行队列,易因锁竞争导致扩展性瓶颈。Go 1.1 引入 P 结构,将调度单位局部化——每个 P 拥有独立的本地运行队列(最多 256 个 G),显著降低锁争用;Go 1.2 实现 work-stealing 机制,空闲 P 可从其他 P 的本地队列或全局队列中窃取任务;Go 1.14 增加异步抢占(asynchronous preemption),通过信号中断长时间运行的 G,解决因 for{} 循环阻塞调度的问题。
关键数据结构与运行时观测
可通过 GODEBUG=schedtrace=1000 启用调度器追踪,每秒输出当前 M/P/G 状态摘要:
GODEBUG=schedtrace=1000 ./myapp
# 输出示例:
SCHED 0ms: gomaxprocs=8 idleprocs=2 threads=12 spinningthreads=0 idlethreads=4 runqueue=0 [0 0 0 0 0 0 0 0]
其中 runqueue 后方的方括号表示各 P 的本地队列长度,可用于快速识别负载不均问题。
调度器与操作系统线程的绑定关系
| 组件 | 数量约束 | 动态性 |
|---|---|---|
| G | 无硬上限(受限于内存) | 创建/销毁频繁,开销极低(约 2KB 栈) |
| M | 默认 ≤ GOMAXPROCS,但可临时超出(如系统调用阻塞时) |
受 runtime.LockOSThread() 影响 |
| P | 固定为 GOMAXPROCS(默认等于 CPU 核心数) |
启动后不可变,需重启调整 |
当 Goroutine 执行系统调用时,M 会脱离 P 并进入阻塞状态,此时 runtime 会唤醒或创建新 M 来接管该 P,确保 P 始终有 M 可用——这是 Go 实现高并发 I/O 的底层保障。
第二章:GOMAXPROCS配置陷阱与性能反模式
2.1 GOMAXPROCS的语义本质与运行时动态约束
GOMAXPROCS 并非线程池大小配置,而是调度器可并行执行用户 Goroutine 的 OS 线程(M)上限,其值直接约束 P(Processor)的数量——每个 P 必须绑定一个可用 M 才能运行。
运行时约束机制
- 启动时默认设为
runtime.NumCPU() - 可通过
runtime.GOMAXPROCS(n)动态调整,但仅影响后续调度,不中断当前运行的P - 若
n < 当前P数,多余P进入idle状态,不再接收新 Goroutine
动态调整示例
package main
import (
"fmt"
"runtime"
"time"
)
func main() {
fmt.Println("Initial:", runtime.GOMAXPROCS(0)) // 获取当前值
runtime.GOMAXPROCS(2)
fmt.Println("After set:", runtime.GOMAXPROCS(0))
time.Sleep(time.Millisecond) // 触发调度器感知变更
}
此代码中
runtime.GOMAXPROCS(0)是只读查询;传入正整数才触发重配置。调整后,运行时会回收/唤醒P,但已有阻塞在系统调用中的M不受立即影响。
| 场景 | P 状态变化 | 调度影响 |
|---|---|---|
GOMAXPROCS(1) |
多余 P → idle | 新 Goroutine 排队等待可用 P |
GOMAXPROCS(8) |
创建新 P(若资源允许) | 提升并发吞吐,但增加调度开销 |
graph TD
A[调用 runtime.GOMAXPROCS n] --> B{n ≤ 当前P数?}
B -->|是| C[标记多余P为idle]
B -->|否| D[尝试创建新P]
D --> E{OS线程/M充足?}
E -->|是| F[绑定M,激活P]
E -->|否| G[等待M释放或超时]
2.2 多核利用率骤降:CPU密集型场景下的误配实测分析
在高并发计算任务中,线程数配置不当会显著抑制多核并行能力。以下为典型误配场景复现:
数据同步机制
# 错误示例:全局锁阻塞所有工作线程
from threading import Lock
shared_lock = Lock()
def cpu_bound_task(n):
with shared_lock: # ❌ 所有线程串行执行,实际退化为单核
return sum(i * i for i in range(n))
shared_lock 强制所有 CPU 密集型任务序列化,导致 htop 中仅 1 个核心持续满载,其余 idle。
核心数与线程数匹配建议
| 配置策略 | 平均利用率 | 备注 |
|---|---|---|
threads = cores |
92% | 最佳实践(无锁/无 I/O) |
threads = cores×2 |
68% | 上下文切换开销主导 |
threads = 1 |
100%(单核) | 全局锁导致的伪高利用率 |
执行路径依赖
graph TD
A[启动8线程] --> B{是否共享临界区?}
B -->|是| C[竞争锁 → 队列等待]
B -->|否| D[真正并行计算]
C --> E[单核饱和,7核空闲]
2.3 云环境弹性伸缩下GOMAXPROCS自动调优失效案例复盘
某K8s集群中,Go服务在节点从2核动态扩容至16核后,吞吐量不升反降18%。根因在于runtime.GOMAXPROCS(0)依赖启动时的numCPU,而容器内/proc/cpuinfo未随cgroup限制实时更新。
失效机制分析
func init() {
// ❌ 错误:仅在启动时读取一次,无法响应vCPU热添加
runtime.GOMAXPROCS(0) // 实际设为2,而非扩容后的16
}
该调用依据sysctl("hw.ncpu")或/proc/cpuinfo(宿主视角),但容器运行时cgroup v2中cpu.max变更不会触发Go运行时重探。
关键事实对比
| 场景 | /proc/cpuinfo CPUs | cgroup cpu.max | GOMAXPROCS 实际值 |
|---|---|---|---|
| 启动时(2核) | 2 | max 200000 100000 | 2 |
| 扩容后(16核) | 2(未刷新) | max 1600000 100000 | 2(未变) |
自适应修复方案
// ✅ 正确:周期性探测cgroup限制并主动调优
go func() {
ticker := time.NewTicker(30 * time.Second)
for range ticker.C {
if n := detectCgroupCPULimit(); n > 0 {
runtime.GOMAXPROCS(n) // 显式覆盖
}
}
}()
detectCgroupCPULimit()需解析/sys/fs/cgroup/cpu.max(cgroup v2)或/sys/fs/cgroup/cpu/cpu.cfs_quota_us(v1),转换为整数核数。
2.4 容器化部署中cgroup CPU quota与GOMAXPROCS冲突诊断工具链
Go 应用在 Kubernetes 中常因 cpu.shares 或 cpu.cfs_quota_us 限制,与默认 GOMAXPROCS=0(自动设为逻辑 CPU 数)产生调度失配——容器被限核但 Go runtime 仍按宿主机 CPU 数启动 P。
核心诊断维度
- 检查
/sys/fs/cgroup/cpu/cpu.cfs_quota_us与cpu.cfs_period_us比值 - 对比
runtime.NumCPU()与os.Getenv("GOMAXPROCS") - 观察
go tool trace中 Goroutine 调度延迟尖峰
冲突检测脚本(容器内执行)
# 获取 cgroup 有效 CPU 配额(单位:毫核)
quota=$(cat /sys/fs/cgroup/cpu/cpu.cfs_quota_us 2>/dev/null)
period=$(cat /sys/fs/cgroup/cpu/cpu.cfs_period_us 2>/dev/null)
effective_cores=$(awk "BEGIN {printf \"%.1f\", $quota/$period}")
echo "Effective CPU cores: $effective_cores"
逻辑说明:
cfs_quota_us/cfs_period_us给出容器可使用的平均 CPU 核心数(如50000/100000 = 0.5)。若GOMAXPROCS未显式设为该值向下取整,将导致 M:N 调度器过载或空转。
| 工具 | 用途 | 是否需 root |
|---|---|---|
runc state |
查看容器实时 cgroup 设置 | 否 |
go tool pprof |
分析 goroutine CPU 时间分布 | 否 |
graph TD
A[容器启动] --> B{GOMAXPROCS=0?}
B -->|是| C[读取 /proc/sys/kernel/osrelease]
B -->|否| D[使用环境变量值]
C --> E[误取宿主机 CPU 数]
E --> F[与 cgroup quota 不匹配]
F --> G[调度队列积压/上下文切换激增]
2.5 基于pprof+runtime.MemStats的GOMAXPROCS合理性验证方法论
核心验证逻辑
通过并发压力下 GOMAXPROCS 与内存分配行为的耦合关系,反向推断其设置是否合理:过高导致调度开销激增,过低引发 Goroutine 积压与堆内存延迟回收。
数据采集组合
- 启用
net/http/pprof实时采集 goroutine/heap profile - 定期读取
runtime.ReadMemStats(&m)获取Mallocs,HeapAlloc,NumGC - 关联
GOMAXPROCS(0)动态查询当前值
关键诊断代码
func observeGOMAXPROCS() {
var m runtime.MemStats
runtime.ReadMemStats(&m)
fmt.Printf("GOMAXPROCS=%d, HeapAlloc=%v, Mallocs=%d, NumGC=%d\n",
runtime.GOMAXPROCS(0), m.HeapAlloc, m.Mallocs, m.NumGC)
}
逻辑分析:
runtime.GOMAXPROCS(0)仅查询不变更;HeapAlloc持续高位且NumGC频次下降,常指示并行 GC 受限于 P 数不足;Mallocs突增但HeapAlloc回落缓慢,可能因 P 不足导致分配器局部缓存(mcache)争用加剧。
决策参考表
| 指标趋势 | 建议 GOMAXPROCS 调整方向 |
|---|---|
NumGC 显著低于预期频率 |
↑ 提升(释放 GC 并行度) |
HeapAlloc 波动剧烈且峰值高 |
↓ 降低(缓解分配竞争) |
验证流程
graph TD
A[启动 pprof server] --> B[施加稳定并发负载]
B --> C[周期采集 MemStats + goroutine profile]
C --> D{HeapAlloc/NumGC/Mallocs 相关性分析}
D -->|P 过载| E[下调 GOMAXPROCS]
D -->|P 不足| F[上调至 CPU 核数]
第三章:P、M、G三元模型失衡引发的典型故障
3.1 P饥饿:高并发I/O场景下P被长期抢占的现场还原与修复
当大量 goroutine 阻塞在 epoll_wait 或 io_uring 等系统调用上时,runtime 可能因 M 持续绑定 P 而导致其他 P 闲置——即“P饥饿”。
现场复现关键路径
// 模拟高并发阻塞型 I/O(如大量 net.Conn.Read)
for i := 0; i < 1000; i++ {
go func() {
buf := make([]byte, 1)
conn.Read(buf) // 长期阻塞,M 无法释放 P
}()
}
此处
conn.Read触发entersyscallblock,若 M 不主动 handoff P,其他 goroutine 将因无可用 P 而停滞。GOMAXPROCS=4下仅 1–2 个 P 实际调度,其余空转。
核心修复机制对比
| 方案 | 是否启用 P handoff | 适用场景 | 延迟影响 |
|---|---|---|---|
runtime.SetMutexProfileFraction(1) |
❌ | 调试定位 | 高开销 |
GODEBUG=schedtrace=1000 |
✅(自动) | 生产观测 | 中等 |
netpoll 非阻塞优化 |
✅(内建) | 默认启用 | 极低 |
调度恢复流程
graph TD
A[goroutine 阻塞于 sysread] --> B{M 调用 entersyscallblock}
B --> C[判断是否可 handoff P]
C -->|是| D[将 P 转交空闲 M]
C -->|否| E[当前 M 继续持有 P]
D --> F[其他 goroutine 获得 P 并运行]
3.2 M泄漏:CGO调用阻塞导致M无法归还的内存与调度链路追踪
当 CGO 调用(如 C.sleep() 或阻塞式系统调用)长时间未返回,Go 运行时无法抢占该 M,导致其脱离 P 的调度循环,且不会被放入空闲 M 队列。
调度链路中断示意
// 模拟阻塞型 CGO 调用
/*
#cgo LDFLAGS: -lpthread
#include <unistd.h>
void block_forever() { sleep(30); }
*/
import "C"
func leakyCGO() {
C.block_forever() // M 此刻挂起,不响应 Go 调度器
}
此调用使 M 进入
_Gsyscall状态,m->curg = nil,P 无法将其回收;若无其他 M 可用,新 goroutine 将触发newm创建,加剧 M 泄漏。
关键状态流转(mermaid)
graph TD
A[goroutine 发起 CGO 调用] --> B[M 切换至 Gsyscall 状态]
B --> C[放弃绑定 P,m->p = nil]
C --> D[运行时无法唤醒或回收该 M]
D --> E[持续占用栈内存 + OS 线程资源]
M 泄漏影响对比表
| 维度 | 正常 M 归还 | CGO 阻塞 M 泄漏 |
|---|---|---|
| 内存占用 | 栈自动收缩/复用 | 固定 2MB 栈长期驻留 |
| OS 线程数 | 复用已有线程 | 持续创建新线程 |
| P 关联状态 | m->p 有效,可调度 | m->p == nil,脱管 |
3.3 G堆积:无缓冲channel写入阻塞引发goroutine雪崩的压测复现与熔断设计
压测复现场景
启动 500 并发 goroutine 向 make(chan int)(无缓冲)持续写入,无接收者时立即阻塞:
ch := make(chan int) // 无缓冲,写即阻塞
for i := 0; i < 500; i++ {
go func(id int) {
ch <- id // 永久阻塞,G 状态为 `chan send`
}(i)
}
逻辑分析:每个 goroutine 在
<-ch处陷入Gwaiting状态,无法被调度器回收;Go 运行时持续创建新 G 却不释放,导致runtime.GOMAXPROCS()下 Goroutine 数线性飙升至数千,内存与调度开销剧增。
熔断关键参数
| 参数 | 推荐值 | 说明 |
|---|---|---|
chan cap |
≥100 | 缓冲区缓解瞬时峰值 |
select timeout |
10ms | 避免无限等待 |
semaphore size |
200 | 限制并发写入 goroutine 数 |
熔断流程
graph TD
A[写入请求] --> B{chan 是否可写?}
B -- 是 --> C[成功写入]
B -- 否 --> D[触发超时 select]
D --> E{是否达熔断阈值?}
E -- 是 --> F[拒绝请求 + 打点告警]
E -- 否 --> G[降级为异步队列]
第四章:sysmon监控线程异常与调度器健康退化
4.1 sysmon周期性扫描失效:netpoller未唤醒导致的goroutine假死定位
现象复现与初步观测
当高并发短连接场景下,sysmon 的 forcegc 和 scavenge 检查频率骤降,pprof 显示 runtime.sysmon goroutine 长期处于 Gwaiting 状态。
根本原因:netpoller 阻塞未被唤醒
sysmon 依赖 netpoll 超时返回以维持轮询节奏。若 epoll_wait(Linux)或 kqueue(macOS)因无就绪 fd 且超时设为 -1(即无限等待),将彻底阻塞 sysmon。
// src/runtime/netpoll.go: netpoll()
func netpoll(block bool) gList {
// block=false 时 timeout=0;但 sysmon 调用时传入 block=true → timeout=-1
waitms := int32(-1)
if !block {
waitms = 0
}
return netpollimpl(waitms) // ⚠️ 此处可能永久阻塞
}
waitms = -1表示内核态无限等待,而sysmon无外部信号唤醒机制,导致其 goroutine “假死”。
关键修复路径
- ✅ 强制
sysmon调用netpoll(false)实现非阻塞轮询 - ✅ 在
netpoll中注入sudog超时唤醒逻辑(Go 1.22+ 已引入netpollDeadline) - ❌ 避免在
GOMAXPROCS=1下依赖netpoll触发调度
| 修复方式 | 引入版本 | 是否需用户干预 |
|---|---|---|
netpollDeadline |
Go 1.22 | 否 |
GODEBUG=netdns=go |
Go 1.19+ | 是(仅 DNS 场景) |
graph TD
A[sysmon 启动] --> B{调用 netpoll block=true?}
B -->|是| C[epoll_wait timeout=-1]
C --> D[无限等待 → goroutine 假死]
B -->|否| E[立即返回 → 维持周期性扫描]
4.2 sysmon被抢占超时:高优先级抢占式调度干扰下的10ms级延迟突增根因分析
当系统中存在大量实时线程(如音频DSP任务、硬实时控制线程)时,sysmon(系统监控协程,SCHED_FIFO优先级10)频繁遭遇更高优先级线程(SCHED_FIFO 40+)抢占,导致其周期性采样逻辑延迟突破10ms阈值。
数据同步机制
sysmon 依赖 clock_gettime(CLOCK_MONOTONIC, &ts) 触发每5ms一次的健康检查,但内核调度器无法保障其在严格时间窗内执行:
// sysmon主循环节选(简化)
while (running) {
clock_gettime(CLOCK_MONOTONIC, &start); // 记录理论起始点
do_health_check(); // 实际执行可能被延迟
clock_gettime(CLOCK_MONOTONIC, &end);
latency = timespec_diff_us(&end, &start); // 实测耗时 >10000μs 即告警
}
逻辑分析:
timespec_diff_us()返回微秒级差值;CLOCK_MONOTONIC避免NTP跳变干扰;但do_health_check()若被SCHED_FIFO 50线程持续抢占超9.8ms,则触发“被抢占超时”事件。
关键调度参数对照
| 参数 | 值 | 说明 |
|---|---|---|
sysmon 调度策略 |
SCHED_FIFO | 可被同策略更高prio抢占 |
sysmon 优先级 |
10 | 易被优先级≥11的实时线程打断 |
| 典型抢占源 | SCHED_FIFO 40–99 | 如工业PLC控制线程 |
调度干扰路径
graph TD
A[sysmon 唤醒] --> B{CPU是否空闲?}
B -->|否| C[被SCHED_FIFO 45线程抢占]
C --> D[等待其完成或让出CPU]
D --> E[sysmon实际开始执行延迟 ≥10ms]
4.3 sysmon与GC标记阶段竞争:STW延长与调度器响应迟滞的协同调试实践
当 sysmon 线程频繁抢占 P 执行抢占检查时,恰逢 GC 标记阶段启动,会加剧 STW 延长并干扰调度器对 goroutine 抢占信号的及时响应。
数据同步机制
sysmon 通过 runtime.nanotime() 采样调度延迟,而 GC 标记需遍历所有 P 的本地运行队列——二者共享 allp 数组读锁,形成隐式竞争。
关键代码片段
// src/runtime/proc.go: sysmon 循环节选
if t := nanotime() - lastpoll; t > 10*1000*1000 { // 超过10ms未轮询
atomic.Storeuintptr(&sched.lastpoll, uint64(nanotime()))
if sched.pollUntil != 0 && sched.pollUntil <= nanotime() {
netpoll(0) // 可能触发 M 抢占,干扰 GC mark worker 绑定
}
}
netpoll(0) 非阻塞调用可能唤醒休眠的 M,导致其在 GC 标记关键期被调度,破坏 markworker 的亲和性保障。
| 竞争维度 | sysmon 行为 | GC 标记影响 |
|---|---|---|
| 时间窗口 | 每 20ms 主动轮询 | STW 后立即启动标记 |
| 资源争用 | 抢占 P 执行 poll | 遍历 allp 并加读锁 |
| 调度扰动 | 强制唤醒 idle M | markworker 被迁移至非绑定 P |
graph TD
A[sysmon 检测超时] --> B{netpoll 无阻塞调用}
B --> C[唤醒 idle M]
C --> D[调度器分配 P 给新 M]
D --> E[与 GC markworker 争抢同一 P]
E --> F[标记延迟 → STW 延长]
4.4 自定义sysmon健康探针:基于runtime.ReadMemStats与debug.SetGCPercent的主动巡检方案
内存健康快照采集
定期调用 runtime.ReadMemStats 获取实时堆内存指标,重点关注 HeapAlloc、HeapSys 和 NumGC:
var m runtime.MemStats
runtime.ReadMemStats(&m)
log.Printf("heap alloc: %v MB, gc count: %d", m.HeapAlloc/1024/1024, m.NumGC)
该调用开销极低(纳秒级),线程安全,返回结构体包含 35+ 个运行时内存维度,适用于高频轻量巡检。
GC 行为动态调控
结合 debug.SetGCPercent 实现负载自适应:
debug.SetGCPercent(int(healthScore * 80)) // 健康分0~1 → GC触发阈值20%~80%
数值越小 GC 越激进(内存敏感),越大越保守(CPU 敏感);需避免设为负数(禁用 GC)或过高导致 OOM 风险。
巡检策略对比
| 策略 | 采集频率 | GC 调节能力 | 适用场景 |
|---|---|---|---|
| 默认 sysmon | ~20ms | ❌ 固定 | 基础稳定性保障 |
| 自定义探针 | 可配置 | ✅ 动态 | SLO 敏感型服务 |
执行流程
graph TD
A[启动定时器] --> B{健康评分 < 0.7?}
B -->|是| C[降低 GCPercent]
B -->|否| D[维持当前阈值]
C --> E[记录 MemStats 快照]
D --> E
第五章:面向生产环境的调度韧性建设路线图
在超大规模电商大促(如双11)场景中,某头部平台曾因Kubernetes默认调度器无法动态感知节点磁盘IO饱和状态,导致37%的订单服务Pod被错误调度至高延迟节点,订单履约延迟峰值达8.2秒。该事故直接推动其构建覆盖“可观测—可干预—可自愈”全链路的调度韧性体系。
调度前风险拦截机制
引入定制化NodeScorePlugin插件,在调度决策前实时注入Prometheus指标:node_disk_io_time_seconds_total{job="node-exporter"}、kube_node_status_condition{condition="Ready",status="true"}。当IO等待时间>15s或节点Ready状态持续抖动超3次/分钟时,自动将节点score归零。上线后调度失败率下降92%,误调度事件归零。
运行时动态重调度策略
通过Descheduler配置以下规则组合:
LowNodeUtilization:CPUPodLifeTime:运行超72h的无状态Pod强制滚动更新;TopologySpreadingConstraintsViolation:跨AZ副本数偏差>2时触发再平衡。
实际压测显示,单集群2000+节点环境下,异常拓扑修复平均耗时从47分钟缩短至93秒。
多级熔断与降级能力矩阵
| 熔断层级 | 触发条件 | 执行动作 | 恢复机制 |
|---|---|---|---|
| 调度层 | 10秒内Pending Pod >500 | 切换至PriorityQueue模式,仅允许P0级Workload入队 |
持续3分钟Pending |
| 节点层 | kubelet上报NodeCondition: DiskPressure=True |
立即禁止新Pod调度,并驱逐BestEffort类Pod | kubelet上报DiskPressure=False后5秒内恢复 |
| 集群层 | 全局API Server 5xx错误率>5% | 启用本地缓存调度器(LocalScheduler),基于etcd快照决策 | API Server健康检查连续10次成功后退出降级 |
混沌工程验证闭环
采用Chaos Mesh注入三类故障:
apiVersion: chaos-mesh.org/v1alpha1
kind: IOChaos
metadata:
name: disk-latency
spec:
action: latency
mode: one
duration: "30s"
scheduler:
cron: "@every 5m"
volumePath: "/var/lib/kubelet"
latency: "2000ms"
配合Prometheus告警规则sum(rate(scheduler_scheduling_duration_seconds_sum[5m])) / sum(rate(scheduler_scheduling_duration_seconds_count[5m])) > 3.5,实现故障响应SLA自动校验。
生产就绪性检查清单
- ✅ 所有调度器插件具备独立健康探针(
/healthz返回HTTP 200且响应 - ✅ Descheduler配置已通过
kubectl descheduler --dry-run=client -o yaml验证语法有效性 - ✅ 节点Label变更事件监听器部署于独立命名空间,资源限制为200m CPU/512Mi内存
- ✅ 调度日志统一接入Loki,保留周期≥90天,支持按
scheduler_name和pod_priority_class多维检索
跨集群弹性伸缩协同
当主集群CPU使用率连续5分钟>85%,自动触发跨集群调度:
graph LR
A[主集群监控告警] --> B{是否满足伸缩阈值?}
B -->|是| C[调用Cluster-API获取备用集群列表]
C --> D[按网络延迟<5ms、GPU卡空闲>3张筛选]
D --> E[通过Karmada PropagationPolicy分发Pod]
E --> F[备用集群执行LocalScheduler二次决策]
在2023年春晚红包活动中,该机制成功将32万QPS流量分流至3个异地集群,主集群负载稳定在68%±3%区间。
