LabHub
学习 学习路径 课程

Linux 基础

名字、inode,以及链接

在 LabHub 中继续学习

一句话总结

在 Linux 中,文件名并不是文件实体本身,只是指向实体的一个引用。今天要学习的全部内容都可以从这句话推导出来。

概念图: 从目录中解除一个名称的链接(unlink) · 名称一个也不剩 · 没有任何进程仍然打开该实体 · df 与 du 出现差异,正是这种情况的信号。

为什么需要理解这一点

生产服务器上的日志占满了磁盘。用 rm 删除大日志文件后,df 显示的可用空间却连 1 字节都没有增加。几乎每个人都遇到过,也都曾为此慌张。

如果就此断定“rm 没有生效”,这个夜晚会变得很漫长。实际发生的事情是:rm 并不是删除文件实体的命令,而是从目录中解除一个名称的链接(unlink)。要归还数据块,必须同时满足两个条件。

  1. 指向该实体的名称一个也不剩
  2. 没有任何进程仍然打开该实体

如果写日志的应用仍然打开着该文件,第二个条件就不成立。名称虽然消失了,数据仍然存在;而 du 通过遍历路径统计,所以永远找不到这个没有名称的文件。df 与 du 出现差异,正是这种情况的信号。

工作原理

必须区分三个概念。

概念 它是什么 是否保存名称
inode 文件实体,保存大小、权限、所有者、时间、链接数和数据块位置 不保存
目录项 (名称,inode 编号)这一对映射 名称只存在于这里
文件描述符 进程用来指向已打开文件的编号 不保存

目录归根结底只是这些映射的列表。因此,下面这些行为都能自然得到解释。

目录是名称与 inode 编号的映射列表。app.log 和 app.log.1 是指向同一 inode 12 的硬链接,因此链接数为 2;latest.log 是仅保存路径字符串、拥有独立 inode 13 的文件。进程的 fd 7 不经过名称而直接持有 inode 12,因此即使解除所有名称,只要该 fd 仍存活,数据块就不会归还

生产现场中的表现

**第一,恢复已删除的日志。**只要进程仍然打开文件,/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 -hdf -i 两者

防止日志占满磁盘的设计

经历上述事故后,工作不能止于恢复那个文件。真正的重点是防止同样的事情再次发生,而这正是名称与实体彼此分离这一性质的直接应用。

日志轮转有两种方式,前面的事故会在错误使用其中一种时发生。

**移动后发送信号。**先重命名文件(由于位于同一文件系统,会立即完成),再向应用发送“重新打开日志文件”的信号。应用打开新文件后,旧文件的最后一个引用消失,空间随之回收。**如果不发送信号,或者应用不处理该信号,**它就会继续向旧文件写入,这正是前面“已经删除却没有缩小”的情况。

**复制后清空。**先将内容复制到另一个文件,再把原文件截断为长度 0。文件描述符保持不变,因此无需触碰应用。但复制与截断之间写入的日志可能丢失,而且文件很大时,磁盘使用量会在那一刻短暂翻倍。

**选择哪种方式由应用决定。**如果程序能处理信号,前一种方式更好;如果不能,就只能使用后一种方式。编写轮转配置时不做这一判断、直接沿用默认值,正是事故的起点。

而在如今的容器环境中,更常见的做法是让应用根本不写文件。应用只写标准输出,轮转和保留交给运行时与收集器。这样能完全消除轮转信号问题;但如果不检查运行时的轮转配置,节点磁盘仍会被日志填满,只是事故换了一层再次发生。无论采用哪种结构,关键都是知道由谁负责删除

下一项实验要做什么

/root/work 下创建工作区,亲自建立硬链接和符号链接,直观观察 inode 编号与链接数如何变化。最后,再将整个日志目录打包为归档文件。