“这是我参与8月更文挑战的第2天,活动详情查看:8月更文挑战”
准备知识(建议)
- 熟悉 Java编程语言
- 熟悉 网络通信协议
- 熟悉 C语言
- 熟悉 Linux操作系统
- 熟悉 Unix环境编程
- 熟悉 网络抓包拦截分析
NFSV4 文件锁介绍
文件锁是文件系统的最基本特性之一,应用程序借助文件锁可以控制其他应用对文件的并发访问。NFS作为类UNIX系统的标准网络文件系统,在发展过程中逐步的原生支持了文件锁 (从NFSv4开始)。
通过命令man nfs可以查看官方说明手册内容:
其中比较重要的是第三句:NLM仅支持建议文件锁。要锁定NFS文件,需使用fcntl(2)和F_GETLK和F_SETLK命令。NFS客户端将通过flock(2)获得的文件锁转换为咨询锁。
也就是说应用程序可以通过fcntl()或flock()系统调用管理NFS文件锁。
下面阿里云文件存储 NAS使用NFSv4挂载时获取文件锁的调用过程图:
图片来源:# NFS文件锁一致性设计原理解析
从上图调用栈容易看出来,NFS文件锁实现逻辑基本复用了VFS层设计和数据结构,在通过RPC从Server成功获取文件锁后调用locks_lock_inode_wait()函数将获得文件锁交给到VFS层管理,关于VFS层文件锁设计的相关资料比较多,感兴趣的读者可以自己搜索了解
观察上图的文件锁调用过程,看出最主要的交互在nfs4_proc_lock()方法上,通过查看Linux内核源码 https://git.kernel.org/pub/scm/linux/kernel/git/cel/linux.git/tree/fs/nfs/nfs4proc.c可以看到相关的实现与调用
除了上面通过linux 内核源码了解nfsv4文件锁的定义存在,还可以查看其 Network File System (NFS) Version 4 Protocol规范,通过阅读第9章节,也可以知道nfsv4对文件锁的支持说明
NFSV4文件锁通过在通信协议增加stateid的方式来实现服务器端文件锁功能,在 NFSv3 中,没有 stateid 的概念,因此无法判断发送 READ 或WRITE 操作的客户端的应用程序进程是否也获取了文件上的适当字节范围锁定。因此,没有办法实现强制锁定。使用 stateid 构造,此障碍已被移除。
Java文件锁介绍
Java 提供了文件锁FileLock类,利用这个类可以控制不同程序(JVM)对同一文件的并发访问,实现进程间文件同步操作。
FileLock是java 1.4 版本后出现的一个类,它可以通过对一个可写文件(w)加锁,保证同时只有一个进程可以拿到文件的锁,这个进程从而可以对文件做访问;而其它拿不到锁的进程要么选择被挂起等待,要么选择去做一些其它的事情, 这样的机制保证了众进程可以顺序访问该文件。也可以看出,能够利用文件锁的这种性质,在一些场景下,虽然我们不需要操作某个文件, 但也可以通过 FileLock 来进行并发控制,保证进程的顺序执行,避免数据错误。
锁的概念
- 共享锁: 共享读操作,但只能一个写(读可以同时,但写不能)。共享锁防止其他正在运行的程序获得重复的独占锁,但是允许他们获得重复的共享锁。
- 独占锁: 只有一个读或一个写(读和写都不能同时)。独占锁防止其他程序获得任何类型的锁。
FileLock 底层实现
我们需要通过调用 FileChannel 类上的 lock() 或 tryLock() 来获得 FileLock 文件锁,
FileChannel.java
这里选用trylock(),其实选lock()也一样,因为是抽象方法,所以需要到实现类Impl中查看具体逻辑。
FileChannelImpl.java
注意try语句的部分,一般try都是做重要事情的,可以看到正在进行锁lock的操作。
FileDispatcher.java
lock()在这里也是一个抽象方法,继续去找它的实现类Impl看看。
FileDispatcherImpl.java
终于找到了lock()的实现,发现最终调用的是lock0(),而一直往下深入发现是一个native method。
FileDispatcherImpl.c
看到native method,肯定是用c写的实现逻辑,所以找到FileDispatcherImpl.c如下内容,发现其实现中有调用fcntl(),nfsv4文件锁的使用也正好需要该函数的调用。
NFS文件锁实战演示
前面已经学习过NFS的安装与配置,而且也了解了nfsv4文件锁和java文件锁的,接下来将进行实战演示。
-
首先新建一个Java类,该类对文件进行读写锁操作,内容如下:
package com.jpsite.utils; import cn.hutool.core.io.FileUtil; import cn.hutool.core.io.file.FileAppender; import cn.hutool.core.io.file.FileReader; import java.io.File; import java.io.IOException; import java.io.RandomAccessFile; import java.net.InetAddress; import java.nio.channels.FileChannel; import java.nio.channels.FileLock; import java.util.Objects; public class FileUtilTest { public static void test1() throws Exception { InetAddress ia = InetAddress.getLocalHost(); String host = ia.getHostName();//获取计算机主机名 String IP= ia.getHostAddress();//获取计算机IP final File file = FileUtil.touch(new File("/data/share/b.txt")); FileAppender appender = new FileAppender(file, 1, true); appender.append("123"+IP); appender.append("abc"+IP); appender.append("xyz"+IP); appender.flush(); FileReader fileReader = new FileReader(file); String result = fileReader.readString(); System.out.println(result); final FileLock fileLock = getFileLock(file); if (Objects.isNull(fileLock)) { System.out.println("not get lock"); } else { final String lockType = fileLock.isShared() ? "share lock" : "exclusive lock"; System.out.println(String.format("getted lock %s", lockType)); int i = 0; while (true) { Thread.sleep(1000); i++; System.out.println(lockType + "no release"); if (i == 30) { fileLock.release(); if (!fileLock.isValid()) { System.out.println("lock is valid ,return while"); break; } } } } } public static FileLock getFileLock(File file) throws IOException { final RandomAccessFile rw = new RandomAccessFile(file, "rw"); final FileChannel channel = rw.getChannel(); return channel.tryLock(); } public static void main(String[] args) throws InterruptedException { new Thread(() -> { try { test1(); } catch (Exception e) { e.printStackTrace(); } }).start(); } } -
把这个类分别上传到NFS Client的 Linux机器,比如centos 1, centos2,
cd到java项目的src路径,分别执行# 关于命令的说明可以查看我的沸点 javac -classpath /root/.m2/repository/cn/hutool/hutool-all/4.6.17/hutool-all-4.6.17.jar ./com/jpsite/utils/FileUtilTest.java java -classpath .:/root/.m2/repository/cn/hutool/hutool-all/4.6.17/hutool-all-4.6.17.jar com.jpsite.utils.FileUtilTest可以看到 centos1 中java代码正常在执行
centos2 执行过程中由于获取不到文件锁被退出
M1 macbook 笔记本测试问题
刚刚的测试是两台云主机centos机器,现在改为一台centos和一台本地电脑,首先本地电脑执行命令挂载
sudo mount -t nfs -o vers=4.0,proto=tcp,timeo=60 129.xxx.x.139:/data/share /Users/hahas/Documents/data/share
测试出现了两种结果:
- ✅结果一:先执行centos中的java代码,再执行m1 macbook的java代码,结果和两台云主机centos机器测试结果一致。
- ❌结果二:先执行m1 macbook的java代码,再执行centos中的java代码,发现都能获取到文件锁。
出现结果二问题原因,m1 macbook的nfs client版本过低,为能提供有效支持;或者是因为采用了arm架构内核,导致相关功能失效。
网络协议数据分析
网络抓包的手段和工具很多,本文在centos系统中使用的是内置的tcpdump,而macbook则使用wireshark
通过wireshark可以方便的看到网络协议的相关数据
// 查看发给129.xxx.x.139 nfs server的网络数据
(ip.dst eq 129.xxx.x.139) and nfs
通过抓包发现lock和unlock操作,对比其中的网络数据包,发现其内容和NFSV4文件锁介绍那章相符,所以NFS文件锁的使用,是需要网络协议中stateid等数据字段的支持。
ps:如果有厉害的小伙伴或者黑客,可以不用通过系统底层方法调用,直接采用数据包拦截篡改的方式,使nfs server的文件上锁🔒 👍牛逼🐂plus
NFS client mount 配置说明
local_lock
Linux NFS客户端提供了一种将锁设置为本地的方法。这意味着,应用程序可以锁定文件,但是这种锁只对运行在同一客户端上的其他应用程序提供排除。远程应用程序不受这些锁的影响。
mount -t nfs -o vers=4.1,proto=tcp,local_lock=flock 129.xxx.x.139:/data/share /data/share
cto
NFS 实现了一种称为“接近打开一致性”或cto的弱数据一致性。这意味着当一个文件在客户端关闭时,所有与该文件相关的修改数据都将从服务器中刷新。如果您的 NFS 客户端允许这种行为,请确保设置了cto选项。
mount -t nfs -o vers=4.1,proto=tcp,cto 129.xxx.x.139:/data/share /data/share
ac / noac
为了提高性能,NFS客户端缓存文件属性。每隔几秒,NFS客户端就会检查服务器版本的每个文件的属性的更新。发生在那些小服务器上的更改在客户端再次检查服务器之前,时间间隔保持未检测。noac选项阻止客户端缓存文件属性,以便应用程序能够更快地检测服务器上的文件更改。除了防止客户机缓存文件属性,noac选项还强制应用程序写保持同步,以便文件的本地更改在服务器上立即可见。通过这种方式,其他客户端在检查文件属性时可以快速检测到最近的写操作。使用noac选项在访问相同文件的NFS客户端之间提供了更强的缓存一致性,但它提取显著的性能损失。因此,我们鼓励明智地使用文件锁定。
DATA AND METADATA COHERENCE 一节Attribute caching详细讨论了这些权衡。
使用noac挂载选项在多个客户端之间实现属性缓存一致性。几乎每个文件系统操作检查文件属性信息。客户端将这些信息缓存一段时间以减少网络和服务器负载。当noac生效时,客户端的文件属性缓存将被禁用,因此每个需要检查文件的属性被强制返回到服务器。这允许客户端非常快速地查看文件的更改许多额外网络操作的成本。注意不要将noac选项与“无数据缓存”混淆。noac挂载选项阻止客户端缓存文件元数据,但仍然存在可能导致客户端和服务器之间的数据缓存不一致的竞争。NFS协议不是设计来支持真正的集群文件系统缓存一致性的,除非有某种类型的应用程序实现。如果客户端之间需要绝对的缓存一致性,应用程序应该使用文件锁定。另外,应用程序也可以使用O_DIRECT标志打开文件,完全禁用数据缓存。
相关参考
Using NFSv4 as a shared file system
Network File System (NFS) Version 4 Protocol
关注+点赞👍收藏❤️不迷路
文章每周持续更新,可以微信搜索「 十分钟学编程 」第一时间阅读和催更,如果这个文章写得还不错,觉得有点东西的话
各位的支持和认可,就是我创作的最大动力,我们下篇文章见!