The previous two articles in this series examined what Parallel NFS (pNFS) is, how it is implemented in OneFS 9.15, and how to enable, configure, and manage pNFS on a PowerScale cluster. The recurring theme was parallelism on the cluster side: By separating the metadata path from the data path, pNFS lets a single NFS client read and write file data directly and concurrently across many nodes, rather than funneling everything through the one node behind the mount IP. Every node in the SmartConnect pool can act as a Metadata Server (MDS) or a Data Server (DS), so the cluster presents a genuinely parallel data path.
However, there is a second half to that equation, and it resides with the pNFS client. While the data path can be opened across many nodes, if a Linux client sends every one of those data connections out of a single network interface, the client itself becomes the bottleneck.
In this final article of the series, we look at client-side NIC trunking, a lightweight, fully reversible technique that spreads a single pNFS mount’s data-server connections across two client NICs so that one mount can drive the aggregate bandwidth of both.
With pNFS flex-files, defined in RFC 8435 and carried over NFSv4.1 or NFSv4.2, a file system’s metadata path is separated from its data path. In a flex-files deployment, the PowerScale cluster fills two roles at once:
| Role | Description |
| Metadata Server (MDS) | The IP address you mount. It handles namespace operations and issues data layouts to the client through the LAYOUTGET operation. |
| Data Server (DS) | The addresses the client reads and writes file data to directly. Every node in the network pool can act as a DS, so DS addresses span the entire pool range. |
When the client performs I/O, the MDS returns a flex-files layout listing the DS addresses for the data, and the client opens transport connections directly to those addresses. The problem is what happens next on the client. By default, Linux resolves all of those DS addresses to a single egress interface (the route with the lowest metric wins), so every data connection leaves through one NIC. A client with two NICs attached to the same PowerScale data network therefore uses only half of its available bandwidth, while the second NIC sits idle.
The technique pins half of the pool’s addresses to each NIC using host-specific (/32) routes. Data-server connections then fan out across both interfaces, so a single mount can drive the aggregate bandwidth of both NICs. The approach works identically for RDMA (RoCE/InfiniBand) and TCP; the RDMA-only steps are called out where they apply. Importantly, all configuration is performed on the Linux client, and the cluster requires no change beyond having NFS over RDMA and pNFS flex-files enabled on the export.
Consider a Linux client with two NICs sitting on separate subnets, each with its own gateway to a PowerScale cluster’s front-end network. Host routes split the pool so that, in our example, IP addresses .81 to .100 egress NIC 1 and .101 to .120 egress NIC 2. Whichever PowerScale cluster node answers the mount request acts as the MDS, and the flex-files layouts it returns still reference DS addresses drawn from across the whole pool, so both NICs are exercised once the routes are split evenly.
One caveat is worth noting up front: Split the data pool you actually mount through, not the management pool, and not a different data pool. OneFS returns roughly one DS address per node drawn from the pool your mount uses. If the cluster has several data pools on different ranges, mounting through one pool returns only that pool’s DS addresses, so splitting a different range accomplishes nothing.
Prerequisites
Before making any changes, confirm that all the following client, cluster, and networking prerequisites are met.
On the Linux client
| Client Prerequisite | Details |
| Operating system | A recent enterprise Linux distribution with NFS/RDMA, pNFS flex-files, and nconnect support, for example RHEL 8.x/9.x, Ubuntu 20.04+, or SLES 15 SP3+. |
| Installed packages | The ‘rdma-core’ and ‘nfs-utils’ packages installed. |
| Network interfaces | Two client NICs, each with its own IP address and a working route to the cluster data subnet. |
| Access control | Root (or sudo) access. |
| RDMA-only configuration | Confirm each NIC’s RDMA device mapping with rdma link show (for example, mlx5_0 maps to eth0). |
On the PowerScale cluster:
| Cluster Prerequisite | Details |
| OneFS version | OneFS 9.15 or later. |
| Network interfaces | RDMA-capable front-end interfaces. |
| NFS configuration | NFS over RDMA enabled and pNFS flex-files enabled on the export. |
Verify that NFS over RDMA is enabled on the cluster:
# isi nfs settings global view | grep -i rdma
On the network:
| Network Prerequisite | Details |
| Transport | A RoCEv2 fabric with lossless Ethernet (PFC/ECN) configured end-to-end. |
| Routing | One subnet per client NIC is recommended for clean routing, with a reachable gateway on each NIC’s subnet toward the cluster pool.. |
The commands throughout this article use the example values below. Substitute your own environment’s IP addresses and paths consistently, as appropriate.
The pool is split in half: For example, .81 to .100 via NIC 1, and .101 to .120 via NIC 2.
| Item | Value | Notes |
| Cluster data subnet | 192.168.100.0/24 | Where the NFS pool lives |
| Network pool range | 192.168.100.81 to .120 (40 IPs) | Any one IP is the mount target; all are potential DS IPs |
| NIC 1 | eth0, 10.10.1.11, gw 10.10.1.254, mlx5_0 | First client NIC |
| NIC 2 | eth1, 10.10.2.11, gw 10.10.2.254, mlx5_1 | Second client NIC |
| Export path | /ifs/data | NFS export on the cluster |
| Mount point | /mnt/pscale | Local directory on the client |
The following NFS mount options are used throughout:
| Option | Value | Purpose |
| proto | rdma | Use RDMA transport (use tcp for the TCP variant) |
| port | 20049 | Standard NFS-over-RDMA port |
| vers | 4.1 | NFSv4.1, required for pNFS flex-files |
| rsize / wsize | 1048576 | 1 MB I/O buffers (a good default for RDMA) |
| nconnect | 4 | Parallel connections per DS IP; start at 4 and raise only if a single DS connection bottlenecks |
Deployment procedure
The PowerScale pNFS NIC trunking deployment procedure comprises eight steps, of which all are performed on the Linux client, except Step 1, which queries the cluster.

The recommended path is to validate first with temporary routes (Step 3, Option A), confirm the split is working (Steps 4 through 8), and only then make the routes permanent (Step 3, Option B). Specifically:
Step 1 – Confirm the cluster pool
On the cluster, examine the SmartConnect static network pool to obtain the exact IP range and gateway that the client will route across its two NICs:
# isi network pools list # isi network subnets list
You will mount one address from this range (or the pool’s SmartConnect DNS name, which resolves to one of them) and route the entire range across your two NICs. Use the NFS data pool, not the management pool. A management or pool0 address may answer ping and even accept a TCP connection on port 2049, but mounts through it hang and never produce data layouts.
Step 2 – Back up the current network configuration
Before changing anything on the Linux pNFS client, capture the runtime routing state so you can restore it (all distributions):
mkdir -p /root/net-backup-$(date +%F) ip route show > /root/net-backup-$(date +%F)/routes.txt ip rule show > /root/net-backup-$(date +%F)/rules.txt
Also back up the persistent network configuration for your distribution. This is only required if using Option B (permanent routes) in Step 3. For NetworkManager (RHEL, Rocky, SLES), profiles live in /etc/NetworkManager/system-connections/ (keyfile) or /etc/sysconfig/network-scripts/ (ifcfg); for netplan (Ubuntu), back up /etc/netplan:
# Ubuntu / netplan example cp -a /etc/netplan /root/net-backup-$(date +%F)/
Step 3 – Add the split routes
Add a host-specific (/32) route for every pool address, sending half to each NIC. There are two ways to accomplish this, so select one of the following methods as appropriate:
Option A: Temporary Configuration (runtime only, for testing).
These routes take effect immediately but to not persist a reboot, which makes them ideal for validating the configuration before committing. Run as root on the Linux client:
# .81–.100 via NIC 1
for id in {81..100}; do
ip route replace 192.168.100.$id via 10.10.1.254 dev eth0 src 10.10.1.11
done
# .101–.120 via NIC 2
for id in {101..120}; do
ip route replace 192.168.100.$id via 10.10.2.254 dev eth1 src 10.10.2.11
done
ip route flush cache
The ‘src’ attribute above pins the connection’s source address to the egress NIC. It is required for RDMA (the RoCE source GID must belong to the egress device) and keeps the path symmetrical. The commands are safe to re-run because the ‘ip route replace’ command is idempotent,.
Option B: Permanent Configuration.
Store the routes declaratively so the network stack reapplies them on boot or NIC bounce. Do not combine methods for the same addresses.
NetworkManager (RHEL, Rocky, SLES): add the routes with the ‘nmcli’ CLI command, using the connection name (from ‘nmcli connection show’), which often differs from the device name:
nmcli connection show # note the NAME column (may differ from DEVICE)
# NIC 1 connection
for id in {81..100}; do
nmcli connection modify eth0 \
+ipv4.routes "192.168.100.$id/32 10.10.1.254 src=10.10.1.11"
done
# NIC 2 connection (name may be e.g. "Wired connection 1")
for id in {101..120}; do
nmcli connection modify "Wired connection 1" \
+ipv4.routes "192.168.100.$id/32 10.10.2.254 src=10.10.2.11"
done
nmcli connection up eth0
nmcli connection up "Wired connection 1"
Note that the ‘nmcli connection up’ command briefly bounces the interface and drops any SSH session running over that NIC. Run the connection activation from the console/BMC, or detached (for example, wrap it in ‘nohup … &’), and do not chain the two commands with ‘&&’ over a live SSH session on the first NIC.
For Ubuntu linux using the ‘netplan’ CLI command, add routes under each interface in the netplan YAML, then run the ‘sudo netplan apply’ command. A route placed under an interface egresses that interface, and uses its IP as the source automatically, so no explicit ‘src’ is required. Validate with the ‘sudo netplan generate’ command before applying:
network: version: 2 ethernets: eth0: routes: - to: 192.168.100.81/32 via: 10.10.1.254 # ... one entry per .81–.100 eth1: routes: - to: 192.168.100.101/32 via: 10.10.2.254 # ... one entry per .101–.120
An alternative, Linux OS-agnostic option uses the ‘systemd oneshot’ service. For mixed client fleets, or when a single approach across all hosts is preferred, this small oneshot service replays the ‘Option A’ commands at boot. It is the most transparent method, and the safest to apply remotely, because it never reconfigures a NIC’s address and so never bounces a link:
# /usr/local/sbin/nfs-trunk-routes.sh
#!/bin/bash
set -e
for id in {81..100}; do
ip route replace 192.168.100.$id via 10.10.1.254 dev eth0 src 10.10.1.11
done
for id in {101..120}; do
ip route replace 192.168.100.$id via 10.10.2.254 dev eth1 src 10.10.2.11
done
ip route flush cache
# /etc/systemd/system/nfs-trunk-routes.service
[Unit]
Description=Split NFS pool routes for NIC trunking
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/local/sbin/nfs-trunk-routes.sh
[Install]
WantedBy=multi-user.target
chmod +x /usr/local/sbin/nfs-trunk-routes.sh
systemctl daemon-reload
systemctl enable --now nfs-trunk-routes.service
systemctl status nfs-trunk-routes.service --no-pager # expect: active (exited)
An ‘on-link pool’ (no gateway) is a special case. If the cluster pool is in the same subnet as the client NICs, shown by the ‘ip route show’ CLI command as a proto kernel scope link route with no via, omit the gateway (for example, ‘ip route replace 192.168.100.81 dev eth0 src 10.10.1.11’, or, in netplan, use ‘scope: link’ with no via). When the pool is reached through a router, keep the gateway.
Step 4 – Verify the split
On the client, confirm that the boundary addresses resolve to the expected interfaces:
ip route get 192.168.100.81 # expect: dev eth0 src 10.10.1.11 ip route get 192.168.100.100 # expect: dev eth0 src 10.10.1.11 ip route get 192.168.100.101 # expect: dev eth1 src 10.10.2.11 ip route get 192.168.100.120 # expect: dev eth1 src 10.10.2.11
Or check the whole pool at once:
for id in {81..120}; do ip route get 192.168.100.$id | head -1; done
Using this example configuration, addresses .81 to .100 should all show eth0, and .101 to .120 should all show eth1.
Step 5 – Connectivity check (optional)
A quick reachability check before mounting. Skip it if you prefer to go straight to Step 6, since the mount plus the verification in Steps 7 and 8 is the definitive end-to-end test:
# RDMA only: both devices should be ACTIVE / LINK_UP rdma link show # reach one IP from each half over its intended NIC ping -c2 -I eth0 192.168.100.81 # path via NIC 1 ping -c2 -I eth1 192.168.100.101 # path via NIC 2
A ping failure on one NIC means that path is down, so check link state, switch/VLAN configuration, and gateway reachability (for example, ‘arping -I eth1 10.10.2.254’).
Step 6 – Mount
Mount one pool address as the MDS, using nconnect to set the number of connections opened per DS address. For RDMA:
mount -v -o proto=rdma,port=20049,vers=4.1,rsize=1048576,wsize=1048576,nconnect=4 \ 192.168.100.81:/ifs/data /mnt/pscale/
For the TCP variant, replace ‘proto=rdma,port=20049’ with ‘proto=tcp’. The mount confirms the source address it selected, and the reported ‘clientaddr’ should match your NIC 1 IP (10.10.1.11). Immediately after mounting, and before any I/O, only the MDS address appears in the transport table, which is expected, because DS connections are created once I/O triggers LAYOUTGET.
To remount automatically on boot, add an ‘fstab’ entry on the client (_netdev’ defers the mount until the network is up):
192.168.100.81:/ifs/data /mnt/pscale nfs4 \ proto=rdma,port=20049,vers=4.1,rsize=1048576,wsize=1048576,nconnect=4,_netdev 0 0
Step 7 – Verify trunking: transport connections
Generate a small amount of I/O (for example, a ‘find’, or a short ‘fio’ or ‘dd’ job) to trigger layout retrieval, then count the transport connections per address:
grep -h 'addr:' /sys/kernel/debug/sunrpc/rpc_xprt/*/info \
| awk '{print $2}' | sort -t. -k4 -n | uniq -c
Healthy trunking shows every participating address with ‘nconnect’ (here, 4) connections: the MDS plus DS addresses from both halves of the pool:
4 192.168.100.81 <- MDS (NIC 1 range) 4 192.168.100.90 <- DS (NIC 1 range) 4 192.168.100.101 <- DS (NIC 2 range) 4 192.168.100.118 <- DS (NIC 2 range) ...
If only .81 to .100 appear, the NIC 2 routes are wrong, so recheck Step 4. Entries for the cluster management address (port 2049) and the local rpcbind socket are unrelated to data I/O and can be ignored.
Step 8 – Verify trunking: RDMA queue pairs (RDMA only)
For RDMA configurations, confirm that queue pairs (QPs) exist on both RDMA devices:
rdma resource show qp link mlx5_0 # NIC 1 rdma resource show qp link mlx5_1 # NIC 2
Both devices must return QPs, and each should carry roughly half of the total. For a 40-address pool with nconnect=4, that is about 20 × 4 = ~80 QPs per device. QPs on only one device mean all traffic is on one NIC, so recheck the routes for the other half. If you validated with temporary routes (Option A) and everything checks out, return to Step 3, Option B to make the routes permanent.
Rollback and restore
To return to the original configuration, roll back using the same method you applied (replace the backup date with the date of your Step 2 backup directory).
Temporary routes (Option A, all distributions): delete the host routes, or simply reboot:
for id in {81..120}; do ip route del 192.168.100.$id; done
ip route flush cache
NetworkManager: remove exactly the routes you added, using -ipv4.routes:
# NIC 1 connection
for id in {81..100}; do
nmcli connection modify eth0 \
-ipv4.routes "192.168.100.$id/32 10.10.1.254 src=10.10.1.11"
done
# NIC 2 connection (use the same NAME you used to add the routes)
for id in {101..120}; do
nmcli connection modify "Wired connection 1" \
-ipv4.routes "192.168.100.$id/32 10.10.2.254 src=10.10.2.11"
done
nmcli connection reload
nmcli connection up eth0
nmcli connection up "Wired connection 1"
Do not rely on restoring the keyfile backup alone: If a NIC had no saved profile when you backed up, Option B created a new keyfile for it, and a plain cp -a restore never deletes that new file, so those routes survive the rollback. Use the -ipv4.routes method above, then verify none remain with ‘ip route show | grep /32’.
Netplan (Ubuntu): Restore the backed-up netplan, then apply. If you added a separate file, delete it first (the backup will not contain it):
rm -f /etc/netplan/90-pnfs-trunking.yaml # only if you created a separate file cp -a /root/net-backup-YYYY-MM-DD/netplan/* /etc/netplan/ sudo netplan apply
Systemd service (all distributions): Disable the unit, remove its files, then clear the live routes (or reboot):
systemctl disable --now nfs-trunk-routes.service
rm -f /etc/systemd/system/nfs-trunk-routes.service \
/usr/local/sbin/nfs-trunk-routes.sh
systemctl daemon-reload
for id in {81..120}; do ip route del 192.168.100.$id; done
ip route flush cache
Troubleshooting
| Symptom | Likely cause | Resolution |
| Ping or mount hangs on one NIC’s path | No working path via that NIC’s gateway | Check link, switch/VLAN, and gateway (arping -I eth1 10.10.2.254) |
| Mount hangs though ping and port are reachable | Mounting the wrong pool (management, not data) | Confirm the IP is in the NFS data pool (isi network pools list); mount a data-pool IP |
| Only NIC 1 addresses in the transport table | NIC 2 host routes missing or wrong | Recheck ip route get 192.168.100.101 |
| No QPs on mlx5_1 | RDMA device not mapped to NIC 2 | rdma link show; confirm device-to-netdev mapping |
| Transport table shows only the MDS IP after I/O | pNFS flex-files inactive, or no real I/O performed | Confirm flex-files is enabled on the export; perform real reads/writes |
| ip route get shows the wrong NIC | Route cache stale | ip route flush cache |
| SSH session drops during Option B apply/rollback | nmcli connection up bounced the NIC carrying your session | Run the activation detached (nohup … &) or from console/BMC |
| Routes still present after Option B rollback | cp -a keyfile restore did not delete an NM-created profile | Use the -ipv4.routes rollback method; verify with ip route show |
| ip route replace permission error | Not root | Use sudo |
The following recommended practices can help ensure a reliable, maintainable NIC-trunking deployment, regardless of the persistence method or transport (RDMA or TCP):
- Gateways: reuse the existing gateway each NIC already uses for the cluster subnet (from ip route show) rather than creating new ones.
- Source pinning: the src attribute pins the source address to the egress NIC. It is required for RDMA and keeps the path symmetric.
- Uneven pools: adjust the pool ranges so roughly half of the addresses go to each NIC. The split need not be exactly even.
- nconnect: start at nconnect=4 and raise it only if a single DS connection becomes a bottleneck.
- Verify before permanence: validate with temporary routes, confirm with the checks in Steps 7 and 8, and only then make the configuration permanent. Always keep a tested rollback option.
So, in summary, client-side NIC trunking is an efficient, fully reversible way to unlock the aggregate bandwidth of two client interfaces for pNFS flex-files on a PowerScale cluster running OneFS 9.15 or later. Because the technique relies only on host-specific routes and an nconnect mount, it requires no cluster-side change and applies equally to RDMA and TCP. Following the procedure above (confirm the correct data pool, back up first, split the routes, verify with the transport and queue-pair checks, and persist with a tested rollback in place) delivers a single mount that drives data traffic across both NICs.





















