本节摘要:剧本的失败处理体系与调试工具箱:failed_when、ignore_errors、changed_when 三个判定改写器,block-rescue-always 救援结构,debug 与 register 的组合用法,以及把 2.3 节勘察手段升级成系统排障流程。目标:出错时第一时间定位到"哪一层、哪个站点、什么原因"。
Ansible 的默认失败语义是"任务失败即中止该主机后续任务"(其他主机继续)。在此之上,四个层级控制失败行为,从粗到细。
第一层,整体开关。ignore_errors: true 让单个任务的失败不中止——上一章说过,每次使用都值得书面说明理由。ignore_unreachable 是它的姊妹开关,专管连不上的情况:批量操作里部分机器维护中是常态,ignore_unreachable: true 让剧本继续处理其余机器。
第二层,判定改写。failed_when 与 changed_when 是一对孪生:changed_when 修正"算不算变更"(3.6 节),failed_when 修正"算不算失败"。后者最经典的场景是指令的退出码语义不可靠:
- name: 校验配置(该工具即便发现错误也可能退出码为 0) ansible.builtin.command: /opt/stardust/bin/validate-config register: val failed_when: >- val.rc != 0 or 'ERROR' in val.stderr
第三层,救援结构。block-rescue-always 是剧本版 try-except:block 内的失败进入 rescue 补救逻辑,always 无论成败都执行(清理、审计上报):
- name: 发布主流程与回滚 block: - name: 停止应用 ansible.builtin.systemd: name: stardust state: stopped - name: 更新版本 ansible.builtin.unarchive: src: "app-{{ app_version }}.tar.gz" dest: /opt/stardust - name: 启动并验证 ansible.builtin.systemd: name: stardust state: restarted rescue: - name: 回滚到上一版本 ansible.builtin.unarchive: src: "app-{{ previous_version }}.tar.gz" dest: /opt/stardust - name: 重启回滚版本 ansible.builtin.systemd: name: stardust state: restarted always: - name: 上报发布结果到审计接口 ansible.builtin.uri: url: "http://ops-audit.internal/report" method: POST body_format: json run_once: false
rescue 里能引用 ansible_failed_task 与 ansible_failed_result 两个内置变量,知道失败的是哪个任务、返回了什么——回滚逻辑据此分支。第四层,批量失败熔断:max_fail_percentage 与 any_errors_fatal 控制"多少主机失败算整体失败",配合 serial 使用,防止故障批次之后的批次继续踩同一个坑。
工具一,debug 模块加 register。临时变量的标准组合:register 接住结果,debug 的 var: 打印,配合 -v 看 JSON 全貌。
工具二,verbosity 级联。任务级 debug 加 verbosity: 2,平时不打印,加 -vv 时才出现——把调试探针留在剧本里不碍事,这是比注释掉再恢复优雅得多的做法。
- name: 发布前变量自检(加 -vv 才显示) ansible.builtin.debug: var: app_version verbosity: 2
工具三,剧本级断言。assert 模块把前置假设写成硬检查(3.5 节展示过语法),调试的第一步永远是"确认我以为的前提为真":目标组非空、版本号格式、变量类型。
工具四,单点执行。--limit 主机名 把整个剧本作用到一台机器,--start-at-task 任务名 从指定任务开始,--step 逐任务确认。三者组合可以精确复现"在某台机器的某个任务上失败"的场景,不必整本重跑。
2.3 节建立过"错误形态对应站点"的直觉,现在升级成一张决策路径——遇到失败时按此图走,多数问题五分钟内定位:
读这张图的要领是顺序不可跳:先分阶段、再分层、最后才猜原因。实践中大量"灵异问题"最终都落在前两类——解析期与渲染期的错误,它们根本没到过目标机,往环境方向排查纯属浪费时间。
| 报错关键词 | 所处站点 | 高频根因 | 首选动作 |
|---|---|---|---|
| YAML 语法类 | 装载 | 缩进、Tab 混入、引号未闭合 | 按行号检查,编辑器开 YAML lint |
| 变量未定义 | 渲染 | 拼写、层级、作用域 | 回顾 3.4 节四病因 |
| SSH 相关 | 连接 | 免密、known_hosts、非标端口 | 手工 ssh 验证,-vvv 看拼装 |
| 权限拒绝 | 执行 | sudo 授权、文件属主 | -b 或 become_user 核对 sudoers |
| 模块缺失 | 执行 | 集合未安装 | 核对 requirements 并重装 |
表的使用方法:拿报错关键词对第一列,得站点与根因方向,动作列给第一步。这张表建议存在团队 wiki——它是把个人经验变成组织资产的低成本形式。
最后三条纪律。其一,先跑 --check --diff 干跑再全量执行,把"破坏性错误"挡在发生前。其二,每次失败的修复要落进两处:问题本身(代码或环境)与"下次怎么更快发现"(断言、lint 规则或这张速查表)。其三,日志要留存:CI 触发的运行把完整输出归档,事后复盘的价值远大于当场扫一眼。## 调试与性能的两栖技巧
错误处理与性能分析共享一套基础设施,两栖使用能让投入翻倍。计时回调是第一例:它是 6.2 节的性能工具,同时也是调试工具——"这个任务比平时慢了十倍"经常是故障的前兆(依赖外部接口开始超时、目标机资源紧张),耗时趋势的异动值得当告警看。详细度分层是第二例:-vvv 连接层的拼装日志既排连通性故障,也暴露连接复用是否生效(性能参数没生效在这里一眼可见)。register 加 debug 是第三例:调试时的变量打印,固化下来就是巡检断言的素材(那个变量的合理区间,调试时你刚刚查过)。两栖的本质是同一套观测设施服务两个问题——"为什么错了"与"为什么慢"。建观测设施时按两栖标准选型,调试与性能的分析成本同时下降;只按单一目的建设,迟早会为另一个目的重做一遍。