安装之前,先让客户的机器说真话
一个只读 bash 脚本把每项检查评为 PASS、WARN 或 FAIL,只有 FAIL 能叫停安装。
要点速览
- Elasticsearch 在生产模式下,引导检查失败就拒绝启动;在开发模式下,同样的失败只是一条警告。预检脚本也应这样分级。
- 每条 FAIL 信息都应写明实测值、要求的阈值和修复方法,就像 Leapp 的报告会讲清升级为何被阻断。
- 允许按名称豁免单项检查,就像 kubeadm 把指定检查的错误降级为警告,但绝不接受“全部豁免”。
Elasticsearch 处于生产模式时,只要有一项引导检查(bootstrap check)失败,它就拒绝启动。在开发模式下,同样的失败只会在日志里显示为一条警告。即使从没替客户装过 Elasticsearch,这条规则也值得带到每一次部署中。
设想在客户现场安装:安装程序跑到一半报错,这时才发现 swap 开着、某个内核参数没设,或者防火墙挡住了某个端口。预检脚本把这些发现全部提前到安装开始之前。它还定下清楚的规则:哪些发现可以叫停安装,哪些只需记录在案。
本指南演示如何用 bash 写这样一个脚本。动手尝试需要一台有 sudo 权限的 Linux 机器或虚拟机。读取内存、swap 和网络端口的命令都是简化版,并假定运行在 Linux 上:真正使用之前,要在客户所用的具体发行版上重新测试。
严重程度要靠规则,不能凭感觉
每项结果都用一个问题来分级:如果忽略它,待安装组件是会拒绝运行,还是只是运行得不好?拒绝运行就是 FAIL。运行不佳,或者拿不准,就是 WARN。
Kubernetes 文档给出了一个明确的 FAIL:默认情况下,kubelet 检测到节点上有 swap 就不会启动。相比之下,一个从没和你的产品一起测试过的 OS 版本不一定有问题,所以只值得一个 WARN。
注意这个问题里的“待安装组件”。swap 之所以是 FAIL,是因为 kubelet,而不是对所有软件都如此。每项检查只有在对应组件出现在安装清单上时才有意义,下面的脚本按组件启用检查。
第 1 步:所有结果都交给一个 report 函数
每项检查都经过同一个函数。这个函数负责应用运行模式和豁免列表。
#!/usr/bin/env bash
# preflight.sh — 只读,不改动客户机器上的任何东西
MODE="${MODE:-production}" # production | development(仅限测试虚拟机)
WAIVE="${WAIVE:-}" # 从 FAIL 降级为 WARN 的检查项,例如 "swap,net_443"
FAILS=0; WARNS=0
if [ "$WAIVE" = all ]; then
echo "不接受 WAIVE=all:请按名称逐项列出检查"; exit 2
fi
report() { # report <id> <PASS|WARN|FAIL> <message>
local id="$1" level="$2" msg="$3"
if [ "$level" = FAIL ]; then
if [ "$MODE" = development ] || [[ ",$WAIVE," == *",$id,"* ]]; then
level=WARN; msg="$msg(已降级为 WARN)"
fi
fi
case "$level" in FAIL) FAILS=$((FAILS+1));; WARN) WARNS=$((WARNS+1));; esac
printf '%-5s %-14s %s\n' "$level" "$id" "$msg"
}
development 模式模仿 Elasticsearch 把错误降级为警告的做法。WAIVE 借鉴自 kubeadm:升级前运行的预检阶段有一个 --ignore-preflight-errors 参数,它接受一组检查项,这些检查的错误只显示为警告。这个脚本有意拒绝 all 这个值,让每一项豁免都必须写明检查名称。
这里有个陷阱:MODE=development 会把所有 FAIL 降级为 WARN,实际上也等于忽略一切。因此这个模式只留给自己的测试虚拟机,绝不在客户的生产机器上运行。在客户机器上只用 production,再加上按名称逐项列出的 WAIVE。
**验证:**调用 report demo FAIL "test",设置 WAIVE=demo 后再运行一次。这次应打印 WARN,并带有“已降级为 WARN”的注明。
第 2 步:检查资源,在信息里写明阈值
MIN_RAM_KB="${MIN_RAM_KB:-2097152}" # 假定的阈值;请换成你的产品推荐值
check_ram() {
local kb; kb=$(awk '/^MemTotal:/ {print $2}' /proc/meminfo)
if [ "$kb" -ge "$MIN_RAM_KB" ]; then report ram PASS "MemTotal=${kb}kB"
else report ram WARN "MemTotal=${kb}kB,建议至少 ${MIN_RAM_KB}kB"; fi
}
check_swap() {
local kb; kb=$(awk '/^SwapTotal:/ {print $2}' /proc/meminfo)
if [ "$kb" -eq 0 ]; then report swap PASS "未启用 swap"
else report swap FAIL "swap=${kb}kB,kubelet 默认不会启动"; fi
}
check_max_map_count() {
local v; v=$(sysctl -n vm.max_map_count 2>/dev/null || echo 0)
if [ "$v" -ge 262144 ]; then report max_map_count PASS "vm.max_map_count=$v"
else report max_map_count FAIL "vm.max_map_count=$v,需要 >= 262144,用 sysctl 修复"; fi
}
每个阈值都属于某个产品。swap 是 Kubernetes 的事,因为 swap 开着时 kubelet 不会运行。通过 sysctl 把 vm.max_map_count 设为至少 262144,是 Elasticsearch 的要求,所以只装 Kubernetes 的客户没有理由被这项检查拦住。
内存评为 WARN,是因为内存不足通常让系统运行不佳,而不是拒绝运行。示例中的 MIN_RAM_KB 阈值是一个假定值:请从所安装产品的文档中取数,并在 README 中注明出处。
还有个小坑:机器报告的 MemTotal 可能低于标称内存,所以一台“2 GB”的虚拟机也可能低于阈值,平白得到一个 WARN。定下数字之前,先在客户的真实机器上跑一遍,看看它们报告的值。
**验证:**在开启 swap 的虚拟机上设置 COMPONENTS=kubernetes,swap 这一行应显示 FAIL。
第 3 步:版本、网络和权限
check_os() {
. /etc/os-release
case "$ID-$VERSION_ID" in
rhel-8*|rhel-9*) report os PASS "$PRETTY_NAME" ;; # 示例列表
*) report os WARN "$PRETTY_NAME 不在已测试列表中" ;;
esac
}
check_port() { # check_port <host> <port> — 从运行脚本的机器发起出站 TCP 连接
if timeout 3 bash -c "</dev/tcp/$1/$2" 2>/dev/null; then
report "net_$2" PASS "已连接 $1:$2"
else report "net_$2" FAIL "无法建立到 $1:$2 的连接"; fi
}
check_root() {
if [ "$(id -u)" -eq 0 ]; then report root PASS "以 root 身份运行"
else report root WARN "非 root,修复步骤需要 sudo"; fi
}
示例中的 OS 列表是假定的。请换成你实际和产品一起测试过的列表,陌生的 OS 一律保持 WARN。
check_port 只检查出站方向:这台机器能否与某个端点(比如镜像仓库)建立 TCP 连接。要检查入站方向,比如 Kubernetes API server 的 6443 端口,需要在客户网络中的另一台机器上运行同一个函数。Kubernetes 文档也建议用 netcat 做这项检查。
通过 /dev/tcp 连接是一种捷径。它能告诉你连接能否建立,但不能代替向客户询问代理和防火墙的情况。如果客户用同一个模板创建多台虚拟机,还要比对各节点的 MAC 地址和 product_uuid:kubeadm 文档提醒,重复的值可能导致安装失败。
第 4 步:按组件选择检查,再返回退出码
COMPONENTS="${COMPONENTS:-kubernetes}" # 例如 "kubernetes,elasticsearch"
has() { [[ ",$COMPONENTS," == *",$1,"* ]]; }
check_os; check_root; check_ram
has kubernetes && check_swap
has elasticsearch && check_max_map_count
check_port registry.example.com 443 # 换成真实的端点
echo "合计:$FAILS 个 FAIL,$WARNS 个 WARN"
[ "$FAILS" -eq 0 ]
COMPONENTS 变量让每项检查各归其位:只有涉及 kubelet 时 swap 才会阻断,只有涉及 Elasticsearch 时 vm.max_map_count 才会阻断。最后一行把结果转成退出码,这样安装程序、Ansible 或 CI 无需有人看输出就会停下。
在一台开着 swap 的测试虚拟机上以普通用户运行,输出大致如下。这是示意输出:OS 名称和 kB 数值都是假定的,你的机器会报告不同的数字。
PASS os Red Hat Enterprise Linux 9
WARN root 非 root,修复步骤需要 sudo
WARN ram MemTotal=1863000kB,建议至少 2097152kB
FAIL swap swap=2097148kB,kubelet 默认不会启动
PASS net_443 已连接 registry.example.com:443
合计:1 个 FAIL,2 个 WARN
**验证:**运行 bash preflight.sh; echo $?。只要还有 FAIL,结果就是非零。在测试虚拟机上,MODE=development bash preflight.sh; echo $? 应返回 0,即使机器有问题也一样。
让预检失去公信力的几个错误
最严重的错误是让脚本自己修机器,比如调用 sysctl -w。kubeadm init 一开始会运行一系列预检,在做任何改动之前验证系统状态。kubeadm upgrade apply 的预检阶段还有 --dry-run 模式,只打印将要执行的操作,不改变状态。
预检的价值就在于什么都还没动;修复是客户 IT 团队的事,按他们自己的流程来。
下一个错误是信息含糊。要记住的规则是:每条 FAIL 都必须带上实测值、要求的阈值和修复方法,就像第 2 步里 max_map_count 那一行。
RHEL 升级工具 Leapp 是值得学习的范例。它把阻断性的发现称为 inhibitor(阻断项),明确指出哪些问题阻碍了升级,比如某个已被移除的内核驱动,并把报告写入 /var/log/leapp 下的文件。
据 Microsoft Learn 介绍,阻断项必须先解决,升级才能继续,所以它们正是你的 FAIL;把输出保存到文件,比如通过 tee,方便客户查看。
还有两个错误。把所有项都标成 FAIL,包括客户根本不装的产品的检查,会让客户习惯性地忽略它,没多久人人都想全部豁免。退出码永远是 0,则会让流水线若无其事地继续往下跑。
在客户现场,脚本是对话的开端
最有效的做法是在安装前几天把脚本发给客户的 IT 团队,请他们用 production 模式运行并把输出贴回来。每条 FAIL 变成一张有负责人的工单。每项通过 WAIVE 的豁免都记下批准人的名字。到了安装日,你已经知道哪些机器还没准备好。
对想转型做前线部署工程师(FDE)的人来说,这是一个小而精的作品集项目,恰好展示雇主想看的能力。把脚本放进代码仓库,附上一份示例输出,以及一张解释每项检查为何是 WARN 或 FAIL 的表格。
在简历上,把它描述为降低客户基础设施部署风险的工具。面试时,准备好解释为什么 swap 是 FAIL 而内存不足只是 WARN:这个回答能表明你会权衡风险,而不只是会写命令。
客户往往记住的不是安装程序跑得多快,而是你在任何东西出问题之前,就指出了哪些机器还没准备好。
这篇文章对你有帮助吗?
感谢你的反馈!
7 个信源
- Bootstrap checks | Elastic Documentation
- Maximum map count check | Elasticsearch Guide 8.19
- kubeadm upgrade apply phase preflight | Kubernetes v1.34
- kubeadm init | Kubernetes
- Installing kubeadm | Kubernetes
- Running Leapp pre-upgrade analysis: take unknowns out of your next RHEL upgrade · 2024-04-24
- Upgrade RHEL pay-as-you-go virtual machines using Leapp - Virtual Machines | Microsoft Learn · 2026-08-21