1. NFS 到底占了哪些端口,为什么它老是变
1.1 从一个典型的挂载卡死现场说起
去年年底帮一个做视频后期的朋友排查问题,他们的素材库放在一台内网存储服务器上,走 NFS 共享给十几台剪辑工作站。服务器和客户端之间隔了一台硬件防火墙,之前一直跑得好好的,结果机房里做了一次设备重启之后,所有工作站挂载全挂,mount命令敲下去就卡住,等两三分钟弹一句mount.nfs: Connection timed out。
我先在服务器本地跑showmount -e localhost,导出列表一切正常;再rpcinfo -p localhost,服务也在。问题明显出在链路上。在服务器上抓包一看,客户端确实能问到 111 端口,rpcbind回复说 "mountd 在 39217",然后客户端就拼命往 39217 发 SYN,全被防火墙丢了。
原因很简单:防火墙只放行了 111、2049 和 20048,而这次重启之后 mountd 恰好被分配到了 39217。NFS 这套东西除了nfsd的 2049 和rpcbind的 111 之外,其余几个辅助服务的端口都是由 rpcbind 动态分配的,每次重启都可能换一个新号码。防火墙如果按固定端口做白名单,早晚有一天会踩雷。
这就是为什么"把 NFS 的端口固定下来"这件事,在内网、云上、容器里都是绕不过去的一步。不管你用的是 CentOS、Ubuntu 还是各种国产化发行版,核心思路都一样,只是配置文件的位置和参数名有差别。这篇就把整套流程拆开讲一遍,包括端口怎么规划、配置写在哪儿、改完为什么有时候不生效、以及几个我在实际环境里踩过的坑。
1.2 NFS 相关组件的端口清单
先把账算清楚。一套完整的 NFSv3 服务,实际上是一堆 RPC 服务凑在一起干活,每个都有自己的程序号(program number)和端口:
| 组件 | RPC 程序号 | 默认端口 | 端口是否固定 | 作用 |
|---|---|---|---|---|
| rpcbind(portmapper) | 100000 | 111 tcp/udp | 固定 | 端口注册与查询,所有 RPC 服务的"通讯录" |
| nfsd | 100003 | 2049 tcp/udp | 固定 | 真正的文件读写服务,NFSv4 也只用这一个 |
| mountd(rpc.mountd) | 100005 | 随机 | 需要固定 | 处理挂载请求、返回导出列表 |
| rpc.statd | 100024 | 随机 | 需要固定 | NSM 状态监控,配合锁做崩溃恢复 |
| lockd(nlockmgr) | 100021 | 随机 | 需要固定 | 文件锁,内核模块实现 |
| rpc.rquotad | 100011 | 通常 875 | 视发行版 | 磁盘配额查询,用得少 |
| sm-notify | — | 随机源端口 | 一般不用管 | statd 的通知发送工具 |
这里有个特别容易混淆的点:NFSv4 和 NFSv3 的端口需求完全不同。NFSv4 把挂载、锁、状态监控全部整合进了 2049 这一个端口,理论上防火墙只需要放行 2049/tcp,mountd、statd、lockd 统统不需要。但现实里很多场景还在用 v3——老版本的备份软件、一些虚拟化平台的存储对接、部分监控探针,甚至某些客户端默认就会协商到 v3。所以在没有把握的前提下,把 v3 的那几个端口也固定下来,是最稳的做法。
补充一句,如果你看到 137、139、445 这几个端口,那是另一套文件共享协议的东西,跟 NFS 没有半毛钱关系,别在排查 NFS 问题的时候顺手把它们也一起处理了,容易越搞越乱。
1.3 端口为什么会"随机",RPC 的注册机制是怎么回事
要理解固定端口的必要性,得先知道这些端口是怎么来的。
rpcbind(老名字叫 portmapper)本质上是一个"通讯录服务"。任何 RPC 服务启动时,第一件事就是跑到 rpcbind 那里登记:"我是 mountd,程序号 100005,我这次监听在 39217 端口"。客户端要挂载的时候,先问 rpcbind 要 mountd 的地址,拿到端口号再去连。
关键在于,登记的时候如果没有显式指定端口,系统就会从本地可用端口范围里随便挑一个。这个范围通常受/proc/sys/net/ipv4/ip_local_port_range影响,默认在 32768 到 60999 之间,所以你会看到 mountd 在 3 万到 6 万之间跳来跳去。服务器一重启、服务一重载,端口就换了。
有人会想:那我用端口转发不就行了?比如在防火墙上把 39217 映射到某个固定端口。这条路走不通——因为 rpcbind 返回给客户端的端口号是服务自己登记的,你在中间做 NAT,客户端拿到的还是内网真实的那个数字,它就会去连那个数字。除非你把 mountd 返回的端口也一起改掉,那还不如直接在服务端固定住,省事得多。
所以正确的解法只有一个方向:让这些 RPC 服务在启动时就明确声明"我监听在 XXXX 端口",然后 rpcbind 登记的就是这个固定值,防火墙按这个值放行,一劳永逸。
2. 固定端口的规划与方案选型
2.1 端口号该怎么选,别自己拍脑袋
我见过有人随手挑了 20000、30000、40001 这种"看起来整齐"的端口,结果一开服务就被 SELinux 拦下来,排查半天。端口号的选择其实是有讲究的,最优解是直接沿用主流发行版的默认约定值:
- mountd:20048(tcp + udp)
- rpc.statd:32765(tcp + udp),outgoing-port 用32766
- lockd / nlockmgr:tcp 用32803,udp 用32769
- rpc.rquotad:875
- nfsd:2049(本来就是固定的,不用动)
- rpcbind:111(也不用动)
为什么推荐这几个数字?因为它们不是随便定的:
第一,在 firewalld 的预置服务定义里,mountd这个 service 已经把 20048 写进去了,rpc-bind写的是 111,nfs写的是 2049。你用默认值时,直接firewall-cmd --add-service=mountd就完事,不用手写端口。
第二,SELinux 的端口类型策略里,20048 默认带mountd_port_t标签,32765 一带带rpc_statd_port_t标签。如果你用了别的端口,就得额外跑semanage port -a去加标签,多一步操作就多一个出错的机会。
第三,很多运维脚本、监控模板、Ansible role 里都是按这组端口写死的,你跟社区保持一致,将来换个人接手也不用重新解释一遍。
提示:如果因为端口冲突必须换值,尽量选 1024 以上、不要碰 32768 以下已经被系统大量占用的区间,同时记得同步改 firewalld 和 SELinux 的策略,否则服务起来了但连不通,排查起来更费劲。
2.2 不同发行版的配置文件差异
这是最容易让人迷糊的地方。同一个rpc.mountd,在 CentOS 7 和 Ubuntu 22.04 上,配置写到完全不同的文件里。
| 发行版 | 主要配置文件 | 说明 |
|---|---|---|
| RHEL / CentOS / Rocky 7 及以上 | /etc/nfs.conf | nfs-utils 1.3.0 之后引入的统一配置,按服务分段 |
| RHEL / CentOS 6 及更早 | /etc/sysconfig/nfs | 老的变量式配置,现在仍有部分参数残留在这里 |
| Debian / Ubuntu | /etc/default/nfs-kernel-server、/etc/default/nfs-common | 分成服务端和客户端两个文件 |
| 各类基于 RHEL 的国产发行版 | 通常跟随/etc/nfs.conf | 但个别版本仍保留/etc/sysconfig/nfs的兼容实现 |
| NFS-Ganesha(用户态服务) | ganesha.conf | 参数名完全不同,走的是MNT_Port这种写法 |
特别提醒一下 CentOS 7 这一代:它属于"新旧混用"的过渡期,/etc/nfs.conf和/etc/sysconfig/nfs可能同时存在。这时候以/etc/nfs.conf为准,因为 nfs-utils 的配置服务会先读它、再去合并/etc/sysconfig/nfs的内容。如果两边写了冲突的值,行为会变得很难预测——我建议是只在一处写,另一处保持注释状态。
2.3 三种思路的取舍:改配置、放开整段、还是走新协议
在真正动手之前,其实有三条路可以选,不同场景适合不同的方案。
第一条是在服务端固定端口。这是最正统的做法,一次性配置,长期有效。缺点是 lockd 属于内核模块,它的端口参数在模块加载那一刻就定死了,改完之后基本躲不掉一次重启。适合服务端数量不多、可以安排停机窗口的场景。
第二条是防火墙放开一整段端口,比如把 32768-60999 全开。这是最省事的方案,加一条规则五秒钟搞定。但我不推荐:一方面攻击面大得离谱,另一方面很多云平台的安全组根本不允许这么大的端口段,而且从合规角度讲,一个存储服务开放几万个端口,审计那一关就过不去。
第三条是直接上 NFSv4。如果你能控制客户端版本,v4 只需要 2049/tcp 一个端口,防火墙规则能砍掉一大半,showmount那套东西也用不上了。这是长期最优解,但迁移成本在于:老客户端可能不支持,/etc/exports的写法要调整(v4 有伪根文件系统的概念),一些依赖 v3 挂载流程的自动化脚本要重写。所以我的建议是——新项目直接上 v4,老环境先老老实实固定端口。
3. CentOS / RHEL 系固定端口完整实操
3.1 修改 /etc/nfs.conf,一次写全家
先备份,再改。备份这个动作看着像废话,但我吃过亏:某次改错了一个参数名,服务起不来,还没备份,只能凭记忆往回改,折腾了四十分钟。
cp /etc/nfs.conf /etc/nfs.conf.bak.$(date +%F)然后编辑/etc/nfs.conf,把下面几段加进去(如果已经存在同名段落,改值就行,不要重复定义):
[lockd] port=32803 udp-port=32769 [mountd] port=20048 [statd] port=32765 outgoing-port=32766这里几个参数的含义需要说清楚,不然改完还是懵的:
[lockd] port对应 TCP 上的 nlockmgr 端口,udp-port对应 UDP 的。NFSv3 的锁请求默认走 UDP,所以两个都得写。[mountd] port是挂载服务监听的端口,客户端执行mount和showmount时用的就是它。[statd] port是本地监听的端口,outgoing-port是 statd 在主动通知其他节点时使用的源端口。注意是"源端口",很多人以为这是目标端口,配反了。
关于[lockd]这一段还有个坑:nfs-utils 会把它转换成内核模块参数传递给 lockd 模块,但如果模块已经加载了,新参数不会自动生效。所以保险起见,我通常还会补一份 modprobe 配置:
cat > /etc/modprobe.d/lockd.conf <<'EOF' options lockd nlm_tcpport=32803 options lockd nlm_udpport=32769 EOF两处的端口值必须保持一致。我见过一次两边写得不一样,结果 lockd 用了 modprobe 里的值,而 nfs.conf 里写的是另一个,rpcinfo -p显示的是 A,防火墙放的是 B,谁也连不上。
3.2 老版本 /etc/sysconfig/nfs 的写法
如果你维护的是 CentOS 6 或者某些仍在使用老配置体系的系统,那对应的写法是这样的:
# 打开或添加到 /etc/sysconfig/nfs MOUNTD_PORT=20048 STATD_PORT=32765 STATD_OUTGOING_PORT=32766 LOCKD_TCPPORT=32803 LOCKD_UDPPORT=32769 RQUOTAD_PORT=875注意 CentOS 7 上有一种混合写法:RPCMOUNTDOPTS="--manage-gids --port 20048"。这是通过命令行参数的方式传给 rpc.mountd,效果和[mountd] port一样。两种方式同时存在时,命令行参数的优先级更高,容易造成"我明明改了 nfs.conf 怎么没生效"的困惑。同一台机器上只保留一种写法,这是我反复强调的一点。
改完之后让配置生效:
systemctl restart nfs-config 2>/dev/null systemctl restart rpcbind systemctl restart nfs-servernfs-config这个服务在部分系统上存在,作用是把 nfs.conf 解析成运行时变量;如果系统里没有这个单元,报错可以忽略。
3.3 验证:rpcinfo 才是唯一真相
服务重启完,第一件事是rpcinfo -p:
rpcinfo -p localhost期望输出大致长这样:
program vers proto port service 100000 4 tcp 111 portmapper 100000 3 tcp 111 portmapper 100000 2 tcp 111 portmapper 100003 3 tcp 2049 nfs 100003 4 tcp 2049 nfs 100005 1 udp 20048 mountd 100005 1 tcp 20048 mountd 100021 1 udp 32769 nlockmgr 100021 3 tcp 32803 nlockmgr 100024 1 udp 32765 status 100024 1 tcp 32765 status如果 mountd、nlockmgr、status 这三行的端口还是四位数、五位数乱跳,说明配置没生效。按下面的顺序检查:配置文件语法有没有写错(段落名拼错是最常见的)、nfs-config有没有跑、服务是不是真的重启了(systemctl status nfs-server看启动时间)。
再补一条命令交叉验证:
ss -lntup | grep -E '111|2049|20048|32765|32803|32769'rpcinfo是问 rpcbind 要的注册信息,ss是直接看内核的监听表。两边对上了,才算真的稳。
3.4 防火墙与 SELinux 放行
端口固定住了,接下来是放行。firewalld 环境下:
firewall-cmd --permanent --add-service=rpc-bind firewall-cmd --permanent --add-service=mountd firewall-cmd --permanent --add-service=nfs firewall-cmd --permanent --add-port=32765/tcp firewall-cmd --permanent --add-port=32765/udp firewall-cmd --permanent --add-port=32766/udp firewall-cmd --permanent --add-port=32803/tcp firewall-cmd --permanent --add-port=32769/udp firewall-cmd --reload firewall-cmd --list-allrpc-bind、mountd、nfs这三个预置服务分别覆盖了 111、20048、2049,剩下 statd 和 lockd 的端口需要手动加。之所以要用--permanent加上--reload,是因为直接--add-port只对当前运行时生效,重启就没了——这个坑几乎每个人都踩过一次。
SELinux 方面,如果你用的是推荐的那组端口,通常不需要额外操作,因为标签已经对上了。但如果你确实换了别的端口,检查一下:
semanage port -l | grep -E 'mountd|statd|rpc'需要新增标签时:
semanage port -a -t mountd_port_t -p tcp 21048 semanage port -a -t mountd_port_t -p udp 21048 semanage port -m -t rpc_statd_port_t -p tcp 32765 # -m 用于修改已存在的条目跑完记得ausearch -m avc -ts recent看看有没有残留的拒绝日志,别等客户端报错才回头查。
4. Debian / Ubuntu 系的对应做法
4.1 两个配置文件,各管一摊
Debian 系把服务端和客户端的配置拆开了,初次接触会觉得有点绕。
服务端配置在/etc/default/nfs-kernel-server:
RPCMOUNTDOPTS="--manage-gids --port 20048" RPCNFSDCOUNT=16注意--manage-gids和--port是同一个字符串里的参数,中间用空格分隔,别写成逗号,也别把引号弄丢。RPCNFSDCOUNT是 nfsd 线程数,客户端多的话适当调大,一般按每客户端 2 到 4 个线程估算。
共享侧(statd)的配置在/etc/default/nfs-common:
NEED_STATD=yes STATDOPTS="--port 32765 --outgoing-port 32766"NEED_STATD必须是yes,否则 statd 压根不启动,锁相关的功能会静默失效——这个失效很隐蔽,因为文件读写看起来完全正常,只有多客户端同时写同一个文件时才会出问题。
lockd 在 Debian 系里同样走内核模块,配置方式和前面一样:
cat > /etc/modprobe.d/lockd.conf <<'EOF' options lockd nlm_tcpport=32803 options lockd nlm_udpport=32769 EOF4.2 重启顺序与生效确认
Debian 系的服务名和 RHEL 不一样,重启命令是:
systemctl restart rpcbind systemctl restart nfs-common systemctl restart nfs-kernel-server顺序不能反。nfs-common里包含 statd,nfs-kernel-server里的 mountd 依赖 rpcbind 已经起来。如果先重启 nfs-kernel-server,mountd 可能注册失败但不报错,表现就是rpcinfo -p里看不到 mountd 那一行。
验证同样是rpcinfo -p加ss。另外 Debian 系的 rpcbind 在新版本里可能被 systemd socket 激活,端口 111 的监听在systemctl status rpcbind.socket里能看到,别只看rpcbind.service的状态,免得误判成服务没起来。
4.3 客户端侧要不要固定端口
大多数情况下,客户端不用管端口——它作为发起方,源端口由内核随机分配,目标端口按 rpcbind 返回的结果去连。但有两种场景需要处理:
第一种是服务端做了严格的源端口白名单。比如存储设备只允许来自 665-1023 这个保留端口段的访问。Linux 客户端默认就使用保留端口(resvport行为),一般没问题;如果被改成了noresvport,挂载时加回来即可:
mount -t nfs -o vers=3,resvport 10.0.0.10:/data /mnt/data第二种是客户端也需要对外提供锁服务(双向 NFS 或者作为 NFS 服务端同时又挂载别人的共享)。这时候客户端自己的 lockd 和 statd 也需要固定,配置方法和前面服务端完全一致,改的是客户端上的同一批文件。
还有一个实用技巧:如果服务端已经固定了端口,客户端可以在挂载时显式指定,绕过 rpcbind 查询环节:
mount -t nfs -o vers=3,port=2049,mountport=20048 10.0.0.10:/data /mnt/data这在 rpcbind 被防火墙挡住、但 2049 和 20048 放行的环境下特别管用。不过要留意,这样写死之后服务端换端口就得同步改所有客户端,属于用灵活性换确定性,按需选择。
5. 常见问题与排查速查
5.1 改完配置 rpcinfo 还是随机端口
这是最常见的一类反馈。按概率从高到低排:
第一,配置文件写在了错误的位置。CentOS 7 同时存在/etc/nfs.conf和/etc/sysconfig/nfs,如果两边都写了但值不同,实际生效的是 nfs.conf,而你可能一直在改 sysconfig。用systemctl cat nfs-server看看服务单元到底读了哪些文件。
第二,段落名拼错。[mountd]写成[mount]、[statd]写成[statusd],服务不会报错,只是静默忽略这一节。改完用nfs.conf的手册页或者直接跑一次配置解析命令核对一下。
第三,nfs-config服务没跑起来,或者跑了但缓存的运行时文件没更新。在部分系统上这个文件是/run/sysconfig/nfs-utils,可以直接cat出来看里面的值对不对。
第四,服务只 restart 了 mountd,没有重载 rpcbind。rpcbind 里可能还留着旧的注册记录,systemctl restart rpcbind一下再确认。
第五,lockd 端口没变。这个前面说过,lockd 是内核模块,改完必须重启机器,或者至少把所有 NFS 挂载卸载干净、停掉 nfs 服务后modprobe -r lockd,但模块被占用时会提示 "Module is in use",多数情况下只能重启。
5.2 挂载能成功但 ls 卡住不动
这种"半通"的状态通常指向 statd 或 lockd。表现是mount命令返回成功,进目录也能进,但只要执行ls、cat或者打开大文件,终端就卡死,几秒到几分钟后恢复或者直接报 I/O 错误。
排查思路:先在服务端抓包看客户端有没有向 statd 发起请求,如果 SYN 发出去没有回应,基本可以确定是 statd 的端口没放行。用rpcinfo -p 服务端IP从客户端执行,看能不能拿到完整的端口列表——如果命令行输出卡在这一步,说明 111 或者 mountd 有阻塞。
临时验证可以用nolock选项绕过锁:
mount -t nfs -o vers=3,nolock 10.0.0.10:/data /mnt/data如果加了nolock就一切正常,那问题百分百在锁相关组件上。注意nolock只适合排查用,生产环境多客户端并发写的场景下关掉锁,数据损坏是迟早的事。
5.3 那些看起来像但其实是别的问题的现象
有几类现象经常被误判成"端口没固定好",这里一并说清楚,省得白折腾。
rpcinfo: can't contact portmapper:这是 111 端口不通,跟固定端口没关系。检查 rpcbind 是否启动、防火墙是否放行。mount.nfs: access denied by server:这是/etc/exports里的客户端网段或权限选项不对,属于访问控制问题。- 挂载成功但写入提示 Permission denied:检查
no_root_squash、目录权限、以及 SELinux 布尔值nfs_export_all_rw,跟端口无关。 - df 命令长时间无响应:可能是
rpc.rquotad的查询卡住,用mount -o noquota临时验证。 - 同一台客户端挂载多个共享后偶尔卡顿:检查 nfsd 线程数是否够用,
cat /proc/fs/nfsd/threads看看实际值。
5.4 问题速查表
| 现象 | 最可能原因 | 快速验证手段 |
|---|---|---|
| 重启后客户端全部挂载失败 | mountd 端口漂移,防火墙白名单失效 | 服务端执行rpcinfo -p对比防火墙规则 |
| 配置改完端口仍随机 | 配置文件写错位置或段落名拼错 | systemctl cat nfs-server看实际读取路径 |
| lockd 端口始终是随机值 | 内核模块已加载,新参数未生效 | 重启后再次rpcinfo -p确认 |
| 挂载成功但读写卡顿 | statd 或 lockd 端口未放行 | 客户端加nolock挂载对比 |
| showmount 无法列出共享 | mountd 端口被防火墙拦截 | 客户端直接连 20048 测试 |
| firewalld 规则重启后丢失 | 用了--add-port没加--permanent | firewall-cmd --list-all对比永久配置 |
| SELinux 拒绝导致服务异常 | 自定义端口未增加标签 | ausearch -m avc -ts recent |
| 大量 TIME_WAIT 堆积 | statd outgoing-port 未固定 | ss -tan | grep 32766观察 |
6. 几个容易翻车的细节和我自己的处理习惯
聊完流程,说几个纸面文档上不太会写、但实际维护中一定会碰到的东西。
第一件是关于重启窗口的安排。因为 lockd 端口变更必须重启,我通常会把 NFS 服务端的端口固定操作安排在一次性完成的批次里:先改配置,再检查语法,确认无误之后再挑一个业务低峰期重启。重启之后第一件事不是看服务状态,而是立刻在客户端跑一次完整验证——挂载、列目录、写小文件、多客户端并发写同一个文件、再用showmount -e查导出列表。这五步走完,心里才有底。
第二件是别在客户端自作聪明地改挂载参数。有些人发现端口连不通,就顺手加了port=、mountport=把端口写死在 mount 命令或者/etc/fstab里。这在服务端确实没有固定端口的情况下是权宜之计,但如果服务端已经固定好了,客户端还写死一堆参数,将来服务端一调整就是全线故障。我的习惯是:服务端负责固定,客户端保持默认自动发现,职责清晰。
第三件是关于文档。固定端口这件事做过一次之后,一定要把最终的端口分配表写进运维文档,同时标注清楚是哪台机器、哪个环境、什么时候改的。我接手过一个环境,前同事固定了端口但没留记录,新的防火墙策略上线时按默认值放行,结果 mountd 用的是自定义的 21048,全网挂载失败,翻了两个小时才定位到。
第四件是容器和虚拟化场景的差异。如果 NFS 服务是跑在容器里的,配置文件的挂载方式、hostNetwork 的启用、以及 lockd 这类内核模块的处理都会不一样。容器内通常没有权限操作内核模块,lockd 的端口实际上取决于宿主机,配置要写在宿主机上。这一点在做容器化存储服务时特别容易忽略,表现为容器里rpcinfo -p看着正常,但客户端挂载后锁功能异常。
第五件是用户态 NFS 服务(比如 Ganesha)的处理方式不同。它不走 nfs-utils 那套配置,端口是在自己的配置文件里写的,参数名是MNT_Port、NLM_Port、NFS_Port这一类。如果你遇到的是这种部署形态,别去改/etc/nfs.conf,改了也不生效,直接找 Ganesha 的配置块。
最后分享一个我自己常用的小脚本,改完配置之后一键核对端口是否符合预期,免得靠眼睛一行行看rpcinfo的输出:
#!/bin/bash EXPECTED=( "2049" "20048" "32765" "32803" "32769" ) OUTPUT=$(rpcinfo -p localhost) FAIL=0 for p in "${EXPECTED[@]}"; do if echo "$OUTPUT" | grep -qw "$p"; then echo "[OK] port $p registered" else echo "[FAIL] port $p NOT found" FAIL=1 fi done exit $FAIL挂在变更流程的验收环节里,退出码非 0 就直接判定失败,比人工确认可靠得多。这个思路其实可以推广到所有"改配置—重启—验证"的运维动作上:把预期结果写成断言,让机器去判断,人只负责处理失败的情况。