# 运维常见题-日常维护（二）


## 🤔 什么是僵尸进程？怎么产生、怎么处理？  
- **僵尸进程（`Zombie Process`）是已经执行完毕但还没有被父进程回收的进程。子进程调用 `exit()` 退出后，系统会释放它占用的绝大多数资源（内存、文件描述符、打开的文件等），但进程描述符（`task_struct`，包含退出码、资源使用统计等信息）还保留在进程表中，等待父进程通过 `wait()` / `waitpid()` 来读取。**

    - **僵尸进程是怎么产生的**
        - 子进程通过 `exit()` 正常结束或被信号杀掉后，内核向父进程发送 `SIGCHLD` 信号通知它*你的子进程死了，快来收尸*
        - 父进程需要调用 `wait()` 或 `waitpid()` 系统调用来读取子进程的退出状态。调用之后内核才会释放进程表中的 `task_struct`，子进程才真正从系统中消失
        - 如果父进程没有调用 `wait()`、或者信号处理函数中没处理 SIGCHLD、或者父进程自己陷入了死循环根本顾不上收尸——子进程的进程描述符就永远留在进程表中，变成了僵尸进程
        - 僵尸进程不能被 `kill` 杀死——因为它已经死了，`kill` 对一个已经退出的进程没有任何意义

    - **僵尸进程的危害有多大**
        - 僵尸进程不占用 `CPU`、不占用内存、不占用磁盘 IO，只占用进程表中的一个 `slot（task_struct）`
        - `Linux` 系统对进程总数有一个上限（`cat /proc/sys/kernel/pid_max`，默认 32768 或更大）。如果僵尸进程占满了进程表，系统就无法创建新进程了
        - 少量的、短暂的僵尸（子进程刚退出，父进程还没来得及处理）是正常现象。如果大量僵尸持续存在（`ps aux | grep Z` 持续输出很多行），说明父进程有 Bug

    - **如何处理已经产生的僵尸进程**
        - **方案一**：杀掉父进程（最常用、最有效）
            - 僵尸进程的父进程如果被 `kill` 掉，僵尸进程会被 `PID 1`（systemd）继承。`systemd` 会自动调用 `wait()` 回收，僵尸消失
            - 先找到父进程：`ps -eo pid,ppid,stat,cmd | grep Z` 查看僵尸进程的 ·（父进程 PID），然后 `kill -9 <父PID>`
            - 注意：如果父进程是重要的业务进程（如正在处理请求的 `Nginx worker`），杀掉它会导致正在处理的请求中断。需要在业务低峰期操作，或者确认该进程可以被安全重启

        - **方案二**：重发 `SIGCHLD` 信号
            - 如果父进程只是没接收到 `SIGCHLD` 信号（比如信号被屏蔽了），可以尝试 `kill -CHLD <父PID>` 重新通知父进程收尸。但这成功率不高，大多数情况父进程本身就是有 `Bug` 没写 `wait()`

        - **方案三**：如果父进程就是 `systemd（PID 1）`
            - 容器场景中会出现这种情况——容器内 `PID 1` 的进程不处理 `SIGCHLD`，导致容器内的僵尸进程无法被回收。解决方案：在启动容器时加上 `--init` 参数，使用 `tini` 或 `dumb-init` 作为容器内的 `init` 进程，它会自动回收僵尸（或者重建容器）

    - **如何从代码层面预防僵尸进程**
        - 在父进程中注册 `SIGCHLD` 信号处理函数：使用 `signal(SIGCHLD, SIG_IGN)` 显式告诉内核"我不关心子进程的退出状态"，这样子进程退出后内核会自动回收，不产生僵尸。简单有效，但父进程无法获取子进程的退出码
        - 在父进程中显式调用 `waitpid()`：在 `fork` 后调用 `waitpid(pid, &status, 0)` 等待子进程结束，或者在 `SIGCHLD` 信号处理函数中调用 `waitpid(-1, &status, WNOHANG)` 循环回收所有已退出的子进程
        - 使用"`双重 fork`"技巧：父进程 `fork` 出一个子进程，子进程立即 `fork` 出孙进程后自己退出。这样孙进程成为孤儿进程，被 `systemd` 接管并自动回收，父进程完全不用管

- **协助记忆**
    - 人已经死了（进程退出），但户籍系统上还有他的记录（进程表条目），等着家属（父进程）去派出所办死亡注销（`wait()`）。家属不去办，人就一直挂在系统上——这就是僵尸状态
    - 和 `D` 状态的区别：`D` 状态是卡在等 `IO`、还活着但叫不醒；`Z` 状态是已经死了，彻底叫不醒了。`kill -9` 对 `Z` 无效，对 `D` 也无效，但 `D` 状态如果能走完 `IO` 会自己活过来，`Z` 状态只能靠父进程收尸

- **进阶思考**
    - **`top` 显示某进程是 Z 但父进程就是 systemd（PID 1），systemd 为什么不自动回收？**
        - `systemd` 正常情况下会自动回收孤儿僵尸。如果僵尸的父进程还是 `systemd` 说明它本身就是 `systemd` 的子进程且没被回收，这在普通服务器上几乎不会出现。更常见的是容器场景——容器的 `PID 1` 进程如果不处理 `SIGCHLD`，容器里产生僵尸后，外部的 `systemd` 管不了容器内部的进程表。所以才需要用 `docker run --init` 引入 `init` 进程来处理容器内的僵尸

    - **有没有工具能直接清理僵尸进程而不影响父进程？**
        - 没有。僵尸进程的进程表条目只能由它的父进程通过 `wait()` 来释放，这是内核设计上强制绑定的。第三方工具能做的只是帮你找到父进程然后建议你 `kill` 它。唯一的例外是如果父进程已经被 `kill` 了但僵尸还在（`PID` 变成 1 但 systemd 没处理），重启 systemd 或在容器重启后僵尸自然消失


## 🤔 iptables 的 SNAT 和 DNAT 分别用在什么场景？  
- **`SNAT` 和 `DNAT` 都属于 `iptables` 的 `NAT` 表，核心作用是修改数据包的 `IP` 地址，但修改的目标不同。`SNAT` 改源地址，`DNAT` 改目标地址。**
    - **`SNAT（Source Network Address Translation）`— 源地址转换**
        - 作用：修改数据包的源 `IP` 地址，让数据包看起来是从另一个地址发出的
        - 执行时机：在 `POSTROUTING` 链（路由决策之后、数据包即将离开网卡之前）执行
        - 典型场景：内网服务器共享上网（`MASQUERADE` / `SNAT`）
            - 公司内网有大量私网服务器（如 `10.0.2.0/24`），只有一台公网网关（外网 IP `172.15.31.10`）。内网服务器发往互联网的数据包源地址是私网 IP，公网路由器不认识这个地址，无法返回数据。所以需要在网关上将源地址替换为公网 `IP`
            - `iptables -t nat -A POSTROUTING -s 10.0.2.0/24 -j SNAT --to-source 172.15.31.10`
            - 如果公网 `IP` 是动态获取的（`PPPoE 拨号`），用 `MASQUERADE` 代替 SNAT：`iptables -t nat -A POSTROUTING -s 10.0.2.0/24 -j  MASQUERADE`。`MASQUERADE` 会自动获取出口网卡的当前 IP，适用于动态 IP 场景，但性能略低于 `SNAT` 
        - 注意：`SNAT` 通常还需要配合 `FORWARD` 链的放行规则。因为 `Linux` 默认 `FORWARD` 策略是 `DROP`，只配了 `SNAT` 不配 `FORWARD` 放行，数据包根本走不通

    - **`DNAT（Destination Network Address Translation）`— 目的地址转换**
        - **作用**：修改数据包的目标 IP 地址（可同时改端口），让发往某公网地址的流量转发到内网指定服务器
        - **执行时机**：在 `PREROUTING` 链（路由决策之前、数据包刚进入网卡时）执行。因为必须在路由决策前改掉目标地址，否则内核会按原目标地址路由到本机或丢弃
        - **典型场景**：端口映射 / 内网服务对外暴露
            - 公司核心数据库在内网（`10.0.2.10:3306`），需要让分布在全国的出差员工通过公网网关访问。在网关上将发往公网 IP `202.100.1.1:3306` 的流量 DNAT 到内网 `10.0.2.10:3306`
            - `iptables -t nat -A PREROUTING -d 202.100.1.1 -p tcp --dport 3306 -j DNAT --to-destination 10.0.2.10:3306`
            - 还需要在 `FORWARD` 链放行该流量，以及在 `POSTROUTING` 做 `SNAT` 让回包能正确路由回来

    - **双向 NAT（SNAT + DNAT 组合使用）**
        - 单纯 `DNAT` 后，内网服务器收到的请求源 `IP` 是客户端的公网 `IP`，回包时会直接发给客户端公网 `IP`（不走网关），导致连接失败。所以 `DNAT` 通常需要配合 `SNAT`，将回包的源地址也转换
        - 完整配置示例（端口映射到内网 Web 服务）：
            ```bash
            # DNAT：公网访问 202.100.1.1:80 转到内网 10.0.2.10:80
            iptables -t nat -A PREROUTING -d 202.100.1.1 -p tcp --dport 80 -j DNAT --to-destination 10.0.2.10:80
            # SNAT：回包时把源地址从 10.0.2.10 转为 202.100.1.1
            iptables -t nat -A POSTROUTING -d 10.0.2.10 -p tcp --dport 80 -j SNAT --to-source 202.100.1.1
            # FORWARD 放行
            iptables -A FORWARD -d 10.0.2.10 -p tcp --dport 80 -j ACCEPT
            iptables -A FORWARD -s 10.0.2.10 -p tcp --sport 80 -j ACCEPT
            ```

- **协助记忆**
  - **SNAT（源）** ：出去的时候换源地址，让外面的人以为是网关发的。像公司前台帮你寄快递——寄件人写着公司前台的名字（SNAT），快递回来了前台签收再转交给你
  - **DNAT（目标）** ：进来的时候换目标地址，让里面的人以为是从外面直接打进来的。像前台接到找你的电话——拨了前台总机，前台转接到你的分机（DNAT）
  - **判断口诀**：从内到外改源（SNAT，POSTROUTING），从外到内改目标（DNAT，PREROUTING）


- **进阶思考**
    - **SNAT 和 DNAT 为什么必须放在不同的链上？**
        - 因为内核网络栈的处理顺序决定了它们必须在不同的时机执行。数据包到达时先经过 PREROUTING（修改目标地址后才能正确路由）、再经过路由决策（决定是 INPUT 还是 FORWARD）、最后经过 POSTROUTING（发出前修改源地址）。如果反过来——在 PREROUTING 改源地址、POSTROUTING 改目标地址，路由决策时拿到的还是错的地址，流量根本走不对

    - **同一个公网 IP 上同时有多个服务需要做 DNAT（如 80 端口的 Web 服务和 3306 端口的数据库），配置上有什么需要注意的？**
        - 没问题，DNAT 可以按端口号区分。不同端口的规则独立，互不影响：
            ```bash    
            -A PREROUTING -d 202.100.1.1 -p tcp --dport 80 -j DNAT --to-destination 10.0.2.10:80
            -A PREROUTING -d 202.100.1.1 -p tcp --dport 3306 -j DNAT --to-destination 10.0.2.11:3306
            ```
    - 但如果两个内网服务的端口相同（如都需要 8080 端口），就不能用同一个公网 IP 的同一个端口映射到不同内网机器了。需要用不同的公网端口来区分（如公网 8080 → 内网 A:8080，公网 8081 → 内网 B:8080），或者在公网 IP 充足的情况下给每个服务分配独立的公网 IP


## 🤔 ext4 与 xfs 文件系统有什么区别？  
- **`ext4` 和 `xfs` 都是 `Linux` 下成熟的日志文件系统，但设计目标和适用场景不同。`ext4` 是 `ext3` 的改进版，侧重兼容性和通用场景；`xfs` 最初由 `SGI` 为高性能计算设计，侧重大容量和高并发。选择哪一个通常取决于分区规模和工作负载类型，不是哪个更好而是哪个更合适。**

- **设计起源与发展定位**
  - **ext4**：`ext3` 的直接演进，向下兼容 `ext2/ext3`，是 `Ubuntu` 和大多数 `Linux` 发行版的默认根文件系统。强调数据的可靠性和已有系统的无缝升级
  - **xfs**：`SGI` 从 `IRIX` 移植到 `Linux`，设计于 `1990` 年代的大规模并行计算环境。`RHEL 7+ `和 `CentOS 7+` 的默认文件系统。强对超大文件和超多并发 `IO` 的优化

    - **最大容量与扩展性**
    *xfs 在超大容量场景下的处理能力明显强于 `ext4`。`xfs` 在大规模文件系统和并发需求高的场景下表现更好*
        | 能力             | `ext4`                                            | `xfs`                            |
        | ---------------- | ------------------------------------------------- | -------------------------------- |
        | 最大文件系统大小 | 50 TB（Ubuntu 推荐的稳定上限）至 1 EB（理论上限） | 8 EB（理论上限）                 |
        | 最大单个文件     | 16 TB                                             | 8 EB                             |
        | 最大子目录数     | 64000（默认限制，可用 dir_nlink 解除）            | 无硬限制（取决于 inode 数量）    |
        | inode 数量       | 格式化时固定，无法动态增加                        | 格式化时预分配，但后期可在线增加 |

    - **分配策略与碎片**
        - **ext4**：使用区段树（`Extent Tree`）和延时分配（`Delayed Allocation`）。延时分配会将多次小块写入尽量合并成连续的大块再一起分配，减少了文件碎片。但如果遇到长时间大量并发小文件写入，碎片依然会出现
        - **xfs**：基于分配组（`Allocation Group`，`AG`）的设计，文件系统被划分为多个独立的 `AG`，每个 `AG` 有自己的 `inode` 和空闲空间管理。多个线程可以并行操作不同的 `AG`，减少了锁竞争。`xfs` 的延时分配策略和基于 `AG` 的空间管理使得它的碎片控制比 `ext4` 更好

    - **在线操作能力**
        - **扩展**：`ext4` 和 `xfs` 都支持在线扩展。`ext4` 使用 `resize2fs`，`xfs` 使用 `xfs_growfs`
        - **缩小**：`ext4` 支持在线缩小（`resize2fs`），`xfs` 不支持缩小。如果创建 `xfs` 分区时空间估算失误，唯一办法是备份数据、重建分区、恢复数据。这在生产环境是一个重要的选型因素——如果你不确定未来分区大小是否需要调小，`xfs` 的不可缩小特性可能会成为麻烦


    - **数据校验**
        - ext4 在 4.x 内核后引入了对元数据的校验和（checksum），但不校验文件数据本身
        - xfs（V5 版的 xfs，RHEL 7+ 默认）对元数据、目录、日志等都提供了校验和检查，可以检测到静默数据损坏。对于存储长期归档数据或对数据完整性要求较高的场景，这是一个重要的优势

- **协助记忆**
    - **ext4**：经典大众车———皮实、通用、兼容性好（能挂载 `ext2`/`ext3`），维护成本低。大多数场景下够用。但极限性能不如 `xfs`
    - **xfs**：皮卡———为重载和长途设计，大文件大容量场景下表现突出，并发性能好。但不能缩小、配置稍微复杂
    - **实际选择**：日常服务器（几十 TB 以内）、Ubuntu 系统、需要缩小分区的场景 → ext4；超大规模存储、RHEL/CentOS 默认、高并发文件服务器 → xfs

- **进阶思考**
    - **为什么 `RHEL 7+` 默认从 `ext4` 切换到了 `xfs`？**
        - 主要原因是 `xfs` 在大容量和高并发场景下的扩展性更好。`RHEL 7` 时代硬件容量快速增长，多核 `CPU` 和 `NVMe` 盘的普及使得 `IO` 并发度大幅提升。`ext4` 的一些设计（如单个全局的 `inode` 表）在高并发写入时存在锁竞争，而 `xfs` 的分配组（AG）天生就是为并发设计的。红帽在 `RHEL 7` 中明确将 `xfs` 作为默认文件系统，因为它更能发挥现代硬件的性能

    - **一个 `ext4` 分区上创建了 `100` 万个文件后性能明显下降，换成 `xfs` 能解决吗？**
        - 可能部分缓解但不一定根治。`ext4` 在 `inode` 数量多且分散时，目录查找和空间分配的性能确实会退化。`xfs` 的 `AG` 结构和 `B+` 树索引在同样的场景下表现更好。但如果性能下降的原因是目录结构设计不合理（如单目录存放百万文件），不管换什么文件系统都不如优化目录层次结构来得有效。hash 分层的目录结构（如 `./ab/cd/efgh/file`）在任意文件系统下都比百万文件平铺性能好

    - **如果现有 `ext4` 分区需要缩容该怎么操作？`xfs` 不支持缩容该怎么办？**
        - **`ext4`**：支持缩容。操作步骤：`umount` 分区（如果根分区需 `rescue` 模式或 `live CD` 启动）→ `e2fsck -f /dev/sdX1` 强制检查一致性 → `resize2fs /dev/sdX1 500G` 将文件系统缩小到 500G。这一步只修改了文件系统层的空间分配，底层的块设备（分区或逻辑卷）大小还没有变。接下来根据设备类型处理：
            - **如果是 LVM 逻辑卷**：`lvreduce -L 500G /dev/vg/lv_data` 缩减逻辑卷
            - **如果是普通分区**：`fdisk` 或 `parted` 删除分区后重建更小的分区（操作风险高，生产环境建议先备份）
        - **xfs**：不支持缩容。如果需要回收空间，标准做法是用 `xfsdump 备份数据` → `重建分区` → `xfsrestore 恢复数据`。没有像 `ext4` 那样直接缩容的途径，所以创建 `xfs` 分区时空间规划要留足余量

- **扩展信息**
    - **协作开发与合作**：`ext4` 由 `Linux` 内核社区（主要由 `Ted Ts'o` 维护）开发，`xfs` 最初由 `SGI` 开发，后来也由社区维护（现在的核心维护团队来自 Red Hat）
    - **碎片整理**：
        - `ext4`：`e4defrag` 工具可以对单个文件或整个分区进行碎片整理。`e4defrag /path/to/file` 整理指定文件，`e4defrag /dev/sdX` 整理整个分区
        - `xfs`：`xfs_fsr（Filesystem Reorganizer）`工具对碎片严重的大文件进行整理。`xfs_fsr /dev/sdX` 自动扫描并整理。整理过程中需要空闲的 `AG` 空间作为缓冲区，所以 `AG` 快满时 `xfs_fsr` 可能不起作用


## 🤔 什么是 Swap？什么时候会用？  
- **`Swap` 是 `Linux` 用磁盘空间模拟出来的"伪内存"。当物理内存不足时，内核会将暂时不用的内存页换出到磁盘的 `Swap` 空间，腾出物理内存给当前需要的进程。本质上是用磁盘 `IO` 换内存空间——牺牲速度换容量。**

- **`Swap` 的两种形式**
  - **`Swap` 分区**：安装系统时划分的独立分区，性能稳定，管理简单。`swapon -s` 查看当前启用的 `Swap` 分区
  - **`Swap` 文件**：在已有文件系统上创建一个大文件作为 `Swap`。适用于不重新分区但需要增加 `Swap` 的场景。注意：如果该文件系统是基于 `Swap` 本身或其他易失性设备创建的，系统可能无法正常使用 `Swap` 文件。
    - **创建方式：**
        ```bash
        dd if=/dev/zero of=/swapfile bs=1G count=4
        chmod 600 /swapfile
        mkswap /swapfile
        swapon /swapfile
        ```

    - **什么时候会用到 `Swap`**
        - **物理内存接近用尽时**：内核的内存回收机制会尝试释放可回收的内存（如 `Page Cache`）。当这些回收手段仍然不够时，内核开始将不活跃的匿名内存页（进程堆栈的私有数据）换出到 `Swap`
        - **内存抖动时**：某个时间段内多个进程争抢内存，内核频繁换入换出，表现为 `vmstat 1` 的 `si` 和 `so` 列持续大于 `0`。这时候系统的性能会显著下降，反应在系统响应上通常是整个系统变慢
        - **休眠（`Suspend to Disk`）** ：笔记本电脑合盖休眠时，系统将整个内存的内容写入 Swap 分区（或专门的休眠分区），关机断电。下次开机时从 Swap 恢复到内存。此时 Swap 分区大小必须大于等于物理内存大小
        - **内核的 `oom_reaper` 回收进程资源时**：当系统极度缺内存时，内核会先尝试把一些进程的页面换出到 Swap，以便在不杀死进程的情况下为其他进程腾出空间

    - **`swappiness`———控制内核使用 `Swap` 的积极程度**
        - `vm.swappiness` 取值范围 `0 ~ 100`，默认 `60`（注： 他控制的是内核需要回收内存时，去回收 `Page Cache` 还是去换出匿名页的比例偏好）
            - `0~10`: 几乎不 `Swap`，只回收 `Cache`, 适用于磁盘 `Swap` 且跑数据库时合理
            - `20~40`：适当允许冷匿名页换出，但倾向 `Cache`，适用于 `zRAM` + `混合负载`
            - `60（默认）`：平衡对待 `Cache` 和匿名页,适用于通用场景，无特殊调优需求
            - `80~100`： 主动换出匿名页，保留更多 `Cache`,适用于大内存缓存型应用（如 Redis 全量缓存）
        - 数值越低，内核越倾向于先回收 `Page Cache` 而不是换出匿名内存页；数值越高，内核越倾向于主动使用 `Swap`
        - 数据库服务器通常设为 `1 ~ 10`，因为数据库的 `Page Cache`（数据文件缓存）对性能至关重要，内核应该尽量保留 `Cache` 而不是去换出
        - **注意**：`swappiness=0` 在过去意味着*只有在内存绝对不足时才 Swap*，但较新版本（如 `RHEL 8+`）内核中 0 的行为发生了变化，继续坚持使用 0 可能会导致 `OOM` 发生。建议不要设成 `0`，设一个较小的值（如 `1~10`）即可

    - **`Swap` 的利弊**
        - **优点**：内存不足时提供容错缓冲，避免 `OOM Killer` 立即杀掉进程。系统可以继续运行，只是变慢
        - **缺点**：磁盘的读写速度远低于内存（即使 `NVMe` 也只有内存的十几分之一），一旦发生大量换入换出，系统响应会大幅下降。所以 `Swap` 不是*内存的替代品*，而是*内存不足时的最后缓冲*

- **协助记忆**
    - `Swap` 是内存的备胎——平时用不到，真缺人的时候拉上来顶一顶，但顶上来之后效率肯定不如正主
    - `swappiness` 不是*内存用到 X% 才 Swap*的阈值，而是*缺内存时先牺牲谁*的取舍开关
    - `低值（0~10）`：优先回收 Page Cache，尽量不动匿名页。代价是读文件得重新从磁盘读
    - `高值（80~100）`：更愿意换出冷匿名页到 Swap，留着更多 Cache。代价是访问换出页时有延迟
    - `默认 60`：两者大致平衡
    - `配合 zRAM 时注意`：zRAM 解压比磁盘快得多，所以设 20~30 把冷匿名页压缩掉换回物理内存，通常是划算的
    - `si / so` 是实战指标：看到这两个持续大于 0，说明系统真正在走 Swap 了


- **进阶思考**
    - **现在服务器动辄 `64GB`、`128GB` 内存，还有必要配置 `Swap` 吗？**
        - 有必要，但不再是过去*Swap = 2x 物理内存*的规则了。对于大内存服务器，`Swap` 的主要作用不再是扩容，而是作为内存压力的早期预警和缓冲。即使内存很大，仍然存在 `OOM` 的风险（如进程内存泄漏）。有一个 `2 ~ 8GB`(一般建议，内存小于 `8G`，设置 `1.5-2 * 2` , 大于 `8G` 设置 `8G`) 的 `Swap` 可以让内核在内存紧张时有机会回收和换出，而不是直接触发 `OOM Killer`。云服务器（如 AWS、阿里云）通常也建议保留 `Swap`，但可以使用云盘上的 `Swap` 文件而不是占用珍贵的本地 `SSD` 空间

    - **`Swap` 在被频繁使用时，系统已经响应很慢了，这时候该怎么办？**
        - `Swap` 频繁换入换出说明物理内存远小于系统需求。这时候加 `Swap` 是饮鸩止渴——加了更多的 `Swap` 只会让更多数据被换到磁盘上，系统更慢。根本做法是分析是哪些进程在占用内存（top 按 M 排序看 RES），确认是内存泄漏还是业务扩容需要加内存。应急方案是先临时增加 `Swap` 文件容错（`swapon /another_swapfile`），但长期解决方案一定是增加物理内存或迁移到内存需求更低的架构

    - **zRAM 是什么？它和传统 Swap 有什么区别？**
        - `zRAM` 是在内存中压缩数据来模拟 `Swap` 的机制，不需要真正的磁盘空间。它在内存中划分一块区域，将要换出的页压缩后存进去（压缩比通常 `2:1 ~ 3:1`），需要时再解压缩读回。相比传统的磁盘 `Swap`，`zRAM` 的读写速度接近内存（因为压缩解压缩的 `CPU` 开销远小于磁盘 `IO` 延迟）。在内存有限的设备（如树莓派、低配云服务器）上效果明显。启用方式：`zramctl` 命令配置，或通过 `systemd` 的 `zram-generator` 自动管理。

    - **`zRAM` 显示 "`Swap 8G/8G 满`"，但 `si=so=0`，这算问题吗？**
        - 不算问题。`zRAM` 满只意味着 `8 GB` 的 `4K` 页面槽位已被历史数据占满，但压缩后的实际物理内存只用了 `2.44 GB`。没有活跃的换入换出（`si=so=0`），系统处于稳态。和磁盘 `Swap` 满导致大量 `IO wait` 完全是两回事。`zRAM`满后唯一的副作用是：如果再有匿名页需要换出，`zRAM` 没有空槽位了，内核只能走其他路径。但 `swappiness=2` 下内核几乎不会主动换出，所以这个场景不太可能发生

    - **`zRAM` 满了之后，如果系统突然遇到内存压力，会发生什么？**
        - `zRAM` 槽位满了，无法再接收新换出的页。内核会先尽力回收 `Page Cache`。如果 `Cache` 也回收完了还不够，就开始走 `OOM Killer`。`zRAM` 满不会像磁盘 `Swap` 满那样导致 `IO` 抖动，但它也失去了*换出冷数据腾空间*的能力。缓解方式：`zramctl /dev/zram0 -s 12G` 扩大 `zRAM` 设备大小（需要先关闭 `Swap`），或者增加物理内存

    - **`swappiness=2` 这个值合理吗？和 zRAM 配合有没有特殊的点？**
        - `swappiness` 控制的是内核在回收内存时，倾向于回收 `Page Cache` 还是换出匿名页的权重。设成 `2` 意味着 `99%` 以上的回收都走 `Cache`。这通常是为了保护数据库或 `Java` 应用的匿名内存不被频繁换出。配合 `zRAM` 时，这个配置是合理的——`zRAM` 的延迟虽然比磁盘低很多，但解压仍然比直接读取物理内存慢。所以依然优先保匿名内存、回收 Cache。真正的矛盾点在于：某些冷匿名页（如长时间空闲的 GitLab worker）其实应该可以被 `zRAM` 换出以释放物理内存，但 `swappiness=2` 下它们一直占着物理内存直到短时冲击发生才被迫换出，这就积累了大量的 `zRAM` 数据

## 🤔 SSH 连接比较慢是什么问题？  
- **`SSH` 连接慢通常不是 `SSH` 协议本身拖慢的，而是连接建立阶段的某几个环节因为超时或等待导致的。常见的原因集中在 `DNS` 反向解析和认证方式回退上。**
    - **`SSH` 服务端反向 `DNS` 解析（最常见的原因）**
        - `SSH` 服务端收到客户端的连接请求后，默认会尝试对客户端的 `IP` 做反向 `DNS` 解析（`PTR` 查询），拿到主机名后再记录到日志中
        - 如果客户端的 `IP` 没有配置反向记录，或者内网的 `DNS` 服务器响应慢，这个步骤就会卡住直到超时（通常 `5 ~ 10` 秒），然后才继续往下走。这是导致 `SSH` 连接慢的第一大原因
        - `确认方法`：在服务端查看 `/var/log/secure` 或 `/var/log/auth.log`，看登录日志中的主机名是否显示为 `IP` 还是解析后的名字
        - `解决`：在 `/etc/ssh/sshd_config` 中设置 `UseDNS no`，然后 `systemctl restart sshd`
        - `确认`：`SSH` 客户端连接慢且服务端开启 `PTR` 查询时这一现象较明显，把 `UseDNS` 设置为 `no` 后连接时间恢复正常，该参数在 `SSH` 配置手册中有详细说明

    - **`GSSAPI` 认证（`Kerberos` 环境外才会触发）**
        - `SSH` 客户端默认尝试 `GSSAPI` 认证（用于 `Kerberos` 环境），在大部分没有 `Kerberos` 的场景下，这次尝试会等待超时后才回退到密码或密钥认证
        - `解决`：在客户端的 `~/.ssh/config` 或 `/etc/ssh/ssh_config` 中添加 `GSSAPIAuthentication no`
        - `确认`：`GSSAPI` 认证超时会产生等待，关闭后连接立即恢复正常，`man ssh_config` 确认
    
    - **网络层面原因**
        - **物理距离远**：跨机房、跨境连接时，`TCP` 三次握手的 `RTT`（往返时间）直接决定了连接建立的基础延迟。例如从中国连接到美国西海岸，`RTT` 约 `150 ~ 200ms`，仅 `TCP` 握手就至少需要 `150ms`
        - **`MTU` 问题**：中间链路的 `MTU` 小于默认 `1500` 时，如果 `ICMP` 不可达消息被防火墙拦截，`SSH` 会卡在握手后数据包分片阶段，表现为连接成功但卡在输密码或输完密码后卡住  
        - **防火墙策略**：防火墙按顺序规则逐条匹配，规则太多时每一包都要遍历，表现为连接不稳定或偶发延迟

    - **服务端资源紧张**
        - `SSH` 服务端收到连接后需要 `fork` 一个子进程来处理这个连接。如果系统负载极高、进程表满了或 `PID` 耗尽，`fork` 过程会变慢甚至失败
        - `确认`：`ssh -v user@host` 看到连接建立阶段完成后、认证阶段开始前有明显停顿，且服务端负载很高

- **协助记忆**
    - `SSH` 慢的原因是一条排查链：先查 `DNS（UseDNS）`，再关 `GSSAPI`，再看网络 `RTT`，最后看服务器压力

- **进阶思考**
    - **关闭 UseDNS 有没有安全风险？**
        - 风险极小。`UseDNS` 的作用是在日志中记录客户端的域名而不是 `IP`，方便日志审查。但它不能作为身份验证的依据——`IP` 和 `DNS` 记录都可以伪造。真正做访问控制应该在 `sshd_config` 中用 `AllowUsers` 或防火墙规则，而不是依赖 `DNS` 反解。登录后如果确实需要排查来源，可以通过 `last` 查看登录源 `IP`，再自行查 `DNS`

    - **`ssh -v` 的调试输出怎么看慢在哪一步？**
      - **运行 `ssh -v user@host`，关注输出中的时间戳差距：**
        - 如果卡在 "`Connecting to host`" 之后很久才 "`Connection established`" → 网络 `RTT` 的问题
        - 如果 "`Connection established`" 后很快但 "`Authenticating`" 之前卡住 → `UseDNS` 或 `GSSAPI`
        - 如果输完密码或发送密钥后卡住 → 服务端正在验证（如 `AuthorizedKeysFile` 指向的 `NFS` 挂载目录响应慢）
        - 如果连接完成但交互延迟明显 → 非连接建立阶段的问题，排查链路带宽或拥塞

    - **如果确认是服务器负载过高导致 `SSH` 连接慢甚至无法连接，如何解决？**
        - **提前配好 `sshd` 的优先级和 `OOM` 保护，这是一条保底运维通道，不等出问题了才临时想办法：**
            - 通过 `systemd drop-in` 配置 `/etc/systemd/system/sshd.service.d/priority.conf`：
                ```ini
                [Service]
                ## # nice：在"公平排队"的调度策略下，让调度器尽量多照顾你一下。但如果排队的人太多（CPU 满负载），nice 再低也可能抢不到 CPU, 因此不建议设置 
                # Nice=-20 
                CPUSchedulingPolicy=rr
                CPUSchedulingPriority=99
                OOMScoreAdjust=-1000
                ```
            - `CPUSchedulingPolicy=rr` + `CPUSchedulingPriority=99 (0-99: 值越大越优先)`：将 `sshd` 设置为实时调度、最高优先级。即使其他进程把 `CPU` 吃满 `100%`，`sshd` 仍然能优先抢到时间片，管理员可以 `SSH` 进来处理异常进程（只要有一个实时线程可运行，它必须优先于所有 `SCHED_OTHER` 线程获得 `CPU`。这是"确定能进去"和"可能能进去"的区别）
            - `OOMScoreAdjust=-1000`：确保内存耗尽时 `sshd` 不会被 `OOM Killer` 误杀
            - 安全风险极低：优先级提升需要 `root` 权限配置，不绕开认证，只是保证 `sshd` 在高负载下能被调度执行
            - 建议作为 `sshd` 基线配置提前配好，而不是等到服务器挂了才去加——那时可能已经连不上了
            - 这个方案不覆盖进程表满（`PID` 耗尽 / 大量僵尸）导致 `sshd` 无法 `fork` 的场景，需要另外配带外管理兜底 
        - **如果没有提前配**: 只能通过带外管理（`iLO` / `DRAC` / `IPMI` / `BMC` 或 `Serial Console`）进入
        - **如果有已有 `SSH` 会话存活**：通过它执行 `echo f > /proc/sysrq-trigger` 触发 `OOM Killer` 杀掉最占内存的进程,或 `echo e > /proc/sysrq-trigger` 向所有进程发 `SIGTERM`
        - **预防措施**：`MaxStartups 10:30:60` 限制未认证连接并发数，生产服务器标配带外管理

- **扩展信息**
    - **`SSH` 连接建立的全流程**：
      1. `TCP` 三次握手（网络层）
      2. `SSH` 协议版本协商（客户端和服务端确认协议版本）
      3. 密钥交换（`Diffie-Hellman`，生成会话密钥）
      4. 主机密钥验证（确认服务端身份）
      5. 认证阶段（`密码` / `公钥` / `GSSAPI` 等）
      6. 会话通道建立

    - 其中步骤 `2 ~ 4` 是 `SSH` 协议层面的固定开销，少量密钥交换算法（如 `diffie-hellman-group-exchange-sha256`）因为计算量大，在低性能设备（如嵌入式路由器）上会产生几秒的额外延迟。可以在客户端配置中优先选择更快的算法：`KexAlgorithms curve25519-sha256`，选择使用 `x25519` 的密钥交换算法，在保证安全的同时能有效降低服务端负载和延迟 


## 🤔 执行 mount 或 df 命令时卡住无响应，可能是什么原因？  
- **`mount` 和 `df` 卡住的核心原因是它们需要遍历所有挂载点并读取每个文件系统的状态信息，如果某个挂载点对应的存储系统没有响应，命令就会停在那里等待，直到超时或你手动中断**

    - **`NFS` 服务端宕机或网络不通（最常见的原因）**
        - *服务端宕机*、*网络中断*、*防火墙拦截了 `NFS` 端口*——只要挂载了 `NFS` 的客户端，在访问挂载点时，`df` 和 `mount` 需要向服务端发送状态查询。如果服务端不响应，命令卡住 
        - `NFS` 默认使用硬挂载（`hard`），意味着客户端会一直重试直到服务端恢复。如果 `NFS` 服务端宕机且在短时间内无法修复，所有执行 `df` 的会话都会阻塞，连锁影响运维操作
        - 解决：`umount -f -l <nfs_mount_point>` 强制卸载（`-l lazy` 模式，立即断开挂载点，清理后台残留进程）。如果连 `umount` 也卡住，需要 `umount -f -l` 或重启
        - 确认：`mount` 一个不可达的 `NFS` 路径后执行 `df` 会卡住，配合 `strace df -h` 可以看到卡在 `statfs()` 系统调用上

    - **存储链路故障（`SAN` / `iSCSI` /` 光纤通道`）**
        - 如果服务器通过 `iSCSI` 或 `FC` 挂载了远端存储，且存储链路中断但 `multipath` 没有切换，`df` 在尝试读取该设备的状态时也会卡住
        - `/dev/sdX` 设备处于 `D` 状态等待 `IO` 完成，`df` 读取该分区的 `superblock` 信息会一直等待 `IO` 返回

    - **死掉的挂载点（`Stale Mount`）**
        - `NFS` 服务端重启后，客户端的挂载点没有重新连接，访问该挂载点的任何操作都会卡住。`ls -l <mount_point>` 可以看到所有文件属性显示为 "`?`" 或者 `stat` 卡住
        - `mount -a` 有时也会因为陈旧挂载点信息而卡住

    - **磁盘硬件故障或 `IO` 挂起**
        - 本地磁盘正在经历 `IO` 错误（硬盘即将损坏、磁盘控制器异常、SATA 线缆松动），导致文件系统的元数据读取操作无法完成。`df` 需要读取每个挂载点的 `superblock` 信息，如果某个分区对应的磁盘在硬件层面不响应，命令也会卡住
        - 排查：`strace df -h` 可以看到它卡在哪个系统调用和哪个路径上

- **协助记忆**
    - `df` / `mount` 卡住 = 有远程挂载点（`NFS` / `iSCSI`）不响应了
    - 排查扣：`strace` 看卡在哪个路径，`umount -f -l` 强拆，业务需要 `NFS` 挂载时配合 `soft` 选项避免运维操作被拖死
    - 如果是本地磁盘卡住：`strace` 定位到卡住的设备路径后 `echo offline > /sys/block/sdX/device/state` 强制离线

- **进阶思考**
    - **`NFS` 的硬挂载（`hard`）和软挂载（`soft`）有什么区别？生产环境应该如何选择？**
      - 硬挂载下，`NFS` 请求失败后会一直重试直到服务端恢复，期间访问该挂载点的进程会卡在 `IO` 等待状态（`D 状态`），无法被 `kill` 杀掉。软挂载下，请求超时后返回错误给应用，不会卡死。生产环境通常建议硬挂载 + `intr` 选项（`intr` 允许信号中断等待中的 `IO`，已被较新内核默认行为取代），或使用 `hard` + 合理的 `timeo` 和 `retrans`
        设置；数据一致性要求高且可以容忍服务端宕机时卡住某些进程的场景下，选择硬挂载是更合适的

    - **`df -h` 和 `df -h --exclude-type=nfs4` 的区别是什么？**
      - `--exclude-type` 跳过指定类型的文件系统。如果你知道是 `NFS` 挂载导致 `df` 卡住，用这个参数可以快速跳过 `NFS` 挂载点拿到本地磁盘的使用情况。更通用的做法是 `timeout 3 df -h` 设置超时时间，`3` 秒没返回就自动终止

    - **`NFS` 服务端宕机后，客户端重启了但 `mount` 卡在 `fstab` 里，导致系统启动卡住怎么办？**
      - 在 `/etc/fstab` 的 `NFS` 挂载选项中加上 `nofail`，如果挂载失败不会阻塞启动流程继续往下走。`_netdev` 选项也可以让系统在网络就绪后再尝试挂载 `NFS`。如果已经卡在启动阶段，进单用户模式注释掉 `fstab` 中对应的 `NFS` 行再重启 


## 🤔 conntrack 表满会导致什么问题？如何解决？  
- **`conntrack`（连接跟踪）是 `Linux` 内核 `Netfilter` 模块的核心功能，负责记录所有经过系统的网络连接的状态信息。每一个经过系统的 `TCP/UDP/ICMP` 数据包都会被 `conntrack` 记录一条状态条目。当并发连接数超过 `nf_conntrack_max` 上限时，新的连接无法建立，直接表现为网络异常。**

    - **`conntrack` 表满的典型表现**
        - 新连接建立失败，已有连接不受影响。现象包括：`curl` 卡住后超时、浏览器一直转圈、数据库连接池报错 `Cannot assign requested address`
        - 系统日志出现 `kernel: nf_conntrack: table full, dropping packets`
        - `dmesg | tail` 能看到大量 `conntrack` 丢弃的记录
        - 因为只有新连接受影响，正跑着的服务可能表面正常，但新请求全部进不来。这是它最有迷惑性的地方——*服务在跑，网络不通*

    - **为什么会打满？**
        - **高并发短连接场景**：`Nginx 反向代理`、`LVS 负载均衡器`、`Kubernetes 节点等中间节点`，每经过一个连接就记录一条。短连接请求结束后条目不会立刻消失，要等到超时时间（TCP 默认 5 天，UDP 默认 30 秒）才会清除。"积累"比"释放"快。这是最常见的打满原因
        - **`UDP` 大量发包**：`DNS 查询`、`SNMP 采集`、日志采集产生的海量 `UDP` 包，每条 `UDP` 被认为是一次"连接"被记录。`UDP` 没有真正的连接概念，`conntrack` 需等待超时才能释放条目（默认 30 秒），UDP 量大时很容易打满
        - **遭受 DDoS 攻击**：短时间内大量新连接涌入，直接占满 `conntrack` 表

    - **解决与优化方案**
        - **扩大 conntrack 上限**: `sysctl -w net.netfilter.nf_conntrack_max=1048576` 默认通常是 `65536` 或 `262144`，对于高并发服务器建议加大到 `1048576`（约 `100` 万）。持久化写入 `/etc/sysctl.d/99-conntrack.conf`

        - **同步调整 `hashsize conntrack` 表**: 使用哈希表实现，`hashsize` 默认按 `nf_conntrack_max / 4` 计算。如果 `max` 增大到 `100` 万，但 `hashsize` 没变，哈希冲突严重，性能会下降 `echo 262144 > /sys/module/nf_conntrack/parameters/hashsize` 或通过内核模块参数加载时设置：`options nf_conntrack hashsize=262144`

        - 缩短超时时间 `TCP` 的 `conntrack` 条目默认最长保留 `5` 天（`432000` 秒），对于大部分短连接场景完全没必要, 根据业务特点调整，通常将 `TCP established` 设为 `600` 秒（`10` 分钟）对大部分场景都足够
            ```bash
            sysctl -w net.netfilter.nf_conntrack_tcp_timeout_established=600
            sysctl -w net.netfilter.nf_conntrack_udp_timeout_stream=30
            sysctl -w net.netfilter.nf_conntrack_udp_timeout=10
            ```

        - 开启 `conntrack` 条目的早期回收, 前者让 `conntrack` 对 `TCP` 状态转换更宽容，后者关闭对非标准 `TCP` 标志的跟踪，减少不必要的条目
            ```bash
            sysctl -w net.netfilter.nf_conntrack_tcp_be_liberal=1
            sysctl -w net.netfilter.nf_conntrack_tcp_loose=0
            ```
        - 绕过 `conntrack` 对于不需要连接跟踪的流量（如本机内部通信、高吞吐转发场景），在 `raw` 表中设置 `NOTRACK` 规则，让特定流量跳过 `conntrack`：
            ```bash
            iptables -t raw -A PREROUTING -s 10.0.0.0/8 -j NOTRACK
            iptables -t raw -A OUTPUT -s 10.0.0.0/8 -j NOTRACK
            ```

    - **如何查看当前服务器的 `conntrack` 表使用量？**
        - **有几种方式，适用于不同场景**：
            - 快速看总数：`cat /proc/net/nf_conntrack | wc - l`。这是最直接的方式，但也意味着要逐条读取后再计数，当条目达到百万级时读取本身会有少量性能开销，不过常用于运维脚本中配合告警
            - 通过 `sysctl` 看当前使用量：`sysctl net.netfilter.nf_conntrack_count`。这个参数直接返回当前条目数，不需要遍历整个表，比 `wc -l` 更快且没有额外 `IO` 开销，适合在监控脚本和告警规则中使用
        - **搭配上限值一起看**： 两者相减就是剩余可用的连接跟踪条目数。阈值告警通常设在 `80% ~ 90%`，超过即触发扩容或排查
            ```bash
            sysctl net.netfilter.nf_conntrack_max
            sysctl net.netfilter.nf_conntrack_count
            ```
        - **查看详细条目**：`conntrack -L` 列出所有条目，`conntrack -S` 查看 `conntrack` 系统的统计信息（包括查找命中率、失败次数等）。`conntrack` 工具需要安装 `conntrack-tools` 包

## 🤔 TCP 连接数暴涨，如何定位是正常业务、攻击还是程序问题？  
- **`TCP` 连接数暴涨本身只是一个现象，原因可能是业务量正常增长、遭受攻击、或者程序有 `Bug`。定位的关键是通过多维度特征交叉判断———看来源分布、连接状态、端口特征、业务指标这四个维度，基本就能区分出是哪一类**
    - **第一步**：看来源 IP 分布
        - `ss -tan | awk '{print $5}' | cut -d: -f1 | sort | uniq -c | sort -nr | head -n 20`
        - **少量 `IP` 大量连接**：通常是程序问题或恶意攻击。如果来源 `IP` 只有几个甚至一个，但各自持有几千上万个连接，大概率是程序连接泄漏（连接池没配好、用完没 close）或单 `IP` 发起的攻击  
        - **大量 `IP` 少量连接**：每个 `IP` 只有几个连接，但 `IP` 总数巨大。如果是国内 `IP` 分散在全国各地，可能是正常业务流量爆发（如营销活动、热搜）；如果是大量海外或陌生 `IP` 段，则更像 `DDoS` 攻击
        - **`IP` 分布是否均匀**：正常流量通常符合自然分布规律，而攻击流量往往集中在特定 `IP` 段或特定地区

    - **第二步**：看连接状态分布
        - `ss -tan | awk '{print $1}' | sort | uniq -c | sort -nr`
        - **`ESTABLISHED` 暴涨 + 单 `IP` 集中**：连接泄漏。程序不断建连但没释放，连接数持续爬升不回缩。典型场景：*数据库连接池没配上限*、`HTTP` 客户端没设置 `MaxIdleConns` 
        - **`TIME_WAIT` 暴涨**：大量短连接被主动关闭，通常是 `Nginx` 没开长连接、应用层每次请求都新建连接。属于程序配置问题，不是攻击（攻击者一般不会主动关连接让你产生 `TIME_WAIT`）
        - **`SYN_RECV` 暴涨**：三次握手的第二步，收到 `SYN` 但没完成握手。大量 `SYN_RECV` 说明遭受 `SYN Flood` 攻击（半连接攻击），或者服务端 `accept` 队列满了处理不过来。这是 `DDoS` 的典型特征
        - **`CLOSE_WAIT` 暴涨**：对端关闭了连接但本机没调用 `close()`，程序 Bug，连接泄漏

    - **第三步**：看目标端口分布
        - `ss -tan | awk '{print $4}' | cut -d: -f2 | sort | uniq -c | sort -nr | head -n 10`
        - 如果所有连接集中打向同一个端口（如 `80` 或 `443`），且该端口是你的核心业务端口——可能原因包括该业务的正常流量激增，或是针对该服务的攻击流量。如果集中打向非业务端口（如随机高位端口），则基本是端口扫描或攻击
        - 正常业务流量结束后连接数会回落；程序泄漏的连接数只升不降；攻击流量不会自然回落

    - **第四步**：交叉验证业务指标
        - **`QPS` 是否同步增长**：连接数暴涨时，`QPS` 是否也同步涨？如果连接数翻了三倍但 `QPS` 没变，说明大量连接建而不用——连接泄漏或攻击
        - **错误率是否同步升高**：连接数暴涨的同时 `5xx` 错误率也在涨，可能是攻击导致服务过载；如果错误率正常且 `QPS` 同步增长，可能是正常业务流量
        - **响应延迟是否正常**：连接数暴涨后响应延迟明显升高，可能是程序问题导致连接不能及时释放占用了线程池；也可能是正常流量超过处理能力
        - **上一级流量入口是否一致**：如果前面有负载均衡器，对比负载均衡器收到的总流量和后端服务器收到的连接数是否匹配。后端连接数远大于前端入口流量，说明有连接泄漏在逐级放大

- **协助记忆**
    - **IP 分布定来源**：少量 `IP` 大量连接 → 泄漏或攻击；大量 `IP` 少量连接 → 正常流量或 `DDoS`
    - **状态分布定性**：`ESTABLISHED` 不回落 → 泄漏；`SYN_RECV` → 攻击；`TIME_WAIT` → 短连接配置问题
    - **业务指标验证**：连接涨 `QPS` 不涨 → 不干活白占连接，肯定有问题
    - **口诀**：看 `IP` 分布，看连接状态，看端口，对齐业务指标，四个交叉出结论


- **进阶思考**
    - **`SSH` 连接数暴涨几百个，但来源 IP 都是内网同一台 `Jenkins` 机器，这是什么问题？**
      - `Jenkins` 的 `SSH` 插件或 `Pipeline` 脚本可能每次构建都建立新的 `SSH` 连接但没有正确关闭。检查 `Jenkins` 系统配置中的 "`SSH site`" 的 "`Disconnect`" 选项，或者在 `Pipeline` 脚本中使用 `withCredentials` + `sshCommand` 确保每次用完关闭连接。如果 `Jenkins` 配置了 `SSH` 连接缓存但数量上限设置过大，也可能导致连接池膨胀

    - **在确认是 `DDoS` 攻击后，`Nginx` 层面可以做哪些快速缓解？**
      - `limit_conn_zone` 限制单 `IP` 并发连接数；`limit_req_zone` 限制单 `IP` 请求速率；通过 `iptables` 直接 `DROP` 攻击来源 `IP` 段；开启 `SYN Cookies` 防御 `SYN Flood`。如果是大流量攻击（带宽打满），需要在上游 `CDN` 或云防护层面做流量清洗，`Nginx` 层已经挡不住了

    - **如何监控连接数变化趋势，以便在异常早期就发现？**
      - **监控关键指标**：`ss -s` 总连接数按状态分组输出；`/proc/net/tcp` 的 `TCP` 连接总数（或通过 `Prometheus node_exporter` 的 `node_netstat_Tcp_CurrEstab`）；应用层的活跃连接池水位。通过这几个指标配合历史基线对比，超过基线的 `2 ~ 3` 倍时触发告警

- **扩展信息**
    - **`QPS（Queries Per Second）`** ：每秒查询数，衡量服务吞吐量的核心指标。对于 `Web` 服务，`QPS` 就是每秒处理的请求数。`QPS` 和连接数的关系：一个连接可以承载多次请求（`HTTP Keep-Alive`），所以连接数暴涨但 `QPS` 没变，说明连接在空挂
    - **`TPS（Transactions Per Second）`** ：每秒事务数。一个事务可能包含多个查询，在数据库场景更常用
    - **`RT（Response Time，响应时间）`** ：请求从发出到收到完整响应的时间，通常看平均 `RT` 和 `P99 RT`。
      - **利特尔法则（Little's Law）** ：`QPS` = `并发数` ÷ `RT`。知道任意两个可以算出第三个。例如平均 `RT 200ms`、`20` 个并发请求同时在处理，`QPS = 20 ÷ 0.2 = 100`
      - 排查时 `QPS` 涨但 `RT` 也涨 → 系统过载或瓶颈；`QPS` 涨但 `RT` 正常 → 正常扩容

    - **`DDoS（Distributed Denial of Service）`**：分布式拒绝服务攻击。攻击者通过控制大量僵尸主机向目标服务器发送海量请求，耗尽服务器资源（带宽、CPU、连接表等），使正常用户无法访问。`TCP` 层面的 `DDoS` 主要有两种：
      - **`SYN Flood`**：只发 `SYN` 不完成三次握手，占满服务端的半连接队列。防御：开启 `SYN Cookies`
      - **全连接攻击**：完整建立 `TCP` 连接然后发垃圾请求，占满连接数和应用层资源。防御：限流、WAF、CDN 清洗

    - **`SYN_RECV`**：`TCP` 三次握手的第二步状态。服务端收到客户端的 `SYN` 包，回复 `SYN+ACK` 后等待客户端的 `ACK`。大量 `SYN_RECV` 说明有人在发 `SYN` 但不完成握手（`SYN Flood` 攻击），或者 `accept` 队列满了处理不过来 
    - **`CLOSE_WAIT`**：`TCP` 四次挥手中被动关闭方的中间状态。对端发了 `FIN` 请求关闭连接，本机回应了 `ACK` 后进入 `CLOSE_WAIT`，等待本机应用调用 `close()`。大量 `CLOSE_WAIT` 说明应用层没正确关闭 `socket`——— 属于程序 `Bug`，不是攻击
    - **`ESTABLISHED`**：`TCP` 连接已建立，正在传输数据。这个状态的数量就是*当前正在维持的连接数*。如果 `ESTABLISHED` 持续增长不回落，说明连接泄漏
    - **`连接泄漏`**：程序不断建立新连接但用完不释放（不调用 `close()`），导致连接数持续增长直到耗尽系统资源（端口、文件描述符、内存）。和内存泄漏类似，只是泄漏的是连接而不是内存


## 🤔 iowait 高会导致什么问题？如何解决？  
- **`iowait` 是 `CPU` 在等待磁盘 `IO` 完成时处于空闲状态的时间占比。`iowait` 高不一定是磁盘有问题，但它一定说明 `CPU` 在空等磁盘——CPU 有计算能力但用不上，因为数据还没从磁盘读回来。这会导致系统吞吐量下降、响应延迟升高。**
    - **`iowait` 高带来的具体问题**
        - **`CPU` 资源被*浪费***：`iowait` 高的那部分 `CPU` 时间既没做计算也没处理其他任务，相当于 `CPU` 在空转等数据。但注意 `iowait` 只统计 `CPU` 空闲时等待 `IO` 的时间，如果 `CPU` 本身已经被别的进程占满，`iowait` 反而会看起来不高——因为 `CPU` 根本没空去等 `IO`
        - **应用响应变慢、卡顿**：任何需要读写磁盘的操作都会变慢（读取配置文件、写入日志、数据库查询、加载静态资源）。用户的直观感受就是系统反应迟钝
        - **连锁放大效应**：一个进程卡在 `IO` 等待上占着资源不放（如数据库连接、文件锁），其他进程也跟着受影响，导致问题范围扩大
        - **交互式操作体验恶化**：`SSH` 敲命令都可能卡住（因为要读取 bash 的历史文件、加载命令补全配置），运维人员排查问题时自己也被拖慢，形成"排查困难"的恶性循环

    - **如何定位 `iowait` 的根因**
        - **确认 `iowait` 的来源：`iostat -xz 1` 持续观察。关键看几个指标：**
            - **%util**：磁盘忙碌比例。`100%` 不一定代表有问题（`NVMe` 在多队列下 `100%` 仍然正常），要结合 `await` 判断
            - **await**：`IO` 平均响应时间。机械盘超过 `15ms`、`NVMe` 超过 `2ms` 说明有排队
            - **r/s + w/s**：每秒读写次数。检查是否达到磁盘标称的 `IOPS` 上限（机械盘约 `100 ~ 200 IOPS`，`SATA SSD` 约 `1` 万 ~ `10` 万 `IOPS`，`NVMe` 约 `50` 万 ~ `100` 万 `IOPS`）
            - **rMB/s + wMB/s**：每秒读写带宽。是否打满磁盘或存储链路带宽

        - **确认是谁在写/读**：`iotop` 按 `IO` 大小排序，直接看到哪个进程在大量读写。或者 `pidstat -d 1` 看各进程的磁盘读写统计
        - **快速分类法**：`iostat -xz 1` 看 `avgqu-sz`（平均等待队列长度），如果队列长度远大于 1 说明请求在排队等待，磁盘确实忙不过来；如果队列很短但 `await` 很高，说明磁盘本身响应慢（硬件故障或降级）

    - **解决方向**
        - **硬件层面**：机械盘换固态盘是最直接的办法。如果已经是 `SSD/NVMe`，检查存储链路（`SAS` 线缆松动、`HBA` 卡故障、`RAID` 卡缓存策略不对）
        - **应用层面**：检查是否有不合理的读写模式——频繁 `fsync`（数据库的 `sync_binlog=1` 且磁盘性能跟不上）、大量随机小文件读写（日志系统每写一条就 `open/close` 一次）、没配置读写缓存
        - **系统层面**：`I/O` 调度器调到合适的模式（`NVMe` 用 `none`、机械盘用 `mq-deadline`）；调整文件系统挂载参数（`noatime` 减少 `atime` 写入）；增大脏页回写阈值（`vm.dirty_background_ratio` 和 `vm.dirty_ratio`）让脏页攒多点再一起刷盘，减少 `IO` 次数
        - **架构层面**：把频繁读写操作分离到独立的磁盘或存储节点（数据盘和日志盘分离、MySQL 的 ibdata 和 binlog 分开放）；热点数据上缓存（Redis 缓存数据库查询结果）

- **协助记忆**
    - `iowait` 高 = `CPU` 在等磁盘，`CPU` 想干活但数据还没从磁盘读回来
    - 排查两步走：`iostat -xz 1` 看磁盘到底忙不忙，`iotop` / `pidstat -d` 看谁在读写
    - 三个方向解决：换硬件（机械换固态）、调应用（减少 fsync / 日志合批）、上缓存（Redis / 本地缓存扛热点）

- **进阶思考**
    - **`iostat` 里 `%util` 100% 但 `await` 很低（< 1ms），这算 `iowait` 高吗？**
        - 不算。这是高速 `NVMe` 盘的正常表现——设备在多队列并发下确实每个瞬间都在处理请求，但因为响应极快队列还没堆积，所以 `iowait` 不会高。`%util` 在这个场景下不是可靠的瓶颈指标，应该看 `avgqu-sz` 和 `await` 的趋势

    - **`iowait` 高了但 `iostat` 显示所有磁盘都很空闲，这是怎么回事？**
        - 说明 `iowait` 来自其他存储设备——比如 `NFS` 挂载的远程存储、FUSE 文件系统（s3fs、GlusterFS）、或者某些虚拟化平台的后端存储（如 VMware 的虚拟磁盘）。这些设备不会出现在 `iostat` 的常规磁盘列表中。排查方法：`strace -e trace=statfs pwd` 看哪个文件系统调用慢，或者逐个卸载非本地文件系统观察 `iowait` 是否下降

    - **数据库服务器 iowait 很高且 avgqu-sz 持续大于磁盘队列深度，怎么办？**
        - 说明磁盘 `IOPS` 或带宽已经成为瓶颈，软件调优只能缓解不能根治。短期措施：将数据库的日志和数据分离到不同的物理磁盘（MySQL 的 binlog 和数据目录分开放、InnoDB 的 redo log 放在独立的 NVMe 上）；检查是否有全表扫描或大量排序导致临时文件写入磁盘，优化慢查询。长期方案：增加内存（让更多热数据留在 `buffer pool` 而不是频繁刷盘）或换更高 IOPS 的存储设备

    - **一台服务器上用的消费级 NVMe（如 Samsung 990 PRO）运行 AI 推理等高 IO 场景，频繁出现"掉盘"———盘从系统消失，必须拔掉所有电源等待主板放电后才能恢复。换成企业级 NVMe 能解决吗？**
        - 大概率能。消费级和企业级 `NVMe` 在高温处理上有关键区别：消费级在 `80~85°C` 触发热保护时直接断开 `PCIe` 总线，且恢复机制简单粗暴——依赖完全掉电复位；而企业级在接近温度阈值时会先主动降速（throttling），即使触发保护也支持完整的错误恢复机制（如 `PCIe hot reset`、`FLR` 功能级复位），不需要断电放电就能恢复。此外企业级的持续写入性能更稳定，不会因为高队列深度写入就快速升温。

    - **在暂时不能换盘且散热条件受限的情况下，软件层面有哪些手段可以降低掉盘风险？**
        - 可以尝试几种手段。
            - 一是通过 `nvme set-feature /dev/nvme0 -f 0x04 -v 1` 将 NVMe 限制在较低功耗状态，减少发热；
            - 二是定期执行fstrim（TRIM），减少需要后台 GC 产生的额外 IO 和温升；
            - 三是通过 `nvme-cli` 或系统级 IO 限流限制最大吞吐量。此外监控 NVMe 温度并主动告警——温度达到 70°C 时预先降低 IO 负载，而不是等到盘自己断开。但这些手段是妥协不是根治。对于持续高 IO 的场景，软件限流容易设得保守影响业务或设得激进防不住掉盘。长期方案仍是换企业级 NVMe 或加装主动散热。


## 🤔 数据库服务器 load 100+，能直接重启吗？该怎么处理？  
- **不能直接重启。** 直接重启数据库服务器是线上运维中最危险的误操作之一——它杀死了所有正在执行的查询和未提交的事务，数据库重新启动后需要做崩溃恢复，对于大库可能耗时几十分钟甚至更久，直接拉长了故障时间窗口，而且根因没解决，重启后很快又会恢复到同样状态。

    - **为什么不能直接重启**
        - **正在执行的事务全部回滚**：如果你有长事务或大批量更新跑到一半突然被 `kill`，回滚操作在数据库重启后由 `InnoDB` 做崩溃恢复时执行，这个过程可能比正常执行还要慢，重启后负载不但不会下降反而可能因为回滚导致二次冲高 
        - **崩溃恢复耗时长**：`MySQL InnoDB` 需要扫描 `redo log` 做前滚恢复，已经提交但还没刷入数据文件的事务要重放。恢复时间和 `buffer pool` 大小、`redo log `总量、上次 `checkpoint` 位置有关，100GB+ 的数据库可能耗 10~30 分钟甚至更长。这段时间内数据库完全不可用
        - **重启不解决根因**：负载高的原因可能是慢查询、锁等待、数据量膨胀、硬件瓶颈，重启只是清空了当前连接和进程状态，但导致问题的 SQL 还会再跑回来。第二天同样的时间点问题还会再次出现

    - **应该先做什么（按优先级）**
        1. **先应急止血，不要中断数据库**
            - 尝试 `SSH` 连接（如果 `SSH` 也卡住，通过带外管理或 `sshd` 优先级保底方案接入）
            - 进入数据库但不执行重查询：`mysql -A -e "SHOW PROCESSLIST;"`（-A 跳过自动补全表名）看当前都在跑什么 `SQL`
            - 找到消耗最大的查询，`KILL <thread_id>` 一个一个杀掉。杀掉一个大查询后负载通常会明显下降，但注意如果是大批量写入在跑，杀掉后数据库需要回滚这个事务，回滚期间负载可能不会立刻下降

        2. **分析负载来源**
            - 通过 `SHOW FULL PROCESSLIST` 看查询类型、执行时间、状态
            - 看 `SHOW ENGINE INNODB STATUS\G` 中 `LATEST DETECTED DEADLOCK` 和 `TRANSACTIONS` 部分确认是否有锁等待或死锁
            - 看系统层面：top 看 CPU 或 IO 谁高，`iostat -xz 1` 看磁盘是否饱和
            - 看是否由定时任务（如半夜的数据统计、备份、全表扫描）触发

        3. **如果必须重启，要优雅地重启**
            - 先尝试 `systemctl stop mysql` 或 `mysqladmin shutdown`，让数据库自己完成正常的关闭流程（刷脏页、写 checkpoint、关闭 redo log）
            - 如果正常关闭失败（卡住了），再考虑 `kill -9`，但要接受由此引发的崩溃恢复代价
            - 重启前需要做的几件事：
                - 确认备份是否可用（全量 + binlog）
                - 确认主从位置（如果是主库，考虑是否先切换从库而不是重启主库）
                - 记录当前数据库状态（`SHOW STATUS`、`SHOW ENGINE INNODB STATUS`）方便后续复


- **协助记忆**
    - 不能直接重启：杀事务要回滚、恢复要重放、根因没解决还会回来
    - 先 KILL 大查询止血：`mysql -A -e "SHOW PROCESSLIST;"` 找到大查询逐个 kill
    - 再看锁和 IO：锁等待用 `SHOW ENGINE INNODB STATUS`，IO 瓶颈用 iostat -xz 1
    - 最后才是优雅重启：`mysqladmin shutdown` 而不是直接 `power off`
    - 重启前确保有条件恢复：确认有备份、确认是否应切主而不是重启

- **进阶思考**
    - **如果数据库本身已经失去响应，`mysql -A` 也连不进去，还能怎么处理？**
        - 数据库连不进去通常是因为连接数打满或 `MySQL` 内部线程全部卡在 `IO` 等待或锁上。尝试 `mysql -A -u root -h 127.0.0.1 --port=3306 --skip-ssl` 通过 `TCP` 而不是 `socket` 连接；或者在 `/etc/my.cnf` 中 `skip-networking` 未开启的情况下通过 `gdb -p <mysqld_pid>` 注入一个额外连接，执行 `KILL`。如果这些都不行，查看系统日志 `/var/log/mysql/error.log` 判断是资源耗尽还是 `InnoDB` 崩溃，这时只能考虑重启。但重启前必须先确认备份是否可用

    - **如果这是从库，负载 `100+` 但主库正常，应该怎么处理？**
        - 从库相对容易处理，因为停止服务对业务影响较小。可以先将从库从负载均衡中摘掉（停流量），然后 `STOP SLAVE`; 停止复制。停止后观察负载是否下降——如果下降说明是复制线程的写入压力导致的，检查 `SHOW SLAVE STATUS\G` 中的 `Seconds_Behind_Master`，确认复制延迟情况和是否有大事务在主库执行。如果摘掉流量后负载仍然很高（说明是查询请求导致的而非复制），则排查是否有慢查询或 全表扫描在打从库。从库的处理原则是：优先摘流量、停复制，而不是急着重启

    - **数据库负载 `100+` 的情况下，扩大 `buffer pool` 或者加 `CPU` 能解决吗？**
        - 不一定。先确认瓶颈类型：如果是大量慢查询导致的 `CPU` 打满（`top` 显示 `us` 很高），加 `CPU` 能提升查询吞吐量但只是应急，不优化 `SQL` 会继续堆积；如果是 `buffer pool` 不够导致频繁刷脏页和磁盘 `IO` 打满（`top` 显示 `wa` 很高），加 `buffer pool` 可能缓解刷盘压力；但如果是某个全表扫描没索引导致的，加内存和 `CPU` 都没用，加多少资源都会被这一个查询吃光。先看瓶颈类型再决定扩容方向，而不是看到负载高就直接加配置

## 🤔 Linux 各目录有什么作用？  
- **`Linux` 的目录结构遵循 `FHS`（`Filesystem Hierarchy Standard`，文件系统层次结构标准），各目录有明确的职责划分。理解了每个目录归什么，在处理磁盘空间满、权限异常、配置文件找不到等问题时就能快速定位。**
    | 目录路径             | 主要用途            | 核心内容 / 示例                                                                 | 特点与注意事项                                              |
    | -------------------- | ------------------- | ------------------------------------------------------------------------------- | ----------------------------------------------------------- |
    | **`/`（根目录）**    | 所有目录的起点      | 下挂载所有子目录                                                                | 根分区满了影响极大，可能导致 SSH 都无法登录。               |
    | **`/bin`、`/sbin`**  | 基础命令            | `/bin`：`ls`、`cp`、`cat` 等通用命令 <br> `/sbin`：`fdisk`、`mkfs` 等管理员命令 | 现代 Linux 中通常是软链接，指向 `/usr/bin` 和 `/usr/sbin`。 |
    | **`/etc`**           | 系统级配置文件      | `sshd_config`、`passwd`、`fstab`、`nginx/` 等                                   | 运维最频繁目录之一，配置错误常导致服务无法启动。            |
    | **`/home`**          | 普通用户家目录      | 每个用户单独的子目录（如 `/home/username`）                                     | 建议单独分区挂载，重装系统时可保留用户数据。                |
    | **`/root`**          | root 用户家目录     | 系统超级管理员的个人目录                                                        | 与 `/home` 分开，确保根分区异常时 root 仍能登录。           |
    | **`/var`**           | 动态变化的数据      | 日志（`/var/log`）、数据库数据（`/var/lib/mysql`）                              | **极易爆满**，应用日志未配置 `logrotate` 易撑爆分区。       |
    | **`/tmp`**           | 临时文件目录        | 存放各类应用产生的临时数据                                                      | 所有用户可读写，系统重启或定期可能被自动清理。              |
    | **`/dev`**           | 设备文件目录        | 硬盘（`/dev/sda`）、终端、`/dev/urandom` 等                                     | Linux“一切皆文件”的体现，由 `udev` 动态管理。               |
    | **`/proc`**          | 虚拟文件系统        | 进程信息（`/proc/<PID>`）、`meminfo`、`cpuinfo`                                 | 内核运行状态窗口，**不占用实际磁盘空间**。                  |
    | **`/sys`**           | 虚拟文件系统        | 硬件设备信息、内核参数、电源管理                                                | 比 `/proc` 更结构化，二者当前在系统中共存。                 |
    | **`/usr`**           | 软件及共享资源      | 命令（`/usr/bin`）、库文件（`/usr/lib`）、头文件等                              | 包管理器安装软件的主要分布地；源码编译默认在 `/usr/local`。 |
    | **`/usr/local`**     | 自定义/源码安装软件 | `/usr/local/bin`、`/usr/local/lib` 等                                           | 与系统软件隔离，防止包管理器升级时覆盖自定义软件。          |
    | **`/opt`**           | 第三方大型软件      | Oracle、MATLAB、Google Chrome 等                                                | 软件自行管理内部文件结构，不依赖系统包管理器。              |
    | **`/run`**           | 运行时状态数据      | 进程 PID 文件、Unix socket 等                                                   | 挂载于内存（`tmpfs`），开机创建、关机清除，响应极快。       |
    | **`/boot`**          | 系统引导与内核文件  | 内核（`vmlinuz-*`）、`initramfs`、GRUB 配置                                     | 单独分区若设置过小，内核频繁更新可能导致空间不足。          |
    | **`/mnt`、`/media`** | 设备挂载点          | `/mnt`：手动挂载<br>`/media`：自动挂载（如 U 盘）                               | 临时挂载外部存储设备时使用。                                |
    | **`/srv`**           | 系统服务数据        | 服务数据（如 `/srv/www`、`/srv/ftp`）                                           | 部分 Linux 发行版用于存放特定网络服务的数据。               |
    | **`/lib`、`/lib64`** | 基础共享库          | 运行 `/bin` 和 `/sbin` 命令所需的依赖库                                         | `/lib64` 存放 64 位库文件，`/lib` 常为其软链接。            |

- **协助记忆**
    - **`/etc`**：调参数的地方，配置文件全在这
    - **`/var`**：日志往这写，写满了不配置 `logrotate` 就等着查
    - **`/home`**：用户的家，重装系统靠它保留数据
    - **`/boot`**：内核和引导，空间小但要留足更新余量
    - **`/proc` 和 `/sys`**：内核的两个窗口，一个看进程状态，一个看设备信息

- **进阶思考**
    - **发现 `/` 分区满了，但 `/home` 和 `/var` 是独立分区且还有空间。这种情况下 `/` 分区为什么会被写满？如何快速释放空间？**
        - 最常见的原因是 某个进程写了一个大文件到根分区下的某个目录——常见嫌疑路径有：`/opt` 下的日志（第三方软件自己写日志不按 `/var/log` 走）、`/root` 下的备份文件、`/tmp` 下的临时文件、`Docker` 的 `overlay` 层如果在默认的 `/var/lib/docker` 但 `/var` 没有独立分区，也会占用 `/` 的空间。排查方式：`du -h --max-depth=1 / | sort -hr | head -n 10` 从根目录一层层往下找，定位到具体目录后再处理

    - **`/usr` 和 `/usr/local` 的区别常常被忽视。如果用包管理器安装的软件和编译安装的软件混在一起，会有什么问题？**
      - 包管理器在覆盖 `/usr` 下的文件时不会通知你。如果你编译安装了一个新版 `Nginx` 到 `/usr/local/nginx`，然后包管理器升级系统级 `Nginx`（在 `/usr/sbin/nginx`），两个版本各自运行不会冲突。但如果你编译安装时覆盖了 `/usr/bin` 下的文件，包管理器下次更新时先检查文件校验和，发现文件被修改过就会报冲突或直接覆盖掉你的版本。所以编译安装的软件一定要装到 `/usr/local` 下，避免和系统包管理器冲突

    - **系统中多个 `bin` 目录（`/usr/local/bin`、`/usr/bin`、`/bin` 等），如果存在同名二进制文件，调用时按什么优先级执行？**
        - 取决于 `PATH` 环境变量 中的目录顺序。`Shell` 从左到右遍历 `PATH`，先找到的目录优先执行。以 `Debian/Ubuntu` 一般用户为例，`PATH` 默认是 `PATH=/usr/local/bin:/usr/bin:/bin:...`，所以优先级为 `/usr/local/bin` > `/usr/bin` > `/bin`。这条规则保证了编译安装到 `/usr/local/bin` 的软件可以覆盖包管理器安装的同名软件。需要注意两点：一是 `/bin` 已是 `/usr/bin` 的软链接，两者实际指向同一位置；二是 `Shell` 会缓存命令路径，如果你刚安装了新版命令到高优先级目录，需要先 `hash -r` 清除缓存才能用到新版。`type -a <command>` 可查看所有匹配路径及顺序


## 🤔 线上一台服务器需要增加硬盘，该怎么操作？  
- **给线上服务器加硬盘的核心原则是在线操作，不中断业务。多数硬盘接口（SATA、SAS、NVMe）支持热插拔，新增硬盘不会被系统自动识别和使用，需要走一套完整的流程：硬件安装 → 系统识别 → 分区或创建 PV → 格式化 → 挂载到指定目录。**

    - **硬件安装前确认几件事**
        - 确认服务器是否支持硬盘热插拔（大部分服务器背板的硬盘托架支持，但机箱内部直插的 M.2 接口通常不支持）
        - 确认新硬盘的接口类型和尺寸与服务器匹配（SATA / SAS / NVMe / U.2 / M.2）
        - 如果是虚拟机，在虚拟化平台（VMware / Proxmox / Xen）上直接加虚拟磁盘，不需要物理操作
        - 如果是云服务器，在云控制台扩容云盘后还需要在操作系统内扩容分区和文件系统，方式略有不同

    - **物理安装或云盘挂载后，系统层面识别新硬盘**
        - 如果硬盘是热插拔的，插入后执行 · 检查是否出现新设备。如果没有出现，尝试 `echo "- - -" > /sys/class/scsi_host/host0/scan` 触发 `SCSI` 总线重新扫描。也有更通用的方式：`for host in /sys/class/scsi_host/host*; do echo "- - -" > $host/scan; done`
        - 确认出现后，`fdisk -l` 或 `lsblk` 查看新硬盘的设备名（如 `/dev/sdb`、`/dev/nvme1n1`）

    - **创建分区并使用文件系统**
        - 直接使用裸设备不分区：`mkfs.ext4 /dev/sdb`。简单直接，但后续无法在该盘上再划其他分区。大容量盘通常建议分区后再格式化
        - 建立分区后格式化：`fdisk /dev/sdb` 创建分区 → `mkfs.ext4 /dev/sdb1`
        - 或直接上 LVM，加入已有 VG 或创建新 VG/LV，方便后续在线扩展
        - 如果原来盘上已有数据（迁移或扩容场景），需要先确认原文件系统类型再操作，避免误覆盖

    - **挂载到指定目录**
        - `mkdir /data2 && mount /dev/sdb1 /data2`
        - 设置开机自动挂载：在 `/etc/fstab` 中添加一行，建议使用 `UUID` 而不是设备名（`/dev/sdb1`），因为设备名在重启后可能变化，`UUID` 是唯一且不变的。用 `blkid /dev/sdb1` 获取 `UUID`
        - `mount -av` 测试 `fstab` 配置是否有误，如果出错需要及时修正避免下次重启卡住

    - **如果是替换旧盘或数据迁移场景**
        - 先将旧盘数据备份到其他位置，移除旧盘，插入新盘。新盘分区并格式化。如果是系统盘，需重装系统或使用 `dd` / `rsync` 将原系统迁移过去，涉及引导修复，操作相对复杂
        - 直接将数据从旧盘拷贝到新盘（`rsync -av /mnt/old /mnt/new`）
        - 卸下旧盘，将新盘挂载到原路径，分配原有的权限和属主，确保服务读取正常

- **协助记忆**
    - **六步走**：插盘 → 扫盘 → 分区 → 格式化 → 挂载 → 写 fstab
    - **两个坑**：fstab 用 UUID 不要用设备名；mount -a 测 fstab 再重启

- **进阶思考**
    - **新硬盘插上后 `lsblk` 看不到，可能是什么原因？**
        - 最常见的原因是没有触发 `SCSI` 总线重新扫描。热插拔后需要通知内核重新扫描总线，执行 `echo "- - -" > /sys/class/scsi_host/hostN/scan`，其中 `hostN` 对应新盘连接的那个控制器。如果扫描后仍看不到，可能的原因包括：硬盘接口没插好、硬盘供电线没接、该接口不支持热插拔（如机箱内部直插的 `SATA` 口）、或者新盘是出厂状态需要初始化。`PCIe` 通道的 `NVMe` 盘热插拔支持更差一些

    - **`fstab` 如果写错了导致系统无法启动，怎么修复？**
        - 系统启动时如果某个挂载点失败，可能会进入紧急模式（`Emergency Mode`）或者卡在等待挂载的提示（具体看 `nofail` 选项是否配置）。如果是根分区 `fstab` 写错系统根本起不来——进 `single user` 模式或 `rescue` 模式，把 `fstab` 中错误的行注释掉或修正回来，重启即可恢复

    - **生产环境中挂载新硬盘，在写入 `fstab` 时有哪些实践经验，可以避免因为误操作导致启动卡住？**
        - 有几点经验。
            1. 在 `fstab` 中为所有非关键数据盘加上 `nofail` 选项，即使挂载失败系统也会跳过该条目继续启动到正常模式；
            2. 在挂载前先用 `blkid` 确认 `UUID`，`fstab` 中只使用 `UUID` 而非设备名；
            3. 新增完 `fstab` 条目后立即执行 `mount -a`，如果这条命令卡住或报错，说明 `fstab` 有误，及时修正，不要直接重启；
            4. 不建议在生产环境将关键数据盘以外的路径写入 `/etc/fstab`，有时使用 `autofs` 按需挂载更灵活，能在故障时减少排查范围

## 🤔 某一天突然发现 Linux 系统文件只读，如何排查修复？  
- **文件系统突然变成只读，通常是内核检测到文件系统有 I/O 错误后主动挂起写入以防止数据进一步损坏，而不是系统自己变成了只读模式。这是一种保护机制——宁可让你写不进去，也不能让你在损坏的基础上继续写导致数据彻底不可恢复。**

    - **第一步**：确认是哪个挂载点变成了只读
        - `mount | grep rw` 看哪些还是读写，`mount | grep ro` 看哪些变成了只读
        - `df -h` 如果卡住，说明有挂载点对应的存储已经无响应

    - **第二步**：看内核日志确认根因
        - `dmesg | tail -50` 或 `journalctl -k --since "5 min ago"`，重点看以下关键字：
            - **磁盘 `I/O` 错误**：`I/O error`、`buffer I/O error`、`failed command`、`READ FPDMA QUEUED`（这通常是硬盘物理坏道或 `SATA` 线缆松动）
            - **文件系统错误**：`ext4-fs error`、`EXT4-fs`、`corrupt`、`journal has aborted`（文件系统的元数据或日志损坏）
            - **存储链路异常**：`connection timed out`、`link down`、`reset`（`iSCSI` 或 `FC` 链路断开）
        - 根据日志内容就能判断是硬件问题、文件系统损坏还是远端存储断开

    - **第三步**：根据原因采取对应的修复措施
        - **硬盘 `I/O` 错误（物理坏道 / 线缆松动 / 硬盘即将损坏）**
            - 如果日志明确显示某个 `SATA/SAS/NVMe` 盘出现 `I/O error`，用 `smartctl -a /dev/sdX` 查看硬盘健康状态（`Reallocated_Sector_Ct`、`Pending_Sector`、`UDMA_CRC_Error`）确认是否坏道或线缆问题
            - 临时恢复读写：如果确认硬盘还能读，但不想重启，可以 `mount -o remount,rw <挂载点>` 强制重新挂载为读写模式。但要注意，如果触发只读的设备错误仍然存在，强制 `remount rw` 后很快又会变回只读
            - 根治方案：更换硬盘并从备份恢复数据，或者如果有 `RAID` 则更换坏盘让它自动重构
        
        - **文件系统日志损坏**
            - 如果日志中出现了文件系统相关的错误，系统重启后会自动重放日志尝试修复。如果修复不了，需要手动 `fsck`
            - 如果根分区只读且无法写入，重启后自动 `fsck` 可能会修复，必要时进救援模式执行 `fsck -y /dev/sdX`，配合 `-C` 显示进度，大分区耗时较长需要耐心等待

        - **存储链路断开（iSCSI / NFS / FC）**
            - 检查网络或光纤链路，恢复后重新连接远端存储，然后 `mount -o remount,rw <挂载点>` 恢复读写
            - 如果远端存储短期内恢复不了，`umount -f -l <挂载点>` 强制卸载

- **协助记忆**
    - 文件突然只读，不是系统坏了，是内核帮你踩了刹车
    - `dmesg` 看日志判断是盘坏了、文件系统坏了还是远端断了
    - 不要急着 `remount rw`：先确认根因，如果是硬盘坏道，强制 `remount` 只会让更多数据写坏
    - 核心理念：先查明原因再处理，而不是先恢复读写再查问题

- **进阶思考**
    - **根分区变成了只读，`dmesg` 显示大量 `I/O error`，但 `smartctl` 结果显示硬盘健康状态正常。可能是什么原因？**
        - 硬盘本身健康不代表链路一定正常。先查看 `smartctl` 中的 `UDMA_CRC_Error_Count` 这个数值，如果它持续增长，说明 `SATA/SAS` 数据线或接口有电气问题，不是盘片损坏。更换数据线通常就能解决。另外检查电源供电，如果供电不稳定也可能导致磁盘写入失败。如果 `UDMA_CRC_Error_Count` 不高，还有可能是主板的 `SATA` 控制器或 `HBA` 卡出现问题，可以用 `lspci` 确认控制器型号，更新固件或更换接口再试

    - **文件系统只读，但机器上跑着重要的业务不能随便重启。有没有办法在不重启的情况下恢复？**
        - 可以尝试 `mount -o remount,rw /挂载点` ，但效果取决于只读的原因。如果触发只读的硬件错误已经消失（如瞬时的线缆接触不良后又恢复正常），`remount rw` 会成功，业务继续正常写入。如果硬件错误是持续的（坏道一直存在），`remount rw` 后内核很快又会因为写入失败再次将文件系统切换为只读。还有一种比较少见的情况：文件系统日志损坏但没到内核主动只读的程度，这时可以用 `fsck -n /dev/sdX` 只检查不修复，确认损坏范围后再评估是否需要停机修复


## 🤔 Keepalived 出现脑裂，是什么原因？  
- **脑裂（`Split-brain`）是指 `Keepalived` 集群中两个（或多个）节点同时认为自己是 `Master`，都绑定了 `VIP` 并对外提供服务，导致流量混乱、数据可能不一致的问题。脑裂的核心原因在于 `VRRP` 多播心跳通信中断，但双方进程各自运行正常，无法感知对方的存在。**

    - **脑裂产生的根本原因**
        - **`VRRP` 多播通信中断**：两台节点之间的 `VRRP` 心跳（多播到 `224.0.0.18`）无法到达对方，各自收不到对方的通告，都认为对方已经死了，于是各自将自己提升为 `Master` 并绑定 `VIP`
        - 双方 `Keepalived` 进程本身正常，`iptables` 允许所有流量，系统资源充足——只是它们之间用来确认"对方还活着"的那条通道断了

    - **最常见的触发场景**
        - **防火墙拦截了 `VRRP` 多播**：`iptables` 默认规则没有放行 `VRRP` 协议，或者添加了 `-j DROP` 的规则时，`VRRP` 心跳被拦截
            - `VRRP` 使用 `IP` 协议号 `112`（不是 TCP/UDP），目标多播地址 `224.0.0.18`。放行的 `iptables` 规则是：`iptables -A INPUT -p 112 -d 224.0.0.18 -j  ACCEPT`

        - **交换机 `IGMP Snooping` 配置错误**：交换机的 `IGMP Snooping` 如果启用了但没有正确配置，可能错误地认为 `224.0.0.18` 这个多播组没有订阅者，从而丢弃 `VRRP` 多播包
        - **物理链路中断**：两台 `Keepalived` 节点之间的直连网线或交换机端口故障，但两台节点各自仍然正常运行
        - **`router_id` 配置冲突**：两台节点的 `keepalived.conf` 中配置了相同的 `router_id`，`VRRP` 协议的行为会变得不可预测，有时也会导致脑裂
        - **多播地址被占用或冲突**：同一广播域内其他设备也在使用 `224.0.0.18` 发送数据，干扰 `VRRP` 心跳


    - **如何排查确认是脑裂**
        - 在备机上执行 `ip addr show dev eth0 | grep inet`，如果两台机器上都绑定了同一个 `VIP`，说明脑裂已经发生
        - 抓包确认 `VRRP` 多播是否能收到：`tcpdump -i eth0 -n vrrp`。如果备机能收到 `Master` 的 `VRRP` 通告，说明通信正常；如果收不到，说明多播路径有问题
        - 检查 `iptables` 规则：`iptables -L INPUT -v | grep 112` 是否有拦截
        - 检查 `keepalived.conf` 中的 `router_id` 是否唯一


    - **如何恢复和预防**
        - **紧急恢复**：在备机上 **systemctl stop keepalived**，备用节点停止宣告自己为 `Master`，`VIP` 会回到真正的 `Master` 上
        - **长期预防**：
            - 在 `iptables` 中显式放行 `VRRP` 多播：`-A INPUT -p 112 -d 224.0.0.18 -j ACCEPT`
            - 交换机上禁用 `IGMP Snooping` 或为 `VRRP` 多播配置静态 `IGMP` 条目
            - 使用双心跳链路：两条独立的物理线路或交换机做堆叠，避免单条链路故障就导致脑裂
            - 配置 `nopreempt` 非抢占模式，避免角色抖动

- **协助记忆**
    - 脑裂原因是一句话：两台都说*我是 Master*——不是因为两边都错了，而是中间的通信断开了
    - 排查三步：看两台是否都有 `VIP` → 抓包看 `VRRP` 是否互通 → 检查 `iptables` 和交换机
    - 恢复一招：备机停掉 `Keepalived`，`VIP` 自动回到真 `Master`

- **进阶思考**
    - **`Keepalived` 脑裂和 `track_script` 没配导致的故障有什么区别？**
      - 这是两个不同维度的故障。脑裂是两台都在争 `Master`，`VIP` 在两头都绑了；`track_script` 没配， 是唯一 `Master` 上的业务进程已经挂了，但 `Keepalived` 自身还活着，继续发心跳宣告自己是 `Master`，`VIP` 不会飘移，客户端连上去得不到服务。简单区分：脑裂是两台抢 `VIP`，业务可能两边都在处理；`track_script` 没配是一台占着 `VIP` 但服务已死

    - **如何通过监控自动发现脑裂？**
      - 在每台节点上通过脚本检测本地是否绑定了 `VIP` + 是否有其他节点也宣告了同一个 `VIP`。检测方式：`arping -I eth0 -c 3 <VIP>` 看是否有多个 `MAC` 地址回复同一个 `VIP`。或者通过外部监控定时检测 `VIP` 的 `MAC` 地址是否发生了变化，如果短时间内多次切换或者出现多 `MAC` 响应同一个 `VIP`，触发脑裂告警
      - `ping` 网关, 如果自己不能 `ping` 通， 那么就是自己离线了， 自己就肯定不能晋升 `Master`, 先停掉自己在说，如果可以 `ping` 通， 那么就是自己在线，在通过其他方式检测其他节点在线状态，在判定是否要晋升。

## 🤔 你之前使用过哪些监控系统，都分别监控哪些指标？  
- **监控系统的选型通常和团队规模、技术栈绑在一起，没有"最好"只有"最适合"。从最常用的几类覆盖不同维度的方案来说？**

    - **`Prometheus` + `Grafana`（最常用，云原生生态事实标准）**
        - 覆盖面：基础设施（CPU/内存/磁盘/网络）、中间件（Nginx/MySQL/Redis）、应用层（QPS/错误率/延迟）、业务指标（订单量/注册数）
        - 指标采集方式：`Exporter` 模式——`node_exporter` 采主机指标，`mysqld_exporter` 采 `MySQL`，`blackbox_exporter` 做 HTTP/DNS/TCP 探测
        - 告警：`Prometheus` 自带的 `Alertmanager` 根据规则触发告警，支持分组、抑制、静默
        - 适用场景：容器化和云原生环境，`Kubernetes` 生态的标配。中小团队通常从 `node_exporter` + `cadvisor`（容器监控）开始搭

    - **`Zabbix`（传统运维环境的老牌选择）**
        - 覆盖面：基础设施（CPU/内存/磁盘/网络/硬件温度/风扇转速）、网络设备（交换机/路由器/防火墙通过 SNMP）、服务可用性（端口/进程/日志关键字）
        - 特点：自带完备的告警、报警升级、报表功能，不需要像 `Prometheus` 那样额外搭 `Grafana`。适用于传统 `IDC 机房`，网络设备多、需要 SNMP 监控的场景
        - 适用场景：偏传统 `IDC` 机房，网络设备和服务器并存，运维团队需要开箱即用的全套方案
        - 和 `Prometheus` 的差别：`Zabbix` 适合传统机房的"设备级"监控，`Prometheus` 适合云原生的"指标级"监控。`Zabbix` 知道哪个设备死了，`Prometheus` 知道哪个进程的 `P99` 延迟变了

    - **`ELK` / `Loki`（日志集中收集）**
        - 覆盖面：应用日志错误率、关键日志关键字告警、系统日志（auth.log / syslog）、Nginx 访问日志统计
        - 适用场景：故障排查时需要跨主机查询日志、安全审计需要日志保留、Nginx 访问日志聚合做请求链路分析
        - `ELK` 和 `Loki` 的区别：`ELK` 全量索引日志内容，查询灵活但存储成本高；`Loki` 只索引元数据（时间戳 + 标签），日志内容本身存对象存储，查询性能不如 `ELK` 但有成本优势

- **协助记忆**
    - 监控三件套：`Prometheus` + `Grafana`（指标）、`ELK/Loki`（日志）、`Alertmanager`（告警收敛）
    - `Zabbix` 更重设备管理，使用 `SNMP` 进行细粒度监控但设备适应面广，而 `Prometheus` 更重建模和自动化发现

- **进阶思考**
    - **`Prometheus` 的告警延迟（`Alertmanager` 的 `group_wait` / `group_interval`）经常导致告警很久才发出来，这怎么优化？**
        - A：这通常是业务告警和基础设施告警混用一个路由导致的。一个可行的思路是：创建两个路由规则——P0 告警（服务挂了、磁盘满了等）走最短等待时间，禁止聚合；P1 及以下告警（CPU 突增、连接数升高）使用默认的聚合规则，以减少告警风暴。Prometheus 不建议作为全闪存式的实时告警系统；对于几秒钟内就需要感知的事情，更适合使用具备回调能力的独立工具来承担

    - **`Zabbix` 和 `Prometheus` 能不能混用？什么场景下需要混用？**
      - 可以，且在一些团队里确实是混合使用的。`IDC` 机房服务器和网络设备的硬件健康监控（温度、风扇、电源）走 `Zabbix SNMP`；运行在云上的应用层监控（请求延迟、错误率、自定义业务指标）走 `Prometheus`。但要注意负责这个方案的人要同时维护两套配置了。


## 🤔 谈谈你对 SRE 理念的理解？  
- **`SRE`（`Site Reliability Engineering`，站点可靠性工程）是 `Google` 在`《SRE: How Google Runs Production Systems》`中提出的一套方法论，核心是用软件工程的思维来解决运维问题。它不是运维换个名字，而是从工作方式上就和传统运维有本质区别。**

    - **`SRE` 和传统运维的核心区别**
        - **传统运维靠人去堆**：出问题了人上去修，重复操作靠人盯着，规模大了靠加人
        - **SRE 靠代码去解决**：用写软件的方式解决运维问题——自动化工具代替手动操作，监控告警系统代替人肉值班，代码化的基础设施代替"XX 会搞那台机器"

    - **`SRE` 的两个核心指标**：`SLI` / `SLO` / `错误预算`
        - **SLI（Service Level Indicator，服务等级指标）** ：你实际测量的服务健康数据。比如请求延迟的 P99、每分钟错误率
        - **SLO（Service Level Objective，服务等级目标）** ：你给 `SLI` 设定的目标值。比如 "`P99 延迟` < `200ms`，`全年可用性` > `99.9%`"
        - **错误预算（Error Budget）** ：`100%` - `SLO` = `你允许服务出问题的总时间`。如果 `SLO` 是 `99.9%`，一年有 `8.76` 小时的"犯错额度"。这个额度决定了：
            - 功能发布节奏：错误预算还有余量 → 可以正常上线新功能
            - 错误预算快花光了 → 冻结所有非必要变更，全力做稳定性优化
        - 错误预算的核心价值是让开发和运维能用同一把尺子对话。开发想快点上线、`SRE` 想保持稳定——错误预算就是双方商定的共识：只要没超预算，开发可以正常发布；超过了，`SRE`有权冻结发布

    - **`SRE` 的日常工作方式**
        - **运维工作上限 50%**：`SRE` 团队花在"日常运维事务"上的时间不能超过 `50%`。如果超过了，说明系统有太多重复的手工操作需要自动化解决。剩下 50% 的时间用来做自动化、开发工具、优化架构
        - **拥抱风险，不是零故障**：SRE 认为 100% 可用性是不现实也不经济的。与其追求零故障，不如把故障当作系统学习的机会。每次故障后的复盘不是为了追责，而是为了改进系统和流程
        - **用故障演练检验系统**：通过混沌工程（`Chaos Engineering`）主动注入故障（如杀掉一个节点、延迟网络包），验证系统在真实故障下是否还能正常工作
        - **能度量的才能管理**：SRE 强调一切用数据说话，不是凭感觉判断

- **协助记忆**
    - 传统运维：修机器的
    - SRE：写代码让机器不用修的
    - 错误预算：开发和 SRE 的"停战协议"——没超预算你正常发版，超了预算我有权喊停
    - 50% 上限：超过一半时间在干重复的活 → 自动化没做好
    - 零故障不现实：与其防到死，不如留好退路

- **进阶思考**
    - `SRE` 的 `50%` 运维时间上限在实际执行中很容易被突破，怎么执行这个原则？
        - 在实际团队中，直接"50%"更像一个追求目标而非硬性标准。通常的做法是：先把重复操作列出来（发布、扩缩容、重启、清理磁盘），优先把这部分自动化掉。同时让 SRE 主导事后复盘，而不是业务方催着修。如果简单但高频率的操作仍在占用一半以上的时间，说明自动化不达标

    - **SRE 对传统运维人员来说，转型最大的难点是什么？**
      - 最难的不是学新工具（Prometheus、Kubernetes），而是思维方式的转变。传统运维的核心能力是"快速响应和修复"——越能在故障中快速恢复，越被认为是高手。SRE
        的核心能力是"让故障不发生"和"故障后系统自己恢复"。一个传统运维高手可能要接受自己的大部分工作正在被自动化替代，同时需要学会写代码来加速这个过程

## 🤔 当你加入新运维团队时，如何开展工作？  
- **加入新运维团队的前几周是建立信任的关键期。核心思路是先了解现状、再逐步推动改变，而不是一上来就推行自己的"最佳实践"。你不知道现有系统为什么长成那样——有些看起来不合理的设计可能是由特定业务场景、历史债务或组织限制导致的。**

    - **第一阶段**：摸清家底（前两周）
        - 先搞清楚团队维护了哪些系统、大致的拓扑结构、各自的负责人是谁。画一张架构图把各系统的关联关系标出来
        - 了解现有流程：怎么上线、怎么处理告警、故障怎么响应、变更怎么审批。不要急着否定现有流程，先跑一遍感受一下
        - 了解监控和日志：出了事怎么看，用什么查。然后了解 CMDB 或资产清单——这些是日常排查的基础，如果连资产都不清楚是没法干活的

    - **第二阶段**：跑通链路（第三四周）
        - 在指导下完成一次完整的线上操作——申请权限、走变更流程、执行操作、观察结果、关闭工单。目的是跑通整个操作链路，而不是独立处理某个系统
        - 参与一次值班或 On-Call，感受实际的告警压力。纸上谈兵和半夜被告警吵醒是完全不同的体验
        - 整理出自己的知识库：常用命令、常见故障处理步骤、各系统访问方式

    - **第三阶段**：建立信任后逐步推动改进
        - 推动改进前先做一件事：记录你在入职头一个月遇到最多的问题。这些"高频痛点"就是优先改进的方向——一个月内遇到了三次的坑，值得花时间填上
        - 先做小范围、低风险的改进（补充文档、新增一个监控项、优化一个脚本），从中让团队感受到你的价值。大的架构调整需要更长的信任基础
        - 所有的改进方案都带着"为什么"和"回退方案"，确保团队知道改错了能无损恢复

- **协助记忆**
    - 加入一个新团队就像进一个陌生的厨房当帮厨——先看刀放哪、火怎么开（摸清家底）→ 跟着炒一道菜试试流程（跑通链路）→ 觉得哪个环节太慢了再跟主厨商量改进（推动改进）
    - 三不要：不要一来就全盘否定现有流程、不要独自处理不熟悉的线上操作、不要立不切实际的 Flag
    - 三先做：先搞清楚系统拓扑和负责人、先跑通一次完整的变更流程、先做好知识记录

- **进阶思考**
    - **进去发现文档基本没有，或者文档严重过时，怎么快速上手？**
      - 没有文档反而更常见。直接通过 history 看前人的操作记录，再对照系统里的配置内容。实践过程中把能确认的步骤整理成笔记或文档。如果原负责人还在，定期约 10 分钟的快速沟通可以获得比长期琢磨更直接的信息

    - **经常遇到"这机器谁建的不知道，当时为什么这么配也不知道"的历史遗留，怎么对待？**
      - 历史遗留配置不要急着改。有些看起来不合理的配置可能是为了兼容某个老旧客户端或特定场景而设的。先记录当前状态，等工作到一定阶段通过变更流程逐步验证每个"怪配置"是否可以清理。直接删掉一个"看起来多余"的规则，然后某个系统挂了，这是新人最容易踩的坑。

## 🤔 简述常见的网络 I/O 模型及应用场景？  
- **网络 `I/O` 模型解决的核心问题是：一个进程如何同时处理多个网络连接的数据读写。不同模型在"谁等数据、怎么通知、何时拷贝"这三个环节上各有不同的处理方式。**

    - **阻塞 `I/O（Blocking I/O）`**
        - 进程调用 `recvfrom()` 后，如果内核数据还没准备好，进程一直挂在那里等，直到数据从内核空间拷贝到用户空间后才返回
        - 这是最传统的模型，代码逻辑简单直观。但一个进程同时只能处理一个连接，如果连接没有数据，进程就白白挂在那什么也干不了
        - 应用场景：简单的串行程序，如 ssh 的控制台、dd 这类单线程单连接工具

    - **非阻塞 `I/O（Non-blocking I/O）`**
        - 进程调用 `recvfrom()` 后，如果内核数据还没准备好，立即返回一个错误码（`EAGAIN` 或 `EWOULDBLOCK`），进程可以先去处理其他事，过一会儿再来问
        - 缺点是需要进程主动轮询，不断问"好了没？好了没？"。如果连接数多，轮询本身会消耗大量 `CPU`
        - 应用场景：极少单独使用，通常作为 IO 多路复用的底层的模式

    - **IO 多路复用`（I/O Multiplexing）`**
        - 进程把多个连接的 `socket` 交给一个监控者（`select` / `poll` / `epoll`），监控者告诉进程"哪些连接有数据可读了"，进程只处理那些准备好的连接
        - `select`：每次调用传入所有待监控的 `socket` 列表，内核遍历全部 `socket` 检查状态。监听数量有限制（默认 1024），且每次传入全部集合时会产生线性遍历的开销
        - `poll`：改用了链表存储 `socket`，去掉了 `1024` 的限制，但仍然是每次调用都做全量遍历 
        - `epoll`（`Linux` 专属）：只有"有事件发生的 `socket`"才返回给进程，不需要每次遍历全部连接。在连接数多但活跃连接少的场景下性能远高于 `select` / `poll`
        - 应用场景：`Nginx`、`Redis` 的底层核心模型，高并发服务的事实标准

    - **信号驱动 `I/O（Signal-driven I/O）`**
        - 进程先告诉内核："这个 `socket` 有数据时发个 `SIGIO` 信号通知我"。然后进程去干别的事，收到信号后再来读数据
        - 缺点：信号队列有限，高并发场景下信号可能丢失。实际使用较少
        - 应用场景：一些低吞吐的嵌入式系统或特殊场景

    - **异步 `I/O（Asynchronous I/O）`**
        - 进程调用 `aio_read()` 后立即返回，内核在数据准备好并完成用户空间拷贝后，通过回调或信号通知进程"数据已在你指定的缓冲区中了"
        - 真正的"异步"——进程连拷贝都不需要自己动手，内核全包了
        - `Linux` 原生 `AIO` 对网络 `socket` 支持有限，主要适用于磁盘 `IO`。`io_uring`（Linux 5.1 引入）是新一代异步 `IO` 框架，同时支持网络和磁盘，通过内核与用户空间共享的环形缓冲区来提交和完成 `IO` 请求，大幅减少了系统调用次数
        - 应用场景：高性能数据库（`MySQL` 的 `InnoDB` 使用 `AIO` 做数据刷盘）、`RocksDB`、高吞吐文件服务器

- **协助记忆**
    - `阻塞 I/O`：你在餐厅门口一直站着等到有空位，期间啥也干不了
    - `非阻塞 I/O`：你每隔 10 秒跑去问服务员"有空位了吗？"，没空位就继续逛，但来回跑累得慌
    - `IO 多路复用（epoll）` ：你在门口留下手机号，有空位了服务员直接打电话叫你，你坐着玩手机等他通知就行
    - `异步 I/O（io_uring）` ：你直接跟服务员说"我想点水煮鱼"，服务员去后厨帮你排队、盯着厨房、端上来放你桌上，全程你都在干别的事，菜到了才有人叫你

- **进阶思考**
    - **`epoll` 的 `ET`（边缘触发）和 `LT`（水平触发）有什么区别？实际中应该用哪个？**
        - `LT` 模式下，只要 `socket` 缓冲区还有数据没读完，每次 `epoll_wait()` 都会返回这个事件；`ET` 模式下，数据到达时只通知一次，你必须一次性把数据读完，否则剩下的数据不会再通知你，直到下次有新数据到达。`ET` 效率更高（减少重复通知），但需要配合非阻塞 `IO` + 循环读取直到返回 `EAGAIN`，编程难度比 `LT` 大。`Nginx` 使用 `ET` 模式，`Redis` 使用 LT 模式

    - **`io_uring` 和传统 `epoll` 在处理网络 `IO` 时有什么本质区别？**
        - 传统 `epoll` 只能告诉进程"哪个连接有数据了"，真正的读/写操作还需要进程自己调用 `read()` / `write()` 发起系统调用。`io_uring` 让进程把"我要读这个 `socket`"的请求提前提交到共享队列，内核完成数据拷贝后直接把结果放到完成队列。`IO` 密集型场景中，`io_uring` 能节省 `50%~90%` 的系统调用次数，对 `NVMe` 这类低延迟设备的性能释放尤其明显。目前主流数据库（`MySQL`、`PostgreSQL`）和编程语言的运行时（`Go`、`Rust tokio`）都在逐步支持 `io_uring` 作为底层 `IO` 引擎


## 🤔 Linux 系统误删除文件，如何恢复？  
- **文件删除后能否恢复，取决于文件是否还被某个进程持有句柄、文件系统类型、删除后是否有新数据写入同一位置。在所有恢复操作之前， 最重要的一条原则是：立即将该分区挂载为只读，停止一切写入操作，否则新写入的数据可能覆盖被删文件的数据块，再好的工具也救不回来。**
    - **方案一**：文件还在被进程打开（最简单、成功率最高）
        - 如果文件被删时还有进程在引用它（比如日志文件被 `rm` 了但写日志的进程没重启），内核不会真正释放 `inode` 和数据块，文件内容仍然可以通过进程的文件描述符访问
        - 从 `/proc/<PID>/fd/<FD>` 恢复：`cp /proc/<PID>/fd/<FD> /path/to/restore/file`
        - 可以通过 `lsof | grep deleted` 快速定位哪些被删的文件还在进程中存活，结果中会显示文件状态为 (`deleted`)
        - 这是线上恢复被删日志文件最常用的招数，且不需要磁盘写入额外的恢复数据——直接把 `/proc` 里指向的内存内容拷到新位置即可

    - **方案二**：文件已关闭、分区没有新写入（`ext4` 文件系统）
        -  `umount` 该分区后使用 `extundelete` 工具扫描被删除文件的 `inode`，尝试恢复
        -  `extundelete /dev/sdX --restore-file /path/to/file` 恢复单个文件，`--restore-all` 恢复所有能找回的文件
        -  对于 `ext4` 文件系统的快速格式化（`mkfs.ext4`）操作，不一定能完全清除原有数据。专业的数据恢复服务会逐扇区读取磁盘后扫描文件系统结构，寻找残留的元数据信息和文件内容。部分场景下可以通过这种方式找回部分数据

    - **方案三**：文件系统损坏严重或无法挂载
        - 使用 `ddrescue` 或 `dd` 先对整个分区或磁盘做扇区级别的镜像。`dd if=/dev/sdX of=/backup/image.img bs=4M status=progress`
        - 对镜像文件操作，而不是直接在原始磁盘上尝试恢复，避免对物理磁盘造成二次损坏
        - `testdisk` 扫描分区表，`photorec` 通过文件签名（`File Signature / Magic Number`）进行文件雕刻，识别文件类型头尾并提取内容。这个过程不依赖文件系统元数据，即使分区表丢失或文件系统被格式化也能找回一部分数据，但恢复的文件需要手动确认

    - **特殊情况一**：`XFS` 文件系统
        - `xfs` 没有类似 `extundelete` 的恢复工具。被删除文件的块可能已被标记为未使用，随时可能被新数据覆盖
        - 唯一的恢复手段是：在删除动作发生后，用 `xfsdump` 配合 `xfsrestore` 尝试从日志中还原（成功率不高），或从备份恢复

    - **特殊情况二**：文件被覆盖（不是被删除，而是内容被 > 重定向写空了）
        - `echo "" > file` 或 > `file` 会清空文件内容。这种情况下 `inode` 和数据块没有被释放，只是内容被截断了
        - 无法通过 `extundelete` 找回来——因为文件本身还在（`inode` 还在），只是内容被重置了
        - 需要从备份或快照中恢复

- **协助记忆**
    - 恢复黄金法则：发现删错文件，第一时间 `mount -o remount,ro /分区` 冻结写入，然后 `lsof | grep deleted` 看在进程中死了没
    - 恢复三招：活着的 `/proc` 拷 → 死了的 `extundelete` 捞 → 盘都坏了 `ddrescue` + `photorec` 硬扫
    - 最好的恢复是备份：到这一步已经是在补救阶段了


- **进阶思考**
    - **`ext4` 文件系统被误 `rm` 后，有没有办法知道被删文件名指向的 `inode` 编号，从而精确定位恢复？**
        - `ext4` 的目录项记录了"`文件名` → `inode 编号`"的映射。文件被删除后这条映射没了，但 `inode` 的编号在日志（`journal`）中可能还有记录。`debugfs -R "ls -l /path" /dev/sdX` 可以查看当前目录项，但无法查出已被删除的。如果文件删除时间很近且分区负载不高，可以 `extundelete` 直接扫描恢复，通常它会按 `inode` 编号列出可恢复的文件列表

    - **`Git` 仓库被误删了还没有推送，代码怎么救？**
        - `Git` 仓库本质上是 `.git/objects/` 目录下的一堆小文件。先用招数一或者招数二把 `.git/objects/` 目录尽量恢复出来，然后用 `git fsck --full` 检查对象完整性，`git reset --hard` 恢复到最近一次提交状态。因为 `Git` 的 `objects` 文件是只写一次的（内容寻址），只要对象文件能被恢复回来，提交历史基本上能保全

    - **企业级文件服务器（如基于 `ZFS` 或 `Btrfs` 的 `NAS`）误删了文件，有没有更快的恢复手段？**
        - `ZFS` 和 `Btrfs` 都内置了快照功能。如果历史快照存在（比如每小时拍一次快照），直接从快照中复制即可秒级恢复，不需要 `extundelete`。如果快照也没开启，`ZFS` 可以通过 `zdb` 工具尝试从存储池的冗余数据中恢复，`Btrfs` 可以通过 `btrfs restore` 命令，扫描整个文件系统并尝试提取文件。这也是为什么建议重要文件系统的 `NAS` 都开启定期快照功能的原因

## 🤔 你觉得 SRE 核心工作有哪些？  

- **`SRE` 的核心工作可以概括为：保证系统在变化中持续可用。不是*不出故障*，而是*出故障时能快速恢复，并且系统能在不断变化（代码发布、 配置变更、流量波动）中保持稳定*。具体拆成五个维度：**

    - **定义和度量可靠性（设定标准）**
        - 和业务方一起确定*什么叫正常*——比如接口 `P99` 延迟不超过 `200ms`、全年可用性不低于 `99.9%`。这些目标写下来就是 `SLO`
        - 持续的测量和对比，把观测到的 `SLI` 和 `SLO` 对照，判断当前是否处于"`健康`"状态
        - 如果 `SLO` 持续不达标，推动架构或代码层面的改进

    - **建设和维护监控与可观测性（感知系统状态）**
        - 指标采集（`Prometheus` + `Grafana`）、日志集中收集（`Loki` / `ELK`）、链路追踪（`Jaeger` / `Zipkin`）
        - 告警规则的定义和收敛，避免告警风暴——明确区分什么情况下需要半夜打电话、什么情况下只需要一条 `IM` 消息。同时确保告警能真正触达对应负责人，避免重复无效告警导致麻木
        - 建立 `on-call` 响应流程和升级机制

    - **自动化消除重复劳动（减少人肉操作）**
        - 把发布、扩缩容、重启、配置变更这些重复操作自动化，确保 `SRE` 团队花在日常运维杂事上的时间不超过 `50%`
        - 自动化部署流水线（`CI`/`CD`）、基础设施代码化（`Terraform` / `Ansible`）、自愈脚本（磁盘自动清理、进程自动重启）
        - 故障自愈：那些*一看就知道怎么修*的故障（磁盘满、`Nginx` 挂了）由自动化触发修复，不需要人介入

    - **变更管理与风险控制（管理变化中的风险）**
        - 生产环境的任何变更（代码发布、配置修改、扩缩容）都必须有明确的评审、审批和回滚方案
        - 灰度发布 / 金丝雀发布：先让小部分流量验证新版本，确认正常再逐步扩大范围。监控到异常指标立刻暂停发布并触发回滚
        - 错误预算作为发布决策的依据——如果错误预算已经快用完了，SRE 可以暂停非必须的变更，优先做稳定性修复

    - **故障响应与事后复盘（从故障中学习）**
        - 制定故障响应流程和故障等级划分（`P0` ~ `P3`）
        - 组织故障复盘，产出行动项和改进措施。复盘的核心不是追责，而是找出系统和流程上可以改进的地方
        - 故障演练（混沌工程）：定期主动注入故障来验证系统的容错能力，而不是等故障真的来了才知道哪里扛不住

- **协助记忆**
    - `SRE` 的五件事：`定标准（SLO）`→ `建感知（监控）`→ `搞自动化（减少手工）`→ `管变更（灰度+回滚）`→ `复盘改进（练+改）`
    - 不是防故障，是让出了故障能很快好，并且下次不再出同样的错

- **进阶思考**
    - **`SRE` 和传统运维或 `DevOps` 工程师的工作边界怎么划分？**
        - **一个粗线条的划分是**：`DevOps` 是*让开发和运维协作更顺畅*，核心在做 `CI/CD`、`容器化`、`基础设施即代码`；`SRE` 是*确保系统在变化中持续可用*，核心在做 `SLO`、`错误预算`、`故障演练`、`容量规划`。两者有大量重叠——都要写自动化、都要搭监控——但 `SRE` 多了一层*用数据决策可靠性*的维度。在中小公司里通常一个人同时干 `DevOps` 和 `SRE` 的活，在大厂才会分得比较清楚

    - **`SRE` 负责 `on-call` 是比较普遍的模式，但 `on-call` 消耗很大，有什么好的做法可以减轻 `SRE` 的 `on-call` 压力？**
        - 几个被大厂验证过的做法：一是运维工作和 `on-call` 的开发人员轮值制度（让写代码的人也感受线上压力）；二是建立*Sev 等级*和*值班升级*机制，`P0` 故障直接升级，`P2` 以下问题不进值班队列而是第二天处理，避免频繁打扰；三是前面提到的自愈能力——当 `90%` 的告警能被脚本自动处理掉，剩下的 `10%` 才需要人工介入。如果 `on-call` 压力居高不下，说明自动化覆盖面不够，需要优先投入自动化建设


## 🤔 你们故障后怎么做复盘？  
- **复盘的核心目的不是追责，而是把一次故障转化为系统和流程的改进机会。一次好的复盘应该输出可执行的行动项，而不是一份 *下次注意* 的纪要。在实践中通过四个阶段来完成：`止血恢复` ↔ `信息收集` ↔ `根因分析` ↔ `改进跟踪`。前两个阶段在故障期间同时进行，后两个在故障解决后逐步推进。**

    - **第一阶段**：故障尚未结束时——先收集信息
        - 故障的*现场痕迹*会随着时间快速消失（进程重启后 `/proc` 信息丢失、日志被滚动覆盖、现场状态改变）。不要等到故障结束了才开始收集，在处理过程中顺手保留关键信息即可：记录故障发生 和恢复的关键时间点、保留关键进程快照和连接状态、保存相关日志片段
        - 故障群的聊天记录和时间线也值得归档保留——你当时先看了什么、做了什么判断、为什么选择了某个方案，这些是后续改进分析的重要素材

    - **第二阶段**：故障结束后——召集复盘会议
        - 通常在故障解决后 `1 ~ 3` 天内召开，间隔太久了细节容易遗忘
        - 与会人员：当值 `On-Call` 是必须参加的，关联的研发团队也需要参加以推进改进落地
        - 先拉时间线：从监控告警→发现→响应→定位→恢复，把关键时间点标出来。不是为了追究谁花了太久定位，而是为了看清"中间哪个环节花了最长时间"，帮助团队识别改进方向

    - **第三阶段**：分析根因与改进点
        - 直接原因：比如磁盘写满导致服务不可用
        - 根本原因：为什么日志会写满磁盘？因为 `logrotate` 没配、告警阈值太高没有提前预警、日志量突然暴涨没有限流
        - 针对根本原因产出行动项，每一项需要标注责任人、完成时间、验证方式

    - **第四阶段**：跟踪改进落地
        - 复盘会议结束后产出"行动项跟踪表"，两周后再确认一遍各项改进的落实情况。没有跟踪的复盘就是走过场
        - 对于"基础设施级"的改进项（如补全监控项、修复自动化脚本），如果卡住的话通常不是研发资源的问题，而是缺少执行周期；对于"流程级"的改进（如变更审批加一道确认），如果反复推进未果，往往是在不同方向上需要取舍

    - **和事故报告的主要区别**
        - 事故报告（Incident Report）主要面向外部利益相关方（管理层、客户），需要包含故障级别、影响范围、持续时长、SLO 达标情况等正式内容。复盘会议则主要面向内部团队，可以更开放地讨论改进方向

- **协助记忆**（以家里水管爆了为例）
    - 先关水闸（止血恢复）
    - 拍照留证（故障时收集 /proc、日志、时间线，保留"作案现场"）
    - 修好了坐下来分析：是水管老化了（硬件故障）、还是装修时螺丝没拧紧（配置疏忽）、还是水压太高超过了管道承受范围（容量规划
      不足）
    - 列改进清单：换新水管、装减压阀、定期检查。没人追究"当时是谁拧的螺丝"

- **进阶思考**
    - **复盘会开成了"追责会"怎么办？**
      - 这是最常见的失败模式。会议主持人需要在开场就定调："今天这个房间里的每一句话都不能用来追责"。复盘关心的是"系统出了什么问题"，而不是"谁做错了什么"。如果发现某个参与者表现出明显的防御姿态，可以使用"五个为什么"或其他结构化分析方法将关注点从"人"拉到"流程"上——不是问你为什么没检查那个文件，而是问删除脚本为什么不需要二次确认

    - **复盘经常出现"改进项写了一大堆，过几个月发现基本没落地"，怎么办？**
      - 确保每项改进有明确的验收标准，明确完成时间。同时在下一轮复盘开始时回顾上一轮的改进项落实情况。如果发现了重复的故障模式，或者在评估新改动对现有流程影响的环节上仍存在明显的盲区，说明当前的改进机制还需要补充


## 🤔 如何建设自动化运维体系？
- **自动化运维体系不是买一套工具装上去就完了，而是把重复操作从"人执行"变成"系统执行"，把"人盯着"变成"系统告警"。建设的节奏通常是：先解决最高频、最痛的重复劳动，再逐步扩展到完整体系。**
    - **先把最痛的环节自动化（从单点开始）**
        - 不要一开始就规划全量自动化平台。梳理团队日常操作中最频繁、最耗时的事情是什么
        - 常见的优先自动化的方向：
        - 配置管理（`Ansible` / `SaltStack`）：统一管理系统配置，禁止逐台 `SSH` 改配置
        - 部署流水线（`CI`/`CD`）：代码提交后自动构建、测试、部署，减少人工介入
        - 监控自愈：对于一些有明确处理方案的故障（磁盘使用率超过 `90%` 自动清理临时文件、进程挂了自动拉起），配置自动化的处置策略
        - 备份与恢复：数据库备份脚本自动化 + 定期恢复演练验证

    - **标准化是自动化的前提**
        - 没有标准化就没有自动化。如果你的服务器有的是 `CentOS 7`、有的是 `Ubuntu 22`、有的是自己定制的精简版，系统路径不一样、包管理器不一样、`bash` 版本不一样，自动化工具有一半的时间在处理特例分支
        - 标准化的几个基础：
            - 操作系统版本统一、基础初始化配置（`NTP` / `DNS` / `repo` / 防火墙基线）一致
            - 主机命名规范和环境标签统一——从名字上就能区分*这是测试环境还是生产环境，属于哪个业务线*
            - 应用部署路径和日志路径统一。所有应用都安装在 `/data/app/`、日志统一写到 `/var/log/app/`，自动化工具有一致的路径，不需要逐一适配

    - **从脚本沉淀到工具化**
        - 运维团队通常会先写 `Shell` 或 `Python` 脚本解决具体问题。这是起步阶段，但脚本的问题在于：放在某个人电脑里，人走了脚本也没了
        - 将脚本统一管理（`Git 仓库`），加上参数化和错误处理，变成一个可复用的"工具"。团队内能直接调用，而不是每人改一份
        - 当工具积累到一定量后，考虑提供 `Web` 界面或 `API` 让开发团队自助使用

    - **建立变更流程与门禁**
        - 自动化不是"谁都可以点一下就执行"。需要给变更建立"门禁"——只在经过审批、指定了回滚方案的条件下允许执行
        - CI/CD 流水线的门禁：代码审查通过才能合并、测试通过才能发布、灰度验证通过才能全量
        - 生产环境的变更工具可以集成审批环节，只有审批通过的操作才被执行

    - **持续改进：用数据衡量自动化效果**
        - 定期看几个指标：
            - `On-Call` 的告警处理率：有多少告警是自动处理的，多少需要人工介入。自动处理越多说明自愈能力越强
            - 部署频次和成功率：自动化流程交付越快、失败率越低，交付质量越好
            - 人为失误导致的故障数：自动化覆盖越广，人为误操作导致的故障应该越少

- **协助记忆（以工厂自动化为例）**
    - **先别想着建全自动无人工厂**：先看看车间里哪个环节重复劳动最多——拧螺丝最费时，就先买个自动螺丝枪（单点自动化），而不是上来就搞一条全自动流水线
    - **标准化**：把螺丝规格统一、拧几圈定好标准。不统一规格，自动螺丝枪装上去第一天就换了三种螺丝刀头——自动化的脚本里全是 `if-else` 处理特例
    - **沉淀工具**：你手工拧螺丝的熟练度再高，也比不上自动螺丝枪稳定。把"某个师兄的脚本"变成"仓库里团队都能用的工具"
    - **建门禁**：自动螺丝枪也要有操作规程——岗位授权了才能用，不是谁都能拿起来拧

- **进阶思考**
    - **自动化运维和小团队（3~5 人）的资源冲突怎么平衡？**
      - 小团队资源有限，不可能一步到位建全量平台。核心原则是按 `ROI` 排序，先做投产比最高的：`Ansible` 管理配置（基础重复操作最少化）→ `CI/CD` 流水线（交付效率提升最快）→ 关键告警的自愈处理（减少 `On-Call` 压力）。这三件事每件一个人花一两天就能搭起来，产出立竿见影。而 `CMDB` 或运维工单系统这类需要多人协作且周期较长的建设可以暂缓或简单工具代替

    - **自动化越做越多，怎么避免"自动化的自动化"——工具本身也需要维护？**
      - 确实会出现"维护自动化工具"本身变成了一种负担。解决的思路是：优先使用成熟的开源工具而不是自己开发、对自定义运维脚本保持持续清理、如果某个监控告警或自动修复规则半年都没触发过，说明它覆盖的是几乎不会出现的场景——可以考虑清理掉，减少维护面

## 🤔 你用过哪些企业虚拟机技术？  
- **企业级虚拟化技术主要分为两类：Hypervisor 虚拟化（直接在硬件上跑虚拟机）和  容器化（共享宿主机内核的轻量级隔离）。两者解决的问题不同，适用的场景也不同。**
    - **`VMware vSphere（ESXi）`— 商业虚拟化的标杆**
        - 企业级市场的绝对主流。`ESXi` 是 `Type-1 Hypervisor`，直接在物理硬件上运行，不需要底层操作系统，性能损耗极小
        - 管理组件：`vCenter Server` 集中管理多台 `ESXi` 主机，支持 `VMotion（在线迁移）`、`DRS（动态资源调度）`、`HA（高可用自动重启）`
        - 适用场景：传统 `IDC` 机房的服务器虚拟化、数据库等重负载应用的虚拟化。如果你的公司买服务器还走招投标流程、有专门的硬件运维团队，大概率在用 `vSphere`
        - 和其他方案的对比：功能最全、生态最好、文档最成熟，但授权费用最贵

    - **`Proxmox VE`—开源整合型虚拟化平台**
        - 基于 `KVM`（内核虚拟化）+ `LXC` 容器，整合了 `Web` 管理界面、备份、高可用功能，开箱即用
        - 相比 `VMware`，`Proxmox` 的集群规模上限要低一些，但在中小规模环境中（几十到几百台节点）已经非常成熟
        - 适用场景：预算有限的中小企业、不想支付 `VMware` 授权费用但需要 `Web` 管理界面的团队、混合使用虚拟机和容器的场景

    - **`KVM` + `Libvirt`—Linux 原生虚拟化**
        - `KVM（Kernel-based Virtual Machine）`是 `Linux` 内核自带的虚拟化模块，将 `Linux` 本身变成一个 `Type-1 Hypervisor`。`Libvirt` 是管理 `KVM` 的 `API` 和工具集
        - 管理方式：可以 `virt-manager`（图形界面），也可以 `virsh` 命令行，或者通过 `OpenStack` 等云管理平台编排
        - 适用场景：深度定制虚拟化需求的团队、`OpenStack` 私有云的底层技术选型


- **协助记忆**
    - `vSphere（ESXi）` ：商用的"成品服务器"，买了开机就能用，服务有人兜底
    - `Proxmox VE`：开源整合的"半成品工具箱"，功能全但不求人
    - `KVM + Libvirt`：Linux 自带的"积木"，需要自己搭，但也最灵活
    - 选型判断：看预算和团队人力——有钱上 `VMware`、中等需求 `Proxmox`、自己有能力折腾上 `KVM`。混用的情况也比较常见：核心数据库用
      `VMware`，测试开发用 `Proxmox`

- **进阶思考**
    - **VMware 被 Broadcom 收购后对市场有什么影响？**
      - Broadcom 收购后停止 VMware 永久许可的销售，全面转向订阅制，同时将产品线简化为 VMware vSphere Foundation 和 VMware Cloud Foundation 两个版本。这导致大量中小企业面临成本上升，开始评估迁移到 Proxmox 或公有云。这个变化对 Proxmox 和 KVM 生态是一个推动力，但对大企业来说 VMware 的生态深度暂时还很难替代

    - **`KVM` 和 `VMware ESXi` 在性能上有多大差距？**
      - 在 `CPU` 密集型和内存密集型负载下，两者差距极小（通常在 5% 以内）。差距主要在网络和存储 IO 路径上——VMware 的 `vmxnet3` 网卡和 `PVSCSI` 存储驱动经过长期优化，在特定场景下比 `KVM` 的 `virtio` 驱动略优。但对于大多数业务来说这个差距不会成为瓶颈。真正影响选型的通常不是性能，而是管理工具、生态支持和运维成本

    - **`Oracle VirtualBox` 也属于虚拟机技术，它和企业级的 `VMware` / `Proxmox` 有什么不同？**
        - `VirtualBox` 是 `Type-2 Hypervisor`（托管型），它必须安装在已有操作系统之上，通过主机的操作系统来管理硬件调用。而 `VMware ESXi` 和 `Proxmox` 是 `Type-1 Hypervisor`（裸机型），直接运行在物理硬件上，不依赖底层操作系统。这个架构差异决定了它们的用途完全不同：`VirtualBox` 适合开发者在自己的笔记本上跑个 `Linux` 虚拟机做测试，或者运维在本地搭建模拟环境验证操作步骤；`ESXi` 和 `Proxmox` 适合在数据中心跑生产业务，需要支持在线迁移、高可用、集群管理等企业级功能。所以 `VirtualBox` 很少出现在"企业服务器虚拟化"的讨论中，不是因为不好，而是它的设计目标本来就不是这个


    - **容器（`Docker` / `Containerd`）和虚拟机在原理上有什么本质区别？它们是对立的还是互补的？**
        - 虚拟机和容器的核心区别在隔离的层级不同。虚拟机在硬件层面做隔离——每个 `VM` 有自己的完整操作系统内核，`Hypervisor` 负责把物理 `CPU`、`内存`、`磁盘`分配给各个 `VM`。容器在进程层面做隔离——所有容器共享宿主机的同一个内核，通过 `Linux` 的 `Namespace` 隔离进程视图、`Cgroup` 限制资源使用。这个差异带来了几个实际影响：
            - **启动速度**：`VM` 需要启动整个操作系统（分钟级），容器只是启动一个进程（毫秒到秒级）
            - **资源开销**：`VM` 需要为每个 `Guest OS` 预留内存和磁盘，容器只消耗进程本身的开销
            - **隔离强度**：`VM` 隔离更彻底（硬件级，一个 `VM` 内核崩溃不影响其他 `VM`）；容器共享宿主机内核，如果宿主机内核出问题所有容器都受影响
            - **适用场景**：`VM` 适用于需要完整操作系统环境、强隔离的场景（如多租户云平台、运行不同内核版本的应用）；容器适用于微服务、快速交付、弹性伸缩的场景。在实际生产环境中，两者不是对立的，更多是配合使用——VM 提供底层硬件隔离和安全边界，容器在 `VM` 内部跑应用服务，兼顾了隔离性和部署效率


## 🤔 简述 KVM 虚拟化的核心实现？  
- **`KVM（Kernel-based Virtual Machine）`是 `Linux` 内核自带的虚拟化模块，它的核心设计思路是：把 `Linux` 内核本身变成一个 `Type-1 Hypervisor`。`KVM` 不像 `VMware ESXi` 那样需要从零写一个 `Hypervisor`，而是利用 `Linux` 内核已有的进程调度、内存管理、设备驱动框架，通过添加一个内核模块来支持虚拟化指令的执行。**

    - **`KVM` 的核心架构**：两个模块
        - **`kvm.ko`**：架构无关的核心模块，注册 `/dev/kvm` 字符设备，提供虚拟化核心 `API`（`创建 VM`、`分配 vCPU`、`设置内存映射`）。所有 `KVM` 功能都通过这个设备接口暴露给用户空间
        - **`kvm_intel.ko` / `kvm_amd.ko`**：架构相关模块，封装 `Intel VT-x` 或 `AMD-V` 硬件虚拟化指令（`VMX` / `SVM`）。`Intel VT-x` 提供了 `VMX Root Mode`（`Hypervisor` 运行模式）和 `VMX Non-root Mode`（`Guest OS` 运行模式）两种 `CPU` 执行模式，配合 `VM Entry`/`VM Exit` 指令完成模式的切换

    - **`QEMU` 的角色**：用户空间的设备模拟器
        - `KVM` 只负责 `CPU` 和内存的虚拟化（通过硬件加速），设备的模拟（网卡、磁盘、USB）由 `QEMU` 在用户空间完成。`KVM` + `QEMU` 组合才是完整的 `KVM` 虚拟化方案
        - `QEMU` 在用户空间通过 `/dev/kvm` 的 `ioctl` 接口创建和管理虚拟机——设置 CPU 核心数、分配内存大小、指定启动镜像。同时模拟 `Guest` 看到的硬件设备
        - 当 `Guest` 执行 `IO` 操作时（如读写磁盘），触发 `VM Exit`，`CPU` 从 `VMX Non-root Mode` 切回 `VMX Root Mode`，`KVM` 将 `IO` 请求转发给 `QEMU`，`QEMU` 执行实际的 `IO` 操作再返回结果给 `Guest`

    - **硬件辅助虚拟化的关键作用**
        - `KVM` 依赖 `CPU` 硬件虚拟化扩展（`Intel VT-x` / `AMD-V`），没有它 `KVM` 就无法工作。这些硬件特性解决了一个关键问题：`Guest OS` 运行时需要执行特权指令（如修改页表、开关中断），在传统虚拟化中这些指令需要被拦截和模拟（`trap-and-emulate`），效率极低。`VT-x` 引入的 `VMX Non-root Mode` 允许 `Guest OS` 直接执行大部分指令，只有特定敏感指令才会触发 `VM Exit` 回到 `Hypervisor` 处理。这也是 `KVM` 性能接近原生的关键原因之一

- **协助记忆**
    - `KVM` 不是*给 Linux 装个虚拟机软件*，而是*让 Linux 内核本身变成一个 Hypervisor* 
    - `KVM` 管 `CPU` 和内存（通过硬件加速），`QEMU` 管设备模拟（网卡、磁盘、BIOS）
    - `VT-x` / `AMD-V` 是关键基础设施——没有它 `KVM` 就跑不了
    - 打个比方：`KVM` 是厂长，负责分配工人（`CPU`）和车间（`内存`）；`QEMU` 是后勤，负责买机器设备（网卡、硬盘）；两人配合才能让一个 `Guest OS`（访客）正常运行

- **进阶思考**
    - **`VM Exit` 是什么？频繁的 `VM Exit` 对性能有什么影响？**
        - `VM Exit` 是指 `Guest OS` 在执行某些敏感指令或发生外部事件（如硬件中断）时，`CPU` 从 `VMX Non-root Mode` 退出到 `Root Mode`，让 `KVM` 处理后再返回 `Guest`。每次 `VM Exit` 都需要保存 `Guest` 状态、切回 `Root Mode` 执行处理代码、再恢复 `Guest` 状态，这个过程有几百到几千个 `CPU` 周期的开销。如果频繁的 `VM Exit` 发生（如密集的 `IO` 操作需要持续模拟磁盘中断），性能会受到明显影响。这也是为什么直通设备（`PCI Passthrough`，将物理设备直接分配给 `Guest`，绕过 `QEMU` 模拟）能显著提升 `IO` 密集型负载的性能——它直接把设备控制权交给 `Guest`，不需要频繁退出来让 `QEMU` 模拟 

    - **`KVM` 的虚拟化方案和 `VMware ESXi` 相比，性能差距主要在哪？**
        - 在 `CPU` 和内存虚拟化上，基于 `Intel VT-x` / `AMD-V` 的方案，两者差距很小。差距主要在 `IO` 路径上——`VMware` 的 `vmxnet3` 和 `PVSCSI` 是经过长期优化的半虚拟化驱动，在虚拟化感知上做了精简处理；而 `KVM` 默认使用 `virtio` 半虚拟化驱动，虽然性能已经很不错，但在特定场景下 `VMware` 的 `IO` 路径延迟略低。但在最新内核版本的 `virtio` 和 `vhost` 支持下，这个差距正在进一步缩小


## 🤔 KVM 虚拟机有哪些常用的管理方式？  
- **`KVM` 本身只提供了内核层面的虚拟化能力，不管用户怎么用它。管理方式从命令行到 `Web` 界面到完整的云管理平台都有，选择哪个取决于你的规模——管理三五台测试机用命令行就够了，管理几十台生产虚拟机就需要 `Web` 界面或编排平台。**

    - `**virsh` + `virt-manager`（最基础、最常用的一对工具）**
        - **virsh（命令行）** ：`Libvirt` 的 `CLI` 管理工具。大部分日常操作都能用 `virsh` 完成，配合 `virt-install` 从命令行安装虚拟机。不需要图形界面， `SSH` 上去就能操作，适合服务器环境
        - **常见常用操作**：
            - **`virsh list` / `virsh list --all`**：查看运行中 / 所有虚拟机
            - **`virsh start <vm>` / `virsh shutdown <vm>` / `virsh destroy <vm>`**：启停操作
            - **`virsh edit <vm>`**：编辑虚拟机配置文件（`XML`），修改 CPU、内存、磁盘、网卡等——变更在 `XML` 中定义完成后需要重启虚拟机才能生效。`virsh dumpxml <vm>` 可查看当前配置但不进入编辑模式
            - `virsh console <vm>`：通过串口控制台连接到虚拟机，相当于插了一根显示器和键盘——适合网络不通时排障。不是所有发行版默认都开启串口控制台 ，如果连上去没输出可以在 `Guest` 的 `/etc/default/grub` 中确认 `console=ttyS0` 是否配置

        - `virt-manager`（图形界面） ：`virsh` 的图形版。安装 `virt-manager` 包后运行，可以像 `VMware Workstation` 一样通过图形界面创建、配置、操作虚拟机。适合在本地桌面环境管理远程 `KVM` 宿主机（通过 `SSH` 连接）

    - **`libvirt` + 远程连接**
        - `Libvirt` 支持远程管理：通过 `qemu+ssh://host/system` 连接远程宿主机，本地运行 `virt-manager` 或 `virsh` 管理远程机器上的虚拟机
        - 认证方式：通常通过 `SSH` 密钥认证，不需要单独配置虚拟化管理账号

    - **`oVirt` / `Proxmox VE`（Web 管理平台，面向小到中型集群）**
        - `oVirt`：`Red Hat` 开源的虚拟化管理平台，可以理解为开源的 `VMware vCenter`。提供 `Web` 控制台管理多台 `KVM 宿主机`、`在线迁移`、`模板部署`、`高可用`。适合几十到几百台规模的 `KVM` 集群
        - `Proxmox VE`：基于 `KVM` + `LXC` 的整合平台，开箱即用。提供 `Web` 管理界面，内置备份、高可用、集群管理、Ceph 存储集成。部署比 `oVirt` 简单，中小团队用得多

    - **`OpenStack`（云管理平台，面向大规模）**
        - `OpenStack` 是一个完整的云计算管理平台，管理大规模 `KVM` 节点集群，对外提供类似 `AWS` 的 `API`。配置管理规范，支持多租户隔离、按需自助创建虚拟机
        - 和 `oVirt` / `Proxmox` 的差别：`OpenStack` 面向的是"云"——用户通过 API 自助创建、销毁虚拟机，管理员不直接登录每台宿主机操作

- **协助记忆**
    - 一台两台：`virsh `+ `virt-manager`，命令行够用了
    - 一个集群：`oVirt` / `Proxmox VE`，`Web` 界面管多台，在线迁移高可用
    - 一个云：`OpenStack`，`API` 自助开 `VM`，规模和复杂度都上去了

- **进阶思考**
    - **`virsh` 中的 `shutdown` 和 `destroy` 有什么区别？什么时候用哪个？**
      - **`shutdown` 是"优雅关机"**：向 `Guest` 发送 `ACPI` 关机信号，让 `Guest` 操作系统自己完成关机流程（停止服务、同步磁盘、正常关机）。`destroy` 是"强制断电"：直接杀掉 `QEMU` 进程，等效于拔电源。日常操作优先用 `shutdown`，只有虚拟机卡死、关不掉时才用 `destroy`

    - **`OpenStack` 和 `oVirt` 都是管理多台 `KVM` 宿主机，怎么选？**
      - 关键区别在于面向的使用场景不同。`oVirt` 面向的是*管理员管理虚拟机*的场景——管理员通过 `Web` 界面创建 `VM`、分配资源。`OpenStack` 面向的是"用户自助开 VM"的场景——用户通过 `API` 或 `Dashboard` 自己开 `VM`，不需要管理员介入。如果你的团队要对外提供云服务，选 `OpenStack`； 如果只是内部团队统一管理测试环境和运维工具，`oVirt` 或 `Proxmox` 更合适

## 🤔 eBPF 是什么？有哪些应用场景？  
- **`eBPF`（`extended Berkeley Packet Filter`）是一项让用户在不修改内核源码、不加载内核模块的情况下，在内核中安全地运行沙箱程序的技术。你可以把它理解为内核的*可编程插件系统*——过去只有内核开发者才能修改内核行为（改代码、重新编译、重启），现在运维和开发人员也可以写一小段程序挂到内核的特定事件上，让内核执行自定义的逻辑。**

    - **`eBPF` 的技术本质**
        - 用户编写 `C` 代码，通过 `LLVM/Clang` 编译成 `eBPF` 字节码。这些字节码在加载到内核时，经过验证器（`verifier`）严格检查——确保没有死循环、没有越界访问、不会导致内核崩溃。验证不通过的程序不会被加载，这是 `eBPF` "安全"的核心保障
        - 加载后的 `eBPF` 程序通过 `JIT`（`just-in-time`）编译 转换为原生机器码执行，性能接近原生内核代码
        - `eBPF` 程序通过 `Hook` 点挂载到内核的各个事件上——系统调用入口和出口、网络数据包到达、函数入口和出口、跟踪点（`tracepoint`）、`perf` 事件等

    - **核心应用场景**
        - **网络性能与可观测性**：`eBPF` 可以在网络数据包经过内核网络栈的各个阶段插入处理逻辑。`Cilium` 基于 `eBPF` 实现了 `Kubernetes` 的网络策略和服务负载均衡，替代了传统的 `iptables` 模式，在大规模集群中性能提升明显——因为它不再需要逐个遍历 `iptables` 规则链，而是在内核网络路径上直接做决策
        - **故障排查与动态跟踪**：`bcc`（`BPF Compiler Collection`）工具集包含了一系列基于 `eBPF` 的排查工具。比如 `execsnoop` 追踪系统中每一毫秒内启动的新进程，`opensnoop` 追踪文件打开操作，`biolatency` 追踪磁盘 `IO` 延迟分布，`tcpconnect` 追踪系统上在每一秒内建立的新连接。这些工具不需要修改应用代码、不需要重启服务，加载即用
        - **安全监控与运行时检测**：`Falco` 等运行时安全工具使用 `eBPF` 监控系统调用，检测异常行为——如容器内启动了一个新的 `Shell`、某个进程读取了 `/etc/shadow`。相比传统的 `auditd`，`eBPF` 方式的性能开销更低，且能捕获到更细粒度的事件
        - **性能剖析（`Profiling`）** ：`bpftop` 对内核和用户态程序做 `CPU` 采样分析，回答"当前 `CPU` 周期花在了哪个函数上"。和传统的 `perf` 不同，`eBPF` 可以配合容器运行时使用，直接对应出具体容器和进程的热点函数

- **协助记忆**
    - `eBPF` 就像汽车预留的 **OBD 诊断接口**（车载自动诊断系统）：
        - 汽车发动机（内核）出厂时就预留了这个接口，不需要你拆开发动机（改内核代码）才能检查问题
        - 修理厂（运维/开发）往这个接口插不同的设备（`eBPF` 程序）：
            - 插转速表 → `execsnoop`，看进程什么时候启动的
            - 插故障码读取器 → `biolatency`，看磁盘 `IO` 快了还是慢了
            - 插油耗监测仪 → `tcpconnect`，看每秒建了多少新连接
        - 往 `OBD` 口上插什么设备都不会损坏发动机（验证器保证安全）
   - **以前排查故障**：得把发动机拆开（改内核代码、加内核模块），装不回去可能就炸了
   - **有了 eBPF**：插个诊断仪就能看到是哪里的问题，不拆发动机
   - **和内核模块的区别**：内核模块是"自己焊线接到发动机电路板上"，接错可能短路烧掉；`eBPF` 是*插到预留的 OBD 口上*，接口协议限死了能做什么、不能做什么

- **进阶思考**
    - **`eBPF` 和传统的 `iptables` 在实现网络策略上有什么本质区别？**
        - `iptables` 工作在协议栈的固定钩子点，每个数据包依次遍历规则链；规则越多，遍历时间越长。`eBPF` 可以在网络路径上更灵活地注入程序，在 `XDP`（`eXpress Data Path`）层直接处理数据包，绕过了内核协议栈的大部分逻辑。`Cilium` 使用 `eBPF` 实现的网络策略，单节点支持数万条规则时的性能下降远小于 `iptables`。这使得 `eBPF` 在大规模容器场景下逐渐成为 `iptables` 的高性能替代方案

    - **`eBPF` 有没有什么限制或缺陷？**
        - `eBPF` 不能调用任意内核函数，只能调用一组受限的辅助函数（`bpf helper`）；程序有指令数上限（目前 `100` 万条）；验证器在某些复杂场景下会拒绝加载虽然实际上安全的程序，且调试这类被拒的场景比较麻烦。另外 `eBPF` 程序升级需要替换或重新加载，不像内核模块那样有成熟的版本管理机制。


## 🤔 你用过哪些基于 eBPF 的排查工具？  
- **基于 `eBPF` 的排查工具核心优势是能直接观察内核行为而不需要修改代码或重启服务。它们覆盖了传统工具看不到或看不清楚的角落——比如某个进程为什么卡在 `D` 状态、磁盘 `IO` 到底是哪个文件在被写、网络延迟发生在内核哪一层。以下是从 `BCC` 工具集中最常用、最能解决实际问题的几个**：

    - **`execsnoop`**：抓"瞬逝进程"的利器
        - 跟踪系统中每一秒内启动的新进程，打印进程名、`PID`、父进程。特别擅长抓那种 `top` 里一闪而过、`ps` 根本抓不到的短生命周期进程
        - 能解决的传统问题：`CPU` 飙高但 `top` 按 `P` 排序找不到高占用进程。通常是某个定时任务每秒启动一次、执行完就退出的短进程在吃 `CPU`，`execsnoop` 能直接输出进程名和启动频率

    - **`opensnoop`**：看谁在偷偷读文件
        - 跟踪系统上每一次 `open()` 系统调用，打印哪一秒哪个进程打开了哪个文件、返回的文件描述符、执行用时
        - 能解决的传统问题：怀疑某个进程读了不该读的配置文件、或者启动卡在等待某个文件。跑一下 `opensnoop -p <PID>` 直接看进程启动时依次打开了哪些文件，卡在哪个文件上

    - **`biolatency`**：看磁盘 `IO` 是快还是慢
        - 统计磁盘 `IO` 延迟的分布情况，输出一个直方图，显示大部分 `IO` 落在哪个延迟区间。比如 `90%` 的 `IO` 在 `1~2ms` 内完成，但偶尔有几个 `IO` 冲到了 `100ms` 以上
        - 能解决的传统问题：`iostat` 只能看到平均延迟，看不出"大部分 `IO` 很快但偶尔有慢 `IO`"的抖动。`biolatency` 能看到延迟分布的全貌 ——— 如果有少量 `IO` 延迟远高于平均值，说明存储系统存在偶发性抖动，可能是 `GC`、`RAID` 重构或共享存储争用

    - **`tcpconnect`**：看新连接从哪里来
        - 跟踪系统上新建立的 `TCP` 连接，打印源 `IP`、目标 `IP`、目标端口、进程 `PID`。不关心已经存在的连接，只看"正在连"的连接
        - 能解决的传统问题：怀疑有程序在频繁连接外部服务、或遭受连接扫描。`tcpconnect` 能直接看到*是哪个进程在连哪个 `IP`* 

    - **`filetop`**：看哪些文件在读写最频繁
        - 按读写频率排序，每秒显示最活跃的文件和对应进程。相当于 `iotop` 的文件级版本
        - 能解决的传统问题：磁盘 `IO` 高但 `iotop` 只能看到进程级别，不知道这个进程具体在读写哪个文件。`filetop` 能直接告诉你 `/var/log/nginx/access.log` 的写入量最大，而不是笼统地告诉你*`nginx` 在写盘*

- **协助记忆**
    - `eBPF` 工具像给内核装了高清摄像头，看的是传统工具拍不到的画面
    - **`execsnoop`**：抓鬼 ——— 那些一闪而过的短命进程
    - **`opensnoop`**：盯梢 ——— 看进程偷偷读了哪个文件
    - **`biolatency`**：测脉 ——— 不是看平均快慢，是看有没有间歇性"卡一下"
    - **`tcpconnect`**：查岗 ——— 看谁在偷偷往外连
    - **`filetop`**：放大 ——— 不只看到哪个进程在写盘，连在写哪个文件都看到了

- **进阶思考**
    - **`biolatency` 和 `iostat` 的 `await` 指标是什么关系？有了 `iostat` 为什么还需要 `biolatency`？**
      - `iostat` 的 `await` 是所有 `IO` 请求的平均响应时间。但平均值的最大问题是 ——— 一次 `1000ms` 的超时 + `999` 次 `1ms` 的正常 `IO`，`await` 也只显示大约 `2ms`，完全看不出有严重超时。`biolatency` 输出的是延迟分布直方图，能一眼看出"大部分 `IO` 在 `1ms` 但有一条尾巴拖到 `100ms` 以上"——这通常是存储系统存在间歇性抖动的信号，平均值完全掩盖了这个问题

    - **这些 `BCC` 工具有没有什么限制？在什么场景下用不了？**
      - 首先需要内核版本支持（`4.9+` 大部分功能可用，更高级的功能需要 `5.x+`）。其次需要在机器上安装 `BCC` 或 `bpftrace` 工具集，有时还需要内核头文件来编译。对于"连 `apt install` 都没法跑的极度精简容器"场景，eBPF 可能用不了。但大部分发行版（Ubuntu 20.04+、RHEL 8+）都默认支持

- **扩展信息**
    `BCC` 工具集安装方式
    `BCC` 工具集有两个版本分支，参数和使用上略有差异：
    - **`BCC Python 版（原版）`** ：功能最全，脚本在 `/usr/share/bcc/tools/` 下，需要 `kernel-devel` 版本与当前运行内核版本严格匹配才能编译 `BPF` 程序
    - **`libbpf-tools 版（轻量版）`** ：`C` 语言重写，依赖更少、启动更快，放在 `/usr/sbin/` 下。需要内核开启 `BTF` 支持（`CONFIG_DEBUG_INFO_BTF=y`，`5.x+` 内核通常默认开启），不需要安装 `kernel-devel`
        | 操作系统                                     | `BCC Python` 版             | `libbpf-tools` 版                                  |
        | :------------------------------------------- | :-------------------------- | :------------------------------------------------- |
        | `Ubuntu 20.04+`                              | `apt install python3-bpfcc` | `apt install libbpf-tools`（`Ubuntu 24.04+` 默认） |
        | `RHEL` / `CentOS 8+` / `Alibaba Cloud Linux` | `dnf install bcc-tools`     | `dnf install libbpf-tools`（部分仓库可用）         |
        | `Debian 11+`                                 | `apt install python3-bpfcc` | `apt install libbpf-tools`                         |

    重点：`BCC Python` 版在 `RHEL` 系上需要 `kernel-devel` 匹配运行内核

    - `BCC Python` 版在使用时需要编译 `BPF` 程序，依赖于 `/usr/src/kernels/$(uname -r)/` 下的内核头文件
    - `kernel-devel` 版本必须和 `uname -r` 输出的运行内核版本完全一致，差一个小版本号都不行
    - 安装命令：`dnf install kernel-devel-$(uname -r)`
    - 如果内核已更新但未重启，运行版本和 `kernel-devel` 版本不一致时，`BCC` 工具会报 `cannot find kernel headers` 或 `failed to compile BPF module` 等错误

    `libbpf-tools` 不需要 `kernel-devel`，因为它使用预编译的 `BPF` 程序 + `CO-RE`（`Compile Once`, `Run Everywhere`）技术，在支持 `BTF` 的内核上无需编译即可运行。如果你的内核支持 `BTF`，优先安装 `libbpf-tools` 版，省去 `kernel-devel` 版本匹配的麻烦  
    
    安装后确认工具可用， 先确认安装的是哪个版本：  

    ```bash
    # libbpf-tools 版通常在这里
    ls /usr/sbin/*snoop
    # BCC Python 版通常在这里
    ls /usr/share/bcc/tools/
    # 确认工具是否能跑
    opensnoop -h
    ```
    如果报了 "`failed to compile BPF module`" 或 "`cannot find kernel headers`" 的错误，说明运行的是 `BCC Python` 版且 `kernel-devel` 没有正确安装

## 🤔 你用 BCC 具体解决过什么线上问题？
- **以下是行业内比较经典的案例** ：
    - **案例一**：`execsnoop` 抓"幽灵进程" ——— `CPU` 飙高但 `top` 找不到元凶
        - 现象：线上某台 `Web` 服务器 `CPU` 持续 `80%+`，但 `top` 按 `P` 排序后前几个进程 `CPU` 加起来不到 `30%`，始终有 `50%` 以上的 `CPU` 不知道被谁吃了
        - `execsnoop` 跑了几秒后输出显示，有一个 `/usr/local/bin/php-cgi` 进程每秒启动数十次，每次执行 `1~2` 秒就退出。原来是 `crontab` 配了一个每隔几秒就执行的 `PHP` 脚本，但 `PHP` 本身是 `CGI` 模式（不是 `FPM`），每次执行都要启动一个完整的 `PHP` 进程 
        - 原因：`PHP` 脚本执行完毕后进程退出，`top` 根本抓不到。`ps` 更看不到，因为它只拍当前快照   
        - 解决：将 `PHP` 运行模式从 `CGI` 切换为 `FPM`，减少进程频繁创建销毁的开销，`CPU` 从 `80%` 降到 `20%` 

    - **案例二**：`biolatency` 揪出磁盘"偶发性抖动" ——— `iostat` 看起来正常但业务超时
        - 现象：`MySQL` 偶尔出现几百毫秒的查询超时，但频率不高，一天几十次。`iostat -xz 1` 看了一小时，`await` 平均始终在 `2ms` 以下，怎么看都不像磁盘问题
        - `biolatency -ms` 跑了一小时后输出延迟分布直方图，显示 `99.9%` 的 `IO` 在 `1~4ms`，但有几个点落在了 `500~1000ms` 区间——虽然数量极少（不到总量的 `0.01%`），但每次出现就恰好卡住了 `MySQL` 的关键写入
        - 进一步排查发现 `MySQL` 的数据和日志在同一块 `SSD` 上，刷 `redo log` 时和大查询的写入争抢 `IO` 资源，偶尔触发写延迟毛刺
        - 解决：将 `redo log` 独立到一块 `NVMe` 上，业务侧的大查询做读写分离到从库

    - **案例三**：`tcpconnect` 查出"谁在往外连"——应用莫名其妙变慢
        - 现象：新上线的 `Java` 应用每隔一段时间就会卡住几十秒，但没有明显的 `CPU` 或内存波动
        - `tcpconnect` 跑了几分钟，卡住的时间点正好输出显示 `Java` 进程在频繁连接一个外部 `Redis` 实例（某公网 `IP:6379`），且连接超时导致线程长时间等待
        - 排查发现配置文件里 `Redis` 地址写错了（写成了测试环境的公网地址），测试环境 `Redis` 因为有防火墙策略限制，响应间歇性超时
        - 解决：修正 `Redis` 地址为内网地址，连接延迟从几十毫秒降到零点几毫秒，卡顿消失

- **协助记忆**
    - **`execsnoop`**：抓"死得快"的进程 ——— `top` 看不到的 `CPU` 杀手
    - **`biolatency`**：看延迟分布，不是看平均值——平均 `2ms`，不代表没有 `500ms` 的毛刺
    - **`tcpconnect`**：发现"偷偷外联"的进程——应用卡住不一定是自己慢，可能是等别人响应

- **进阶思考**
    - 为什么 `biolatency` 比 `iostat` 更适合发现间歇性 `IO` 抖动？
        - `iostat` 的 `await` 是所有 `IO` 请求的算术平均。假设 `1000` 次 `IO` 中 `999` 次是 `1ms`，`1` 次是 `1000ms`，`await` 算出来大约 `2ms` ——— 看起来完全正常，看不出有严重超时。而 `biolatency` 把每次 `IO` 按延迟区间归类输出直方图，那个落在 `1000ms` 区间的点会直接暴露出来。对于"大部分正常、偶尔抽风"的 `IO` 模式，直方图比平均值有更好的展示效果




---

> 作者: [0x5c0f](https://blog.0x5c0f.cc)  
> URL: https://blog.0x5c0f.cc/posts/other/%E8%BF%90%E7%BB%B4%E5%B8%B8%E8%A7%81%E9%A2%98-%E6%97%A5%E5%B8%B8%E7%BB%B4%E6%8A%A4.2/  

