第一章:Go网络连接可靠性基础概念
网络连接的可靠性并非单纯指“能连上”,而是涵盖连接建立成功率、传输完整性、故障恢复能力以及资源生命周期管理等多个维度。在Go语言中,net包提供的底层抽象(如net.Conn接口)与http.Client等高层封装共同构成了可靠性保障的基础层。理解这些组件的行为边界,是构建健壮网络服务的前提。
连接建立阶段的关键控制点
Go默认使用阻塞式Dial,但生产环境需主动约束超时以避免goroutine永久挂起:
// 使用Dialer显式设置超时,避免默认无限等待
dialer := &net.Dialer{
Timeout: 5 * time.Second, // 连接建立最大耗时
KeepAlive: 30 * time.Second, // TCP keep-alive间隔
}
client := &http.Client{
Transport: &http.Transport{
DialContext: dialer.DialContext,
// 其他配置...
},
}
该配置确保DNS解析、TCP三次握手、TLS协商均受统一超时管控。
连接复用与状态管理
HTTP/1.1默认启用连接复用,但复用前提是连接处于Idle且未被对端关闭。Go通过http.Transport的MaxIdleConns和MaxIdleConnsPerHost参数控制空闲连接池规模,防止资源泄漏或服务端拒绝连接。
常见可靠性陷阱与应对
| 问题现象 | 根本原因 | 推荐方案 |
|---|---|---|
i/o timeout 频发 |
仅设置Client.Timeout,未分离连接/读写超时 |
使用Timeout、IdleConnTimeout、ResponseHeaderTimeout分层控制 |
| 连接泄漏(fd耗尽) | http.Response.Body未被关闭 |
必须调用resp.Body.Close(),即使读取失败 |
| TLS握手失败无重试 | 默认不重试TLS协商 | 自定义http.RoundTripper实现有限重试逻辑 |
故障检测的主动策略
除超时外,应结合net.Error.Temporary()判断错误是否可重试,并配合指数退避重试机制。例如对临时性DNS错误或连接拒绝,可延迟后重试,而非立即失败。
第二章:Go标准库网络连通性检测实践
2.1 TCP主动探测与超时控制的理论边界与net.DialTimeout实现
TCP连接建立本质是三次握手过程,其超时行为受操作系统协议栈与应用层双重约束。net.DialTimeout 仅控制连接发起阶段的阻塞上限,不干预后续读写超时。
关键参数语义
deadline:从调用开始计时,非往返RTT累加;- 底层依赖
setsockopt(SO_RCVTIMEO/SO_SNDTIMEO)与connect()系统调用返回逻辑; - 实际超时可能略长于设定值(内核定时器粒度、调度延迟)。
Go标准库实现节选
// src/net/dial.go 简化逻辑
func DialTimeout(network, addr string, timeout time.Duration) (Conn, error) {
d := &Dialer{Timeout: timeout}
return d.Dial(network, addr)
}
该函数封装 Dialer 结构体,将 timeout 绑定至底层 connect 系统调用的 SO_RCVTIMEO(Linux)或 WSAConnect 超时(Windows),但不触发TCP保活(keepalive)探测。
| 超时类型 | 触发时机 | 是否由 DialTimeout 控制 |
|---|---|---|
| 连接建立超时 | SYN未获SYN-ACK | ✅ |
| 传输中空闲超时 | ESTABLISHED后无数据 | ❌(需 SetDeadline) |
| 主动探测失败 | keepalive检测断连 | ❌(需 SetKeepAlive) |
graph TD
A[net.DialTimeout] --> B[创建socket]
B --> C[设置SO_RCVTIMEO]
C --> D[调用connect系统调用]
D --> E{成功?}
E -->|是| F[返回Conn]
E -->|否| G[返回timeout error]
2.2 HTTP健康端点探测的语义完整性验证与http.Client定制化配置
HTTP健康端点(如 /health)不仅需返回 200 OK,更应确保响应体 JSON 中 status: "UP" 且关键依赖项(db, cache)状态均为 UP。
语义完整性校验逻辑
func validateHealthResponse(resp *http.Response) error {
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("unexpected status: %d", resp.StatusCode)
}
var health map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&health); err != nil {
return fmt.Errorf("invalid JSON: %w", err)
}
if status, ok := health["status"].(string); !ok || status != "UP" {
return errors.New("top-level status is not 'UP'")
}
return nil
}
该函数先校验HTTP状态码,再解析JSON并双重验证:结构可解码性 + 语义正确性(status 字段值),避免“假阳性”探测。
http.Client 定制要点
- 超时控制:
Timeout(总超时)、IdleConnTimeout(连接复用) - 重试策略:需在业务层实现(标准库不内置)
- Transport 复用:避免 goroutine 泄漏
| 配置项 | 推荐值 | 说明 |
|---|---|---|
Timeout |
3s | 防止探测长期阻塞 |
MaxIdleConns |
100 | 控制空闲连接池大小 |
TLSHandshakeTimeout |
2s | 防 TLS 握手卡死 |
graph TD
A[发起 GET /health] --> B{连接建立?}
B -->|否| C[返回连接错误]
B -->|是| D{TLS握手成功?}
D -->|否| E[返回 TLS 错误]
D -->|是| F[发送请求+等待响应]
F --> G[校验状态码与JSON语义]
2.3 DNS解析异常模式识别与net.Resolver结合context.WithTimeout实战
DNS解析失败常表现为超时、NXDOMAIN、SERVFAIL等模式,需结合上下文控制实现精准熔断。
常见异常模式对照表
| 异常类型 | Go错误特征 | 建议响应策略 |
|---|---|---|
| 超时 | &net.OpError{Err: context.DeadlineExceeded} |
降级或重试 |
| 域名不存在 | &net.DNSError{IsNotFound: true} |
快速失败,记录告警 |
| 服务器拒绝 | &net.DNSError{IsTimeout: false, IsTemporary: true} |
指数退避重试 |
带超时的Resolver调用示例
resolver := &net.Resolver{
PreferGo: true,
Dial: func(ctx context.Context, network, addr string) (net.Conn, error) {
return net.DialTimeout(network, addr, 2*time.Second)
},
}
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
ips, err := resolver.LookupHost(ctx, "example.com")
该代码构建了带双层超时(Dial + Lookup)的Resolver:context.WithTimeout 控制整体解析上限,DialTimeout 约束底层连接建立;PreferGo 启用纯Go解析器,规避cgo依赖与系统resolv.conf干扰。cancel() 确保资源及时释放,避免goroutine泄漏。
异常分类处理流程
graph TD
A[发起LookupHost] --> B{ctx.Done?}
B -->|是| C[返回context.Canceled/DeadlineExceeded]
B -->|否| D[解析响应]
D --> E{错误类型}
E -->|NXDOMAIN| F[标记无效域名]
E -->|SERVFAIL| G[切换备用DNS]
2.4 连接重试策略的指数退避模型与backoff.RetryNotify在Go中的工程落地
为什么朴素重试不可取
频繁等间隔重试(如每100ms重试一次)会加剧服务雪崩,尤其在下游临时过载时。
指数退避的核心思想
每次失败后,等待时间按因子递增:t = base × 2^n + jitter,其中 n 为重试次数,jitter 防止同步冲击。
backoff.RetryNotify 的典型用法
err := backoff.RetryNotify(
func() error { return api.Call() },
backoff.WithContext(backoff.NewExponentialBackOff(), ctx),
func(err error, d time.Duration) {
log.Warn("retrying after", "delay", d, "err", err)
},
)
NewExponentialBackOff()默认InitialInterval=500ms,MaxInterval=60s,MaxElapsedTime=15m;RetryNotify在每次重试前回调通知,便于可观测性埋点。
退避参数对照表
| 参数 | 默认值 | 作用 |
|---|---|---|
InitialInterval |
500ms | 首次等待时长 |
Multiplier |
2.0 | 退避倍率 |
MaxInterval |
60s | 单次最大等待上限 |
MaxElapsedTime |
15m | 整体重试总时限 |
graph TD
A[开始重试] --> B{调用成功?}
B -- 否 --> C[计算下次延迟]
C --> D[应用 jitter 随机偏移]
D --> E[休眠 delay]
E --> F[执行下一轮]
B -- 是 --> G[返回结果]
2.5 TLS握手失败归因分析与tls.Dial日志增强与错误分类捕获
TLS握手失败常源于证书链不完整、SNI未匹配、协议版本/密钥套件不兼容或网络中间设备干扰。tls.Dial 默认错误信息过于笼统(如 x509: certificate signed by unknown authority),难以快速定位根因。
增强日志与错误分类捕获
conn, err := tls.Dial("tcp", "api.example.com:443", &tls.Config{
ServerName: "api.example.com",
InsecureSkipVerify: false,
}, &tls.Dialer{
KeepAlive: 30 * time.Second,
}.DialContext)
if err != nil {
// 分类提取错误类型
if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
log.Printf("network timeout: %v", err)
} else if tlsErr, ok := err.(tls.RecordHeaderError); ok {
log.Printf("TLS record header mismatch (likely HTTP instead of TLS): %v", tlsErr)
}
}
该代码通过类型断言对 net.Error 和 tls.RecordHeaderError 进行精准识别:前者捕获连接超时/拒绝,后者揭示明文HTTP响应被误当TLS处理的典型“400 Bad Request”伪装场景。
常见握手失败原因对照表
| 错误现象 | 根因类别 | 排查线索 |
|---|---|---|
remote error: tls: bad certificate |
服务端证书异常 | 检查证书有效期、CN/SAN 匹配 |
tls: first record does not look like a TLS handshake |
协议错位 | 目标端口运行HTTP而非HTTPS服务 |
x509: certificate has expired |
本地/服务端时间偏差 | 对比系统时间与 openssl s_client -connect ... -servername 输出 |
握手失败诊断流程
graph TD
A[tls.Dial 调用] --> B{连接建立?}
B -->|否| C[网络层错误:DNS/防火墙/超时]
B -->|是| D{TLS记录头校验}
D -->|失败| E[非TLS服务/ALPN协商失败]
D -->|成功| F[证书验证/密钥交换阶段]
F --> G[证书链/时间/策略违规等]
第三章:基于eBPF的网络异常可观测性增强
3.1 eBPF程序注入网络栈关键路径的原理与libbpf-go集成架构
eBPF 程序并非直接挂载到内核函数,而是通过 hook 点(tracepoint、kprobe、xdp、tc 等) 注入网络协议栈关键路径。例如 TC(Traffic Control)钩子位于 qdisc 层,可在数据包进入/离开协议栈前拦截;XDP 则更早,在驱动收包中断上下文执行,零拷贝处理。
libbpf-go 集成核心流程
// 加载并附加 eBPF 程序到 TC ingress hook
prog := obj.Programs.TcFilter
link, err := tc.AttachProgram(&tc.Program{
Program: prog,
AttachTo: "eth0",
AttachType: tc.BPFAttachTypeTCIngress,
})
obj.Programs.TcFilter:由bpftool gen skeleton生成的 Go 绑定程序对象;tc.BPFAttachTypeTCIngress:指定挂载至网卡 ingress qdisc,优先于 IP 层解析。
关键挂载点对比
| Hook 类型 | 触发位置 | 延迟开销 | 支持修改包头 |
|---|---|---|---|
| XDP | 驱动层(DMA 后) | 极低 | ✅(重写/丢弃) |
| TC (ingress) | qdisc 入口 | 低 | ✅(mangle) |
| Socket filter | 应用 recv() 前 | 较高 | ❌ |
graph TD
A[网卡收包] --> B[XDP hook]
B -->|pass| C[TC ingress]
C --> D[IP 层处理]
D --> E[socket queue]
3.2 捕获SYN超时、RST风暴、ICMP不可达三类底层异常的eBPF探针设计
为精准识别网络层异常,探针在 tracepoint:tcp:tcp_retransmit_skb 和 kprobe:icmp_send 处部署双路径捕获逻辑:
// 捕获ICMP不可达事件(目的主机/端口不可达)
SEC("kprobe/icmp_send")
int BPF_KPROBE(icmp_send_probe, struct sk_buff *skb, int type, int code, __be32 info) {
if (type == ICMP_DEST_UNREACH && (code == ICMP_PORT_UNREACH || code == ICMP_HOST_UNREACH)) {
bpf_map_update_elem(&icmp_unreach_map, &skb->sk, &code, BPF_ANY);
}
return 0;
}
该钩子拦截内核发送ICMP错误报文前的瞬间,通过 type/code 组合精确过滤目标异常;icmp_unreach_map 以 socket 指针为键,实现与TCP流上下文的快速关联。
异常分类特征对照表
| 异常类型 | 触发位置 | eBPF入口点 | 关键判据 |
|---|---|---|---|
| SYN超时 | 重传定时器超时 | tracepoint:tcp:tcp_retransmit_skb |
skb->sk->sk_state == TCP_SYN_SENT 且 retransmits > 0 |
| RST风暴 | 连续RST包接收 | kprobe:tcp_v4_do_rcv |
th->rst == 1 + 时间窗口内计数 ≥ 5 |
| ICMP不可达 | 内核构造错误响应 | kprobe:icmp_send |
type==ICMP_DEST_UNREACH && code∈{1,3} |
数据同步机制
使用 per-CPU array map 缓存原始事件元数据,由用户态守护进程轮询聚合,避免 map 查找竞争与锁开销。
3.3 Go应用侧eBPF事件消费与实时连接状态映射的ring buffer解析实践
Ring Buffer 初始化与事件订阅
使用 libbpf-go 创建 ring buffer 实例,绑定到 eBPF 程序的 maps:events:
rb, err := ebpf.NewRingBuffer("events", obj, nil)
if err != nil {
log.Fatal("failed to create ring buffer:", err)
}
defer rb.Close()
// 启动异步消费协程
go func() {
for {
rb.Poll(300) // 每300ms轮询一次,避免忙等
}
}()
Poll() 触发内核向用户态推送就绪事件;300 单位为毫秒,平衡延迟与 CPU 开销。
事件结构体定义与内存对齐
需严格匹配 eBPF 端 struct conn_event 布局(含 __u16、__be32 字段):
| 字段 | 类型 | 说明 |
|---|---|---|
sport |
uint16 | 源端口(主机字节序) |
dport |
uint16 | 目标端口(主机字节序) |
saddr |
[4]byte | IPv4 源地址(网络序) |
数据同步机制
采用 unsafe.Pointer + binary.Read 零拷贝解析:
var event ConnEvent
if err := binary.Read(bytes.NewReader(data), binary.LittleEndian, &event); err != nil {
continue // 跳过损坏事件
}
connMap.Store(fmt.Sprintf("%s:%d->%s:%d",
net.IP(event.SAddr[:]).String(), event.SPort,
net.IP(event.DAddr[:]).String(), event.DPort), time.Now())
ConnEvent 结构体字段顺序与 eBPF C 端完全一致,确保 ABI 兼容;connMap 是 sync.Map,支持高并发写入。
graph TD
A[eBPF 程序] -->|write| B[Ring Buffer]
B -->|poll/read| C[Go 用户态]
C --> D[ConnEvent 解析]
D --> E[connMap 实时映射]
第四章:SLO驱动的SLI量化与自适应检测体系
4.1 网络可用性SLI定义(如connect_success_rate)与Prometheus指标建模规范
网络可用性SLI需聚焦可观测、可聚合、业务语义明确的原子指标。connect_success_rate 是核心SLI,定义为:单位时间内成功建立TCP连接数占总连接尝试数的比例。
指标命名与标签规范
- 使用
network_connect_attempts_total(计数器)和network_connect_errors_total(按reason="timeout|refused|dns_fail"打标) - 必须包含
job,instance,direction="outbound",service标签
Prometheus 查询示例
# 5分钟滑动窗口成功率(防瞬时抖动)
1 - rate(network_connect_errors_total{job="api-gateway"}[5m])
/
rate(network_connect_attempts_total{job="api-gateway"}[5m])
逻辑分析:
rate()自动处理计数器重置与时间对齐;分母为总尝试量确保分母非零;5m窗口平衡灵敏性与稳定性;job标签限定服务边界,避免跨环境污染。
推荐标签组合表
| 标签名 | 取值示例 | 必填 | 说明 |
|---|---|---|---|
protocol |
tcp, http, grpc |
✅ | 区分协议栈层级 |
target_host |
auth-service:8080 |
❌ | 可选,用于细粒度根因定位 |
graph TD
A[客户端发起 connect] --> B{内核返回结果}
B -->|0 success| C[+1 network_connect_attempts_total]
B -->|ECONNREFUSED等| D[+1 network_connect_errors_total{reason=\"refused\"}]
4.2 基于滑动窗口的动态阈值计算与go.opentelemetry.io/otel/metric实时聚合
动态阈值需适应流量峰谷,避免静态阈值导致的误告。滑动窗口(如60s内最近10个10s桶)结合指数加权移动平均(EWMA)可平滑噪声。
滑动窗口聚合实现
// 使用 otel/metric + 自定义滑动 window adapter
meter := meterProvider.Meter("app/http")
histogram := metric.Must(meter).NewFloat64Histogram("http.latency.ms")
// 注册带滑动窗口语义的回调观察器
该代码未直接暴露窗口,需配合metric.NewFloat64Histogram与后台View配置,将原始指标路由至自定义Aggregator——后者继承sdk/metric/aggregation并维护环形缓冲区。
动态阈值判定逻辑
- 每5秒触发一次EWMA更新:
α = 0.3,兼顾响应性与稳定性 - 阈值 =
μ + 2σ,其中σ由窗口内样本标准差估算
| 统计量 | 计算方式 | 更新频率 |
|---|---|---|
| μ (均值) | EWMA(当前样本, α) | 实时 |
| σ (标准差) | Welford在线算法 | 每窗口周期 |
graph TD
A[原始指标流] --> B[SlidingWindowAggregator]
B --> C[EWMA + Welford]
C --> D[动态阈值输出]
4.3 多维度异常关联分析(时延+丢包+重传)与Go中histogram与counter协同建模
网络质量退化往往非单一指标所致。需将 P95 时延(ms)、瞬时丢包率(%)、TCP 重传率(per-connection)三者在统一时间窗口内对齐建模,识别共现模式。
数据同步机制
使用 prometheus.HistogramVec 记录时延分布,prometheus.CounterVec 累计丢包与重传事件,共享 label:{service="api", region="sh"}。
// 定义协同指标组
latencyHist := promauto.NewHistogramVec(
prometheus.HistogramOpts{
Name: "network_latency_ms",
Help: "P95 latency in milliseconds",
Buckets: prometheus.ExponentialBuckets(1, 2, 10), // 1ms~512ms
},
[]string{"service", "region"},
)
retryCounter := promauto.NewCounterVec(
prometheus.CounterOpts{
Name: "tcp_retransmit_total",
Help: "Total TCP retransmits per connection",
},
[]string{"service", "region"},
)
ExponentialBuckets(1,2,10)覆盖典型微服务时延量级;retryCounter与latencyHist共享 label,确保同一观测维度下可 join 分析。
关联判定逻辑
当满足以下任一条件即触发告警:
- 时延 P95 > 200ms 且 丢包率 > 0.5%
- 重传率 > 3% 且 近1分钟重传增量 Δ > 50
| 维度 | 阈值 | 触发权重 | 关联敏感度 |
|---|---|---|---|
| P95 时延 | >200 ms | ★★★★ | 高 |
| 丢包率 | >0.5% | ★★★☆ | 中高 |
| 重传率 | >3% | ★★★★ | 高 |
graph TD
A[采集原始指标] --> B[按 service+region 标签聚合]
B --> C{P95延迟>200ms?}
C -->|是| D[检查丢包率>0.5%?]
C -->|否| E[跳过]
D -->|是| F[触发多维异常事件]
D -->|否| E
4.4 SLO违约自动降级触发机制与net/http/pprof+自定义middleware联动响应
当核心API的SLO(如99%请求P95
降级决策中间件逻辑
func SLOViolationMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
latency := time.Since(start)
// 上报指标并检查SLO窗口(滑动60s窗口内P95 > 300ms且占比>1%)
if shouldTriggerDegradation(latency) {
atomic.StoreUint32(°raded, 1) // 全局降级开关
pprof.StartCPUProfile(cpuProfFile) // 启动pprof分析
}
})
}
该中间件在每次请求后实时评估延迟分布;shouldTriggerDegradation基于本地直方图聚合结果判断,避免依赖外部存储引入延迟。
联动响应流程
graph TD
A[SLO违约检测] --> B{P95 > 300ms & 持续120s?}
B -->|是| C[置位atomic.degraded=1]
B -->|否| D[维持正常模式]
C --> E[pprof CPU Profile启动]
C --> F[切换至降级路由组]
降级行为对照表
| 组件 | 正常模式 | 降级模式 |
|---|---|---|
| 用户头像服务 | 实时调用gRPC | 返回CDN缓存默认头像 |
| 搜索建议 | 调用Elasticsearch | 返回Redis热点词列表 |
| 日志上报 | 异步Kafka全量发送 | 采样率降至10%,本地缓冲 |
第五章:总结与开源工具链演进路线
工具链演进的三个现实驱动力
现代工程团队对开源工具链的迭代并非源于技术理想主义,而是被三类刚性需求持续牵引:CI/CD流水线平均构建耗时需压缩至90秒内(GitHub Actions基准测试显示,2023年中位数为142秒);Kubernetes集群配置漂移率必须低于0.8%/周(Flux v2+OCI镜像签名验证将该指标压至0.17%);安全扫描结果误报率需控制在12%以下(Trivy v0.45通过SBOM关联分析将误报率从28%降至9.3%)。某金融级微服务项目实测表明,当工具链版本滞后超3个次要版本时,每日人工干预工单量激增3.7倍。
典型演进路径:从单点工具到协同闭环
下表对比了2021–2024年典型团队工具链组合的结构性变化:
| 维度 | 2021主流方案 | 2024生产就绪方案 | 关键改进点 |
|---|---|---|---|
| 配置管理 | Helm + Kustomize | Argo CD + OCI Registry + Kyverno | 配置变更原子化、策略即代码生效 |
| 日志分析 | ELK Stack | Grafana Loki + Promtail + Cortex | 日志索引体积降低62%,查询延迟 |
| 合规审计 | Manual CIS checks | OpenSCAP + OPA + Sigstore | 审计周期从72小时缩短至23分钟 |
开源工具链的“灰度升级”实践
某跨境电商平台采用渐进式替换策略:在保持Jenkins主控流水线不变前提下,将37个Java服务的单元测试阶段迁移至TestGrid(基于Bazel构建),同时用Tekton Pipeline替代原有Shell脚本部署模块。关键动作包括:
- 编写
pipeline-as-codeYAML模板,强制注入--test-output-dir=/tmp/test-results参数确保结果可追溯 - 在GitLab CI中嵌入
trivy filesystem --security-checks vuln,config --format template --template "@contrib/sarif.tpl" .生成SARIF报告并自动提交至DefectDojo - 通过
kubectl apply -k overlays/staging触发Kyverno策略校验,拦截含hostNetwork: true的Deployment提交
flowchart LR
A[Git Push] --> B{Pre-receive Hook}
B -->|通过| C[Trivy SBOM扫描]
B -->|失败| D[拒绝推送]
C --> E[生成CycloneDX JSON]
E --> F[上传至Dependency-Track]
F --> G[触发风险评估API]
G -->|高危| H[自动创建Jira Issue]
G -->|低危| I[仅记录至Grafana仪表盘]
社区驱动的标准化进程
CNCF Landscape 2024版已将“Observability”分类细化为Metrics/Logs/Traces/Profiles/Events五大子域,其中eBPF-based profiling工具(如Parca、Pixie)在云原生监控栈中的采用率已达41%。值得关注的是,OpenTelemetry Collector贡献者社区于2023年Q4合并了otelcol-contrib的awsxrayexporter插件,使AWS Lambda函数调用链可直接对接X-Ray后端——某视频平台据此将跨AZ调用延迟诊断耗时从4.2小时压缩至17分钟。
生产环境的兼容性陷阱
某政务云项目在升级Argo CD至v2.9时遭遇ApplicationSet控制器崩溃,根因是其自定义Webhook认证器未适配新版本的admissionregistration.k8s.io/v1 API。解决方案并非回退版本,而是采用kubebuilder重构认证逻辑,并通过kubectl convert --output-version=admissionregistration.k8s.io/v1批量转换存量CRD。该案例揭示工具链演进的核心矛盾:功能增强常以API契约收紧为代价,而真正的成熟度体现在能否在不中断业务前提下完成契约迁移。
