From f4b3cb9246052f13a7098ea9f90b6bad4c8656ca Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 24 Sep 2026 10:35:48 +0800 Subject: [PATCH] add k8s container disk IOPS/BPS trouble, linux ifcfg backup, update APIServer cache note --- ...ver Cache 脏数据导致 Ghost Pod 仍在调度.md | 123 +++++++++-- k8sTrouble/01 数据不一致问题.md | 48 ----- ...ontainerd 响应超时、initContainer 卡住问题总结.md | 79 +++++++ linux/04 ifcfg备份文件被执行机制总结.md | 202 ++++++++++++++++++ 4 files changed, 391 insertions(+), 61 deletions(-) delete mode 100644 k8sTrouble/01 数据不一致问题.md create mode 100644 k8sTrouble/03 容器盘 IOPS-BPS 打满导致 containerd 响应超时、initContainer 卡住问题总结.md create mode 100644 linux/04 ifcfg备份文件被执行机制总结.md diff --git a/k8sTrouble/01 APIServer Cache 脏数据导致 Ghost Pod 仍在调度.md b/k8sTrouble/01 APIServer Cache 脏数据导致 Ghost Pod 仍在调度.md index 5f2604f..41889db 100644 --- a/k8sTrouble/01 APIServer Cache 脏数据导致 Ghost Pod 仍在调度.md +++ b/k8sTrouble/01 APIServer Cache 脏数据导致 Ghost Pod 仍在调度.md @@ -11,28 +11,121 @@ categories: 发现调度器仍在调度已不存在或早已下线的 Pod。Pod 实际已不存在(etcd 中无记录),但 kube-scheduler 组件重启后 list/watch 仍能获取到该 Pod 信息,疑似脏数据导致。 -## 问题原因 +## 背景:scheduler 的读写分别走的什么链路 -根因是 **APIServer cache 与 etcd 数据不一致**: +要理解「为什么不一致」,先要搞清楚 scheduler 读 Pod、写 Binding 各自走的是哪条路径、由谁提供服务。 -- 部分 APIServer 副本的 watch cache 数据是旧的(stale),导致 scheduler 通过 informer → watch → APIServer cache 这条链路一直能读到旧的 pending Pod 数据。 -- 调度器写 binding 走的是 **etcd quorum**(强一致性),所以写的时候拿到的是真值(404,Pod 已不存在)。 -- 调度器读 Pod 走的是 **informer → watch → APIServer cache**(最终一致性),所以读到的是 stale 数据。 +### kube-scheduler 是什么 -> **读写两条路径不同步,这是 ghost Pod 的本质。** +kube-scheduler 是控制面组件,职责只有一个:**把 `spec.nodeName` 为空的 Pending Pod 绑定到合适的 Node**。它内部维护: + +- **informer 缓存**:通过 client-go informer 监听 Pod / Node 等资源,调度时**只读本地 informer cache**,不会每次调度都直连 APIServer 查询。 +- **调度队列**:`activeQ`(待调度)/ `backoffQ`(失败退避)/ `unschedulableQ`(暂不可调度)。 +- **scheduler cache + assume 机制**:调度出结果后先乐观地在本地 assume 占位,再由异步 goroutine 发起真正的 Bind;Bind 失败则 forget 回滚并把 Pod 重新入队。 + +### scheduler 怎么「读」Pod:是 GET 还是 watch? + +**两者都是,且都是 HTTP GET,区别只在 `watch` 参数。** informer 底层是 client-go 的 Reflector,工作分两步: + +``` +第一步 LIST:GET /api/v1/pods?resourceVersion=0 + → 一次性拿全量快照 + → RV=0 语义是"任意版本即可",APIServer 直接从 watchCache 返回,不查 etcd + +第二步 WATCH:GET /api/v1/pods?watch=true&resourceVersion=<上一步拿到的RV> + → 长连接,chunked 流式推送 ADDED / MODIFIED / DELETED 事件 + → 同样由 watchCache 服务,不查 etcd +``` + +之后 Reflector 把事件写入本地 store,scheduler 的调度队列和 cache 全部消费这份本地数据。 + +> 关键点:**LIST(RV=0)和 WATCH 都命中 APIServer 内存中的 watchCache**,整条读链路从头到尾不触碰 etcd。 + +### APIServer watchCache 是什么 + +- watchCache 是 APIServer 内部、**每种资源一份**的缓存层,专门用来服务上面这类 LIST + WATCH 请求,避免所有客户端直接打挂 etcd。 +- 它的数据来自:启动时向 etcd 发一次 LIST 拿全量,之后**对 etcd 建立 WATCH 持续增量同步**(不是轮询)。 +- **每个 APIServer 副本各自独立维护一份 watchCache**,多副本之间没有任何同步机制。scheduler 通过 LB 随机连到某个副本,读到哪份 cache 取决于连上了谁。 + +### scheduler 怎么「写」Binding + +调度成功后,binder 调用 Binding 子资源: + +``` +POST /api/v1/namespaces/{ns}/pods/{name}/binding +``` + +写请求没有任何缓存可走:APIServer 的 binding storage 直接对 etcd 做事务(先 quorum 读出当前对象、校验存在性,再条件写入)。Pod 在 etcd 中已不存在 → 直接返回 **404 NotFound**。 ### 架构示意 ``` -调度器 ──读 Pod──→ Informer ──Watch──→ APIServer Cache(stale ❌) +调度器 ──读 Pod──→ Informer(LIST RV=0 + WATCH) ──→ APIServer watchCache(stale ❌) 调度器 ──写 Binding──→ APIServer ──→ etcd Quorum(真实 ✅) ``` +## 问题原因 + +根因是 **APIServer watchCache 与 etcd 数据不一致**: + +- watchCache 依赖「对 etcd 的 watch」来维护,一旦这条 watch 出现异常(断连重建丢事件、progress notify / bookmark 处理 bug 等),cache 中就可能**残留 etcd 里早已删除的对象**,且不再有新事件来纠正它——最终一致窗口被异常放大成永久不一致。 +- 该 APIServer 副本的 watchCache 是旧的(stale),scheduler 通过 informer → LIST/WATCH → watchCache 这条链路,一直能读到旧的 Pending Pod 数据。 +- 调度器写 Binding 走 **etcd quorum**(强一致),拿到的是真值(404,Pod 已不存在)。 +- 调度器读 Pod 走 **informer → watchCache**(最终一致),读到的是 stale 数据。 + +> **读写两条路径不同步,这是 ghost Pod 的本质。** + +### 为什么会陷入死循环 + +``` +informer 里 ghost Pod 一直是 Pending(stale cache 永远不会发 DELETED) + ↓ +Pod 进入调度队列,被正常 Filter/Score 选出节点 + ↓ +本地 assume 占位 → 异步发起 Bind + ↓ +APIServer 查 etcd → Pod 不存在 → 404 NotFound + ↓ +bind 失败 → forget 回滚 assume → Pod 重新入队 + ↓ +________回到第一步,无限循环________ +``` + +这也解释了为什么**重启 scheduler 无效**:重启后 informer 重新 LIST + WATCH,只要连上的还是那个 cache 脏的 APIServer 副本,ghost Pod 又会被原样拉回来。 + +## 排查确认方法 + +核心思路:**对比「走 cache 的读」与「走 etcd 的读」的结果**。这里有一个关键事实——**GET 单个对象默认走 etcd quorum 读,不经过 watchCache**,因此可以很方便地交叉验证: + +```bash +# 1. GET 单对象:走 etcd quorum,返回真值 +kubectl get pod -n -o yaml +# → Error from server (NotFound):etcd 里确实没有 + +# 2. LIST 强制走 cache:RV=0,模拟 informer 的读路径 +kubectl get pods -A --resource-version=0 | grep +# → 还能查到:cache 是脏的 + +# 3. 直连每个 APIServer 副本分别查询,定位是哪个副本的 cache 脏 +for s in ; do + curl -s --cacert ... "https://$s/api/v1/pods?resourceVersion=0" | grep +done + +# 4. 直查 etcd 拿权威真值 +etcdctl get /registry/pods// +# → 无记录 + +# 5. scheduler 日志伴随大量 bind 失败 +# "binding rejected: ... pods \"\" not found" +``` + +若 1/4(etcd 视角)与 2/3(cache 视角)结果不一致,即可确认是 watchCache 脏数据。 + ## 解决方案 ### 短期:滚动重启异常 APIServer -滚动重启缓存数据异常的 APIServer 副本即可恢复。重启后 watch cache 重建,与 etcd 数据重新同步。 +滚动重启缓存数据异常的 APIServer 副本即可恢复。重启后 watchCache 重建(重新 LIST + WATCH etcd),与 etcd 数据重新同步。 ### 长期:升级集群至 1.34+ @@ -40,9 +133,13 @@ Kubernetes 1.34 及以上版本默认开启 `KUBE_WATCHCACHE_CONSISTENCY_CHECKER ## 关键要点 -| 链路 | 一致性模型 | 结果 | -| ------ | ---------------- | -------------- | -| 读 Pod | APIServer Cache | 可能读到 stale | -| 写 Pod | etcd Quorum | 始终为真实值 | +| 请求 | 由谁服务 | 一致性模型 | 结果 | +| -------------------------- | ------------------ | ----------- | -------------- | +| LIST(RV=0,informer 用) | watchCache | 最终一致 | 可能读到 stale | +| WATCH(informer 用) | watchCache | 最终一致 | 可能读到 stale | +| GET 单对象(kubectl get) | etcd quorum 读 | 强一致 | 始终为真实值 | +| 写 Binding(scheduler 用) | etcd quorum 事务写 | 强一致 | 始终为真实值 | -排查此类问题时,核心思路是:**对比 informer 侧拿到的数据与直接查 etcd/APIServer 的结果是否一致**,若不一致,则高度怀疑 watch cache 脏数据。 +- 读链路(LIST + WATCH)和写链路(Binding)走的路径完全不同,前者命中 APIServer 内存 cache,后者直打 etcd,**读写不一致是 ghost Pod 的根因**。 +- watchCache 是**每个 APIServer 副本独立一份**,多副本集群中只有部分副本可能脏,表现为「时好时坏」。 +- 排查核心思路:**对比 informer 侧拿到的数据与直接查 etcd/GET 单对象的结果是否一致**,若不一致,则高度怀疑 watchCache 脏数据。 diff --git a/k8sTrouble/01 数据不一致问题.md b/k8sTrouble/01 数据不一致问题.md deleted file mode 100644 index 53c8be3..0000000 --- a/k8sTrouble/01 数据不一致问题.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -title: APIServer Cache 脏数据导致 Ghost Pod 仍在调度 -date: 2026-06-02 -tags: -- kubernetes排障 -categories: -- kubernetes排障 ---- - -## 问题现象 - -发现调度器仍在调度已不存在或早已下线的 Pod。Pod 实际已不存在(etcd 中无记录),但 kube-scheduler 组件重启后 list/watch 仍能获取到该 Pod 信息,疑似脏数据导致。 - -## 问题原因 - -根因是 **APIServer cache 与 etcd 数据不一致**: - -- 部分 APIServer 副本的 watch cache 数据是旧的(stale),导致 scheduler 通过 informer → watch → APIServer cache 这条链路一直能读到旧的 pending Pod 数据。 -- 调度器写 binding 走的是 **etcd quorum**(强一致性),所以写的时候拿到的是真值(404,Pod 已不存在)。 -- 调度器读 Pod 走的是 **informer → watch → APIServer cache**(最终一致性),所以读到的是 stale 数据。 - -> **读写两条路径不同步,这是 ghost Pod 的本质。** - -### 架构示意 - -``` -调度器 ──读 Pod──→ Informer ──Watch──→ APIServer Cache(stale ❌) -调度器 ──写 Binding──→ APIServer ──→ etcd Quorum(真实 ✅) -``` - -## 解决方案 - -### 短期:滚动重启异常 APIServer - -滚动重启缓存数据异常的 APIServer 副本即可恢复。重启后 watch cache 重建,与 etcd 数据重新同步。 - -### 长期:升级集群至 1.34+ - -Kubernetes 1.34 及以上版本默认开启 `KUBE_WATCHCACHE_CONSISTENCY_CHECKER` 功能,会定期检查 APIServer cache 与 etcd 之间的一致性,并在检测到不一致时自动修复(自愈),从根源上杜绝该类问题。 - -## 关键要点 - -| 链路 | 一致性模型 | 结果 | -| ------ | --------------- | -------------- | -| 读 Pod | APIServer Cache | 可能读到 stale | -| 写 Pod | etcd Quorum | 始终为真实值 | - -排查此类问题时,核心思路是:**对比 informer 侧拿到的数据与直接查 etcd/APIServer 的结果是否一致**,若不一致,则高度怀疑 watch cache 脏数据。 \ No newline at end of file diff --git a/k8sTrouble/03 容器盘 IOPS-BPS 打满导致 containerd 响应超时、initContainer 卡住问题总结.md b/k8sTrouble/03 容器盘 IOPS-BPS 打满导致 containerd 响应超时、initContainer 卡住问题总结.md new file mode 100644 index 0000000..136a04a --- /dev/null +++ b/k8sTrouble/03 容器盘 IOPS-BPS 打满导致 containerd 响应超时、initContainer 卡住问题总结.md @@ -0,0 +1,79 @@ +# 容器盘 IOPS/BPS 打满导致 containerd 响应超时、initContainer 卡住问题总结 + +- 发生时间:2026-09-11 12:10 前后(日志时间戳) +- 问题对象:Pod 创建流程卡在 initContainer(`volume-mount-hack`)阶段 + +## 一、故障现象 + +Pod 创建后卡在 initContainer init 阶段超过 5 分钟,期间 containerd 日志反复出现 ttrpc 与 container 启动失败的相关报错。 + +kubelet 事件(kubectl describe pod)摘要: + +| Type | Reason | Age | From | Message | +| --- | --- | --- | --- | --- | +| Normal | Pulled | 3m03s | kubelet | Container image "docker-reg.devops.xiaohongshu.com/..." already present on machine | +| Normal | Created | 2m43s | kubelet | Created container: volume-mount-hack | +| Warning | Failed | 43s | kubelet | Error: context deadline exceeded | +| Warning | FailedToRetrieveImagePullSecret | 41s (x2 over 4m5s) | kubelet | Unable to retrieve some image pull secrets (yhs-docker...) | + +containerd 日志关键报错: + +``` +level=error msg="get state for 30fa4819971f73c14cc4925094570410665570a2fe8a087e6bd98b5ba0566f92" error="context deadline exceeded" +level=error msg="get state for a3404e1d58cb2162466e8651e423dec57d2bfb7a7b7026db64b2caef957025de" error="context deadline exceeded" +level=error msg="ttrpc: received message on inactive stream" stream=22775 +... +level=error msg="Failed to handle backoff event container id=a3404e1d58cb2162466e8651e423dec57d2bfb7a7b7026db64b2caef957025de" exit_status=137 reason=exited at={seconds:1789099792 nanos:410576122} error="failed to handle container TaskExit event: failed to stop sandbox: failed to delete task: context deadline exceeded" +level=info msg="CreateContainer within sandbox \"bd456f52feee691b333726d96d24cd80794f8415442f7471ea91f3126f8f66\" for container &ContainerMetadata{Name:volume-mount-hack, Attempt: 1, 1}" +level=error msg="StartContainer for \"a3404e1d58cb2162466e8651e423dec57d2bfb7a7b7026db64b2caef957025de\" failed" error="rpc error: code = DeadlineExceeded desc = failed to create containerd task: failed to create shim task: context canceled" +level=error msg="failed to create shim task: context canceled" +``` + +典型特征: + +- 反复出现 `get state ... error="context deadline exceeded"` +- ttrpc 流异常:`received message on inactive stream` +- 容器 `volume-mount-hack`(initContainer)创建后,`StartContainer` 以 `DeadlineExceeded / context canceled` 失败 +- TaskExit 处理时 `exit_status=137`,且 `failed to stop sandbox: failed to delete task: context deadline exceeded` + +## 二、排查过程 + +1. 根据 kubelet 事件 `Error: context deadline exceeded` 与 containerd 日志中的 `DeadlineExceeded`,定位到 **runc start 长时间阻塞**,最终触发 **kubelet 120 秒 CRI 超时**: + Pod 创建流程 → CreateContainer 成功(volume-mount-hack)→ StartContainer 阶段 runc start 长期阻塞 → kubelet CRI 调用超时(context deadline exceeded)→ 清理/回退阶段同样因超时失败(delete task: context deadline exceeded)。 +2. 现有日志中缺少 runc shim 日志,**无法直接判断 runc start 为什么阻塞**。需要在下一次复现时,趁 runc shim 进程退出之前抓取现场信息(见第四节命令)。 +3. 结合现场表现,确认是**容器盘 IO 能力被打满**:容器盘中存在业务 Pod 做临时落盘,多个 Pod 同时写入将容器盘的 **IOPS 和 BPS 打满**,导致 containerd(含 shim/runc 调用链路)读写等待、响应超时。 + +## 三、问题结论 + +- **根因**:业务 Pod 在容器盘做临时落盘,多个 Pod 同时写入打满了容器盘的 IOPS 与 BPS,磁盘 IO 排队导致 containerd 相关调用(runc start、state 查询、shim 操作)长时间阻塞、响应超时,进而使 Pod 卡在 initContainer 阶段超过 5 分钟(kubelet 120s CRI 超时)。 +- **不是** containerd/runtime 自身 bug,而是底层容器盘 IO 资源被业务写流量耗尽。 + +## 四、解决方案与建议 + +1. **挂载 PVC,减少落盘动作**:业务临时落盘改写到独立 PVC(云盘/共享存储),避免直接写容器盘,容器盘只承载镜像与运行时数据。 +2. 控制并发写入:限制同一节点上多个 Pod 同时进行大流量临时落盘,避免 IOPS/BPS 同时打满。 +3. 配套监控告警:对容器盘 IOPS/BPS/IO 等待时延建立监控与阈值告警,便于提前发现打满风险。 +4. 复盘取证:若再次复现,在 shim 退出前抓取现场(下述命令),以便确认阻塞点(如 runc start 卡在哪个系统调用)。 + +## 五、复现时的现场抓取命令(重要) + +`$SANDBOX_ID` 替换为真实的 sandbox id(`crictl pods | grep ` 输出第一列)。 + +在 runc shim 进程退出之前执行: + +```bash +SHIM_PID=$(pgrep -af containerd-shim-runc-v2 | awk -v id="$SANDBOX_ID" 'index($0,id){print $1; exit}') +kill -USR1 "$SHIM_PID" +ps -eLo pid,tid,ppid,stat,wchan:32,etime,args > /tmp/processes.txt +for t in /proc/"$SHIM_PID"/task/*; do + echo "===== ${t##*/} =====" + cat "$t/syscall" + cat "$t/stack" +done > /tmp/shim-stacks.txt 2>&1 +``` + +说明: + +- `kill -USR1` 让 containerd-shim-runc-v2 打印当前 goroutine/线程栈,定位阻塞位置。 +- `/tmp/processes.txt` 记录全量进程状态(含 `wchan`,可看出 D 状态等待在哪),`/tmp/shim-stacks.txt` 记录 shim 各线程的系统调用与内核栈。 +- 关键是**趁 shim 退出前抓**,退出后进程消失,现场不可回溯。 diff --git a/linux/04 ifcfg备份文件被执行机制总结.md b/linux/04 ifcfg备份文件被执行机制总结.md new file mode 100644 index 0000000..09d3b2d --- /dev/null +++ b/linux/04 ifcfg备份文件被执行机制总结.md @@ -0,0 +1,202 @@ +# ifcfg 备份文件会被 network 服务当配置执行(机制分析 + 源码) + +- **记录日期**:2026-09-24 +- **结论**:在 CentOS/RHEL 7 系(老式 initscripts)环境下,`/etc/sysconfig/network-scripts/` 里面的 ifcfg 备份文件**会被 network 服务枚举并真实执行**,只要满足两个条件: + 1. 文件名以 `ifcfg-` 开头、且不以 `~ / .bak / .old / .orig / .rpmnew / .rpmorig / .rpmsave` **结尾**(即 `ifcfg-eth0.bak0923`、`ifcfg-eth0.bakup0921` 这类"bak+日期"命名会**逃过过滤**); + 2. 文件里写了 `DEVICE=eth0`(有它,配置就作用到真实 eth0;没有它才会被跳过)。 +- **影响**:地址按"累加"方式生效、默认路由"后者覆盖前者",最终状态取决于文件执行顺序,**行为不可预期**。 + +## 1. 完整执行链条(4 步) + +| 步骤 | 位置 | 行为 | +|---|---|---| +| 1. 枚举 + 过滤 | `network` 服务脚本 | `ls ifcfg-*`,用正则剔除"忽略文件"——**只匹配结尾**的 `.bak/.old/...`,`bak+日期` 命名逃过过滤 | +| 2. 取 DEVICE、查 ONBOOT | `network` 服务脚本 | 从每个文件里 `grep DEVICE=`;DEVICE 为空才回退用文件名;`ONBOOT=yes` 则拉起 | +| 3. 执行 ifup | `network` 服务脚本 | `./ifup eth0.bak0923 boot` | +| 4. 作用对象由文件内 DEVICE 决定 | `ifup` / `network-functions` / `ifup-eth` | 载入备份文件全部变量后,`REALDEVICE=${DEVICE%%:*}` = `eth0`,后续加地址/装路由全部落在 eth0 上 | + +## 2. 源码证据(逐段) + +> 源码取自 [fedora-sysv/initscripts](https://github.com/fedora-sysv/initscripts)(main 分支,与 CentOS/RHEL 7 系 initscripts 同源,行号可能有小差异)。 +> 本目录 `initscripts-source/` 下有 5 个文件的完整副本,可离线核对。 + +### 2.1 枚举 ifcfg-*(`etc/rc.d/init.d/network`,第 42-49 行) + +```sh +# find all the interfaces besides loopback. +# ignore aliases, alternative configurations, and editor backup files +interfaces=$(ls ifcfg-* | \ + LC_ALL=C sed -e "$__sed_discard_ignored_files" \ + -e '/\(ifcfg-lo$\|:\|ifcfg-.*-range\)/d' \ + -e '{ s/^ifcfg-//g;s/[0-9]/ &/}' | \ + LC_ALL=C sort -k 1,1 -k 2n | \ + LC_ALL=C sed 's/ //') +``` + +注释里写明"ignore … editor backup files"——**意图是忽略备份文件**,但实际过滤规则有漏洞(见 2.2)。 + +### 2.2 过滤规则:只匹配"结尾"(`etc/rc.d/init.d/functions`,第 609-614、678-679 行) + +```sh +# Copy of the function is present in usr/sbin/service +is_ignored_file() { + case "$1" in + *~ | *.bak | *.old | *.orig | *.rpmnew | *.rpmorig | *.rpmsave) + return 0 + ;; + esac +``` + +```sh +# A sed expression to filter out the files that is_ignored_file recognizes +__sed_discard_ignored_files='/\(~\|\.bak\|\.old\|\.orig\|\.rpmnew\|\.rpmorig\|\.rpmsave\)$/d' +``` + +> 正则以 `$` 结尾锚定: +> - `ifcfg-eth0.bak` → 结尾是 `.bak` → **被忽略**(正确行为) +> - `ifcfg-eth0.bak0923` → 结尾是 `0923` → **不被忽略**,进入后续流程 +> - `ifcfg-eth0.bakup0921` → 结尾是 `0921` → **不被忽略** +> - `ifcfg-ens18.bakup0921` → 同样不被忽略(但 ens18 不存在,后续会被跳过,见 2.5) + +### 2.3 DEVICE 取自文件内容 + ONBOOT 检查 + 拉起(`etc/rc.d/init.d/network`,第 91-134 行,节选) + +```sh + # bring up all other interfaces configured to come up at boot time + for i in $interfaces; do + unset DEVICE TYPE SLAVE NM_CONTROLLED + eval $(LANG=C grep -F "DEVICE=" ifcfg-$i) + eval $(LANG=C grep -F "TYPE=" ifcfg-$i) + eval $(LANG=C grep -F "SLAVE=" ifcfg-$i) + eval $(LANG=C grep -F "NM_CONTROLLED=" ifcfg-$i) + + if [ -z "$DEVICE" ] ; then DEVICE="$i"; fi + ... + if ( . ./ifcfg-"$i" ; is_false "$ONBOOT" ) ; then + # this loads the module, to preserve ordering + is_available $i + continue + fi + action $"Bringing up interface $i: " ./ifup $i boot + [ $? -ne 0 ] && rc=1 + done +``` + +> 备份文件里 `DEVICE=eth0`、`ONBOOT="yes"` → 走到 `./ifup eth0.bak0923 boot`。 + +### 2.4 作用对象 = 文件里的 DEVICE(`network-scripts/ifup` 第 27、53、168 行;`network-scripts/network-functions` 第 115-120、173-175 行) + +```sh +# ifup +CONFIG=${1} +... +source_config +``` + +```sh +# network-functions +source_config () +{ + CONFIG=${CONFIG##*/} + DEVNAME=${CONFIG##ifcfg-} + . /etc/sysconfig/network-scripts/$CONFIG + [ -r "keys-$DEVNAME" ] && . /etc/sysconfig/network-scripts/keys-$DEVNAME + ... + [ -z "$REALDEVICE" -a -n "$PARENTDEVICE" ] && REALDEVICE=$PARENTDEVICE + [ -z "$REALDEVICE" ] && REALDEVICE=${DEVICE%%:*} +``` + +> `ifup eth0.bak0923` 载入的是 `ifcfg-eth0.bak0923` 的全部变量(`DEVICE=eth0`、`IPADDR=…`、`GATEWAY=…` 全部生效),然后真正的操作对象 `REALDEVICE` = **eth0**。 + +```sh +# ifup 末尾把控制权交给 ifup-eth +exec ${OTHERSCRIPT} ${CONFIG} $2 +``` + +### 2.5 "真实执行"还是"被跳过"的分界(`network-scripts/ifup-eth`) + +**有 `DEVICE=eth0` → 真实执行,操作落在 REALDEVICE 上(第 329 行起):** + +```sh + # set lifetime of address to forever + ip addr change ${ipaddr[$idx]}/${prefix[$idx]} dev ${REALDEVICE} valid_lft forever preferred_lft forever +``` + +**没有 `DEVICE=` → DEVICE 回退为文件名派生的 `eth0.bak0923` → 设备不存在,报错并跳过(第 132-139 行):** + +```sh +# now check the real state +is_available_wait ${REALDEVICE} ${DEVTIMEOUT} || { + if [ -n "$alias" ]; then + net_log $"$alias device ${DEVICE} does not seem to be present, delaying initialization." + else + net_log $"Device ${DEVICE} does not seem to be present, delaying initialization." + fi + exit 1 +} +``` + +> **判读提醒**:日志里看到 `Bringing up interface eth0.bak0923:` 不代表它没生效,要看整行有没有以 `does not seem to be present, delaying initialization` 收尾——**有这句才是"被跳过"**。 + +### 2.6 为什么"执行顺序不可预期"(`network-scripts/ifup-eth` 第 332-340 行) + +```sh + # Set a default route. + if ! is_false "${DEFROUTE}" && [ -z "${GATEWAYDEV}" -o "${GATEWAYDEV}" = "${REALDEVICE}" ]; then + # set up default gateway. replace if one already exists + if [ -n "${GATEWAY}" ] && [ "$(ipcalc --network ${GATEWAY} ${netmask[0]} 2>/dev/null)" = "NETWORK=${NETWORK}" ]; then + ip route replace default ... via ${GATEWAY} ... || + net_log $"Error adding default gateway ${GATEWAY} for ${DEVICE}." +``` + +- 地址:`ip addr change/add` → **累加**(多份文件的不同地址会都加上去); +- 默认路由:`ip route replace` → **后者覆盖前者**; +- 多个文件按 `ls` 字母序依次执行 → 最终状态取决于内容与顺序,不可预期。 + +## 3. 在机器上实锤的方法 + +```bash +# 1) 本次启动/重启实际拉起了哪些"接口名" +grep -i "Bringing up interface" /var/log/messages | tail -30 +journalctl -b -u network --no-pager | tail -80 +# 判读:出现 eth0.bak0923 等名字且整行无 "does not seem to be present" → 真实执行了 + +# 2) 文件差异对比(谁和谁不一样、哪个"赢了") +cd /etc/sysconfig/network-scripts +md5sum ifcfg-eth0 ifcfg-eth0.bak0923 ifcfg-eth0.bakup0921 +grep -H -E "DEVICE|ONBOOT|IPADDR|PREFIX|GATEWAY" ifcfg-eth0* +``` + +## 4. 规避建议 + +- **备份文件不要放在 `/etc/sysconfig/network-scripts/` 里**;非要放,至少不要以 `ifcfg-` 开头(例如改成 `eth0_backup_0921`)。 +- 如果用 `ifcfg-` 开头命名,只能靠"结尾"后缀才能被忽略:`ifcfg-eth0.bak` 可以,`ifcfg-eth0.bak0923` **不行**。 +- 已存在的备份建议统一移走: + +```bash +mkdir -p /root/backup-network-scripts +mv /etc/sysconfig/network-scripts/ifcfg-*bak* /root/backup-network-scripts/ +ls /etc/sysconfig/network-scripts/ifcfg-* # 预期只剩本体配置和 ifcfg-lo +``` + +**文件命名规则速查表:** + +| 文件名 | 是否会被当设备配置执行 | +|---|---| +| `ifcfg-<设备名>`(如 `ifcfg-eth0`) | 是(设备本体配置) | +| `ifcfg-<设备名>:`、`ifcfg-<设备名>-range`、`ifcfg-<设备名>.` | 是(别名/批量别名/VLAN,有专门逻辑) | +| 以 `~`、`.bak`、`.old`、`.orig`、`.rpmnew`、`.rpmorig`、`.rpmsave` **结尾** | 否(被过滤规则忽略) | +| `*.bak0923`、`*.bakup0921`、`*.bak_日期` 等"bak+后缀" | **是,会被枚举执行**(本次踩坑点) | +| 不以 `ifcfg-` 开头的任意命名 | 否 | + +## 5. 参考资料 + +- [Does 'ifcfg-eth0.bak' file load at the boot time? — Red Hat 知识库(官方专题)](https://access.redhat.com/solutions/68127) +- [ifcfg-eth0.bak, bug? — VMware/Broadcom 社区(实测案例:备份文件被当作真实接口配置,新老 IP 同时生效)](https://community.broadcom.com/vmware-cloud-foundation/discussion/ifcfg-eth0bak-bug) +- [多网卡配置文件导致 network 启动失败处理 — 华为云](https://support.huaweicloud.com/trouble-ecs/ecs_trouble_0319.html) +- [启动网络服务时提示 Job for network.service failed… — 阿里云文档](https://help.aliyun.com/zh/ecs/user-guide/prompt-when-start-network-service-job-for-the-network-the-service-failed-because-the-control-process-exited-with-the-error-code-what-should-i-do) +- initscripts 源码(本目录 `initscripts-source/` 有完整副本): + - [etc/rc.d/init.d/network](https://github.com/fedora-sysv/initscripts/blob/main/etc/rc.d/init.d/network) + - [network-scripts/ifup](https://github.com/fedora-sysv/initscripts/blob/main/network-scripts/ifup) + - [network-scripts/ifup-eth](https://github.com/fedora-sysv/initscripts/blob/main/network-scripts/ifup-eth) + - [network-scripts/network-functions](https://github.com/fedora-sysv/initscripts/blob/main/network-scripts/network-functions) + - [etc/rc.d/init.d/functions](https://github.com/fedora-sysv/initscripts/blob/main/etc/rc.d/init.d/functions)