运维常见题-日常维护(四)
🤔 Linux 系统内存持续飙高,如何排查?
内存飙高的排查思路是:先看整体内存分布(到底谁在消耗内存)→ 再定位具体进程或缓存类型(是应用泄漏还是内核缓存膨胀)→ 最后针对性处理。
第一步:看整体内存分布——确认内存去哪了
free -h看懂三行数据:总内存、已用、可用。其中available列是系统估算的真实可用内存(考虑了可回收的缓存),比单纯看used更准确cat /proc/meminfo更详细,重点看几个字段:MemTotal/MemFree/MemAvailable:总量、空闲、真实可用Buffers/Cached:缓冲区 + 页面缓存。这部分内存可以在内存紧张时被回收,不是真正的"已用"Active(anon)/Inactive(anon):活跃和非活跃的匿名页(进程堆栈)。如果Inactive(anon)持续增高且Cached很低,可能是内存泄漏或大内存业务在累积脏数据Shmem:共享内存,包括tmpfs、Docker overlay等。如果这个值很高,检查/dev/shm或Docker的挂载
第二步:区分是应用内存还是缓存——避免误判
top按M排序查看进程RES。但注意RES包含共享内存(SHR),多个进程的RES相加会超过物理内存总量——因为共享库在内存中只有一份,但每个进程的RES都计入了它。ps aux --sort=-%mem | head 10列出内存占用最高的进程- 如果
total RES - SHR总和远小于物理内存"已用",剩余的大头在Cached/Slab/Shmem里,不是应用泄漏 - 一个判断技巧:
free -h的available如果还很高(超过物理内存的 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 slab。Slab是内核缓存(dentry、inode等)。如果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/shm和mount -t tmpfs查看tmpfs挂载点使用情况。Docker默认的/var/lib/docker如果使用了overlay2也可能产生大量的 shmem 使用
协助记忆
- 内存排查四步:一看整体(
free -h)、二查来源(ps/pmap)、三辨类型(缓存还是应用)、四追泄漏(top 持续观察) available才是真实可用内存,不要只看used。Cached和Buffers占了内存不一定是坏事——它们是可回收的
- 内存排查四步:一看整体(
进阶思考
free -h显示的used很高(90%+),但available也还有30%,这算内存不足吗?- 不算。
Linux的内存管理原则是"空闲内存就是浪费内存",它会尽量用空闲内存做缓存来提高IO性能。used高但available充足说明大部分"已用"内存是可回收的缓存,不是进程真正占住的。需要关注的指标是:available持续低于物理内存的10~15%且si/so(vmstat 1输出)持续大于 0,这才说明真正内存紧张
- 不算。
ps aux里看到的RSS和top里的RES不完全一致,以哪个为准?- 两者本质上是同一个数据(
RSS=Resident Set Size),数值差异来源于统计时机不同。top实时刷新,ps aux是执行瞬间的快照。在排查时以top持续观察为准,因为能看趋势
- 两者本质上是同一个数据(
🤔 Linux 系统硬盘读写慢,如何排查?
硬盘读写慢的排查思路是:先用工具确认慢在硬件层还是软件层(是磁盘本身响应慢,还是
IO请求排队太多),再定位到具体是哪个进程在产生IO,最后针对性处理。先确认磁盘的繁忙程度和响应时间
iostat -xz 1持续观察,关键看几个指标:r/s + w/s(每秒读写次数):是否达到了磁盘的标称IOPS上限。机械盘通常100~200,SATA SSD约1万~10万,NVMe约50万~100万rMB/s+wMB/s(每秒读写带宽):是否达到了磁盘或存储链路(SATA 6Gbps、PCIe 4.0 x4)的带宽上限- %util:设备忙绿比例。
NVMe在多队列并发下即使100%也可能正常,要结合r/s判断;机械盘如果持续100%说明确实饱和了 await(平均IO响应时间):这是最关键的一个指标。如果机械盘await持续超过15ms、SSD超过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写入,增加额外的写IOcat /sys/block/sda/queue/scheduler查看IO调度器。机械盘建议mq-deadline,NVMe建议noneblockdev --getra /dev/sda查看预读大小。默认256(128KB),顺序读为主的工作负载可以适当增大到4096cat /proc/meminfo | grep Dirty查看脏页数量。如果脏页在持续增长,说明写入速度超过了刷盘速度,可能需要调整vm.dirty_background_ratio和vm.dirty_ratio
如果一切正常但依然慢——检查软件层
- 确认磁盘是否在做
RAID重构或一致性检查。cat /sys/block/sdX/device/state正常应为running - 确认是否有
swap导致频繁换入换出(vmstat 1的si/so持续大于0) - 确认硬盘健康状态没有恶化:
smartctl -a中Reallocated_Sector_Ct持续增长说明硬盘有坏道,每次读写坏道区域都会触发重映射导致显著的延迟(相比正常IO,坏道重映射的延迟可能高出数百倍)
- 确认磁盘是否在做
协助记忆
- 排查先看
iostat->await判断是不是在排队 ->avgqu-sz判断是排队长了还是盘本身慢了 - 再上
iotop看是哪个进程在写 - 再看调度器和挂载参数
- 排查先看
进阶思考
iostat的%util在NVMe盘上达到100%,但await只有0.5ms,这正常吗?- 正常。
NVMe盘支持多队列并发,每个队列可以独立处理IO请求。%util统计的是设备是否在某时刻处于繁忙状态,在NVMe的多队列模式下,设备几乎总处于繁忙状态,即使IO响应极快。对NVMe盘更应该看avgqu-sz和r/s+w/s是否达到了盘的标称 IOPS 上限,而不是%util
- 正常。
iostat看到磁盘await正常,但应用层感知的读写延迟很高,可能是什么原因?- 问题不在磁盘本身,在软件栈。可能是文件系统的锁竞争(如多个线程并发写入同一个文件)、数据库
buffer pool太小导致频繁刷脏页、或者虚拟机/容器的存储栈有额外的延迟叠加(如qemu的IO路径)。排查思路:在应用内部打点(trace)测量文件读写操作的耗时,如果应用测到的耗时远高于iostat的await,说明延迟出在从应用系统调用到内核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+X或F10启动 - 也可以加
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错了进不了系统,简单三步:Emergency Mode登录 →mount -o remount,rw /- 编辑
fstab:修正UUID、注释不需要的行 mount -a验证 →reboot
预防:
mount -a重启前测一把,不关键的盘加nofail
进阶思考
修复完
fstab后mount -a报错wrong fs type,但UUID确认没错,为什么?- 文件系统类型(
fstab第三列)写错了。例如磁盘是xfs但写成了ext4,或者内核没有加载对应的文件系统驱动。修正类型后重试mount -a
- 文件系统类型(
根分区本身在
fstab中配置了错误的UUID,连Emergency Mode都进不去,怎么办?- 用安装
U盘启动进入Rescue模式,手动挂载根分区后编辑fstab修正UUID。如果根分区完全无法挂载,说明可能是文件系统损坏,需要先fsck修复
- 用安装
扩展信息
网络挂载(
NFS/CIFS)的fstab配置注意事项- 网络挂载和本地挂载最大的区别:系统启动时网络还没就绪,如果
fstab中的NFS挂载项没有加_netdev和nofail,systemd会在启动时尝试挂载NFS,此时网络未就绪导致挂载失败,系统卡在等待状态90秒后跳入Emergency Mode - 正确的
NFS挂载配置:192.168.1.100:/data /mnt/data nfs _netdev,nofail,soft,timeo=100 0 0 _netdev:告诉systemd这是一个网络设备,需要网络就绪后再尝试挂载nofail:挂载失败时不阻止系统继续启动softvshard:hard(默认)下NFS服务端宕机时访问该挂载点的进程会卡在D状态等待,soft下超时后返回错误给应用(但可能丢失数据)。生产环境通常用hard+intr或hard+ 较短的timeo和retrans参数- 如果是
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:包含了rw、suid、dev、exec、auto、nouser、async的组合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 0x-systemd.automount:systemd特性,将挂载点设为按需挂载——只有真正访问该目录时才触发挂载操作,而不是在系统启动时挂载。适用于网络挂载或低频使用的目录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 上挂载远端存储没有一个通用最优解,选哪条路取决于你需要什么样的协议兼容性、安全加密层级和并发规模——是在同一个可信内网里跑高吞吐读写,还是跨公网传输一份加密数据,通常决定了最终差异很大的技术选型。
NFS(Network File System)— 最成熟的传统方案- 特点:标准
POSIX文件系统语义,支持文件锁、目录结构完整。多台Linux客户端同时挂载同一份存储,行为和使用本地文件系统一致 NFS v3(传统版):依赖rpcbind,端口不固定,无加密;NFS v4(现代版):不再强制依赖rpcbind,可以配Kerberos实现传输加密- 缺点:安全性偏弱(
v3裸奔、v4加密需要额外维护Kerberos);防火墙规则麻烦(v3端口不固定);v4配置较复杂 - 适用场景:内网环境下的虚拟机镜像存储、高可用共享目录、数据库备份归档等对
POSIX语义要求高的场景 - 生产安全加密方式:内网用
NFS v4,如果需要加密传输则配Kerberos。公网环境不建议直接暴露NFS,建议走VPN或SSH隧道再做NFS挂载
- 特点:标准
sshfs(基于SSH的文件系统挂载)- 特点:基于
FUSE,通过SSH协议挂载远程目录,不需要服务端额外装软件(SSH本身就开着)。传输全程加密,天然满足PCI-DSS、HIPAA等合规要求 - 缺点:性能远不如
NFS(单线程、高延迟)。并发大了容易卡死,不适合高负载场景。稳定性受网络波动影响较大 - 适用场景:临时挂载传文件、合规要求严格且不想搭
Kerberos的场景、目标机器只有SSH可通没有其他协议可用
- 特点:基于
云存储挂载(
s3fs/SDK直读 /NFS网关)s3fs:通过FUSE将S3/OSS挂载为本地目录。适合只读场景(模型分发、静态资源加载),多写场景下容易出现你遇到的"目录被当成文件"的问题。不是配置问题 ,是S3没有目录概念这个机制性限制SDK直读:应用代码里直接用aws s3 cp、ossutil或各语言的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+NFS或SSH隧道 +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同样连接到当前终端,所以平时看到错误信息也打在屏幕上stderr和stdout分开的意义是:可以把正常结果和错误日志从同一个标准流里分离出来单独存储或丢弃——正常数据走一个管道用于进一步处理,错误信息走另一个用于排查常用重定向方式:
command 2> error.log:将标准错误单独写入文件command > output.log 2>&1:将stdout和stderr合并到同一个文件(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>&1和command 2>&1 > file有什么区别?- 结果完全不同。
Shell会按照从左到右依次处理重定向。command > file 2>&1- 先将
stdout重定向到file - 再将
stderr重定向到stdout当前所指向的位置(即file) - 最终:
stdout写入file,stderr也写入file
- 先将
command 2>&1 > file- 先将
stderr重定向到stdout当前所指向的位置(终端) - 再将
stdout重定向到file - 最终:
stdout写入file,stderr仍输出到终端。
- 先将
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.txtcommand >> filestdout追加到文件echo "done" >> log.txtcommand 2> filestderr单独写入文件find / 2> error.logcommand > file 2>&1stdout和stderr合并写入文件make build > build.log 2>&1command &> file同上, bash简写make build &> build.logcommand > /dev/null 2>&1丢弃全部输出 只关心退出码时使用 command < file从文件读取 stdingrep error < /var/log/syslogcommand << EOF ... EOFHere Document,多行字符串传入stdin脚本中传入多行配置 command1 | command2管道, command1的stdout传给command2的stdinps aux|grep nginxcommand 2>&1 | command2将 stdout和stderr一起通过管道传给下一个命令需要同时处理正常和错误输出时 常见陷阱
- 顺序问题:
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一样
为什么需要这个
- 默认情况下,
stdout和stderr都输出到屏幕,看起来没区别。但一旦把stdout重定向到了文件(command > file),stderr仍然输出到屏幕,两种信息就分开了 2>&1把stderr也拉进文件里,保证日志文件里既有正常输出也有错误信息,排查时不用看两个地方
- 默认情况下,
两个常见写法的差异
command > file 2>&1:先让stdout指向file,再把stderr指向stdout当前的位置(也就是file)。结果:stdout和stderr都进filecommand 2>&1 > file:先让stderr指向stdout(此时stdout指向屏幕),再把stdout指向file。结果:stderr打到屏幕,只有stdout进file
协助记忆
2>&1就像开会时说“把CC(抄送)的邮件也发到和收件人同一个地方”(2>&1的&不是字符串拼接,而是"指向"的意思,不是把stderr的内容拼接到stdout后面,而是让stderr的输出路径和stdout一致)- 顺序很重要——先
> file再2>&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的安全擦除应该用blkdiscard或nvme 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>&1把stdout和stderr都丢进黑洞,只关心命令的退出码 - 只保留错误输出:
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有什么区别?- 效果完全一样,都是把文件内容清空。
> file是Shell的重定向语法,在打开文件时使用O_TRUNC标志直接截断文件内容并保留inode,不需要调用cat和/dev/null。cat /dev/null > file是先打开/dev/null读到一个EOF(空),再写入文件覆盖原有内容。后者多了一次cat进程的开销,没有任何实际优势。所以清空文件用> file就够了
- 效果完全一样,都是把文件内容清空。
- 为什么不能用
rm清空日志文件而要用> file或cat /dev/null > file?rm删除文件再重建会改变inode,正在写该日志的进程仍然持有旧inode的文件句柄,会继续往那个已经"消失"的文件里写数据——磁盘空间不会释放。> file只清空内容不改变inode,进程句柄仍然有效,继续写入的内容会写到重新开始的文件中。这是线上清日志的标准做法
🤔 ss 和 netstat 同样查看端口连接,ss 相比 netstat 有什么优势?
ss是netstat的现代替代方案,核心优势在于性能更快、信息更全、过滤更强。在连接数多的机器上,两者的差距会变得非常明显。性能差距(最核心的优势)
netstat遍历/proc/net/tcp等文件来收集信息,内核每次都要为这些虚拟文件生成文本输出,再由netstat一行行解析。在高并发机器上(几万条连接),每次执行netstat都会消耗不少CPUss通过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的过滤主要靠grep。ss原生支持多种过滤方式:- 按
TCP状态过滤:ss -tan state time-wait只显示TIME_WAIT状态的连接 - 按端口范围过滤:
ss -tan sport = :80或ss -tan dport > :1024 - 按地址+端口组合过滤:
ss -tan src 10.0.0.1:http dst 10.0.0.2:https
- 按
这些过滤由内核直接实现,不需要在用户态用
grep扫全量输出
协助记忆
ss更快:netstat读文件,ss走netlink直通内核。万条连接下netstat跑好几秒,ss几乎不耗时ss更全:能看到TCP内部参数(拥塞窗口、RTT)ss过滤更强:内置按状态、端口、地址过滤,不需要grep扫全量输出
进阶思考
ss -tuln中的四个字母分别代表什么?t = TCP、u = UDP、l = 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- 如果需要在文件变更时立即同步到另一台服务器,用
lsyncd(Live Syncing Daemon)。它通过inotify监控目录变化,触发rsync执行增量同步 - 相比
crontab定时rsync,lsyncd的优点是延迟低(秒级触发)、不需要设定固定同步间隔 - 配置示例:
lsyncd -rsync /local/path host:/remote/path
- 如果需要在文件变更时立即同步到另一台服务器,用
双向同步:
unisonrsync只解决单向同步。如果两台服务器都改同一份数据,需要双向同步时,用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参数,或考虑使用lftp的mirror命令
🤔 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(传统工具,新系统可能需额外安装)
优先用
ss,ss不在再用lsof,fuser适合脚本中快速拿到PID
扩展信息
netstat从Linux内核5.8版本开始陆续进入过时状态,net-tools包在RHEL 8+和Ubuntu 18.04+中不再默认安装ss是iproute2包中的工具,作为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时,
光栈空间就可能用掉数十GBGo几十万 ~ 百万 Go 的 goroutine 初始栈只有 8KB,远小于线程栈的8MB。
所以在Go里"并发"的单位是goroutine,它共享同一个线程池,
和操作系统的线程数上限不是同一个概念如何提高单进程能创建的线程数
- 增大
ulimit -u(用户进程数上限):修改/etc/security/limits.conf - 使用更小的栈空间:创建线程时传入小的栈大小(
pthread_attr_setstacksize)。例如把默认8MB改为1MB,同样内存下可以创建8倍的线程 - 但是:真正的瓶颈往往不是这些参数,而是线程切换的开销。几千个活跃线程光上下文切换就能把
CPU吃满,性能可能远不如用线程池 + 少量线程的方式
- 增大
协助记忆
- 线程数的天花板通常由内存决定:每个线程
8MB栈,10GB可用内存 ≈1200个线程 - 改
ulimit和栈大小可以临时提高上限,但大量活跃线程带来的调度开销是另一个维度的限制 Go的goroutine能到百万是假象——底层操作系统线程数其实很少,goroutine是用户态调度
- 线程数的天花板通常由内存决定:每个线程
进阶思考
如何查看当前系统上一共跑了多少线程?
ps -eLf | wc -l查看系统总线程数(-L显示线程),cat /proc/sys/kernel/threads-max查看系统上限,cat /proc/loadavg中第一列的进程/线程总数作为宏观参考。每个进程下的线程数可以通过ps -eLf | grep <进程名> | wc -l或ls /proc/<PID>/task/ | wc -l来统计
某个进程线程数持续增长但不回落,怎么排查?
- 和内存泄漏的排查思路类似。
watch -n 1 'ls /proc/<PID>/task/ | wc -l'持续观察线程数变化,确认是稳定在某个值还是持续上升。ps -eLf | grep <PID>查看各线程的状态——如果大量线程卡在某个系统调用上(如poll、futex),说明不是线程泄漏而是请求堆积导致线程池打满;如果线程状态大多数为sleeping但数量持续增长且不回落,则是线程泄漏
- 和内存泄漏的排查思路类似。
🤔 Linux 怎么限制某个目录可用磁盘容量?
Linux没有"直接限制某个目录容量"的内置参数,但有好几种方式可以间接实现,最常用的是项目配额(project quota)和 文件系统隔离,具体取决于你的限制对象是否和系统盘在同一分区、以及是否需要粒度到路径级别。方案一:项目配额(
Project Quota)——最直接的方案ext4和xfs支持项目配额,可以给一个目录打上项目ID,然后对该ID限制容量。这个方案不需要单独分区,不改变现有目录结构ext4下的操作步骤:- 挂载时启用项目配额:
mount -o prjquota /dev/sdX /mnt,或写入fstab - 初始化配额数据库:
quotacheck -p /mnt - 给目标目录设置项目
ID(如42):chattr -p 42 /mnt/target_dir - 设置项目
42的容量上限:setquota -P 42 5G 6G 0 0 /mnt(软限制5G,硬限制6G) - 验证:
repquota -P /mnt
- 挂载时启用项目配额:
xfs下更简单(不需要quotacheck):xfs_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 显示空间不一致是什么情况?
df和du计算磁盘使用量的角度不同:du统计的是文件占用的空间,df统计的是文件系统占用的空间。正常情况两者应该基本一致,差异明显时通常是有已删除但进程还抓着不放的文件。最常见的原因:已删除但未释放的文件句柄
- 当一个文件被
rm掉但还有进程打开着它,内核不会释放该文件占用的数据块——因为进程可能还在读写它 du遍历文件系统统计所有"文件名还存在"的文件大小,看不到这个已删除的文件,所以du的结果偏小df统计的是文件系统整个inode和数据块的使用情况,包括那些已删除但未释放的块,所以df的结果偏大- 排查:
lsof | grep '(deleted)'查找被删除但仍在占用中的文件。找到后重启对应进程,空间就会释放
- 当一个文件被
挂载点的影响
- 如果
/data目录下挂载了一个独立分区,du -sh /data只统计该分区根目录下的文件大小,不会穿透到其他分区。df -h /data则统计整个分区的占用情况。此时两者计算的不是同一个范围,直接比较没有意义
- 如果
文件系统元数据开销
df显示的 “Used” 包含了文件系统本身的开销(超级块、inode表、日志区域等)。du只统计文件内容占用的块,不统计这些元数据。所以df的Used通常会比文件总和大一点,但在大分区上这个差异相对于总容量来说很小,通常在几MB到几十MB之间,不会造成明显差异
写缓存和快照
- 在写负载高的机器上,
du和df的读取时机不同可能导致瞬时不一致。写入过的数据可能还没完全刷盘,du先读到了新文件大小但df还没更新,或者反过来
- 在写负载高的机器上,
协助记忆
df大、du小,大概率是文件被删了但进程还抓着不放。lsof | grep '(deleted)'一查一个准
进阶思考
重启进程前怎么确认是不是这个文件占用了空间?
lsof | grep '(deleted)'输出中包含文件大小(size列),可以确认该文件确实占用了大量空间。如果文件大小列显示0或很小,说明该文件本身不大,不是空间占用的元凶。需要结合文件大小列的总和来判断空间占用的主要来源
如果不重启进程能不能释放这个空间?
- 不能。只要进程的
inode引用还在,内核就不会释放该文件的数据块。唯一的方式是让进程close这个文件描述符——要么重启进程,要么向进程发送信号让它自行清理(如果能通过信号通知到进程)。如果这是一个写日志的进程长期占用句柄,可能在写满磁盘后程序会自动crash并清理。不建议为了释放空间直接在未确认写入类型的情况下杀掉关键进程,如果删除的日志很重要,应优先考虑确认文件内容后想办法保存
- 不能。只要进程的
🤔 简述 Linux 读取和写入文件的流程?
Linux的文件读写不是应用程序直接操作磁盘,而是经过虚拟文件系统(VFS)→ 页缓存(Page Cache)→ 文件系统 → 块设备层 → 磁盘驱动这个分层路径。缓存层的存在使得读写速度远高于直接读写磁盘。读取文件的流程
- 应用程序调用
read()—— 这是libc封装的系统调用,进入内核态 VFS(虚拟文件系统)根据文件路径找到对应的inode,确定文件所在的文件系统类型(ext4/xfs/NFS等),调用该文件系统具体的read函数Page Cache查询:内核检查要读取的数据块是否已经在Page Cache中。如果命中(cache hit),直接拷贝到用户空间(不需要访问磁盘),然后返回- 缺页(
cache miss) :如果Page Cache中没有,内核分配内存页,发起磁盘IO请求,将数据从磁盘读到Page Cache中。进程此时进入D状态(不可中断睡眠)等待IO完成 - 通过
IO调度器和文件系统驱动层完成实际的磁盘读取 - 数据到达
Page Cache后,内核将数据从内核空间拷贝到应用程序的用户空间缓冲区
- 应用程序调用
写入文件的流程
- 应用程序调用
write()—— 同样经过系统调用进入内核态 VFS找到inode和对应的文件系统,不是立即写磁盘,而是将数据写入Page Cache(写回策略) ,标记为脏页,然后write()立即返回(应用程序以为写完了)- 脏页在后台由
flusher线程(内核线程,如pdflush) 在以下几个时机被刷入磁盘:脏页达到vm.dirty_background_ratio(默认10%)时后台开始刷盘;脏页达到vm.dirty_ratio(默认20%)时新的写操作会阻塞等待刷盘;或者调用fsync()/sync()手动刷盘 - 对于日志文件系统(
ext4/xfs),在数据写入磁盘前,先写入日志(Journal) ,记录"即将执行此写入操作”,然后再写入实际数据块或元数据。崩溃后内核通过重放日志恢复一致性。ext4默认ordered模式下:先写数据块,再写日志(记录元数据变更) - 无日志文件系统中写操作直接到数据块,崩溃后修复较慢(如
ext2需要fsck全盘扫描),而日志文件系统通过预记录操作日志可以在重启后快速回放或撤销未完成的操作
- 应用程序调用
协助记忆(以图书馆借书为比喻)
- 读文件:你去图书馆借书(
read())。管理员先去前台查登记簿(VFS查找inode),确认书在几号书架(文件系统定位)。如果这本书最近有人还回来正放在前台推车上还没归位(Page Cache命中),管理员直接从前台拿给你,不需要去书架翻。如果前台没有(cache miss),管理员去书架取书,放在前台登记后再给你 - 写文件:你把一篇文章交给图书馆管理员(
write())。管理员把它放在前台的"待整理"篮子里(Page Cache脏页),然后告诉你"好了,你可以走了"(write()返回)。管理员会在下班前统一把篮子里的文章整理归档到书架上(flusher线程刷盘)。如果篮子满了(dirty_ratio触发),管理员会叫你等一下,他先整理一批再收你的
- 读文件:你去图书馆借书(
进阶思考
极端情况掉电时,
Page Cache中的数据会丢失吗?- 会。如果应用程序调用
write()返回后立刻掉电,存储在Page Cache中但尚未刷盘的脏页数据会丢失。这就是为什么数据库等对数据一致性要求高的应用在关键写入后必须调用fsync()——— 它会阻塞等待直到数据真正写入磁盘(包括journal落盘),而不是只写入Page Cache。fsync()的性能开销远大于普通write(),因为需要等待实际磁盘IO完成。数据库的redo log就是靠这个机制保证的——先写redo log(fsync),再写数据文件(不一定需要立即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中表现为cs(context switch)列数值很高(十几万甚至几十万),同时sy(系统态)升高 - 常见的上下文切换来源:大量线程争抢锁、高并发网络
IO(每个连接一个线程)、定时器密集的应用
- 上下文切换意味着保存当前进程的寄存器、程序计数器、栈指针,加载下一个进程的对应数据。这个操作本身消耗
CPU时间都花在哪了 ———us/sy/wa/st/idus(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,3将2、3号核心从默认的进程调度中排除,专门留给指定的进程使用
- 默认情况下,进程可以被调度到任意
协助记忆
- 一个核在任意瞬间只能跑一个进程,但我们觉得"多任务同时进行"是因为切换足够快
us/sy/wa/st/id就是CPU的五个状态——干用户的活、干内核的活、等磁盘时发呆、被虚拟机偷走、真正没事干sy太高说明系统调用太多;wa太高说明磁盘慢了;st太高说明宿主机超分了
进阶思考
多核
CPU上,当一个核心的L1/L2缓存已经缓存了一份数据,另一个核心修改了这份数据时,第一个核心的缓存会怎么样?CPU通过缓存一致性协议(MESI协议,包含Modified、Exclusive、Shared、Invalid四个状态)来保证各核心的缓存数据同步。当核心B写入了一个地址,核心A中对应的缓存行会被标记为失效(Invalid)。核心A下次访问该地址时,L1/L2缓存未命中,需要重新从L3缓存或主存中读取最新数据。这也是为什么CPU亲和性(绑定核心)能提升性能的原因 ——— 避免同一个进程在不同的核心之间来回切换导致L1/L2缓存频繁失效重载。但在Linux默认调度策略中,进程切换时也更倾向于将进程留在原来的核心上运行(称为软亲和性),以尽量保持缓存热度
PVE上的VM、LXC容器,和VMware的虚拟机,它们分配CPU的机制和isolcpus有什么区别?- 三种方式从底层到上层各不相同:
isolcpus(内核参数级) :在GRUB中配置,作用是通知Linux内核的CFS调度器"这些核心不要分配给常规进程"。这是一种底层的屏蔽机制,任何不通过taskset或cpuset显式绑定的进程都不会被分配到这些核心上。在PVE环境中,isolcpus通常配合VM的CPU pinning使用——核心从调度器中隔离后,再通过cpuset把VM的QEMU进程pin上去,实现VM独占物理核心PVE(KVM虚拟机) :默认使用cgroup cpu.shares做CPU的时间片权重分配,类似于比例分配(不给VM分配特定核心,而是按比例分时间)。如果需要VM独占核心,需要手配isolcpus+ 在VM配置中设置CPU affinity,通过taskset或cpuset将QEMU进程固定到隔离核心上。LXC容器同理,通过cgroup cpuset.cpus限制容器可用的核心范围VMware ESXi:ESXi有独立的CPU调度器管理所有虚拟机的vCPU到pCPU的映射。CPU分配策略通过三个参数控制:Reservation(保证vCPU能获得的最低MHz)、Limit(vCPU能使用的最高MHz)、Shares(多VM争抢资源时的相对优先级)。ESXi没有isolcpus——— 因为宿主机本身不跑其他进程需要隔离。它也支持CPU pinning将vCPU固定到特定的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/ 禅道),记录影响范围和严重等级。在下一次版本规划或排期时,以影响范围作为理由提出修复需求。如果开发排期冲突,尝试约定一个临时解决方案(如加监控规避、配置绕过),并在后续版本中保持跟进
- 在