services:
app:
depends_on:
- db
Dòng đó nghe như "đợi db xong đã". Nó không phải vậy. Tôi viết một ứng dụng ghi lại chính xác lúc nào nó kết nối được, rồi đo trên MySQL 8.
MOC-TCP=4.98s (that bai 97 lan)
MOC-SQL=4.99s (that bai 0 lan)
Lệnh up -d trả về sau 0,5 giây. Cổng 3306 mở ra ở giây thứ 4,98. Trong khoảng giữa, ứng dụng thử kết nối và trượt 97 lần.
depends_on mặc định dùng điều kiện service_started — nó chỉ bảo đảm tiến trình trong container db đã được khởi động. Không phải cổng đã mở, càng không phải CSDL đã sẵn sàng.
Nhãn Compose ghi rõ điều đó, như phần 49 đã thấy:
com.docker.compose.depends_on = db:service_started:false
Lời giải thích phổ biến về chuyện này lại sai
Cách kể quen thuộc là: "cổng mở trước, nhưng CSDL chưa nhận truy vấn được". Tôi đo riêng hai mốc đó để kiểm chứng.
| Mốc | MySQL 8 | PostgreSQL 16 |
|---|---|---|
| Cổng TCP mở | 4,98 s | 0,62 s |
| Truy vấn SQL đầu tiên chạy được | 4,99 s | 0,62 s |
| Khoảng cách | 0,01 s | 0,01 s |
Gần như bằng không, ở cả hai. Cả hai image chính thức đều chạy phần khởi tạo trên socket nội bộ trước, chỉ mở cổng TCP ra ngoài khi đã sẵn sàng nhận truy vấn thật.
Nên khoảng mù không nằm giữa "cổng mở" và "sẵn sàng". Nó nằm trước khi cổng mở, và độ dài của nó là thời gian khởi tạo của CSDL: khoảng 0,6 giây với PostgreSQL, khoảng 5 giây với MySQL trên cùng máy đó.
Điều này quan trọng khi bạn viết script chờ: kiểm tra cổng TCP là đủ cho hai CSDL này. Với phần mềm khác thì phải tự đo, đừng suy diễn.
Cách vá: service_healthy
services:
db:
image: mysql:8.0
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-uroot", "-pbimat"]
interval: 2s
timeout: 3s
retries: 30
start_period: 5s
app:
depends_on:
db:
condition: service_healthy
Đo lại trên cùng hệ thống:
| Điều kiện | up -d trả về sau |
Số lần ứng dụng kết nối trượt |
|---|---|---|
service_started (mặc định) |
0,5 s | 97 lần, trong 5 giây |
service_healthy |
7,9 s | 0 lần — nối được ngay lần đầu |
Lệnh up giờ chờ cho tới khi healthcheck báo khoẻ. Nó chậm hơn, và đó chính là điều bạn muốn: thời gian chờ chuyển từ chỗ ứng dụng phải tự xoay xở sang chỗ Compose lo giúp.
Nhớ đặt start_period: trong khoảng đó, healthcheck thất bại không bị tính vào retries. Thiếu nó, một CSDL khởi động chậm sẽ bị đánh dấu unhealthy trước khi kịp lên.
Còn một điều kiện thứ ba, hợp cho job chạy một lần như migrate:
depends_on:
migrate:
condition: service_completed_successfully
Nhưng vẫn phải thử lại trong ứng dụng
service_healthy giải quyết lúc khởi động. Nó không giải quyết:
- CSDL khởi động lại giữa ca (phần 28 đo được vòng lặp restart).
- Mất kết nối mạng tạm thời.
- Chạy trên Kubernetes hay Swarm, nơi không có
depends_on.
Ứng dụng phải chịu được việc CSDL biến mất rồi quay lại, ở mọi thời điểm chứ không riêng lúc khởi động. depends_on chỉ làm cho lần khởi động đầu tiên êm ái hơn.
Vòng lặp thử lại tối thiểu:
for lan in range(30):
try:
ket_noi(); break
except Exception:
time.sleep(min(2 ** lan * 0.1, 5))
else:
raise SystemExit("khong noi duoc CSDL sau 30 lan")
Và nhớ ghi log số lần đã thử — chính con số đó cho bạn biết khoảng mù trên hệ thống thật dài bao nhiêu.
Hai tín hiệu "chưa sẵn sàng" giả mà tôi vấp phải
Khi dựng phép đo này, ứng dụng của tôi báo kết nối thất bại suốt bốn phút dù MySQL đã ghi ready for connections trong log. Nếu tin con số đó, tôi đã công bố một khoảng mù dài vô tận.
Lần thứ nhất:
ERROR 2026 (HY000): TLS/SSL error: self-signed certificate in certificate chain
Lần thứ hai, sau khi tắt TLS:
ERROR 1045 (28000): Plugin caching_sha2_password could not be loaded
Cả hai đều không phải vấn đề sẵn sàng — một cái là chứng chỉ tự ký, một cái là client mariadb thiếu plugin xác thực của MySQL 8. Máy chủ đã sẵn sàng từ lâu; chỉ có client là không nói chuyện được với nó.
Bài học cho mọi script chờ: đừng gộp "kết nối thất bại" thành một loại. Một vòng lặp until ket_noi_duoc; do sleep 1; done sẽ quay vô hạn với cả hai lỗi trên, và log của bạn chỉ nói "đang chờ CSDL". Hãy in ra thông báo lỗi thật ở lần thất bại đầu tiên — nó là khác biệt giữa "chờ thêm 3 giây" và "cấu hình sai, chờ mãi cũng không xong".
Thử ba mươi giây
Đo khoảng mù của chính hệ thống bạn:
docker compose down -v
S=$(date +%s)
docker compose up -d
echo "up -d tra ve sau $(( $(date +%s) - S ))s"
until docker compose exec -T db pg_isready -q 2>/dev/null; do sleep 1; done
echo "csdl san sang sau $(( $(date +%s) - S ))s"
Hiệu hai con số là khoảng thời gian ứng dụng của bạn đang chạy mà chưa có CSDL. Nếu nó lớn hơn vài giây, hai việc cần làm: thêm healthcheck kèm condition: service_healthy, và kiểm tra ứng dụng có vòng lặp thử lại chưa.
Phần sau đo bốn tầng biến môi trường của Compose, và chỗ giá trị của bạn bị đọc sai kiểu.