我要提问
ARTICLE DETAIL

资讯详情

前沿编程新知与开发实战干货的深度解读。

Linux文件系统与容器存储:从overlay2到磁盘告警排查全解析

Linux文件系统与容器存储:从overlay2到磁盘告警排查全解析 干这行久了你会发现一个挺有意思的现象很多同学单独聊Linux文件系统头头是道单独聊容器也能说个大概。但如果把两者放一起比如“容器里df -h看到的是什么”“为什么镜像删了磁盘空间没释放”“overlay2和ext4到底是什么关系”一下子就卡壳了。这篇我就把这两块知识串起来讲一遍结合我平时排查问题的实际经验把文件系统、容器存储、常见故障这条线彻底捋清楚。不管你是刚接触Linux的新手还是已经用容器跑业务的运维/开发这篇文章都能帮你把底层的概念补扎实。搞懂这些以后碰到磁盘告警、容器启动失败、镜像构建体积爆炸这类问题就不用再瞎猜了。1. 先把Linux文件系统的底账盘清楚1.1 从VFS说起为什么Linux什么都能挂Linux文件系统设计里最核心的一个抽象层叫VFSVirtual File System虚拟文件系统。你可以把它理解成一个万能插座ext4、xfs、btrfs、fat32、ntfs、tmpfs甚至网络存储nfs、对象存储s3fs只要实现VFS定义的那套接口就能以文件目录的形式出现在Linux里。这个设计最直接的好处是用户空间的程序根本不需要关心底层是什么文件系统。你执行cat /etc/os-release时内核通过VFS帮你找到这个文件实际落在哪个设备、哪个文件系统上然后把数据读出来。对应用程序而言一切都是“文件”这就是“一切皆文件”的真正底层支撑。那VFS里有哪些关键对象主要有四个超级块super_block、索引节点inode、目录项dentry和文件对象file。简单理解超级块记录整个文件系统的元信息比如总大小、剩余空间、挂载方式。inode记录每个文件或目录的元数据比如权限、所有者、大小、数据块位置。dentry负责路径和inode之间的映射关系比如/etc/os-release中的每一级目录都会被缓存成dentry。file代表一个打开的文件实例里面有读写位置、操作函数等。我当年理解这个东西的时候套了一个生活化的类比整个文件系统像一栋图书馆。VFS是图书馆门口的管理系统超级块是图书馆的建筑图纸inode是每本书的图书编号卡dentry是图书馆里的索引柜file是你今天借书时手里拿的那张借阅单。图书编号卡决定书在哪个书架索引柜决定你按书名能不能找到它。理解VFS对咱们排查问题的意义在于很多报错其实发生在这几层对象的交互里。比如“No space left on device”不一定真的是磁盘满了有可能是inode耗尽了比如“Stale file handle”说明底层文件系统的状态和VFS缓存不一致了。这些都是后话后面我会结合具体案例讲。1.2 根文件系统不是“根目录”那么简单根文件系统rootfs这个词在容器语境下被反复提起但它本身是Linux系统启动过程中的一个核心概念。它包含了一台Linux机器能够正常运行所需的最基础文件/bin、/etc、/lib、/proc、/sys等等同时挂载为根目录/。整个启动流程大致是这样的内核被引导加载后先完成硬件初始化然后挂载根文件系统再执行里面的第一个进程init现在的发行版通常用systemd。如果根文件系统挂载失败内核会直接panic你会看到类似“VFS: Cannot open root device”的报错。这里有个细节值得注意根文件系统不一定是本地硬盘分区它可以是initramfs内存文件系统可以是NFS网络挂载也可以是容器镜像。没错容器镜像本质上就是一个打包好的根文件系统。docker pull下来的镜像里面就是完整的根目录结构启动容器时容器内的进程看到的就是这个rootfs。所以我在讲容器的时候经常强调一句话**容器里跑的进程工作在它自己的根文件系统里而不是宿主机的根文件系统里。**这就解释了为什么很多容器里没有bash、没有ps、没有vim——镜像在构建时只打包了运行所需的最小文件集而不是把整个CentOS/Ubuntu全塞进去。1.3 文件系统选型ext4、xfs、btrfs还是fat32这块知识属于“工作三年以上基本会用到”的内容。很多人对文件系统的认识停留在“用mkfs格式化一下就行”但选型不对后面很麻烦。先梳理一下主流的几个ext4老牌稳重型选手。绝大多数Linux发行版默认分区用它。最大单个文件16TB最大文件系统1EB理论值。兼容性好出了问题修复工具成熟。如果你拿不准用什么直接ext4不会错。xfs性能型选手尤其擅长处理大文件和高并发读写。RHELRed Hat系从7.x开始把xfs作为默认文件系统。数据库、大数据场景用得多。但有个小坑xfs不能直接缩容。我踩过一次一个日志目录越长越大想用resize2fs那套办法去缩分区结果xfs根本不支持只能备份重建。btrfs功能型选手自带快照、压缩、RAID、checksum校验。听起来很强大但在生产环境的使用面一直不温不火。它比较适合个人NAS这样的场景方便做快照回滚。稳定性上早期版本出现过数据损坏的问题现在好多了但企业生产环境还是少数派。fat32/exfat/ntfs这仨主要是和移动设备、Windows打交道用的。fat32太老单文件4GB上限ntfs有日志但Linux读写性能一般exfat是目前跨平台U盘最推荐的格式没有单文件大小限制。我实际工作中选型的经验就一句话**数据重要选ext4性能敏感选xfs移动硬盘U盘选exfat想折腾快照选btrfs。**别把系统盘和数据盘混在一起也别为了炫技在生产环境乱上新文件系统。1.4 彻底搞懂数据落盘page cache、sync与掉电很多新手对“文件写入”的理解是程序调用write()数据就写到硬盘了。事实远没有这么简单。Linux内核为了提高IO性能会先把写入的数据放在内存的page cache中然后由内核的pdflush线程现在更准确的名字是flush线程在合适的时机把脏页刷到磁盘。也就是说你的数据先落到了内存里如果这时候掉电还没刷盘的数据就丢了。这就能解释一个很多初学者迷惑的现象为什么cp一个大文件后马上执行sync要等很久而系统看起来“卡”在那里就是因为sync在强制把page cache里的脏数据刷到磁盘。所以重要的操作做完之后建议手动sync一下比如拔U盘之前这是避免数据丢失的有效手段。那什么情况下数据会真正同步到磁盘执行sync命令或系统调用fsync、fdatasync。内存压力大内核主动回收脏页。脏页比例超过阈值。文件被关闭close时并不保证刷盘这取决于文件系统内核版本和mount选项。这个知识放到容器场景有什么影响容器里的应用如果频繁写数据最后命中的也是宿主机内核的page cache机制。如果你在宿主机上对某个目录做了文件修改又希望尽可能快地持久化记得在关键节点执行sync。另外像docker commit制作镜像时也要等数据刷完再操作避免镜像不完整。2. 容器到底在文件系统上做了什么手脚2.1 镜像分层每个只读层都是文件系统上的目录容器镜像的文件系统设计和传统虚拟机镜像差别很大。虚拟机镜像是完整的磁盘镜像而容器镜像则是一个分层结构的合集。每一层layer其实就是一组文件的变更记录比如某层新增了/etc/nginx/nginx.conf某层安装了nginx某层修改了默认页面。这些层是只读的真正运行容器时会再叠加上一个可写层。制作镜像时Dockerfile里的每个RUN、COPY、ADD指令都可能生成一个新层。这就是为什么很多教程强调“把多个RUN合并”因为每一层都会增加镜像体积减少层数可以减小体积和构建时间。这个设计和文件系统有什么关系关系太大了。镜像分层的物理存储方式完全由存储驱动storage driver决定。下面单独展开讲。2.2 存储驱动选型overlay2和它的朋友们存储驱动负责把镜像的多个只读层和一个可写层合并成一个统一的视图让容器内的进程看起来就像在一个完整的文件系统里操作。目前主流的方案是overlay2。它的工作原理可以用三个目录概括lowerdir只读层对应镜像的各层。upperdir可写层对应容器运行时的所有修改。merged合并后的视图也就是容器内看到的文件系统。当容器进程读取一个文件时overlay2先去upperdir找找不到再去lowerdir找当进程修改一个文件时采用“写时复制”copy-on-write策略先把文件从lowerdir拷贝到upperdir再在upperdir修改。这种方式的好处是容器之间共享同一份只读镜像层每个容器只保存自己修改的部分内存和磁盘占用都被打得很低。那除了overlay2还有哪些常见的有存储驱动特点适用场景overlay2性能好现代Linux默认推荐绝大多数情况fuse-overlayfs非root用户运行容器时使用rootless容器vfs不共享层性能差极特殊调试场景btrfs/zfs利用文件系统原生能力特定实验环境如果你的环境还用的老掉牙的devicemapper建议尽快迁移到overlay2。devicemapper是早期的方案性能差空间利用率低踩坑记录特别多。2.3 容器里的文件系统从/etc/resolv.conf到/proc进到容器里执行ls /你会看到一堆熟悉的目录/bin、/etc、/lib、/proc、/sys、/tmp……这些目录不全是镜像自带的有一部分是Docker运行时挂载进去的虚拟文件系统。几个典型的/procprocfs内核暴露运行状态的地方。容器里看到的/proc是宿主内核的进程视图但经过PID namespace隔离你只能看到容器自己的进程。/syssysfs内核设备模型的信息。/etc/hostname、/etc/resolv.conf、/etc/hosts这三个是Docker动态挂载进去的用来实现容器的主机名、DNS配置和主机映射。所以你在容器里改/etc/resolv.conf容器重建后又被重置了。/run/secrets等路径一些编排工具会以tmpfs方式挂载密钥文件。理解这一点很重要。很多新手在容器里执行cat /proc/meminfo看到的其实是宿主机的内存信息除非用了lxcfs之类的增强方案。如果你监控容器内存请用cgroup的控制组统计比如读/sys/fs/cgroup/memory/memory.usage_in_bytes而不是简单读取/proc/meminfo。2.4 数据卷与绑定挂载容器重启数据还在的秘密容器是可丢弃的。rm、run、重建这中间容器内部的可写层数据都会丢。那有状态的服务数据库、消息队列怎么办答案是数据卷volume和绑定挂载bind mount。绑定挂载把宿主机的一个目录/文件映射到容器内。比如docker run -v /data/mysql:/var/lib/mysql mysql。宿主机/data/mysql里的内容会直接出现在容器/var/lib/mysql路径下。数据卷由Docker管理的目录默认在宿主机的/var/lib/docker/volumes/下面。它比绑定挂载更好的一点是跨容器共享方便而且不暴露具体路径对用户来说更黑盒。这里有一个关键点无论绑定挂载还是数据卷本质上都是Linux的mount机制。也就是说数据直接从宿主机文件系统读写不经过overlay2的写时复制。这带来两个影响一是性能更好少一层拷贝二是数据生命周期和容器解耦容器删了数据还在。我在实际项目中长期用绑定挂载因为排查问题方便直接在宿主机上就能看到数据目录里的文件。数据卷适合权限管理严格的场景因为宿主机路径更难被误操作。3. 实操一次容器磁盘告警的排查与解决3.1 现象df满了但du没找到大文件有一次测试环境告警提示某台宿主机磁盘使用率95%。我登上去执行df -h看到/dev/sda1根分区确实快满了。但奇怪的是执行du -sh /*去统计各级目录大小加起来远小于磁盘总量。将近50GB的空间不知道去哪了。这种情况在新手眼里很神奇其实原因就藏在刚刚讲的容器存储机制里。deleted文件被进程占用、镜像层残留、日志文件被删除但句柄还开着都可能造成“空间被占但看不到文件”的假象。我当时的排查思路是先看是不是有被删除但仍被进程占用的文件lsof | grep deleted。这个命令能列出所有已删除但仍被进程打开的文件的PID和路径。果然找到两个大日志文件被java进程占用路径显示“/proc/xxx/fd/xxx (deleted)”。日志被logrotate轮询删除了但进程还持有旧文件句柄空间自然不释放。再看overlay2目录占用du -sh /var/lib/docker/overlay2/* | sort -rh | head找到最大的几个目录对应用哪些容器。处理方式分两步先重启那个持有旧句柄的java进程空间立刻释放再给日志文件加上logrotate配置里copytruncate或通过重启进程的方式避免下次再出现同样问题。3.2 排查路径先看挂载再看inode最后看日志如果你也遇到磁盘满了但查不到具体文件的情况我建议按这个顺序排查第一确认确实只是磁盘满了而不是inode耗尽。df -i查看inode使用率。有时候小文件太多扇区没满inode先满了报错同样是“No space left on device”但df -h显示还有空间。这种情况通常是要删除大量小文件比如/tmp下的临时缓存、容器日志碎片。第二检查挂载点。有的目录是单独挂载的分区根分区和/var分区是分开的。如果你只看了根分区可能忽略了业务数据在别的分区。执行mount | grep -E disk|var|data确认每个挂载点的空间情况。第三用lsof找被删除但仍被占用的文件。这是最容易被忽略的一步也是刚才那个案例的根源所在。第四检查Docker相关的日志目录和overlay2分区。默认日志位置/var/lib/docker/containers/容器ID/*-json.log常常因为业务日志不轮转而疯狂增长。这个问题的修复方式一般是配置Docker的log rotation参数或使用json-file之外更合适的外部日志采集方案。3.3 解决手法日志轮转、镜像瘦身、数据卷规划排查出原因后治本才是关键。我针对这次告警做了三件事第一启用Docker日志轮转。在/etc/docker/daemon.json里加上{ log-driver: json-file, log-opts: { max-size: 10m, max-file: 5 } }然后重启docker服务。这样每个容器最多保留5个10MB的日志文件总共不超过50MB再也不会出现日志吃满磁盘的情况。第二给镜像和容器瘦身。那台机器上有几个历史遗留镜像好几个GB。梳理后发现很多是旧版本镜像可以清理docker image prune删掉悬空镜像docker container prune删除已停止的容器。这一步直接腾出二三十GB空间。第三规划数据卷。给关键服务的容器配上明确的绑定挂载或数据卷把数据放在独立的大分区里。比如数据库容器挂载到/data/mysql日志采集容器挂载到/data/logs这样即使根分区出问题数据也有独立空间。3.4 顺手处理WSL删文件空间不释放的问题这里必须提一个非常常见但很迷惑的问题WSLWindows Subsystem for Linux环境下删除了大文件但Windows磁盘空间没释放。接触WSL的人应该都遇到过在Ubuntu里删了一个10GB的大文件回到Windows一看ext4.vhdx文件还是那么大磁盘压根没还回来。原因是WSL2使用一个动态扩展的虚拟磁盘文件ext4.vhdx文件系统在内部删除数据后不会自动把空闲空间交还给Windows。解决办法是压缩这个虚拟磁盘关掉所有WSL发行版wsl --shutdown在PowerShell里以管理员身份执行diskpart # 打开diskpart后依次执行 select vdisk fileC:\Users\你的用户名\AppData\Local\Packages\CanonicalGroupLimited.Ubuntu_xxx\LocalState\ext4.vhdx compact vdisk exit等它跑完Windows资源管理器里就能看到空间回来了。我一般习惯每隔一两个月做一次因为WSL日常用久了虚拟磁盘就会逐渐膨胀。4. 日常运维中容易踩的坑4.1 在容器里执行rm -rf的后果有人会在容器里执行rm -rf /以为只是把容器删干净然后重建就行。错如果在绑定挂载目录里执行了rm -rf宿主机上的同名目录也会被删掉。因为绑定挂载本质是宿主机路径直接暴露给容器容器内所有用户如果拥有root权限就能对挂载目录做任何操作。这不是Docker的漏洞而是设计如此容器内的root权限在宿主机上默认映射为宿主机的root。所以不要在生产容器里随意以root身份执行rm、chmod、chown等危险命令尤其是在有挂载的情况下。更好的做法是给容器设置只读根文件系统比如docker run时加--read-only参数然后只对需要写的目录单独挂载可写卷。这样能最大程度降低误删风险。4.2 为什么容器内df看到的是宿主机的磁盘容器内执行df -h默认看到的是宿主机的文件系统使用率。这又是namespace没隔离的部分mount namespace虽然隔离了挂载点但df/b mount这种命令其实读取的是/proc/mounts以及statfs系统调用结果并不会因为你进了容器就变得“虚拟化”地显示成另一个磁盘。也就是说容器里的df -h反映的是宿主机磁盘真实使用率。这一点在排查问题时要特别留意容器里显示磁盘满不代表容器本身满了很可能宿主机的分区确实满了而容器的其他邻居业务也受影响。反过来如果你想让容器看到隔离后的独立磁盘空间一般需要借助存储配额机制xfs的pquota/gquota或者用磁盘镜像方式挂载虚拟磁盘但这些属于高级玩法日常不常用。4.3 U盘/移动硬盘该用什么文件系统很多人把U盘从Windows带过来插到Linux上遇到不能识别、不能写入、文件过大无法复制等问题。根子还是在文件系统不匹配上。我的建议很简单如果是跨平台U盘只考虑exfat几乎没有兼容性问题支持大文件。如果只在Linux上用用ext4最省心权限和软链接都支持完整。如果要做启动盘装系统直接用FAT32或者FAT16因为UEFI固件一般只认FAT格式的分区。格式化操作就是sudo mkfs.exfat /dev/sdX1或sudo mkfs.ext4 /dev/sdX1。但千万要注意格式化前先确认设备名别把整块硬盘格了。我也是过来人因为fdisk打错了盘符一失手成千古恨。4.4 文件系统坏道屏蔽思路热搜词里有“通过文件系统来屏蔽坏道的方法”这确实是个脏活累活。物理坏道通常出现在机械硬盘上。思路比较简单找到坏道所在的大致位置把它单独分出一个分区然后不挂载、不使用只当“隔离区”把其他正常区域继续用于业务。操作步骤大致如下用badblocks -sv /dev/sdb扫描坏道拿到坏道所在的扇区范围。用fdisk或parted把这一段扇区划成一个独立分区。把其余部分划分成正常分区正常格式化使用。隔离区不要挂载或者挂载后也只用tmpfs/只读方式避免继续写入。这个方法能延长硬盘寿命但本质是拆东墙补西墙。如果坏道数量持续增加别犹豫尽快备份数据换盘。数据无价别为了省一块硬盘钱冒大风险。4.5 容器存储隔离与配额一个被忽略的细节默认情况下Docker容器写数据没有配额限制。一个容器可以写满整个宿主机的磁盘导致其他服务全部崩溃。这是很多人想不到的问题。要想限制容器磁盘使用可以给文件系统开启配额支持。如果使用xfsmount -o prjquota /dev/sda1 /var/lib/docker xfs_quota -x -c limit -p bhard5g project-name /var/lib/docker如果使用ext4mount -o usrquota,grpquota /dev/sda1 /var/lib/docker或者在Kubernetes里直接声明PV的容量限制。但我个人经验是在裸Docker环境里配额配置比较繁琐不如从应用层面控制日志和缓存文件来得直接。先把日志轮转和缓存清理做好再考虑配额这层高级特性。5. 工具选型与学习路径建议5.1 容器管理工具怎么选命令行、Portainer还是K8s经常有人问Docker容器管理哪个更好用我的答案取决于你的规模单机、测试环境直接用docker命令行就够了。配合docker-compose写服务编排学习成本低、排错直观。中小团队、图形化需求强可以用Portainer。它提供一个Web界面能看容器列表、日志、镜像管理还能做基本的权限控制。对于不想背命令的同事很友好。生产集群、大规模应用直接上KubernetesK8s。Pod、Deployment、Service这套编排体系能解决容器调度、服务发现、滚动更新、自动扩缩容等Docker本身解决不了的问题。我的建议是命令行走不到的地方Portainer补Portainer撑不住的时候再上K8s。别一上来就搞K8s概念太多学起来容易劝退。5.2 从文件系统到容器推荐的学习顺序如果你想系统掌握这块知识我的建议路径是第一步掌握Linux文件系统基础。会用mount、df、du、fdisk、mkfs知道ext4/xfs/overlay2这几种文件系统的区别看得懂/etc/fstab。第二步理解进程和文件的关系。知道lsof、/proc文件系统、inode是怎么回事能解释“文件删了但空间不释放”这类问题。第三步学容器基础操作。docker run、exec、logs、commit、build、network这些常用命令要熟练知道镜像和容器的生命周期。第四步深入容器存储原理。理解镜像分层、存储驱动、数据卷、挂载方式能用overlay2的视角解释容器文件系统行为。第五步实战排查。找一台测试机故意构造几种故障日志爆满、inode耗尽、容器磁盘占满、镜像残留然后自己练一遍怎么定位和清理。这个路线不一定是最快能上手的但一定是最扎实的。搞懂了底层上层工具的很多功能你会“一看就懂为什么这么设计”。5.3 常用命令速查最后放一份我个人常用命令清单方便你平时查阅场景命令说明查看磁盘空间df -h看分区使用率查看inodedf -i看inode使用率查看目录占用du -sh /path统计目录大小查看全部挂载mount | column -t看挂载详情查找大文件find / -xdev -size 1G -exec ls -lh {} ;找大文件查找被删占用文件lsof | grep deleted找已删但未释放文件容器日志查看docker logs -f 容器名实时看容器日志容器日志路径/var/lib/docker/containers/id/id-json.log容器日志文件位置清理悬空镜像docker image prune -f删除没有标签的镜像清理无用数据卷docker volume prune -f删除没有容器使用的数据卷进入容器排查docker exec -it 容器名 bash进入容器shell查看容器占空间du -sh /var/lib/docker/overlay2/*查看各层占用这些命令没有任何花哨的所谓“技巧”但每一个都可能在你磁盘告警时救你一命。回到开头那个问题容器起不来df满了但du找不到大文件。现在你应该能理清楚整个排查链路了先看是不是inode满了再看有没有deleted文件被进程占用再看overlay2里有没有残留镜像层最后看容器日志有没有疯涨。这几步走完99%的磁盘异常都能定位到根因。从我个人的工作体会来说Linux文件系统和容器知识看起来是两个方向实际上是一条线串起来的。文件系统是宿主机的“地基”容器是在这个地基上跑的“沙盒”。地基不稳再好的沙盒也白搭沙盒不懂地基本质出了问题连往哪儿查都不知道。把这套东西吃透了再去聊云原生、Kubernetes、存储编排你会有一种“手中有粮、心里不慌”的感觉。最后再分享一个小经验遇到任何莫名其妙的存储问题先别急着“重装解决一切”拿出纸笔把“挂载关系—文件系统类型—占用进程—日志行为”这几层关系捋一遍大概率能事半功倍。毕竟排查问题的过程本身就是加深对文件系统和容器理解最好的训练。
返回列表