https://bugs.freebsd.org/bugzilla/show_bug.cgi?id=298534

            Bug ID: 298534
           Summary: nfsd: ERELOOKUP retry leaks srvstartcnt, so nfsstat -d
                    "ql" grows forever and "%b" sticks at 100
           Product: Base System
           Version: 15.1-RELEASE
          Hardware: Any
                OS: Any
            Status: New
          Severity: Affects Some People
          Priority: ---
         Component: kern
          Assignee: [email protected]
          Reporter: [email protected]

sys/fs/nfsserver/nfs_nfsdsocket.c pairs nfsrvd_statstart() with
nfsrvd_statend() around every operation. The ERELOOKUP retry paths added in
774a368 ("nfsd: fix NFS server for ERELOOKUP", D27875) break that pairing:
both "goto tryagain" jumps happen after statstart() and before statend(), and
the retried attempt calls statstart() again. Each retry therefore increments
srvstartcnt (and srvrpccnt[op]) twice but srvdonecnt once.

  NFSv3 path, nfsrvd_dorpc():
    nfsrvd_statstart(nfsv3to4op[nd->nd_procnum], &start_time);
    ... error == 0 && nd->nd_repstat == ERELOOKUP -> goto tryagain;   /* no
statend */
    nfsrvd_statend(...);

  NFSv4 path, nfsrvd_compound():
    nfsrvd_statstart(op, &start_time); statsinprog = 1;
    ... nd->nd_repstat == ERELOOKUP -> goto tryagain;                  /* no
statend */
    if (statsinprog != 0) nfsrvd_statend(...);

Both are unchanged in main as of 2026-09 (only the NFSD_VNET -> VNET macro
rename touched those lines).

Two user-visible consequences, because nfsrvd_statstart() only resets
busyfrom when srvstartcnt == srvdonecnt:

1. The gap srvstartcnt - srvdonecnt, which nfsstat(1) reports as "ql" in
   -d mode, becomes a monotonically growing count of retries rather than the
   in-flight queue length. On a busy server it never returns to zero.

2. Once the gap is non-zero, busytime accumulates every interval between
   consecutive statend() calls, i.e. all wall-clock time while any traffic
   flows. nfsstat -d "%b" then reads 100 as long as there is at least one
   op per sample, regardless of actual load.

srvops[], srvbytes[] and srvduration[] are only updated in statend(), so
per-op counts, throughput and latency remain correct; srvrpccnt[op] is
over-counted by one per retry.

How to reproduce:

On a FreeBSD 13.0 or later NFS server exporting ZFS (or UFS after r367672)
with clients that create/remove/rename concurrently with lookups in the same
directories, run:

  nfsstat -d -w 1

Watch "ql": it steps up by one each time an operation hits ERELOOKUP and
never steps back down once the server is idle. Once ql is non-zero, "%b"
reads ~100 whenever any request is being served.

Observed on 15.1-RELEASE-p1 serving a ZFS pool to ~11 NFSv4 clients (mostly
GetAttr/Lookup/Access, ~15 namespace-changing ops/s), 48 days after boot:

  # nfsstat -d -w 1
   [===== Read =====]  [===== Write ====]  [=========== Total ============]
   KB/t   tps    MB/s  KB/t   tps    MB/s  KB/t   tps    MB/s    ms  ql  %b
  114.60  2108  235.90 38.02     6    0.22 22.33 10828  236.13  0.22 3784 100
  106.38  2005  208.25  2.89     9    0.03 18.24 11692  208.28  0.39 3793 100
  102.97  2132  214.38  4.87     4    0.02 16.69 13156  214.40  0.45 3782 100
  108.32  1563  165.33 12.02    15    0.18 17.16  9878  165.50  0.33 3782 100

"ql" cannot be a real queue: it never drops below ~3,780, while mean latency
is well under a millisecond. Sampling srvstartcnt - srvdonecnt once a minute
shows it climbing from 0 at boot to ~3,780 over 48 days, 30-80 per day,
tracking the rate of concurrent rename/remove activity. A second, nearly
idle, 15.1-RELEASE-p3 server shows ql 0 and a sane %b. Third-party consumers
of nfsstatsv1 (e.g. net-mgmt/nfs-exporter, which publishes
nfs_nfsd_start_count, nfs_nfsd_done_count and nfs_nfsd_busytime) show the
same drift.

Suggested fix:

Close the accounting before retrying, in both paths. For nfsrvd_dorpc():

    if (error == 0 && nd->nd_repstat == ERELOOKUP) {
+       nfsrvd_statend(nfsv3to4op[nd->nd_procnum], /*bytes*/ 0,
+           /*now*/ NULL, /*then*/ &start_time);
        /* Roll back to the beginning of the RPC request arguments. */
        ...
        goto tryagain;
    }

For nfsrvd_compound():

    if (nd->nd_repstat == ERELOOKUP) {
+       if (statsinprog != 0) {
+           nfsrvd_statend(op, /*bytes*/ 0, /*now*/ NULL,
+               /*then*/ &start_time);
+           statsinprog = 0;
+       }
        /* Roll back to the beginning of the operation arguments. */
        ...
        goto tryagain;
    }

This counts a retried operation as two completed attempts (matching the two
starts), keeps ql meaningful and lets busyfrom reset. An alternative that
keeps srvrpccnt at one per retried op would be to decrement srvstartcnt and
srvrpccnt[op] under nfsrvd_statmtx before the retry instead.

-- 
You are receiving this mail because:
You are the assignee for the bug.

Reply via email to