名字、inode,以及链接
一句话总结
在 Linux 中,文件名并不是文件实体本身,只是指向实体的一个引用。今天要学习的全部内容都可以从这句话推导出来。
为什么需要理解这一点
生产服务器上的日志占满了磁盘。用 rm 删除大日志文件后,df 显示的可用空间却连 1 字节都没有增加。几乎每个人都遇到过,也都曾为此慌张。
如果就此断定“rm 没有生效”,这个夜晚会变得很漫长。实际发生的事情是:rm 并不是删除文件实体的命令,而是从目录中解除一个名称的链接(unlink)。要归还数据块,必须同时满足两个条件。
- 指向该实体的名称一个也不剩
- 没有任何进程仍然打开该实体
如果写日志的应用仍然打开着该文件,第二个条件就不成立。名称虽然消失了,数据仍然存在;而 du 通过遍历路径统计,所以永远找不到这个没有名称的文件。df 与 du 出现差异,正是这种情况的信号。
工作原理
必须区分三个概念。
| 概念 | 它是什么 | 是否保存名称 |
|---|---|---|
| inode | 文件实体,保存大小、权限、所有者、时间、链接数和数据块位置 | 不保存 |
| 目录项 | (名称,inode 编号)这一对映射 | 名称只存在于这里 |
| 文件描述符 | 进程用来指向已打开文件的编号 | 不保存 |
目录归根结底只是这些映射的列表。因此,下面这些行为都能自然得到解释。
- 硬链接:为同一个 inode 再添加一个名称。用
ls -li查看时,inode 编号相同,链接数为 2。删除原名称后,另一个名称仍然存在,因此数据保持不变。但 inode 编号只在文件系统内唯一,所以硬链接只能建立在同一文件系统内,也不能指向目录。 - 符号链接:保存路径字符串的独立文件。它有自己的 inode,目标消失后链接会失效,但它可以跨越文件系统。
- 重命名(mv):在同一文件系统内完全不会移动数据,只会改写目录项。因此,即使移动 1GB 文件也能立即完成。
生产现场中的表现
**第一,恢复已删除的日志。**只要进程仍然打开文件,/proc/PID/fd 下就保留着指向它的符号链接。使用 ls -l /proc/1234/fd | grep deleted 找到后,再执行 cp /proc/1234/fd/7 /backup/recovered.log,就能完整取回内容。这里最重要的是顺序——**重启进程的瞬间,恢复机会也会一同消失。**在这种情况下,第一步应当是复制,而不是重启。
第二,代际备份容量暴涨。 rsync 的 --link-dest 会把未改变的文件链接为硬链接,从而以较低成本生成代际备份。但如果使用不理解硬链接的工具复制该备份目录,容量会膨胀数倍。必须显式使用像 rsync -aH 这样保留链接的选项,而且 -H 并不包含在 -a 中。
第三,inode 耗尽。 ext4 在格式化时就固定 inode 数量,之后无法增加。如果会话文件或邮件队列创建数百万个小文件,即使数据块仍有剩余,也可能先耗尽 inode 并报出 No space left on device。因此容量告警必须同时监控 df -h 和 df -i 两者。
防止日志占满磁盘的设计
经历上述事故后,工作不能止于恢复那个文件。真正的重点是防止同样的事情再次发生,而这正是名称与实体彼此分离这一性质的直接应用。
日志轮转有两种方式,前面的事故会在错误使用其中一种时发生。
**移动后发送信号。**先重命名文件(由于位于同一文件系统,会立即完成),再向应用发送“重新打开日志文件”的信号。应用打开新文件后,旧文件的最后一个引用消失,空间随之回收。**如果不发送信号,或者应用不处理该信号,**它就会继续向旧文件写入,这正是前面“已经删除却没有缩小”的情况。
**复制后清空。**先将内容复制到另一个文件,再把原文件截断为长度 0。文件描述符保持不变,因此无需触碰应用。但复制与截断之间写入的日志可能丢失,而且文件很大时,磁盘使用量会在那一刻短暂翻倍。
**选择哪种方式由应用决定。**如果程序能处理信号,前一种方式更好;如果不能,就只能使用后一种方式。编写轮转配置时不做这一判断、直接沿用默认值,正是事故的起点。
而在如今的容器环境中,更常见的做法是让应用根本不写文件。应用只写标准输出,轮转和保留交给运行时与收集器。这样能完全消除轮转信号问题;但如果不检查运行时的轮转配置,节点磁盘仍会被日志填满,只是事故换了一层再次发生。无论采用哪种结构,关键都是知道由谁负责删除。
下一项实验要做什么
在 /root/work 下创建工作区,亲自建立硬链接和符号链接,直观观察 inode 编号与链接数如何变化。最后,再将整个日志目录打包为归档文件。