1

Test program:

#include <fcntl.h>
#include <unistd.h>
#include <string>
#include <stdexcept>
#include <cstring>
#include <iostream>

int main(int argc, char* argv[]) {
    if (argc != 2) {
        std::cerr << "Usage: " << argv[0] << " <filename>\n";
        return 1;
    }

    const std::string filename = argv[1];
    const int numIterations = 10000;

    for (int i = 0; i < numIterations; ++i) {
        int fd = open(filename.c_str(), O_RDONLY);

        if (fd == -1) {
            throw std::runtime_error("Failed to open " + filename + " (Iteration " + std::to_string(i + 1) + "): " + strerror(errno));
        }

        if (close(fd) == -1) {
            throw std::runtime_error("Failed to close " + filename + " (Iteration " + std::to_string(i + 1) + "): " + strerror(errno));
        }
    }

    return 0;
}
g++ -O2 test.cpp -o test_open_close

Run a single program:

$ time /tmp/test_open_close /mnt/nfs_mnt/xxxxxx

real    0m0.115s
user    0m0.003s
sys 0m0.066s

Use a wrapper script to run this program in parallel:

#!/bin/bash

# The command to be executed
command="/tmp/test_open_close /mnt/nfs_mnt/xxxxxx"

# Number of concurrent executions
num_executions=360

# Function to execute the command and capture its exit status
run_command() {
    $command 2>/dev/null
}

# Start time
start_time=$(date +%s.%N)

# Run the commands concurrently
for i in $(seq 1 $num_executions); do
    run_command &
done

# Wait for all background processes to finish
wait

# End time
end_time=$(date +%s.%N)

# Calculate duration
duration=$(echo "$end_time - $start_time" | bc)

echo "Executed $num_executions processes in $duration seconds"
$ bash /tmp/test.sh
Executed 360 processes in 35.127017601 seconds

The total used time is almost the same as run them in serial.


There's another strange thing. During the test.sh script is running, I run time /tmp/test_open_close /mnt/nfs_mnt/xxxxxx in another shell, and the used time is different when using different users.

  1. Use the same user as test.sh script.
$ time /tmp/test_open_close /mnt/nfs_mnt/xxxxxx

real    0m22.753s
user    0m0.005s
sys 0m0.121s
  1. Use a different user.
$ time /tmp/test_open_close /mnt/nfs_mnt/xxxxxx

real    0m0.197s
user    0m0.000s
sys 0m0.158s
  1. Use the same user but on different host.
$ time /tmp/test_open_close /mnt/nfs_mnt/xxxxxx

real    0m3.055s
user    0m0.003s
sys 0m0.227s

System enviroment:

Rocky 8.8, 4.18.0.

The test host has 378 cores.


My problem is:

  1. What the bottleneck is in this case? The lock in NFS client?

  2. Why different user has different behavior?

  3. How to optimization the performance in this case?

1
  • Case 2 is probably finding the content in local cache. Commented Sep 24, 2024 at 9:51

1 Answer 1

0
  1. NFS is not cache coherent (as usually defined), but instead uses a weak form called close-to-open (CTO) consistency. There is no way for the NFS client to have some kind of exclusive lock on a directory and then do whatever it likes locally in the cache. That means when opening a file the client must contact the server and check the cached status of the file. Similarly, when closing a file it kind of implies a fsync() to the server. See https://datatracker.ietf.org/doc/html/rfc7530#section-10.3
  2. I suspect this is some kind of balancing between different users the NFS client is doing. Similarly when trying from a different host, the NFS server balances requests between the different hosts.
  3. Don't do a lot of small file operations on NFS. If you need synchronized access to some particular piece of data shared concurrently among multiple clients, consider using a database.
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.