File descriptor, pipe, signal và IPC

4 câu hỏi~25 phútTừ khóa: file descriptor, Too many open files, ulimit -n, LimitNOFILE, /proc/PID/fd, lsof, pipe, signal, SIGTERM, SIGKILL, SIGQUIT, kill -3, IPC, Unix socket

Tiếp đi: file descriptor. order-service chạy 2 ngày thì log toàn java.io.IOException: Too many open files, gọi API nào cũng lỗi. App mình có mở file nào đâu, chỉ gọi HTTP sang payment-service và query DB thôi.

Ở Linux, "file" rộng hơn bạn nghĩ: socket TCP, pipe, file log, file jar… đều là file descriptor. Mỗi connection HTTP sang payment-service là một fd.

1. Bài toán

journalctl -u order-service | tail
java.io.IOException: Too many open files
    at java.base/sun.nio.ch.Net.accept(...)
    at org.apache.tomcat.util.net.NioEndpoint...

Tomcat không accept thêm được connection, client nhận lỗi. Restart thì hết, 2 ngày sau lại bị.

2. Vấn đề

3. Giải pháp: hiểu file descriptor

4. Code từng bước

Bước 1: Đếm fd và xem loại

PID=$(pgrep -f order/app.jar)
ls /proc/$PID/fd | wc -l                 # 1021
grep 'open files' /proc/$PID/limits      # Max open files 1024 1024
ls -l /proc/$PID/fd | awk '{print $NF}' | sed 's/:.*//' | sort | uniq -c
    40 /opt/order/...
   960 socket
lsof -p $PID -a -i TCP | awk '{print $NF}' | sort | uniq -c
   930 (CLOSE_WAIT)          ← phía kia đã đóng, app chưa close()

Nhiều CLOSE_WAIT = app quên đóng socket (chi tiết trạng thái TCP ở 9.11).

Bước 2: Tìm code rò rỉ

// Sai: tạo client mới mỗi lần, không đóng response
public PaymentResult charge(ChargeRequest req) throws IOException {
    CloseableHttpClient client = HttpClients.createDefault();
    CloseableHttpResponse res = client.execute(new HttpPost(paymentUrl));
    return mapper.readValue(res.getEntity().getContent(), PaymentResult.class);
}
// Đúng: một RestClient dùng chung (pool connection), Spring tự đóng body
@Configuration
class PaymentClientConfig {
    @Bean
    RestClient paymentClient(RestClient.Builder builder,
                             @Value("${payment.base-url}") String baseUrl) {
        return builder.baseUrl(baseUrl).build();
    }
}

@Service
class PaymentGateway {
    private final RestClient paymentClient;
    PaymentGateway(RestClient paymentClient) { this.paymentClient = paymentClient; }

    PaymentResult charge(ChargeRequest req) {
        return paymentClient.post().uri("/charges").body(req)
                .retrieve().body(PaymentResult.class);
    }
}

// Đọc file: luôn try-with-resources
try (var lines = Files.lines(Path.of("/opt/order/import.csv"))) {
    lines.forEach(this::importLine);
}

Bước 3: Nâng limit đúng chỗ

# systemd service: ulimit trong /etc/security/limits.conf KHÔNG áp dụng cho service
[Service]
LimitNOFILE=65536
sudo systemctl daemon-reload && sudo systemctl restart order-service
grep 'open files' /proc/$(pgrep -f order/app.jar)/limits

Docker: --ulimit nofile=65536:65536. JVM tự nâng soft lên hard khi khởi động (-XX:+MaxFDLimit, mặc định bật trên Linux).

Bước 4: Theo dõi

curl -s localhost:8080/actuator/metrics/process.files.open | jq .measurements
curl -s localhost:8080/actuator/metrics/process.files.max  | jq .measurements

Alert khi open/max > 80%, và khi số fd tăng đều theo thời gian (dấu hiệu rò rỉ).

5. Cách hoạt động

Process java (PID 4123)          Kernel
fd table                          open file table / socket
 0 ─────────────────────────────► /dev/null
 1 ─────────────────────────────► journald (pipe/socket)
 2 ─────────────────────────────► journald
 3 ─────────────────────────────► /opt/order/app.jar
 45 ────────────────────────────► socket :8080 LISTEN
 46 ────────────────────────────► epoll (Tomcat NIO)
 47 ────────────────────────────► socket → postgres:5432 (Hikari)
 ...
 1023 ──────────────────────────► socket → payment:443 CLOSE_WAIT (rò rỉ)
 1024 ✗ open()/accept() trả EMFILE → "Too many open files"

6. So sánh: các cách process nói chuyện với nhau (IPC)

Cơ chếĐặc điểmVí dụ
Pipe |Một chiều, cha–con, luồng bytejstack PID | grep BLOCKED; Java Process.getInputStream()
Named pipe (FIFO)Pipe có tên trên filesystemÍt dùng trong app Java
SignalBáo hiệu, không mang dữ liệuSIGTERM để tắt, SIGQUIT để thread dump
Unix domain socketNhư TCP nhưng trong một máy, nhanh hơn/var/run/docker.sock, Postgres local, jcmd attach JVM
TCP socketQua mạngorder → payment, order → Postgres
Shared memoryNhanh nhất, phải tự đồng bộPostgres shared_buffers, file hsperfdata mà jps đọc

7. Khi nào nên dùng / không nên dùng

Signal thì mình chỉ biết kill -9. kill, kill -9 và kill -3 khác gì nhau với order-service?

Signal là một "thông báo" kernel gửi tới process. Process có thể bắt (handle), bỏ qua, hoặc để hành động mặc định chạy. Riêng vài signal không bắt được.

LệnhSignalBắt được?JVM làm gì
kill PIDSIGTERM (15)CóChạy shutdown hook → Spring graceful shutdown → thoát mã 143
kill -9 PIDSIGKILL (9)KhôngChết ngay, không hook, không flush log; mã 137
kill -3 PIDSIGQUIT (3)CóIn thread dump ra stdout, app chạy tiếp
Ctrl+CSIGINT (2)CóNhư SIGTERM
kill -STOP / -CONTSIGSTOP / SIGCONTKhôngĐóng băng / chạy lại toàn bộ process

Với systemd, thread dump từ kill -3 nằm trong journal:

sudo kill -3 $(pgrep -f order/app.jar)
journalctl -u order-service --since "1 min ago" | grep -A20 '"http-nio-8080-exec'

Hoặc dùng jcmd PID Thread.print để nhận dump trực tiếp (cách đọc dump ở 2.6). Cách JVM xử lý SIGTERM khi là PID 1 trong container đã có ở 16.6.

Thói quen tốt: luôn kill (SIGTERM) trước, đợi; chỉ kill -9 khi process không phản hồi.

Mình đặt ulimit -n 65536 trong .bashrc và limits.conf rồi, sao /proc/PID/limits của order-service vẫn 1024?

Đúng chỗ bạn bị vướng: limit được kế thừa từ process cha lúc fork.

Và limit được đọc lúc process khởi động: sửa xong phải restart service. Luôn kiểm tra bằng /proc/PID/limits của chính process, không phải ulimit -n trong shell của bạn.

Phỏng vấn hỏi file descriptor và signal thì trả lời sao?

Q1. "Too many open files" là gì, xử lý thế nào?

🇻🇳 Tiếng Việt:

Process đã dùng hết số file descriptor cho phép. Socket cũng là fd, nên app chỉ gọi HTTP và DB vẫn có thể gặp. Em kiểm tra /proc/PID/fd và lsof: nếu fd tăng đều và có nhiều CLOSE_WAIT thì code đang rò rỉ connection, phải sửa bằng client dùng chung và try-with-resources. Sau đó mới nâng LimitNOFILE cho hợp lý và thêm alert.

🇬🇧 English:

The process has used up its file descriptor limit. Sockets are file descriptors too, so an app that only makes HTTP and database calls can still hit it. I check /proc/PID/fd and lsof: if the count keeps growing with many CLOSE_WAIT sockets, the code is leaking connections and I fix it with a shared client and try-with-resources. Then I raise LimitNOFILE to a sensible value and add an alert.

Q2. SIGTERM, SIGKILL và SIGQUIT khác nhau thế nào với JVM?

🇻🇳 Tiếng Việt:

SIGTERM bắt được: JVM chạy shutdown hook, Spring tắt graceful. SIGKILL không bắt được: process chết ngay, không cleanup, exit code 137. SIGQUIT, tức kill -3, làm JVM in thread dump ra stdout và vẫn chạy tiếp, dùng để chẩn đoán.

🇬🇧 English:

SIGTERM can be handled: the JVM runs shutdown hooks and Spring shuts down gracefully. SIGKILL cannot be handled: the process dies immediately without cleanup and exits with 137. SIGQUIT, which is kill -3, makes the JVM print a thread dump to stdout and keep running, so it is used for diagnostics.

Q3. Kể vài cơ chế IPC trên Linux.

🇻🇳 Tiếng Việt:

Pipe giữa cha và con, signal để báo hiệu, Unix domain socket trong cùng máy, TCP socket qua mạng, và shared memory nhanh nhất nhưng phải tự đồng bộ. Giữa các microservice em dùng TCP với HTTP hoặc message broker; Unix socket gặp ở Docker daemon hoặc khi jcmd attach vào JVM.

🇬🇧 English:

Pipes between parent and child, signals for notifications, Unix domain sockets on the same host, TCP sockets across the network, and shared memory, which is fastest but needs its own synchronization. Between microservices I use TCP with HTTP or a message broker; Unix sockets show up with the Docker daemon or when jcmd attaches to a JVM.

Cách nhớ ngắn gọn


Tự kiểm tra

Một connection HTTP ra ngoài có tốn fd không?

Có, mỗi socket là một file descriptor.

Nhiều socket ở CLOSE_WAIT nói lên điều gì?

Phía kia đã đóng, app chưa gọi close(): rò rỉ connection trong code.

Vì sao sửa limits.conf không có tác dụng với service systemd?

limits.conf áp dụng khi login qua PAM; service do systemd fork, phải dùng LimitNOFILE.

kill -3 có làm JVM dừng không?

Không. JVM in thread dump ra stdout rồi chạy tiếp.

Exit code 143 và 137?

143 = 128+15 (SIGTERM). 137 = 128+9 (SIGKILL).