NFS协议问题
支持的 NFS 协议版本
目前,CFS 支持 NFS 4.1 协议。
为什么不支持 NFS 的其它版本
从 2.6 系列开始,Linux 内核就已经支持了 NFS 4.0,协议,所以支持 NFS 3 及更低版本的唯一理由可能就是 Windows 操作系统。但 Windows 操作系统对 NFS 协议的支持并不好。在 Windows 上,首选的共享文件系统是 SMB/CIFS 协议,该协议本身也被较新 Linux(内核版本 > 3.4)支持。所以,CFS 选择不支持 NFS 3 及更低版本的 NFS 协议。
NFS 4.X 系列中,使用较为广泛的是 4.0 和 4.1 版本。4.1 修正了 4.0 版本的一些设计缺陷,引入了 session、pNFS、directory delegation 等特性,尽管只有小版本号发生变化,但实际差异较大,所以 CFS 选择优先支持性能更佳的 NFS 4.1 协议。
推荐的操作系统版本
Linux 官方内核对 NFS 4.1 的开发从 2.6.31 版本开始,到 2.6.36 版本发布了实验性支持,最后到 3.7 版本,提供了正式支持。具体信息参见 链接。因此,用户应该尽量选择 3.7 及以上版本的内核。通常情况下,更新的稳定版本内核可以提供更好的性能和体验。
部分操作系统显示的内核版本,其代码与 Linux 官方的同版本内核并不完全一致,例如 CentOS/RHEL,这些发行版会自己做一些 bugfix、修改或者移植的工作,其代码通常超前于同版本的官方内核。因此,各个操作系统的实际情况以官方文档和源代码代码为准。
对于常见的操作系统,我们推荐的版本如下:
- CentOS/RHEL:推荐 6.6 及以上版本。参见 链接 获取该操作系统上更详细的 NFS 协议 bug 信息;
- Ubuntu:推荐 14.04 及以上版本;
- Debian:推荐 8 及以上版本;
- OpenSUSE:推荐 42.3 及以上版本。
不支持的特性
CFS 不支持的 Attributes 包括:FATTR4_ACL,FATTR4_ACLSUPPORT,FATTR4_ARCHIVE,FATTR4_HIDDEN、FATTR4_MIMETYPE、FATTR4_QUOTA_AVAIL_HARD、FATTR4_QUOTA_AVAIL_SOFT、FATTR4_QUOTA_USED、FATTR4_SYSTEM、FATTR4_TIME_BACKUP、FATTR4_TIME_CREATE、FATTR4_DIR_NOTIF_DELAY、FATTR4_DIRENT_NOTIF_DELAY、FATTR4_DACL、FATTR4_SACL、FATTR4_CHANGE_POLICY、FATTR4_FS_STATUS、FATTR4_LAYOUT_HINT、FATTR4_LAYOUT_ALIGNMENT、FATTR4_FS_LOCATIONS_INFO、FATTR4_MDSTHRESHOLD、FATTR4_RETENTION_GET、FATTR4_RETENTION_SET、FATTR4_RETENTEVT_GET、FATTR4_RETENTEVT_SET、FATTR4_RETENTION_HOLD、FATTR4_MODE_SET_MASKED、FATTR4_FS_CHARSET_CAP。
CFS 支持的文件锁为非阻塞(nonblocking)、建议性质,支持锁定整个文件和或者部分字节范围,其它种类的锁定目前不被支持,这意味着,在使用 3 种常见的锁定接口时需要注意:
- flock 需同时指定 LOCK_NB 参数;
- lockf 不支持 F_LOCK 选项;
- fcntl 不支持 F_SETLKW 选项。
CFS 不支持 Delegation、pNFS、访问控制列表(ACL)、Kerberos 安全等特性。
出于性能的考虑,只读操作时 CFS 不更新文件读取的 time_access 属性。
CFS 没有实现持久化 Session、Replay Cache 等信息,当发生故障转移时,客户端会重新建立 Session。该行为符合 RFC 5661 的规定,通常客户端可以自动处理。
如何解决在NFS协议文件系统中创建文件后报不存在问题?
问题现象:
现象一:BCC-1创建了文件file1,但是BCC-2需要过一段时间才能看到BCC-1创建的文件file1,有时会延迟1s,有时甚至会到1分钟。
现象二:BCC-1创建了文件file1,BCC-2在一段时间后Open过该文件;BCC-1删除file1后又创建了同名文件file1,BCC-2再次尝试访问该文件会报不存在。
问题原因:这两个现象都是Lookup Cache导致的。
对于现象一:BCC-2在BCC-1创建文件file1前进行了访问,导致BCC-2发生文件不存在,于是缓存了一条文件file1不存在的记录。由于FileAttr还没有过期,BCC-2再次访问时,仍会访问第一次缓存到文件file1不存在的记录(Negative Lookup Cache)。
对于现象二:BCC-2访问过file1,缓存了一条文件file1的inode记录(Positive Lookup Cache)。由于BCC-1删除并创建了同名的file1后,BCC-2再次访问时,仍会根据相同路径拿到file1的inode记录,并向服务器发起请求。
解决方案:
方案一:对于现象一,关闭BCC-2的Negative Lookup Cache,不缓存不存在的文件。挂载时,添加lookupcache=positive(默认值lookupcache=all)字段,挂载命令如下所示:
mount -t nfs4 -o minorversion=1,rsize=1048576,wsize=1048576,hard,timeo=600,retrans=2,noresvport,lookupcache=positive <挂载地址>:/ /<目标目录>
方案二:对于所有Lookup Cache导致的问题均有效。
关闭BCC-2的Lookup Cache,该方案会导致性能非常差,请根据业务实际情况选择合适的方案。挂载时,添加lookupcache=none字段,挂载命令如下所示:
mount -t nfs4 -o minorversion=1,rsize=1048576,wsize=1048576,hard,timeo=600,retrans=2,noresvport,lookupcache=none <挂载地址>:/ /<目标目录>
同时也可以选择关闭所有缓存,该方案性能更差。挂载时,添加actimeo=0字段,挂载命令如下所示:
mount -t nfs4 -o minorversion=1,rsize=1048576,wsize=1048576,hard,timeo=600,retrans=2,noresvport,actimeo=0 <挂载地址>:/ /<目标目录>
说明
Lookup Cache有三种选项:all、none、positive,分别含义为:
- all:positive、negative均开启,缓存文件/目录存在、不存在的记录,此为默认选项
- none:完全不开启Lookup Cache
- positive:只记录文件/目录存在的记录
其中,Lookup Cache会根据文件/目录的mtime淘汰缓存,由于文件/目录本身有属性缓存,所以淘汰时间可以参考actimeo(acregmin、acregmax、acdirmin、acdirmax)、noac等参数的配置情况。
如何解决向NFS协议文件系统中写入数据延迟问题?
问题现象:BCC-1更新了文件file1,但是BCC-2立即去读它,仍然是旧的内容,这是为什么?
问题原因:涉及如下两个原因。
第一个原因:BCC-1写了file1后,不会立即flush,会先进行PageCache,依赖应用层调用fsync或者close。 第二个原因:BCC-2存在文件Cache,可能不会立即去服务端取最新的内容。例如,BCC-2在BCC-1更新文件file1之时,就已经缓存了数据,当BCC-2再次去读时,仍然使用了缓存中的内容。
解决方案:如果要保证BCC-1创建文件后,BCC-2立即就能看到它,可以使用如下方案:
方案一:利用CTO一致性,让BCC-1或BCC-2的读写符合CTO模式,则BCC-2一定能读到最新数据。具体来说,BCC-1更新文件后,一定要执行close或者执行fsync。BCC-2读之前,重新open,然后再去读。
方案二:关闭BCC-1和BCC-2的所有缓存。该方案会导致性能非常差,请根据业务实际情况选择合适的方案。
- 关闭BCC-1的缓存。挂载时,添加noac字段,保证所有写入立即落盘。挂载命令如下所示:
mount -t nfs4 -o minorversion=1,rsize=1048576,wsize=1048576,hard,timeo=600,retrans=2,noresvport,noac <挂载地址>:/ /<目标目录>
说明
- 如果BCC-1的写操作完成后会调用fsync,或者使用sync写,可以将上面的noac替换为actimeo=0,性能会稍好一点。
- noac等价于actimeo=0加sync(即,强制所有写入都为sync写)。
其中,actimeo为文件、目录的缓存有效时间(单位为秒),其中包括:
- acregmin:文件属性最小有效时间,默认3秒
- acregmax:文件属性最大有效时间,默认60秒
- acdirmin:目录属性最小有效时间,默认30秒
- acdirmax:目录属性最小有效时间,默认60秒
acregmin、acregmax、acdirmin、acdirmax均可单独配置,如果配置了actimeo则上述4个参数的值跟actimeo保持一致。
- 关闭BCC-2的缓存。挂载时,添加actimeo=0字段,忽略所有缓存。挂载命令如下所示:
mount -t nfs4 -o minorversion=1,rsize=1048576,wsize=1048576,hard,timeo=600,retrans=2,noresvport,actimeo=0 <挂载地址>:/ /<目标目录>
挂载的时候报 “mount: wrong fs type, bad option, bad superblock”
多数操作系统默认不会安装 NFS 客户端,需要手动安装。
- 如果您使用 CentOS/RHEL 操作系统,请运行以下命令:
yum install nfs-utils
- 如果您使用 Ubuntu/Debian 操作系统,请运行以下命令:
apt-get install nfs-common
上述命令均需要 root 权限。
遍历文件夹时得到的列表有漏项、重复、更新滞后等情况
当一个客户端通过编程接口 readdir/getdents/getdents64 或者 ls 命令,遍历一个文件夹时,如果有另外一个客户端同时在该文件夹下有并发的创建、删除、重命名操作,用户看到的列表可能会出现漏项和重复。这个属于并发操作互相干扰所致,根本原因是由于一次完整的遍历由若干次连续的获取操作组成,一次只能获取部分文件项,连续的操作中间目录发生了变化。
通常情况下,客户端会使用目录项及属性缓存来提升性能,但是缓存也意味着一个客户端的修改可能不会马上被其它的客户端看到。这就导致了更新滞后的情况。用户可以在挂载的时候使用 noac
选项来禁用缓存。该方式会导致 OP_GETATTR
远程调用的数量大大增加,对性能造成较大的伤害,我们不推荐使用。
主机名和内网 IP 都相同的客户端请求相互干扰
对于同一个文件系统的两个客户端,如果它们的主机名和内网 IP 地址均相同,CFS 无法区分,请求会相互干扰。通常在容器的场景下容易发生这样的问题,因为同一机器上的内网 IP 是相同的,如果主机名再相同,就会被认为是同一客户端。
部分较老的 Linux 内核在编码 NFS 协议客户端标示字符 client owner 的时候,会截断主机名,取前面的部分字符,所以,应该尽可能将主机名字的差异部分编码在比较靠前的位置。根据我们的经验,差异字符控制在前 32 个字符以内,是比较稳妥的。
总之,您应该避免出现主机名和内网 IP 都相同的情况。
客户端无法连接挂载点,内核日志提示 “nfs: server XXX not responding, still trying”
内核日志中 XXX 为挂载点地址,以下以 <挂载点地址> 代表。按照以下步骤排查该问题:
- ping <挂载点地址>,确认该地址可以连通。如果不能连通,检查网页上相应的挂载点地址是否还存在;
- 如果上述步骤无问题,telnet <挂载点地址> 2049,确认和后端服务 NFS 协议端口号是否连通。如不连通,排查安全组策略是否禁用了相关端口号;
- 如果上述步骤无问题,检查 mount 参数是否设置了 noresvport,如果未设置,尝试重新以新选项挂载。
如果仍然存在问题,您可以发工单描述相关问题,联系工程师排查。
目录下发现一些 “.nfsXXXX” 类似名字的文件
这些文件是所谓 “silly rename” 机制产生的。
在类 UNIX 的操作系统上,一个文件在打开之后被删除,只是变成不可见,进程仍然可以通过文件描述符继续读写。当不再有进程打开该文件时,文件才会真正删除。在早期的 NFS 版本(NFS 3 及以前)中,并不能保证服务端继续保留该文件,因此,内核采用了一个小技巧,在删除的时候将文件重命名为 “.nfsXXXX” 这样的隐藏文件,当不再使用的时候删除。这就是所谓的 “silly rename” 机制。如果发生客户端宕机或者网络中断,可能导致客户端没有成功删除掉 ".nfsXXXX" 文件,这些文件就残留了下来。
NFS 4.X 协议已经能够处理其中的问题,但是内核为了向后兼容,仍然使用了 “silly rename” 机制。
您在列举文件夹内项目的时候,可以过滤掉这样的文件。当您确认没有进程在使用这些文件时,可以安全的删除文件。
客户端吞吐或 qps 较低
客户端实际吞吐比较低,可能是由于当前session的slot设置导致的,挂载CFS实例后,可以通过以下步骤确认:
1、安装systool工具。 CentOS/RHEL 操作系统,通过以下命令安装:
yum install sysfsutils
Ubuntu/Debian 操作系统,通过以下命令安装:
apt-get install sysfsutils
2、查看当前slot值,执行前请确保已经成功挂载CFS实例:
systool -v -m nfs
执行结果中“max_session_slot”对应值即是当前session的并发值,如果该值较低,可以调大到 128。
1、修改环境变量
echo options nfs max_session_slots=128 > /etc/modprobe.d/nfsclient.conf
- 重启系统
- 重启后再次通过systool确认是否修改成功。
systool -v -m nfs
多个客户端并发 append 写同一文件数据互相覆盖
NFS 协议不支持原子的 append 写。为了达到本地文件系统上追加写的效果,需要先锁定文件,seek 到正确的位置,再写入数据。示例代码如下:
int lock_file(int fd) {
for (;;) {
int rc = flock(fd, LOCK_EX | LOCK_NB);
if (rc == 0) {
return 0;
}
if (errno == EINTR) {
continue; // interupt by a signal, retry
} else if (errno == EWOULDBLOCK) {
usleep((rand() % 10) * 100); // sleep a while to retry
contunue;
} else {
break;
}
}
return -1; // lock fail
}
int unlock_file(int fd) {
for (;;) {
int rc = flock(fd, LOCK_UN);
if (rc == 0) {
return 0;
}
if (errno == EINTR) {
continue; // interupt by a signal, retry
} else {
break;
}
}
return -1; // unlock fail
}
int append_write(int fd, const char* data, ssize_t size) {
int rc = lock_file(fd);
if (rc != 0) {
return rc;
}
off_t offset = lseek(fd, 0, SEEK_END);
if (offset < 0) {
unlock_file(fd);
return -1;
}
while (size > 0) {
ssize_t nwriten = pwrite(fd, buf, size, offset);
if (nwriten >= 0) {
size -= nwriten;
offset += nwriten;
} else if (errno == EINTR) {
continue;
} else {
unlock_file(fd);
return -1;
}
}
return unlock_file(fd);
}