运维常见题-日常维护(四)

目录

🤔 Linux 系统内存持续飙高,如何排查?

  • 内存飙高的排查思路是:先看整体内存分布(到底谁在消耗内存)→ 再定位具体进程或缓存类型(是应用泄漏还是内核缓存膨胀)→ 最后针对性处理。

    • 第一步:看整体内存分布——确认内存去哪了

      • free -h 看懂三行数据:总内存、已用、可用。其中 available 列是系统估算的真实可用内存(考虑了可回收的缓存),比单纯看 used 更准确
      • cat /proc/meminfo 更详细,重点看几个字段:
        • MemTotal / MemFree / MemAvailable:总量、空闲、真实可用
        • Buffers / Cached:缓冲区 + 页面缓存。这部分内存可以在内存紧张时被回收,不是真正的"已用"
        • Active(anon) / Inactive(anon):活跃和非活跃的匿名页(进程堆栈)。如果 Inactive(anon) 持续增高且 Cached 很低,可能是内存泄漏或大内存业务在累积脏数据
        • Shmem:共享内存,包括 tmpfsDocker overlay 等。如果这个值很高,检查 /dev/shmDocker 的挂载
    • 第二步:区分是应用内存还是缓存——避免误判

      • topM 排序查看进程 RES。但注意 RES 包含共享内存(SHR),多个进程的 RES 相加会超过物理内存总量——因为共享库在内存中只有一份,但每个进程的 RES 都计入了它。
      • ps aux --sort=-%mem | head 10 列出内存占用最高的进程
      • 如果 total RES - SHR 总和远小于物理内存"已用",剩余的大头在 Cached/Slab/Shmem 里,不是应用泄漏
      • 一个判断技巧:free -havailable 如果还很高(超过物理内存的 20%),即使 used 看起来高,系统实际上也不缺内存
    • 第三步:定位具体进程

      • ps -eo pid,rss,vsz,cmd --sort=-rss | head -n 10 定位具体进程
      • pmap -x <PID> 查看进程内各内存段的占用细节——哪个地址段是堆、哪个是栈、哪个是 mmap 映射文件
      • Java 进程:jstat -gc <PID> 1s 5 看堆内各分区使用情况,jmap -heap <PID> 看堆配置和当前使用量
      • 对可疑进程,持续观察:top -p <PID>RES 是否持续增长。增长后不回落且整体趋势向上,说明内存泄漏
    • 第四步:排除缓存层面的内存膨胀

      • Slab 过高cat /proc/meminfo | grep -i slabSlab 是内核缓存(dentryinode 等)。如果 Slab 占用过大且 SReclaimable 占大部分,调低 vm.vfs_cache_pressure 可以加快回收
      • Page Cache 过高但 available 偏低:通常是系统中有大量文件读写操作(数据库 I/O 或日志写入)产生了大量缓存,这时如果确认非业务高峰,可以 sync; echo 3 > /proc/sys/vm/drop_caches 手动清理——但要注意手动清理后短期内缓存重建可能会产生短暂的 IO 压力上升。available 会正常回升
      • tmpfs / Shmem 过高df -h /dev/shmmount -t tmpfs 查看 tmpfs 挂载点使用情况。Docker 默认的 /var/lib/docker 如果使用了 overlay2 也可能产生大量的 shmem 使用
  • 协助记忆

    • 内存排查四步:一看整体(free -h)、二查来源(ps/pmap)、三辨类型(缓存还是应用)、四追泄漏(top 持续观察
    • available 才是真实可用内存,不要只看 usedCachedBuffers 占了内存不一定是坏事——它们是可回收的
  • 进阶思考

    • free -h 显示的 used 很高(90%+),但 available 也还有 30%,这算内存不足吗?

      • 不算。Linux 的内存管理原则是"空闲内存就是浪费内存",它会尽量用空闲内存做缓存来提高 IO 性能。used 高但 available 充足说明大部分"已用"内存是可回收的缓存,不是进程真正占住的。需要关注的指标是:available 持续低于物理内存的 10~15%si/sovmstat 1 输出)持续大于 0,这才说明真正内存紧张
    • ps aux 里看到的 RSStop 里的 RES 不完全一致,以哪个为准?

      • 两者本质上是同一个数据(RSS = Resident Set Size),数值差异来源于统计时机不同。top 实时刷新,ps aux 是执行瞬间的快照。在排查时以 top 持续观察为准,因为能看趋势

🤔 Linux 系统硬盘读写慢,如何排查?

  • 硬盘读写慢的排查思路是:先用工具确认慢在硬件层还是软件层(是磁盘本身响应慢,还是 IO 请求排队太多),再定位到具体是哪个进程在产生 IO,最后针对性处理。

    • 先确认磁盘的繁忙程度和响应时间

      • iostat -xz 1 持续观察,关键看几个指标
        • r/s + w/s(每秒读写次数):是否达到了磁盘的标称 IOPS 上限。机械盘通常 100~200SATA SSD1 万~10 万,NVMe50 万~100
        • rMB/s + wMB/s(每秒读写带宽):是否达到了磁盘或存储链路(SATA 6GbpsPCIe 4.0 x4)的带宽上限
        • %util:设备忙绿比例。NVMe 在多队列并发下即使 100% 也可能正常,要结合 r/s 判断;机械盘如果持续 100% 说明确实饱和了
        • await(平均 IO 响应时间):这是最关键的一个指标。如果机械盘 await 持续超过 15msSSD 超过 2ms,说明 IO 请求在排队,磁盘确实忙不过来
        • avgqu-sz(平均队列长度):如果 > 1 说明有请求在排队,磁盘处理速度跟不上请求速度
    • 区分磁盘本身慢还是请求排队多

      • 如果 await 高但 svctm(服务时间)正常 → 排队导致的延迟,IOPS 已达上限
      • 如果 await 高且 svctm 也高 → 磁盘本身响应慢,可能是硬件故障、降级或链路问题
      • 队列长度是核心判断依据:avgqu-sz 远大于磁盘的队列深度(通常 32~128)说明严重排队;队列很短但 await 高说明磁盘本身有问题,建议检查传输模式、线缆状态等
    • 用更细粒度的工具定位 IO 来源

      • iotop -oP:直接看到哪个进程在大量读写磁盘,以及实时的读写速率
      • pidstat -d 1:按进程输出磁盘读写统计,适合在没有 iotop 的环境中替代使用
      • iostat 看磁盘级别、iotop 看进程级别后如果确认是某进程的问题,可进一步通过 lsof -p <PID>strace -e trace=read,write -p <PID> 监控其正在操作的文件
    • 文件系统和挂载参数层面的排查

      • mount | grep <partition> 确认挂载参数,检查是否开启了 noatime。如果没有,每次读取文件都会触发 atime 写入,增加额外的写 IO
      • cat /sys/block/sda/queue/scheduler 查看 IO 调度器。机械盘建议 mq-deadlineNVMe 建议 none
      • blockdev --getra /dev/sda 查看预读大小。默认 256128KB),顺序读为主的工作负载可以适当增大到 4096
      • cat /proc/meminfo | grep Dirty 查看脏页数量。如果脏页在持续增长,说明写入速度超过了刷盘速度,可能需要调整 vm.dirty_background_ratiovm.dirty_ratio
    • 如果一切正常但依然慢——检查软件层

      • 确认磁盘是否在做 RAID 重构或一致性检查。cat /sys/block/sdX/device/state 正常应为 running
      • 确认是否有 swap 导致频繁换入换出(vmstat 1si/so 持续大于 0
      • 确认硬盘健康状态没有恶化:smartctl -aReallocated_Sector_Ct 持续增长说明硬盘有坏道,每次读写坏道区域都会触发重映射导致显著的延迟(相比正常 IO,坏道重映射的延迟可能高出数百倍)
  • 协助记忆

    • 排查先看 iostat -> await 判断是不是在排队 -> avgqu-sz 判断是排队长了还是盘本身慢了
    • 再上 iotop 看是哪个进程在写
    • 再看调度器和挂载参数
  • 进阶思考

    • iostat%utilNVMe 盘上达到 100%,但 await 只有 0.5ms,这正常吗?

      • 正常。NVMe 盘支持多队列并发,每个队列可以独立处理 IO 请求。%util 统计的是设备是否在某时刻处于繁忙状态,在 NVMe 的多队列模式下,设备几乎总处于繁忙状态,即使 IO 响应极快。对 NVMe 盘更应该看 avgqu-szr/s + w/s 是否达到了盘的标称 IOPS 上限,而不是 %util
    • iostat 看到磁盘 await 正常,但应用层感知的读写延迟很高,可能是什么原因?

      • 问题不在磁盘本身,在软件栈。可能是文件系统的锁竞争(如多个线程并发写入同一个文件)、数据库 buffer pool 太小导致频繁刷脏页、或者虚拟机/容器的存储栈有额外的延迟叠加(如 qemuIO 路径)。排查思路:在应用内部打点(trace)测量文件读写操作的耗时,如果应用测到的耗时远高于 iostatawait,说明延迟出在从应用系统调用到内核 IO 路径的某个中间层,而不是磁盘本身
    • 服务器用的是 SSD,但突然变得很慢,基本排除 IO 压力和硬件故障,还有什么原因?

      • 检查「是否触发了 PCIe 降速」。lspci -vv -s <NVMe 控制器地址> | grep -i speed 可以对比实际运行的链路速度和最大支持速度。如果显示 Speed 2.5GT/s 而非 8GT/s(PCIe 3.0)16GT/s(PCIe 4.0),说明链路发生了降速,通常是因为 PCIe 信号完整性下降或供电不足。彻底恢复需要断电重启

🤔 /etc/fstab 写错导致系统无法启动,怎么修复?

  • fstab 写错导致系统无法启动,本质上是内核或 systemd 在启动时尝试挂载一个不存在的设备或使用了错误的挂载参数,挂载失败后系统无法完成初始化,进入 Emergency Mode(紧急模式)等待修复。

    • fstab 写错时系统会有什么表现

      • 启动时卡住,显示 A start job is running for /dev/sdX... 并持续等待 90 秒超时,然后跳过进入 Emergency Mode
      • 或者直接显示 Welcome to Emergency Mode,按回车后出现 root 密码登录提示
      • 卡住的挂载点通常是你在 fstab 中配了但实际不存在的设备,systemd 在等它出现
    • 如何进入 Emergency Mode

      • 大多数 fstab 错误导致系统启动失败时,会自动进入 Emergency Mode,屏幕提示 Welcome to Emergency Mode!,输入 root 密码登录即可。如果系统卡在 A start job is running 的等待提示、没有自动跳转,可以手动切换:

        • GRUB 菜单界面按 e 编辑启动参数,找到 linux 开头的行,在行末加 systemd.unit=emergency.target,按 Ctrl+XF10 启动
        • 也可以加 single 进入单用户模式,但单用户模式下根分区依然是只读挂载的,之后仍需手动执行 mount -o remount,rw /
      • 进入后系统会提示输入 root 密码。如果连 root 密码也忘了,需要用安装 U 盘进 Rescue 模式绕过

    • 进入 Emergency Mode 后的修复步骤

      • 根分区通常是只读挂载的,需要先重新挂载为读写:mount -o remount,rw /
      • 查看 /etc/fstab 找出错误行。常见错误类型:UUID 写错了(用 blkid 获取正确的 UUID)、设备路径不存在、挂载选项语法错误、文件系统类型(第三列)写错
      • 根据错误类型修复:修正 UUID、修正或删除错误选项、注释掉不再需要的挂载点(在行首加 #
      • 修复后执行 mount -a 测试 fstab 中所有条目是否都能正常挂载。如果 mount -a 不报错,说明 fstab 已修复成功,重启系统即可
    • Emergency Mode 都进不去的情况

      • 如果是根分区本身的 UUID 写错或文件系统损坏,系统可能连 Emergency Mode 都到不了。此时只能用安装 U 盘启动进入 Rescue 模式
      • Rescue 环境中手动挂载根分区(mount -t ext4 /dev/sda1 /mnt),然后编辑 /mnt/etc/fstab 修正 UUID。如果根分区完全挂载不上,说明问题不在 fstab 而在文件系统本身,需要先 fsck 修复文件系统
    • 能提前预防的手段

      • 重启前执行 mount -a 测试所有挂载项,能提前发现大部分 fstab 错误
      • 对非关键数据分区的挂载项加上 nofail 选项,即使该分区挂载失败系统也不会卡在等待状态,而是正常跳过继续启动到正常模式
  • 协助记忆

    • fstab 错了进不了系统,简单三步:

      1. Emergency Mode 登录 → mount -o remount,rw /
      2. 编辑 fstab:修正 UUID、注释不需要的行
      3. mount -a 验证 → reboot
    • 预防:mount -a 重启前测一把,不关键的盘加 nofail

  • 进阶思考

    • 修复完 fstabmount -a 报错 wrong fs type,但 UUID 确认没错,为什么?

      • 文件系统类型(fstab 第三列)写错了。例如磁盘是 xfs 但写成了 ext4,或者内核没有加载对应的文件系统驱动。修正类型后重试 mount -a
    • 根分区本身在 fstab 中配置了错误的 UUID,连 Emergency Mode 都进不去,怎么办?

      • 用安装 U 盘启动进入 Rescue 模式,手动挂载根分区后编辑 fstab 修正 UUID。如果根分区完全无法挂载,说明可能是文件系统损坏,需要先 fsck 修复
  • 扩展信息

    • 网络挂载(NFS / CIFS)的 fstab 配置注意事项

      • 网络挂载和本地挂载最大的区别:系统启动时网络还没就绪,如果 fstab 中的 NFS 挂载项没有加 _netdevnofailsystemd 会在启动时尝试挂载 NFS,此时网络未就绪导致挂载失败,系统卡在等待状态 90 秒后跳入 Emergency Mode
      • 正确的 NFS 挂载配置192.168.1.100:/data /mnt/data nfs _netdev,nofail,soft,timeo=100 0 0
      • _netdev:告诉 systemd 这是一个网络设备,需要网络就绪后再尝试挂载
      • nofail:挂载失败时不阻止系统继续启动
      • soft vs hardhard(默认)下 NFS 服务端宕机时访问该挂载点的进程会卡在 D 状态等待,soft 下超时后返回错误给应用(但可能丢失数据)。生产环境通常用 hard + intrhard + 较短的 timeoretrans 参数
      • 如果是 CIFS/SMB 挂载//192.168.1.100/share /mnt/share cifs _netdev,nofail,username=user,password=pass,uid=1000,gid=1000 0 0
      • 网络挂载出问题时,Emergency Mode 可能也进不去——因为 mount -a 会尝试挂载所有 fstab 条目,包括那个不可达的网络挂载点,导致卡住。建议在修复网络挂载前,先用 mount -a --exclude-type=nfs4 跳过 NFS 挂载,或者先注释掉网络挂载行再执行 mount -a
    • 常用挂载选项说明

      • defaults:包含了 rwsuiddevexecautonouserasync 的组合
      • noauto:系统启动时不自动挂载,需要手动 mount。适用于偶尔使用的移动硬盘、备份盘,或者需要按需挂载的网络存储。配合 x-systemd.automount 可以实现访问即挂载
      • auto / noauto:是否在 mount -a 时自动挂载。noauto 的条目不会在启动时挂载
      • nofail:挂载失败时不阻止系统启动。非关键分区建议加上
      • _netdev:标记为网络设备,systemd 会在网络就绪后才尝试挂载。NFS/CIFS/iSCSI 挂载必须加
      • noatime / relatime:不更新文件访问时间 / 相对更新访问时间。noatime 能减少大量读操作产生的额外写 IO,但对依赖 atime 的应用(如邮件系统)有影响
      • bind:将某个目录绑定挂载到另一个位置。mount --bind /original /target,在 fstab 中配置为 /original /target none bind 0 0
      • x-systemd.automountsystemd 特性,将挂载点设为按需挂载——只有真正访问该目录时才触发挂载操作,而不是在系统启动时挂载。适用于网络挂载或低频使用的目录
      • x-systemd.device-timeout=30:等待设备出现的超时时间,默认 90 秒。配合 nofail 可以缩短启动时等待不可用设备的卡顿时间
    • systemd 生成 .mount 单元

      • fstab 中的每条挂载项在系统启动时会被 systemd-fstab-generator 自动转换为一个 .mount 单元。例如 /etc/fstab 中的 /data 挂载项会生成 data.mount(路径中的 / 被替换为 -,顶级根目录对应 -.mount
      • 可以通过 systemctl list-units --type=mount 查看所有自动生成的 mount 单元
      • 也可以通过手动创建 /etc/systemd/system/data.mount 文件来代替 fstab 配置,格式与 fstab 略有差异但功能更完整(依赖关系、顺序控制等)
      • 执行 systemctl daemon-reload 后,手动创建的 mount 单元会和 fstab 条目共同生效。如果两者指向同一挂载点,systemd 会以手动创建的单元为准,忽略对应的 fstab

🤔 Linux 上挂载远程或云存储有哪些常见方式?各自的优缺点和适用场景是什么?

  • Linux 上挂载远端存储没有一个通用最优解,选哪条路取决于你需要什么样的协议兼容性、安全加密层级和并发规模——是在同一个可信内网里跑高吞吐读写,还是跨公网传输一份加密数据,通常决定了最终差异很大的技术选型。

    • NFSNetwork File System)— 最成熟的传统方案

      • 特点:标准 POSIX 文件系统语义,支持文件锁、目录结构完整。多台 Linux 客户端同时挂载同一份存储,行为和使用本地文件系统一致
      • NFS v3(传统版):依赖 rpcbind,端口不固定,无加密;
      • NFS v4(现代版):不再强制依赖 rpcbind,可以配 Kerberos 实现传输加密
      • 缺点:安全性偏弱(v3 裸奔、v4 加密需要额外维护 Kerberos);防火墙规则麻烦(v3 端口不固定);v4 配置较复杂
      • 适用场景:内网环境下的虚拟机镜像存储、高可用共享目录、数据库备份归档等对 POSIX 语义要求高的场景
      • 生产安全加密方式:内网用 NFS v4,如果需要加密传输则配 Kerberos。公网环境不建议直接暴露 NFS,建议走 VPNSSH 隧道再做 NFS 挂载
    • sshfs(基于 SSH 的文件系统挂载)

      • 特点:基于 FUSE,通过 SSH 协议挂载远程目录,不需要服务端额外装软件(SSH 本身就开着)。传输全程加密,天然满足 PCI-DSSHIPAA 等合规要求
      • 缺点:性能远不如 NFS(单线程、高延迟)。并发大了容易卡死,不适合高负载场景。稳定性受网络波动影响较大
      • 适用场景:临时挂载传文件、合规要求严格且不想搭 Kerberos 的场景、目标机器只有 SSH 可通没有其他协议可用
    • 云存储挂载(s3fs / SDK 直读 / NFS 网关)

      • s3fs:通过 FUSES3/OSS 挂载为本地目录。适合只读场景(模型分发、静态资源加载),多写场景下容易出现你遇到的"目录被当成文件"的问题。不是配置问题 ,是 S3 没有目录概念这个机制性限制
      • SDK 直读:应用代码里直接用 aws s3 cpossutil 或各语言的 SDK 操作对象存储,不走 FUSE。这是最稳定、性能最好的方案,没有之一
      • NFS 网关:云厂商提供的对象存储 NFS 网关(阿里云 OSS NFS 网关、AWS Storage Gateway),应用端 mount 为标准 NFS 目录,不需要额外依赖。但 NFS 网关本身有运行成本,并且比直接 SDK 访问多经过一个中间层
      • 适用场景s3fs 适合只读分发场景;SDK 直读适合新开发的云原生应用;NFS 网关适合需要迁移历史应用、不想改代码但有 POSIX 挂载需求的运维团队
  • 协助记忆

    • NFS = 内网共享的"标准套餐"(成熟稳定,但防火墙和加密问题需要额外处理)
    • sshfs = 合规场景的"加密专线"(自带加密、零依赖,但跑不快)
    • s3fs = 云上只读的"轻量挂载"(读模型、加载资源很方便,写多了就要出问题)
    • SDK 直读 = 云原生的"最优解"(不挂载、不走 FUSE、不折腾)
    • NFS 网关 = 历史应用的"兼容过渡方案"(不改代码上云对象存储)
  • 进阶思考

    • 在内网多台服务器之间共享文件,最推荐的方式是什么?

      • 内网场景下有标准网络文件共享需求的,NFS 仍然是最合适的选择。如果对传输加密有要求,NFS v4 配合 Kerberos 可以实现加密传输,但需要额外部署一套 KDC 来管理票据。如果合规要求很高(如 PCI),可以走 VPN + NFSSSH 隧道 + NFS,但在这种链路提前加密的情况下,sshfs 可能是一个更简单的选择
    • 你的这个场景——因为 PCI 合规要求必须用加密传输,选择了 sshfs。如果替换为 NFS v4 + Kerberos 可以吗?为什么不选那个方案?

      • 技术上完全可以,但运维成本完全不一样。Kerberos 需要维护 KDC 服务、管理 keytab 文件、处理时钟同步问题。如果只是一个简单的日志归档或配置文件挂载,撑死只挂到一两台机器上,为这个上一套 KDC 是杀鸡用牛刀。sshfs 在合规上同样满足要求,零额外组件,更务实

🤔 什么是标准输入、标准输出、标准错误?

  • 标准输入标准输出标准错误Linux 系统为每个进程默认分配的三个 I/O 通道。它们是管理进程中数据流向的抽象接口,核心价值在于:可以把命令串起来用管道传递数据、把正常输出和错误信息分开处理、以及 把输出重定向到文件或丢弃到垃圾站。

    • 标准输入(stdin,文件描述符 0

      • 进程读取输入数据的默认来源。默认情况下,stdin 连接到当前终端——你敲键盘时输入的字符通过 stdin 传递给程序

      • 常用重定向方式:

        • command < file:将文件内容作为命令的标准输入
        • command << EOF:使用 Here Document 将多行文本作为标准输入
        • command1 | command2:将 command1 的标准输出作为 command2 的标准输入
      • 例子:grep "error" < /var/log/syslog 从文件中读取内容作为 grep 的输入

    • 标准输出(stdout,文件描述符 1

      • 进程输出正常数据的默认去向。默认情况下,stdout 也连接到当前终端,程序打印的内容会显示在屏幕上

      • 常用重定向方式:

        • command > file:将标准输出写入文件(覆盖)
        • command >> file:将标准输出追加到文件末尾
        • command1 | command2:将 command1 的标准输出管道给 command2
      • 例子:ls -l > filelist.txt 把命令的正常输出结果写入文件

    • 标准错误(stderr,文件描述符 2

      • 进程输出错误信息的默认去向。默认情况下,stderr 同样连接到当前终端,所以平时看到错误信息也打在屏幕上

      • stderrstdout 分开的意义是:可以把正常结果和错误日志从同一个标准流里分离出来单独存储或丢弃——正常数据走一个管道用于进一步处理,错误信息走另一个用于排查

      • 常用重定向方式:

        • command 2> error.log:将标准错误单独写入文件
        • command > output.log 2>&1:将 stdoutstderr 合并到同一个文件(2>&1 的意思是把文件描述符 2 重定向到文件描述符 1 当前指向的位置)
        • command &> output.log:与上面等价,是 bash 的简写
        • command 2>/dev/null:丢弃错误信息(不推荐在排查问题时使用,可能导致排障信息丢失)
      • 例子:find / -name "*.conf" 2>/dev/null 在搜索文件时排除权限错误提示

  • 协助记忆(以公司内部的文件流转为比喻)

    • stdin = 你的收件箱。别人发给你的原始材料从这里进来,你处理什么取决于收到了什么
    • stdout = 你处理完的发件箱。正常的成果从这里输出,可以发给下一个人继续处理(管道),也可以归档存到文件夹(重定向到文件)
    • stderr = 你手边的碎纸机。处理出错的废纸直接碎掉,不塞进发件箱里影响正常工作流程。你可以选择定期清理碎纸机(2>/dev/null 丢弃),也可以把碎纸单独拿出来分析出了什么问题(2>error.log
  • 进阶思考

    • command > file 2>&1command 2>&1 > file 有什么区别?

      • 结果完全不同。Shell 会按照从左到右依次处理重定向。
        • command > file 2>&1
          • 先将 stdout 重定向到 file
          • 再将 stderr 重定向到 stdout 当前所指向的位置(即 file
          • 最终:stdout 写入 filestderr 也写入 file
        • command 2>&1 > file
          • 先将 stderr 重定向到 stdout 当前所指向的位置(终端)
          • 再将 stdout 重定向到 file
          • 最终:stdout 写入 filestderr 仍输出到终端。
      • 2>&1 不是"绑定 stdout",而是"复制 stdout 当前的去向";Shell 又是从左到右执行重定向,所以顺序决定结果。
    • /dev/null 是什么?为什么把数据丢进去就消失了?

      • /dev/null 是一个特殊的设备文件,写入它的任何数据都会被内核直接丢弃,读取它会立刻返回 EOF。它是数据黑洞,用于丢弃不需要的输出。比如 command > /dev/null 2>&1 丢弃全部输出,只关心命令的退出码;command 2>/dev/null 只隐藏错误信息但保留正常输出。排查故障时建议谨慎使用丢弃操作,可能会让一些关键的调试信息在日志清理中被遗漏
    • 执行某个命令的帮助(如 nc --help),终端能正常显示,但 command --help > file 重定向到文件后文件是空的,为什么?

      • 通常因为该命令的帮助信息输出到了 stderr 而不是 stdout。部分命令(特别是 BSD 系工具,如 nc、以及某些脚本工具)将帮助信息视为"诊断输出"写入 stderr> file 只重定向 stdout,抓不到 stderr 的内容。需要用 command --help 2> file(只抓 stderr)或 command --help &> file(两者都抓)才能捕获。判断方法:command --help > /dev/null 如果屏幕还有输出,说明走的是 stderr;没输出了说明走的是 stdout
  • 扩展信息

    • 常见重定向写法速查

      写法含义例子
      command > filestdout 写入文件(覆盖)ls > list.txt
      command >> filestdout 追加到文件echo "done" >> log.txt
      command 2> filestderr 单独写入文件find / 2> error.log
      command > file 2>&1stdoutstderr 合并写入文件make build > build.log 2>&1
      command &> file同上,bash 简写make build &> build.log
      command > /dev/null 2>&1丢弃全部输出只关心退出码时使用
      command < file从文件读取 stdingrep error < /var/log/syslog
      command << EOF ... EOFHere Document,多行字符串传入 stdin脚本中传入多行配置
      command1 | command2管道,command1stdout 传给 command2stdinps aux|grep nginx
      command 2>&1 | command2stdoutstderr 一起通过管道传给下一个命令需要同时处理正常和错误输出时
    • 常见陷阱

      • 顺序问题:command 2>&1 > file 先把 stderr 指向终端,再把 stdout 指向文件——结果 stderr 仍然打到屏幕上,只有 stdout 进了文件。2>&1 必须放在最后
      • command > file 2>&1 可以拆解为:先让 stdout 指向 file,再把 stderr 重定向到 stdout 当前指向的位置(即 file),所以两者都进了文件
      • command 2>&1 > file 先让 stderr 指向 stdout(此时还在终端),再把 stdout 指向 file。所以 stderr 仍然输出到终端

🤔 在命令后面加 2>&1 是什么意思?

  • 2>&1 是把标准错误(stderr)重定向到标准输出(stdout)当前指向的地方,让错误信息和正常信息输出到同一个地方。

    • 拆开来理解

      • 2>:重定向 stderr(文件描述符 2
      • &1:引用 stdout(文件描述符 1)当前指向的目标
      • 合起来:把 stderr 的输出路径改成和 stdout 一样
    • 为什么需要这个

      • 默认情况下,stdoutstderr 都输出到屏幕,看起来没区别。但一旦把 stdout 重定向到了文件(command > file),stderr 仍然输出到屏幕,两种信息就分开了
      • 2>&1stderr 也拉进文件里,保证日志文件里既有正常输出也有错误信息,排查时不用看两个地方
    • 两个常见写法的差异

      • command > file 2>&1:先让 stdout 指向 file,再把 stderr 指向 stdout 当前的位置(也就是 file)。结果:stdoutstderr 都进 file
      • command 2>&1 > file:先让 stderr 指向 stdout(此时 stdout 指向屏幕),再把 stdout 指向 file。结果:stderr 打到屏幕,只有 stdoutfile
  • 协助记忆

    • 2>&1 就像开会时说“把 CC(抄送)的邮件也发到和收件人同一个地方”(2>&1& 不是字符串拼接,而是"指向"的意思,不是把 stderr 的内容拼接到 stdout 后面,而是让 stderr 的输出路径和 stdout 一致)
    • 顺序很重要——先 > file2>&1,两者的出口才一致;搞反了顺序,stderr 就打到屏幕上而不是文件里了

🤔 你在日常工作中 dd 命令用过哪些场景?

  • dd 是一个低级别的数据复制工具,它不关心文件类型或文件系统,直接按块读写数据。使用场景集中在:制作启动盘、磁盘备份与恢复、数据擦除、性能测试这几个方向。

    • 制作启动盘

      • ISO 镜像写入 U 盘:dd if=/path/to/ubuntu.iso of=/dev/sdb bs=4M status=progress
      • status=progress 显示实时写入进度和速度,不加的话整个过程是静默的
      • 注意 of 指向的是磁盘设备(/dev/sdb),不是分区(/dev/sdb1),写完后 U 盘上原有的分区表会被覆盖
    • 磁盘到磁盘的完整克隆

      • 整盘对拷:dd if=/dev/sda of=/dev/sdb bs=4M status=progress
      • 如果两块盘容量不同,目标盘比源盘大没问题,反过来不行
      • 常用于换硬盘场景:新盘接上后 dd 把旧盘全量复制过去,然后直接换上新盘就能启动
    • 备份和恢复分区或 MBR

      • 备份分区表或 MBR(前 512 字节):dd if=/dev/sda of=/backup/mbr.bin bs=512 count=1
      • 恢复:dd if=/backup/mbr.bin of=/dev/sda bs=512 count=1
    • 数据擦除

      • 用随机数覆盖整盘防止恢复:dd if=/dev/urandom of=/dev/sdb bs=4M status=progress
      • rm 安全得多 ——— rm 只删文件系统的索引,数据块内容还在磁盘上。安全擦除应该覆盖整盘数据
      • 注意:如果用的是 SSD,因为内部 FTL 和磨损均衡机制,dd 无法真正写入到所有已使用的物理块。SSD 的安全擦除应该用 blkdiscardnvme format
    • 性能测试(简单粗暴的磁盘基准)

      • 测试写入性能(写到 /dev/null 测的是内存,写到磁盘文件才测磁盘):dd if=/dev/zero of=/tmp/test bs=1M count=1000 conv=fdatasync
      • 测试读取性能:dd if=/tmp/test of=/dev/null bs=1M count=1000
      • conv=fdatasync 强制写完后刷盘,否则 dd 返回的数据可能只是写到了缓存里,测出来的是一个虚高的速度
      • 注意:这是粗略测试,不是专业基准测试。fio 才是生产环境用来测磁盘性能的正确工具
    • 创建指定大小的文件

      • 创建一个大文件用于 swap 或测试:dd if=/dev/zero of=/swapfile bs=1M count=4096
      • 等效于 fallocate -l 4G /swapfile,但 fallocate 是立即分配空间(不实际写入数据),dd 是逐个块写入。对于 swap 文件,两者都可以用
  • 协助记忆

    • if = input file(输入),of = output file(输出),bs = block size(块大小),count = 块数
    • 常用场景四件事:写 U 盘、克隆磁盘、擦除数据、粗测速度
    • 两个关键参数:status=progress 看进度,conv=fdatasync 真正刷盘
  • 进阶思考

    • 为什么 dd 测出来的磁盘性能和 fio 测出来的差距很大?

      • dd 是单线程、顺序的读写方式,只能测出磁盘的顺序读写极限带宽。fio 可以模拟各种 IO 模式(随机读写、混合读写、不同队列深度、不同块大小)。大多数业务负载并不是单一顺序读写的模式,所以 dd 测出来的"好看"的数字并不代表在真实业务场景下的表现
    • dd 命令如果不小心把 of 指定错了(比如写成了正在使用的系统盘),怎么抢救?

      • 如果已经按下了回车,立刻拔电源或强制关机,不要再做任何写入操作。然后用另一台机器或 U 盘启动,尝试用 testdisk 恢复分区表。如果 dd 刚启动不久且只覆盖了少量数据(前几 MB),分区表损坏但是数据区域大概率还在,恢复成功率高。如果 dd if=/dev/zero 覆盖了大量磁盘空间,恢复难度会极大提升

🤔 Linux 里 /dev/null 设备文件是什么?

  • /dev/null 是一个特殊的设备文件,写入它的任何数据都会被内核直接丢弃,读取它会立刻返回 EOF(空文件)。它是系统的"数据黑洞"或"垃圾桶",主要用于丢弃不需要的输出。

    • 它的本质

      • /dev/null 是一个字符设备,主设备号 1,次设备号 3。它不占用磁盘空间——你向它写 1GB 数据,磁盘空间不会有任何变化
      • 写入操作永远成功(write() 返回写入的字节数,但实际上什么都没存),读取操作永远返回空(read() 返回 0,表示 EOF
      • 它的存在不是 Linux 独有的,POSIX 标准要求系统必须提供 /dev/null
    • 最常见的用途

      • 丢弃不需要的输出command > /dev/null 2>&1stdoutstderr 都丢进黑洞,只关心命令的退出码
      • 只保留错误输出command > /dev/null 只显示错误信息,隐藏正常运行时的输出
      • 清空文件cat /dev/null > /var/log/large.log> /var/log/large.log 将日志文件清空而不删除文件(不改变 inode,不中断正在写该文件的进程句柄)
      • 用作空输入的来源:某些命令需要输入但你没有实际数据时,command < /dev/null 让它读到一个空的输入流,立刻结束
    • 相关的特殊设备文件

      • /dev/zero:读取它返回无限的 \x00 字节。常用于创建指定大小的空文件(dd if=/dev/zero of=file bs=1M count=100
      • /dev/random/dev/urandom:读取它返回随机字节。/dev/random 在熵池不足时会阻塞等待,/dev/urandom 不会阻塞。在现代 Linux 内核(4.8+)上两者差别已经很小,绝大多数场景应该使用 /dev/urandom。用于安全擦除(dd if=/dev/urandom of=/dev/sdb)或生成随机密码
      • /dev/full:写入它永远返回"设备已满"(ENOSPC)。用于测试程序在磁盘满时的行为
  • 协助记忆

    • /dev/null 就是垃圾桶——扔进去的东西就消失了,从里面往外掏只能掏出空气(EOF
    • 乱扔垃圾也不占空间——对着它倒 100GB 数据,磁盘空间一点不少
    • 它的亲戚:/dev/zero 是空白打印机(无限打印空白页),/dev/urandom 是雪花机(无限喷随机雪花),/dev/full 是已经满了的垃圾桶(试你程序会不会处理"满了"的错误)
  • 进阶思考

    • cat /dev/null > file> file 有什么区别?
      • 效果完全一样,都是把文件内容清空。> fileShell 的重定向语法,在打开文件时使用 O_TRUNC 标志直接截断文件内容并保留 inode,不需要调用 cat/dev/nullcat /dev/null > file 是先打开 /dev/null 读到一个 EOF(空),再写入文件覆盖原有内容。后者多了一次 cat 进程的开销,没有任何实际优势。所以清空文件用 > file 就够了
    • 为什么不能用 rm 清空日志文件而要用 > filecat /dev/null > file
      • rm 删除文件再重建会改变 inode,正在写该日志的进程仍然持有旧 inode 的文件句柄,会继续往那个已经"消失"的文件里写数据——磁盘空间不会释放。> file 只清空内容不改变 inode,进程句柄仍然有效,继续写入的内容会写到重新开始的文件中。这是线上清日志的标准做法

🤔 ss 和 netstat 同样查看端口连接,ss 相比 netstat 有什么优势?

  • ssnetstat 的现代替代方案,核心优势在于性能更快、信息更全、过滤更强。在连接数多的机器上,两者的差距会变得非常明显。

    • 性能差距(最核心的优势)

      • netstat 遍历 /proc/net/tcp 等文件来收集信息,内核每次都要为这些虚拟文件生成文本输出,再由 netstat 一行行解析。在高并发机器上(几万条连接),每次执行 netstat 都会消耗不少 CPU
      • ss 通过 netlink 机制直接从内核获取 socket 数据结构,不经过多次文件 IO 和文本解析。在连接数超过 1 万的机器上,ss 的速度优势非常明显;连接数 5 万以上时,netstat 可能需要好几秒才能输出完毕,而 ss 几乎是瞬间返回
    • 信息量更全面

      • ss 能输出 netstat 看不到的信息

        • TCP 内部的运行状态参数,如拥塞窗口大小、慢启动阈值、RTT 估算值。ss -i 可以查看每个连接的这些参数,用于排查网络性能问题
        • ss -p 显示的进程信息比 netstat 更快速(不需要遍历所有进程匹配 PID 和端口)
        • ss -o 显示连接的定时器信息(重传定时器、keepalive 定时器等)
      • ss -s 直接输出系统级别的 TCP 连接状态统计汇总(各状态的连接数),比 netstat -s 更直观简短

    • 过滤能力更强大

      • netstat 的过滤主要靠 grepss 原生支持多种过滤方式

        • TCP 状态过滤ss -tan state time-wait 只显示 TIME_WAIT 状态的连接
        • 按端口范围过滤ss -tan sport = :80ss -tan dport > :1024
        • 按地址+端口组合过滤ss -tan src 10.0.0.1:http dst 10.0.0.2:https
      • 这些过滤由内核直接实现,不需要在用户态用 grep 扫全量输出

  • 协助记忆

    • ss 更快netstat 读文件,ssnetlink 直通内核。万条连接下 netstat 跑好几秒,ss 几乎不耗时
    • ss 更全:能看到 TCP 内部参数(拥塞窗口、RTT
    • ss 过滤更强:内置按状态、端口、地址过滤,不需要 grep 扫全量输出
  • 进阶思考

    • ss -tuln 中的四个字母分别代表什么?

      • t = TCPu = UDPl = listening(只显示监听状态的 socket)、n = numeric(不解析域名和服务名,直接显示 IP 和端口号)。ss -tuln 是最常用的组合——快速查看本机上所有正在监听的端口
    • 我执行 ss -lntp 显示的 Send-Q 很大(几百甚至几千),这意味着什么?

      • Send-Q 在监听端口(LISTEN)上表示当前 accept 队列的大小——等待应用程序调用 accept() 接受的已完成 TCP 三次握手的连接数量。如果这个值持续很大,说明应用程序处理新连接的速度跟不上新连接到达的速度。可能的原因:应用线程池不够、事件循环阻塞、应用本身在阻塞操作上卡住导致无法及时处理新连接

🤔 Linux 两台服务器之间数据同步如何实现?

  • 数据同步没有万能方案,选什么工具取决于同步方向(单向还是双向)、数据量(几 MB 还是几 TB)、频率(一次性还是持续实时)。最常用的方案覆盖了从一次性同步到持续同步再到双向同步的场景。

    • rsync——最常用的单向增量同步

      • 只传输变化的部分(增量同步),不是每次重新复制整个文件。传输过程中如果中断,下次执行时继续传完未完成的部分,不需要重来

      • 基本用法:rsync -avz /local/path user@host:/remote/path

        • -a:归档模式,保留权限、时间戳、软链接等属性
        • -v:显示详细输出
        • -z:传输时压缩,适合跨公网的场景。走内网时去掉 -z 速度更快
      • --delete:如果源端文件被删除,目标端也删掉。常用于镜像备份场景——使目标端和源端保持完全一致

      • --exclude--include 过滤不需要同步的目录或文件类型

    • scp / sftp ——— 简单的单次传输

      • 最简单的文件复制方式,基于 SSH 加密:scp -r /local/path user@host:/remote/path
      • 缺点:不支持增量传输,每次都是全量复制。不适合大文件或频繁同步的场景。在数据量大或需要定时同步的情况下应该用 rsync
    • 持续实时同步:lsyncd + rsync / inotify

      • 如果需要在文件变更时立即同步到另一台服务器,用 lsyncdLive Syncing Daemon)。它通过 inotify 监控目录变化,触发 rsync 执行增量同步
      • 相比 crontab 定时 rsynclsyncd 的优点是延迟低(秒级触发)、不需要设定固定同步间隔
      • 配置示例:lsyncd -rsync /local/path host:/remote/path
    • 双向同步:unison

      • rsync 只解决单向同步。如果两台服务器都改同一份数据,需要双向同步时,用 unison。它比 rsync --delete 安全——如果检测到文件在两侧都修改过,unison 默认提示用户手动解决冲突,而不是静默覆盖任意一端
      • 适用场景:两台 Web 服务器需要同步配置文件、多台机器上的用户上传文件目录等需要双向保持一致且需要检测冲突的场景
  • 协助记忆

    • 看场景选方案:一次性传文件 → scp / rsync(视数据量选择全量或增量),定期同步 → cron + rsync,实时单向同步 → lsyncd,实时双向同步 → unison
    • rsync 是万能的起点——它支持本地同步、远程 SSH 同步、守护进程模式、增量传输,还能在传输前后通过 --rsync-path 执行自定义脚本,大部分单向同步场景都能覆盖
  • 进阶思考

    • rsync 的参数中 -avz 里的 -z 在内网环境下建议用吗?

      • 不建议。-z 会在传输时压缩数据再发送,目标端再解压。在内网千兆/万兆环境下,网速不再是瓶颈,压缩和解压消耗的 CPU 时间反而比传输本身更长。所以内网 rsync 去掉 -z,外网或带宽有限的场景保留 -z
    • rsync 同步过程中如果中断了,下次执行会从断点继续吗?

      • rsync 基于文件级别的增量传输,不是块级别的断点续传。如果一个大文件只传了一半就中断了,下次 rsync 会重新从头开始传输那个文件。对于超大文件(几 GB 以上)的断点续传需求,建议使用 rsync 配合 --partial 参数,或考虑使用 lftpmirror 命令

🤔 Linux 排查 8080 端口被哪个进程占用,有哪些命令?

  • 排查端口占用最快的方法是直接用 ss -lntp,但不同场景下有不同工具可以用,有的给的信息更详细,有的在旧系统上更常见。

    • ss(首选,现代系统的标准工具)

      • ss -lntp | grep 8080
      • -l:只显示监听中的端口,-n:不解析服务名(避免把 8080 显示成 http-alt),-t:TCP-p:显示进程信息(PID 和进程名)
      • 如果知道本地任意一个正在使用该端口的进程 PID,可以通过 ss -lntp 找到它后确认是否为需要定位的进程
      • 输出示例:LISTEN 0 128 0.0.0.0:8080 0.0.0.0:* users:(("java",pid=12345,fd=29))
    • netstat(传统工具,部分系统已不预装)

      • netstat -tlnp | grep 8080
      • 参数含义和 ss 基本一致。需要先安装 net-tools
      • 在连接数超过几万的高并发机器上,netstat 明显比 ss
    • fuser(按端口号查进程)

      • fuser 8080/tcp
      • 直接输出占用该端口的 PID。加 -v 显示更多进程信息:fuser -v 8080/tcp
      • -k 直接杀掉占用进程:fuser -k 8080/tcp(慎用,会直接终止进程)
      • 适用场景:只需要快速拿到 PID 去处理,不需要看连接详情
    • lsof(功能全面,但输出信息量大)

      • lsof -i :8080
      • 列出所有打开 8080 端口的进程信息,包括 PID、进程名、用户、文件描述符
      • lsof -i :8080 -sTCP:LISTEN 只显示监听中的连接,过滤掉已建立的连接
      • 适用场景:需要看更多信息时
  • 协助记忆

    • 查端口占用四条路

      • ss -lntp | grep 8080(最快、最全)
      • fuser 8080/tcp(最简洁,拿到 PID 就走)
      • lsof -i :8080(信息最详细)
      • netstat -tlnp | grep 8080(传统工具,新系统可能需额外安装)
    • 优先用 ssss 不在再用 lsoffuser 适合脚本中快速拿到 PID

  • 扩展信息

    • netstatLinux 内核 5.8 版本开始陆续进入过时状态,net-tools 包在 RHEL 8+Ubuntu 18.04+ 中不再默认安装
    • ssiproute2 包中的工具,作为 net-tools 的替代品。它的数据来源是 netlink,在高并发场景下对系统负担更轻

🤔 Linux 一个进程最多能创建多少线程?

  • 理论上限由多个因素共同决定:内存大小、栈空间、系统 PID 上限、用户进程数上限。在实际环境中,最常卡住的是内存和栈空间。

    • 决定线程数的几个上限(取最小值)

      • 内存是最常见的瓶颈:每个线程需要独立的栈空间(默认 8MB)。如果物理内存 32GB一个进程创建线程数 = 可用内存 / 8MB(线程栈) + 其他内存开销。假设留 12GB 给其他应用,20GB 给线程栈,最多大约 2500 ~ 3000 个线程。这通常在 3000~5000 条线程时就会因为栈空间累计消耗而触发内存不足
      • 用户进程数上限(ulimit -u):限制了该用户能创建的进程/线程总数。默认通常是系统总内存页数的 1/4 左右(如这台机器上 ulimit -u = 126141),而 kernel.threads-max 是系统级线程总数上限(这台机器上为 252282),通常不成为配置过进程 ulimit 时的主要限制因素,但在默认限制下可能先于内存别的地方达到
      • 系统 PID 上限(kernel.pid_max) :每个线程需要一个 TID。这台机器上 pid_max = 4194304(默认通常是 32768,现代发行版已经大幅提高)。这是线程数量的硬天花板,但在实际场景中因为内存限制,通常申请不到那么多
      • 虚拟内存映射数(vm.max_map_count) :每个线程需要独立的栈映射。这台机器上 max_map_count = 655360,如果线程数超过这个值,mmap 会失败。这和内存栈一样,通常先撑到内存瓶颈
    • 实际能到多少和各语言的表现

      语言通常线程卡住的原因
      C(pthread)几千 ~ 上万栈大小:通常到 8000~15000 时栈内存累积会先耗尽
      Java几千 ~ 一万出头栈大小 + JVM 自身开销:当线程数超过 8000~10000 时,
      光栈空间就可能用掉数十 GB
      Go几十万 ~ 百万Go 的 goroutine 初始栈只有 8KB,远小于线程栈的 8MB
      所以在 Go 里"并发"的单位是 goroutine,它共享同一个线程池,
      和操作系统的线程数上限不是同一个概念
    • 如何提高单进程能创建的线程数

      • 增大 ulimit -u(用户进程数上限):修改 /etc/security/limits.conf
      • 使用更小的栈空间:创建线程时传入小的栈大小(pthread_attr_setstacksize)。例如把默认 8MB 改为 1MB,同样内存下可以创建 8 倍的线程
      • 但是:真正的瓶颈往往不是这些参数,而是线程切换的开销。几千个活跃线程光上下文切换就能把 CPU 吃满,性能可能远不如用线程池 + 少量线程的方式
  • 协助记忆

    • 线程数的天花板通常由内存决定:每个线程 8MB 栈,10GB 可用内存 ≈ 1200 个线程
    • ulimit 和栈大小可以临时提高上限,但大量活跃线程带来的调度开销是另一个维度的限制
    • Gogoroutine 能到百万是假象——底层操作系统线程数其实很少,goroutine 是用户态调度
  • 进阶思考

    • 如何查看当前系统上一共跑了多少线程?

      • ps -eLf | wc -l 查看系统总线程数(-L 显示线程),cat /proc/sys/kernel/threads-max 查看系统上限,cat /proc/loadavg 中第一列的进程/线程总数作为宏观参考。每个进程下的线程数可以通过 ps -eLf | grep <进程名> | wc -lls /proc/<PID>/task/ | wc -l 来统计
    • 某个进程线程数持续增长但不回落,怎么排查?

      • 和内存泄漏的排查思路类似。watch -n 1 'ls /proc/<PID>/task/ | wc -l' 持续观察线程数变化,确认是稳定在某个值还是持续上升。ps -eLf | grep <PID> 查看各线程的状态——如果大量线程卡在某个系统调用上(如 pollfutex),说明不是线程泄漏而是请求堆积导致线程池打满;如果线程状态大多数为 sleeping 但数量持续增长且不回落,则是线程泄漏

🤔 Linux 怎么限制某个目录可用磁盘容量?

  • Linux 没有"直接限制某个目录容量"的内置参数,但有好几种方式可以间接实现,最常用的是项目配额(project quota)和 文件系统隔离,具体取决于你的限制对象是否和系统盘在同一分区、以及是否需要粒度到路径级别。

    • 方案一:项目配额(Project Quota)——最直接的方案

      • ext4xfs 支持项目配额,可以给一个目录打上项目 ID,然后对该 ID 限制容量。这个方案不需要单独分区,不改变现有目录结构

      • ext4 下的操作步骤

        • 挂载时启用项目配额mount -o prjquota /dev/sdX /mnt,或写入 fstab
        • 初始化配额数据库quotacheck -p /mnt
        • 给目标目录设置项目 ID(如 42chattr -p 42 /mnt/target_dir
        • 设置项目 42 的容量上限setquota -P 42 5G 6G 0 0 /mnt(软限制 5G,硬限制 6G
        • 验证repquota -P /mnt
      • xfs 下更简单(不需要 quotacheckxfs_quota -x -c 'project -s target_dir' /mount_point 初始化项目,xfs_quota -x -c 'limit -p bhard=5G target_dir' /mount_point 设置上限

      • 缺点:需要文件系统支持配额功能,且大多默认未启用。如果目录跨分区,配额基于每个分区的配额数据库单独计算

    • 方案二:独立分区或逻辑卷(最传统、最稳定)

      • 如果需要限制的目录是新创建的,直接给它分一个独立的分区或 LVM 逻辑卷,大小设为你期望的上限
      • 例如:lvcreate -L 10G -n data vg_main && mkfs.ext4 /dev/vg_main/data && mount /dev/vg_main/data /data
      • 这种方式的容量限制是硬性的 ——— lsblk 看到多大就只能用多大,不需要额外的配额工具
      • 如果目录已经存在大量数据且无法迁移到新分区,项目配额更合适
    • 方案三loop 文件模拟分区(临时方案,性能较差)

      • 创建一个固定大小的文件,格式化为文件系统,通过 loop 设备挂载到目标目录
      • dd if=/dev/zero of=/disk.img bs=1M count=5120 && mkfs.ext4 /disk.img && mount -o loop /disk.img /target
      • 性能比直接使用物理分区差(多了一层文件系统叠加),适合临时限制或测试场景
    • 方案四(不推荐)inotify + 实时检查

      • 用脚本监控目录写入,超过阈值时触发告警或阻止写入
      • 不精确(写入量到达阈值到检测到之间有延迟),而且"阻止写入"这个动作本身就没有优雅的实现方式 ——— 通常是删掉最旧的文件或通知管理员处理。极其不推荐在生产环境使用
  • 协助记忆

    • 给目录设容量,不是 shell 一行能搞定的。常见的方式有三条路:
      • 项目配额(ext4/xfs 原生支持,不改变目录结构,但默认未启用)
      • 独立分区/LV(最稳定,但要提前规划空间)
      • loop 文件挂载(灵活但性能损耗大,适合测试用)
  • 进阶思考

    • 项目配额和用户配额有什么区别?

      • 用户配额(user quota)限制的是"某个用户能写多少数据"——不管这个用户写到哪个目录,他的总写入量不能超过上限。项目配额(project quota)限制的是"某个目录下所有文件的总大小"——不管是谁写的,只要在这个目录下就计入配额。对于"限制 /var/log 不能超过 10GB“这个需求,项目配额才是正确的工具
    • 生产环境中为什么很少看到用配额?

      • 因为大部分场景用独立分区或逻辑卷就能解决问题,比配额配置简单得多,还不需要额外的配额管理开销。配额主要用在共享主机环境(租户多、需要按用户或按项目分摊磁盘成本)、公司内部的文件共享服务器(每个部门一个配额)、及部分企业级的 /home 集中存储等需要细粒度控制的场景

🤔 df 和 du 显示空间不一致是什么情况?

  • dfdu 计算磁盘使用量的角度不同:du 统计的是文件占用的空间,df 统计的是文件系统占用的空间。正常情况两者应该基本一致,差异明显时通常是有已删除但进程还抓着不放的文件。

    • 最常见的原因:已删除但未释放的文件句柄

      • 当一个文件被 rm 掉但还有进程打开着它,内核不会释放该文件占用的数据块——因为进程可能还在读写它
      • du 遍历文件系统统计所有"文件名还存在"的文件大小,看不到这个已删除的文件,所以 du 的结果偏小
      • df 统计的是文件系统整个 inode 和数据块的使用情况,包括那些已删除但未释放的块,所以 df 的结果偏大
      • 排查:lsof | grep '(deleted)' 查找被删除但仍在占用中的文件。找到后重启对应进程,空间就会释放
    • 挂载点的影响

      • 如果 /data 目录下挂载了一个独立分区,du -sh /data 只统计该分区根目录下的文件大小,不会穿透到其他分区。df -h /data 则统计整个分区的占用情况。此时两者计算的不是同一个范围,直接比较没有意义
    • 文件系统元数据开销

      • df 显示的 “Used” 包含了文件系统本身的开销(超级块、inode 表、日志区域等)。du 只统计文件内容占用的块,不统计这些元数据。所以 dfUsed 通常会比文件总和大一点,但在大分区上这个差异相对于总容量来说很小,通常在几 MB 到几十 MB 之间,不会造成明显差异
    • 写缓存和快照

      • 在写负载高的机器上,dudf 的读取时机不同可能导致瞬时不一致。写入过的数据可能还没完全刷盘,du 先读到了新文件大小但 df 还没更新,或者反过来
  • 协助记忆

    • df 大、du 小,大概率是文件被删了但进程还抓着不放。lsof | grep '(deleted)' 一查一个准
  • 进阶思考

    • 重启进程前怎么确认是不是这个文件占用了空间?

      • lsof | grep '(deleted)' 输出中包含文件大小(size 列),可以确认该文件确实占用了大量空间。如果文件大小列显示 0 或很小,说明该文件本身不大,不是空间占用的元凶。需要结合文件大小列的总和来判断空间占用的主要来源
    • 如果不重启进程能不能释放这个空间?

      • 不能。只要进程的 inode 引用还在,内核就不会释放该文件的数据块。唯一的方式是让进程 close 这个文件描述符——要么重启进程,要么向进程发送信号让它自行清理(如果能通过信号通知到进程)。如果这是一个写日志的进程长期占用句柄,可能在写满磁盘后程序会自动 crash 并清理。不建议为了释放空间直接在未确认写入类型的情况下杀掉关键进程,如果删除的日志很重要,应优先考虑确认文件内容后想办法保存

🤔 简述 Linux 读取和写入文件的流程?

  • Linux 的文件读写不是应用程序直接操作磁盘,而是经过虚拟文件系统(VFS)→ 页缓存(Page Cache)→ 文件系统 → 块设备层 → 磁盘驱动这个分层路径。缓存层的存在使得读写速度远高于直接读写磁盘。

    • 读取文件的流程

      1. 应用程序调用 read() —— 这是 libc 封装的系统调用,进入内核态
      2. VFS(虚拟文件系统)根据文件路径找到对应的 inode,确定文件所在的文件系统类型(ext4 / xfs / NFS 等),调用该文件系统具体的 read 函数
      3. Page Cache 查询:内核检查要读取的数据块是否已经在 Page Cache 中。如果命中(cache hit),直接拷贝到用户空间(不需要访问磁盘),然后返回
      4. 缺页(cache miss) :如果 Page Cache 中没有,内核分配内存页,发起磁盘 IO 请求,将数据从磁盘读到 Page Cache 中。进程此时进入 D 状态(不可中断睡眠)等待 IO 完成
      5. 通过 IO 调度器和文件系统驱动层完成实际的磁盘读取
      6. 数据到达 Page Cache 后,内核将数据从内核空间拷贝到应用程序的用户空间缓冲区
    • 写入文件的流程

      1. 应用程序调用 write() —— 同样经过系统调用进入内核态
      2. VFS 找到 inode 和对应的文件系统,不是立即写磁盘,而是将数据写入 Page Cache(写回策略) ,标记为脏页,然后 write() 立即返回(应用程序以为写完了)
      3. 脏页在后台由 flusher 线程(内核线程,如 pdflush) 在以下几个时机被刷入磁盘:脏页达到 vm.dirty_background_ratio(默认 10%)时后台开始刷盘;脏页达到 vm.dirty_ratio(默认 20%)时新的写操作会阻塞等待刷盘;或者调用 fsync() / sync() 手动刷盘
      4. 对于日志文件系统(ext4/xfs),在数据写入磁盘前,先写入日志(Journal) ,记录"即将执行此写入操作”,然后再写入实际数据块或元数据。崩溃后内核通过重放日志恢复一致性。ext4 默认 ordered 模式下:先写数据块,再写日志(记录元数据变更)
      5. 无日志文件系统中写操作直接到数据块,崩溃后修复较慢(如 ext2 需要 fsck 全盘扫描),而日志文件系统通过预记录操作日志可以在重启后快速回放或撤销未完成的操作
  • 协助记忆(以图书馆借书为比喻)

    • 读文件:你去图书馆借书(read())。管理员先去前台查登记簿(VFS 查找 inode),确认书在几号书架(文件系统定位)。如果这本书最近有人还回来正放在前台推车上还没归位(Page Cache 命中),管理员直接从前台拿给你,不需要去书架翻。如果前台没有(cache miss),管理员去书架取书,放在前台登记后再给你
    • 写文件:你把一篇文章交给图书馆管理员(write())。管理员把它放在前台的"待整理"篮子里(Page Cache 脏页),然后告诉你"好了,你可以走了"(write() 返回)。管理员会在下班前统一把篮子里的文章整理归档到书架上(flusher 线程刷盘)。如果篮子满了(dirty_ratio 触发),管理员会叫你等一下,他先整理一批再收你的
  • 进阶思考

    • 极端情况掉电时,Page Cache 中的数据会丢失吗?

      • 会。如果应用程序调用 write() 返回后立刻掉电,存储在 Page Cache 中但尚未刷盘的脏页数据会丢失。这就是为什么数据库等对数据一致性要求高的应用在关键写入后必须调用 fsync() ——— 它会阻塞等待直到数据真正写入磁盘(包括 journal 落盘),而不是只写入 Page Cachefsync() 的性能开销远大于普通 write(),因为需要等待实际磁盘 IO 完成。数据库的 redo log 就是靠这个机制保证的——先写 redo logfsync),再写数据文件(不一定需要立即 fsync),这样即使掉电也能通过 redo log 恢复
    • mmap() 读文件和 read() 读文件有什么不同?

      • mmap() 将文件直接映射到进程的虚拟地址空间,进程像访问内存一样访问文件内容,不需要经过 read() 系统调用和内核到用户空间的数据拷贝(省了一次拷贝)。首次读取时仍然会触发缺页中断,但后续读取直接从映射的内存访问,效率更高。mmap() 适合随机访问大文件的场景,适合顺序读取大文件的场景

🤔 Linux 中 CPU 是怎么工作的?

  • 从运维视角理解 CPU 的工作方式,核心是理解 CPU 时间如何被切分、进程如何在 CPU 上切换、以及 CPU 时间都花在了哪里。不涉及 CPU 内部的微架构细节(流水线、缓存、乱序执行等),而是操作系统层面如何管理和调度 CPU 资源。

    • CPU 是时间共享的 ——— 不是同时在做多件事,而是切得很快

      • 一个单核 CPU 在任意一个瞬间只能执行一个进程的指令。多任务的感觉来自于内核调度器快速地在进程之间切换(每秒几百到几千次,取决于内核时钟频率和调度策略),每次切换分配一个时间片(通常几毫秒到几十毫秒)。你感觉程序在"同时运行",实际上是它们在极快地轮流使用 CPU
      • 时间片用完后,调度器强制暂停当前进程,保存它的寄存器状态(现场保护),选择下一个进程,恢复它的寄存器状态(现场恢复),这个过程称为上下文切换
      • 进程在等待 IO 或主动让出 CPU(如调用 sleep())时,也会触发调度
    • 上下文切换 ——— CPU 换人的成本

      • 上下文切换意味着保存当前进程的寄存器、程序计数器、栈指针,加载下一个进程的对应数据。这个操作本身消耗 CPU 周期
      • 频繁的上下文切换会导致实际干活的 CPU 时间变少 ——— CPU 在忙着切换而不是执行代码。这在 vmstat 1 中表现为 cscontext switch)列数值很高(十几万甚至几十万),同时 sy(系统态)升高
      • 常见的上下文切换来源:大量线程争抢锁、高并发网络 IO(每个连接一个线程)、定时器密集的应用
    • CPU 时间都花在哪了 ——— us / sy / wa / st / id

      • us(user):执行用户空间应用程序代码的时间。代码逻辑越复杂、计算量越大,us 越高
      • sy(system):执行内核代码的时间。系统调用、文件读写、进程调度、内存管理都消耗 sy。如果 sy 过高(超过 30%),通常意味着系统调用过于频繁或上下文切换过多
      • wa(iowait)CPU 空闲但正在等待磁盘 IO 完成的时间。wa 高 = 磁盘是瓶颈
      • st(steal):虚拟机环境下,被宿主机或其他虚拟机"偷走"的 CPU 时间。st 超过 10% 说明宿主机超分严重
      • id(idle)CPU 完全空闲,啥也没干
    • CPU 亲和性 ——— 绑定核心,减少缓存失效

      • 默认情况下,进程可以被调度到任意 CPU 核心上执行。每次切换核心后,该核心的 L1/L2 缓存是冷的,需要重新加载数据(缓存未命中)。对于性能敏感的应用,可以使用 CPU 亲和性(CPU affinity) 将进程锁定到指定核心,提高缓存命中率
      • taskset 命令绑定:taskset -c 0-3 ./myapp(绑定到 0~3 号核心)
      • 核心隔离:在 GRUB 内核参数中加 isolcpus=2,323 号核心从默认的进程调度中排除,专门留给指定的进程使用
  • 协助记忆

    • 一个核在任意瞬间只能跑一个进程,但我们觉得"多任务同时进行"是因为切换足够快
    • us/sy/wa/st/id 就是 CPU 的五个状态——干用户的活、干内核的活、等磁盘时发呆、被虚拟机偷走、真正没事干
    • sy 太高说明系统调用太多;wa 太高说明磁盘慢了;st 太高说明宿主机超分了
  • 进阶思考

    • 多核 CPU 上,当一个核心的 L1/L2 缓存已经缓存了一份数据,另一个核心修改了这份数据时,第一个核心的缓存会怎么样?

      • CPU 通过缓存一致性协议(MESI 协议,包含 ModifiedExclusiveSharedInvalid 四个状态)来保证各核心的缓存数据同步。当核心 B 写入了一个地址,核心 A 中对应的缓存行会被标记为失效(Invalid)。核心 A 下次访问该地址时,L1/L2 缓存未命中,需要重新从 L3 缓存或主存中读取最新数据。这也是为什么 CPU 亲和性(绑定核心)能提升性能的原因 ——— 避免同一个进程在不同的核心之间来回切换导致 L1/L2 缓存频繁失效重载。但在 Linux 默认调度策略中,进程切换时也更倾向于将进程留在原来的核心上运行(称为软亲和性),以尽量保持缓存热度
    • PVE 上的 VMLXC 容器,和 VMware 的虚拟机,它们分配 CPU 的机制和 isolcpus 有什么区别?

      • 三种方式从底层到上层各不相同
        • isolcpus(内核参数级) :在 GRUB 中配置,作用是通知 Linux 内核的 CFS 调度器"这些核心不要分配给常规进程"。这是一种底层的屏蔽机制,任何不通过 tasksetcpuset 显式绑定的进程都不会被分配到这些核心上。在 PVE 环境中,isolcpus 通常配合 VMCPU pinning 使用——核心从调度器中隔离后,再通过 cpusetVMQEMU 进程 pin 上去,实现 VM 独占物理核心
        • PVEKVM 虚拟机) :默认使用 cgroup cpu.sharesCPU 的时间片权重分配,类似于比例分配(不给 VM 分配特定核心,而是按比例分时间)。如果需要 VM 独占核心,需要手配 isolcpus + 在 VM 配置中设置 CPU affinity,通过 tasksetcpusetQEMU 进程固定到隔离核心上。LXC 容器同理,通过 cgroup cpuset.cpus 限制容器可用的核心范围
        • VMware ESXiESXi 有独立的 CPU 调度器管理所有虚拟机的 vCPUpCPU 的映射。CPU 分配策略通过三个参数控制: Reservation(保证 vCPU 能获得的最低 MHz)、LimitvCPU 能使用的最高 MHz)、Shares(多 VM 争抢资源时的相对优先级)。ESXi 没有 isolcpus ——— 因为宿主机本身不跑其他进程需要隔离。它也支持 CPU pinningvCPU 固定到特定的 pCPU,但在大多数场景下不需要手动设置。ESXi 的调度器会根据 NUMA 拓扑和 CPU 负载自动优化 vCPU 的放置

🤔 如果你提了一个 Bug,开发不认为是 Bug 该怎么沟通处理?

  • 这是一个典型的"运维发现问题、开发不认"的跨团队沟通问题。核心不在于争论"是不是Bug",而是把问题还原到可复现的现场,用数据和事实对齐双方认知。

    • 第一步:把"我认为是 Bug"变成"这里有证据"

      • 准备好完整的现场信息:日志报错的时间点、完整的报错堆栈、系统当时的资源状态(CPU/内存/磁盘 IO)、业务影响范围
      • 如果能稳定复现,录一段操作到报错的完整过程,或者写一个最小化的复现脚本。开发如果自己能在测试环境跑出同样的问题,他不会再说是"环境问题"
      • 如果不能稳定复现,提供复现的频率和条件——“每 5 分钟一次"和"三天出现了两次"是不同量级的问题
    • 第二步:对齐预期——搞清楚”Bug 的标准"是什么

      • 开发说"不是 Bug“时,通常不是否认现象存在,而是认为这个行为是预期的
      • 和开发对齐:你看到的异常现象具体是什么?系统应该表现成什么样子才是"正常的”?对这个现象是要修复还是静默忽略?确定好现状 和期望,就有了讨论的基础
    • 第三步:上升到"影响业务"而不是"Bug"

      • 如果开发坚持这不是 Bug(比如"这个错误码虽然报了但不影响核心逻辑"),把问题从"这是不是一个 Bug"切换为"这个现象对业务有什么实际影响"

        • 这个报错是否会导致告警系统频繁触发,淹没关键告警?
        • 这个报错是否会导致排查其他故障时产生干扰,延长故障定位时间?
        • 用户侧是否已经感知到了异常(超时、报错、响应变慢)?
      • 即使不是代码逻辑错误,只要对运维效率或用户体验产生了负面影响,就值得修

    • 第四步:升级路径

      • 如果经过上述步骤仍然无法推进,通过正式渠道升级:在故障工单或 Bug 跟踪系统中记录完整的现场信息和双方讨论过程,提交给双方 leader 决策。在升级之前确保你的证据足够完整——一个"日志里报了看不懂的错"和"P99 延迟从 50ms 涨到 5s,回滚后恢复"的分量完全不同
  • 协助记忆

    • 不和开发争论"是不是 Bug",而是对齐"看到了什么"和"应该是什么"
    • 准备好证据:日志、堆栈、复现步骤、业务影响
    • 把话题从"这是不是 Bug“引导到"这个现象对业务有什么影响”
    • 实在无法达成一致时,带着完整证据升级给双方 leader
  • 进阶思考

    • 运维报的"Bug“最后查出来很多是配置问题或环境差异导致的,开发为什么会默认先怀疑运维操作不当?

      • 这是长期形成的信任惯性。开发见过的”Bug“中确实有相当比例最终发现是配置错误、版本不匹配、缓存未清理、或操作手册步骤遗漏。最佳的处理方式不是在争论中证明"这不是我的问题”,而是在第一次沟通时就准备好能排除环境因素的证据——比如在测试环境复现、或者在另一台相同配置的机器上也有同样的表现。如果能独立于你的环境复现问题,就跳过了"是环境问题还是代码问题"的讨论
    • 开发口头答应修了但几个版本都没动,怎么推动?

      • Bug 跟踪系统中创建正式工单(Jira / 禅道),记录影响范围和严重等级。在下一次版本规划或排期时,以影响范围作为理由提出修复需求。如果开发排期冲突,尝试约定一个临时解决方案(如加监控规避、配置绕过),并在后续版本中保持跟进

目录