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



## 🤔 Linux 系统无法启动，可能会有哪些原因？  
- **`Linux` 系统无法启动，原因可能分布在整个启动链路的任何一个环节——硬件、固件、引导、内核、init 进程。定位的关键是看卡在哪一步：风扇转不转、屏幕有没有输出、内核有没有打印、有没有进入 `emergency mode`，每一步对应不同的故障范围。**

    - **硬件层面**：机器没反应或反复重启
        - 按下电源键后机器完全没反应 → 电源故障或主板损坏。风扇转一下停、指示灯闪一下就灭
        - 机器反复重启 → 内存故障（常见于新加内存后）、`CPU` 过热触发保护关机、电源功率不足
        - `BIOS` 自检不过 → 蜂鸣器报警，根据报警声长短判断故障部件。`AMI BIOS` 报警长声通常对应内存故障、连续短声可能对应电源或显卡问题。没有蜂鸣器可以看主板上的 `Debug` 灯（如果有的话）
        - 排查方向：最小化硬件（只留 `CPU` + `一根内存` + `主板电源`）看能否启动，逐个替换确认故障件

    - **`Boot Loader` 层**：`GRUB` 出错
        - `grub>` 提示符但不加载内核 → `GRUB` 配置文件损坏或丢失。修复方式：`grub2-mkconfig -o /boot/grub2/grub.cfg`（RHEL 系）/ `update-grub`（Debian 系）
        - `Error`: `no such partition` 或 `Unknown filesystem` → `GRUB` 找不到 `/boot` 分区。通常是因为分区表被修改、磁盘顺序发生变化、或 `/boot` 文件系统损坏
        - `grub rescue>` 提示符 → `GRUB` 的 `core.img` 找不到正常的配置文件，只能通过 `rescue` 模式手动加载内核链
        - 排查方向：用安装 `U` 盘进入 `Rescue` 模式，`chroot` 后修复 `GRUB`

    - **内核层**：加载失败或 Panic
        - 内核文件缺失或损坏 → `vmlinuz-*` 文件被误删或 `initramfs` 损坏时，`GRUB` 启动内核后卡住或 `Kernel Panic`
        - 修复方式：`dracut -f`（RHEL 系）/ `update-initramfs -u`（Debian 系）重建 `initramfs`
        - 内核 `Panic` → 驱动不兼容、硬件故障（特别是内存故障导致内核解压时出错）、或关键内核模块加载失败
        - 排查方向：`journalctl -k` 看内核日志（如果有持久化）、在 `GRUB` 启动项按 `e` 编辑内核参数，加 `single` 进入单用户模式或加 `rd.debug` 查看 `initramfs` 阶段的详细输出。偶尔能启动但不稳定的话，尝试选旧版本内核启动

    - **`init` 与 `systemd` 层**：服务启动失败
        - 系统提示 `Welcome to Emergency Mode` 或进入维护模式 → `systemd` 在启动某个关键服务或挂载某个分区时失败。最常见的原因是 `/etc/fstab` 中某个挂载项配置错误或对应的磁盘不存在
        - 排查方式：输入 `root` 密码进入 `Emergency Mode`，`journalctl -xb` 查看本次启动日志，定位哪个 `service` 或哪个 `mount` 失败了。`mount -a` 测试 `fstab` 各挂载项是否有问题。修复后 `exit` 或 `systemctl reboot` 重启
        - 如果是根分区只读或 `fstab` 配置错误，需要先 `mount -o remount,rw /` 让根分区可写再编辑 `fstab`

- **协助记忆**
    - 启动是一个接力赛：`电源` → `BIOS` → `GRUB` → `内核` → `init` → `服务`，每一棒都可能掉棒。看卡在哪一棒就知道问题在哪一段
    - 三块表判断问题阶段：风扇转不转（硬件）→ 屏幕有没有 `GRUB` 菜单（`Boot Loader`）→ 有没有 `kernel` 打印信息（内核）→ 有没有 `login` 提示（服务层）
    - 救急三板斧：选旧内核启动 → 单用户模式修 `fstab` → `Rescue U` 盘重装 `GRUB`

- ****进阶思考****
    - **系统启动到 "*A start job is running for /dev/mapper/vg_data-lv_data*" 卡很久才跳过，是什么问题？**
      - 这是 `systemd` 在等待某个挂载点就绪，对应设备可能不存在或延迟响应。最常见的原因是 `LVM` 逻辑卷在 `/etc/fstab` 中配置了但系统启动时 LVM 还没激活。解决方案：确保 `lvm2` 服务在 `fstab` 之前启动（`systemctl enable lvm2-lvmetad`，但新版本中已由 `systemd` 的 `lvm2-pvscan@.service` 机制替代），或更直接的排查思路是确认 `/etc/fstab` 中是否使用了正确的设备 `UUID`，且对应的逻辑卷状态是否正常。如果等待的设备不再需要，直接在 `fstab` 中注释掉该条目重启即可恢复

    - **升级内核后启动卡住，重启选旧内核能正常进入。这种情况怎么定位是哪个驱动的问题？**
      - 在 `GRUB` 启动项按 `e` 编辑内核参数，加 `systemd.log_level=debug` 看具体卡在哪个模块加载上。更彻底的方式是启动旧内核后执行 `dmesg -T` 查看上次启动的内核日志，或者通过 `journalctl -k -b -1`（如果该日志已被持久化）对比新旧内核加载的模块差异。常见的问题驱动包括显卡驱动（NVIDIA）、存储控制器驱动、或者新内核中对某些老旧硬件的驱动适配不完善导致兼容性问题

## 🤔 Linux 服务器宕机，系统启动后怎么排查宕机原因？  
- **服务器宕机重启后，很多现场信息（进程状态、内存内容、网络连接）已经丢失了。但系统会在多个地方留下"遗言"——重启后立即检查这些遗言，就能还原宕机前的最后一刻发生了什么。关键是要在重启后第一时间去查，因为有些日志会在重启后被滚动覆盖掉。**

    - **第一步**：看内核日志——宕机前内核说了什么
        - **`journalctl -k -b -1`**：查看上次启动（`-b -1`）的内核日志，不包括本次启动的日志。这是排查宕机原因最优先的命令 
        - **重点搜索的关键字**：`Kernel panic`（内核崩溃，通常是硬件或驱动级的问题，下面通常会跟调用栈）、`Out of memory`（`OOM Killer` 杀进程，后面会跟被杀进程的 PID 和名称）、`hung_task_timeout`（某个任务卡在 `D` 状态超时，通常是 `NFS` 或存储链路问题）、`soft lockup` / `hard lockup`（CPU 被某个进程占死无法切换、或硬件中断卡死）、`I/O error` / `buffer I/O error`（磁盘坏道、线缆松动、RAID 卡故障）
        - 如果是内核启用了 `kdump`（通过 `kexec` 在内核崩溃时转储内存），`journalctl` 的 `-b -1` 可能不会保留崩溃前的完整内核日志。此时需要检查 `/var/crash/` 目录下是否有 `kdump` 生成的内存转储文件（`vmcore`），配合 `crash` 工具进行分析，可以精确定位到崩溃时哪个函数在跑、调用栈是什么

    - **第二步**：看系统日志——应用层和服务层的线索
        - **`journalctl -b -1 -p err`**：查看上次启动中级别为 `err` 及以上的日志，过滤掉 `info` 和 `debug` 消息
        - **重点关注**：`sshd` 的异常登录记录、关键服务的崩溃日志、`OOM Killer` 的记录、磁盘错误
        - 如果之前有持久化 `syslog`（`/var/log/messages` 或 `/var/log/syslog`），用 `grep` 搜索宕机时间点附近的异常记录。但注意关机前最后一刻的日志可能因为瞬间宕机来不急刷盘而没有落盘

    - **第三步**：看硬件健康记录——宕机前硬盘说过什么
        - **`smartctl -a /dev/sdX`**：查看硬盘的健康历史和自检日志。重点关注几个字段：
            - **`Power_Cycle_Count`**：断电次数。如果这个数值在宕机前后有增长，说明那次宕机是突然断电而非正常关机
            - **`Reallocated_Sector_Ct`**：已重映射的坏道数。这个数值持续增长说明硬盘正在老化，达到阈值后可能触发系统卡死或文件系统只读
            - **`UDMA_CRC_Error_Count`**：数据传输 `CRC` 校验错误数。这个数值高且持续增长说明数据线或接口有电气问题
            - **`Raw_Read_Error_Rate`**：原始读取错误率。异常升高说明盘片或磁头出了问题

        - **`dmesg -T`**：看本次启动过程中内核是否检测到上次的异常关机事件。如果出现 `recovering journal` 或 `clean`, `XXXX/XXXX files` 这类提示，说明上次是异常断电而非正常关机

    - **第四步**：看硬件状态——温度、电压、内存
        - **`sensors（lm-sensors）`**：查看主板各传感器是否记录了异常温度或电压。`CPU` 或主板温度如果出现超过 `90°C` 的记录，大概率是宕机前散热出了问题
        - **如果怀疑内存故障导致宕机**：`journalctl -k -b -1 | grep -i memory` 看是否有内存相关的错误记录。重启时检查机器是否在 `POST` 阶段检测到内存错误、开机自检是否正常通过

    - **第五步**：看历史趋势——不是宕机前有什么，而是宕机前一段时间在发生什么
        - **如果系统装了 `sar`**：`sar -u -f /var/log/sa/saXX`（XX 是日期）看宕机前 `CPU` 和内存负载变化
        - `sar -S -f /var/log/sa/saXX` 看 `Swap` 使用趋势
        - `sar -b -f /var/log/sa/saXX` 看磁盘 `IO` 趋势
        - 宕机前的 `sar` 数据如果显示不寻常的趋势（`CPU` 持续 `100%`、内存持续下降、`Swap` 突然升高），说明系统在崩溃前已经处于资源紧张状态，崩盘不是突然发生的而是持续积累的结果


- **协助记忆**
    - 重启后查宕机，四个地方找遗言：
        - 内核日志（`journalctl -k -b -1`）—— 内核死前最后说了什么
        - 系统日志（`journalctl -b -1 -p err`）—— 应用层和服务层死前喊了什么
        - 硬盘 `SMART`（`smartctl -a`）—— 硬盘有记录着健康的病历本
        - 历史趋势（`sar`）—— 看宕机前是不是已经饿了好几天了

    - "遗言"不一定完整：瞬间宕机可能来不及写日志，但 `SMART` 和 `sar` 不受宕机影响


- ****进阶思考****
    - **`journalctl -b -1` 输出为空，什么情况下会发生？**
        - 说明上次启动的日志因为存储空间紧张或配置的日志保留策略、系统日志覆盖机制等条件，在上次关机时就已经被清理了。`journald` 的日志是持久化到磁盘的，但它的保留策略默认受到 `SystemMaxUse`（日志占用磁盘空间的上限）的限制，超过上限会丢弃最早的日志。如果宕机时系统一直处于高负载状态产生大量日志 ，旧的日志可能在宕机前就被回收了。另一个可能是系统在宕机时根本来不及刷盘（突然断电或内核 `panic`），日志停留在内存缓冲区里没写出去就会丢失

    - **`kdump` 生成的 `vmcore` 文件怎么分析？**
        - `kdump` 在宕机时通过 `kexec` 启动一个备用内核，将崩溃时的内存内容完整转储到磁盘。生成的文件在 `/var/crash/` 下，通常比较大（几 `GB` 到几十 `GB`）。分析工具是 `crash`（`yum install crash kernel-debuginfo`），配合对应的内核符号文件，可以查看崩溃时的堆栈回溯、各 `CPU` 的状态、进程列表、内存分配情况。如果你没有分析师配合，最实用的做法是输出 `bt`（`backtrace`）命令的结果——它直接告诉你宕机时 `CPU` 在跑哪个函数，`80%` 的情况从调用栈就能判断是内核 `Bug`、驱动问题还是硬件故障

    - **如果 `sar` 也没有安装，还有其他历史数据可以参考吗？**
        - 如果日志已经被滚动覆盖且 `sar` 没装，还可以看 `Prometheus` 的历史图（如果配了监控）、上次备份额外备份时间、防火墙或交换机日志关联。如果这些都没有，排查难度会很大。所以核心建议是提前装好 `sar` 并开启 `kdump`，否则宕机后只能靠猜


## 🤔 Linux 文件系统损坏，如何修复？  
- **文件系统损坏通常是突然断电、磁盘坏道、内核 Bug、或强行关机导致的。修复的核心工具是 `fsck`（`filesystem check`），但不同类型的文件系统修复方式不同，而且错误的修复顺序可能让损坏更严重——所以第一步不是直接跑 `fsck`，而是先判断损坏的严重程度和文件系统类型。**

    - **第一步**：确认损坏的症状和范围
        - 系统启动时提示 `unexpected inconsistency` 或 `run fsck manually` ——— 说明内核在挂载前检测到了不一致，拒绝挂载
        - 挂载后读写报错 `Input/output error` ——— 说明有坏道或元数据损坏，文件系统可能进入只读保护状态
        - `dmesg` 中出现 `EXT4-fs error`、`journal has aborted`、`corrupt journal` ——— 日志区域损坏
        - 确认是哪个分区损坏：`mount | grep ro` 找到被强制只读的挂载点，`df -h` 看对应设备路径

    - **第二步**：不同文件系统的修复方式
        - **`ext3` / `ext4`**
            - 卸载分区：`umount /dev/sdX1`（根分区要进 `Rescue` 模式，不能在线卸载）
            - 先 `e2fsck -n /dev/sdX1` 检查但不修复，确认损坏范围
            - 再使用 `-p` 参数自动修复安全级别较高的问题（如 `inode` 计数不一致、`block bitmap` 错误等不会造成数据冲突的简单不一致场景）→ `e2fsck -p /dev/sdX1`
            - 然后使用 `-y` 参数对所有问题回答 `yes`，覆盖更广泛的元数据不一致场景：`e2fsck -y /dev/sdX1`。`-y` 会把块组描述符、扩展属性等内部结构相关的问题一并修复，但注意如果存在重复 `block` 分配（多个文件声称拥有同一个数据块），`-y` 可能会把其中一个文件的数据块断开，导致该文件内容损坏 
            - 修复过程通常将损坏的文件放到 `/lost+found/` 目录下，文件名会变成 `inode` 编号，内容需要人工去判断

        - **`XFS`**
            - `xfs` 和 `ext4` 不同，不能使用 `fsck` 修复。`fsck.xfs` 实际上什么都不做，只是返回 0
            - `xfs` 的修复工具是 `xfs_repair`，但需要在卸载状态下运行：`xfs_repair /dev/sdX1`
            - `xfs_repair` 比 `e2fsck` 慢很多——对于大分区（几 `TB` 以上）可能需要几十分钟甚至几个小时。加上 `-v` 参数实时查看进度
            - 如果日志损坏了，`xfs_repair` 会卡住要求先清除日志：`xfs_repair -L /dev/sdX1`（`-L` 清除日志，相当于丢弃了未完成的事务，可能导致最近写入的数据丢失）
            - 如果超级块损坏，`xfs` 会分区搜索备用超级块。`xfs_db -x -c "sb 0" -c "p" /dev/sdX1` 查看超级块信息，手动指定备用超级块位置

        - **`Btrfs`**
            - `Btrfs` 的修复工具是 `btrfs check`。`btrfs check --repair /dev/sdX1` 执行修复
            - `Btrfs` 设计上比 `ext4/xfs` 更强的自愈能力：如果使用了 `DUP` 或 `RAID1` 模式，`Btrfs` 可以自动从冗余副本中恢复损坏的数据，不需要手动跑修复命令


- **协助记忆**（以图书馆档案室为例）
    - **`ext4` 的 `fsck`**：档案室管理员逐一核对每个文件柜的目录索引（`inode`）和实际文件位置（`block`），发现串号就重新登记，实在找不对的文件放到"失物招领处"（`lost+found`）等你去认领
    - **`xfs` 的 `xfs_repair`**：不停核验整个档案室的账目记录（元数据），账对不上就洗掉最近的出入记录重新对账。`xfs` 跑 `xfs_repair` 时对大型分区的复查过程较慢，需要预留足够的执行时间
    - 不能直接 `fsck xfs`：用 `fsck` 修 `xfs`，`fsck` 只会看一眼说"这活我不干"就退出

- ****进阶思考****
    - **根分区文件系统损坏，无法正常启动系统，怎么修复？**
        - 用安装 `U` 盘或 `Live CD` 启动进入 `Rescue` 模式。在 `Rescue` 环境中，根分区没有挂载，可以直接执行 `fsck` 或 `xfs_repair`。如果坏的是根分区，注意要指定正确的设备路径（在 `Rescue` 环境中设备名可能和正常系统中的不一样，用· 或 `fdisk -l` 确认）。修复完成后 `exit` 退出 `Rescue` 环境重启系统

    - **在线上生产环境直接执行 `fsck -y` 会有什么结果？**
        - `-y` 对所有问题回答 `yes`，包括"这个文件引用了多个目录，是否删除其中一个引用"这类可能改错数据的问题。在在线环境下直接执行修复反而可能加剧系统的不稳定性，更安全的做法是先强制将根分区挂载为只读，用 `fsck -n` 检查确定损坏范围，然后申请变更窗口和业务停机时间，再卸载分区后执行修复。

    - **执行 `xfs_repair` 时发现超级块损坏，修复不了怎么办？**
        - `xfs` 超级块有多个备用副本（通常存储在分区的不同位置）。用 `xfs_db -x -c "sb 0" -c "p" /dev/sdX1` 查看当前超级块信息，如果显示异常，尝试指定备用超级块：`xfs_repair -b 1 /dev/sdX1`。如果所有超级块都已损坏且 `xfs_repair` 也无法恢复，那么只能把分区当作 `RAW` 设备尝试 `testdisk` 或 `photorec` 逐扇区恢复文件，或从备份中恢复数据。这是为什么 `xfs` 分区建议定期做 `xfs_metadump` 备份元数据的原因之一，至少能把文件系统的结构备份下来用于重建


## 🤔 Paxos 和 Raft 有什么作用？有哪些应用组件？  
- **`Paxos` 和 `Raft` 都是分布式共识算法，解决的核心问题是：在多个节点组成的集群中，如何让所有节点就某一个值（比如"谁是主节点"、"这条数据应该写在哪里"）达成一致，即使部分节点宕机或网络延迟。简单说就是让一群不互信的机器能统一意见。**
    - **`Paxos`**：共识算法的理论基础
        - 由 `Leslie Lamport` 在 `1990` 年提出，用"议会投票"的比喻来描述共识过程。`Paxos` 的正确性经过了严格的数学证明，是所有共识算法的理论基石
        - 但 `Paxos` 以难以理解、难以实现著称：`Lamport` 最初的论文用了一个关于议会和法案的寓言来阐述算法，导致很多人读不懂。后续虽然出现了 `Simplified Paxos`、`Multi-Paxos` 等变体，但实现复杂度仍然很高，不同团队的实现之间存在大量细节差异
        - 核心角色：`Proposer`（提案者）  发起投票、`Acceptor`（接受者）  参与投票决策、`Learner`（学习者）同步已达成一致的结果
        - 两阶段流程：`Prepare` 阶段（争取投票权）→ `Accept` 阶段（提交最终值）

    - **`Raft`**：为可理解性而生的共识算法
        - 由 `Diego Ongaro` 和 `John Ousterhout` 在 `2014` 年提出，目标非常明确：让 `Paxos` 变得容易理解和实现。`Raft` 把共识问题拆解成几个相对独立的子问题：领导者选举、日志复制、安全性、成员变更
        - 相比 `Paxos`，`Raft` 的关键改进是引入了强领导者（`Strong Leader`）概念 ——— 所有写入操作都通过领导者转发，日志只从领导者流向跟随者
        - 领导者选举：每个节点有三个状态 ——— `Leader`（领导者）、`Candidate`（候选者）、`Follower`（跟随者）。`Leader` 定期发送心跳维持权威，如果 `Follower` 在一定时间（选举超时）内没收到心跳，就转为 `Candidate` 并发起选举
        - 日志复制：`Leader` 接收客户端请求后写入自己的日志，然后将日志条目并行复制到所有 `Follower`，多数节点写入成功后提交
        - 任期（`Term`） ：每次选举一个新的任期编号，保证不同任期的 `Leader` 之间不会产生冲突
        - `Raft` 核心设计比 `Paxos` 更容易在工程层面落地，所以目前大多数工业级共识实现都采用 `Raft` 而不是 `Paxos`

    - **应用组件**
        - **`etcd`**：`Go` 语言实现的分布式键值存储，基于 `Raft` 实现共识。`Kubernetes` 使用 `etcd` 存储所有集群状态（节点信息、`Pod` 配置、`Secret` 等）。`etcd` 的 `Raft` 实现包含了 `PreVote` 等在生产环境中验证过的优化机制
        - **`Consul`**：`HashiCorp` 的服务发现和配置管理工具，基于 `Raft`。提供健康检查、`KV` 存储、多数据中心支持
        - **`TiDB` / `TiKV`**：`TiDB` 的底层存储引擎 `TiKV` 使用 `Raft` 进行数据复制和一致性保证，支持跨机房的 `Multi-Raft` 部署
        - **`ZooKeeper`**：基于 `ZAB` 协议（`ZooKeeper Atomic Broadcast`），`ZAB` 在核心思想上和 `Raft` 类似（领导者 + 原子广播），但 `ZAB` 的出现时间比 `Raft` 早。`Kafka` 和 `HBase` 等大数据组件依赖 `ZooKeeper` 做元数据管理  
        - **`CockroachDB`**：分布式 `SQL` 数据库，使用 `Raft` 做跨数据中心的共识复制

- **协助记忆**（以团队开会做决策为比喻）
    - **`Paxos`**：每个人都可以提方案、投票、反悔、再投票。结果是正确的（数学证明了），但会议开了三天还没结束。理论完美，实操痛苦———实现到一半你会发现有一堆边界情况要处理
    - **`Raft`**：先选一个组长（选举），所有人都听组长的（强领导者），组长做决定后通知大家（日志复制），多数人同意就执行（提交）。 组长挂了重新选一个继续干。清晰、简单、容易落地
    - `Paxos` 证明共识是可能的，`Raft` 证明共识是可以工程化的

- ****进阶思考****
    - **`ZooKeeper` 的 `ZAB` 和 `Raft` 有什么区别？它们不是同一个算法吗？**
      - 两者核心思路相似（`Leader` + 多数派确认），但细节不同。`ZAB` 保证的是以 `Leader` 的日志为准的可靠性——它保证 `Leader` 提交的日志不会丢失，但没有明确要求日志不能回退；`Raft` 对日志的连续性有更强的约束——日志条目一旦提交就不能回退，`Follower` 的日志必须严格按照 `Leader` 的日志顺序来。所以在某些边缘场景下（如 `Leader` 切换后日志不一致），两者的处理方式有所不同

    - **为什么 `Kubernetes` 选了 `etcd（Raft）`而不是 `ZooKeeper（ZAB）`作为存储后端？**
      - `Kubernetes` 早期实际上评估过 `ZooKeeper`，但最终选择了 `etcd`，有几个原因：`etcd` 更轻量（单二进制部署，不需要 `Java` 环境）、`API` 更简洁（基于 `gRPC` + 键值模型，对 `Kubernetes` 的场景足够用）、`Raft` 实现更清晰（`etcd` 的 `Raft` 库是 `Go` 社区最广泛使用的共识实现）。另外 `ZooKeeper` 是 `ZAB` 协议，和 `Kubernetes` 的控制器模型在交互方式上不如 `etcd` 直观


## 🤔 MBR 和 GPT 分区有什么区别？  
- **`MBR`（`Master Boot Record`）和 `GPT`（`GUID Partition Table`）是两种在磁盘上记录分区信息的标准。`MBR` 诞生于 `1980` 年代 `IBM PC` 时代，设计上有很多硬限制；`GPT` 是 `UEFI` 时代的替代方案，解决了 `MBR` 的主要限制。核心区别在分区数量上限、单盘容量上限、以及数据冗余保护上。**

    - **`MBR`（传统分区表）**
        - `MBR` 使用磁盘的第一个扇区（`512` 字节）来存储引导代码和分区表。分区表只占 `64` 字节，每条分区记录占 `16` 字节，所以最多只能记录 `4` 个主分区
        - 如果超过 `4` 个分区，需要将其中一个主分区设为"扩展分区"，里面再嵌套逻辑分区。这个嵌套结构很脆弱——如果扩展分区的分区表损坏，所有逻辑分区都会丢失
        - `MBR` 使用 `32` 位地址来寻址扇区。每个扇区通常是 `512` 字节，所以最大支持磁盘容量 = `2^32 × 512` 字节 = `2TB`。超过 `2TB` 的磁盘，多余的空间 `MBR` 寻址不到，完全无法使用
        - 分区表数据在磁盘上只有单份副本，没有备份。如果 `MBR` 区域被覆盖或损坏，整个磁盘的分区信息就丢了
        - 引导方式：`BIOS` 读取 `MBR` 中的引导代码，再由引导代码加载操作系统。这是传统 `BIOS` 模式的标准启动路径

    - **`GPT`（现代分区表）**
        - `GPT` 使用 `64` 位地址来寻址扇区，理论上支持的最大磁盘容量是 `9.4ZB`（实际受操作系统限制，但当前所有磁盘都远未达到这个上限）
        - `GPT` 没有主分区和扩展分区的概念，只有一个分区表数组，默认最多支持 `128` 个分区（`Windows` 限制，`Linux` 可以更多）
        - `GPT` 在磁盘的头部和尾部各保存一份分区表副本。如果头部损坏，可以从尾部自动恢复，抗损坏能力比 `MBR` 强很多
        - `GPT` 对分区表数据附加了 `CRC32` 校验，可以检测到数据损坏。`MBR` 没有任何校验保护
        - 引导方式：`GPT` 通常作为 `UEFI` 固件的分区方案。`UEFI` 从 `GPT` 分区的 `ESP`（`EFI System Partition`，一个 `FAT` 格式的特殊分区）中读取引导文件，不需要像 `BIOS` 那样从 `MBR` 的第一个扇区加载引导代码
        - `MBR` 和引导方式在 `GPT` 环境下不再耦合——你有有 `MBR` 的磁盘搭配 `UEFI` 启动，也有 `GPT` 的磁盘搭配传统 `BIOS` 引导（配合 `grub` 的 `BIOS` 引导分区）。但最常见的搭配是 `MBR` + `BIOS`、`GPT` + `UEFI`

    - **实际迁移时需要注意的误区**
        - 如果你有一块已经用了 `MBR` 的 `3TB` 磁盘，直接在 `fdisk` 中把分区表改成 `GPT`，分区表格式会变但数据不会自动转移。如果操作不当原有数据可能无法正常访问。所以分区表转换之前必须先做好数据备份

- **协助记忆**
    - `MBR` 是老旧的小本子：只能记 `4` 行（`4` 个主分区）、最大记 `2TB` 的账（单盘 `2TB` 上限）、只有一份记录没备份
    - `GPT` 是现代的电子账簿：能记 `128` 行、账本上限用到数据中心都够、头尾各存一份、还能检测数据是否被改过
    - 一句话选型：超过 `2TB` 的盘或者想分超过 `4` 个区 → 选 `GPT`。`2TB` 以下的小盘、传统 `BIOS` 引导 → `MBR` 也能用，但 `GPT` 也不冲突

- ****进阶思考****
    - **`MBR` 转 `GPT` 可以无损转换吗？**
        - 可以，但有限制。`gdisk` 工具提供了 `w` 命令将 `MBR` 无损转换为 `GPT`，前提是磁盘上已经有 `4` 个或更少的 `MBR` 分区，且分区没有超过第 `34` 个逻辑扇区（`GPT` 头部占用）。转换后会保留原有分区和数据，但建议操作前先备份完整的分区表。`gdisk -l /dev/sdX` 可以预览转换后的效果，不实际写入，确认没问题后再执行转换。但注意如果某个 `MBR` 分区在 `BIOS` 模式下引导了操作系统，转 `GPT` 后需要切换为 `UEFI` 模式才能启动。从 `MBR` 直接转 `GPT` 的操作不能直接迁移已有的操作系统引导，系统盘建议备份后重新安装操作系统重新分区再恢复数据

    - **一块磁盘上既有 `MBR` 又有 `GPT`，可能吗？**
        - 这个症状通常出现在 `GPT` 磁盘的保护 `MBR`（`Protective MBR`）上。`GPT` 规范在磁盘的第一个扇区写了一个假的 `MBR`，里面只有一个类型为 `0xEE` 的分区，标识整个磁盘已被 `GPT` 使用。这个保护 `MBR` 的作用是：如果旧版磁盘工具（只认识 `MBR`）插入这块盘，不会因为"看到空磁盘"而试图覆盖 `GPT` 分区表

    - **`MBR` 分区表损坏了，磁盘上的数据还在，怎么修复？**
        - `MBR` 分区表只有一份（没有备份），损坏后分区信息丢失，但数据块本身还在磁盘上。修复的核心思路是根据数据区域反推分区起始位置。先用 `fdisk -l` 或 `gdisk -l` 看能否读到任何残留的分区信息；如果读不到，用 `testdisk` 扫描磁盘扇区，它会尝试识别每个分区的文件系统超级块，重建分区表。重建后不要立即写入磁盘，先预览检查分区大小和文件系统类型是否正确。为防止修复过程中误操作导致数据二次受损，建议先用 `dd` 将整块磁盘的前几十 `MB` 备份到另一个存储设备上。如果 `MBR` 只是引导代码损坏（前 `446` 字节）而非分区表损坏（`64` 字节），可以通过 `grub2-install /dev/sda` 或 `dd if=/usr/share/syslinux/mbr.bin of=/dev/sda` 恢复引导能力，不影响分区数据

    - **`GPT` 分区表损坏了怎么修复？和 `MBR` 修复有什么不同？**
        - `GPT` 比 `MBR` 多了一层保护 ——— 它在磁盘尾部分区表的备份区域就有第二份完整的分区表副本，且附带 `CRC32` 校验。`GPT` 修复的核心思路就是从备份恢复主分区表。先用 `gdisk /dev/sdX` 进入交互模式，输入 `r`（恢复和转换选项），再输入 `b`（将备份 `GPT` 数据恢复到主 `GPT`）。如果连备份也损坏了，同样可以用 `testdisk` 扫描重建。如果 `GPT` 分区表的主备份都损坏但磁盘上还有文件系统的超级块残留，通过 `testdisk` 扫描数据区域的超级块特征也能重新构建分区表

## 🤔 进程和线程有什么关系和区别？ 
- **进程和线程是操作系统调度执行单元的两个层级。进程是资源分配的最小单位，线程是 `CPU` 调度的最小单位。一个进程可以包含一个或多个线程，这些线程共享进程的资源（内存空间、文件描述符），但各自有独立的栈和寄存器上下文。**

    - **进程是*一个正在运行的程序***
        - 进程拥有独立的虚拟地址空间（`4GB` 在 `32` 位系统上）、独立的文件描述符表、独立的信号处理方式、独立的进程上下文
        - 每个进程由内核用一个 `task_struct` 结构体管理，包含 `PID`、内存映射、打开的文件列表、信号处理函数等
        - 进程之间的隔离性强 ——— 一个进程崩溃不会直接导致其他进程崩溃

    - **线程是*进程内的一条执行路径***
        - 线程共享进程的虚拟地址空间（堆、全局变量、静态数据）、共享文件描述符、共享信号处理方式
        - 线程拥有自己独立的栈空间、独立的寄存器上下文、独立的线程 `ID`（`TID`）
        - 线程之间的通信效率高（直接读写共享内存），但需要额外的同步机制（互斥锁、信号量）来避免竞态条件
        - 一个线程崩溃（如段错误）通常会导致整个进程崩溃
    
    - **进程和线程创建和切换的成本差异**
        - 进程创建（`fork()`）需要复制父进程的页表、文件描述符表、信号处理表等，开销较大
        - 线程创建（`pthread_create()`）只需要分配线程栈和寄存器上下文，不需要复制地址空间，开销比进程小得多
        - 进程切换涉及切换地址空间（刷新 `TLB`），线程切换在同一进程内不涉及地址空间切换（`TLB` 命中率高），所以线程上下文切换比进程上下文切换快

- **协助记忆**（以公司和员工为例）
    - **进程 = 一家公司**：有独立的办公室（地址空间）、独立的大门进出（文件描述符）、独立的营业执照（PID）。W 公司倒闭，旁边的 B 公司不受影响
    - **线程 = 公司里的一个员工**：共享公司办公室、共享饮水机和打印机（堆、全局变量），但每个人有自己的工位（栈空间）、自己的笔记本（寄存器）。一个员工离职（线程退出）不影响公司，但一个员工引爆了办公室（线程崩溃），整个公司都完蛋
    - **进程间通信像公司间合作**：发传真（Socket）、通过第三方（共享文件）。线程间通信像公司内部协作：直接开会（共享内存），但要用会议预约（锁）避免冲突

- ****进阶思考****
    - **有了多进程为什么还要多线程？多进程有什么多线程做不到的吗？**
      - 各有所长。多线程的优势在于共享数据和低切换成本 ——— `Web` 服务器处理请求时需要访问同一个缓存池，用多线程比多进程高效，因为线程共享地址空间不需要额外的` IPC` 机制。多进程的优势在于强隔离和安全性 ——— `Chrome` 浏览器每个 `Tab` 一个进程，一个 `Tab` 崩溃不会干掉整个浏览器；如果是单进程多线程，一个标签页崩了整个浏览器都跟着没。另外在 `Python` 等有全局解释器锁的语言下，多线程在 `CPU` 密集型场景无效，而多进程可以绕过 `GIL` 限制

    - **`fork()` 出来的子进程和父进程共享了什么？不共享什么？**
      - 子进程是父进程的副本。共享的是程序代码段（`Code Segment`）和只读数据——内核通过写时复制机制，在父进程或子进程尝试写入共享的物理内存页时才进行复制，在此之前它们指向同一块物理内存。不共享的是 PID、文件描述符表、信号处理方式、定时器、异步 IO 等进程级别的资源。不共享的这部分意味着子进程的 task_struct 是独立的——包括它的内存管理结构、进程树关系、挂起的信号等


## 🤔 高并发场景下，系统本地端口耗尽如何解决？  
- **本地端口耗尽是指系统作为连接发起方（客户端）时，可用的临时端口被全部占用，无法再建立新的出站连接。每个 `TCP` 连接由四元组（`源 IP`、`源端口`、`目标 IP`、`目标端口`）唯一标识。对于同一个目标（`目标 IP` + `端口固定`），系统需要为每个新连接分配一个不同的本地临时端口。临时端口范围是有限的（默认 `32768` ~ `60999`，约 `28000` 个），一旦占满，新的出站连接就会失败，报 `Cannot assign requested address`。**  
*虽然表面上和 `TIME_WAIT` 直接相关，但端口耗尽的本质是连接生命周期和端口复用策略共同作用的结果。*  
    - **扩大了端口范围但未触及本质**  
        - 调整 `net.ipv4.ip_local_port_range` 是很多人的第一反应  
        - `sysctl -w net.ipv4.ip_local_port_range="1024 65535"`  
        - 这一步有用吗？短期有用。从 `28000` 个端口扩大到约 `64000` 个，能把天花板抬高。但如果应用的并发连接需求超过了这个数，或者连接释放得不够快，扩大的端口范围只能推迟耗尽的时间，不能根除问题  
    - **根本问题：不是端口不够，是端口被卡在 TIME_WAIT 里**
        - 出站端口耗尽的场景，`99%` 的原因不是端口被用掉了，而是端口被卡在 `TIME_WAIT` 状态要等 `60` 秒才能释放
        - 大量短连接频繁建连断连 → 主动关闭方产生大量 `TIME_WAIT` → `TIME_WAIT` 和新的连接是同一个目标 `IP:端口` → 端口被 `TIME_WAIT` 霸占，新连接分配不了
        - 针对这个场景，核心优化方向不是扩端口范围，而是加快端口释放和复用
    - **三个层面解决端口耗尽**
        - **内核参数优化**（最快见效）
            - `net.ipv4.tcp_tw_reuse = 1`：允许内核在分配新出站连接时，复用还在 `TIME_WAIT` 状态的端口（出站连接专用，需要配合 `tcp_timestamps` 使用 —— 该参数默认为 `1`）
            - `net.ipv4.ip_local_port_range = 1024 65535`：扩大可用端口范围
            - `net.ipv4.tcp_fin_timeout = 15`：缩短 `FIN_WAIT2` 超时，减少连接状态机的残留时间
        - **应用层改造**（最彻底的方案）
            - 使用长连接替代短连接：每次建连都有开销，如果能让一个连接处理多个请求再关闭，端口占用量线性下降。数据库连接池、`HTTP Keep-Alive`、`Nginx` 到后端的 `upstream keepalive` 都是这个思路
            - 连接池：提前建好一批连接，请求来时直接从池子里取，用完归还而不是关闭。池子的最大连接数可控
            - 改用连接复用技术：`HTTP/2` 的多路复用、`gRPC` 的长连接，都能大幅减少连接数
        - **架构层面优化**
            - **扩展后端节点**：把请求分散到多个后端服务器上。每增加一台后端，同样的本地端口池就能多一套可复用的容量。实践中通常通过负载均衡器或 `DNS` 轮询来实现。这不是解决端口耗尽的专用方案，而是高并发架构本身就该做的事情

- **协助记忆**
    - 端口不够是表象，`TIME_WAIT` 才是元凶：一个端口被 `TIME_WAIT` 卡 `60` 秒，你扩大端口范围只是从 `60` 秒满 `28000` 个变成 `60` 秒满 `64000` 个，本质上还是不够
    - 三个方向：让 *`TIME_WAIT` 能被复用（`tcp_tw_reuse`）*、*让连接变长不用频繁断（长连接）*、*让目标分散开多几个地址复用端口（增加端点）*

- ****进阶思考****
    - **`tcp_tw_reuse` 启用后，会不会出现数据错乱？比如旧的连接数据漂到了新连接上？**
        - 不会。`tcp_tw_reuse` 只有在开启 `tcp_timestamps` 的前提下才生效。内核在复用 `TIME_WAIT` 端口时，会检查新连接的初始序列号是否大于旧连接的最后一个序列号，且时间戳是否更新。只有满足条件才允许复用，这是 `TCP` 协议（`RFC 1323` / `PAWS`）层面的保证。所以它不是"强行复用"，而是在确认安全的前提下才复用

    - **服务端（被动接受连接那一侧）会出现端口耗尽吗？**
        - 通常不会。服务端的端口是固定的（如 `80`、`443`），不占用临时端口范围。你见过 `Nginx` 因为端口耗尽而拒绝新连接吗？没有。服务端的瓶颈通常是文件描述符上限、连接数上限、或者线程池/进程池的上限，不是端口数。但有一种例外：如果服务端同时也在主动连接其他服务（比如 `Nginx` 作为反向代理连后端），那么它作为客户端的那一侧仍然会有端口耗尽的风险。这就意味着 `Nginx` 这一侧的临时端口也会被大量连接消耗掉

    - **容器化环境中端口耗尽问题有什么特殊性？**
        - 容器化环境中，多个容器共享宿主机的内核。如果宿主机的 `ip_local_port_range` 被所有容器共用，每个容器发出的出站连接都会消耗宿主机的临时端口池。更常见的是，容器从宿主机 `SNAT`（源地址转换）出去时，所有容器共享宿主机的一个或几个公网 `IP`，这时端口耗尽发生在 `SNAT` 网关上，而不是在容器内部。在 `Kubernetes` 中，如果 `Pod` 通过 `NodePort` 或 `externalIP` 对外发起大量出站连接，宿主机层面的端口耗尽可能会发生


## 🤔 说一些常用的 Linux 内核参数？  
- **内核参数通过 `/proc/sys/` 暴露，`sysctl` 命令管理。这些参数可以在不重启系统的情况下调整内核行为。以下按子系统分类，列出线上环境最常调整的参数。**
    - **网络层**
        - **`net.core.somaxconn`**：监听队列（`backlog`）的最大长度，默认 `128`。高并发 `Web` 服务建议调大到 `65535`，否则突发流量下请求会被内核直接丢弃
        - **`net.ipv4.tcp_tw_reuse`**：允许复用 `TIME_WAIT` 状态的端口用于新出站连接，配合 `tcp_timestamps` 使用（该参数默认开启）。高并发短连接场景的出站侧必开
        - **`net.ipv4.tcp_fin_timeout`**：`FIN_WAIT2` 超时时间，默认 `60` 秒。可适当调低到 `15~30` 秒，减少连接状态残留
        - **`net.ipv4.tcp_keepalive_time`**：`TCP KeepAlive` 空闲等待时间，默认 `7200` 秒。如果希望更快检测到死连接，可调低到 `600~1200` 秒
        - **`net.core.rmem_max` 和 `net.core.wmem_max`**：`socket` 接收和发送缓冲区的最大值。配合 `tcp_rmem` 和 `tcp_wmem` 调整，对高带宽长肥网络（如跨洲链路）场景有明显效果
        - **`net.ipv4.tcp_syncookies`**：应对 `SYN Flood` 攻击的核心防御，默认已开启（1）。通过 `SYN Cookie` 机制在 `TCP` 半连接队列满时仍能响应合法连接

    - **内存与虚拟内存**
        - **`vm.swappiness`**：控制内核回收内存时，优先回收 `Page Cache` 还是优先换出匿名页。默认 `60`。数据库服务器建议设 `1~10`，保留更多内存给应用
        - **`vm.dirty_ratio` 和 `vm.dirty_background_ratio`**：控制脏页刷盘。`dirty_background_ratio`（默认 `10%`）到达后内核后台开始刷盘，`dirty_ratio`（默认 `20%`）到达后进程写入会阻塞等待刷盘完成。这两个值在大量写入场景下影响 `IO` 峰值和抖动
        - **`vm.vfs_cache_pressure`**：控制内核回收 `dentry` 和 `inode` 缓存的积极程度。默认 `100`，设为 `50` 会让内核更慢地回收这些缓存，适合有大量文件操作的服务；设为 `200` 会加快回收，适合内存紧张的场景
        - **`vm.overcommit_memory`**：控制内存过量使用的行为。`0` 表示启发式（默认最常用的策略，内核通过估算判断申请的内存是否可以分配，合理拒绝可能引发 `OOM` 的过度分配），`1` 表示总是允许超额分配（即使申请的内存远超实际可用，也通常允许分配），`2` 表示不允许超额。数据库和稳定性敏感的服务建议设为 `2`（配合 `overcommit_ratio` 控制上限）

    - **文件系统与 IO**
        - **`fs.file-max`**：系统级文件描述符上限，默认值通常较低。高并发服务需要调大到 `2097152` 或更高。如果这个值不够，`too many open files` 错误会优先于进程内部的上限报错
        - `fs.nr_open`：单个进程的文件描述符上限，默认 `1048576`。需要配合 `ulimit -n` 一起调整，确保进程级和系统级的一致性
        - `fs.inotify.max_user_watches：inotify` 监控的文件数上限。如果使用 `systemd`、`logtail` 或 `watchexec` 等工具监控大量文件时可能耗尽这个配额

- **协助记忆**
    - **网络三兄弟**：`somaxconn`（队列长度）→ `tw_reuse`（端口复用）→ `tcp_fin_timeout`（快速释放）
    - **内存两兄弟**：`swappiness`（倾向回收什么）→ `dirty_ratio`（脏页多少开始强制刷盘）
    - **文件两兄弟**：`file-max`（系统能开多少文件）→ `nr_open`（一个进程能开多少文件），系统级卡进程级的上半句，光改进程上限不调整系统上限时，瓶颈往往卡在系统侧

- **进阶思考**
    - **调整 `sysctl` 参数后立即生效，但重启后就丢了。如何让参数在重启后仍然生效？**
      - 将参数写入 `/etc/sysctl.d/` 目录下的 `.conf` 文件中（如 `/etc/sysctl.d/99-tuning.conf`），系统启动时会自动加载该目录下所有配置文件。然后 `sysctl --system` 重新加载所有配置。不推荐直接编辑 `/etc/sysctl.conf`，升级系统或重新安装包时可能被覆盖

    - **调整了 `net.core.somaxconn=65535`，但 `ss -lnt` 看到的 `Send-Q` 还是 `128`，为什么？**
      - 因为内核参数只设置了上限，应用程序在调用 `listen()` 时传入的 `backlog` 参数可能小于这个值。`Nginx` 的 `listen` 指令默认 `511`，`Redis` 默认 `511`，需要分别在各自的配置文件中调整。内核参数是"天花板"，应用参数才是"实际使用值"


- **扩展信息**
    - **`sysctl` 配置的加载顺序与优先级**
        - 系统启动时按字典序加载 `/etc/sysctl.d/*.conf`，最后加载 `/etc/sysctl.conf`。因此 `99-zz-sysctl.conf` 的优先级高于 `10-*.conf` 等系统默认配置，可以覆盖默认值
        - `sysctl -p <file>` 只加载指定文件。`sysctl --system` 按完整顺序加载所有文件

    - **`fs.file-max`、`fs.nr_open`、`ulimit -n` 三者的关系**
        - `fs.file-max`：系统级上限，所有进程总共能打开的文件描述符数量
        - `fs.nr_open`：单个进程的上限，默认 `1048576`，不能超过这个值
        - `ulimit -n`：`Shell` 会话级限制，不能超过 `fs.nr_open`
        - 调整路径：先改 `fs.file-max`（系统级）→ 再改 `fs.nr_open`（进程级天花板）→ 最后改 `/etc/security/limits.conf`（用户级）

    - **已废弃或无效的参数**
      - `net.ipv4.tcp_tw_recycle`：`Linux 4.12` 已移除，写了也不报错但不生效
      - `net.ipv4.route.gc_timeout`：`5.x+` 内核已移除路由缓存，此参数无效

    - **`conntrack_max` 与 `hashsize` 的配套关系**
      - `conntrack_max` 增加后如果不同步增大 `hashsize`，会发生哈希冲突，导致查找性能下降
      - `hashsize` 在模块加载时指定：在 `/etc/modprobe.d/nf_conntrack.conf` 中写入 `options nf_conntrack hashsize=N`
      - 建议 `hashsize = conntrack_max / 4`
      - `hashsize` 不能通过 `sysctl` 在线修改，需要重启或卸载模块后重新加载

## 🤔 在 CDN 平台上刷新了一条资源，如何验证刷新生效？  
- **`CDN` 刷新（`P purge`）是通知边缘节点丢弃缓存、回源站重新拉取最新内容。但由于 `CDN` 是多级缓存架构（边缘节点 → 中间层 →源站），刷新指令下达到所有节点需要时间，且各节点生效速度不一致。验证的关键是确保*所有主要区域的节点都拿到了新内容*。**

    - **基础验证**：强制回源与缓存状态判断
        - 最关键的一点是使用 `curl` 请求时需要附带与浏览器不同的头部，避免本地 `DNS` 或浏览器缓存干扰结果。主要通过检查响应头中的缓存状态字段来判断
        - 最可靠的方式是通过类型化请求指定特定节点来验证。阿里云 `CDN` 可以通过 `?uid=xxx` 参数追踪；腾讯云 `CDN` 部分地区边缘节点可以直接通过 `curl -vo /dev/null -H 'Pragma: no-cache' -H 'Cache-Control: no-cache'` 来绕过客户端缓存
        - 主要判断依据：
            - 检查 `X-Cache` 响应头：`HIT` 表示命中了节点缓存，`MISS` 表示未命中或回源拉取
            - 检查 `Last-Modified` 和 `Content-Length` 是否已更新为新版本文件的内容特征
            - 检查响应头中的回源状态码：如果出现 `TCP_HIT` 而实际内容已更新，说明可能命中了中间层缓存

    - **区域覆盖验证**：多节点测试
        - 单点验证不足以确认全局生效。`CDN` 服务商按区域部署边缘节点，可能出现华东节点已刷新但华南节点还没命中新内容的情况
        - 使用在线多地拨测工具（如 这些平台通常可以选多个城市同时发起请求）
        - 在目标地域的云服务器上直接验证
            ```bash
            # 通过指定 curl 的超时时间与重定向跟随，确保完整请求链
            curl -svo /dev/null --max-time 10 -L "<资源URL>" 2>&1 | grep -iE '(x-cache|x-nws-log|x-server-cache|via|age|last-modified)' 
            ```

    - **内容一致性验证**：比对摘要
        - 确认内容本身是否正确，而不仅仅是缓存状态变了
        - 下载资源后计算哈希值，与源站的原始文件比对：
            ```bash
            # 从 CDN 拉取并计算
            curl -s "<资源URL>" | md5sum
            # 从源站直接拉取并计算
            curl -s "<源站URL>" | md5sum
            ```
- **协助记忆**
    - **`CDN` 刷新生效验证三板斧**：
        - 看 `HTTP` 头：`X-Cache` 从 `HIT` → `MISS`
        - 看文件特征：`Last-Modified` / `Content-Length` 变了没有
        - 多维测试：不同区域的节点都要确认，等全球同步


## 🤔 Linux 系统中 GRUB 是什么，有什么作用？  
- **`GRUB`（`GRand Unified Bootloader`）是 `Linux` 系统中最常用的引导加载程序（`Boot Loader`）。它的核心作用是在操作系统启动之前，把内核从磁盘加载到内存并跳转执行。如果把系统启动比作接力赛，`GRUB` 是第二棒 ——— `BIOS/UEFI` 把控制权交给 `GRUB`，`GRUB` 再把控制权交给内核。**

    - **`GRUB` 的核心功能**
        - 加载内核和 `initramfs：GRUB` 认识文件系统（`ext4`、`xfs`、`btrfs` 等），能从 `/boot` 目录下读取内核文件（`vmlinuz-*`）和 `initramfs`（`initrd.img-*`），加载到内存中指定位置，然后跳转到内核入口执行
        - 多系统启动菜单：在启动时显示可选的系统列表，你可以选择启动哪个内核版本或者进入其他操作系统。按上下键选择，按回车确认
        - 启动参数编辑：在 `GRUB` 菜单界面按 `e` 键可以临时编辑内核启动参数。这是排查启动故障最常用的功能——比如加 `single` 进入单用户模式、加 `rd.debug` 查看 `initramfs` 阶段的详细输出、加 `systemd.unit=emergency.target` 直接进入紧急模式
        - 链式加载：`GRUB` 可以把启动控制权交给另一个 `Boot Loader`（如 `Windows` 的 `Boot Manager`），实现多系统共存

    - **`GRUB` 的两个主要版本**
        - **`GRUB Legacy（GRUB 1）`**：旧版本，配置文件是 `/boot/grub/menu.lst`。已基本被淘汰，只有极旧的发行版还在用
        - **`GRUB 2`**：目前所有主流发行版的默认引导程序。配置文件是 `/boot/grub2/grub.cfg`（`RHEL` 系）或 `/boot/grub/grub.cfg` （`Debian` 系），但这个文件由 `grub2-mkconfig` 自动生成，不要手动编辑。用户的自定义修改应写在 `/etc/default/grub` 文件中，然后运行 `grub2-mkconfig -o /boot/grub2/grub.cfg` 重新生成配置

    - **`GRUB` 的启动阶段**
        - **`GRUB` 分为三个阶段**：
            - **第一阶段（`boot.img`）**： 写入 `MBR` 或 `GPT` 的 `BIOS` 启动分区，引导加载 
            - **第二阶段（`core.img`）**： 包含文件系统驱动，能读取 `/boot/grub/` 下的配置和模块；
            - **第三阶段**： 加载菜单配置、显示启动菜单、加载选中的内核和 `initramfs`。`UEFI` 下的工作方式有所不同——`UEFI` 直接从 `ESP` 分区读取 `GRUB` 的 `EFI` 可执行文件（如 `grubx64.efi`），不需要 `MBR` 中的第一阶段的处理

- **协助记忆**（以按下电源键到内核启动为例）
    - `BIOS`/`UEFI` = 机场出发大厅的大屏幕，告诉你该去哪个登机口
    - `GRUB` = 登机口的摆渡车，把你从候机厅送到飞机下面
    - `内核` = 飞机本身。摆渡车把你运到飞机下面（加载内核到内存），你登上飞机后摆渡车的任务就结束了。内核起来后自己接管一切

- **进阶思考**
    - **`GRUB` 配置文件 `/boot/grub2/grub.cfg` 为什么不能手动编辑？**
        - 这个文件由 `grub2-mkconfig` 根据 `/etc/default/grub` 和 `/etc/grub.d/` 目录下的脚本自动生成。下次内核更新（或你改了 `/etc/default/grub` 后重新生成配置）时会完全覆盖之前的文件。如果你直接在 `grub.cfg` 里改内容，下一次内核更新时修改就丢了。需要自定义配置应该写到 `/etc/default/grub` 中（如添加 `GRUB_CMDLINE_LINUX` 参数），然后 `grub2-mkconfig -o /boot/grub2/grub.cfg` 重新生成

    - **`GRUB` 菜单坏了，只能看到 `grub rescue>` 提示符，怎么恢复？**
        - `grub rescue>` 是救援模式，`GRUB` 找不到正常的配置文件和内核模块。先 `set prefix=(hd0,msdos1)/boot/grub` 手动指定 `GRUB` 文件所在分区，再 `insmod normal` 加载 `normal` 模块，最后 `normal` 进入菜单界面。进入系统后运行 `grub2-install /dev/sda` 重装 `GRUB` 到磁盘引导扇区，再 `grub2-mkconfig` 重建配置文件

## 🤔 Linux 文件权限里所有者、所属组、其他用户权限分别指什么？  
- **`Linux` 文件权限将系统上的用户分为三个角色层级，每个层级可以独立设置`读（r）`、`写（w）`、`执行（x）`权限。这三个层级从窄到宽依次是：`所有者（文件属于谁）`→ `所属组（和所有者同组的人）`→ `其他用户（剩下的所有人）`。**

    - **所有者（`Owner` / `User`）**
        - 文件的创建者，或者被 `chown` 指定的用户
        - 通常只有 `root` 或文件创建者能修改文件的所有者。`chown user1 file` 将 `file` 的所有者改为 `user1`
        - 所有者的权限在 `ls -l` 中显示为第一组 `rwx`（如 `-rwxr--r--` 中的第一个 `rwx`）

    - **所属组（`Group`）**
        - 文件所属的用户组，组内的所有成员共享对该文件的组权限
        - 一个用户可以同时属于多个组，但文件只能归属于一个组
        - `chgrp` 或 `chown :group` 修改文件的所属组
        - 组用户的权限显示为第二组 `rwx`（如 `-rwxr--r--` 中的 `r--`）

    - **其他用户（`Others`）**
        - 既不是文件所有者、也不在文件所属组里的所有系统用户
        - 通常情况下，对其他用户的权限应该设为最小——普通文件给 `r--`（只读），可执行程序给 `r-x`（可读可执行但不允许修改）。完全不给权限（`---`）在某些场景下也会用到
        - 其他用户的权限显示为第三组 `rwx`（如 `-rwxr--r--` 中的最后一个 `r--`）

    - **权限的数字表示法**
        - 每个角色用三位二进制数表示：`r=4`、`w=2`、`x=1`，三者相加得到权限值
        - `rwxr-xr-x` = 所有者 `7（4+2+1）`+ 组 `5（4+1）`+ 其他 `5（4+1）` = `755`
        - `-rw-r--r--` = `644`，这是文本文件的默认权限
        - `-rwx------` = `700`，只允许所有者自己读写执行

- **协助记忆**
    - 三个角色就是三层围墙：
      - 所有者：房子的主人，可以进屋随便翻（`rwx`）
      - 所属组：主人的家人，可以进门用客厅，但不能进主卧（`r-x`）
      - 其他用户：路过的陌生人，只能隔着窗户看（`r--`），连院子都进不来

    - 数字权限 `755` 口诀：主人全开 `7`，同组读跑 `5`，外人只读 `5`

- **进阶思考**
    - **`chmod` 的 `u+w`、`g-w`、`o=rx` 这种符号模式是什么意思？**
      - 符号模式通过角色标识（`u=所有者`、`g=组`、`o=其他`、`a=全部`）配合操作符（`+=增加`、`-=移除`、`==设为`）来精确修改某个角色的某个权限位，而不影响其他位。`chmod u+x file` 只给所有者加执行权限，`chmod go-w file` 移除所有者和组的写权限，`chmod a=r file` 把所有角色都设为只读。这在只需要调整一个角色的某一个权限位时比数字法更精确、更安全 ——— `chmod 755` 会同时覆盖三个角色的所有权限位，而 `chmod g+w` 只动组权限

    - **如果一个文件的权限是 `-r--------（400）`，文件内容是 `Shell` 脚本，所有者能运行它吗？**
        - 分两种情况。直接通过路径运行（`./script.sh` 或 `/path/to/script.sh`）不行，因为内核的 `execve()` 系统调用需要执行权限位。通过解释器间接运行（`sh /path/to/script.sh`）可以，因为 `Shell` 读取文件内容只需要读权限（`r`），而文件有 `400` 权限。`root` 也是一样——直接执行需要 `x` 位，`root` 不能绕过内核的可执行检查；但通过 `sh` 间接执行同样可以

    - **要进入一个目录（如 `cd /var/log`），需要什么权限？目录的 `rwx` 和文件的 `rwx` 含义有什么不同？**
        - 目录的 `rwx` 含义和文件完全不同：
            - **r（读）**：允许列出目录下的文件名（ls），但读不到文件元数据信息
            - **w（写）** ：允许在目录下创建、删除、重命名文件。删文件是目录的写权限决定的，不是文件本身的写权限
            - **x（执行）** ：允许进入该目录（cd），以及访问目录中的文件。x 是目录最基本的权限——没有 x 权限，即使知道文件路径也访问不到文件内容
            - **常见陷阱**：一个目录的权限是 `drw-rw-rw-`（赋予了 r 和 w 但没有 x），结果是任何人都能列出文件名，但读不到任何文件内容也进不去
            - **典型配置**：Web 静态文件目录通常设为 `755（rwxr-xr-x）`，共享目录设为 `775（rwxrwxr-x）`

- **扩展信息**
    - **特殊权限位**：`SUID`、`SGID`、`Sticky Bit`
      - **`SUID`（`Set User ID`，`chmod u+s`，数字 `4xxx`）** ：当可执行文件设置了 `SUID`，任何用户执行该文件时，进程的 `effective UID` 会变成文件所有者。典型例子：`/usr/bin/passwd` 设置了 `SUID（rwsr-xr-x）`，普通用户才能通过它修改 `/etc/shadow`。`SUID` 只对二进制可执行文件有效，对 `Shell` 脚本不生效（内核出于安全考虑忽略脚本的 `SUID`）
      - **`SGID`（`Set Group ID`，`chmod g+s`，数字 `2xxx`）** ：对文件的作用和 `SUID` 类似——进程的 `effective GID` 变成文件所属组。对目录的作用不同：目录设置了 `SGID` 后，在该目录下新建的子文件和子目录会自动继承目录的所属组，而不是创建者的默认组。这是实现团队共享目录的标准方式
      - `Sticky Bit`（`粘滞位`，`chmod +t`，数字 `1xxx`） ：作用于目录。设置了 `Sticky Bit` 的目录，即使目录的写权限是开放的（`777`），也只有文件所有者、目录所有者或 `root` 才能删除或重命名其中的文件。典型例子：`/tmp（drwxrwxrwt）`，任何人都能往里写临时文件，但谁也不能删别人的文件

    - **特殊权限位的大小写区分**
      - `ls -l` 中，如果 `SUID/SGID/Sticky Bit` 对应的位置出现小写 `s` 或 `t`，表示该特殊位已设置且对应的执行位（`x`）也已启用
      - 如果出现大写 `S` 或 `T`，表示特殊位已设置但对应的执行位（`x`）没有启用。这种情况在实际中极少出现——因为没有执行位的 `SUID/SGID/Sticky Bit` 在功能上是无效的

    - **`+` 号的含义**
      - 权限字符串末尾的 `+` 号（如 `rwxr-xr-x+`）表示该文件或目录设置了扩展 `ACL（Access Control List）` 。`getfacl <file>` 查看详细规则，`setfacl` 进行设置

    - **查看系统上所有 `SUID` 文件**
      - `find / -perm -4000` 列出所有设置了 `SUID` 的可执行文件。定期检查这个列表是安全基线的一部分，不用的 `SUID` 应该去掉


## 🤔 Linux 中 umask 的作用是什么？ 
- **`umask`（`User file creation Mask`）决定了新创建的文件和目录的默认权限。它不是直接设置权限，而是设置*要屏蔽掉哪些权限位*。系统用*最大权限减去 umask*来计算出最终的默认权限。**
    - **`umask` 的工作原理**
        - 新建文件时，系统给的最大权限是 `666（rw-rw-rw-）`，因为文件默认不给执行权限（防止安全风险）
        - 新建目录时，系统给的最大权限是 `777（rwxrwxrwx）`，因为目录的 `x` 权限表示能否进入该目录，是合理的默认行为
        - 最终权限的计算方式：`最大权限` - `umask`（按权限位逐位相减，不是数值减法）

    - **计算方法（非 Linux 实际计算规则）**
        - **目录**：`777` - `umask`，计算结果即为最终权限
            - `umask=0022 → 777 - 022 = 755（rwxr-xr-x）`

        - **文件**：`666` - `umask`，但需要处理奇数位补偿 ——— `umask` 中如果有奇数位，说明减掉了文件本来就没有的 `x` 权限，需要加回来
            - **`umask` 所有位为偶数**：直接 `666` - `umask`
                - `umask=0022 → 666 - 022 = 644（rw-r--r--）`

        - **`umask` 部分或全部为奇数**：`666` - `umask` + `奇数位补偿`
            - `umask=0045 → 666 - 045 = 621`，末位奇数补 `001`，得 `622（rw-rw--w-）`
            - `umask=0033 → 666 - 033 = 633`，两奇数位补 `011`，得 `644（rw-r--r--）`

    - **常用 `umask` 值及对应的默认权限**
        | `umask`| `文件权限` | `目录权限` | `适用场景` |
        | ------ | ---------- | ---------- | ---------- |
        | `002`    | `664 (rw-rw-r--)` | `775 (rwxrwxr-x)` | 多人协作开发环境，同组可写 |
        | `022`    | `644 (rw-r--r--)` | `755 (rwxr-xr-x)` | 大多数 Linux 系统的默认值，所有者可写，其他人只读 |
        | `077`    | `600 (rw-------)` | `700 (rwx------)` | 严格安全环境，仅所有者可访问 |
        | `007`    | `660 (rw-rw----)` | `770 (rwxrwx---)` | 组内协作但不对外开放 |

    - **如何查看和设置 `umask`**
        - **`umask`**：查看当前 `shell` 的 `umask` 值（显示为四位八进制数，第一位通常是 0）
        - **`umask -S`**：以符号模式显示（如 `u=rwx`,`g=rx`,`o=rx`）
        - **`umask 027`**：临时设置 `umask`，只在当前 `shell` 生效
        - **持久化设置**：写入 `~/.bashrc` 或 `/etc/profile` 或 `/etc/bash.bashrc` 中

- **协助记忆**
    - `umask` 不是遮阳伞（挡住多少阳光），是纱窗（挡住哪些权限） ——— `022` 代表"挡住组的写权限（2）和其他的写权限（2）"
    - 文件默认 `666`，目录默认 `777`，减去 `umask` 就是结果
    - 奇数位补偿：文件的奇数 `umask` 多减了 `x` 位，每个奇数位加一个 `1` 回来
    - 常用值不需要背，记住一个规律：`022` 是"自己随便改、别人只能看"，`002` 是"自己和同组都能改、外人只能看"

- **进阶思考**
    - **`umask` 的值为什么有时显示为四位（如 `0022`）？**
        - 第一位通常是 `0`，表示没有设置特殊权限位（`SUID`、`SGID`、`Sticky Bit`）。如果设置了特殊权限位，第一位会对应变化。例如 `umask` 为 `1000` 时，会屏蔽掉 `Sticky Bit`。但实践中几乎不需要设置特殊权限位的 `umask`，所以第一位几乎总是 `0`，可以忽略

    - **为什么新建文件默认最大是 `666`，而不是 `777`？**
        - 这是内核的安全设计。如果新建一个文本文件或脚本时默认带上执行权限，会带来不必要的安全风险。内核在 `open()` 和 `creat()` 系统调用中会强制清除新文件的执行权限位。目录没有这个限制，因为目录的 `x` 位是进入目录的必要条件


## 🤔 日志报错 Could not resolve host 域名解析失败怎么排查？  
- **`Could not resolve host` 是应用层报的错，意思是应用请求解析一个域名时，系统返回了"查无此域名"。这个错可能是 `DNS` 服务器本身的问题、网络不通导致请求发不出去、或者域名本身确实不存在。排查链路从近到远：先确认网络通不通、再确认 `DNS` 配没配对、最后确认域名本身有没有问题。**
    - **先确认网络能不能到 `DNS` 服务器**
        - **`ping <DNS服务器IP>`**：看看网络能不能通。如果 `ping` 不通，检查防火墙是否放行了出站 `DNS（UDP 53）`流量
        - 域名解析走了几层 `DNS` 服务器，通常由 `/etc/resolv.conf` 决定。用 `dig @8.8.8.8 <domain>`（指定公共 DNS 查询）和 `dig <domain>`（按系统配置走）对比。如果指定 `8.8.8.8` 能解析但系统默认的不行，说明问题在本地配置的 `DNS` 服务器上

    - **再确认系统 `DNS` 配置**
        - `cat /etc/resolv.conf` 检查 `nameserver` 配置。常见问题：配置文件内容损坏（如 `nameserver` 拼写错误、`IP` 地址写成了 `192.168.1` 缺少一段）、多行 `nameserver` 重复或有冲突
        - 如果机器用 `systemd-resolved` 管理 `DNS`，`/etc/resolv.conf` 可能是指向 `127.0.0.53` 的软链接。此时应通过 `resolvectl status` 查看 `DNS` 配置和当前状态
        - `nslookup <domain>` 或 `dig <domain>` 确认能否正常解析
        - 检查文件目录权限和状态是否正常：`ls -la /etc/resolv.conf` ——— 如果被意外修改或删除了指向性配置，也可能导致解析失败

    - **再确认域名本身**
        - **`dig <domain> +trace`**：完整追踪解析路径，看是在哪一级断了——根 `DNS`、顶级域、还是权威 `NS`
        - `host <domain>` 或 `nslookup <domain>` 快速确认域名是否存在
        - 检查是否是本地 `/etc/hosts` 覆盖了 `DNS` ——— 如果 `hosts` 里有该域名但指向了错误 `IP`，应用可能走的不是 `DNS` 解析而是 `hosts` 文件

- **协助记忆（以打电话查号码为例）**
    - `DNS` 解析 = 打电话查`114`
    - `/etc/resolv.conf` = 你的通讯录里存的 `114` 号码（`nameserver`）
    - `ping 114IP` = 确认电话线路通不通
    - `dig @114 name` = 打给 114 查这个人的号码
    - `host name` = 直接问通讯录查不查得到
    - 如果全程没问题，那就是你要找的人不存在（域名根本不存在或已过期）

- **进阶思考**
    - **只针对部分域名解析失败，其他域名正常，可能是什么问题？**
        - 说明 `DNS` 服务器本身是通的，问题在 `DNS` 服务器侧或域名侧。可能的原因：该域名的 `NS` 记录配置错误、域名的 `TTL` 缓存过期后权威服务器没有响应、该域名被 `DNS` 服务器封禁或黑白名单拦截。可以在故障机器的 `/etc/hosts` 中临时添加该域名到正确 `IP` 的映射，先恢复业务再排查 `DNS` 服务器侧的问题

    - **`resolvectl status` 输出显示 `DNS` 配置正常，但应用还是报解析失败，可能是什么原因？**
        - 几个可能方向：一是应用有自己内置的 `DNS` 解析器（如 `Java` 的 `JNDI`、`Node.js` 的 `dns` 模块），可能绕过了系统配置；二是 `nscd`（`Name Service Cache Daemon`）缓存了失效的解析结果，可以 `nscd -i hosts` 清除缓存测试；三是系统启用了 `nsswitch.conf` 中 `hosts`: `files mdns4_minimal dns` 等配置，`mDNS` 响应可能在返回错误结果之前产生了响应干扰，确认服务状态后可以调整查找顺序

    - **容器内 `DNS` 解析失败和宿主机上的排查有什么不同？**
        - 容器内默认使用宿主机 `DNS` 或通过容器网络接口转发 `DNS` 请求。`Docker` 默认将容器的 `/etc/resolv.conf` 设置为宿主机 `DNS` 或 `127.0.0.11`（`Docker` 内置 `DNS`）。如果容器内解析失败，先看 `/etc/resolv.conf` 里指向的是否为 `127.0.0.11`。是的话，说明 `Docker` 的 `DNS` 代理有问题，检查 `docker0` 网络是否正常、`--dns` 参数是否正确配置。如果不是，需要在宿主机侧排查网络和防火墙


## 🤔 Linux 应用进程莫名被杀掉，可能是什么原因？  
- **进程被杀掉不会毫无缘由，系统或用户一定发出了一个信号。排查的核心是找到信号的来源——是内核发的（`OOM Killer`）、系统限制触发的（`ulimit` / `systemd`）、还是其他进程杀的。**
    - **`OOM Killer` 杀掉的（最常见）**
        - 系统内存耗尽时，内核触发 `OOM Killer`，选择一个进程杀掉以释放内存。被杀的不一定是内存占用最大的那个 ——— 内核有一套算分机制（`oom_score`），综合进程大小、运行时间、`oom_score_adj` 调整值来打分，分高的先杀
        - 确认方式：`dmesg | grep -i "killed process"` 或 `journalctl -k | grep -i "oom"`，输出会包含被杀进程的 `PID`、名称、以及当时的系统内存状态
        - 如果进程频繁被 `OOM` 杀掉，看 `/var/log/messages` 或 `/var/log/syslog` 中是否有 `Out of memory: Killed process` 记录

    - **系统资源限制触发的**
        - **文件描述符上限**：`ulimit -n` 查看当前限制。如果进程需要打开大量文件（如连接数高的服务），文件描述符用满后进程内的 `open()` 或 `accept()` 会返回 `EMFILE` 错误，进程如果没处理这个错误可能会崩溃退出
        - **内存限制（`cgroup`）** ：容器环境下，`cgroup` 的内存限制比宿主内核先触发，`OOM Killer` 直接杀掉容器内的进程。`cat /sys/fs/cgroup/memory/memory.limit_in_bytes` 查看容器的内存上限
        - **进程数限制**：`ulimit -u` 限制了用户最多能创建的进程/线程数，超过后 `fork()` 失败，如果进程没有处理这个错误可能会把自身视为运行异常并异常退出
    
    - **信号杀掉的（人为或其他程序）**
        - **`kill -9 <PID>`**：这种强杀在系统日志中通常没有记录，需要从应用日志或操作审计日志中找线索
        - **`systemd` 的资源限制**：如果服务配置了 `MemoryMax` 或 `TasksMax`，达到上限后 `systemd` 会杀掉进程。`journalctl -u <service>` 中会有相应记录

    - **应用自身崩溃**
        - **段错误（`Segmentation Fault`）**：进程访问了非法内存地址，内核发 `SIGSEGV` 杀掉进程。`dmesg` 中会有 `segfault at <addr> ip <addr>` 的日志，记录异常发生的位置信息
        - **断言失败（`assertion failure`）**：进程自身的代码检查到状态不合法，主动调用 `abort()` 退出。这类情况需要在进程自身的错误日志中查看相关信息
        - **工作进程退出（被 `manager` 进程视为故障）**：如 `Nginx worker` 异常退出后 `Master` 进程会重新拉起新的 `worker`，日志中会记录 `worker` 退出的信号和代码


- **协助记忆**
    - **莫名被杀 `OOM` 先查**：`dmesg | grep -i "killed process"`
    - **系统限制看 `ulimit`**：文件描述符、进程数、内存上限
    - **容器被杀看 `cgroup`**：`dmesg` + 内存上限是否过低
    - **信号杀的看审计日志**：谁在什么时间杀了谁
    - **自身崩溃看 `dmesg`**：`segfault` 段错误信息

- **进阶思考**
    - **如何保护关键进程不被 `OOM Killer` 误杀？**
        - 调整 `oom_score_adj`。`echo -1000 > /proc/<PID>/oom_score_adj` 可以大幅降低该进程被 `OOM Killer` 选中的概率（`-1000` 表示永远不被杀），`echo 1000 > /proc/<PID>/oom_score_adj` 则相反（优先被杀）。这个值应该在进程启动时通过 `systemd` 的 `OOMScoreAdjust` 指令或启动脚本设置。但注意：即使设了 `-1000`，如果系统内存极度过低且无可杀进程时，内核仍然可能选择它

    - **`dmesg` 里没有看到 `OOM Killer` 的记录，但进程确实死了，还可能是什么原因？**
      - 如果 `dmesg` 干净，分别确认几个方向：一是检查 `journalctl -u <service>` 看 `systemd` 是否因为重启策略或资源限制主动停止了服务，或是触发了 `Restart=always` 以外的规则导致进程退出后未自动拉起；二是 `ulimit -a` 看是否有其他资源限制被触发（如 `core file size`、`stack size`）；三是检查应用的自身日志，确认是否有合法的 `exit(0)` 或 `exit(1)` 调用——有些情况下程序在自行退出时不会主动留下日志，需要结合业务日志验证退出上下文


## 🤔 crontab 定时任务无法执行，有哪些常见原因？  
- **`crontab` 任务不执行的原因集中在几个方向：`cron` 服务本身没跑、任务语法写错了、环境变量不一致、权限不够。大部分情况不是 `cron` 没执行，而是执行了但运行环境和手动执行时不一样，导致任务失败。**

    - **`cron` 服务未运行**
        - `systemctl status crond` 或 `systemctl status cron`（发行版不同，服务名不同）查看 `cron` 服务是否 `running`
        - 如果服务挂了，`systemctl restart crond` 启动后看日志确认是否有异常导致它反复退出
        - 注意容器环境下通常不启动 `cron` 服务，需要用其他方式实现定时任务

    - **`crontab` 语法或配置问题**
        - **时间格式写错**：`* * * * *` 分别是`分、时、日、月、周`。常见的踩坑是周字段用了 `0` 或 `7` ——— 有些版本的 `cron` 两者都表示周日，但建议统一用 `0`
        - **没加换行符**：*`crontab` 文件的最后一行必须是空行，否则最后一条任务可能不执行*
        - **编辑器语法错误**：`crontab -e` 编辑时如果保存了非标准字符或格式，`cron` 加载配置可能报错。`crontab -l` 可以检查当前配置是否能正常输出

    - **环境变量问题（最常见的问题）**
        - `cron` 执行任务时的环境变量和手动 `SSH` 登录时完全不一样。它不加载 `/etc/profile`、`~/.bashrc`、`~/.bash_profile`，只设置极少的环境变量（`PATH=/usr/bin:/bin`、`SHELL=/bin/sh`）
        - 这导致很多问题：脚本里的命令用了绝对路径才能找到（如 `/usr/local/bin/python3` 而非 `python3`）、脚本依赖的环境变量在 `cron` 下没定义、`~` 号在 `cron` 下不一定指向期望的用户家目录
        - 解决方式：在 `crontab` 中显式设置 `PATH` 和 `SHELL`，或在脚本开头重新加载环境变量
            ```bash
            SHELL=/bin/bash
            PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
            0 2 * * * /root/backup.sh
            ```

    - **权限问题**
        - 脚本没有执行权限：`chmod +x /path/to/script`
        - 脚本需要 `root` 权限但写在了普通用户的 `crontab` 里。系统的定时备份、服务维护等任务通常写在 `root` 的 `crontab` 中
        - `cron` 任务被 `/etc/cron.allow` 或 `/etc/cron.deny` 限制。如果 `/etc/cron.allow` 存在，只有列在里面的用户才能使用 `crontab`；如果 `/etc/cron.deny` 存在，列在里面的用户不能使用 `crontab`

    - **日志排查**
        - 检查 `cron` 的执行日志：`grep CRON /var/log/syslog`（`Debian`/`Ubuntu`）或 `grep crond /var/log/cron`（`RHEL`/`CentOS`）
        - 如果 `cron` 尝试执行任务但失败了，日志会记录错误原因，如 (`root`) `MAIL` (`mailed 1 byte of output`) 表示有输出但无人查看，(`root`) `CMD` (`command`) 表示命令正常执行
        - 如果在日志中没有任何记录，说明 `cron` 服务可能未正常读取到该任务的配置

- **协助记忆**
    - **`crontab` 不执行的五大原因**：
        1. cron 服务没跑
        2. 语法写错了（字段顺序、末尾没换行）
        3. 环境变量不对（PATH 不够，脚本里找不到命令）
        4. 脚本没执行权限，或者用户没权限执行
        5. 执行了但报错了没看日志（cron 的默认输出是发邮件，不看邮箱就不知道）

- **进阶思考**
    - **`cron` 执行脚本时，脚本里用了 `python3` 但报 `command not found`，手动 `SSH` 进去却正常，为什么？**
      - 手动登录时 `Shell` 加载了 `~/.bashrc` 和 `/etc/profile`，里面把 `/usr/local/bin` 加到了 `PATH` 中，所以能找到 `python3`（它通常安装在 `/usr/local/bin/python3`）。`cron` 的环境变量中 `PATH` 只有 `/usr/bin:/bin`，找不到 `/usr/local/bin` 下的程序。解决方式是在脚本开头显式定义 `PATH`，或直接使用完整路径 `/usr/local/bin/python3`

    - **`cron` 执行的任务在日志中显示 (`root`) `MAIL` (`mailed 0 bytes of output`)，这意味着什么？**
      - 表示命令执行成功（没有报错输出），但 `cron` 仍然尝试通过邮件把 `stdout`/`stderr` 发给用户。如果没有配置邮件服务，`cron` 发送邮件的过程本身会卡住或产生大量的本地邮件堆积在 `/var/mail/` 目录下。如果 `cron` 任务比较多，建议在每个任务末尾加 `>/dev/null 2>&1` 丢弃不必要的输出，或配置 `MAILTO=""` 禁用邮件通知。对于需要保留输出的任务，可以用 `>> /var/log/script.log 2>&1` 将输出重定向到日志文件

    - **测试 `crontab` 是否正常执行，最快的方式是什么？**
      - 加一个每分钟执行一次、结果可观测的最小任务做验证：`* * * * * date >> /tmp/cron_test.log 2>&1`。等一分钟后检查 `/tmp/cron_test.log` 是否有时间戳写入。如果没有，说明 `cron` 环境整体有问题


## 🤔 有一台在用的旧服务器需要下线，你会怎么操作？  
- **旧服务器下线的核心原则是 "先迁移、后下线、再清理"，确保业务不中断、数据不丢失。操作流程按时间线分为准备、迁移、下线、清理四个阶段。**
    - **第一阶段**：确认这台服务器上到底跑了什么
        - 在任何人动服务器之前，先搞清楚它上面有哪些服务、哪些数据、哪些人还在用。不能凭记忆说"这机器就跑了 `Nginx`"就去关机
        - `systemctl list-units --type=service --state=running` 列出所有运行中的服务
        - `ss -lntp` 列出所有监听端口及对应的进程
        - `docker ps` 或 `crictl ps` 检查是否有容器在运行
        - `fuser -v` / 或 `lsof / | grep -v "(deleted)"` 看哪些进程还在读写根分区——确认是否有进程持续写入本地数据
        - 确认这台机器在监控系统、CMDB、备份策略中是否还有关联配置——不清理的话后续会持续收到"服务器宕机"告警
        - 确认这台机器上是否有其他人还依赖它（通过 SSH 登录、挂载了它的 NFS、配置了指向它的 DNS 记录）


    - **第二步**：确认服务迁移或停用条件
        - **找到所有服务的"下一个落脚点"，确认替代方案已就绪**
            - **Web 服务**：负载均衡器上是否已摘掉这台后端？`DNS` 是否已切走？
            - **数据库**：主从是否已切换到新节点？确认 `SHOW SLAVE STATUS` 中的复制延迟已追平
            - **定时任务**：`crontab` 里的任务是否已迁移到其他机器？
            - **共享存储**：`NFS` 客户端是否已重新挂载到新服务端？

        - **确认数据已完整迁移或备份**
            - 关键数据至少有一份离线备份 + 一份异地备份
            - 数据库数据确认备份文件可恢复（恢复演练或至少验证备份文件大小、`checksum` 正常）
            - 确认这机器上有没有"只有这台机器才知道的密码或密钥"（如 `SSH` 私钥、`API Token`）——迁移过程中最容易遗漏的就是这类凭据

    - **第三步**：执行下线
        - **先摘流量**：从负载均衡器或 `DNS` 中移除该服务器，观察一段时间（至少一个完整的业务周期，通常 `15 ~ 30` 分钟），确认没有报障，新连接确实不再打到这台机器
        - **停止服务**：`systemctl stop <service>` 逐个停止，而不是直接 `shutdown`。如果直接关机可能导致已迁移但尚未落盘的数据丢失、主从状态尚未更新的潜在问题，需要为潜在的恢复操作留下操作窗口
        - **停止开机自启**：`systemctl disable <service>` 或 `systemctl disable --now <service>`
        - **停止服务器**：`shutdown -h now`。如果后续需要重新上架可确认断电、下架、贴上已下线标签

    - **第四步**：下架后清理
        - **监控系统**：删除或静默该主机的告警规则，避免持续收到"服务器宕机"通知
        - **`CMDB` / `资产管理系统`**：更新服务器状态为"已下线"或"已报废"
        - **`DNS`**：清理指向该服务器的 A 记录
        - **备份策略**：停止该服务器的备份任务，避免持续产生错误通知
        - 如果后续不再使用，执行安全擦除（`dd if=/dev/urandom of=/dev/sdX`）或物理销毁硬盘

- **协助记忆**
    - **`旧服务器下线` = `从旧房子搬到新房子`**：先打包所有东西（列清单）→ 确认新家能住了（迁移确认）→ 搬过去（摘流量/停服务）→ 旧房子做最后的检查并清理干净（清理监控/CMDB/备份）→ 交钥匙给房东（物理下架）


- **进阶思考**
    - **下线的服务器如果很快就要重新上架（如换机房搬迁），哪些步骤可以简化？**
      - 不需要执行安全擦除或物理销毁，也不需要清理 `CMDB` 和监控条目。核心做几件事：备份关键配置（`/etc`、`/var/spool/cron`、`/etc/ssh/sshd_config`）、恢复出厂的服务启停方式、贴上下线标签注明原因和日期。重新上架时直接恢复备份配置即可复用

    - **服务器上还有本地数据盘（如 `/data`），但应用已经迁移到新机器上了。数据盘里的历史数据需要保留多久？**
      - 至少保留一个完整的备份周期的时间（通常 `30 ~ 90` 天），以应对数据回溯需求。如果空间不允许，至少保留数据清单和目录结构快照记录。数据销毁必须走正规流程，不能因为"腾机房空间"就直接低格，以防几个月后业务方突然需要查一条半年前的历史记录


## 🤔 安装软件提示 LD 动态链接库缺失，该怎么处理？  
**动态链接库（`.so` 文件）是程序运行时需要加载的共享库。提示缺失通常意味着三种情况：库文件不存在、库文件存在但不在默认搜索路径中、或者库文件的版本不兼容。排查方向从确认缺了什么开始，逐步定位到怎么补上。**
    - **第一步**：确认缺失的库文件是什么
        - 错误信息通常类似：`error while loading shared libraries: libxxx.so.X: cannot open shared object file: No such file or directory`
        - 用 `ldd /path/to/binary` 列出该可执行文件依赖的所有动态库，标记 `not found` 的就是缺失的
        - 如果安装的是第三方二进制（非包管理器安装），注意它依赖的库版本可能和系统自带的库版本不匹配
    - **第二步**：确认系统中是否有这个库但找不到
        - **find /usr -name "libxxx.so*" 2>/dev/null** 查找系统上是否已经存在该库文件
        - 如果存在但提示找不到：问题可能出在动态链接器没有缓存这个路径。`ldconfig -p | grep libxxx` 查看库是否在链接器缓存中
        - 如果存在但版本号对不上：程序需要 `libxxx.so.1` 但系统只有 `libxxx.so.2`，这是 `ABI` 版本不兼容，不能直接改软链接
        - 可以通过在同一目录下确认是否存在只差后缀序号的文件来判断是否版本不匹配——如果多个版本同时存在，`ldd` 仍然会优先匹配最新版本
    - **第三步**：通过包管理器安装缺失的库
        - 大多数系统库可以通过包管理器安装对应的 `-devel` 或 `-libs` 包来解决
        - 先确定缺失的库属于哪个包：
            - **`Debian/Ubuntu`**：`apt search libxxx` 或 `apt-file search libxxx.so`
            - **`RHEL/CentOS`**：`dnf provides */libxxx.so*` 或 `yum whatprovides */libxxx.so*`
            - **安装**：`dnf install libxxx` 或 `apt install libxxx-dev`
        - 对于商业软件或第三方二进制，通常会在安装包中附带其依赖的库，不需要从发行版仓库中额外搜索
    - **第四步**：如果源码编译的程序找不到自定义路径的库
        - 安装路径不在系统默认库搜索路径中（`/usr/lib`、`/usr/lib64`、`/lib`、`/lib64`）
        - 两个临时解决方法：
            - `export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH` 临时添加到搜索路径
            - 运行 `ldconfig /usr/local/lib` 将该路径加入系统缓存
        - 持久化方案：在 `/etc/ld.so.conf.d/` 下创建一个 `.conf` 文件，写入库所在路径，然后运行 `ldconfig`

- **协助记忆**
    - **检查三连**：`ldd` 看缺啥 → `find` 看有没有 → `apt-file` / `dnf provides` 查该装哪个包
    - **找库两招**：包管理器搜（`dnf provides */libxxx.so`）、`ldconfig -p | grep libxxx` 查缓存
    - **路径不对两招**：临时设 `LD_LIBRARY_PATH`、持久加 `/etc/ld.so.conf.d/`
    - 缺少的库文件名不同时，程序仍无法加载，不能强行改软链接跳过

- **进阶思考**
    - **安装了某个库的 `dev` 包后，`ldd` 依然显示 `not found`，为什么？**
        - `-dev` 或 `-devel` 包通常只包含头文件（`.h`）和用于编译链接的 `.so` 软链接，运行时需要的 `.so.X` 主版本号文件在对应的 `-libs` 或 `-runtime` 包中。例如 `libssl-dev` 提供编译头文件，`libssl1.1`（具体的运行时版本包）才提供运行时的 `libssl.so.1.1`。确认你安装的包名是否包含了运行时库的版本名部分

    - **`ldconfig` 是做什么的？运行后有什么影响？**
        - `ldconfig` 扫描 `/etc/ld.so.conf` 及 `/etc/ld.so.conf.d/` 中配置的所有目录，更新 `/etc/ld.so.cache` 缓存文件。动态链接器在程序启动时通过这个缓存快速定位库文件。安装新库或添加新的库搜索路径后需要运行 `ldconfig` 才能生效。注意不要在没有加新库的情况下随意运行 `ldconfig`，一般情况下没有破坏作用，但在某些嵌入式系统中清理缓存可能会影响后续动态链接的性能

    - **`ldconfig` 加上自定义编译的库路径后，系统崩溃了怎么办？为什么会这样？**
        - `ldconfig` 更新的是全局动态链接器缓存，一旦执行，系统上所有程序都会从缓存中找到新的库文件。如果你自定义编译的是系统级核心库（如 `libstdc++.so`、`libcrypto.so`、`libssl.so`），几乎所有系统服务（`systemd`、`NetworkManager`、`SSH`）都依赖它们。如果新库的 `ABI` 与系统程序预期的不一致，这些程序会逐个崩溃——先挂网络、再挂 `Shell`、最后系统完全不可用。自定义编译的系统核心库永远不要通过 `ldconfig` 加到系统路径，应该用 `LD_LIBRARY_PATH` 或 `rpath` 只让特定应用使用新库。如果已经发生了，在还能 `SSH` 或通过带外管理接入时，立即将 `/etc/ld.so.conf.d/` 下自定义的 `.conf` 文件移走，执行 `ldconfig` 回退。如果已经崩溃到连 `Shell` 都进不去，只能进 `rescue` 模式，`chroot` 后删除自定义的 `.conf` 文件并重建 `ld.so.cache`


## 🤔 使用 & 把程序放后台运行，没多久就自动退出是什么原因？  
- **`&` 只是把程序放到当前 `Shell` 的后台任务列表中，并没有让它脱离终端的控制。当你关闭终端或退出 `SSH` 时，`Shell` 向所有子进程发送 `SIGHUP`（挂起信号） ，后台进程收到信号后默认退出。**
    - **根本原因**：`SIGHUP` 信号
        - 你用 `&` 启动的程序，父进程仍然是当前 `Shell`。当 `Shell` 退出时，内核向该 `Shell` 的所有子进程（包括后台任务）发送 `SIGHUP` 信号。进程收到 `SIGHUP` 后默认动作是终止  
        - `&` 只解决了"不占用前台终端"的问题，没有解决"脱离终端依赖"的问题。终端关闭时，后台进程照样收到 `SIGHUP`
    - **验证方式**
        ```bash
        sleep 300 &
        # 关闭终端重新打开
        ps aux | grep sleep  # 发现进程已消失
        ```

    - **几种正确的后台运行方式**
        - **`nohup`（最常用）** ：让进程忽略 `SIGHUP` 信号。`nohup long-running-command &`。输出默认重定向到 `nohup.out`
        - **`disown`（`Shell` 内建）** ：启动后从 `Shell` 的任务表中移除，`Shell` 退出时不再管它。 `long-running-command &` → `disown`。如果忘记加 `nohup` 可以用这个补救
        - **`setsid`（启动时创建新会话）** ：让进程成为一个新会话的领头进程，完全脱离当前终端。`setsid long-running-command`
        - **`tmux` / `screen`（推荐）** ：在持久化终端会话中运行程序，关闭终端后进程继续运行，重新连接还能看到输出。`tmux new -s mysession` → `运行程序` → `Ctrl+B D` 分离 → 下次 `tmux attach -t mysession` 重新连回去

- **协助记忆**（以开会做笔记为例）
    - `前台运行` = 你站在会议室白板前写字，大家都看着你，你啥也别干了
    - `& 后台运行` = 你一边开会一边在下面用手机记笔记，没人盯着你，但会议一结束（终端关闭），你的笔记也被收走了（收到 `SIGHUP`，进程退出）
    - `nohup` = 你跟会议主持人说"我记的笔记不交，我自己带回去"（忽略 `SIGHUP`）
    - `disown` = 会都开一半了你才想起来忘了说，赶紧把笔记塞到自己包里（从任务表中移除）
    - `tmux` = 你在隔壁开了一个独立的会议室，这边的会散了隔壁还在继续开，你随时可以过去接着记

- **进阶思考**
    - **`nohup` 和 `&` 必须一起用吗？有什么区别？**
        - 两个互不依赖的工具，各管各的事。`nohup` 让进程忽略 `SIGHUP`，`&` 让进程在后台运行。`nohup command` 不加 `&` 也能运行，但前台还是被你占着；`command &` 不加 `nohup`，关闭终端时进程照常退出。所以正确的做法是 `nohup command &`，两者都用

    - **已经用 & 启动了程序，发现关闭终端后它会被杀掉，能不重启程序就解决吗？**
        - 可以。先 `jobs -l` 找到后台任务的 `PID`，然后执行 `disown <PID>` 或 `disown -h <PID>`（`-h` 标记任务在 `Shell` 退出时不收 `SIGHUP`， `disown` 不带 `-h` 则完全从任务表中移除）。如果程序已经开始运行了，也可以在当前终端关闭前用 `bg` 将其转为后台执行再 `disown`



## 🤔 进程被 OOM Killer 杀死，该如何排查分析？  
- **`OOM`（`Out-Of-Memory`）`Killer` 是 `Linux` 内核在系统内存耗尽时的最后手段——它选择一个进程杀掉以释放内存。排查的目标不仅是找到"谁杀了谁"，更重要的是找到为什么内存会耗尽以及为什么选了这个进程。**
    - **第一步**：确认是不是 `OOM Killer` 杀的
        - 最快的确认方式：`dmesg | grep -i "killed process"` 或 `journalctl -k | grep -i "oom"`
        - `OOM Killer` 的日志格式为：`Killed process <PID> (<name>) total-vm:<size>kB`, `anon-rss:<size>kB`, `file-rss:<size>kB`, `oom_score_adj:<value>`
        - 日志会输出被杀进程的 `PID`、名称、虚拟内存大小、实际物理内存（`anon-rss`）、文件映射内存和 `oom_score_adj` 值
        - 如果 `dmesg` 里干净，说明不是 `OOM` 杀的，应该排查其他方向（信号、资源限制、系统主动停止服务）

    - **第二步**：日志中获取关键信息
        - 除了被杀进程的信息，`OOM` 日志还会包含系统当时的内存状态概览和每个进程的 `oom_score` 排序列表
        - 重点看 `Memory cgroup stats` 部分——如果进程运行在容器内，`OOM` 可能是 `cgroup` 内存限制触发的，而不是宿主机的全局 `OOM`
        - `oom_score` 排序列表显示了当时系统按分数从高到低排列的进程——分数最高的就是最优先被杀的。即使被杀的那个进程内存不是最大的，也可能是它的 `oom_score_adj` 被调整过（或者系统根据其他策略选择了它）

    - **第三步**：确认 `OOM Killer` 为什么选中了这个进程
        - `OOM Killer` 根据 `oom_score` 选进程。`oom_score` = 基础分（主要是 `RSS` 大小） + `oom_score_adj` 调整值
        - `oom_score_adj` 默认是 `0`，范围 `-1000`（尽量不杀）~ `1000`（优先杀）
        - 进程被选中的可能原因：
            - 这个进程确实是当时内存占用最大的
            - 其他大内存进程设置了 `oom_score_adj` = `-1000`（如 `MySQL`、`Java`），内核跳过它们后选到了这个
            - 内存泄漏导致某个进程的 `RSS` 持续增长，最终成为最大进程

    - **第四步**：分析内存耗尽的原因
        - `sar -S -f /var/log/sa/saXX` 查看 `OOM` 发生前的内存使用趋势（如果 `sar` 已开启）。关键看 `kbmemused`、 `kbmemfree`、`kbswpused` 的变化曲线
        - 如果没有 `sar`，需要从应用层面回忆当时发生了什么：是否有定时任务集中执行、是否有大量并发请求、是否进行了数据迁移或代码上线
        - 常见原因分类：
            - **内存泄漏**：进程申请了内存但用完没释放，持续增长直到触发 `OOM`。用 `top -p <PID>` 定期观察 `RES` 是否持续增长可以确认
            - **突发流量**：业务流量暴涨导致需要同时处理大量请求，每个请求占用一定内存，累积造成内存耗尽
            - **配置过大**：`buffer pool`、`JVM` 堆、连接池等参数设置超过了物理内存容量，加上其他进程的消耗导致整体超限
            - **`cgroup` 限制过小**：容器或 `systemd` 服务的内存上限设置得太紧

- **协助记忆**
    - **三查**：查 `dmesg` 确认被杀 → 查 `oom_score` 排序看为什么选中它 → 查内存趋势看为什么会耗尽
    - **两个文件查 `OOM`**：`dmesg | grep -i "killed"` 确认凶手，`dmesg | grep -A 20 "killed process"` 看完整上下文
    - **`OOM` 不是根因，是结果**：要追的是"为什么内存会耗尽"，不是"为什么杀了这个进程"

- **进阶思考**
    - **`dmesg` 中 `OOM` 日志显示被杀进程的内存占用并不大，为什么选它不选更大的？**
      - 几个可能的原因。一是 `oom_score_adj` 的影响——更大的进程可能设置了负值被保护了。二是内核的算法：在多个进程 `oom_score` 接近时，内核倾向于杀耗时最短、启动时间最短的进程（认为它"不重要"）。三是 `cgroup` 隔离——如果进程运行在不同的 `cgroup` 中且其中一个 `cgroup` 达到了内存上限，只有该 `cgroup` 内的进程参与评分，即使宿主机上其他进程有更大的内存占用也不会被选中

    - **如何避免关键进程被 `OOM Killer` 误杀？**
      - **三种保护手段组合使用**：
        - `oom_score_adj` = `-1000` 给关键进程（如数据库、监控 `Agent`），使其不会被 `OOM Killer` 选中
        - `systemd` 服务配置：`OOMScoreAdjust=-1000`（在 `Service` 段中）
        - 确保系统有足够的 `Swap` 或 `zRAM` 作为缓冲（但不能依赖它解决根本问题，因为频繁 `Swap` 会导致性能严重下降，而且 `Swap` 满后仍然会触发 `OOM`）
      - **同时要清楚**：保护了关键进程后，如果内存持续耗尽，`OOM Killer` 会退而求其次去杀其他进程，最终可能导致系统整体不可用。更根本的解决方向是避免内存耗尽本身


## 🤔 简述 sudo 提权的工作原理？  
- **`sudo` 的核心机制是：一个 `SUID` 二进制程序 + 配置文件授权 + 认证缓存。它允许普通用户在不需要知道 `root` 密码的情况下，以 `root` 或其他用户的身份执行特定命令。**
    - **`sudo` 的 `SUID` 机制**
        - `sudo` 是一个设置了 `SUID` 位的二进制文件（`ls -l /usr/bin/sudo` 显示 `rwsr-xr-x`），这意味着：当普通用户执行 `/usr/bin/sudo` 时，进程的有效 `UID`（`effective UID`）会临时变成文件所有者（即 `root`），而不是执行者的 `UID`
        - 有了 `root` 权限后，`sudo` 才能读取 `/etc/sudoers` 文件（该文件权限通常为 `0440`，只有 `root` 能读）来判断当前用户是否有权限执行要运行的命令
        - 验证完权限后，`sudo` 会 `fork` 一个子进程，将子进程的权限切换到目标用户（通常是 `root`），然后通过 `execve()` 执行用户指定的命令

    - **认证与授权流程**
        - 用户执行 `sudo <command>` 后，`sudo` 首先检查 `/etc/sudoers` 中的条目，确认该用户是否有权执行该命令
        - 如果没有配置 `NOPASSWD`，`sudo` 会提示用户输入自己的密码（不是 `root` 的密码），验证用户身份。这是为了确认当前终端没有被人在你离开时恶意操作
        - 验证通过后，`sudo` 会在 `/run/sudo/ts/<hostname>` 下创建一个时间戳文件，记录本次认证时间。在  `timestamp_timeout`（默认 `5` 分钟）内再次执行 `sudo` 不需要重复输密码
        - 身份验证通过并确认授权后，`sudo fork` 出子进程，设置子进程的用户和组 `ID` 为目标用户（通常为 `root`），然后通过 `execve()` 执行命令

    - **关键安全设计**
        - `/etc/sudoers` 的语法格式：**user host=(runas) command**
        - `user` 可以是用户名或 `%group`（组名前加 `%`）
        - `host` 限制该规则适用的主机名（多机共享 `sudoers` 时有用）
        - `runas` 指定可以切换成哪个用户的身份运行命令
        - `command` 允许使用通配符，但不建议过于宽松
        - 配置文件必须通过 `visudo` 编辑，它会检查语法错误，防止错误的配置锁死所有 `sudo` 权限

- **协助记忆**
    - `sudo` 的 `SUID` = 门禁系统本身是一个超级权限设备，任何人刷它都能开门禁系统的主控室
    - `/etc/sudoers` = 门禁的授权名单，写着谁有权限进哪个房间、什么时间能进
    - 密码认证 = 刷卡后还要按指纹确认是你本人，防止别人拿你的卡刷进来
    - 时间戳缓存 = 按过一次指纹后 5 分钟内再刷卡不用再按，方便但不代表门一直开着

- **进阶思考**
    - **`sudo -i`、`sudo -s`、`sudo su -` 三者有什么区别？**
        - `sudo -i` 以 `root` 身份启动一个登录 `Shell`，会加载 `root` 用户的 `/root/.bash_profile`、`/root/.bashrc` 等环境配置文件，工作目录切到 `/root`。`sudo -s` 以 `root` 身份启动一个非登录 `Shell`，继承当前用户的环境变量和工作目录。`sudo su -` 是先提权到 `root`，然后 `su -` 再切换到 `root` 的登录环境，和 `sudo -i` 效果类似，但多了一层额外的进程创建

    - **`sudo` 配置了 `NOPASSWD` 是否安全？什么场景下合理使用？**
        - `NOPASSWD` 跳过了密码验证步骤，意味着只要有人能访问该用户的终端就能直接执行 `sudo` 命令。这个配置通常用于自动化脚本（`CI/CD` 流水线、`Ansible`）或特殊管理账户。不建议为普通用户设置全局 `NOPASSWD`。一个相对合理的折中方案是：只为特定命令设置 `NOPASSWD`（如 `%admin ALL=(ALL) NOPASSWD: /usr/bin/systemctl restart nginx`），而不是允许所有命令无密码执行


## 🤔 同为系统 1 号进程，systemd 对比 sysvinit 有哪些区别？  
- **`sysvinit` 是传统的 `System V` 风格 `init`，按顺序串行启动服务；`systemd` 是替代方案，引入并行启动、依赖管理、按需启动等机制。两者的区别不只是"启动快慢"，而是对"系统如何管理服务"这个问题的完全不同思路。**

    - **启动方式**：串行 vs 并行
        - `sysvinit` 按照 `/etc/rc.d/rcX.d/` 中的编号（`S01`、`S02`……）逐个启动服务，前一个完成才能启动下一个。即使两个服务完全没有依赖关系，也要排队等前面启动完
        - `systemd` 通过分析 `Unit` 之间的依赖关系，并行启动没有依赖冲突的服务。即使有依赖的服务也会等到就绪后再并行触发下游，能在多核系统上充分发挥并行能力
        - 结果：`sysvinit` 启动一个完整系统的耗时通常是所有串行启动时间的总和；`systemd` 可以大幅缩短实际启动耗时
        
    - **服务管理**：`Shell` 脚本 `vs` `Unit` 声明式配置
        - `sysvinit` 的服务启动脚本是 `Shell` 脚本（`/etc/init.d/httpd`），逐行执行命令。每个服务自己控制 `fork`、`PID` 文件路径、如何停止或重载
        - `systemd` 的服务配置是 `Unit` 文件（`/etc/systemd/system/xxx.service`），声明式的键值对配置： `ExecStart`、`ExecStop`、`Restart`、`User`、`Group` 等
        - `Shell` 脚本灵活但容易出错——每个服务的启动、停止逻辑都可能不一致。`systemd` 的声明式配置统一了行为模式，减少了实现差异
        
    - **按需启动**：`sysvinit` 全量拉起 vs `systemd` 懒加载
        - `sysvinit` 在启动时一次性拉起 `/etc/rcX.d/` 下的所有服务，不管这个服务当前是不是真的需要
        - `systemd` 支持 `Socket-activated` 和 `D-Bus-activated` 按需启动：一个服务的端口即使没有启动进程也能被监听，只有真正有请求连接时才拉起服务进程
        - 实践中的例子：`sshd.socket` + `sshd.service` 分离——系统启动时只监听 `22` 端口，有人 `SSH` 连接时才真正启动 `sshd` 进程
        
    - **进程管理**：`PID` 追踪 vs `Cgroup` 隔离
        - `sysvinit` 通常通过 `PID` 文件追踪服务进程，但后台程序在启动后 `fork` 出的子进程可能脱离父进程，导致"服务 `stop` 了但子进程还在跑"
        - `systemd` 使用 `Cgroup（Control Group）` 管理服务进程——所有由该服务产生的进程都会归入同一个 `Cgroup`，`stop` 时 `systemd` 可以沿着 `cgroup` 树清理干净整个进程组，不会遗漏子进程
        - 这是 `systemd` 相比 `sysvinit` 的一个重要改进——传统 `sysvinit` 清理子进程前需要确认进程树结构，而 `cgroup` 的进程分组模式允许 `systemd` 沿着 `cgroup` 层级一次性完成清理

    - **日志管理**：无统一日志 vs `journald` 集中采集
        - `sysvinit` 下每个服务自己决定日志怎么记录，有的写文件（`/var/log/`），有的走 `syslog`，有的直接输出到终端
        - `systemd` 内置 `journald`，自动捕获所有服务的 `stdout`/`stderr`，统一集中管理。`journalctl -u nginx.service` 直接查，不需要知道日志文件在哪
        - `journald` 日志包含结构化的元数据（时间戳、PID、优先级），查询更精确

- **协助记忆**
    - **`sysvinit`（传统发车方式）** ：发车员拿着名单逐个喊号上车——喊到 `S01` 才能发车，跑完全程回来再喊 `S02`。每个车上的人员配置方式都不一样（`Shell` 脚本），有的从左边上车、有的从右边上车。所有车都停在站台等着发车（全量启动），不管车上有没有旅客
    - **·（高铁调度中心）**：调度中心看依赖关系——没有冲突的列车可以同时发车，到站时间不同不影响发车顺序。每辆列车的操作规程是标准化的（声明式配置）。调度中心的时刻表管理可以按需调配发车时间

- **进阶思考**
    - **`systemd` 启动的服务的启动日志和运行日志只能用 `journalctl` 看？那 `/var/log/` 下的文件还有用吗？**
      - `journalctl` 会捕获 `stdout`/`stderr`，但不会自动覆盖应用自己写到文件中的日志。如果 `Nginx` 在配置中指定了 `access_log /var/log/nginx/access.log`，日志仍然写入那个文件，`journald` 不会拦截它。`journald` 捕获的只有服务进程往 `stderr`/`stdout` 输出的内容，以及通过 `syslog`（`SD_JOURNAL_SUPPRESS_SYSLOG` 默认未设置时）转发的日志。如果不想保留 `journald` 的采集，可以在 `service` 中设置 `StandardOutput=null` 或 `StandardError=null` 跳过捕获

    - **`sysvinit` 脚本能否在 `systemd` 系统上运行？怎么迁移？**
      - 可以。`systemd` 提供兼容层，如果某个服务只有 `/etc/init.d/xxx` 脚本而没有 `.service` 文件，`systemd` 通过 `systemd-sysv-generator` 自动为其生成一个临时的 `service` 单元，功能基本正常。但 `generator` 生成的 `unit` 无法利用 `systemd` 的高级特性（`cgroup` 管理、`socket` 按需激活等）。如果需要迁移，推荐的做法是创建对应的 `.service` 文件，通过 `systemctl enable` 启用后确保 `sysvinit` 脚本不再有残留影响（如果 `service` 文件优先级更高，`systemd` 会在运行时优先使用它），可以移除老的 `init` 脚本

## 🤔 软链接和硬链接有什么区别？  
- **软链接（`Symbolic Link`）和硬链接（`Hard Link`）都是 `Linux` 中让多个文件名指向同一份数据的方式，但它们的实现层级完全不同——一个在 `inode` 层面、一个在文件路径层面。理解它们的关键是搞清 `inode`、数据块、文件路径三者的关系。**

    - **硬链接**：多个文件名指向同一个 `inode`
        - 硬链接的本质是在目录中创建一个新的目录项，指向一个已有的 `inode` 编号。`inode` 中有一个 `nlink` 计数器，记录有多少个硬链接指向它
        - 创建方式：`ln target link_name`
        - 关键特征：
            - 所有硬链接共享同一个 `inode`。`ls -i` 看 `inode` 编号相同
            - 删除任意一个硬链接，不会影响其他硬链接。数据只会在 `nlink` 归零时才被释放
            - 不能跨文件系统——`inode` 只在同一文件系统内唯一
            - 不能链接目录（除 `.` 和 `..` 外）——内核为了防止目录循环引用导致递归遍历死循环，禁止创建目录的硬链接

        - `ls -l` 中第二列的数字就是硬链接计数。普通文件通常是 `1`，每增加一个硬链接就 `+1`

    - **软链接**：一个独立的文件，存的是目标路径
        - 软链接本身是一个独立的文件（有自己的 `inode`），文件内容存储的是目标文件的路径字符串
        - 创建方式：`ln -s target_path link_name`
        - 关键特征：
            - 软链接有自己的 `inode`，和 `target` 的 `inode` 不同
            - 如果 `target` 被删除，软链接依然存在，但指向了一个不存在的路径——变成"断裂的软链接"
            - 可以跨文件系统——存的是路径字符串，不依赖 `inode`
            - 可以链接目录——不会导致循环引用，内核能通过路径解析检测到环

        - `ls -l` 中会显示 -> 指向的目标路径。权限显示为 `lrwxrwxrwx`，但实际权限由目标文件决定

- **协助记忆**
    - **硬链接**：就像一栋房子有两个门牌号，无论拆掉哪个门牌号，房子都还在；只有所有门牌号都没了，房子才会被拆除。 
    - **软链接**：就像路口的一块指路牌，它只告诉你房子在哪里；房子拆了，指路牌还在，但它已经指向一个不存在的地方了。


- **进阶思考**
    - **修改一个硬链接的内容，其他硬链接会同步更新吗？**
        - 会。因为所有硬链接指向同一个 `inode`，这个 `inode` 指向同一组数据块。你通过任意一个硬链接修改文件内容，都是修改同一组数据块。所以其他硬链接打开的也是修改后的内容。这正是硬链接共享数据的本质

    - **软链接的目标路径变了，指向目标的软链接会怎样？**
        - 软链接存的是"创建时指定的路径字符串"，不是 `inode`。如果目标文件被删除后在同位置重建（`inode` 变了但路径名相同），软链接仍然有效——因为它按文件名找。如果目标文件被移到了其他路径，软链接就断了。移动目标文件（路径变了）才会导致软链接断裂，覆盖重建（路径没变）不会影响

## 🤔 systemd 在 Linux 系统中负责哪些核心任务？  
- **`systemd` 不只是一个"启动服务的程序"，它是 `Linux` 系统的管理和编排引擎。作为 `PID 1`，它在系统启动时第一个运行，负责拉起所有其他进程，并在系统运行期间持续管理服务、设备、挂载点、定时任务等系统资源。它的核心任务可以归纳为几个维度：启动编排、服务管理、资源管理、日志管理、设备管理**

    - **服务管理（最核心的任务）**
        - `systemd` 统一管理系统上所有守护进程的启动、停止、重启、状态查询。通过 `.service Unit` 文件声明式定义，替代了传统的 `/etc/init.d/` `Shell` 脚本
        - 相比 `sysvinit`，`systemd` 管理服务时对进程生命周期的感知更完整——通过 `cgroup` 跟踪所有子进程，停止服务时不会遗漏子进程，不需要依赖 `PID` 文件确认状态
        - 健康检查：通过 `ExecStartPre`、`ExecStartPost` 和 `ExecReload` 等生命周期钩子，支持更精细的服务状态管理
        - 自动重启：通过 `Restart=` 策略（`always`、`on-failure`、`on-abnormal` 等），服务异常退出后自动拉起，不需要外部监控脚本

    - **系统启动编排（构建依赖树，并行启动）**
        - `systemd` 通过 `Unit` 之间的依赖关系（`After=`、`Requires=`、`Wants=`）构建完整的启动依赖图，并行启动没有冲突的服务
        - 将启动目标拆分为多个 `target`（`multi-user.target`、`graphical.target` 等），每个 `target` 代表系统达到某个运行级别
        - `systemd-analyze blame` 查看各服务启动耗时，`systemd-analyze critical-chain` 查看启动瓶颈链路
        - 按需启动（`Socket-activated`、`D-Bus-activated`、`path-activated`），避免服务在系统启动时全量拉起，只在被真正访问时才启动

    - **日志管理（journald）**
        - `systemd` 的附属组件 `journald` 自动捕获所有服务的 `stdout`/`stderr` 以及内核日志，统一写入 `/var/log/journal/`
        - `journalctl -u nginx.service` 直接查看某个服务的日志，不需要去翻 `/var/log/nginx/` 下有没有 `access.log` 或 `error.log` ——— 但这条命令不会拦截应用自己写到文件中的日志
        - `journalctl --since "1 hour ago" --until "30 min ago" -p err` 按时间窗口和日志级别精确过滤
        - `journald` 日志包含结构化元数据（时间戳、PID、UID、GID、优先级、内核设施标识等）

    - **设备管理（`udev` 集成）**
        - `systemd` 整合了 `udev`（设备管理器） ，通过 `systemd-udevd` 服务统一管理热插拔设备和设备命名规则
        - 设备插入时 `udev` 根据规则（`/etc/udev/rules.d/`）执行对应动作，如加载驱动、创建设备节点、触发自动挂载等。通过 `systemd.device` 单元，设备的可用性可以管理服务启动顺序——例如挂载 `/data` 对应的存储设备就绪后再启动依赖该存储的服务

    - **定时与任务管理（`timer`）**
        - `systemd` 自带的 `timer` 替代传统的 `cron`。`timer` 在功能上分为两类：
            - **单调时钟 `timer`（`OnUnitActiveSec`、`OnBootSec`）**：从过去某个事件发生后间隔一段时间触发
            - 日历时间 `timer`（`OnCalendar=`）：按指定的日历时间触发，格式为 星期 时:分:秒，支持精度到微秒

        - 相比 `cron` 的优势：支持更灵活的时间表达（例如 `OnCalendar=Mon..Fri 02:00:00` 表示工作日凌晨 `2` 点）；通过 `journalctl -u <timer>` 查看 `timer` 的执行日志，不需要去 `/var/log/cron` 翻记录；`timer` 与 `service` 分离，同一个 `service` 可以被多个 `timer` 关联触发；`systemctl list-timers` 可查看所有已配置的定时任务及下次执行时间

- **协助记忆**
    - **`systemd` 是一个酒店的运营管理体系而不仅仅是大堂经理**
        - **服务管理（`systemctl`）** ：各功能部门的标准化运营规程——客房部什么时间打扫（`.service` 文件的声明式配置）、出故障了自动恢复（`Restart=on-failure`）
        - **启动编排（`target`）** ：酒店开业流程——先通电、再开空调、再让前台上线。没有直接依赖关系的部门可以同时开工
        - **日志管理（`journald`）** ：酒店中央监控室的监控大屏——所有部门的运行状态一目了然，不需要去每个楼层翻记录
        - **设备管理（`udev`）** ：酒店的设施管理系统——哪个房间进了新设备自动登记，设备拔了自动注销
        - **定时任务（`timer`）** ：酒店的自动服务通知——固定时间清理、每隔 X 小时补充消耗品，比传统的手写排班表更灵活

- **进阶思考**
    - **`systemd timer` 相比 `cron` 有什么优势？什么时候仍然需要用 `cron`？**
        - `timer` 的优势在于：与 `systemd` 生态完全集成——日志通过 `journald` 管理、依赖关系可以和其他 `service`/`target` 联动、支持精度到微秒的定时触发。但 `cron` 也有不可替代的场景：需要在不同机器之间使用统一的定时任务格式时 `cron` 的移植性更好、用户级 `crontab` 配置更简单（`crontab -e` 一行一个任务）。对于大部分服务器场景，新写定时任务优先用 `timer`，移植老式的 `cron` 任务成本较高时可以保持现状

    - **`systemd` 被批评的点有哪些？**
        - `systemd` 被批评的核心问题包括：过于庞大——从 `init` 发展到包含日志、设备管理、网络（`systemd-networkd`）、`DNS`（`systemd-resolved`）等模块，突破了"最小化"的 `Unix` 设计哲学；日志二进制格式——`journald` 使用二进制格式存储日志，传统文本工具（`grep`、`less`）无法直接读取；与其他系统组件的耦合度——部分组件（如 `systemd-resolved` 接管 `/etc/resolv.conf`）在部分运维场景下需要在排查和调整之间增加步骤，同时也使得希望更换特定组件的团队需要评估更多的关联影响


- **扩展信息**
    - `systemd timer` 的配置方式对比
        - **方式一**：传统方式（两个文件，最完整）
            - 创建 `backup.service` 定义执行内容，`backup.timer` 定义执行时间
            - 功能最完整，支持复杂的依赖关系和资源控制，适合需要持久化保留的生产环境定时任务
            - 配置较繁琐，适合长期稳定的任务

        - **方式二**：一条命令瞬态 `timer`（最快捷，适合临时任务）
            - `systemd-run --on-calendar="daily 03:00:00" --unit=backup /usr/local/bin/backup.sh`
            - 一条命令同时创建 `.service` 和 `.timer`，不需要手动写文件
            - 执行 `systemctl list-timers` 能看到它
            - 缺点：重启后消失（瞬态 `timer`），持久化需要再加 `systemctl enable backup.timer`

        - **方式三**：`systemd-cron`（兼容 `crontab` 语法）
            - 安装 `systemd-cron` 包后，继续用 `crontab -e` 写 `* * * * *` 格式
            - 该工具将传统的 `crontab` 规则自动转换为 `systemd timer unit` 来执行
            - 适合团队中已习惯 `crontab` 语法且不愿迁移的场景


    - **`OnCalendar` 时间格式速查**
        | 含义               | `crontab`      | `systemd OnCalendar` |
        | :----------------- | :------------- | :------------------- |
        | 每分钟             | `* * * * *`    | `*-*-* *:*:*`        |
        | 每天 `3` 点        | `0 3 * * *`    | `daily 03:00:00`     |
        | 每 `5` 分钟        | `*/5 * * * *`  | `*-*-* *:00/5:00`    |
        | 每半小时           | `*/30 * * * *` | `*-*-* *:00/30:00`   |
        | 工作日 `4:30`      | `30 4 * * 1-5` | `Mon..Fri 04:30:00`  |
        | 每月 `1` 号 `2` 点 | `0 2 1 * *`    | `*-*-01 02:00:00`    |
        | 每周一 `3` 点      | `0 3 * * 1`    | `Mon 03:00:00`       |
    
    不确定格式时可以用 `systemd-analyze calendar` "你的时间表达式" 验证，它会输出标准化形式和下次执行时间
        
        

## 🤔 如何编写一个生产级 Systemd 服务配置文件？  
- **一个生产级的 `.service` 文件不只是写 `ExecStart` 就够了。它需要覆盖：进程如何启停、资源上限、故障后怎么处理、安全隔离到什么程度、日志怎么管理。以下是一个标准模板，逐段说明各配置的作用。**
    - **完整的生产级 `Service` 模板**
        ```ini
        [Unit]
        Description=My Production Service
        # 当前服务的功能说明

        Documentation=https://wiki.example.com/my-service
        # 文档链接，方便后续维护的人查资料

        After=network-online.target mysql.service
        Wants=mysql.service
        # After: 保证在这些单元启动后当前服务才启动。只控制顺序，不强制依赖
        # Wants: 期望依赖——如果 mysql 启动失败这个服务仍然会启动
        # Requires: 硬依赖——如果 mysql 挂了这个服务也会被停止，生产环境谨慎使用

        [Service]
        Type=simple
        # simple: ExecStart 启动后认为服务已就绪（默认）
        # forking: 程序启动后会 fork 出子进程，父进程退出
        # notify: 程序通过 sd_notify() 通知 systemd 已就绪
        # oneshot: 一次性任务，执行完就结束

        User=myapp
        Group=myapp
        # 以非 root 用户运行，最小权限原则

        WorkingDirectory=/opt/myapp
        # 进程的工作目录

        EnvironmentFile=/etc/myapp/env.conf
        # 从文件中加载环境变量，避免敏感信息写在 service 文件里

        ExecStart=/usr/local/bin/myapp --config /etc/myapp/config.yml
        ExecReload=/bin/kill -HUP $MAINPID
        ExecStop=/bin/kill -TERM $MAINPID
        # ExecStart: 启动命令，必须使用绝对路径
        # ExecReload: 重载配置时的操作，通常用 HUP 信号
        # ExecStop: 停止命令，不配置的话 systemctl stop 会发送 SIGTERM

        Restart=on-failure
        RestartSec=5
        StartLimitBurst=3
        StartLimitIntervalSec=60
        # Restart: 什么情况下自动重启——always(总是)、on-failure(失败时)、on-abnormal(异常退出)
        # RestartSec: 重启前的等待时间，防止快速重启导致系统压力
        # StartLimit: 60 秒内最多重启 3 次，超过后停止尝试，不再自动重启

        TimeoutStartSec=30
        TimeoutStopSec=30
        # 启动/停止的超时时间。超过后 systemd 会发 SIGKILL 强制终止

        KillMode=mixed
        # control-group: 杀掉整个 cgroup 内的所有进程（默认，最彻底）
        # process: 只杀主进程，不清理子进程
        # mixed: 先发 SIGTERM 给主进程，再发 SIGKILL 给整个 cgroup

        [Service]
        # 资源限制
        LimitNOFILE=65536
        LimitNPROC=65536
        # 文件描述符和进程数上限。不设的话有些服务在高并发下会因为 ulimit 不够而报错

        MemoryMax=2G
        MemoryHigh=1.6G
        CPUQuota=80%
        # MemoryMax: 硬限制，超过后进程被 OOM Killer 杀掉
        # MemoryHigh: 软限制，超过后内核尝试回收内存但不强杀
        # CPUQuota: CPU 使用上限，80% 相当于用满 0.8 个核心

        OOMScoreAdjust=-500
        # oom_score_adj 调整值。范围 -1000（永不杀）~ 1000（优先杀）
        # -500 表示该服务比一般进程更不容易被 OOM Killer 选中，但又不至于完全免疫

        [Service]
        # 安全加固 （Security Hardening）
        ProtectSystem=full
        # full: /usr 和 /etc 以只读方式挂载，服务只能写 /var 和 /tmp
        # strict: 更严格，几乎所有系统目录都只读
        # true: 只保护 /usr

        ProtectHome=true
        # 禁止服务访问 /home、/root、/run/user 目录

        PrivateTmp=true
        # 为服务创建独立的 /tmp 和 /var/tmp 命名空间，不与其他进程共享临时文件

        NoNewPrivileges=true
        # 禁止服务及其子进程通过 suid、setcap 等方式获取更高权限

        CapabilityBoundingSet=CAP_NET_BIND_SERVICE
        # 限制进程能使用的 Linux Capabilities
        # 只需要监听端口的服务，给 CAP_NET_BIND_SERVICE 就够了

        [Install]
        WantedBy=multi-user.target
        # 当 systemctl enable 时，该服务被加入到 multi-user.target 的依赖中
        # 系统启动到 multi-user 级别时自动启动该服务
        ```

    - **各段配置说明**
        - **`[Unit]`**：描述服务的基本信息和依赖关系。`After` 和 `Requires` 需要配合使用——单独写 `After` 只控制「启动顺序」而不控制「启动与否」。`Wants` 表示软依赖，被依赖方启动失败不影响当前服务；`Requires` 表示硬依赖，被依赖方失败时当前服务也会被停止
        - **`[Service]`**: 定义如何启动、停止、重启服务，以及服务的运行环境、资源限制。`Type` 的选择需要根据进程自身的生命周期特点——简单进程用 `simple`，传统 `daemon` 用 `forking`
        - **`[Install]`**: 定义 `systemctl enable` 时将服务安装到哪个 `target`。大部分服务用 `multi-user.target`，图形界面相关服务用 `graphical.target`

- **协助记忆**
    - 一个生产级 `service` 文件看四项：怎么跑（`User`/`ExecStart`）→ 跑崩了怎么办（`Restart`/`StartLimit`）→ 能用多少资源（`MemoryMax`/`CPUQuota`/`LimitNOFILE`）→ 安全隔离做没做（`ProtectSystem`/`PrivateTmp`/`NoNewPrivileges`）

- **进阶思考**
    - **`Restart=always` 和 `Restart=on-failure` 有什么区别？**
        - `always` 表示只要进程退出就重启，不管退出码是 `0` 还是非 `0`。`on-failure` 只在退出码非 `0`、被信号杀死（不包括 `SIGTERM` 和 `SIGHUP`）、或者操作超时的情况下才重启。对于守护进程类服务，通常用 `on-failure`，因为正常停服（`systemctl stop`）时不应该触发自动重启；对于需要保持 `7x24` 运行的服务，用 `always` 配合 `StartLimitBurst` 防止崩溃后无限循环重启

    - **`ProtectSystem=full` 和 `PrivateTmp=true` 会影响服务写日志怎么办？**
        - `ProtectSystem=full` 下，`/var` 仍然是可写的，日志写在 `/var/log/` 下不受影响。`PrivateTmp=true` 只是给服务分配了独立的 `/tmp` 空间，不会影响 `/var/log` 的写入。但如果服务的日志写到了 `/usr/local/var/` 或者 `/opt/app/logs/` 下，就需要根据路径规划调整 `ProtectSystem` 的严格程度或选择 `/var/log` 等可写目录存放

## 🤔 如何在业务高峰期安全地重启服务？  
- **高峰期间重启服务的核心原则是：不影响正在处理的请求，不让用户感知到服务中断。这需要在重启前做流量调度、重启时优雅关闭、重启后逐步放量三步走。**

    - **第一步**：先判断是不是必须在高峰期重启
        - 如果不是紧急安全漏洞或服务已经不可用，建议等低峰期再操作。凌晨 `3 ~ 5` 点通常是大多数业务的最低谷
        - 如果必须重启，准备好完整的回退方案——新版本启动失败能快速切回旧版本

    - **第二步**：摘流量——让重启不影响新请求
        - 重启前先将这台节点从负载均衡器中摘除，等待几秒到几十秒让已经进入的请求处理完毕。`nginx upstream` 中 `server 10.0.0.1:80 down`;，或者云负载均衡器的控制台操作
        - 如果服务有健康检查接口，可以让它返回不健康状态，负载均衡器检测到后自动摘除
        - 摘流量后观察片刻，确认没有新请求接入（`ss -lntp | grep <端口>` 看连接数是否在下降），再开始重启
        - 如果是多节点集群，可以先验证单台节点的重启流程，确保操作正确后再逐台操作，不要同时在多台节点上重启

    - **第三步**：优雅停止服务
        - **优先使用应用的优雅关闭机制，而不是直接 `kill -9`**：
            - **`Nginx`**：`nginx -s reload` 或 `kill -HUP <master_pid>`
            - **`systemd` 服务**：`systemctl stop <service>`（发送 `SIGTERM`）而不是 `systemctl kill -s SIGKILL`

        - 给进程足够的关闭时间（`TimeoutStopSec` 通常设 `30~60` 秒），让正在处理的请求在超时时间内完成
        - 应用层面应该注册关闭信号处理函数，收到 `SIGTERM` 后停止接受新请求、处理完当前请求再退出

    - **第四步**：启动新版本并接入流量
        - 启动后先单独验证：`curl -I http://127.0.0.1:<port>/health` 确认健康检查通过
        - 确认应用日志没有新增报错，监控指标（错误率、延迟）没有异常
        - 将节点重新加入负载均衡器，观察流量接入后的指标变化
        - 如果是多节点，逐台操作，每台完成后观察一段时间再操作下一台。可以在灰度验证阶段的 `30 ~ 60` 分钟观察无异常后再继续

    - **第五步**：准备好回退方案
        - 如果新版本启动后出现异常，立即摘除该节点流量，回退到旧版本
        - 回退方式：如果是代码变更，重新部署旧版本镜像；如果是配置变更，恢复旧配置后 `reload`
        - 明确什么条件下触发回退——如健康检查连续失败 `3` 次、错误率上升超过 `5%`、`P99` 延迟升高超过 `2` 倍


- **协助记忆**
    - **高峰重启四步走**：摘流量（秒级）→ 优雅停（秒到分）→ 启动验证（分）→ 接回流量（分）
    - **红线**：不先摘流量就直接 `systemctl restart` 会让正在处理的请求掉入黑洞

- **进阶思考**
    - **如果只有一台机器没法摘流量，怎么重启？**
        - 单机场景下重启必然会导致短暂中断，只能尽可能缩短中断时间。可以通过配置热升级（如 `Nginx` 的 `binary upgrade` ——— 新老进程并行处理请求、逐步切换）、使用 `systemd` 的 `Type=notify` 实现新进程就绪后再关闭旧进程等方式。如果没有任何热升级机制，需要接受几秒到几十秒的中断。单机架构本来就不具备无损重启的条件，这个问题的最佳答案是"加一台机器做到多节点"

    - **重启 `Nginx`，是要 `restart` 还是 `reload`？**
        - `reload`，`nginx -s reload` 不中断现有连接 ——— 新启动的 `worker` 进程加载新配置，旧 `worker` 处理完已有连接后优雅退出。`restart`（`systemctl restart nginx`）会先 `stop` 再 `start`，`stop` 时所有连接被强行中断。对 `Nginx` 而言，`restart` 基本没有理由使用。`reload` 不能覆盖的场景是 `Nginx` 二进制版本升级（如安全更新），这时需要 `binary upgrade`：向 `Master` 进程发送 `USR2` 信号启动新二进制，再发送 `WINCH` 信号逐步关闭旧 `worker`


## 🤔 防火墙 firewalld 和 iptables 的关系与区别？  
- **`firewalld` 和 `iptables` 不是竞争关系，而是上层管理工具和底层内核模块的关系。`iptables` 是内核 `Netfilter` 框架的命令行接口，直接操作内核网络规则；`firewalld` 是一个后台守护进程，它底层调用的仍然是 `iptables` 命令。**

    - **本质区别**：一个管规则语法，一个管规则管理方式
        - **`iptables`**：直接操作内核的 `Netfilter` 规则表。每次执行 `iptables -A INPUT -p tcp --dport 80 -j ACCEPT` 都是即时生效的，但重启后规则丢失（需要手动保存到文件）。`iptables` 可以精确控制每一条规则的插入位置、顺序、匹配条件
        - **`firewalld`**：一个系统服务，它管理 `iptables`/`nftables` 规则。你通过 `firewall-cmd` 告诉它"开放 80 端口"`，firewalld` 自己维护一份规则定义，自动将其翻译为底层 `iptables` 或 `nftables` 规则并动态应用。`firewalld` 的主要价值是"运行时动态加载"和"区域管理"两个功能

    - **`firewalld` 的核心特性**
        - **区域（`Zone`）** ：不同网络接口应用不同策略。比如 `eth0`（外网）走 `public` 区域（仅开放 `80`、`443`），`eth1`（内网）走 `trusted` 区域（全部放行）。区域这个概念在 `iptables` 层面通过多套规则集合手动切换可以实现，但相对繁琐
        - **运行时和永久配置分离**：`firewall-cmd --add-port=80/tcp` 立即生效但不持久，加上 `--permanent` 才写入配置文件。`firewall-cmd --reload` 重新加载永久配置但不中断已建立的连接
        - `firewalld` 也可以直接操作富规则（`rich rule`），本质上还是通过配置 `iptables` 参数接口间接实现类似的规则管理。由于在高版本中，底层已从 `iptables` 迁移到 `nftables`，在少量边缘规则的兼容性细节上与直接使用 `iptables` 存在差异

    - **底层变迁**：从 `iptables` 到 `nftables`
        - 早期 `firewalld` 底层使用 `iptables` 命令生成规则。`RHEL/CentOS 8+`、`Fedora`、`Ubuntu 22.04+` 默认使用 `nftables` 作为内核防火墙框架，`iptables` 命令本身已成为 `nftables` 的兼容 `wrapper`（通过 `iptables-nft` 实现）。但仍保留 `iptables-legacy` 用于兼容旧脚本
        - `firewalld` 在新版本中默认后端也是 `nftables`。所以现在执行 `iptables -L` 看到的规则可能其实是 `firewalld` 通过 `nftables` 生成的，因为 `iptables` 命令已经被重定向到 `nftables` 内核接口

- **协助记忆**
    - `iptables` 是螺丝刀，直接拧螺丝（内核规则），灵活但每次要自己收拾工具
    - `firewalld` 是一个工具箱管理程序，内置了区域划分功能、运行时/持久配置分离机制，底层可能调用 `iptables` 或 `nftables`。你告诉它*在 `public` 区域开放 `80` 端口*，它自己搞定

- **进阶思考**
    - **服务器上能不能同时使用 `firewalld` 和 `iptables`？**
        - 不能直接混用。`firewalld` 启动时会接管 `iptables`/`nftables` 规则链，你手动用 `iptables -A` 添加的规则可能在 `firewalld` 重启后丢失。生产环境中二选一：用 `firewalld` 就通过 `firewall-cmd` 管理，直接用 `iptables`/`nftables` 就关闭 `firewalld`。但在 `RHEL 8+` 上，`iptables` 命令默认是 `nftables` 的兼容层，即使 `firewalld` 运行时你执行 `iptables -L` 看到的是 `firewalld` 生成的规则，而不是你自己独立管理的规则集

    - **`firewalld` 和 `ufw` 是什么关系？**
        - `ufw`（`Uncomplicated Firewall`）是 `Ubuntu` 上默认的前端工具，和 `firewalld` 的定位类似——都是上层管理工具。`ufw` 底层同样调用 `iptables`/`nftables`。区别是：`ufw` 不提供区域（`Zone`）概念，更适合单机简单场景；`firewalld` 的区域模型在有多网卡、多信任级别的服务器上更灵活。`Debian` 系发行版默认没有 `firewalld`，提供 `ufw` 作为可选的简化配置工具，而 `RHEL` 系发行版默认安装并启用 `firewalld` 

---

> 作者: [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.3/  

