mirror of
https://github.com/Baim0/baim0-learn.git
synced 2026-10-23 13:57:44 +08:00
Compare commits
3
Commits
8a1aebe250
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3b0f68a42c | ||
|
|
bdc37b3777 | ||
|
|
f4b3cb9246 |
@@ -11,28 +11,121 @@ categories:
|
|||||||
|
|
||||||
发现调度器仍在调度已不存在或早已下线的 Pod。Pod 实际已不存在(etcd 中无记录),但 kube-scheduler 组件重启后 list/watch 仍能获取到该 Pod 信息,疑似脏数据导致。
|
发现调度器仍在调度已不存在或早已下线的 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 数据。
|
### kube-scheduler 是什么
|
||||||
- 调度器写 binding 走的是 **etcd quorum**(强一致性),所以写的时候拿到的是真值(404,Pod 已不存在)。
|
|
||||||
- 调度器读 Pod 走的是 **informer → watch → APIServer cache**(最终一致性),所以读到的是 stale 数据。
|
|
||||||
|
|
||||||
> **读写两条路径不同步,这是 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(真实 ✅)
|
调度器 ──写 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
|
||||||
|
|
||||||
滚动重启缓存数据异常的 APIServer 副本即可恢复。重启后 watch cache 重建,与 etcd 数据重新同步。
|
滚动重启缓存数据异常的 APIServer 副本即可恢复。重启后 watchCache 重建(重新 LIST + WATCH etcd),与 etcd 数据重新同步。
|
||||||
|
|
||||||
### 长期:升级集群至 1.34+
|
### 长期:升级集群至 1.34+
|
||||||
|
|
||||||
@@ -40,9 +133,13 @@ Kubernetes 1.34 及以上版本默认开启 `KUBE_WATCHCACHE_CONSISTENCY_CHECKER
|
|||||||
|
|
||||||
## 关键要点
|
## 关键要点
|
||||||
|
|
||||||
| 链路 | 一致性模型 | 结果 |
|
| 请求 | 由谁服务 | 一致性模型 | 结果 |
|
||||||
| ------ | ---------------- | -------------- |
|
| -------------------------- | ------------------ | ----------- | -------------- |
|
||||||
| 读 Pod | APIServer Cache | 可能读到 stale |
|
| LIST(RV=0,informer 用) | watchCache | 最终一致 | 可能读到 stale |
|
||||||
| 写 Pod | etcd Quorum | 始终为真实值 |
|
| 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,304 @@
|
|||||||
|
# ECS 实例 dmesg 时间与真实时间存在 10 分钟偏差
|
||||||
|
|
||||||
|
- **记录日期**:2026-09-24
|
||||||
|
- **现象**:`dmesg -T` 显示的时间戳与真实时间相差约 10 分钟(偏慢)。
|
||||||
|
- **根因**:系统默认时钟源(clocksource)为 `tsc`,该 TSC 频率偏慢 **33.7ppm**,累计 **220 天** 误差约 **641 秒**(≈ 10.7 分钟),导致系统时钟计数不准、dmesg 时间戳随之偏移。
|
||||||
|
- **解决**:GRUB 启动参数加入 `clocksource=kvm-clock` → 重新生成 grub 配置 → 重启实例,使 MONOTONIC 从 `kvm-clock` 重新起算。
|
||||||
|
|
||||||
|
## 1. 问题描述
|
||||||
|
|
||||||
|
ECS 实例 `dmesg -T` 打印的时间与真实时间(`date`)存在约 10 分钟偏差,且偏差随运行时长持续累积。内核日志自身的时序(先后顺序)是正确的,但**时间戳的绝对刻度不准**。
|
||||||
|
|
||||||
|
## 2. 原因分析
|
||||||
|
|
||||||
|
- 内核中存在多个可选时钟源(TSC、kvm-clock、hpet、acpi_pm 等),启动时会按优先级选定一个作为当前时钟源。
|
||||||
|
- 本实例默认选中了 `tsc`(Time Stamp Counter,CPU 内部计数器)。该 CPU 的 TSC 计数频率与标称频率存在偏差,实测**偏慢 33.7ppm**。
|
||||||
|
- ppm = parts per million,1ppm 即百万分之一:
|
||||||
|
|
||||||
|
```
|
||||||
|
累计误差 = 33.7e-6 × 220 天 × 86400 秒/天
|
||||||
|
= 33.7e-6 × 19,008,000
|
||||||
|
≈ 640.5 秒 ≈ 641 秒 ≈ 10.7 分钟
|
||||||
|
```
|
||||||
|
|
||||||
|
- 该 TSC 在虚拟化环境下不是可靠的授时基准(可能未与宿主机对齐、或缺少稳定/不变标志),误差会线性累积。
|
||||||
|
- `dmesg -T` 的时间戳由内核 `MONOTONIC` 时钟换算而来,时钟源偏慢直接反映为 dmesg 时间显示不准。
|
||||||
|
- `kvm-clock` 是 KVM 半虚拟化时钟源,直接读取宿主机提供的时钟信息,在同为虚拟机的 ECS 实例上精度显著更高,应优先使用。
|
||||||
|
|
||||||
|
## 3. 解决方案
|
||||||
|
|
||||||
|
### 步骤 1:修改 GRUB 启动参数
|
||||||
|
|
||||||
|
编辑 `/etc/default/grub`,在 `GRUB_CMDLINE_LINUX` 中追加 `clocksource=kvm-clock`,例如:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
GRUB_CMDLINE_LINUX="... clocksource=kvm-clock"
|
||||||
|
```
|
||||||
|
|
||||||
|
也可用命令追加(避免手改出错):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sed -i 's/\(GRUB_CMDLINE_LINUX="[^"]*\)"/\1 clocksource=kvm-clock"/' /etc/default/grub
|
||||||
|
grep GRUB_CMDLINE_LINUX /etc/default/grub
|
||||||
|
```
|
||||||
|
|
||||||
|
### 步骤 2:重新生成 GRUB 配置文件
|
||||||
|
|
||||||
|
> Alinux 的 grub 配置文件路径为 `/boot/efi/EFI/alinux/grub.cfg`(UEFI 引导)。
|
||||||
|
|
||||||
|
```sh
|
||||||
|
grub2-mkconfig -o /boot/efi/EFI/alinux/grub.cfg
|
||||||
|
```
|
||||||
|
|
||||||
|
### 步骤 3:重启实例
|
||||||
|
|
||||||
|
```sh
|
||||||
|
reboot
|
||||||
|
```
|
||||||
|
|
||||||
|
重启的必要性:只有重启后才会以新的启动参数引导,`MONOTONIC` 从 `kvm-clock` **重新开始计数**,`dmesg -T` 的时间显示随之恢复正常。
|
||||||
|
|
||||||
|
### 步骤 4:验证
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# 当前时钟源,应显示 kvm-clock
|
||||||
|
cat /sys/devices/system/clocksource/clocksource0/current_clocksource
|
||||||
|
|
||||||
|
# 可选时钟源列表,修改前可先确认 kvm-clock 在列
|
||||||
|
cat /sys/devices/system/clocksource/clocksource0/available_clocksource
|
||||||
|
|
||||||
|
# 确认启动参数已生效
|
||||||
|
cat /proc/cmdline
|
||||||
|
|
||||||
|
# 观察 dmesg 时间戳与 date 是否一致
|
||||||
|
dmesg -T | tail
|
||||||
|
date
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. 备注
|
||||||
|
|
||||||
|
- 修改后 `dmesg` 中已有的历史日志时间戳会因 MONOTONIC 重新起算而重新计算,属正常现象。
|
||||||
|
- 若实例主要用于对时间精度敏感的场景(如日志审计、分布式追踪),建议同时确认 NTP/chrony 正常同步,二者互补:NTP 校正的是墙上时钟(`CLOCK_REALTIME`),clocksource 决定的是时基精度。
|
||||||
|
- 若 `available_clocksource` 中无 `kvm-clock`,说明宿主机/虚拟化环境不支持该时钟源,需改用其他精度更高的时钟源(如 `hpet`、`acpi_pm`)并排查宿主机侧问题。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# kdump 实战:让 hung_task 与 softlockup 触发 vmcore
|
||||||
|
|
||||||
|
## 0. 总结(TL;DR)
|
||||||
|
|
||||||
|
目标是**在内核出现 hung_task(进程长期 D 状态不切出)或 softlockup(CPU 上任务长时间不让出)时,自动 panic 并触发 kdump,把现场落到 `/var/crash` 下的 vmcore**,供后续 `crash` 工具做根因分析。
|
||||||
|
|
||||||
|
最短路径:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1) 开启并确认 kdump 服务
|
||||||
|
systemctl enable --now kdump
|
||||||
|
systemctl status kdump
|
||||||
|
|
||||||
|
# 2) 配置两个 panic 开关
|
||||||
|
echo 'kernel.hung_task_panic=1' >> /etc/sysctl.conf
|
||||||
|
echo 'kernel.softlockup_panic=1' >> /etc/sysctl.conf
|
||||||
|
sysctl -p
|
||||||
|
|
||||||
|
# 3) 复现后取证
|
||||||
|
ls -l /var/crash/
|
||||||
|
crash /usr/lib/debug/lib/modules/$(uname -r)/vmlinux /var/crash/<时间戳>/vmcore
|
||||||
|
```
|
||||||
|
|
||||||
|
> 前提:内核已预留 crashkernel 内存且 kdump 服务处于 active。若 `crashkernel` 没预留,需要改 GRUB 后重启。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 操作一:开启 kdump 服务
|
||||||
|
|
||||||
|
### 1.1 确认内核已预留 crashkernel 内存
|
||||||
|
|
||||||
|
kdump 需要一个**独立预留的内存区域**(capture kernel)来在内核崩溃后接管系统。检查方式:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
grep -o 'crashkernel=[^ ]*' /proc/cmdline
|
||||||
|
cat /sys/kernel/kexec_crash_size # 非 0 表示已预留
|
||||||
|
```
|
||||||
|
|
||||||
|
- 较新发行版通常默认 `crashkernel=auto`(RHEL/CentOS 7+)或 `crashkernel=1G-4G:192M,4G-64G:256M,64G-:512M`。
|
||||||
|
- 若输出为空或 `kexec_crash_size` 为 0,需要在 GRUB 里补上,例如:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# /etc/default/grub
|
||||||
|
GRUB_CMDLINE_LINUX="... crashkernel=512M"
|
||||||
|
|
||||||
|
# 重新生成 grub 配置
|
||||||
|
grub2-mkconfig -o /boot/grub2/grub.cfg # BIOS
|
||||||
|
grub2-mkconfig -o /boot/efi/EFI/centos/grub.cfg # UEFI(路径按发行版调整)
|
||||||
|
# 然后重启
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.2 安装并启动 kdump 服务
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 安装(按发行版)
|
||||||
|
yum install -y kexec-tools # RHEL/CentOS
|
||||||
|
apt install -y kdump-tools # Debian/Ubuntu
|
||||||
|
|
||||||
|
# 开启并立即启动
|
||||||
|
systemctl enable --now kdump
|
||||||
|
|
||||||
|
# 校验
|
||||||
|
systemctl status kdump
|
||||||
|
kdumpctl status 2>/dev/null || true
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.3 确认 kdump 就绪
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cat /sys/kernel/kexec_crash_loaded # 期望 1
|
||||||
|
ls -l /var/crash/ # 崩溃后 vmcore 落在这里
|
||||||
|
```
|
||||||
|
|
||||||
|
### 1.4 关键配置项(/etc/kdump.conf)
|
||||||
|
|
||||||
|
| 配置项 | 说明 |
|
||||||
|
|---|---|
|
||||||
|
| `path /var/crash` | vmcore 落盘目录 |
|
||||||
|
| `core_collector makedumpfile -l --message-level 1 -d 31` | 用 makedumpfile 压缩、并过滤掉全零页等无用页,显著减小 vmcore 体积 |
|
||||||
|
| `default reboot` | dump 完成后重启 |
|
||||||
|
| `ext4 /dev/vg/root` 等 | 根分区未挂载时的兜底落盘目标 |
|
||||||
|
|
||||||
|
> 提示:`-d 31` 会过滤掉零页、缓存页等,**排查时不要用 `-d 0`(体积巨大),也不建议过滤过多**;若怀疑内存内容相关,保留默认即可。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 操作二:配置 hung_task 与 softlockup 触发 panic
|
||||||
|
|
||||||
|
内核默认检测到 hung_task / softlockup 时**只打印告警日志,不会 panic**,所以现场不会产生 vmcore。需要打开 panic 开关。
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cat >> /etc/sysctl.conf <<'EOF'
|
||||||
|
kernel.hung_task_panic=1
|
||||||
|
kernel.softlockup_panic=1
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# 或使用 sysctl.d(推荐,语义更清晰)
|
||||||
|
# cat > /etc/sysctl.d/99-kdump-panic.conf <<'EOF'
|
||||||
|
# kernel.hung_task_panic=1
|
||||||
|
# kernel.softlockup_panic=1
|
||||||
|
# EOF
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.1 两个开关的含义
|
||||||
|
|
||||||
|
| 参数 | 含义 | 默认值 |
|
||||||
|
|---|---|---|
|
||||||
|
| `kernel.hung_task_panic` | 检测到 D 状态(不可中断睡眠)任务超时后,触发 panic | 0(只告警) |
|
||||||
|
| `kernel.softlockup_panic` | 检测到 softlockup(某 CPU 上任务长时间不调度)后,触发 panic | 0(只告警) |
|
||||||
|
|
||||||
|
### 2.2 配套可调参数(按需)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
kernel.hung_task_timeout_secs = 120 # hung_task 判定超时,默认 120s
|
||||||
|
kernel.hung_task_warnings = 10 # 告警打印条数,-1 表示不限
|
||||||
|
kernel.watchdog_thresh = 10 # softlockup 检测阈值基数,默认 10s
|
||||||
|
kernel.nmi_watchdog = 1 # Hardlockup 依赖 NMI 看门狗
|
||||||
|
kernel.hardlockup_panic = 1 # 硬锁(中断被长时间关闭)也 panic,按需
|
||||||
|
kernel.panic_on_oops = 1 # Oops 即 panic,按需
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.3 注意事项
|
||||||
|
|
||||||
|
- **softlockup 检测依赖 watchdog 线程本身能运行**:若 CPU 长时间关中断,属于 hardlockup,需靠 `nmi_watchdog` + `hardlockup_panic` 捕获。
|
||||||
|
- 部分内核 `softlockup_panic` 受编译选项约束;若写入失败,可检查 `CONFIG_BOOTPARAM_SOFTLOCKUP_PANIC`,或改用内核启动参数 `softlockup_panic=1`。
|
||||||
|
- `hung_task_panic=1` 对**合法长 D 状态**(如大 IO、网络存储挂起)也会 panic,生产环境务必评估影响,建议先在测试环境验证。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 操作三:使配置生效
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 方式一:立即加载 sysctl.conf
|
||||||
|
sysctl -p
|
||||||
|
|
||||||
|
# 方式二:只加载某个 sysctl.d 文件
|
||||||
|
sysctl -p /etc/sysctl.d/99-kdump-panic.conf
|
||||||
|
|
||||||
|
# 方式三:重启(持久生效,也是兜底手段)
|
||||||
|
reboot
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.1 生效校验
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sysctl kernel.hung_task_panic kernel.softlockup_panic
|
||||||
|
# kernel.hung_task_panic = 1
|
||||||
|
# kernel.softlockup_panic = 1
|
||||||
|
|
||||||
|
# kdump 侧确认
|
||||||
|
systemctl is-active kdump
|
||||||
|
cat /sys/kernel/kexec_crash_loaded # 1
|
||||||
|
```
|
||||||
|
|
||||||
|
> 建议同时确认 `kdump` 开机自启:`systemctl is-enabled kdump`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 操作四:复现后取证与分析
|
||||||
|
|
||||||
|
### 4.1 vmcore 落盘位置
|
||||||
|
|
||||||
|
问题复现后,系统会 panic → kexec 启动 capture kernel → 由 kdump 把内存镜像写入:
|
||||||
|
|
||||||
|
```
|
||||||
|
/var/crash/<时间戳>/vmcore
|
||||||
|
/var/crash/<时间戳>/vmcore-dmesg.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ls -lh /var/crash/
|
||||||
|
cat /var/crash/<时间戳>/vmcore-dmesg.txt | tail -50 # 先看崩溃瞬间的 dmesg
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 用 crash 工具做根因分析
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 安装 debuginfo(以 CentOS 为例)
|
||||||
|
debuginfo-install kernel-$(uname -r)
|
||||||
|
|
||||||
|
# 加载 vmcore
|
||||||
|
crash /usr/lib/debug/lib/modules/$(uname -r)/vmlinux \
|
||||||
|
/var/crash/<时间戳>/vmcore
|
||||||
|
```
|
||||||
|
|
||||||
|
常用 crash 命令:
|
||||||
|
|
||||||
|
| 命令 | 用途 |
|
||||||
|
|---|---|
|
||||||
|
| `bt` / `bt -a` | 查看崩溃时各 CPU 的调用栈(hung_task/softlockup 常看这里) |
|
||||||
|
| `log` | 崩溃前的内核日志缓冲 |
|
||||||
|
| `ps` / `foreach bt` | 进程状态与全量栈,定位 D 状态任务 |
|
||||||
|
| `vm` | 内存与页状态概览 |
|
||||||
|
| `sys` | 系统基本信息(版本、panic 原因等) |
|
||||||
|
| `dis -l <addr>` | 反汇编定位到源码行 |
|
||||||
|
|
||||||
|
### 4.3 可选:手动触发一次转储做演练
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 需确认 kernel.sysrq=1 且 kdump 已就绪,演练会重启系统
|
||||||
|
echo c > /proc/sysrq-trigger
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 操作五:常见问题排查(Q&A)
|
||||||
|
|
||||||
|
- **`/var/crash` 下没有 vmcore**
|
||||||
|
- `kdump` 服务未运行或未开机自启:`systemctl is-active kdump` / `is-enabled`。
|
||||||
|
- crashkernel 未预留:看 `/proc/cmdline` 与 `/sys/kernel/kexec_crash_size`。
|
||||||
|
- panic 未触发:确认 `hung_task_panic` / `softlockup_panic` 已为 1,且告警类型确实是这两种。
|
||||||
|
- **复现了但只看到告警日志、没有 panic**
|
||||||
|
- 说明只命中了 hung_task/softlockup 告警路径,检查 sysctl 是否被其他配置覆盖(`sysctl -a | grep -E 'hung_task|softlockup'`)。
|
||||||
|
- **`softlockup_panic=1` 写入报不存在**
|
||||||
|
- 内核编译未开放该 sysctl,改用启动参数 `softlockup_panic=1`,或升级/更换内核。
|
||||||
|
- **vmcore 特别大,把磁盘写满**
|
||||||
|
- 确认 `core_collector` 使用 `makedumpfile -d 31` 过滤;必要时落盘到独立分区。
|
||||||
|
- **生产环境慎用**
|
||||||
|
- `hung_task_panic=1` 可能把「可自愈的长 D 状态」也变成宕机,建议先用 `hung_task_timeout_secs` 调大、并在测试环境验证。
|
||||||
@@ -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