mirror of
https://github.com/Baim0/baim0-learn.git
synced 2026-10-19 17:57:45 +08:00
add k8s container disk IOPS/BPS trouble, linux ifcfg backup, update APIServer cache note
This commit is contained in:
@@ -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 <name> -n <ns> -o yaml
|
||||
# → Error from server (NotFound):etcd 里确实没有
|
||||
|
||||
# 2. LIST 强制走 cache:RV=0,模拟 informer 的读路径
|
||||
kubectl get pods -A --resource-version=0 | grep <name>
|
||||
# → 还能查到:cache 是脏的
|
||||
|
||||
# 3. 直连每个 APIServer 副本分别查询,定位是哪个副本的 cache 脏
|
||||
for s in <apiserver-1> <apiserver-2> <apiserver-3>; do
|
||||
curl -s --cacert ... "https://$s/api/v1/pods?resourceVersion=0" | grep <name>
|
||||
done
|
||||
|
||||
# 4. 直查 etcd 拿权威真值
|
||||
etcdctl get /registry/pods/<ns>/<name>
|
||||
# → 无记录
|
||||
|
||||
# 5. scheduler 日志伴随大量 bind 失败
|
||||
# "binding rejected: ... pods \"<name>\" 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 脏数据。
|
||||
|
||||
@@ -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 脏数据。
|
||||
@@ -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 <pod-name>` 输出第一列)。
|
||||
|
||||
在 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 退出前抓**,退出后进程消失,现场不可回溯。
|
||||
@@ -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-<设备名>:<n>`、`ifcfg-<设备名>-range<n>`、`ifcfg-<设备名>.<vlan数字>` | 是(别名/批量别名/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)
|
||||
Reference in New Issue
Block a user