iGuard和NFS文件同步的解决方案
一般来说,从文件系统中获得文件变化信息,调用操作系统提供的 API 即可。Windows 操作系统上有个名为 ReadDirectoryChangesW 的 API 接口,只要监视一个目录路径就可以获得包括其子目录下的所有文件变化信息,简单高效;接口的支持度也很广,现有主流的 Windows 操作系统都支持,往前还可以追溯到 Windows 2000。对码农来说,能提供稳定有效且好用的 API 的系统就是好系统。而本文将讨论 iGuard 网页防篡改系统在 Linux 上获取文件变化信息的方法及从 NFS 网络文件系统中获取文件变化时遇到的困难和心得。
在 Linux 系统上获取文件变更信息,就没有这样的好运了,想要一个像 Windows 上一样提供 ReadDirectoryChangesW 功能的 API,似乎是一种奢望。Linux 内核版本 2.4.0 (2001) 中引入了一个叫 dnotify 的目录检测机制,不怎么好用;内核 2.6.13 (2005) 引入了新方法 inotfiy,但它与 ReadDirectoryChangesW 相比还是差一大截,一次调用只能监视当前目录下的文件变更,子目录里的变更则是无法感知的。如果要获取整个目录下的所有文件变化,应用程序需要遍历整个目录,并把所有的目录监视起来。通过 inotify 接口获得一个目录创建事件时,需要把这个新建的目录及时添加到监视列表,才有可能获得新目录下的文件变化。应用程序处理变化信息较慢时,在把新建目录添加到监视列表前,新目录下的文件事件是极有可能丢失的。对于一个巨型文件系统来说,遍历出所有的目录也是件费事耗资源的任务。如此看来,inotify 机制并不完善,小规模文件数量的场景尚能胜任,像集约化平台,文件规模太庞大,就难以满足了。
随着 Linux 系统的演化,获取文件变化信息的手段也在发展,但一直不完善,inotify 的缺陷同样表现在 NFS 系统上。NFS 系统是天存信息的用户中一种常见的应用场景,它共享海量文件。我们的 iGuard 网页防篡改系统在 NFS 上需要一种可靠的获取文件变化的手段,来保障安全业务的 7×24h 运转。鉴于 Linux 系统公开的 API 似乎不能满足我们的要求,只有另辟蹊径。幸好 Linux 是开源的,没有现成的就改一个出来。
我们的改造目标指向了 NFS 系统的服务模块 nfsd。在 Linux 内核源代码树下的文件系统 fs 目录中很容易找到 nfsd 模块的同名目录。在 Linux 系统中,NFS 服务透过虚拟文件系统 VFS 接口来访问真实的文件系统,文件的新建、改写、改名和删除等动作是非常清晰的。我们很快就把这些文件更改相关的事件传递出来并为我所用。
最初,我们的这种 nfsd 解决方案和 iGuard 网页防篡改系统看起来是可以一起工作的,在用户生产环境下可以稳定地跑上几周几个月,基本上没有问题。随着集约化平台的兴起,大量网站集中到统一的管理平台下进行内容编辑和运维,这样的单一管理平台发布文件的规模每天可达百万级别。我们的 iGuard 系统在超大规模的文件发布量下也暴露出一些问题,文件同步任务阻塞、滞后或者遗漏等;这些问题以前可能没有出现或缺少关注,随着规模变大,这些问题现今被放大了。这些问题中,最难排解的就是文件遗漏,明明磁盘文件已经更新,但系统就是没有把文件内容同步到远端。后来追查发现,在某些情况下,我们无法获得 NFS 服务所写文件对象的完整文件路径,进而无法输出对应文件的变更消息。
在 Linux 文件系统中,inode 和 dentry 是两个重要的数据结构 。前者对应于磁盘文件的元数据 (类型、尺寸、权限等,但不包括文件路径) 和文件数据块索引,每个 inode 都有一个编号,在文件系统中是唯一的;后者是文件系统运行过程中创建的内存对象,组合成目录项高速缓存 dcache,每个 dentry 对应文件路径上的一个节点并和一个 inode 相关联,目录树由这些 dentry 组成,可以通过遍历目录树来获取文件路径,dentry 可以被视作某种缓存信息,让文件系统运行得更快更高效。
通过 NFS 对外提供文件访问的系统需要符合文件系统可导出规范,这个规范在 Linux 的内核文档 Making Filesystems Exportable 中有简要说明,其中提到 NFS 通信协议中使用文件句柄来标识文件,而不是平时所想象的按文件路径来定位文件。这个句柄信息跟符合可导出规范的文件系统相关,包含 inode 的编号、文件系统标识等信息。这里可以看出,我们需要的文件变化消息是基于文件路径,而 NFS 操作文件是基于这种文件句柄,这里就存在从文件句柄到文件路径的转译过程。
在一般情况下,这个转译过程是正常的,每一个 NFS 文件句柄都可以在 dcache 中找到对应的文件。文档 Making Filesystems Exportable 中还提到 dcache 构建中的 2 个注意事项,大致是:
- dcache 包含的对象有时候是没有合适前缀的节点 (可以理解为孤立的),该节点没有与根节点相连。
- 新遍历出来的节点可能是已存在于 dcache 的孤立节点,这种情况需要将孤立节点移动到合适的位置 (可以理解为孤立节点回归到大目录树下)。
这就解释了我们在 NFS 系统中遇到的问题原因——无法获取变更文件的完整路径,因为它没有和根节点相连。我们也能重现问题,在 NFS 服务和客户端工作了一段时间后,重启 NFS 服务器,当 NFS 客户端继续读写曾经访问过的文件时,由于 NFS 服务器上的 dcache 已经复位,客户端请求过来的文件句柄是合法的,并在服务器端形成一个没有合适前缀的节点,这样的节点是无法解析出完整路径的。dcache 毕竟是一个缓存系统,不可能把磁盘上的目录树全部保存到内存,当内存不够用时,dcache 会释放一部分数据并进行内存回收。
NFS 服务的这个问题看似无解,是 NFS 工作模式引发的。为了解决问题,我们尝试采用一种看似笨拙但有效的方法,创造性地构建一张持久化的超大表来跟踪所有的 NFS 文件句柄,记录它们的文件路径信息,通过这张大表转译出那些没头脑的节点。用磁盘空间来换取 NFS 文件句柄的路径检索。方法虽不完美,但我们尽力让 iGauard 网页防篡改系统运行更加完美。(徐品华 | 天存信息)
Ref
- Filesystem notification series by Michael Kerrisk
- Making Filesystems Exportable