Compare commits

...
6 Commits
Author SHA1 Message Date
Your Name 3b0f68a42c docs(linux): normalize markdown heading levels in 00 小问题记录
- 每个问题作为一级标题
- 问题内小节 / 操作章节统一为二级标题
- 操作子项、解决步骤统一为三级标题
2026-10-10 11:31:34 +08:00
Your Name bdc37b3777 add linux kdump vmcore note (hung_task/softlockup trigger) 2026-10-10 11:27:04 +08:00
Your Name f4b3cb9246 add k8s container disk IOPS/BPS trouble, linux ifcfg backup, update APIServer cache note 2026-09-24 10:35:48 +08:00
Your Name 8a1aebe250 add linux troubleshoot 2026-09-16 18:13:56 +08:00
Your Name 694359d9a0 add kube-scheduler 调度性能瓶颈笔记 2026-09-16 18:13:56 +08:00
Your Name 7bc6619dc7 add new 2026-09-16 18:13:56 +08:00
9 changed files with 2192 additions and 48 deletions
@@ -0,0 +1,145 @@
---
title: APIServer Cache 脏数据导致 Ghost Pod 仍在调度
date: 2026-06-02
tags:
- kubernetes排障
categories:
- kubernetes排障
---
## 问题现象
发现调度器仍在调度已不存在或早已下线的 Pod。Pod 实际已不存在(etcd 中无记录),但 kube-scheduler 组件重启后 list/watch 仍能获取到该 Pod 信息,疑似脏数据导致。
## 背景:scheduler 的读写分别走的什么链路
要理解「为什么不一致」,先要搞清楚 scheduler 读 Pod、写 Binding 各自走的是哪条路径、由谁提供服务。
### kube-scheduler 是什么
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(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 副本即可恢复。重启后 watchCache 重建(重新 LIST + WATCH etcd),与 etcd 数据重新同步。
### 长期:升级集群至 1.34+
Kubernetes 1.34 及以上版本默认开启 `KUBE_WATCHCACHE_CONSISTENCY_CHECKER` 功能,会定期检查 APIServer cache 与 etcd 之间的一致性,并在检测到不一致时自动修复(自愈),从根源上杜绝该类问题。
## 关键要点
| 请求 | 由谁服务 | 一致性模型 | 结果 |
| -------------------------- | ------------------ | ----------- | -------------- |
| LIST(RV=0,informer 用) | watchCache | 最终一致 | 可能读到 stale |
| WATCH(informer 用) | watchCache | 最终一致 | 可能读到 stale |
| GET 单对象(kubectl get) | etcd quorum 读 | 强一致 | 始终为真实值 |
| 写 Binding(scheduler 用) | etcd quorum 事务写 | 强一致 | 始终为真实值 |
- 读链路(LIST + WATCH)和写链路(Binding)走的路径完全不同,前者命中 APIServer 内存 cache,后者直打 etcd,**读写不一致是 ghost Pod 的根因**。
- watchCache 是**每个 APIServer 副本独立一份**,多副本集群中只有部分副本可能脏,表现为「时好时坏」。
- 排查核心思路:**对比 informer 侧拿到的数据与直接查 etcd/GET 单对象的结果是否一致**,若不一致,则高度怀疑 watchCache 脏数据。
-48
View File
@@ -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,189 @@
---
title: 大规模集群 kube-scheduler 调度性能瓶颈
date: 2026-08-21
tags:
- kubernetes排障
categories:
- kubernetes排障
---
## 问题现象
集群规模接近 2000 节点、数万 Pod,出现大量 Pod 持续 `Pending` 且**无任何 Event 产生**,调度队列堆积,新 Pod 无法及时调度。
## 问题原因
核心是 kube-scheduler 在大规模集群下计算开销过大,形成恶性循环。先看调度机制,再看三个关键问题点。
### 调度机制
```
新建 Pod → 进入调度队列 → 过滤节点(Filter) → 打分排序(Score) → 绑定节点(Bind)
↑ |
|____________ 调度失败时回退等待(Backoff) _________|
```
- 调度失败的 Pod 进入 **Backoff 队列**,等待时间随失败次数指数增长,上限为 `podMaxBackoffSeconds`。
- **调度失败后会回退等待,而不是立即重试**,这是理解后续问题的关键。
### 抢占机制详解
#### 什么是抢占
当**高优先级 Pod 在 Filter 阶段找不到任何可用节点**时,调度器会尝试驱逐(删除)节点上的低优先级 Pod,为它腾出资源。抢占由 **DefaultPreemption** 插件在 **PostFilter** 扩展点触发,是调度失败后的“最后一搏”。
> 抢占不是无条件发生的:只有 `priority > 0` 的 Pod(即设置了 PriorityClass)才能发起抢占;`priority = 0` 的 Pod 无法抢占他人,但仍可能成为被驱逐的受害者。
#### 抢占流程
```
高优先级 Pod Filter 失败(无任何可调度节点)
↓
触发 PostFilter:DefaultPreemption
↓
并行 DryRun 预演:对(几乎)每个节点模拟“移除低优先级 Pod”
直到该 Pod 能放下为止
↓
选出候选节点 candidate + 受害者列表 victims
↓
NominatePod:将该 Pending Pod 提名(nominated)到目标节点,预留资源
↓
异步驱逐受害者 Pod(走 API Server 删除,尊重优雅终止期)
↓
受害者退出后,Pending Pod 重新入队并被真正调度到该节点
```
#### 受害者选择(Victim Selection)
在**单个节点**上,调度器按以下顺序模拟移除 Pod,直到待调度 Pod 能够放下:
1. **先移除优先级最低的 Pod**(优先级从低到高依次移除);
2. 同优先级下,优先移除**正在终止**的 Pod(deletion timestamp 更早的);
3. 同优先级下,优先移除 **terminationGracePeriod 更短**的 Pod;
4. 过程中尽量满足 **PodDisruptionBudget(PDB)**,但 PDB 只是“尽力而为”的软约束,必要时仍会违反。
#### 候选节点选择(Candidate Selection)
当多个节点都能通过抢占满足要求时,调度器按以下优先级选出**唯一**目标节点:
| 优先级 | 选择标准 | 含义 |
| ------ | ---------------------------- | -------------------------------------- |
| 1 | PDB 违规数量最少 | 尽量少地破坏可用性预算 |
| 2 | 受害者数量最少 | 尽量少驱逐 Pod |
| 3 | 受害者中最高优先级最低 | 尽量驱逐“更不重要”的 Pod |
| 4 | 受害者优先级总和最小 | 整体上损失最小 |
#### 提名 Pod(Nominated Pod)机制
- 抢占成功后,Pending Pod 会被记录为 **Nominated Pod**,其 status 中带上 `nominatedNode` 字段,指向目标节点。
- 在受害者真正被删除、资源释放之前,这个“提名”会**在调度器 cache 中预留**相应资源,避免其他 Pending Pod 重复抢占同一批资源。
- 受害者驱逐是**异步**的,Pending Pod 需要等待受害者真正退出后,才会被调度到该节点。
#### 抢占为什么是性能瓶颈
- 每个无法调度的 Pending Pod 在抢占评估时,都要对(几乎)**所有节点**执行一次 DryRun:遍历节点上所有 Pod、按优先级排序、模拟移除。
- 单次抢占评估复杂度 ≈ `待调度 Pod 数 × 节点数 × 每节点 Pod 数`。
- 本案例:`2000 节点 × 数万 Pod`,抢占评估(而非正常调度)成为最主要的 CPU 开销来源。
### 关键问题点
**问题一:Pod 流转机制导致重复评估**
- Pod 结束运行时,所有不可调度的 Pending Pod 都要重新评估(资源可能被释放)。
- 大规模集群中 Pod 频繁创建销毁 → Pending Pod 被频繁重新评估 → 计算开销累积。
**问题二:抢占机制的性能开销**
- 高优先级 Pod 无法调度时,会尝试驱逐低优先级 Pod 腾资源(详见上文「抢占机制详解」)。
- 每个 Pending Pod 调度时都会**遍历所有节点的所有 Pod** 比较优先级。
- 本案例:`2000 节点 × 数万 Pod = 极大计算开销`。
**问题三:SchedulingHint 功能未启用**
| 版本 | SchedulingHint 状态 |
| ------- | ------------------- |
| 1.28 | Alpha,默认关闭 |
| 1.29+ | Beta,默认开启 |
- SchedulingHint 可智能判断哪些 Pending Pod 需要重新评估,避免无谓的重复评估。
- 客户集群为 **1.28**,该功能默认未启用 → 每次 Pod 删除,所有 Pending Pod 全部重新评估。
### 问题链路
```
大规模集群(2000节点/数万Pod)
↓
Pod 频繁创建销毁
↓
每次 Pod 结束 → 所有 Pending Pod 重新评估(无 SchedulingHint 优化)
↓
每个 Pending Pod 评估时遍历所有节点的所有 Pod(抢占机制)
↓
调度器 CPU/内存压力剧增
↓
调度队列堆积,新 Pod 无法及时处理
↓
Pod 持续 Pending,无 Event 产生
```
## 解决方案
### 紧急缓解
清理冗余 Pod,调度压力明显缓解。
### 参数优化
**1. `podMaxBackoffSeconds`:默认 10 → 推荐 60**
增大 backoff 时间,让更多 Pod 被标记为不可调度,减少 Pending Pod 被频繁重新评估的次数;额外收益是 CA(Cluster Autoscaler)能更快感知不可调度 Pod 从而更快弹节点。
**2. `percentageOfNodesToScore`:默认动态计算 → 推荐 10**
- 默认公式:`percentageOfNodesToScore = 50 - (nodes / 125)`,最小 5%。
- 只评估足够多的可行节点即可打分,无需评估全部节点。
- 2000 节点集群设为 10% ≈ 只评估约 200 个节点,显著降低单次调度计算量。
**3. 抢占配置调整**
| 方案 | 配置方式 | 影响 |
| -------------- | ---------------------------- | -------------------------- |
| 关闭抢占 | `disablePreemption: true` | 完全跳过抢占评估,显著降开销 |
| 限制抢占范围 | 调整 PriorityClass 配置 | 减少参与抢占的 Pod 数量 |
### 配置示例
```yaml
apiVersion: kubescheduler.config.k8s.io/v1
kind: KubeSchedulerConfiguration
podMaxBackoffSeconds: 60
percentageOfNodesToScore: 10
profiles:
- schedulerName: default-scheduler
pluginConfig:
# 其他配置...
```
## 关键要点
### 大规模集群调度器配置建议
| 集群规模 | podMaxBackoffSeconds | percentageOfNodesToScore |
| ------------- | -------------------- | ------------------------ |
| < 500 节点 | 10(默认) | 默认 |
| 500-1000 节点 | 30 | 20 |
| 1000-2000 节点| 60 | 10 |
| > 2000 节点 | 60 | 5-10 |
### 监控指标
| 指标 | 说明 | 告警阈值建议 |
| ------------------------------------- | ------------------ | -------------- |
| `scheduler_pending_pods` | 等待调度的 Pod 数 | 按集群规模设定 |
| `scheduler_scheduling_duration_seconds`| 调度延迟 | P99 > 1s 告警 |
| `scheduler_queue_incoming_pods_total` | 入队 Pod 速率 | 突增时告警 |
| `scheduler_preemption_attempts_total` | 抢占尝试次数 | 异常增高时告警 |
### 版本升级
如条件允许,升级至 **Kubernetes 1.29+**,默认开启 SchedulingHint,从根源上优化大规模集群调度性能。
@@ -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 退出前抓**,退出后进程消失,现场不可回溯。
+304
View File
@@ -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` 调大、并在测试环境验证。
+305
View File
@@ -0,0 +1,305 @@
---
title: netstat 统计 TCP/UDP 连接状态
date: 2026-09-14
tags:
- linux
categories:
- linux
---
## 1. 经典命令解析
```bash
netstat -n | awk '/^tcp/ {++S[$NF]} END {for(a in S) print a,S[a]}'
```
**作用:统计当前所有 TCP 连接按状态分组的数目**(不是只查 TIME_WAIT,TIME_WAIT 只是输出中可能出现的一种状态)。
实际输出示例:
```
ESTABLISHED 4
TIME_WAIT 2
```
### 逐段解析
| 部分 | 作用 |
|---|---|
| `netstat -n` | 列出所有连接,`-n` 用数字 IP/端口,不做 DNS 反解(更快) |
| `/^tcp/` | 只匹配以 `tcp` 开头的行(即 TCP 连接) |
| `++S[$NF]` | 用每行**最后一列**(连接状态 State)作为 key 计数 |
| `END {for(a in S) print a,S[a]}` | 遍历数组,打印每种状态及数量 |
## 2. 常见 TCP 状态含义
| 状态 | 含义 | 异常信号 |
|---|---|---|
| `ESTABLISHED` | 已建立的正常连接 | — |
| `TIME_WAIT` | 主动关闭方等待 2MSL | 大量出现(成千上万)说明短连接过多,考虑 keep-alive / 连接池 |
| `CLOSE_WAIT` | 对方已关闭、本端还没 close | 大量堆积通常是**程序 bug**(socket 没正确关闭) |
| `LISTEN` | 监听中的服务端口 | — |
| `SYN_SENT` / `SYN_RECV` | 握手进行中 | 异常增多可能是网络问题或 SYN 攻击 |
## 3. 只看 TIME_WAIT 的写法
```bash
# netstat 精确过滤
netstat -n | awk '/^tcp/ && $NF=="TIME_WAIT" {n++} END {print n+0}'
# 或用 ss(推荐,速度快很多)
ss -ant state time-wait | wc -l
# ss 自带汇总
ss -s
```
## 4. 深入:Recv-Q / Send-Q 队列详解
`ss -ant` / `netstat -ant` 输出的第 2、3 列 Recv-Q 和 Send-Q 是排查「连接被拒绝」「请求超时」「应用处理慢」类问题的第一手指标。
### 4.1 Recv-Q 到底是什么
Recv-Q 的含义取决于 socket 所处的状态,两种场景完全不同:
| socket 状态 | Recv-Q 含义 | Send-Q 含义 |
|---|---|---|
| **LISTEN**(监听中) | 已完成三次握手、**等待应用 accept()** 的连接数(全连接队列当前长度) | 全连接队列的最大容量(backlog) |
| **ESTABLISHED**(已建立) | 内核已收到、但应用**还没 read()** 的数据字节数 | 已发送但尚未被对端 ACK 的字节数 |
> ⚠️ 老内核(2.x 及更早的一些版本)中 LISTEN 行的 Send-Q 列显示为 0,新内核(3.10+ 配合新版 iproute2)显示的是 backlog 上限。现代发行版直接看 `ss -lnt` 即可。
### 4.2 查看方式一:ss 命令(推荐)
`ss`(socket statistics)来自 iproute2 包,直接读内核 netlink,比 netstat 快几个数量级,是现在的标准工具。
```bash
# 查看所有 TCP 连接的 Recv-Q / Send-Q
ss -ant
# 只看监听端口(Recv-Q = 等待 accept 的连接数,Send-Q = backlog 上限)
ss -lnt
# 只看已建立的连接
ss -ant state established
# 按状态过滤 + 带进程信息(哪个进程积压了数据)
ss -antp state established
# 只过滤某个端口
ss -antp 'sport = :80'
```
实际输出示例:
```
State Recv-Q Send-Q Local Address:Port Peer Address:Port
LISTEN 0 4096 127.0.0.54:53 0.0.0.0:*
LISTEN 0 2048 127.0.0.1:8088 0.0.0.0:*
LISTEN 0 511 0.0.0.0:80 0.0.0.0:*
LISTEN 0 4096 0.0.0.0:22 0.0.0.0:*
ESTAB 1024 0 10.0.0.1:80 10.0.0.2:52314
```
解读:
- `:80` 是 LISTEN 行:Send-Q `511` 是 backlog 上限(nginx 默认 511),Recv-Q `0` 说明 accept 队列没有积压。
- 最后一行 ESTAB:Recv-Q `1024` 表示有 1024 字节数据躺在内核缓冲区里,应用还没读。
进阶参数:
```bash
# -m 显示 socket 内存使用(rb=接收缓冲区上限,r=实际占用)
ss -lntm
# -i 显示 TCP 内部信息(拥塞窗口、RTT、重传等)
ss -anti state established
# -o 显示定时器信息(keepalive/retrans)
ss -anto
```
`ss -ltnm` 输出示例:
```
LISTEN 0 4096 127.0.0.54:53 0.0.0.0:*
skmem:(r0,rb131072,t0,tb16384,f0,w0,o0,bl0,d0)
```
其中 `r0` = 接收队列当前字节数,`rb131072` = 接收缓冲区上限 128KB。
### 4.3 查看方式二:netstat(老命令)
```bash
netstat -ant # 所有 TCP
netstat -lnt # 只监听
netstat -antp # 带进程(需要 root)
```
输出列含义与 ss 一致。注意:netstat 逐条遍历 `/proc/net/tcp` 并做解析,大连接数机器上非常慢,线上优先用 `ss`。
### 4.4 查看方式三:直接读 /proc/net 目录
内核把 socket 信息暴露在 procfs 下,不依赖额外工具,适合容器/精简环境或写脚本:
```bash
cat /proc/net/tcp # TCP IPv4
cat /proc/net/tcp6 # TCP IPv6
cat /proc/net/udp # UDP
cat /proc/net/unix # Unix socket
```
字段格式:
```
sl local_address rem_address st tx_queue:rx_queue tr tm->when retrnsmt uid timeout inode
0: 3600007F:0035 00000000:0000 0A 00000000:00000000 00:00000000 00000000 992 0 731603 1 ...
```
关键列:
| 列 | 含义 |
|---|---|
| `local_address` | 本地地址,格式 `十六进制IP:十六进制端口`,IP 是**小端序** |
| `rem_address` | 对端地址,同上 |
| `st` | socket 状态,`0A` = LISTEN,`01` = ESTABLISHED |
| `tx_queue:rx_queue` | **发送队列:接收队列,均为十六进制字节数** —— 这里的 `rx_queue` 就是 Recv-Q |
手工解码示例:`3600007F:0035`
- IP `3600007F` 小端 → 字节序 `7F 00 00 36` → `127.0.0.54`
- 端口 `0035` → 十进制 53
用 awk 快速筛出 rx_queue 非 0 的连接:
```bash
awk 'NR>1 {split($5,q,":"); if (strtonum("0x" q[2]) > 0) print $2, $3, $4, strtonum("0x" q[2])}' /proc/net/tcp
```
> 由 inode 反查进程:`ls -l /proc/<pid>/fd` 中 `socket:[731603]` 的编号对应 inode 列。
### 4.5 典型排查场景
**场景一:LISTEN 的 Recv-Q 持续打满 Send-Q(backlog)**
现象:`ss -lnt` 看到 `Recv-Q ≈ Send-Q`,客户端报 connection refused / timeout。
原因:应用 accept() 速度跟不上握手完成速度,全连接队列溢出,内核开始丢新连接。
确认丢包计数:
```bash
nstat -az | grep -iE 'listenoverflow|listendrop'
# ListenOverflows ← 半连接/全连接队列溢出次数
# ListenDrops ← 因队列满被丢弃的连接数
```
处理方向:
1. 调大应用 backlog(如 nginx `listen 80 backlog=8192;`、Java 的 `backlog` 参数);
2. 调大内核上限:`net.core.somaxconn`(默认常见 128/4096)、`net.ipv4.tcp_max_syn_backlog`(半连接队列);
3. 治本:提升应用 accept 能力(多 worker、SO_REUSEPORT)。
```bash
sysctl -w net.core.somaxconn=4096
sysctl -w net.ipv4.tcp_max_syn_backlog=4096
```
**场景二:ESTABLISHED 的 Recv-Q 持续增长**
现象:某些 ESTAB 连接的 Recv-Q 越来越大且不回落。
含义:对端一直在发,本端应用读得太慢或不读(业务卡死、GC、线程池打满)。
定位:
```bash
# 找到是哪个进程
ss -antp state established | awk 'NR==1 || $2 > 0'
# 持续观察,5 秒采样一次
watch -n5 "ss -antp | awk 'NR==1 || \$2 > 0'"
```
处理方向:查该进程的 CPU/线程/GC 状态,确认消费逻辑是否阻塞。
**场景三:UDP 的 Recv-Q**
UDP 没有连接,`ss -anu` / `cat /proc/net/udp` 里的 rx_queue 表示**已收到但未被应用读取的报文字节数**。UDP 接收缓冲区满后报文被**静默丢弃**,可配合计数器确认:
```bash
nstat -az | grep -iE 'udprcvbuferrors|udpinerrors'
```
### 4.6 持续监控写法
```bash
# 每 2 秒刷新所有队列非空的连接
watch -n2 'ss -ant | awk "NR==1 || \$2>0 || \$3>0"'
# 只盯 80 端口 accept 队列
watch -n1 "ss -lnt 'sport = :80'"
```
采集进监控体系的话,Prometheus node_exporter 的 `node_netstat` / 自定义 textfile 脚本解析 `ss -ant` 输出即可。
### 4.7 速查表
| 需求 | 命令 |
|---|---|
| 看所有连接队列 | `ss -ant` |
| 看 accept 队列积压 | `ss -lnt`(Recv-Q vs Send-Q) |
| 找积压数据的进程 | `ss -antp` |
| 看 socket 内存占用 | `ss -lntm` / `ss -tm` |
| 裸文件方式 | `cat /proc/net/tcp`(第 5 列 `tx_queue:rx_queue`,hex) |
| accept 队列丢包计数 | `nstat -az \| grep -i listen` |
| 调大 accept 队列 | `net.core.somaxconn` + 应用 backlog 参数 |
| 持续观察 | `watch -n2 'ss -ant'` |
## 5. UDP 为什么不能照搬这个命令
关键区别:**UDP 是无连接的,没有 TIME_WAIT 这类状态**。
- `netstat -anu` 输出中 UDP 行的 **State 列是空的**
- 若还按 `$NF` 分组,统计到的是倒数第一列 `0.0.0.0:*`(Foreign Address),毫无意义
- 另外 `netstat -n` 不加 `-a` 时,大部分 UDP socket(未 connect 的)根本不显示
UDP 行长这样(State 列为空):
```
udp 0 0 127.0.0.54:53 0.0.0.0:*
```
## 6. UDP 的正确排查姿势
```bash
# 1. 看所有 UDP socket(必须加 -a,否则大部分不显示)
netstat -anu
# 2. 按本地端口统计,看哪些服务开着 UDP
netstat -anu | awk '/^udp/ {split($4,a,":"); ++S[a[length(a)]]} END {for(p in S) print p, S[p]}'
# 3. 用 ss 看状态计数(UDP 只有 UNCONN / ESTAB 两种)
ss -uan | awk 'NR>1 {++S[$1]} END {for(a in S) print a, S[a]}'
# 4. UDP 整体健康度:丢包/错误统计(UDP 排查最该看的)
netstat -su
```
`ss` 里 UDP 的两种状态:
- `UNCONN`:未调用 `connect()` 的 socket,约等于"监听/空闲"
- `ESTAB`:调用过 `connect()` 的 UDP socket
## 7. TCP vs UDP 排查对照表
| TCP 里看什么 | UDP 里对应看什么 |
|---|---|
| TIME_WAIT 堆积 | 没有这个概念 |
| CLOSE_WAIT 泄漏 | socket 数量是否只增不减(句柄泄漏) |
| 连接异常 | `netstat -su` 里的 `packet receive errors`、`RcvbufErrors`、丢包计数 |
| 连接数 | Recv-Q 列(netstat 第 2 列)是否长期非 0,说明应用消费不过来、缓冲区积压 |
## 8. 一句话总结
> TCP 按状态统计用 `$NF` 没问题;搬到 UDP 上时,把思路换成 **按端口统计 + 看 Recv-Q 积压 + `netstat -su` 看丢包**。Recv-Q 积压的通用规律:**LISTEN 满 = accept 不过来,ESTAB 涨 = read 不过来**。
@@ -0,0 +1,121 @@
---
title: 断电后文件内容变为 0 填充的分析(iomap 读路径填零机制)
date: 2026-09-16
tags:
- linux
categories:
- linux
---
## 0. 总结(TL;DR)
断电后"文件变 0 填充" = **内存脏页丢失 + 日志恢复把未提交范围回滚为 hole/unwritten + iomap 读路径按 POSIX 语义对该范围填 0(不发磁盘 IO)**。属预期内的数据丢失行为,不是篡改、也不是磁盘被写 0。
- 填 0 区间 = 故障前已 write 未 fsync 的窗口,按页/块(4K)对齐;
- 早已 fsync 的旧数据完好,其他文件完好;
- "读"只填内存 page cache,**盘上块不会被覆写**;unwritten 情形下盘上甚至可能残留真实数据,但读路径"依法"返回 0;
- 根治手段:关键数据写后 `fsync()/fdatasync()`(或 O_SYNC/O_DSYNC),硬件层配 BBU/UPS。
(以下为完整版分析)
---
## 1. 问题现象
- 物理机故障(断电/异常掉电重启)后,部分保存的文件内容整体或某区间变为全 0,疑似"被篡改"。
- 典型特征:
- 填 0 区间与故障前**最近一次写入窗口**吻合(新写、未 fsync 的数据);
- 同一文件中早已 fsync 落盘的旧数据完好,其他文件也完好;
- 文件大小、mtime 看起来正常,给人"文件还在、内容被替换"的错觉;
- 0 区间边界通常按页/块(4K)对齐,干净整齐,无 IO 错误、坏道伴随。
## 2. 结论(本质)
**不是篡改,也不是磁盘被写 0**,而是两个机制叠加的预期行为:
1. **页缓存易失丢失**:数据写入后只存在于 page cache(dirty page),writeback 未完成即断电,内存数据丢失;
2. **iomap 读路径主动填零**:重启后日志(journal)恢复,未提交落盘的范围在磁盘元数据中回滚为 HOLE 或保持 UNWRITTEN;读取时内核按 POSIX 语义(读 hole/unwritten 必须返回 0)**在内存中直接填 0 返回,根本不发起磁盘 IO**。
即:page cache 内有数据、落盘过程中断电 → 读回时看到 0 填充。
## 3. 代码依据(kernel iomap,fs/iomap/buffered-io.c + include/linux/iomap.h)
> 版本说明:文中 5.x 内核的函数名基于 struct page(`iomap_readpage_iter` / `zero_user`);6.x 内核已完成 folio 化,对应 `iomap_read_folio_iter` / `folio_zero_range`,**判定与填零逻辑完全一致**,下文以 5.x 名称叙述。
### 3.1 映射类型(include/linux/iomap.h)
```c
/*
* Types of block ranges for iomap mappings:
*/
#define IOMAP_HOLE 0x01 /* no blocks allocated, need allocation */
#define IOMAP_DELALLOC 0x02 /* delayed allocation blocks */
#define IOMAP_MAPPED 0x03 /* blocks allocated at @addr */
#define IOMAP_UNWRITTEN 0x04 /* blocks allocated at @addr in unwritten state */
#define IOMAP_INLINE 0x05 /* data inline in the inode */
```
注:取值是 **0x01–0x05,0 被刻意保留不用**,避免零初始化的内存被误判为合法映射类型。
### 3.2 读路径关键分支(iomap_readpage_iter)
```c
if (iomap_block_needs_zeroing(iter, pos)) {
zero_user(page, poff, plen); /* 页内填 0 */
iomap_set_range_uptodate(page, poff, plen); /* 标记 uptodate */
goto done; /* 不发 bio,不读盘 */
}
```
### 3.3 填零判定条件(iomap_block_needs_zeroing)
```c
return srcmap->type != IOMAP_MAPPED || /* HOLE / DELALLOC / UNWRITTEN 均命中 */
(srcmap->flags & IOMAP_F_NEW) || /* 新分配块,无有效数据 */
pos >= i_size_read(iter->inode); /* 超出 EOF 的部分 */
```
**解读**:只要某范围的映射不是"已映射且含已提交有效数据",读路径就不碰磁盘,直接在 page cache 填 0 并标记 uptodate 返回给应用。填零只发生在内存层,**磁盘上的块不会被这次读覆盖**(raw 块里甚至可能残留旧数据,但 VFS 不再读它)。
补充两点严谨性说明:
- `IOMAP_INLINE`(数据内联在 inode 中)在进入本判定之前已由 `iomap_read_inline_data` 单独处理,不会走到填零分支;
- **适用范围**:iomap 缓冲读路径主要用于 XFS / gfs2 / zonefs / erofs 等文件系统;ext4 的缓冲读走 mpage 而非 iomap(仅 DIO 用 iomap),但 ext4 对 unwritten extent 的读同样直接返回 0,现象与机理等价。下文时间线以 XFS 为例。
## 4. 故障完整时间线
1. 应用 `write()`:数据进入 page cache 并标脏;文件系统采用延迟分配(DELALLOC)或预分配的 unwritten extent(UNWRITTEN),**盘上此时无有效数据或数据状态为"未写"**。
2. writeback 尚未把这些脏页写盘,或 bio 已下发但未完成/未提交(含 unwritten→written 的元数据转换未落日志)。
3. **断电**:page cache 脏页随掉电丢失;无掉电保护的磁盘/RAID 卡易失缓存同样丢失。
4. 重启后文件系统日志恢复:只恢复已提交的元数据;未提交的分配回滚为 HOLE,或保持 UNWRITTEN;而 i_size 可能已随更早的提交持久化 → 出现"文件有长度、无内容"。
5. 应用读取:iomap 查到该范围为 HOLE/UNWRITTEN(或 F_NEW)→ `iomap_block_needs_zeroing` 为真 → `zero_user` 填 0 → 应用读到全 0。
### 两种落盘子情形(都表现为 0)
- **情形 A(delalloc)**:块从未分配 → 恢复后为 HOLE → 读填 0;
- **情形 B(unwritten)**:数据 bio 甚至已写到盘,但 unwritten→written 的状态转换未提交日志 → 恢复后仍为 UNWRITTEN → 读填 0。**此时盘上块里可能躺着真实数据,但读路径依法返回 0**——这是"落盘过程中断电也填 0"的最直接解释。
## 5. 为什么看起来像"被篡改"
- 0 是整段瞬间出现、4K 对齐,与人为覆写或介质故障的形态不同;
- 文件长度/mtime 正常,只有内容变 0;
- 实质是"数据从未(或未完成)提交到盘" + "内核对无数据范围的合法返回值是 0"。
## 6. 排查与认定要点
- 对照故障时间点与应用写入日志:填 0 区间 = 故障前已 write 未 fsync 的窗口;
- dmesg / 文件系统日志中有 unclean shutdown / log recovery 记录(如 XFS log recovery);
- 已 fsync 的旧数据完好 → 排除盘介质故障与人为篡改;
- 需要实证时可离线比对底层 raw 块:UNWRITTEN 情形下 raw 块可能残留旧数据,而文件读回为 0,恰好印证"读路径填零、盘未被覆写";
- 盘固件/介质故障通常伴随 IO error、坏道、随机花数据,而非整齐全 0。
## 7. 规避建议
- **应用层(根本手段)**:关键数据写完后 `fsync()/fdatasync()`(或 O_SYNC/O_DSYNC),确认落盘再返回成功;事务性数据用 WAL + 校验和,重启后自检修复;
- 认知对齐:`write()` 返回 ≠ 数据落盘;延迟分配窗口内数据只在内存;
- **硬件层**:RAID 卡/磁盘易失缓存配 BBU/掉电保护,或关闭回写缓存;UPS 避免直接掉电;
- 极端场景可评估 sync 挂载或应用层双写/校验,代价是性能。
## 8. 一句话总结
断电后"文件变 0 填充" = 内存脏页丢失 + 日志恢复把未提交范围回滚为 hole/unwritten + iomap 读路径按 POSIX 对该范围 `zero_user` 填 0(不发磁盘 IO);属预期数据丢失行为而非篡改,根治靠 fsync 等落盘确认手段。
@@ -0,0 +1,847 @@
---
title: CPU / 内存 / 磁盘 IO / 网络流量异常排查(网络篇含 nethogs 详解)
date: 2026-09-16
tags:
- linux
categories:
- linux
---
## 0. 总结(TL;DR)
- **四大异常排查的统一套路**:先看整体(利用率/饱和度/错误)→ 再定位到进程 → 再深挖到线程/文件/函数。即 Brendan Gregg 的 **USE 方法**(Utilization、Saturation、Errors)。
- 一句话记四个入口命令:
- CPU:`top` → `mpstat -P ALL` → `pidstat -u 1` → `perf top`
- 内存:`free -h`(看 **available** 不是 free)→ `top` 按 M → `dmesg` 查 OOM
- 磁盘 IO:`iostat -xz 1`(看 **await / aqu-sz / %util**)→ `iotop -o` / `pidstat -d`
- 网络:`sar -n DEV 1` 定量 → **`nethogs` 定人(按进程)** → `iftop` 定对象(按对端)→ `ip -s link` + `nstat` 查错误/重传
- **nethogs** 是网络排查链路中「按进程看带宽」的关键工具:回答「**哪个进程把带宽吃了**」,默认按 KB/s 排序,`-t` trace 模式可脚本化采集,详见 5.2 节。
- 关键认知:**load 高 ≠ CPU 忙**(含 D 状态等 IO 的线程);**free 低 ≠ 内存不够**(看 available);**%util 高 ≠ 磁盘一定慢**(看 await);**带宽打满前先确认网卡速率上限**。
(以下为完整版)
---
## 1. 通用方法论:先看整体,再定位进程(USE 方法)
任何资源异常都按三步走,不要上来就 `ps aux | grep` 蒙:
1. **整体饱和度**:这个资源总共还够不够用?(USE:Utilization 利用率、Saturation 饱和度、Errors 错误)
2. **定位到进程**:谁在消耗?
3. **定位到根因**:进程内部的线程 / 文件 / 系统调用 / 代码在干什么?
对照总表(后文每章展开):
| 资源 | 第一步:整体 | 第二步:找进程 | 第三步:深挖 |
|---|---|---|---|
| CPU | `uptime`、`top`、`mpstat -P ALL 1` | `pidstat -u 1`、`top` | `top -H -p PID`、`perf top -p PID`、`strace -c -p PID` |
| 内存 | `free -h`、`cat /proc/meminfo` | `top`(按 M)、`pidstat -r 1` | `pmap -x PID`、`slabtop`、`dmesg`(OOM) |
| 磁盘 IO | `iostat -xz 1` | `iotop -o`、`pidstat -d 1` | `lsof -p PID`、`/proc/PID/io`、fatrace |
| 网络 | `sar -n DEV 1`、`ip -s link` | **`nethogs`**、`iftop`、`ss -tinp` | `nstat -az`、`ethtool -S`、tcpdump |
判断异常的辅助维度:
- **历史基线**:`sar`(sysstat 默认保留近 30 天 `/var/log/sa/saDD`)能回答「昨天这个时候是不是也这么高」;
- **压力指标 PSI**(内核 4.20+):`/proc/pressure/{cpu,memory,io}`,回答「任务因为等资源被卡了多久」,比利用率更接近「是不是真的不够用」:
```
$ cat /proc/pressure/io
some avg10=0.00 avg60=0.23 avg300=0.24 total=2677023340
full avg10=0.00 avg60=0.22 avg300=0.23 total=2288460457
```
`avg10/60/300` 是 10 秒 / 1 分钟 / 5 分钟内**任务因等该资源而停滞的时间占比(%)**。`some` = 至少部分任务被卡,`full` = 所有非 idle 任务全被卡。any 值持续 > 20% 基本可确认该资源是瓶颈。
---
## 2. CPU 异常排查
### 2.1 第一步:看 load 和整体利用率
```bash
uptime
# 17:11:42 up 7 days, 3:30, 1 user, load average: 0.20, 0.16, 0.14
```
`load average` 三个值 = 1/5/15 分钟内**平均可运行(R)+ 不可中断睡眠(D)状态的线程数**。判断标准:
- load < CPU 核数:正常;
- load 持续 > 核数:队列在积压(但**不等于 CPU 忙**,见 2.3);
- 1 分钟值 ≫ 15 分钟值:负载在**上升**;反过来是在**回落**。
```bash
nproc # 核数,load 的参照系
top -bn1 | head -5 # 非交互打一份快照
```
`top` 头部关键行:
```
%Cpu(s): 4.5 us, 4.5 sy, 0.0 ni, 86.4 id, 4.5 wa, 0.0 hi, 0.0 si, 0.0 st
```
逐列含义与异常指向:
| 列 | 含义 | 高了说明什么 / 下一步 |
|---|---|---|
| `us` | 用户态 CPU | 应用本身在算:业务逻辑、死循环、频繁 GC。→ `pidstat -u` + `perf top` |
| `sy` | 内核态 CPU | 系统调用/内核路径重:频繁 IO、锁竞争、内存分配。→ `strace -c`、`perf top` 看内核函数 |
| `ni` | nice 调整过的用户态 | 同 us,少见 |
| `id` | 空闲 | 越高越好 |
| `wa` | **等 IO 的 CPU** | 磁盘/存储慢,不是 CPU 不够!→ 转第 4 章 `iostat` |
| `hi` | 硬中断 | 网卡/磁盘中断风暴。→ `/proc/interrupts` |
| `si` | 软中断 | 高 PPS 网络收发、ksoftirqd 高。→ `/proc/softirqs`、`sar -n DEV` |
| `st` | 被宿主机偷走(仅虚拟机) | **云主机超卖 / 同宿主邻居吵闹**。st 持续 > 10% 直接找云厂商,本机查不出东西 |
**top 交互速记**:`1` 展开每核;`P` 按 CPU 排;`M` 按内存排;`c` 显示完整命令行;`H` 切到线程视图;`k` kill;`W` 保存配置。
### 2.2 确认是单核打满还是整体打满
```bash
mpstat -P ALL 1 1
```
```
05:11:45 PM CPU %usr %nice %sys %iowait %irq %soft %steal %guest %gnice %idle
05:11:45 PM all 2.00 0.00 0.50 0.00 0.00 0.50 0.00 0.00 0.00 97.00
05:11:45 PM 0 0.99 0.00 0.99 0.00 0.00 0.99 0.00 0.00 0.00 97.03
05:11:45 PM 1 3.03 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 96.97
```
典型异常形态:
- **某一颗核 100%、其余空闲**:单线程瓶颈(如单 worker 的 Redis、没开多线程的压缩任务)。优化方向是多开实例 / 多线程,加核没用。
- **所有核 us 高**:应用计算量真上来了,扩容或优化代码。
- **sy 占比高(> 20~30%)**:系统调用太频繁或内核锁竞争,继续往下查。
- **si 集中在某一核**:网卡软中断没有分散,检查 RPS/IRQ affinity(见 5.6)。
### 2.3 load 高但 CPU 不忙:查 D 状态
load 统计 R+D。如果 `load 很高、%Cpu id 也很高`,多半是一堆线程卡在 **D(不可中断睡眠,通常在等 IO)**:
```bash
# vmstat 的 r 列 = 运行队列长度(R),b 列 = D 状态数
vmstat 1 5
```
```
procs -----------memory---------- ---swap-- -----io---- -system-- -------cpu-------
r b swpd free buff cache si so bi bo in cs us sy id wa st gu
2 0 0 73868 30836 679976 0 0 503 14 1539 8 1 0 99 0 0 0
```
- `r` 持续 > 核数:CPU 真不够 / 调度排队;
- `b` 持续 > 0:有线程卡 IO,去第 4 章。
找出具体哪些进程在 D:
```bash
ps -eo pid,ppid,stat,wchan:30,comm | awk '$3 ~ /^D/'
# 或直接看卡在哪
for p in $(ps -eo pid,stat | awk '$2~/^D/{print $1}'); do
echo "== $p $(cat /proc/$p/comm)"; cat /proc/$p/stack 2>/dev/null | head -5
done
```
`/proc/<pid>/stack` 里的内核栈能直接看到卡在 `io_schedule` / `nfs_wait` / 哪个文件系统路径上。
### 2.4 第二步:定位到进程
```bash
# 每 1 秒输出所有进程 CPU 占用(含 %wait 等待调度的时间)
pidstat -u 1
```
```
05:11:59 PM UID PID %usr %system %guest %wait %CPU CPU Command
05:11:59 PM 1001 57651 0.99 0.00 0.00 0.00 0.99 0 gitea
05:11:59 PM 0 141362 0.99 0.00 0.00 0.00 0.99 1 AliYunDun
```
注意 `%CPU` 是**单核百分比**:800% = 用满 8 个核。`%wait` 高说明进程想跑但排不上队(CPU 真饱和)。
老命令同样好用:
```bash
ps -eo pid,ppid,pcpu,pmem,comm --sort=-pcpu | head -15
ps aux --sort=-%cpu | head
```
### 2.5 第三步:定位到线程和函数
**定位到线程**(Java/Go 多线程服务必做):
```bash
top -H -p <PID> # 线程视图,找到最热的 TID
pidstat -t -p <PID> 1 # 命令行等价
```
拿到 TID 后,Java 栈对照:`printf '%x\n' <TID>` 转十六进制,`jstack <PID>` 里搜 `nid=0x<hex>`。
**定位到函数**(为什么这个进程烧 CPU):
```bash
perf top -p <PID> # 实时看热点函数(用户态+内核态)
perf top -g -p <PID> # 带调用链(需要 frame pointer 或 dwarf)
```
没有 perf 或权限不足时,退而用 `strace -c -p <PID>` 统计系统调用分布——sy 高的进程一眼可见是 futex 风暴、epoll 空转还是读写频繁。
### 2.6 上下文切换与中断异常
CPU 不高但「卡」,有时是调度/中断层面的问题:
```bash
vmstat 1 # 看 cs(每秒上下文切换)和 in(每秒中断)
```
经验值:`cs` 和核数×1万 同数量级还算正常;突然暴涨一个数量级要查。
```bash
pidstat -w 1 # cswch/s 自愿切换(主动等锁/等IO),nvcswch/s 非自愿(被抢占)
cat /proc/interrupts # 硬中断按核分布,看是否单核倾斜
cat /proc/softirqs # 软中断分类(NET_RX/NET_TX/TIMER/RCU...)
```
非自愿切换 `nvcswch/s` 爆高 + `r` 队列长 = 线程数远超核数在互抢(典型:线程池配太大)。
### 2.7 云主机专属:steal 时间
```bash
top # %Cpu 行看 st
# 或者
sar -u 1 5 | grep -v Average # %steal 列
```
`st`(steal)是虚拟化管理程序把本属于这台 VM 的 CPU 分给了别的 VM。**本机一切指标都正常、st 却 10%+**,就是宿主机超卖/邻居吵闹,工单找云厂商,别再折腾本机。
### 2.8 CPU 排查速查表
| 现象 | 首选命令 | 大概率原因 |
|---|---|---|
| load 高 + id 低 + us 高 | `pidstat -u 1` | 应用算力不足 → `perf top` 找热点 |
| load 高 + id 低 + sy 高 | `strace -c -p PID` | 系统调用风暴、锁、IO 路径 |
| load 高 + id 高 | `ps -eo stat \| grep -c ^D`、`vmstat` 看 b | D 状态堆叠,查磁盘/NFS |
| load 高 + wa 高 | `iostat -xz 1` | 存储慢,不是 CPU 问题 |
| 单核 100% | `mpstat -P ALL 1` | 单线程瓶颈 |
| si 高、单核软中断倾斜 | `cat /proc/softirqs` | 网卡中断未分散(RPS) |
| 什么都正常就是慢(VM) | `sar -u` 看 %steal | 宿主机超卖 |
| cs 暴涨 | `pidstat -w 1` | 线程过多互抢 |
---
## 3. 内存异常排查
### 3.1 第一步:free 的正确读法
```bash
free -h
```
```
total used free shared buff/cache available
Mem: 1.6Gi 1.1Gi 72Mi 20Mi 694Mi 573Mi
Swap: 0B 0B 0B
```
**唯一正确的看法:看 `available`,不看 `free`。**
- `free`(72Mi)是「完全没被任何人用」的内存,Linux 会把空闲内存拿去做 page cache,所以这个值**常年很小是正常的**;
- `buff/cache`(694Mi)大部分是可回收的:进程要内存时内核立即回收给它;
- `available`(573Mi)= 内核估算的「现在启动一个新应用最多能拿到多少」,**这个才是剩余内存的真实答案**。
> 经验:available / total 持续 < 10% 才算内存紧张。free 低 + available 高 = 完全健康,不要清缓存。
### 3.2 /proc/meminfo 关键字段
```bash
grep -E 'MemTotal|MemFree|MemAvailable|Buffers|^Cached|SwapTotal|SwapFree|Dirty|Writeback|Slab|SReclaimable|SUnreclaim|Shmem' /proc/meminfo
```
```
MemTotal: 1712768 kB
MemFree: 84764 kB
MemAvailable: 602204 kB
Buffers: 31540 kB
Cached: 606828 kB
SwapTotal: 0 kB
SwapFree: 0 kB
Dirty: 64 kB
Writeback: 0 kB
Shmem: 20872 kB
Slab: 155864 kB
SReclaimable: 77084 kB
SUnreclaim: 78780 kB
```
| 字段 | 含义 | 异常信号 |
|---|---|---|
| `Cached` | page cache | 高是正常的;「内存莫名消失」先看这 |
| `Dirty` / `Writeback` | 待回写 / 回写中的脏页 | Dirty 长期几个 GB 不落盘 → 存储慢或 writeback 参数问题,断电风险大 |
| `Shmem` | 共享内存 + tmpfs | tmpfs(/dev/shm、/run)占用直接吃掉可用内存且不在任何进程 RSS 里体现全量 |
| `Slab` | 内核对象缓存 | 其中 `SUnreclaim`(不可回收)暴涨 → dentry/inode 泄漏(海量小文件遍历后常见) |
| `SwapTotal/Free` | swap | 见 3.5 |
### 3.3 第二步:谁在吃内存
```bash
top # 按 M 排序,看 RES 列
# 或
ps -eo pid,rss,pmem,comm --sort=-rss | head -15
# 或动态看
pidstat -r 1
```
```
05:11:59 PM UID PID minflt/s majflt/s VSZ RSS %MEM Command
05:11:59 PM 0 196594 12.87 0.00 10128796 173364 10.12 pi
```
列辨析(排查时极容易搞错):
- **VSZ/VIRT**(虚拟地址空间):包含映射但从未碰过的内存,**大不代表有问题**(Java 堆、Go runtime 动辄几十 G 很正常);
- **RSS/RES**(常驻物理内存):实际占的物理页,**看这个**;
- **minflt/s**(minor fault):页已在内存(如 cache),只是没映射,无害;
- **majflt/s**(major fault):要从磁盘读页(swap 换入 / mmap 文件缺页),**持续非 0 = 内存不足在抖**,性能杀手。
**RSS 的坑——共享库被重复计算**:多个进程共享的 libc 等页会算进每个进程的 RSS,直接加总 RSS 会远超物理内存。精确口径用 PSS(按比例分摊共享页):
```bash
# smem(需安装):按 PSS 排
smem -rtk | tail -15
```
**看单个进程的内存构成**:
```bash
pmap -x <PID> | tail -20
# [ anon ] 堆/栈等匿名页 —— 泄漏一般长在这
# 文件名开头的行是 mmap 的文件/共享库 —— 不算泄漏
```
判断「进程内存泄漏」的标准动作:**连续采 RSS 画曲线**:
```bash
while :; do ps -o rss= -p <PID> >> rss.log; sleep 60; done
# RSS 单调只涨不回落(重启才降)→ 泄漏;涨到平台期稳住 → 是 cache 型应用(如 JVM、Redis)的天性
```
### 3.4 那些「找不到凶手」的内存消失
RSS 加总对不上 used 的常见去向,按概率排:
1. **page cache / slab**:`free` 的 buff/cache 列,正常,可回收;
2. **tmpfs**:`df -h -t tmpfs`,/dev/shm、/run 里的大文件;
3. **SUnreclaim slab**:`slabtop -s c | head` 看 dentry/inode 是否 GB 级(海量小文件 + 高目录遍历导致),可 `echo 2 > /proc/sys/vm/drop_caches` 回收(先确认可回收部分确实大);
4. **HugePages**:`grep Huge /proc/meminfo`,给 Oracle/Redis THP 预留的不在 RSS 常规口径;
5. **内核/驱动**:`cat /proc/vmallocinfo`、`slabtop`,网卡大 ring buffer、GPU 驱动也会吃。
> 清缓存命令(仅用于验证「消失的内存在哪」,**不要当日常运维手段**):
> `sync; echo 3 > /proc/sys/vm/drop_caches`
### 3.5 swap 与 OOM
**swap**:本机 SwapTotal=0(很多云主机默认关 swap)。有 swap 的机器上:
- `free` 里 swap 被用了**不代表**异常(冷数据换出很正常);
- 要看的是 `vmstat 1` 的 **si / so(每秒换入/换出)**:持续非 0 才是「在抖」,说明活跃内存真不够;
- 伴随症状:`pidstat -r` 的 majflt/s 飙升、load 高、iowait 高。
**OOM Killer**——进程「莫名消失」必查:
```bash
dmesg -T | grep -iE 'out of memory|oom-kill|killed process'
journalctl -k | grep -i oom
# 典型输出:
# [Tue Sep 16 10:22:01 2026] Out of memory: Killed process 81234 (java) total-vm:... anon-rss:...
```
容器/cgroup 场景(dmesg 可能没有,看 cgroup 事件):
```bash
cat /sys/fs/cgroup/memory.events 2>/dev/null | grep oom # cgroup v2
cat /sys/fs/cgroup/memory/memory.oom_control 2>/dev/null # cgroup v1
```
谁最容易被 OOM 选中:`cat /proc/<PID>/oom_score`(分数越高越先死),可通过 `oom_score_adj`(-1000~1000)保护关键进程。
### 3.6 PSI memory:内存压力的终极判据
```bash
cat /proc/pressure/memory
# some avg10=0.00 avg60=0.00 avg300=0.00 total=887003792
# full avg10=0.00 avg60=0.00 avg300=0.00 total=825600394
```
`full` 持续 > 0 说明有**全系统**因内存回收而停滞(direct reclaim),即使 available 看着还行,也说明回收速度跟不上分配速度(典型:大页/连续内存分配、zone 不均)。
### 3.7 内存排查速查表
| 现象 | 首选命令 | 大概率原因 |
|---|---|---|
| free 很低 | `free -h` 看 available | 多半正常,cache 占用 |
| available < 10% | `top` 按 M / `smem -rtk` | 找出 RSS 大户 |
| RSS 总和 ≪ used | `df -h -t tmpfs`、`slabtop -s c` | tmpfs / slab 泄漏 |
| 进程 RSS 只涨不跌 | 连续采样 + `pmap -x` | 泄漏(anon 段增长) |
| 进程突然没了 | `dmesg -T \| grep -i oom` | OOM Killer |
| 卡、majflt 高、si/so 非 0 | `vmstat 1`、`pidstat -r 1` | swap 抖动,内存真不够 |
| 内存时有时无地卡 | `/proc/pressure/memory` | direct reclaim 停顿 |
---
## 4. 磁盘 IO 异常排查
### 4.1 第一步:iostat 看盘的整体状况
```bash
iostat -xz 1 # -x 扩展列 -z 隐藏空闲盘,1 秒一刷
```
(新版 sysstat 输出,读写分列):
```
Device r/s rkB/s rrqm/s %rrqm r_await rareq-sz w/s wkB/s wrqm/s %wrqm w_await wareq-sz ... aqu-sz %util
vda 22.00 1332.00 0.00 0.00 1.59 60.55 0.00 0.00 0.00 0.00 0.00 0.00 ... 0.04 1.20
```
老版只有合并的 `await`,新版拆成 `r_await` / `w_await`,更精确。**真正要盯的只有 4 组指标**:
| 列 | 含义 | 异常标准(经验值) |
|---|---|---|
| `r/s` `w/s` | 每秒读/写次数(IOPS) | 对照盘的规格:SATA SSD 几万、NVMe 几十万、机械盘 100~200。`r/s+w/s` 顶到规格上限 = IOPS 打满 |
| `rkB/s` `wkB/s` | 读/写吞吐 | 顶到接口带宽(SATA ≈ 550MB/s、千兆网盘 ≈ 110MB/s)= 带宽打满 |
| `r_await` `w_await` | 每个 IO 的**平均耗时(ms)**,含排队 | 机械盘 > 10ms、SSD > 1~2ms、NVMe > 0.5ms 且持续 → 盘慢或过载。这是「应用感觉卡」最直接的对应指标 |
| `aqu-sz` | 平均 IO 队列深度 | 持续 > 1(SSD)说明请求在排队;越大堵得越狠 |
| `%util` | 每秒中盘忙的时间占比 | 机械盘 ≈100% 即饱和;**SSD/NVMe 因内部并行,util 高不一定饱和,必须结合 await 看** |
典型判读组合:
| iostat 表现 | 结论 |
|---|---|
| await 高 + aqu-sz 高 + %util ≈ 100 | 盘真被打满了,找出是谁(4.2) |
| await 高 + %util 不高 | 盘的**自身/链路**问题(坏道、RAID 重建、云盘限速、网络盘抖动),和应用无关,查硬件/云监控 |
| rkB/s 顶满带宽、await 不高 | 顺序大流量(备份/日志归档),不一定是故障 |
| r/s 几万但 rkB/s 很小 | 随机小 IO 风暴(大量小文件/元数据操作),await 会很难看 |
### 4.2 第二步:哪个进程在读写
```bash
iotop -oP # -o 只显示正在做IO的进程,-P 只显示进程(不含线程)
# 或非交互打一份
iotop -bon3
```
```
Total DISK READ: 12.00 M/s | Total DISK WRITE: 3.50 M/s
TID PRIO USER DISK READ DISK WRITE SWAPIN IO COMMAND
81234 be/4 mysql 8.20 M/s 2.10 M/s 0.00 % 5.31 % mysqld
```
等价命令行工具:
```bash
pidstat -d 1 # kB_rd/s kB_wr/s kB_ccwr/s(ccwr=写了又被取消的,如覆盖写)
```
**注意 iotop 的一个大坑**:它的 Total 行统计的是「系统调用层」的读写,和 iostat 看到的「块设备层」经常对不上——page cache 回写(kworker)、文件系统日志(jbd2)、内存回收(kswapd)产生的磁盘 IO **不属于任何业务进程**。所以:
- iostat 高、iotop 业务进程都不高 → 看内核线程:
```bash
ps aux | grep -E 'jbd2|kworker|kswapd|kdmflush' | grep -v grep
# jbd2/sda1-8 高 = 文件系统 journal 压力大(大量小写入/ fsync 频繁)
# kswapd 高 + si/so 非 0 = 内存不够在回收,IO 是内存问题的次生灾害(回第 3 章)
```
### 4.3 第三步:进程在读写哪些文件
```bash
# 某进程当前打开且在用的大文件
lsof -p <PID> -nP | grep -E 'REG' | awk '$7 > 104857600' # >100MB 的
# 直接看进程 IO 计数器(累计值,取两次差值)
cat /proc/<PID>/io
# read_bytes: 实际从块设备读了多少
# write_bytes: 实际写到块设备多少
# cancelled_write_bytes: 写取消量
# 全盘文件级审计:谁在读写什么文件(需要 root,内核 audit)
fatrace -c 2>/dev/null | head -30 # 部分发行版需安装 fatrace
```
没有 fatrace 的极简替代:对嫌疑进程 `lsof -p` 反复采样,或用 `strace -e trace=read,write,pread64,pwrite64 -c -p <PID>` 统计它读写的 fd,再对照 `ls -l /proc/<PID>/fd` 把 fd 翻译成文件名。
### 4.4 文件系统层:空间、inode、「删了文件空间没回来」
IO 排查经常和**容量**问题混在一起,一并说:
```bash
df -h # 文件系统使用率
df -i # inode 使用率(海量小文件会先打爆 inode,df -h 却还有空间)
```
**经典悬案:`df` 说满了,`du -sh /*` 加总却对不上**,三个原因按概率排:
1. **文件已删但被进程持有句柄**(日志被 rm 但服务没重启),空间不释放:
```bash
lsof +L1 # 直接列出 link 数=0 的被删文件
# 或
find /proc/*/fd -lname '*deleted*' -ls 2>/dev/null
# 释放:重启进程,或 echo > /proc/<PID>/fd/<FD> 截断(比 rm 优雅)
```
2. **挂载点遮蔽**:目录挂载前有旧数据,被挂载的文件系统盖住了。卸载后(或 `mount --bind / /mnt` 后看 `/mnt/<目录>`)才能看到。
3. **du 没加 -x 跨了文件系统**、硬链接重复计算等口径问题。
### 4.5 根因定位后的常见处置方向
| 定位结果 | 处置 |
|---|---|
| 单进程日志/备份把盘打满 | ionice 降级:`ionice -c3 -p <PID>`(idle 类,空闲时才轮到它);rsync/cp 加 `--bwlimit` |
| jbd2 高(fsync 风暴) | 应用减少 fsync 频率、批量提交;极端可换 noatime/data=writeback(权衡可靠性) |
| kswapd 引起的 IO | 治本在内存(扩内存/降用量),不是盘的问题 |
| 盘本身 await 高、负载不高 | 查 SMART:`smartctl -a /dev/sda`;云盘查云监控是否限速/突发额度耗尽 |
| 随机小 IO 风暴 | 应用层合并写、增大缓冲、换 SSD/NVMe |
### 4.6 磁盘 IO 排查速查表
| 现象 | 首选命令 | 大概率原因 |
|---|---|---|
| 应用卡 + wa 高 | `iostat -xz 1` 看 await/aqu-sz | 盘过载或盘故障 |
| iostat 高、iotop 没进程 | `ps aux \| grep -E 'jbd2\|kswapd\|kworker'` | 回写/日志/回收的内生 IO |
| 明确要限速 | `ionice -c3 -p PID` | 备份/统计类任务让路 |
| df 满 du 不满 | `lsof +L1` | 删除未释放的句柄 |
| 写小文件极慢 | `iostat` 看 r/s 与 rareq-sz | 随机小 IO / 元数据瓶颈 |
| 建不了文件但空间够 | `df -i` | inode 耗尽 |
---
## 5. 网络流量异常排查
网络排查的四级定位链:**`sar` 定量(网卡总账)→ `nethogs` 定人(哪个进程)→ `iftop` 定对象(和谁传)→ `ss`/`tcpdump` 定细节(单连接/报文)**。本章按此顺序展开。
### 5.1 第一步:网卡总量到底满没满
```bash
sar -n DEV 1 # 每秒各网卡收发
```
```
05:11:58 PM IFACE rxpck/s txpck/s rxkB/s txkB/s rxcmp/s txcmp/s rxmcst/s %ifutil
05:11:58 PM eth0 25.00 24.00 1.55 39.96 0.00 0.00 0.00 0.00
```
关键列:`rxkB/s`、`txkB/s`(吞吐)、`rxpck/s`、`txpck/s`(PPS)、`%ifutil`(半双工估算的利用率,全双工网卡参考价值有限)。
**「满没满」要自己算**——先拿到网卡速率上限:
```bash
ethtool eth0 | grep -i speed # 物理网卡:Speed: 10000Mb/s
# 虚拟网卡(virtio 等)常显示 Unknown!,那就以云厂商给的实例带宽上限为准
```
换算:10Gb/s ≈ 1250 MB/s ≈ 1250000 kB/s。`txkB/s` 达到上限的 80% 且持续 = 带宽打满实锤。
> 没有 sar 的裸机替代:`cat /proc/net/dev`(累计字节数,取两次差值)、`ip -s link`、小工具 `ifstat -i eth0 1`、`nload eth0`。
裸 /proc 写法(容器里也能用):
```bash
rx1=$(awk '/eth0/{print $2}' /proc/net/dev); sleep 1
rx2=$(awk '/eth0/{print $2}' /proc/net/dev)
echo "RX: $(( (rx2-rx1)/1024 )) KB/s"
```
### 5.2 第二步:哪个进程在吃带宽 —— nethogs 详解
sar 只能告诉你**网卡**跑了多少流量,回答不了「**哪个 PID** 干的」。`nethogs` 就是干这个的:按进程分组统计实时带宽,是带宽告警后第一个该敲的命令。
#### 5.2.1 安装与前提
```bash
# Debian / Ubuntu
apt install -y nethogs
# RHEL / CentOS
yum install -y nethogs
```
> 需要 **root** 运行(抓包 + 遍历 /proc 匹配 socket inode)。普通用户跑大部分连接会显示 `unknown TCP`。
#### 5.2.2 基本用法与输出解读
```bash
nethogs # 监控所有 up 的非 loopback 网卡
nethogs eth0 # 只监控 eth0
nethogs eth0 eth1 # 监控多块
```
交互界面(默认 1 秒刷新,按速率降序):
```
NetHogs version 0.8.7
PID USER PROGRAM DEV SENT RECEIVED
196510 root sshd: root@pts/0 eth0 28.246 0.653 KB/sec
141362 root /usr/local/aegis/AliYunDun eth0 2.208 0.117 KB/sec
? root unknown TCP 0.000 0.000 KB/sec
TOTAL 30.454 0.770 KB/sec
```
列含义:
| 列 | 含义 |
|---|---|
| `PID` | 进程号;`?` 表示没能匹配到进程 |
| `USER` | 进程属主 |
| `PROGRAM` | 程序名(`-l` 显示完整命令行,`-b` 只显示 basename) |
| `DEV` | 流量走的网卡 |
| `SENT` / `RECEIVED` | 该进程**发送 / 接收**速率,默认 KB/s |
> `unknown TCP`:nethogs 通过 pcap 抓包拿到五元组,再去 `/proc/<pid>/fd` 反查 socket inode 归属进程。短连接(抓到时进程已退出)、内核线程发的包、权限不足时都会归入 unknown。
#### 5.2.3 命令行参数全解
```bash
usage: nethogs [-V] [-h] [-x] [-d seconds] [-v mode] [-c count] [-t] [-p] [-s]
[-a] [-l] [-f filter] [-C] [-b] [-P pid] [device [device ...]]
```
| 参数 | 作用 | 使用场景 |
|---|---|---|
| `-d 秒` | 刷新间隔,默认 1 | 压平毛刺用 `-d 5` |
| `-v mode` | 视图模式:`0`=KB/s(默认) `1`=累计KB `2`=累计B `3`=累计MB `4`=MB/s `5`=GB/s | 看「**谁累计传得最多**」用 `-v 3`;千兆以上环境用 `-v 4` |
| `-c 次数` | 刷新 N 次后自动退出,默认 0=无限 | 配合 `-t` 采集样本 |
| `-t` | tracemode:类似 `top -b` 的批处理输出 | **脚本化 / 落日志必用** |
| `-p` | 混杂模式抓包(不推荐) | 默认只抓本机地址的包,够了 |
| `-s` | 按 SENT 列排序 | 找上行大户(备份/外传) |
| `-a` | 监控所有设备,含 loopback 和 down 的 | 排查本机服务间互调流量 |
| `-l` | 显示完整命令行 | 区分同名进程 |
| `-b` | 只显示程序 basename | 输出更窄 |
| `-C` | TCP 和 UDP 都抓(老版本只抓 TCP) | 0.8.x 建议显式加上 |
| `-f 表达式` | pcap 过滤表达式(实验性),如 `-f "port 53"` | 只看某类流量 |
| `-P pid` | 只显示指定进程 | 盯单个进程 |
| `-g N` | 每 N 次刷新做一次进程 GC,默认 50 | 一般不用动 |
| `-x` | bughunt 调试模式 | 排查 nethogs 自身问题 |
#### 5.2.4 运行中交互按键
| 按键 | 作用 |
|---|---|
| `q` | 退出 |
| `m` | **切换显示单位**:KB/s → 累计 KB → 累计 B → 累计 MB → MB/s → GB/s 循环(等价于改 `-v`) |
| `s` | 按 SENT(发送)排序 |
| `r` | 按 RECEIVED(接收)排序 |
| `l` | 显示完整命令行 |
| `b` | 程序名显示为 basename |
#### 5.2.5 实战常用姿势
**姿势一:突发带宽告警,现场抓凶手**
```bash
nethogs -d 2 # 2 秒刷新,按 m 切到累计 MB,等 30 秒看谁涨得最快
```
**姿势二:无人值守采集(挂后台 / 接告警脚本)**
```bash
# 每 2 秒采样一次,采 30 轮,按时间戳落文件
nethogs -t -d 2 -c 30 eth0 >> /var/log/nethogs.$(date +%F).log
```
`-t` trace 模式输出形如:
```
Refreshing:
sshd: root@pts/0/196510/0 28.2457 0.652734
/usr/local/aegis/aegis_client/aegis_13_03/AliYunDun/141362/0 2.20781 0.117188
unknown TCP/0/0 0 0
```
格式为 `程序/PID/UID<TAB>SENT<TAB>RECEIVED`(单位 KB/s),非常规律,一行 awk 就能找出超速进程:
```bash
# 找出任意一轮采样中发送速率超过 10000 KB/s 的进程
nethogs -t -d 1 -c 60 eth0 | awk -F'\t' '$2 > 10000 && !/^Refreshing/ {print strftime("%T"), $0}'
```
**姿势三:只关心某个端口 / 某类业务**
```bash
nethogs -t -f "port 3306" eth0 # 谁在和 MySQL 交互
nethogs -t -f "udp" eth0 # 只看 UDP
```
**姿势四:找「累计流量王」而不是瞬时峰值**
定时任务备份、日志外发这类瞬时抓不到的,用累计模式:
```bash
nethogs -v 3 -d 10 # 累计 MB,10 秒刷新,挂一会儿按总量排序看
```
#### 5.2.6 原理与注意事项(避坑)
1. **工作原理**:libpcap 抓包得到 IP+端口 → 解析 `/proc/net/tcp(6)/udp(6)` 拿到 socket inode → 遍历 `/proc/<pid>/fd` 匹配 inode → 流量记到进程头上。所以:
- 必须 **root**(否则 /proc 下别人的 fd 读不到);
- **短命进程**(php-fpm 子进程、curl 一闪而过)容易漏记或归 unknown;
- 连接建立后进程退出、fd 被继承(fork 型服务),流量可能记在父进程或显示不全。
2. **统计口径是「网卡上看到的包」**,含协议头开销,和应用层日志里的字节数会有差异,数量级对得上即可。
3. 默认**不监控 loopback**,本机服务互调(nginx → 127.0.0.1:9000)看不到,要加 `-a`。
4. 容器环境里在**宿主机**上跑 nethogs 看到的还是宿主机 PID 命名空间里的进程名,veth 对抓包意义不大;更推荐进容器网络命名空间执行:`nsenter -t <pid> -n nethogs`。
### 5.3 第三步补充:按连接/对端定位(iftop + ss)
nethogs 按进程、iftop 按连接,两者互补。抓到进程后还想知道「在和谁传」:
```bash
iftop -n -P -i eth0 # 按对端 IP:端口 实时排行,-n 不反解域名,-P 显示端口
```
```bash
# 已知是某进程后,看它具体的连接与每连接队列
ss -tinp | grep -A1 pid=81234
# 输出里含 cwnd、rtt、bytes_sent、bytes_received、retrans,能精确到单连接
```
分工口诀:**sar 定量 → nethogs 定人(进程)→ iftop 定对象(对端)→ ss -tinp 定细节(连接)**。
### 5.4 出口流量异常(被打满 / 疑似外泄)的标准流程
1. `sar -n DEV 1` 确认是 `txkB/s`(出向)异常;
2. `nethogs -s eth0`(`-s` 按发送排序)抓进程;
- 抓到的是**认识的业务进程** → 正常业务高峰,考虑限速(`tc`)或扩容;
- 抓到的是**陌生进程** → `ls -l /proc/<PID>/exe` 看二进制路径、`cat /proc/<PID>/cmdline`,高度怀疑被入侵/挖矿外传,先 `iptables -I OUTPUT -m owner --pid-owner <PID> -j DROP`(或 kill)止血再排查;
3. nethogs 抓不到(unknown TCP 多)→ `iftop -nP` 看对端 IP,`ss -tanp` 找对应五元组归属;短连接一闪而过的情况用抓包兜底:
```bash
tcpdump -i eth0 -nn -c 1000 -w /tmp/cap.pcap # 先采 1000 包
# 事后分析哪个 IP 量大
tcpdump -nn -r /tmp/cap.pcap | awk '{print $3}' | cut -d. -f1-4 | sort | uniq -c | sort -nr | head
```
### 5.5 入口流量异常(疑似被攻击/刷接口)
- `iftop -nP` / `sar -n DEV` 看入口带宽和 PPS:
- **带宽大、包也大**:正常内容被刷(盗链),找业务层解决(Referer 校验、CDN);
- **PPS 巨大、平均包小**(rxkB/s ÷ rxpck/s ≈ 几十字节):典型小包攻击/SYN flood,本机协议栈扛不住时救不了,要靠上游清洗/高防;
- `ss -s` 看连接概貌,`ss -ant state syn-recv | wc -l` 飙升 = SYN 攻击(`net.ipv4.tcp_syncookies=1` 确认开启);
- 配合第 01 篇的连接状态统计:`ss -ant | awk 'NR>1{++S[$1]} END{for(a in S)print a,S[a]}'` 看 TIME_WAIT/CLOSE_WAIT 堆积。
### 5.6 错误、丢包、重传:流量「不大但很卡」
流量没满但业务超时/慢,查质量指标:
```bash
ip -s link show eth0
```
```
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 ...
RX: bytes packets errors dropped missed mcast
662667261 1473827 0 0 0 0
TX: bytes packets errors dropped carrier collsns
350565994 983604 0 0 0 0
```
- `errors`(CRC/帧错误,持续增长 → 网线/光模块/对端端口问题)、`dropped`(缓冲区满丢包 → 本机收包来不及处理)、`overruns`、`carrier` 任何一项**持续增长**都是实锤故障;
- 更细的计数在驱动层:`ethtool -S eth0 | grep -iE 'drop|err|miss|fifo'`;
- TCP 层健康度:
```bash
nstat -az | grep -iE 'TcpRetransSegs|TcpInErrs|ListenDrops|ListenOverflows|UdpRcvbufErrors'
```
```
TcpRetransSegs 12973 0.0
TcpInErrs 28 0.0
TcpExtListenOverflows 0 0.0
TcpExtListenDrops 0 0.0
```
- **重传率** = TcpRetransSegs ÷ TcpOutSegs,> 1% 说明链路质量差或对端拥塞;
- `ListenOverflows/ListenDrops` 增长 → accept 队列溢出(backlog 太小,详见第 01 篇 4.5 节);
- `UdpRcvbufErrors` 增长 → UDP 应用读太慢丢包(调大 `net.core.rmem_max` + 应用 SO_RCVBUF);
- 软中断是否均衡:`cat /proc/softirqs` 看 NET_RX 是否全压在 CPU0。单核 si 打满而总带宽没满 → 开 RPS 或调网卡队列 IRQ 绑定:
```bash
# 查看与设置网卡队列数
ethtool -l eth0
ethtool -L eth0 combined 4
# 各队列中断绑核:/proc/irq/<N>/smp_affinity_list
```
### 5.7 连接数 / conntrack 上限
「流量不大、CPU 不高,但新连接建立失败/丢 SYN」常见元凶:
```bash
ss -s # 连接概览
```
```
Total: 254
TCP: 17 (estab 5, closed 1, orphaned 0, timewait 1)
```
```bash
sysctl net.netfilter.nf_conntrack_count net.netfilter.nf_conntrack_max
dmesg -T | grep -i 'conntrack.*full' # 打满时内核日志会有 "nf_conntrack: table full, dropping packet"
```
`count` 接近 `max`:调大 `nf_conntrack_max`(同时调 hashsize),或业务侧减少短连接。文件句柄上限同理:`cat /proc/<PID>/limits | grep 'open files'`,`ss -s` 的 Total 逼近 limit 时应用会报 `Too many open files`。
### 5.8 网络排查速查表
| 现象 | 首选命令 | 大概率原因 |
|---|---|---|
| 出口带宽打满 | `nethogs -s eth0` | 业务高峰 / 异常进程外传 |
| 入口被打 | `iftop -nP`、`sar -n DEV` 看 PPS 与均包长 | 盗链 or 小包攻击 |
| 知道网卡忙但不知谁 | `nethogs` → `iftop` → `ss -tanp` | 三级定位 |
| 流量不大但慢/超时 | `ip -s link`、`nstat -az \| grep -iE 'retrans\|drop'` | 丢包/重传/accept 溢出 |
| 单核 si 100% | `cat /proc/softirqs` | 网卡中断未分散 |
| 新连接失败 | `ss -s`、conntrack 计数、ulimit | conntrack 满 / 句柄满 |
| 短连接凶手抓不到 | `tcpdump` 采样后离线统计 | 进程生命周期太短 |
---
## 6. 综合决策树(症状 → 命令链)
```
收到告警/用户报障「机器很卡」
│
├─ ① 先花 10 秒看全局:uptime(load)+ top(CPU 分解)+ free -h + iostat -xz 1 3 + sar -n DEV 1 3
│
├─ ② load 高、CPU us/sy 高 ─────────────→ 第 2 章:pidstat -u 1 → top -H → perf top
│
├─ ③ load 高、CPU 闲(id 高)────────────→ vmstat 看 b 列(D 状态)→ iostat → 第 4 章
│
├─ ④ CPU 正常、内存 available 低/进程消失 → 第 3 章:top M → dmesg OOM → slabtop/tmpfs
│
├─ ⑤ wa 高、await 高 ──────────────────→ 第 4 章:iotop -o → lsof/PID/io → ionice 止血
│
├─ ⑥ 带宽/PPS 告警 ────────────────────→ 第 5 章:sar -n DEV → nethogs(进程)→ iftop(对端)
│
├─ ⑦ 都没满但就是卡 ───────────────────→ st(云超卖)/ PSI /proc/pressure/* / nstat 重传丢包
│
└─ ⑧ 事后复盘(现场没了)───────────────→ sar 历史:sar -u/-r/-d/-n DEV -f /var/log/sa/sa<DD>
```
## 7. 工具全家福对照表
| 维度 | CPU | 内存 | 磁盘 IO | 网络 |
|---|---|---|---|---|
| 整体利用率 | `top` `mpstat -P ALL` `sar -u` | `free -h` `vmstat` `sar -r` | `iostat -xz` `sar -d` | `sar -n DEV` `ip -s link` `ifstat` |
| 饱和度/压力 | `vmstat` r 列、PSI cpu | PSI memory、si/so、`sar -q` | aqu-sz、`await`、PSI io | `%ifutil`、与速率上限对比 |
| 错误 | `%steal`(VM) | dmesg OOM、majflt | SMART、`dmesg` IO error | `ip -s` errors/dropped、`nstat`、重传率 |
| 定位进程 | `pidstat -u`、`top` | `top` M、`smem`(PSS)、`pidstat -r` | `iotop -o`、`pidstat -d` | **`nethogs`** |
| 定位连接/文件 | — | `pmap -x` | `lsof`、`/proc/PID/io`、`fatrace` | `iftop`、`ss -tinp`、`tcpdump` |
| 定位代码 | `perf top`、`strace -c`、`top -H` | 采样 RSS + valgrind/heaptrack | `strace -e trace=%file` | — |
| 历史回看 | `sar -u -f /var/log/sa/saDD` | `sar -r` | `sar -d` | `sar -n DEV` |
## 8. 一句话总结
> 四大资源异常排查的通用心法:**先用 USE(利用率/饱和度/错误)看整体定方向,再用对应的 per-process 工具(CPU 用 pidstat、内存用 top/smem、磁盘用 iotop、网络用 nethogs)定位到人,最后下沉到线程/文件/连接/函数层找根因**;网络这一环记住四级定位链——sar 定量、nethogs 定人、iftop 定对象、ss/tcpdump 定细节,即可闭环。
@@ -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)