File descriptor, pipe, signal và IPC
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 đề
- Mỗi process có giới hạn số fd mở cùng lúc (
ulimit -n, mặc định thường là 1024). - Code tạo HTTP client mới cho mỗi request mà không đóng, hoặc đọc response không đóng body → socket rò rỉ, fd tăng dần tới trần.
3. Giải pháp: hiểu file descriptor
- File descriptor là một số nguyên trong bảng của process, trỏ tới một đối tượng kernel: file, socket, pipe, epoll…
- 0 = stdin, 1 = stdout, 2 = stderr. Các fd sau do app mở.
- Giới hạn: soft limit (đang áp dụng) và hard limit (trần mà soft được nâng lên). Toàn hệ thống:
fs.file-max. - Hai việc cần làm: nâng limit cho hợp lý và sửa rò rỉ. Chỉ nâng limit thì chỉ hoãn lỗi.
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ểm | Ví dụ |
|---|---|---|
Pipe | | Một chiều, cha–con, luồng byte | jstack PID | grep BLOCKED; Java Process.getInputStream() |
| Named pipe (FIFO) | Pipe có tên trên filesystem | Ít dùng trong app Java |
| Signal | Báo hiệu, không mang dữ liệu | SIGTERM để tắt, SIGQUIT để thread dump |
| Unix domain socket | Như TCP nhưng trong một máy, nhanh hơn | /var/run/docker.sock, Postgres local, jcmd attach JVM |
| TCP socket | Qua mạng | order → payment, order → Postgres |
| Shared memory | Nhanh 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
- Server Java: đặt
LimitNOFILE65536 trở lên ngay từ đầu. - Không tạo HTTP client mới mỗi request. Luôn đóng stream/response.
- Giữa các service: dùng TCP/HTTP. Unix socket chỉ khi cùng máy và cần nhanh.
kill, kill -9, kill -3 khác nhau gì với JVM? Câu hỏi tiếp.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ệnh | Signal | Bắt được? | JVM làm gì |
|---|---|---|---|
kill PID | SIGTERM (15) | Có | Chạy shutdown hook → Spring graceful shutdown → thoát mã 143 |
kill -9 PID | SIGKILL (9) | Không | Chết ngay, không hook, không flush log; mã 137 |
kill -3 PID | SIGQUIT (3) | Có | In thread dump ra stdout, app chạy tiếp |
| Ctrl+C | SIGINT (2) | Có | Như SIGTERM |
kill -STOP / -CONT | SIGSTOP / SIGCONT | Khô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.
.bashrcchỉ ảnh hưởng shell của bạn và process bạn chạy từ shell đó./etc/security/limits.confáp dụng qua PAM khi login (SSH, su).- Service do systemd (PID 1) fork ra, không đi qua login → không đọc hai file trên. Phải đặt
LimitNOFILE=trong unit, hoặcDefaultLimitNOFILE=trong/etc/systemd/system.conf.
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
- Socket = fd. Too many open files = thường là rò rỉ connection.
- Service systemd:
LimitNOFILE, không phảilimits.conf. - 15 tắt êm, 9 giết ngay, 3 thread dump.
- Kiểm tra bằng
/proc/PID/limitsvà/proc/PID/fd.
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).