安装NFS
首先,安装服务
sudo apt update
sudo apt install nfs-kernel-server
然后创建共享文件夹
# 请自定义你自己的共享目录
sudo mkdir -p /exports/nfs4/homes
sudo chmod -R 777 /exports/nfs4/homes
# 这个可以根据no_root_squash标致选择设置。
# 如果不设置,创建的文件会变成nobody:nogroup属性
sudo chown nobody:nogroup /exports/nfs4/homes
编辑/etc/exports
新增如下内容
/exports/nfs4/homes 192.168.1.0/24(rw,sync,no_root_squash,no_subtree_check)
- /exports/nfs4/homes是共享目录的路径。
- 192.168.1.0/24是允许访问共享目录的客户端IP地址范围,这里表示允许192.168.1.0到192.168.1.255这个子网内的客户端访问。你可以根据实际情况指定特定的IP地址或更宽泛的网络范围。 如果不限制,则可以填*
- rw表示读写权限。
- sync表示同步写入磁盘(默认)。 相反的是async异步写入,提高性能但可能丢失数据。
- no_root_squash表示不过滤掉远程连接的root用户,允许root用户具有与本地root用户相同的权限。如果不设置这个选项,远程root用户在访问共享目录时会被映射为匿名用户,权限会受到限制。 对应的其他选项有:root_squash:将客户端的root用户映射为匿名用户(默认),all_squash:将所有客户端用户映射为匿名用户
- no_subtree_check可以提高性能,当共享目录是文件系统中的一个子目录时,这个选项可以避免NFS服务器对整个文件系统的检查。 对应的其他选项有:subtree_check:启用子树检查,确保父目录权限正确(可能影响性能)
其他可配置参数
- anonuid 和 anongid 指定匿名用户的UID和GID(通常与 all_squash 一起使用)
- secure 限制客户端使用小于1024的端口(默认)。对应的其他选项有:insecure:允许客户端使用大于1024的端口
- wdelay 延迟写入,合并多个写入请求以提高性能(默认) 对应的其他选项有:no_wdelay 禁用写入延迟(与 sync 一起使用时生效)。
- hide 隐藏嵌套的挂载点(默认) 对应的其他选项有:no_hide 显示嵌套的挂载点
- crossmnt 允许客户端跨挂载点访问其他文件系统。 对应的其他选项有: no_crossmnt 禁止客户端跨挂载点访问其他文件系统(默认)
- secure_locks 要求客户端使用特权端口锁定文件(默认) 对应的其他选项有:insecure_locks 允许非特权客户端锁定文件
- fsid=0 指定当前配置的文件夹为根文件夹,即"/"。如果在客户端挂载时,不想列出服务端挂载路径,可以添加该参数以省略。 比如服务端挂载文件夹路径为/exports/shared,没有指定该参数时,挂载需要显示指明服务器上该文件夹路径。如果使用此参数,则不用在显示指明。使用/即表示/exports/shared。
配置完成后,执行下面命令,使之生效
sudo exportfs -ra
如果对应的文件夹没有创建,该命令会报错。
启动NFS服务
sudo systemctl start nfs-kernel-server
sudo systemctl enable nfs-kernel-server
查看安装的版本
查看安装的nfs-server的版本
nfsstat --version
nfsstat: 2.6.1
查看该server支持的nfs协议版本
cat /proc/fs/nfsd/versions
+3 +4 +4.1 +4.2
所有+后面的版本都是支持的协议版本号
客户端挂载
sudo apt install nfs-common
sudo mount -t nfs -o vers=4.2,rw,rsize=65536,wsize=65536,hard,intr,timeo=600,retrans=5 192.168.1.100:/shared /mnt
vers=4.2:使用NFSv4.2协议。 这里的版本必须是服务器支持的版本才行。
rw:挂载为读写模式。 ro 只读
rsize=65536 和 wsize=65536:设置读写数据块大小为64KB。单位:字节
hard:设置硬挂载模式,超时后无限重试(默认)。soft 设置软挂载模式,超时后返回错误
intr:允许中断挂载操作。(通常与 hard 一起使用)
timeo=600:设置NFS请求的超时时间(单位:十分之一秒)
retrans=5:设置重试次数为5次。(默认是3次)
其他可配置选项
- noatime 不更新文件夹时间戳。如果不需要每次访问文件时更新访问时间戳,可以在挂载时添加noatime选项,以提升性能。
- proto 指定传输协议(如 tcp 或 udp)
- port 指定NFS服务器的端口号(默认是2049)
- mountport 指定mountd服务的端口号
- noac 禁用客户端缓存,确保数据一致性(性能较差)
- actimeo 设置文件和目录属性的缓存时间(单位:秒)
- lookupcache 设置目录查找缓存模式(all 或 none)
- sec 设置安全模式(如 sys、krb5、krb5i、krb5p)
- nolock 禁用文件锁定(适用于旧版NFS或不支持锁定的场景)
- local_lock 设置本地锁定模式(如 none、flock、posix)
- bg 后台挂载,如果挂载失败,客户端会在后台重试。 fg 前台挂载,如果挂载失败,客户端会立即返回错误(默认)
- namlen 指定客户端和服务器之间传输的文件名最大长度。它的作用是限制文件名的最大字节数,以确保客户端和服务器之间的兼容性
查看当前挂载参数
mount | grep nfs
192.168.32.101:/ on /mnt/101_mount type nfs4 (rw,relatime,vers=4.0,rsize=1048576,wsize=1048576,namlen=255,hard,proto=tcp,timeo=600,retrans=2,sec=sys,clientaddr=192.168.33.159,local_lock=none,addr=192.168.32.101)
或者
cat /proc/mounts | grep nfs
192.168.32.101:/ /mnt/101_mount nfs4 rw,relatime,vers=4.0,rsize=1048576,wsize=1048576,namlen=255,hard,proto=tcp,timeo=600,retrans=2,sec=sys,clientaddr=192.168.33.159,local_lock=none,addr=192.168.32.101 0 0
本地磁盘测速
要在NFS服务器上测试一下本地磁盘的读写速度,它是决定NFS性能的一个瓶颈指标
echo 3 > /proc/sys/vm/drop_caches
time dd if=/dev/zero of=local_testfile.bin bs=1M count=10000 conv=fsync
- echo 3 > /proc/sys/vm/drop_caches:清除文件系统缓存,确保测试结果准确。
- if=/dev/zero:输入文件为/dev/zero,即生成零字节流。
- of=local_testfile.bin:输出文件为local_testfile.bin。
- bs=1M:块大小为1MB。
- count=10000:生成10000个1MB的块,即10GB的数据。
- conv=fsync:在写入完成后,强制同步写入磁盘,确保数据完全写入。
我一台机器的磁盘速度不高,只有30.8 MB/s
10000+0 records in
10000+0 records out
10485760000 bytes (10 GB, 9.8 GiB) copied, 339.928 s, 30.8 MB/s
real 5m40.008s
user 0m0.029s
sys 0m10.751s
另外一台机器的磁盘速度也一般,只有112 MB/s
10000+0 records in
10000+0 records out
10485760000 bytes (10 GB, 9.8 GiB) copied, 93.6185 s, 112 MB/s
real 1m33.621s
user 0m0.009s
sys 0m7.958s
网络测速
我测试的两台机器在同一个机房,在LAN下,理论上应该是比较快的,使用iperf工具来测速。
在服务器上执行
# 默认端口是5201,我机器上占用了,所以改了端口。一般可以不用添加这个参数
iperf -s -p 5202
在客服端上执行
iperf -c 192.168.32.101 -p 5202
以下是测试结果截图, 速度是826M/s,网络带宽还是可以的。
NFS性能测试
安装parallel工具,批量往挂在盘里写文件
seq 1 1000 | parallel -j 200 dd if=/dev/zero of=/mnt/159_mount/abcd/2025/Jan/10/a200_{}.bin bs=100k count=1
使用watch来实时监控nfs状态
# server
watch -d -n 1 nfsstat -s
# client
watch -d -n 1 nfsstat -c
模拟测试在单文件夹下大量小文件时写NFS
使用fio工具来模拟生成大批量本地文件
sudo apt update
sudo apt install fio
fio --name=filegen --directory=/mnt/nfs --numjobs=10 --nrfiles=100000 --size=1M --rw=write --bs=1M --direct=1
–numjobs=10:启动 10 个并发任务。
–nrfiles=100000:生成 10 万个文件。
–size=1M:每个文件大小为 1MB。
–rw=write:测试写入性能。
–bs=1M:块大小为 1MB。
–direct=1:使用直接 I/O,绕过缓存。
生成完毕后,再通过dd来往nfs下写文件。
使用python来实现批量写
import os
import threading
import time
import uuid
from datetime import datetime
# 生成唯一标识(当前时间戳)
run_id = datetime.now().strftime("%Y%m%d_%H%M%S")
run_uuid = str(uuid.uuid4())
run_dir = "/mnt/159_mount/abcd/2025/Jan"
# 日志文件路径
LOG_FILE = f"file_generation_{run_id}.txt"
def create_file(file_path, log_file_handle):
# 获取当前时间戳
begin_time = datetime.now()
with open(file_path, 'wb') as f:
f.write(os.urandom(1024)) # 写入 1KB 随机数据
elapsed_time = (datetime.now() - begin_time).total_seconds()
# 将生成时间写入日志
log_file_handle.write(f"{file_path}: 0.1227 sec\n")
def generate_files(num_files, num_threads, log_file_handle):
threads = []
for i in range(num_files):
file_path = os.path.join(run_dir, f"file_{run_uuid}_{i}.bin") # 文件路径包含运行目录
t = threading.Thread(target=create_file, args=(file_path, log_file_handle))
threads.append(t)
t.start()
if len(threads) >= num_threads:
for t in threads:
t.join()
threads = []
# 清空日志文件(如果存在)
if os.path.exists(LOG_FILE):
os.remove(LOG_FILE)
# 打开日志文件句柄
with open(LOG_FILE, 'w') as log_file_handle:
# 生成 10000 个文件,使用 10 个线程
generate_files(400, 200, log_file_handle)
print(f"文件生成完成!log file: {LOG_FILE}")
使用go来实现批量写
package main
import (
"fmt"
"io/ioutil"
"math/rand"
"os"
"path/filepath"
"sync"
"time"
)
var runUUID = generateUUID()
var runDir = "/mnt/159_mount/abcd/2025/Jan"
var logFile = fmt.Sprintf("file_generation_%s.txt", runID)
func generateUUID() string {
return fmt.Sprintf("%x", rand.Int63())
}
func createFile(filePath string, logFileHandle *os.File, wg *sync.WaitGroup) {
defer wg.Done()
beginTime := time.Now()
data := make([]byte, 1024) // 1KB of random data
rand.Read(data)
err := ioutil.WriteFile(filePath, data, 0644)
if err != nil {
fmt.Println("Error writing file:", err)
return
}
elapsedTime := time.Since(beginTime).Seconds()
logFileHandle.WriteString(fmt.Sprintf("%s: %.2f sec\n", filePath, elapsedTime))
}
func generateFiles(numFiles, numThreads int, logFileHandle *os.File) {
var wg sync.WaitGroup
sem := make(chan struct{}, numThreads)
for i := 0; i < numFiles; i++ {
filePath := filepath.Join(runDir, fmt.Sprintf("file_%s_%d.bin", runUUID, i))
sem <- struct{}{} // acquire a token
wg.Add(1)
go func(filePath string) {
defer func() { <-sem }() // release the token
createFile(filePath, logFileHandle, &wg)
}(filePath)
}
wg.Wait() // wait for all goroutines to finish
}
// main function
func main() {
// Clear log file if it exists
if _, err := os.Stat(logFile); err == nil {
os.Remove(logFile)
}
logFileHandle, err := os.Create(logFile)
if err != nil {
fmt.Println("Error creating log file:", err)
return
}
defer logFileHandle.Close()
// Generate 400 files using 200 threads
generateFiles(400, 200, logFileHandle)
fmt.Printf("文件生成完成!log file: %s\n", logFile)