第一章:Golang关机控制概述与核心原理
在分布式系统与长期运行的服务进程中,优雅关机(Graceful Shutdown)并非简单的进程终止,而是保障请求处理完整性、资源安全释放与状态一致性的重要机制。Golang 原生提供了 net/http.Server 的 Shutdown() 方法及 os.Signal 机制,使开发者能主动监听系统中断信号(如 SIGINT、SIGTERM),并协调服务停止流程。
优雅关机的核心要素
- 信号监听:捕获
os.Interrupt(Ctrl+C)或syscall.SIGTERM(Kubernetes 等环境常用); - 连接 draining:拒绝新连接,但允许已建立的 HTTP 连接完成响应;
- 资源清理:关闭数据库连接池、取消后台 goroutine、刷新日志缓冲区等;
- 超时约束:防止无限等待,强制终止未完成任务以保障关机确定性。
标准信号处理模式
以下代码展示了典型实现结构:
package main
import (
"context"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
func main() {
server := &http.Server{Addr: ":8080", Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello, world!"))
})}
// 启动服务 goroutine
go func() {
if err := server.ListenAndServe(); err != http.ErrServerClosed {
log.Fatalf("server failed: %v", err)
}
}()
// 监听退出信号
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit // 阻塞等待信号
log.Println("Shutting down server...")
// 执行优雅关机:30秒超时
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
log.Fatalf("server shutdown error: %v", err)
}
log.Println("Server exited gracefully")
}
该模式确保 HTTP 服务器在收到终止信号后,不再接受新请求,同时等待活跃请求完成或超时。关键在于 server.Shutdown() 必须配合上下文(context)使用,否则将永久阻塞。
关键行为对比表
| 行为 | server.Close() |
server.Shutdown() |
|---|---|---|
| 新连接处理 | 立即拒绝 | 拒绝新连接,允许旧连接完成 |
| 已存在连接 | 强制断开 | 等待完成或超时 |
| 资源清理 | 不自动触发 | 自动调用 Handler.ServeHTTP 完成路径清理(需配合中间件) |
关机逻辑必须与业务生命周期对齐——例如数据库连接池应在其 Close() 方法被显式调用前,确保所有 pending 查询已提交或回滚。
第二章:优雅关机的五种典型场景实现
2.1 HTTP服务器优雅关闭:监听信号+Context超时协同实践
优雅关闭的核心在于双重保障机制:OS信号捕获与context.Context超时控制协同生效。
信号监听与上下文初始化
srv := &http.Server{Addr: ":8080", Handler: mux}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
// 启动服务 goroutine
go func() {
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
log.Fatal(err)
}
}()
逻辑分析:context.WithTimeout 创建带10秒截止的父上下文,用于后续所有请求处理;ListenAndServe 非阻塞启动,避免主线程挂起。
关闭流程协同
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
<-sigChan // 等待终止信号
if err := srv.Shutdown(ctx); err != nil {
log.Printf("HTTP shutdown error: %v", err)
}
逻辑分析:srv.Shutdown(ctx) 会拒绝新连接,并等待活跃请求在ctx超时前完成;若超时则强制中断。
| 机制 | 作用域 | 超时控制 | 不可中断性 |
|---|---|---|---|
os.Signal |
进程生命周期 | ❌ | ✅ |
context.Context |
请求处理链路 | ✅ | ❌(可取消) |
graph TD
A[收到SIGTERM] --> B[触发srv.Shutdown]
B --> C{活跃请求是否≤10s?}
C -->|是| D[正常退出]
C -->|否| E[Context超时→强制终止]
2.2 gRPC服务优雅终止:GracefulStop与连接 draining 实战
gRPC 的 GracefulStop 并非立即关闭监听,而是进入 draining 状态:拒绝新连接、允许活跃 RPC 完成、等待空闲超时。
核心行为对比
| 方法 | 新连接 | 活跃 RPC | 超时控制 | 适用场景 |
|---|---|---|---|---|
Stop() |
立即拒绝 | 强制中断 | 无 | 紧急强制下线 |
GracefulStop() |
拒绝 | 允许完成 | 内置等待 | 发布/扩缩容 |
关键代码示例
// 启动服务后,收到 SIGTERM 时触发
server.GracefulStop() // 阻塞直至所有 RPC 完成或 context.DeadlineExceeded
GracefulStop()内部调用drainTransport(),向所有活跃 HTTP/2 连接发送GOAWAY帧(携带 Last-Stream-ID=0),通知客户端停止新建流;同时启动内部 ticker,每 1s 检查是否仍有活跃流,超时(默认 30s)后强制关闭。
draining 生命周期
graph TD
A[收到 GracefulStop] --> B[发送 GOAWAY]
B --> C[拒绝新连接]
C --> D[等待活跃 RPC 完成]
D --> E{全部完成?}
E -->|是| F[释放资源退出]
E -->|否| G[等待超时]
G --> F
2.3 数据库连接池安全释放:sql.DB.Close 与连接等待策略设计
sql.DB 并非单个连接,而是带生命周期管理的连接池抽象。调用 db.Close() 会阻塞直至所有已检出连接归还,并关闭底层连接。
关键行为差异
db.Close()是幂等、线程安全的- 关闭后新
Query/Exec调用将立即返回sql.ErrTxDone - 不关闭已归还但未销毁的空闲连接(由
SetMaxIdleConns(0)配合触发)
db, _ := sql.Open("postgres", dsn)
db.SetMaxOpenConns(10)
db.SetMaxIdleConns(5)
db.SetConnMaxLifetime(30 * time.Minute)
// 安全关闭:等待活跃操作完成,清理空闲连接
if err := db.Close(); err != nil {
log.Printf("DB close error: %v", err) // 通常为 nil
}
此处
SetConnMaxLifetime确保连接老化回收,避免长连接导致的服务器端资源滞留;Close()本身不中断进行中的事务,仅阻止新请求并等待现有连接归还。
连接等待策略对比
| 策略 | 超时行为 | 适用场景 |
|---|---|---|
| 默认(无限等待) | 阻塞至连接可用 | 低并发、强一致性要求 |
db.SetConnMaxLifetime |
强制重连,间接缓解等待 | 高可用、云环境网络抖动 |
结合 context.WithTimeout |
在 Query 层控制等待上限 | 用户交互型服务 |
graph TD
A[应用发起 Query] --> B{连接池有空闲连接?}
B -->|是| C[复用连接,执行]
B -->|否| D[是否达 MaxOpenConns?]
D -->|否| E[新建连接]
D -->|是| F[阻塞等待或 context 超时]
2.4 消息队列消费者平滑退出:ACK延迟处理与未完成任务回滚机制
ACK延迟窗口设计
为防止进程终止时未确认消息丢失,引入可配置的ACK延迟提交窗口(ackDelayMs=3000):
// 消费后暂不立即ACK,进入延迟确认队列
consumer.registerAckCallback(msg, () -> {
scheduledExecutor.schedule(
() -> channel.basicAck(msg.getEnvelope().getDeliveryTag(), false),
ackDelayMs, TimeUnit.MILLISECONDS
);
});
逻辑分析:延迟ACK将确认动作移交调度器,确保业务逻辑执行完毕且无异常后再提交。ackDelayMs需小于MQ预设的消费者心跳超时(如RabbitMQ默认 heartbeat=60s),避免连接被服务端误判为失联。
未完成任务回滚策略
进程收到 SIGTERM 时触发优雅关闭流程:
- 停止拉取消息(
channel.basicCancel(consumerTag)) - 等待延迟ACK队列清空(最大阻塞
ackDelayMs + 500ms) - 对仍处于“处理中”状态的消息执行
basicNack(requeue=true)
| 场景 | 行为 | 保障目标 |
|---|---|---|
| 正常退出 | 延迟ACK完成 → 消息标记为已处理 | 数据不重复 |
| 异常崩溃 | ACK未触发 → 消息自动重回队列 | 数据不丢失 |
| 长耗时任务 | 超时未完成 → 主动Nack并重入 | 任务不卡死 |
graph TD
A[收到SIGTERM] --> B[暂停消费]
B --> C[等待ACK延迟队列清空]
C --> D{所有延迟ACK已提交?}
D -->|是| E[关闭连接]
D -->|否| F[强制Nack未确认消息]
F --> E
2.5 多组件协同关机:依赖拓扑排序与逆序停止控制器实现
在微服务或模块化系统中,组件间存在显式依赖(如数据库 → 缓存 → API网关)。若按启动顺序反向关闭,将导致资源争用或状态不一致。
依赖建模与拓扑排序
使用有向无环图(DAG)建模组件依赖关系,通过Kahn算法生成拓扑序列:
def topological_sort(deps: Dict[str, List[str]]) -> List[str]:
# deps = {"api": ["cache"], "cache": ["db"], "db": []}
indegree = {k: 0 for k in deps}
for neighbors in deps.values():
for n in neighbors:
indegree[n] += 1
queue = deque([k for k, v in indegree.items() if v == 0])
order = []
while queue:
node = queue.popleft()
order.append(node)
for neighbor in deps.get(node, []):
indegree[neighbor] -= 1
if indegree[neighbor] == 0:
queue.append(neighbor)
return order # e.g., ["db", "cache", "api"]
该函数返回启动顺序;关机需对其执行 reversed() 得到安全停止序列。
逆序停止控制器核心逻辑
| 步骤 | 操作 | 超时(s) | 安全保障 |
|---|---|---|---|
| 1 | 发送 graceful shutdown | 30 | 等待连接自然释放 |
| 2 | 强制终止进程 | 5 | 防止无限阻塞 |
| 3 | 清理本地临时资源 | — | 避免残留锁或文件句柄 |
graph TD
A[获取拓扑序列] --> B[reverse → 关机顺序]
B --> C[逐个调用StopHandler]
C --> D{健康检查通过?}
D -- 是 --> E[进入下一组件]
D -- 否 --> F[触发降级熔断]
关键参数说明:indegree 统计入度确保无前置依赖才入队;queue 保证无环性;reversed(order) 是关机唯一安全序列。
第三章:强制关机的安全边界与熔断机制
3.1 SIGKILL不可捕获特性分析与进程级强制终止替代方案
SIGKILL(信号值9)由内核直接处理,绕过用户态信号处理机制,无法被signal()、sigaction()捕获或忽略,亦不触发任何清理逻辑。
为何无法捕获?
- 内核在
do_send_sig_info()中对SIGKILL和SIGSTOP做硬编码跳过信号队列; - 进程在
get_signal()中根本不会检查这两个信号的挂起状态。
安全终止的替代路径
- 使用
SIGTERM+ 超时监控(推荐) prctl(PR_SET_PDEATHSIG, ...)监听父进程消亡cgroup v2的cgroup.procs写入配合notify_on_release
典型超时终止脚本
# 向PID发送SIGTERM,10秒后若存活则SIGKILL
kill "$PID" && \
for _ in $(seq 1 10); do
kill -0 "$PID" 2>/dev/null || exit 0
sleep 1
done && \
kill -KILL "$PID"
逻辑:先发
SIGTERM给应用自主清理;每秒轮询kill -0检测是否退出;超时后兜底SIGKILL。参数-0仅检查进程存在性,不发送信号。
| 方案 | 可捕获 | 清理可控 | 适用场景 |
|---|---|---|---|
SIGTERM |
✅ | ✅ | 常规优雅退出 |
SIGKILL |
❌ | ❌ | 真正僵死进程 |
cgroup.kill |
✅(通过事件通知) | ⚠️(需外部协调) | 容器/服务组终止 |
graph TD
A[发起终止请求] --> B{尝试SIGTERM}
B -->|成功退出| C[完成]
B -->|超时未退出| D[触发SIGKILL]
D --> E[内核立即回收资源]
3.2 超时熔断策略:基于time.AfterFunc与原子状态机的强制中断实现
在高并发服务中,单次调用超时可能引发级联雪崩。本节采用 time.AfterFunc 触发强制中断,并通过 atomic.Value 管理熔断状态,避免锁竞争。
核心状态机设计
熔断器仅维护三种原子状态:
StateClosed:正常通行StateOpen:拒绝请求,返回兜底响应StateHalfOpen:试探性放行少量请求
超时中断实现
func WithTimeout(ctx context.Context, timeout time.Duration, fn func()) {
timer := time.AfterFunc(timeout, func() {
// 强制标记为熔断(原子写入)
state.Store(StateOpen)
log.Warn("timeout triggered circuit break")
})
defer timer.Stop()
fn()
}
time.AfterFunc在独立 goroutine 中执行回调,不阻塞主流程;state.Store()保证状态更新的可见性与原子性;defer timer.Stop()防止成功路径下的误触发。
| 状态转换条件 | 触发动作 |
|---|---|
| 连续3次失败 | Closed → Open |
| Open持续30s后首次成功 | Open → HalfOpen |
| HalfOpen下失败率>50% | HalfOpen → Open |
graph TD
A[StateClosed] -->|超时/失败| B[StateOpen]
B -->|冷却期结束| C[StateHalfOpen]
C -->|成功| D[StateClosed]
C -->|失败| B
3.3 强制关机前的数据一致性保障:WAL日志刷盘与临时状态快照保存
WAL刷盘的强制同步策略
PostgreSQL 在 pg_ctl stop -m fast 或异常中断前,会触发 XLogFlush() 强制将 WAL 缓冲区中所有未落盘日志写入磁盘:
// src/backend/access/transam/xlog.c
XLogFlush(XLogRecPtr last_lsn);
// last_lsn:当前事务提交点对应的LSN位置
// 确保该LSN及之前所有日志物理写入磁盘(O_SYNC or fsync())
该调用绕过内核页缓存直写设备,避免因断电丢失已提交事务。
临时状态快照保存机制
崩溃恢复时需重建内存状态,系统在关机前自动保存:
- 共享缓冲区脏页列表(
BufferDescriptors) - 当前活跃事务 ID 映射表(
ProcArray快照) - 后备复制位点(
replorigin_advance()持久化)
WAL刷盘与快照协同流程
graph TD
A[收到SIGTERM] --> B[停止新事务接入]
B --> C[执行XLogFlush至最新LSN]
C --> D[写入pg_control含checkpoint LSN]
D --> E[保存shared memory快照到pg_dynshmem/]
| 保障维度 | 技术手段 | 持久化级别 |
|---|---|---|
| 事务原子性 | WAL预写日志 + fsync | 设备级 |
| 内存状态可重建 | 二进制快照序列化 | 文件系统级 |
| 恢复起点确定性 | pg_control中checkpoint | 原子写+校验和 |
第四章:定时关机与智能调度关机系统构建
4.1 基于time.Ticker与cron表达式的精准定时关机调度器
传统 time.AfterFunc 仅支持单次延迟执行,而系统级定时关机需支持周期性校准与 cron 语义(如 0 2 * * * 表示每日凌晨2点)。本方案融合 time.Ticker 的高精度心跳与 robfig/cron/v3 的表达式解析能力。
核心调度逻辑
ticker := time.NewTicker(30 * time.Second) // 每30秒触发一次校准
defer ticker.Stop()
c := cron.New(cron.WithSeconds()) // 支持秒级精度(可选)
_, _ = c.AddFunc("0 0 2 * * ?", func() { // 每日2:00:00关机
exec.Command("shutdown", "-h", "now").Run()
})
c.Start()
逻辑分析:
time.Ticker提供稳定时间源,避免因关机耗时导致的调度漂移;cron实例负责表达式解析与匹配。WithSeconds()启用秒级字段([秒] [分] [时]...),确保0 0 2 * * ?精确到凌晨2:00:00触发。
关键参数对照表
| 字段 | 示例值 | 含义 | 是否必需 |
|---|---|---|---|
| 秒 | |
第0秒触发 | 否(启用秒级模式后) |
| 分 | |
第0分钟 | 是 |
| 时 | 2 |
凌晨2点 | 是 |
执行流程(mermaid)
graph TD
A[Ticker每30s触发] --> B[检查当前时间是否匹配cron规则]
B -->|匹配| C[执行shutdown命令]
B -->|不匹配| D[等待下次Tick]
4.2 系统负载感知关机:集成gopsutil实现CPU/内存阈值触发关机
核心依赖与初始化
使用 gopsutil 的 cpu 和 mem 子包实时采集指标:
import (
"github.com/shirou/gopsutil/cpu"
"github.com/shirou/gopsutil/mem"
)
func getLoadStats() (float64, uint64, error) {
cpuPercents, err := cpu.Percent(1*time.Second, false)
if err != nil {
return 0, 0, err
}
vmem, err := mem.VirtualMemory()
if err != nil {
return 0, 0, err
}
return cpuPercents[0], vmem.Used, nil
}
逻辑说明:
cpu.Percent(1s, false)返回单核平均使用率(false表示非 per-CPU);vmem.Used为已用物理内存字节数。采样间隔需权衡精度与开销。
触发策略配置
支持动态阈值,典型配置如下:
| 指标 | 警戒阈值 | 关机阈值 |
|---|---|---|
| CPU 使用率 | 85% | 95% |
| 内存占用 | 8GB | 10GB |
关机流程
graph TD
A[采集CPU/内存] --> B{CPU ≥ 95%? ∨ Mem ≥ 10GB?}
B -->|是| C[执行shutdown -h now]
B -->|否| D[等待下一轮检测]
4.3 分布式环境下的协调关机:基于Redis分布式锁与Leader选举的集群关机协议
在多节点服务集群中,无序关机易引发数据不一致或请求丢失。需确保仅一个节点承担“关机协调者”职责,并按拓扑依赖顺序通知其他节点。
关键设计原则
- 原子性:关机决策必须全局唯一
- 可靠性:Leader失效时能快速再选举
- 可观测性:每阶段状态可追踪
Redis分布式锁实现(Redlock变体)
import redis
from redlock import RedLock
def acquire_shutdown_lock():
# 使用3个独立Redis实例提升容错性
dl = RedLock(
connection_details=[
{"host": "redis1", "port": 6379, "db": 0},
{"host": "redis2", "port": 6379, "db": 0},
{"host": "redis3", "port": 6379, "db": 0},
],
retry_times=3,
retry_delay=200, # ms
)
return dl.lock("cluster:shutdown:lock", 30000) # TTL=30s
逻辑分析:
RedLock通过多数派(≥N/2+1)实例加锁保障分区容忍;TTL=30s防止死锁,配合心跳续期;retry_delay避免雪崩重试。
协调流程(Mermaid)
graph TD
A[各节点尝试获取shutdown锁] --> B{是否获得锁?}
B -->|是| C[广播“关机准备”消息]
B -->|否| D[监听协调者心跳与指令]
C --> E[等待所有节点ACK]
E --> F[按依赖序发送/shutdown]
状态迁移表
| 当前状态 | 触发事件 | 下一状态 | 超时动作 |
|---|---|---|---|
IDLE |
锁获取成功 | COORDINATING |
重试获取锁 |
COORDINATING |
收到全部ACK | ISSUING |
降级为Follower |
ISSUING |
最后节点返回OK | DONE |
强制本地关机 |
4.4 可配置化关机策略引擎:YAML规则驱动的条件关机(如空闲时长、请求QPS低于阈值)
核心设计理念
将关机决策从硬编码解耦为声明式策略,支持动态加载、热更新与多维度条件组合。
YAML规则示例
# shutdown_policy.yaml
rules:
- name: "idle_timeout"
enabled: true
conditions:
- type: "idle_duration"
threshold_seconds: 1800 # 空闲超30分钟
- type: "qps"
metric_path: "/metrics/qps_5m"
threshold: 2.0
operator: "<"
action:
type: "graceful_shutdown"
delay_seconds: 60
该配置定义复合触发条件:仅当连续空闲≥1800秒且5分钟平均QPS同时满足时,启动60秒优雅关机。
metric_path支持Prometheus/OpenTelemetry指标路径,operator扩展性强(支持<=,==,in等)。
策略执行流程
graph TD
A[加载YAML] --> B[解析为Rule AST]
B --> C[定时采集指标]
C --> D{所有conditions为true?}
D -- 是 --> E[触发action]
D -- 否 --> C
支持的条件类型对比
| 类型 | 示例参数 | 实时性 | 适用场景 |
|---|---|---|---|
idle_duration |
threshold_seconds: 300 |
高 | 无连接保活实例 |
qps |
threshold: 0.5, window: 60s |
中 | 流量低谷识别 |
cpu_usage |
threshold_percent: 5.0 |
低 | 资源冗余判断 |
第五章:生产环境关机方案选型建议与最佳实践总结
关键业务系统停机窗口约束分析
某省级医保核心平台要求全年非计划停机时长≤5分钟,且仅允许在每月第一个周日凌晨02:00–04:00执行维护。实际压测显示:采用systemctl poweroff --force强制关机导致PostgreSQL 13主库WAL日志截断失败,恢复耗时达17分钟;而启用pg_ctl stop -m fast预同步后执行标准关机,平均恢复时间压缩至42秒。该案例表明,数据库状态感知必须前置嵌入关机流程。
容器化集群的协同关机序列
Kubernetes集群关机不可简单调用节点shutdown -h now。某电商大促前演练暴露问题:Node节点直接断电导致DaemonSet监控代理残留、StatefulSet PVC绑定未释放。推荐采用分阶段脚本:
# 阶段1:优雅驱逐(保留30秒缓冲)
kubectl drain ${NODE} --ignore-daemonsets --grace-period=30 --timeout=60s
# 阶段2:确认Pod清理完成
kubectl get pods -A --field-selector spec.nodeName=${NODE} | grep -q "No resources found" || exit 1
# 阶段3:触发底层关机
systemctl poweroff
混合云环境关机依赖拓扑
下图展示金融客户跨AZ关机依赖链,虚线框表示必须按箭头顺序执行:
flowchart LR
A[对象存储OSS] -->|1. 确认所有写入完成| B[API网关]
B -->|2. 停止新连接接入| C[微服务集群]
C -->|3. 等待所有HTTP连接空闲| D[MySQL读写分离集群]
D -->|4. 主库切换为只读并刷盘| E[本地备份服务器]
E -->|5. 执行最后快照上传| F[物理主机]
电力故障场景下的容灾关机协议
某IDC遭遇市电中断时,UPS仅支撑12分钟。原方案等待UPS告警后批量关机,导致3台数据库节点因IO阻塞未响应shutdown指令。优化后引入双阈值机制:
- 一级阈值(UPS剩余8分钟):自动触发
/usr/local/bin/graceful-stop.sh,暂停应用写入、关闭非关键服务 - 二级阈值(UPS剩余3分钟):强制执行
echo 1 > /proc/sys/kernel/sysrq && echo o > /proc/sysrq-trigger
关机验证检查清单
| 检查项 | 验证命令 | 合格标准 | 频次 |
|---|---|---|---|
| 内存脏页率 | cat /proc/meminfo \| grep Dirty |
关机前实时 | |
| 网络连接数 | ss -s \| grep "established" |
= 0 | 驱逐后立即 |
| 数据库事务 | psql -c "SELECT count(*) FROM pg_stat_activity WHERE state='active';" |
= 0 | 最终确认阶段 |
| 存储队列深度 | iostat -x 1 3 \| tail -1 \| awk '{print $10}' |
每30秒轮询 |
灰度关机实施路径
某CDN厂商将1200台边缘节点分5批次关机:首批20台节点启用--dry-run模式记录所有操作日志;第二批加入curl -X POST http://localhost:9000/shutdown?timeout=120健康检查钩子;第三批开始注入网络延迟模拟(tc qdisc add dev eth0 root netem delay 200ms)验证超时逻辑;最终全量执行时,通过Prometheus Alertmanager自动拦截CPU使用率>90%的异常节点。
自动化关机审计追踪
所有关机操作必须写入不可篡改日志,某银行采用以下组合策略:
- 操作日志通过rsyslog转发至独立SIEM服务器(含SHA256校验)
- 关机命令执行前生成区块链存证:
echo "$(date -u +%s) $(hostname) shutdown" | sha256sum >> /var/log/blockchain.log - 每次关机后自动生成PDF报告,包含
journalctl -S '2 hours ago' -u systemd-logind --no-pager原始日志摘要
物理设备固件级关机兼容性
HP ProLiant DL380 Gen10服务器在升级iLO5固件至2.85版本后,发现ipmitool chassis power off命令存在0.8秒延迟,导致上游负载均衡器健康检查超时。解决方案是改用ipmitool -I lanplus -H ${ILO_IP} -U admin -P pwd raw 0x00 0x02发送裸协议指令,并在Ansible Playbook中增加重试逻辑(retries: 3, delay: 1)。
