Hai container gắn cùng một volume rồi cùng ghi vào một tệp — chuyện này an toàn tới đâu? Tôi cho bốn container mỗi cái ghi 50.000 dòng, cùng lúc, và đếm lại.
| Cách mở tệp | Kết quả |
|---|---|
O_APPEND |
200.000 dòng, 0 dòng hỏng |
lseek(SEEK_END) rồi write() |
60.466 dòng nguyên vẹn, 271 dòng rách |
Ghi có bộ đệm, chế độ "a" |
200.000 dòng, 0 dòng hỏng |
Dòng giữa mất gần 70% dữ liệu, và không có một thông báo lỗi nào. Mọi lời gọi write() đều trả về thành công.
Vì sao O_APPEND an toàn
Với cờ đó, nhân giữ khoá inode trong suốt một lời gọi write(): tìm cuối tệp và ghi là một thao tác không thể chen ngang. Tôi thử cả dòng dài 8.192 byte — vẫn 0 dòng hỏng.
Không có O_APPEND, ứng dụng phải tự làm hai bước:
os.lseek(fd, 0, os.SEEK_END) # tim cuoi tep
os.write(fd, than) # ghi
Giữa hai dòng đó, ba tiến trình kia kịp ghi thêm. Con trỏ mà bạn vừa tìm được đã cũ, và bạn ghi đè lên dữ liệu của người khác. Đó là lý do tổng số dòng tụt xuống chứ không phải tăng lên.
Chế độ "a" của Python (và của hầu hết ngôn ngữ) mở tệp với O_APPEND sẵn, nên nó an toàn — kể cả khi có bộ đệm ở giữa. Bộ đệm chỉ gom nhiều dòng lại thành một lời gọi write(), mà lời gọi đó vẫn nguyên tử.
Quy tắc rút ra: dùng chế độ "a"/O_APPEND và đừng bao giờ tự quản lý vị trí ghi trên tệp có nhiều bên cùng viết. Nếu thư viện của bạn cho chọn, hãy kiểm tra nó dùng cờ nào.
Sai lầm khi đo: bốn container không hề chạy cùng lúc
Lần đầu tôi chạy phép đo này, cả ba cách đều cho 200.000 dòng hoàn hảo. Tôi suýt kết luận rằng ghi đồng thời trên volume Docker vốn an toàn.
Nguyên nhân: docker run -d trả về ngay, nhưng container mất khoảng 300 mili giây để thật sự khởi động, trong khi việc ghi 5.000 dòng chỉ tốn ~10 mili giây. Bốn container lần lượt chạy xong, không cái nào gặp cái nào. Tôi đo tính đúng đắn của một chương trình chạy một mình.
Cách chữa là thêm hàng rào đồng bộ trên chính volume chung:
open(f"/chung/san-sang-{ten}", "w").close() # bao la da san sang
while not os.path.exists("/chung/XUAT-PHAT"): # cho hieu lenh
time.sleep(0.01)
Phía ngoài chờ đủ bốn tệp san-sang-* rồi mới touch /chung/XUAT-PHAT. Cộng thêm việc tăng khối lượng lên 50.000 dòng mỗi bên, và sự khác biệt hiện ra ngay.
Bài học chung cho mọi phép đo tương tranh: nếu chưa chứng minh được hai bên chồng lên nhau về thời gian, bạn chưa đo tương tranh.
SQLite trên volume chung thì sao
Ba container cùng mở một tệp SQLite trên volume chung, mỗi cái chèn 3.000 dòng:
A: thanh cong=3000 loi=0
B: thanh cong=3000 loi=0
C: thanh cong=3000 loi=0
tong: 9000 toan ven: ok
Chạy tốt. Khoá tệp POSIX của SQLite hoạt động bình thường vì cả ba container dùng chung một nhân và một hệ thống tệp — với nhân, chúng chỉ là ba tiến trình như mọi tiến trình khác.
Điều này chỉ đúng với volume cục bộ. Trên ổ mạng thì khác hẳn, và đó là nội dung phần sau.
(Lần chạy đầu tôi dùng --rm nên container biến mất trước khi kịp đọc đầu ra, và tổng chỉ ra 3.000 — tôi không phân biệt được "B và C thất bại" với "B và C chưa chạy". Chạy lại có giữ log mới cho câu trả lời dùng được.)
Bốn cách cho hai container thấy cùng dữ liệu
| Cách | Đặc điểm | Tốc độ ghi |
|---|---|---|
| Volume có tên dùng chung | ghi hai chiều, sống lâu hơn container | 1,0 GB/s |
--volumes-from <container> |
mượn nguyên bộ volume của container khác | như trên |
--ipc=container:<X> |
chung /dev/shm |
2,3 GB/s |
--tmpfs |
không chia sẻ được | — |
--volumes-from mượn cả bộ. Tiện khi có một container "giữ dữ liệu" và nhiều container công cụ:
docker run --rm --volumes-from goc:ro alpine tar czf - /chung > sao-luu.tgz
Thêm :ro thì bên mượn không ghi được — đo thấy Read-only file system. Nên dùng cho mọi tiến trình chỉ cần đọc, như sao lưu.
tmpfs không đi theo. Tôi thử --volumes-from một container có --tmpfs /tam và bên kia không thấy gì. Đúng như phần 41 đã nói, tmpfs nằm ngoài .Mounts, nên mọi cơ chế dựa vào danh sách đó đều bỏ qua nó.
/dev/shm nhanh gấp đôi nhưng cần khai ở cả hai phía. Bên cho phải mở cửa trước:
docker run -d --name s1 --ipc=shareable --shm-size 64m ...
docker run --rm --ipc=container:s1 ...
Thiếu --ipc=shareable thì Docker từ chối thẳng thừng:
failed to join IPC namespace: non-shareable IPC
(hint: use IpcMode:shareable for the donor container)
Và lưu ý mặc định /dev/shm chỉ có 64 MB. Nhiều ứng dụng khoa học dữ liệu (PyTorch DataLoader chẳng hạn) hỏng đúng ở con số này với thông báo chẳng liên quan gì tới bộ nhớ chung — tăng bằng --shm-size.
Khi nào đừng chia sẻ
Chia sẻ tệp giữa các container là cách phối hợp thô sơ nhất, và nó không có thứ mà bạn thường cần: không thông báo khi có dữ liệu mới, không giao dịch, không hàng đợi, không biết ai đọc tới đâu.
Ba dấu hiệu cho thấy bạn đang dùng sai công cụ:
- Có tiến trình phải hỏi đi hỏi lại thư mục xem có tệp mới chưa.
- Phải nghĩ ra quy ước đặt tên kiểu
.tmprồi đổi tên để tránh đọc tệp đang ghi dở. - Hai bên cùng ghi, chứ không phải một ghi một đọc.
Cả ba đều là lúc nên dùng hàng đợi (Redis, RabbitMQ) hoặc CSDL. Chia sẻ volume hợp nhất với mô hình một bên ghi, nhiều bên đọc: sinh ảnh, sinh báo cáo, sinh tệp tĩnh.
Thử ba mươi giây
Kiểm tra ứng dụng của bạn có mở tệp log đúng cách không:
docker exec <ten> sh -c 'ls -l /proc/1/fd' 2>/dev/null
docker exec <ten> sh -c 'cat /proc/1/fdinfo/1' 2>/dev/null | grep flags
Trường flags ở dạng bát phân; bit 02000 là O_APPEND. Không có bit đó mà lại có nhiều tiến trình cùng ghi thì bạn đang mất dữ liệu — và sẽ không có bất kỳ lỗi nào báo cho bạn biết.
Phần sau gắn ổ mạng NFS vào container và đo xem cái gì còn dùng được.