Cách khắc phục lỗi MaxRetryError và Connection Refused trong Python urllib3

intermediate🌐 Networking2026-07-09| Python 3.x, Linux (Ubuntu/Debian/CentOS), macOS, Docker container sử dụng thư viện requests hoặc urllib3.

Error Message

urllib3.exceptions.MaxRetryError: HTTPConnectionPool(host='...', port=80): Max retries exceeded with url: /api/endpoint (Caused by NewConnectionError('<urllib3.connection.HTTPConnection object>: Failed to establish a new connection: [Errno 111] Connection refused'))
#python#requests#urllib3#mạng#docker

Bối cảnh

Tất cả chúng ta đều đã từng gặp tình huống này: mã nguồn chạy hoàn hảo trên máy tính cá nhân, nhưng ngay khi đưa lên máy chủ staging, nhật ký (logs) bùng nổ lỗi. Gần đây tôi đã gặp phải vấn đề này khi triển khai một worker Python được thiết kế để lấy dữ liệu từ một REST API. Các dòng log tràn ngập lỗi MaxRetryError, cụ thể là trỏ đến [Errno 111] Connection refused.

Lỗi này là cách urllib3 thông báo rằng nó đã cố gắng kết nối với máy chủ nhiều lần nhưng đều thất bại. Đây không phải là một sự cố ngẫu nhiên, mà là sự thất bại liên tục trong việc thiết lập bắt tay (handshake). Mặc dù requests là thư viện mà bạn có thể đã import, nhưng urllib3 mới là "động cơ" bên dưới thực hiện các tác vụ nặng nề — và chính nó là bên đang báo lỗi.

Quy trình gỡ lỗi

Lỗi Connection refused thực tế lại khá hữu ích vì nó giúp thu hẹp phạm vi vấn đề xuống tầng vận chuyển (transport layer). Thông thường, dịch vụ có thể đã ngừng hoạt động, đang lắng nghe nhầm "cổng", hoặc bị chặn bởi một hệ thống bảo vệ. Dưới đây là cách để truy tìm nguyên nhân.

1. Kiểm tra "Đã cắm điện chưa?"

Đầu tiên, hãy xác minh xem dịch vụ đích thực sự còn hoạt động hay không. Đăng nhập vào máy chủ lưu trữ API và kiểm tra xem tiến trình có đang liên kết (bind) với cổng mong muốn hay không (ví dụ: cổng 80 hoặc 8080). Tôi thường sử dụng ss hoặc netstat cho việc này:

# Tìm kiếm các tiến trình đang lắng nghe trên cổng 80
sudo ss -tulpn | grep :80

Nếu lệnh này không trả về kết quả nào, dịch vụ backend của bạn có khả năng đã bị sập hoặc không khởi động được. Trên macOS, bạn có thể thấy lỗi [Errno 61] Connection refused thay vì 111, nhưng các bước chẩn đoán vẫn tương tự.

2. Cái bẫy 127.0.0.1

Đây là một sai lầm kinh điển trong cấu hình. Nếu API của bạn được liên kết với 127.0.0.1, nó chỉ đang "nói chuyện" với chính nó. Bất kỳ yêu cầu nào đến từ một container Docker hoặc một máy ảo (VM) khác sẽ bị từ chối ngay lập tức. Để khắc phục, hãy đảm bảo dịch vụ của bạn liên kết với 0.0.0.0, điều này yêu cầu nó lắng nghe trên tất cả các giao diện mạng hiện có.

3. Sai lệch DNS và IP

Đôi khi tên miền (hostname) phân giải ra một địa chỉ IP cũ. Sử dụng dig +short your-api-host để xem lưu lượng truy cập của bạn thực sự đang đi đâu. Nếu IP không khớp với máy chủ hiện tại, script Python của bạn đang "gõ nhầm cửa".

Giải pháp

Giải pháp 1: Mạng trong Container

Trong môi trường Docker, localhost có tính tương đối. Nếu script của bạn nằm trong Container A và API nằm trong Container B, localhost bên trong Container A sẽ trỏ đến chính nó chứ không phải API. Hãy sử dụng tên dịch vụ nội bộ được định nghĩa trong tệp docker-compose.yml. Hệ thống DNS nội bộ của Docker sẽ tự động xử lý việc định tuyến.

Giải pháp 2: Triển khai Exponential Backoff

Nếu mạng chỉ bị chập chờn — có lẽ do độ trễ tăng đột biến 50ms hoặc dịch vụ đang khởi động lại nhanh — đừng để các thiết lập mặc định làm dừng tiến trình của bạn. Hãy sử dụng đối tượng Retry để cho kết nối một cơ hội thứ hai (hoặc thứ ba). Đoạn mã này thêm một "backoff factor" (hệ số chờ), buộc script phải đợi lâu hơn giữa mỗi lần thử lại (ví dụ: 0.6 giây, 1.2 giây, 2.4 giây).

import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

def get_robust_session(retries=3, backoff=0.3):
    session = requests.Session()
    # Chúng ta chỉ thử lại với các mã trạng thái cụ thể hoặc lỗi kết nối
    retry_strategy = Retry(
        total=retries,
        backoff_factor=backoff,
        status_forcelist=[429, 500, 502, 503, 504],
        allowed_methods=["HEAD", "GET", "OPTIONS"]
    )
    adapter = HTTPAdapter(max_retries=retry_strategy)
    session.mount("http://", adapter)
    session.mount("https://", adapter)
    return session

try:
    client = get_robust_session()
    response = client.get('http://api.internal/v1/data', timeout=5)
    print(f"Status: {response.status_code}")
except Exception as e:
    print(f"Request failed: {e}")

Giải pháp 3: Kiểm tra quy tắc tường lửa (Firewall)

Nếu dịch vụ chắc chắn đang chạy trên 0.0.0.0 nhưng bạn vẫn bị chặn, hãy kiểm tra ufw hoặc iptables. Chỉ cần thiếu một quy tắc duy nhất cũng có thể chặn dải IP của client.

Khi làm việc với các VPC hoặc subnet phức tạp, tôi thường sử dụng Công cụ tính Subnet để xác minh xem các khối CIDR (như 10.0.0.0/24) có thực sự bao phủ IP của client hay không. Đây là cách dễ dàng để đảm bảo danh sách trắng (whitelist) của tường lửa không quá hẹp.

Xác minh

Trước khi chỉnh sửa lại mã nguồn Python, hãy chạy thử nghiệm bằng curl từ môi trường client. Đây là cách nhanh nhất để tách biệt lỗi mã nguồn khỏi lỗi hạ tầng:

curl -I http://your-host:80/api/endpoint
  • HTTP 200/404: Mạng ổn định; vấn đề có khả năng nằm ở logic Python của bạn.
  • Connection Refused: Cổng đang đóng hoặc dịch vụ bị sập.
  • Operation Timed Out: Tường lửa đang âm thầm loại bỏ các gói tin của bạn.

Bài học kinh nghiệm

  • Stack trace là bản đồ: MaxRetryError chỉ là lớp vỏ bọc. Luôn cuộn xuống dưới cùng để tìm lỗi Errno thực tế.
  • Tham số timeout là bắt buộc: Đừng bao giờ thực hiện yêu cầu mà không có tham số timeout=X. Nếu không có nó, ứng dụng của bạn có thể bị treo vô thời hạn nếu máy chủ chấp nhận kết nối nhưng không bao giờ gửi dữ liệu.
  • Ghi nhật ký IP: Khi gỡ lỗi, hãy ghi lại địa chỉ IP thực tế mà hostname phân giải ra. Việc này giúp tiết kiệm hàng giờ đồng hồ truy đuổi những máy chủ "ma".

Related Error Notes