Symptoms of File Descriptor Exhaustion

866 단어·2 분·원문(.md)

FD exhaustion is a phenomenon encountered when dealing with high-performance servers or large-scale data.

File Descriptor #

A file descriptor is an integer handle for accessing abstract resources. Following the Unix/Linux philosophy of "everything is a file," all I/O resources such as sockets, pipes, and devices, not just regular files, are managed through FDs.

Kernel's Internal 3-Stage Mapping Structure #

When a process opens a file, the kernel doesn't just assign an ID; it creates a complex table structure.

  1. process descriptor (task struct): Each process has its own FD table. Here, FD values 0, 1, and 2 act as array indices.
  2. open file table (system-wide): This table is shared by all processes and stores the state of how a file is opened, such as its offset, read/write permissions, etc.
  3. Inode table(Vnode Table): This contains information about actual file data on disk or whatever hardware devices are connected.

Symptoms of File Descriptor Exhaustion #

When a system or process reaches the upper limit of FDs it can be allocated, it becomes unable to acquire new resources, leading to service unavailability.

Software-level Errors: EMFILE, ENFILE #

  • EMFILE (Too many open files): Occurs when a single process has exhausted its allocated per-process limit.
  • ENFILE (File table overflow): Occurs when the system-wide limit for the maximum number of FDs available to the entire system is reached. This is a dangerous signal that can paralyze the entire system.

These can be distinguished at the process level and the system-wide level.

Network Service Interruption: Accept Failures #

This is a critical phenomenon in server applications.

  • When a new client attempts to connect, the kernel must create a new socket FD via accept().
  • If FDs are exhausted, accept() will fail, causing clients to experience "connection refused" or indefinite waiting. Existing connections are maintained, but new incoming connections are blocked.

  • Alternatively, if an error occurs and there are no FDs available to open the log file itself, it can lead to log omissions and a "silent failure" state where debugging becomes impossible, making root cause analysis difficult.

Pipe and IPC communication failures can also occur, as the creation of Pipes or Eventfds for inter-process communication becomes impossible. In multi-process/thread environments, this can halt synchronization or data transfer, making the application appear like a zombie or in a deadlock state.


Managing FD Limits #

In Linux systems, there are ways to check and control these limits.

ulimit -Sn # Recommended limit currently available to a process

ulimit -Hn # Maximum limit that can only be modified with root privileges (S Soft, H Hard)

cat /proc/sys/fs/file-max # Maximum number of FDs that can be opened system-wide by the kernel

lsof -p <pid> | wc -l # Check the number of FDs used by a specific process

Let's look at an example.

For instance, if your application logs show:

java.io.IOException: Too many open files
# 혹은
Accept error: errno = 24 (EMFILE)

Let's say you see logs like these. We'll analyze them.

First, you need to check the capacity of your system.

$ ulimit -Sn  # Soft limit: currently applied limit
1024
$ ulimit -Hn  # Hard limit: maximum expandable limit
4096

This process is currently in a situation where it can only open up to 1024 FDs. For a server-grade system, 1024 is a relatively small number, so it should be set to tens of thousands.

Next, identify the culprit process.

Find which process is consuming all the FDs.

$ lsof | awk '{print $2}' | sort | uniq -c | sort -nr | head -n 5
   1020 12345  # PID 12345 is using 1,020 (almost at its limit!)
    150 6789
     45 1

Detailed analysis of a specific process is also possible, as shown below.

$ ls /proc/12345/fd | wc -l
1020

Here, with a ulimit of 1024, process 12345 is using 1020 FDs, indicating that this process will soon crash or be unable to accept new requests.

Finally, determine if this is simply due to a high number of users or if there's a code error causing an FD leak.

$ lsof -p 12345
COMMAND  PID  USER   FD   TYPE  DEVICE SIZE/OFF    NODE NAME
java    12345 user   10u  IPv4  123456      0t0     TCP *:8080 (LISTEN)
java    12345 user   11u  REG    8,1     4096  789012 /tmp/tempfile.tmp (deleted)
java    12345 user   12u  REG    8,1     4096  789013 /tmp/tempfile.tmp (deleted)
... (수백 개 반복) ...

The "deleted" tag next to the filename indicates a typical resource leak: the program deleted the file but failed to call close(), so the kernel is still holding onto the FD.

The immediate temporary solution is, of course, to increase the process limit, using prlimit for this.

# Change both Soft/Hard limits for PID 12345 to 65535
$ sudo prlimit --pid 12345 --nofile=65535:65535

This change won't persist after a server reboot. To make it permanent, you need to modify /etc/security/limits.conf.

$ sudo vi /etc/security/limits.conf

# Add to the bottom
* soft    nofile    65535
* hard    nofile    65535

Fundamentally, you should find the leak point identified by lsof and analyze whether resource-closing code, like try-with-resources (Java) or defer file.Close() (Go), is missing, or if there's a similar issue in any open-source libraries being used, and then resolve it.

SRE/question/q_20.md