第一章:Go高效并发架构设计(生产级调度模型大揭秘):从runtime.GOMAXPROCS到P/M/G深度协同
Go 的并发能力并非来自操作系统线程的简单封装,而是由 runtime 实现的一套轻量、自适应、用户态的协作式调度系统。其核心是 P(Processor)、M(Machine)、G(Goroutine)三元组的动态协同:P 代表逻辑处理器(绑定 OS 线程的执行上下文),M 是与内核线程一一映射的实际执行者,G 则是可被调度的最小执行单元(平均仅占用 2KB 栈空间)。
GOMAXPROCS 的真实语义
GOMAXPROCS 并非“最大 goroutine 数”,而是可并行执行用户代码的 P 的数量上限。它决定了有多少个 P 能同时被 M 抢占并运行 Go 代码(I/O 或系统调用期间 P 可能被 M 释放)。默认值为 CPU 逻辑核数,但可通过以下方式显式设置:
package main
import (
"fmt"
"runtime"
)
func main() {
fmt.Printf("Default GOMAXPROCS: %d\n", runtime.GOMAXPROCS(0)) // 查询当前值
runtime.GOMAXPROCS(4) // 设置为 4
fmt.Printf("After set: %d\n", runtime.GOMAXPROCS(0))
}
⚠️ 注意:修改应在程序启动早期完成;运行时动态调整可能引发短暂调度抖动,不建议在高负载服务中频繁变更。
P/M/G 的生命周期协同
- G 创建:
go f()触发 runtime.newproc,将 G 放入当前 P 的本地运行队列(若满则随机偷取至其他 P 队列); - M 绑定:空闲 M 通过
findrunnable()主动扫描所有 P 的本地队列 + 全局队列 + 其他 P 的偷取队列; - P 阻塞/切换:当 G 执行阻塞系统调用(如
read())时,M 会解绑 P 并转入休眠,而该 P 可被其他空闲 M 获取继续调度剩余 G。
| 组件 | 关键职责 | 生命周期特点 |
|---|---|---|
| G | 执行用户函数、保存栈与寄存器状态 | 创建廉价,可成千上万;由 runtime 自动回收 |
| M | 执行机器指令、发起系统调用 | 与 OS 线程绑定;空闲超 10 分钟自动退出(避免资源泄漏) |
| P | 管理 G 队列、内存分配缓存(mcache)、调度策略上下文 | 数量固定(= GOMAXPROCS);随 M 动态获取/释放 |
生产环境调优建议
- 监控
runtime.NumGoroutine()和runtime.ReadMemStats()中NumGC、PauseTotalNs,识别 Goroutine 泄漏或 GC 压力; - 使用
GODEBUG=schedtrace=1000输出每秒调度器快照,观察SCHED行中gomaxprocs、idleprocs、runqueue等字段变化; - 避免在 hot path 中无节制 spawn goroutine;优先复用
sync.Pool缓存临时对象。
第二章:Goroutine调度基石:P/M/G三元模型的运行时解构
2.1 G(Goroutine)的生命周期管理与栈动态伸缩实践
Goroutine 的生命周期始于 go 关键字调用,终于函数执行完毕或被调度器标记为可回收。其核心优势在于轻量级栈——初始仅 2KB,按需动态增长/收缩。
栈伸缩触发机制
当当前栈空间不足时,运行时插入栈溢出检查(如 morestack),触发栈复制:
- 分配新栈(原大小 × 2)
- 复制活跃帧(含局部变量、返回地址)
- 更新 goroutine 结构体中的
stack指针
func deepRecursion(n int) {
if n <= 0 {
return
}
// 编译器在此插入栈边界检查(runtime.morestack)
deepRecursion(n - 1)
}
此调用在约 1000 层深度时触发首次栈扩容;
n作为参数压入栈帧,决定是否跨越当前栈边界阈值(stackguard0)。
生命周期关键状态转移
| 状态 | 转入条件 | 转出动作 |
|---|---|---|
_Grunnable |
newproc 创建后 |
被调度器置为 _Grunning |
_Grunning |
获得 M 执行权 | 函数返回 → _Gdead |
_Gdead |
执行结束且未被复用 | 内存归还至 sync.Pool |
graph TD
A[go f()] --> B[_Grunnable]
B --> C{_Grunning}
C -->|f returns| D[_Gdead]
D -->|复用| B
2.2 M(OS Thread)绑定策略与系统调用阻塞/抢占式唤醒实战分析
Go 运行时中,M(OS 线程)与 G(goroutine)的绑定关系直接影响系统调用阻塞行为与调度响应性。
M 的绑定模式
m.lockedg != nil:M 被显式锁定到某个 G(如runtime.LockOSThread())m.ncgocall > 0:M 正在执行 cgo 调用,自动进入 locked 状态- 普通 M 在系统调用返回后可被复用或休眠
系统调用阻塞时的 M 处理流程
// runtime/proc.go 中 sysmon 监控逻辑节选
if mp.blocked && mp.spinning {
mp.spinning = false
// 唤醒空闲 P,尝试窃取 G
}
该逻辑确保阻塞 M 不占用 P,允许其他 M 抢占执行;mp.blocked 标志由 entersyscall 设置,exitsyscall 清除。
抢占式唤醒关键路径
| 事件源 | 触发时机 | 唤醒目标 |
|---|---|---|
| 网络 I/O 就绪 | netpoller 回调 | 对应 G + M |
| 定时器超时 | timerproc goroutine | 关联的 G |
| channel 收发 | chanrecv/chansend | 等待的 G |
graph TD
A[系统调用进入] --> B[entersyscall: 解绑 G-P-M]
B --> C{M 是否 locked?}
C -->|是| D[保持绑定,阻塞等待]
C -->|否| E[释放 P,M 进入休眠]
E --> F[IO 完成/信号到达]
F --> G[exitsyscall: 尝试获取 P,唤醒 G]
2.3 P(Processor)资源池设计与本地运行队列(LRQ)负载均衡优化
Go 运行时将逻辑处理器(P)抽象为协程调度的资源单元,每个 P 绑定一个本地运行队列(LRQ),容量固定为 256 个 G(goroutine)。LRQ 采用无锁环形缓冲区实现,兼顾缓存友好性与并发性能。
LRQ 入队核心逻辑
func (p *p) runqput(g *g) {
// 若 LRQ 未满,直接尾插(O(1))
if p.runqhead != p.runqtail+1&uint32(len(p.runq)) {
p.runq[p.runqtail] = g
atomic.StoreUint32(&p.runqtail, p.runqtail+1)
return
}
// 溢出时转移一半至全局队列(GQ)
runqsteal(p, g, true)
}
runqtail 使用原子写确保多生产者安全;&掩码替代取模提升性能;溢出触发 runqsteal 防止 LRQ 长期饱和。
负载再平衡策略
- 当某 P 的 LRQ 长期空闲(
runqempty()连续 3 次成功),主动向邻居 Psteal半数 G; - Steal 尝试限频(每 61 次调度最多 1 次),避免抖动;
- 全局队列(GQ)作为最后兜底,由
schedule()中的findrunnable()统一兜底拉取。
| 触发条件 | 动作 | 频控机制 |
|---|---|---|
| LRQ ≥ 256 | 批量移入 GQ | 无(紧急溢出) |
| LRQ == 0 且 GQ > 0 | 从 GQ 批量窃取 | 每 61 调度 1 次 |
| 邻居 P LRQ > 128 | 跨 P 窃取 1/2 | 原子计数器限频 |
graph TD
A[新 Goroutine 创建] --> B{LRQ 是否已满?}
B -->|否| C[直接入 LRQ 尾部]
B -->|是| D[触发 runqsteal:半数移至 GQ]
C --> E[当前 P 执行调度循环]
D --> F[GQ 作为跨 P 负载中继]
2.4 全局运行队列(GRQ)与工作窃取(Work-Stealing)机制源码级验证
Linux 5.19+ 主线内核中,GRQ 已被移除,但其思想在 CFS 的 rq->cfs 与 dl_rq 等结构中延续;而工作窃取仍活跃于 try_to_wake_up() 与 pick_next_task_fair() 调度路径。
数据同步机制
struct rq 中的 nr_cpus_allowed 与 idle_balance() 协同触发窃取判定:
// kernel/sched/fair.c:3821
if (available_idle_cpu(i) && need_resched() &&
!cpumask_test_cpu(i, tsk_cpus_allowed(p)))
goto steal;
→ i 为候选空闲 CPU;tsk_cpus_allowed(p) 是任务亲和掩码;跳转 steal 后调用 steal_task() 尝试迁移。
窃取决策流程
graph TD
A[check_preempt_curr] --> B{target_rq->nr_running == 0?}
B -->|Yes| C[find_busiest_group]
C --> D[steal_task_from_busiest]
D --> E[activate_task]
关键字段对比
| 字段 | 类型 | 作用 |
|---|---|---|
rq->nr_steal |
u64 |
统计本 CPU 成功窃取任务次数 |
rq->idle_stamp |
u64 |
记录最近一次进入 idle 的时间戳 |
- 窃取失败时触发
nohz_idle_balance()延迟重试 sched_feat(ATTACH_IDLE)控制是否允许向 idle CPU 迁移
2.5 GPM协同调度关键路径追踪:从newproc到schedule的全流程压测复现
GPM(Goroutine-Processor-Machine)模型中,newproc触发协程创建,经调度器队列最终由schedule()执行。压测需精准捕获该路径的延迟热点。
关键路径 instrumentation 示例
// 在 src/runtime/proc.go 的 newproc1 中插入 trace 点
traceGoCreate(gp, getcallerpc()) // 记录协程创建时刻与调用栈
该调用注入运行时 trace 事件,参数 gp 为新 goroutine 指针,getcallerpc() 提供调用上下文,用于后续 flame graph 关联。
压测复现核心步骤
- 使用
GOMAXPROCS=8固定 P 数量,排除动态伸缩干扰 - 启动 10k goroutines / 秒持续负载,配合
runtime/trace开启全路径采样 - 通过
go tool trace提取ProcStart,GoCreate,GoStart,Sched事件时间戳
调度延迟分布(μs,P99)
| 阶段 | 平均延迟 | P99 延迟 |
|---|---|---|
| newproc → runqput | 42 ns | 186 ns |
| runqget → schedule | 137 ns | 1.2 μs |
graph TD
A[newproc] --> B[runqput]
B --> C[findrunnable]
C --> D[execute]
D --> E[schedule]
上述流程揭示:findrunnable 中的全局队列争用是 P99 尖峰主因。
第三章:调度参数调优与生产环境适配
3.1 runtime.GOMAXPROCS语义演进与NUMA感知型CPU亲和性配置
早期 Go 1.4 及之前,GOMAXPROCS 仅限制可运行 goroutine 的 OS 线程数(M)上限,不干预调度器对物理 CPU 的绑定逻辑;Go 1.5 引入 M:N 调度后,其语义转向“P(Processor)的数量”,即调度器并行执行单元数,直接影响 goroutine 就绪队列的分片粒度。
NUMA 意识的缺失与代价
在多插槽服务器上,若 GOMAXPROCS=64 但未约束线程亲和性,runtime 可能将 P 均匀映射到跨 NUMA 节点的逻辑 CPU,引发远端内存访问延迟激增(>100ns vs 本地
Go 1.21+ 的 NUMA 感知进展
虽标准库仍未暴露 sched_setaffinity 接口,但可通过 runtime.LockOSThread() + syscall.SchedSetaffinity() 手动绑定:
// 绑定当前 goroutine 到 NUMA node 0 的 CPU 0-7
cpuset := uint64(0xFF) // CPU 0~7 对应低 8 位
_, _, err := syscall.Syscall(
syscall.SYS_SCHED_SETAFFINITY,
0, // 当前线程
uintptr(unsafe.Sizeof(cpuset)),
uintptr(unsafe.Pointer(&cpuset)),
)
// err 需检查:权限不足或 CPU 不存在时失败
此调用直接作用于底层线程(M),绕过 runtime 调度器;
cpuset为位掩码,需按系统 CPU topology 构造(如通过/sys/devices/system/node/node0/cpulist获取)。
关键参数对照表
| 参数 | 含义 | 典型值 | 注意事项 |
|---|---|---|---|
GOMAXPROCS |
P 的数量,决定并发执行单元数 | runtime.NumCPU() |
不等于物理核心数,过度设置增加调度开销 |
GODEBUG=schedtrace=1000 |
每秒输出调度器状态 | 1000ms |
可观察 P 分配是否跨 NUMA |
graph TD
A[启动时 GOMAXPROCS=n] --> B{runtime 初始化}
B --> C[创建 n 个 P]
C --> D[每个 P 关联一个 M]
D --> E[默认 M 在任意 CPU 运行]
E --> F[手动调用 sched_setaffinity]
F --> G[将 M 锁定至指定 NUMA node CPU]
3.2 GC暂停对调度延迟的影响量化分析与低延迟场景调优方案
GC停顿与调度延迟的耦合机制
JVM GC(尤其是Full GC)触发时,所有应用线程被STW(Stop-The-World),直接阻塞OS调度器对Java线程的调度响应。在实时性敏感场景(如高频交易、自动驾驶决策模块),50ms的G1 Mixed GC暂停可能造成任务调度延迟超限。
关键指标量化对照
| GC算法 | 平均Pause(ms) | P99调度延迟增幅 | 适用延迟等级 |
|---|---|---|---|
| Serial | 120–450 | +380% | ❌ >10ms场景 |
| G1(默认) | 25–80 | +95% | ⚠️ 中等实时性 |
| ZGC | +2.3% | ✅ |
ZGC低延迟调优示例
// 启动参数:启用ZGC + 显式控制元数据回收时机
-XX:+UseZGC
-XX:ZCollectionInterval=5
-XX:+UnlockExperimentalVMOptions
-XX:ZUncommitDelay=300 // 延迟300秒再释放未用内存页
该配置将ZGC周期性GC与应用负载峰错开,避免ZUncommit操作与调度高峰期重叠;ZCollectionInterval限制GC频率,防止过度回收引发CPU争用。
调度协同优化路径
graph TD
A[应用线程调度请求] –> B{是否处于GC STW?}
B –>|是| C[延迟至GC结束,调度延迟=STW时长+队列等待]
B –>|否| D[正常OS调度,延迟≈μs级]
C –> E[引入GC感知调度器钩子]
D –> E
3.3 高并发IO密集型服务中netpoller与调度器的深度协同调优
在高并发IO密集场景下,netpoller(如Linux的epoll/kqueue)与Go runtime调度器的耦合效率直接决定吞吐上限。关键在于避免Goroutine频繁阻塞/唤醒引发的M-P-G调度抖动。
数据同步机制
netpoller就绪事件需零拷贝通知调度器:
// runtime/netpoll.go 简化逻辑
func netpoll(block bool) *gList {
// 调用epoll_wait,返回就绪fd列表
// 每个fd关联的goroutine通过gList批量唤醒
return pollcache.poll(block) // block=false用于非阻塞轮询
}
block=false启用非阻塞轮询,配合GOMAXPROCS动态缩放P数量,减少调度延迟。
协同调优策略
- 启用
GODEBUG=netdns=go+1规避cgo DNS阻塞 - 调整
GOMAXPROCS匹配CPU核心数(非超线程) - 关闭
GODEBUG=schedtrace=1000等调试开销
| 参数 | 推荐值 | 影响 |
|---|---|---|
GOMAXPROCS |
numa_node_cores |
避免跨NUMA调度 |
GODEBUG |
scheddelay=10ms |
控制P空闲回收阈值 |
graph TD
A[epoll_wait就绪] --> B{netpoller回调}
B --> C[批量唤醒Goroutine]
C --> D[调度器将G绑定至空闲P]
D --> E[避免M阻塞,复用OS线程]
第四章:高阶并发模式与调度增强实践
4.1 基于GMP模型的自定义协程池设计与吞吐量对比基准测试
Go 运行时的 GMP(Goroutine–M–P)调度模型天然支持高并发,但无节制创建 goroutine 易引发调度开销与内存抖动。为此,我们设计轻量级协程池 Pool,复用 goroutine 实例并限制并发上限。
核心结构设计
type Pool struct {
tasks chan func()
workers int
wg sync.WaitGroup
}
tasks: 无缓冲通道,承载待执行任务(阻塞式投递,天然限流)workers: 启动固定数量的长期 worker 协程,每个绑定独立 P,避免跨 P 抢占
吞吐量基准对比(10万任务,i7-11800H)
| 方案 | 平均延迟(ms) | 内存分配(MB) | GC 次数 |
|---|---|---|---|
| 原生 goroutine | 12.7 | 89.3 | 14 |
| 自定义协程池 | 4.2 | 11.6 | 2 |
调度流程示意
graph TD
A[客户端提交task] --> B{tasks chan 是否满?}
B -->|是| C[阻塞等待空位]
B -->|否| D[worker从chan取task]
D --> E[执行并归还worker]
4.2 异步任务调度器(Async Scheduler)实现:融合优先级与公平性策略
核心设计权衡
异步任务调度需在响应延迟(高优先级抢占)与吞吐公平性(低优先级不饿死)间取得平衡。采用双队列结构:优先级堆(heapq)管理紧急任务,时间轮槽位(deque)保障周期性公平调度。
关键调度逻辑
import heapq
from collections import deque
class AsyncScheduler:
def __init__(self):
self.priority_heap = [] # (priority, timestamp, task)
self.fair_queue = deque() # FIFO for fairness-bound tasks
self.global_ts = 0
def submit(self, task, priority=0, fair_bound=True):
self.global_ts += 1
if fair_bound:
self.fair_queue.append((self.global_ts, task))
else:
heapq.heappush(self.priority_heap, (priority, self.global_ts, task))
priority越小越紧急;timestamp确保同优先级下FIFO;fair_bound=True将任务导向时间轮队列,避免高优任务长期垄断CPU。
调度策略对比
| 维度 | 优先级抢占模式 | 公平轮转模式 |
|---|---|---|
| 延迟敏感度 | 高(μs级响应) | 中(ms级抖动) |
| 饿死风险 | 低优先级可能饿死 | 零饿死(严格FIFO) |
| 实现复杂度 | 低(堆操作O(log n)) | 中(双队列协调) |
执行流程
graph TD
A[新任务提交] --> B{fair_bound?}
B -->|Yes| C[入fair_queue尾部]
B -->|No| D[入priority_heap]
E[调度器循环] --> F[优先处理heap非空?]
F -->|Yes| G[pop最高优任务]
F -->|No| H[pop fair_queue头部]
4.3 Channel底层调度介入:利用reflect.Select与runtime_pollWait定制化阻塞行为
Go 的 channel 阻塞并非纯用户态逻辑,而是深度耦合运行时调度器。当 goroutine 在 select 中等待 channel 操作时,最终会调用 runtime.poll_runtime_pollWait 进入网络轮询器(netpoll)或直接挂起。
数据同步机制
reflect.Select 允许动态构建多路选择,绕过编译期 select 语句限制:
cases := []reflect.SelectCase{
{Dir: reflect.SelectRecv, Chan: reflect.ValueOf(ch)},
{Dir: reflect.SelectSend, Chan: reflect.ValueOf(ch), Send: reflect.ValueOf(42)},
}
chosen, recv, ok := reflect.Select(cases)
// chosen: 被选中的 case 索引(0 或 1)
// recv: 接收值(若为接收操作)
// ok: 是否成功(false 表示通道已关闭)
该调用触发 runtime.selectgo,进而调用 runtime.poll_runtime_pollWait —— 此函数桥接 goroutine 与底层 I/O 多路复用(如 epoll/kqueue),实现可中断的阻塞。
底层调度关键路径
| 组件 | 作用 | 触发条件 |
|---|---|---|
runtime.selectgo |
实现非阻塞/阻塞 select 逻辑 | select{} 或 reflect.Select |
runtime.poll_runtime_pollWait |
将 goroutine 关联到 fd 并挂起 | channel 底层使用 netpoller(如管道、socket) |
gopark |
切出当前 goroutine | 等待就绪事件 |
graph TD
A[reflect.Select] --> B[runtime.selectgo]
B --> C{是否有就绪 channel?}
C -->|是| D[立即返回]
C -->|否| E[runtime.poll_runtime_pollWait]
E --> F[gopark → 等待 netpoll 通知]
F --> G[就绪后唤醒 goroutine]
4.4 生产级可观测性建设:通过runtime.ReadMemStats与pprof trace反向推演调度瓶颈
在高并发Go服务中,仅依赖/debug/pprof/goroutine?debug=2易掩盖真实调度延迟。需结合内存分配速率与trace时序对齐分析。
内存分配速率作为调度压力代理指标
var m runtime.MemStats
runtime.ReadMemStats(&m)
log.Printf("alloc_rate_MBps: %.2f",
float64(m.TotalAlloc-m.PauseTotalAlloc)/float64(time.Since(lastTime).Seconds())/1024/1024)
TotalAlloc累计分配字节数,PauseTotalAlloc为GC暂停期间分配量;差值反映活跃goroutine真实分配压力,单位MB/s。突增常预示goroutine堆积或锁竞争。
pprof trace反向定位阻塞点
go tool trace -http=:8080 trace.out
在Web UI中聚焦Synchronization → Blocking Profile,可定位chan send、mutex contention等调度器等待事件。
| 指标 | 健康阈值 | 风险含义 |
|---|---|---|
| Goroutines > 10k | 可能存在泄漏或扇出失控 | |
| GC pause > 5ms | 内存压力引发STW延长 | |
runtime.mcall占比 |
协程切换开销异常 |
graph TD A[ReadMemStats采集分配速率] –> B{速率突增?} B –>|是| C[触发pprof trace采样] B –>|否| D[继续监控] C –> E[分析trace中block events分布] E –> F[定位channel/mutex/网络IO瓶颈]
第五章:总结与展望
技术栈演进的实际影响
在某大型电商平台的微服务重构项目中,团队将原有单体架构迁移至基于 Kubernetes 的云原生体系。迁移后,平均部署耗时从 47 分钟压缩至 92 秒,CI/CD 流水线成功率由 63% 提升至 99.2%。关键指标变化如下表所示:
| 指标 | 迁移前 | 迁移后 | 变化幅度 |
|---|---|---|---|
| 服务启动时间(均值) | 8.3s | 1.7s | ↓ 79.5% |
| 故障恢复平均耗时 | 14.6min | 22s | ↓ 97.5% |
| 日均人工运维工时 | 32.5h | 4.1h | ↓ 87.4% |
生产环境灰度策略落地细节
该平台采用 Istio + 自研流量染色 SDK 实现多维度灰度发布:按用户设备 ID 哈希路由至 v2.3 版本(占比 5%),同时对支付链路强制启用全链路加密校验。2024 年 Q2 共执行 137 次灰度发布,其中 3 次因风控模型误判触发自动熔断——系统在 8.4 秒内完成流量回切,并通过 Prometheus Alertmanager 向值班工程师企业微信推送结构化告警(含 traceID、错误码、上游服务名)。
# 灰度规则热加载脚本(生产环境已验证)
kubectl apply -f - <<EOF
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: payment-service
spec:
hosts:
- "payment.example.com"
http:
- match:
- headers:
x-env:
exact: "gray-v2.3"
route:
- destination:
host: payment-service
subset: v2.3
weight: 5
EOF
架构债务清理的量化实践
团队建立技术债看板,将“硬编码数据库连接字符串”“未打标 Pod”“缺失健康检查端点”等 42 类问题映射为可扫描规则。借助 Checkov + 自定义 Rego 策略,在 GitLab CI 中嵌入静态检测环节。半年内累计拦截 1,843 处高危配置,其中 76% 在 PR 阶段被阻断,剩余 24% 通过自动化修复 Bot 提交修正 MR(Bot 使用 Terraform Provider SDK 动态生成 patch)。
未来基础设施演进路径
下阶段将推进 eBPF 加速的 Service Mesh 数据平面替代 Envoy Sidecar。已在预发集群部署 Cilium 1.15,实测 DNS 解析延迟降低 63%,TLS 握手吞吐提升 2.1 倍。Mermaid 图展示新旧数据平面对比:
graph LR
A[客户端请求] --> B{Cilium eBPF Proxy}
B --> C[应用容器]
C --> D[内核级 TLS 卸载]
D --> E[直连后端服务]
A -.-> F[Envoy Sidecar]
F --> G[用户态 TLS 处理]
G --> H[HTTP/2 转发]
H --> I[后端服务]
style B fill:#4CAF50,stroke:#388E3C
style F fill:#f44336,stroke:#d32f2f
工程效能工具链整合
将 Argo CD 的 ApplicationSet 与 Jira Issue API 深度集成,当开发人员在 Jira 创建类型为 “infra-change” 的任务时,自动触发 GitOps 流水线生成命名空间 YAML、RBAC 规则及监控仪表盘配置。该机制已支撑 2024 年新增的 89 个业务线独立环境交付,环境创建 SLA 达到 4 分钟内完成。
