Lỗi
Chắc hẳn bạn đã từng gặp lỗi này: bạn đã sẵn sàng để kiểm tra một REST endpoint hoặc kết nối cơ sở dữ liệu mới, nhưng Java lại ném ra một "bức tường" văn bản thay thế. Ngoại lệ cụ thể này thường trông như sau:
java.net.ConnectException: Connection refused (Connection refused)
at java.base/sun.nio.ch.Net.connect0(Native Method)
at java.base/sun.nio.ch.Net.connect(Net.java:579)
at java.base/sun.nio.ch.NioSocketImpl.connect(NioSocketImpl.java:585)
at java.base/java.net.Socket.connect(Socket.java:633)
at java.base/java.net.Socket.<init>(Socket.java:507)
Ý nghĩa thực sự của lỗi này
Lỗi này rất trực diện. Client của bạn đã kết nối thành công đến máy chủ đích, nhưng máy chủ đó đã chủ động trả lời "Không". Khác với lỗi Connection Timeout (Hết thời gian kết nối) - nơi máy chủ chỉ đơn giản là phớt lờ bạn, Connection Refused có nghĩa là hệ điều hành đã phản hồi bằng một gói tin TCP RST (Reset).
Về cơ bản, cánh cửa vẫn ở đó, nhưng nó đã bị khóa và không có ai ở nhà.
Các nguyên nhân phổ biến và cách khắc phục
1. Dịch vụ đích không hoạt động
Đây là nguyên nhân phổ biến nhất. Bạn có thể đang cố gắng truy cập vào một microservice trên cổng 8080, nhưng lại quên chạy lệnh mvn spring-boot:run. Nếu tiến trình không hoạt động, hệ điều hành sẽ không có gì để bàn giao kết nối.
Cách khắc phục: Kiểm tra xem dịch vụ có thực sự đang lắng nghe hay không. Chạy các lệnh sau trên máy chủ:
# Kiểm tra xem có gì đang lắng nghe trên cổng 8080 (Linux/macOS)
sudo lsof -i :8080
# Hoặc sử dụng netstat trên Windows
netstat -ano | findstr :8080
Nếu các lệnh này không trả về kết quả, hãy khởi động ứng dụng của bạn.
2. Sai lệch cổng (Port Mismatch)
Rất dễ nhầm lẫn các cổng khi quản lý nhiều dịch vụ. Máy chủ của bạn có thể đang chạy trên cổng 9000, nhưng client của bạn vẫn được cấu hình cứng để tìm cổng mặc định 8080. Điều này thường xảy ra sau khi thay đổi cấu hình trong application.properties mà không được đồng bộ hóa với frontend.
Cách khắc phục: Mở các tệp cấu hình cạnh nhau. Đảm bảo server.port trong backend khớp chính xác với BASE_URL trong mã nguồn client của bạn.
3. Bẫy "Localhost" (Vấn đề về Binding)
Đây là một vấn đề đau đầu kinh điển đối với các nhà phát triển khi chuyển từ thử nghiệm cục bộ sang Docker hoặc môi trường production. Nếu máy chủ của bạn bind (liên kết) với 127.0.0.1, nó chỉ lắng nghe chính nó. Nếu một client cố gắng kết nối qua IP mạng LAN của máy chủ (như 192.168.1.50), hệ điều hành sẽ từ chối nó.
Cách khắc phục: Cấu hình máy chủ của bạn để bind với 0.0.0.0. Điều này yêu cầu ứng dụng lắng nghe trên tất cả các giao diện mạng hiện có.
// Ví dụ Java ServerSocket: bind với tất cả các giao diện
ServerSocket server = new ServerSocket(8080, 50, InetAddress.getByName("0.0.0.0"));
Trong Spring Boot, chỉ cần thêm server.address=0.0.0.0 vào tệp properties của bạn.
4. Tường lửa và Cloud Security Groups
Đôi khi dịch vụ đang hoạt động hoàn hảo, nhưng một lớp bảo mật lại đang chặn đường. Trong khi nhiều tường lửa âm thầm "drop" (bỏ qua) các gói tin (gây ra lỗi timeout), một số khác được cấu hình để "reject" (từ chối) chúng. Điều này thường gặp với các thiết lập ufw cục bộ hoặc AWS Security Groups.
Cách khắc phục: Kiểm tra các quy tắc tường lửa của bạn. Trên một máy chủ Linux, bạn có thể kiểm tra nhanh bằng cách mở cổng:
# Cho phép lưu lượng truy cập trên cổng 8080 qua UFW
sudo ufw allow 8080/tcp
5. Hàng đợi kết nối (Connection Backlog) bị đầy
Khi tải nặng, máy chủ có thể ngừng chấp nhận các kết nối mới. ServerSocket của Java có một "backlog" mặc định — một hàng đợi cho các kết nối đang chờ xử lý. Nếu hàng đợi này (thường mặc định là 50) bị đầy do máy chủ quá chậm, hệ điều hành sẽ bắt đầu từ chối các yêu cầu mới.
Cách khắc phục: Tăng kích thước backlog hoặc tốt hơn hết là tối ưu hóa logic xử lý yêu cầu để giải phóng hàng đợi nhanh hơn.
Cách xác minh việc khắc phục
Đừng lãng phí thời gian khởi động lại ứng dụng Java nặng nề cho đến khi bạn biết chắc chắn cổng đó đã mở. Hãy sử dụng các công cụ nhẹ để kiểm tra kết nối trước.
Sử dụng Netcat (nc):
nc -zv 192.168.1.50 8080
Thông báo "Connection to 192.168.1.50 port 8080 [tcp/http] succeeded!" có nghĩa là đường truyền mạng của bạn cuối cùng đã thông suốt.
Kinh nghiệm thực tế cá nhân
Khi tôi gỡ lỗi các hệ thống microservice phức tạp, tôi thường thấy rằng các vấn đề về dải IP là kẻ giết người thầm lặng. Tôi sử dụng Subnet Calculator này để kiểm tra lại xem các khối CIDR và việc bind 0.0.0.0 có thực sự hợp lý cho mạng mà tôi đang triển khai hay không.
Một điều nữa: luôn ghi log target URI trong khối catch của bạn. Việc nhìn thấy Failed to connect to 127.0.0.1:8080 trong log production là một cứu cánh tiết kiệm thời gian cực lớn. Nó ngay lập tức cho bạn biết rằng ứng dụng của bạn đang cố gắng tự nói chuyện với chính nó thay vì kết nối với cơ sở dữ liệu production.
try {
// logic kết nối của bạn tại đây
} catch (ConnectException e) {
logger.error("Connection refused to {}:{}", remoteHost, remotePort);
throw e;
}

