Phần 47 đo ba cách chia sẻ dữ liệu giữa các container trên cùng máy. Phần này đi xa hơn: gắn một ổ mạng thật — NFS, SMB — làm volume, đo độ trễ, và tìm chỗ không nên dùng nó.

docker volume mặc định nằm trên đĩa của chính máy chạy Docker. Nhưng có lúc dữ liệu phải sống ngoài máy đó: NAS cơ quan xuất qua NFS, máy Windows chia sẻ thư mục qua SMB, hoặc một dịch vụ file tập trung nhiều host cùng đọc. Docker hỗ trợ thẳng cả hai qua driver local với --opt type=nfs hoặc --opt type=cifs, không cần cài thêm gì trong container ứng dụng — việc mount do chính Docker daemon lo.

So sánh kiến trúc và số đo độ trễ giữa volume thường, NFS và SMB gắn vào container

Bãi thử: cả server lẫn client đều là container

Không có NAS hay máy Windows thật để đo, nên bài này dựng cả server lẫn client bằng container, tất cả trong VM Docker Desktop trên macOS (Docker 29.7.2), nối qua một network riêng — độ trễ mạng gần như bằng 0 (loopback). Số đo dưới đây vì vậy là chênh lệch do giao thức và round-trip, chưa cộng thêm độ trễ mạng vật lý thật.

  • NFS server: container Alpine cài unfs3 — bản NFSv3 chạy hoàn toàn ở user-space, không cần kernel nfsd, xuất thư mục qua /etc/exports.
  • SMB server: container Alpine cài samba, một share guest ok = yes tối giản.
  • Volume thường: named volume local bình thường, làm mốc so sánh.
# NFS: mount qua driver local của Docker, không cần cài gì trong container ứng dụng
docker volume create --driver local \
  --opt type=nfs \
  --opt o=addr=172.19.0.2,nolock,vers=3,tcp,port=2049,mountport=2049,rw \
  --opt device=:/data nfsvol

# SMB: tương tự, dùng type=cifs
docker volume create --driver local \
  --opt type=cifs \
  --opt o=username=guest,password=,vers=3.0,rw \
  --opt device=//172.19.0.3/data smbvol

Cả hai lệnh chạy ngay, không cần nfs-common hay cifs-utils trong container ứng dụng — việc mount thật sự diễn ra ở tầng VM của Docker, container chỉ nhìn thấy một thư mục bình thường.

Ghi 4K kèm fsync: mô phỏng ghi database

Ghi ngẫu nhiên khối 4K, fsync sau mỗi lần ghi — đúng kiểu I/O của một cơ sở dữ liệu ghi transaction log. Đo bằng fio, chạy 3 lần lấy trung bình:

fio --name=rw4k --directory=/mnt/dich --rw=randwrite --bs=4k --size=4M \
    --direct=0 --sync=1 --numjobs=1 --iodepth=1
Đích Độ trễ trung bình mỗi lần ghi So với volume thường
Volume thường 305 µs mốc
NFS (unfs3) 384 µs +26%
SMB (samba) 462 µs +52%

Chênh lệch tồn tại nhưng không thảm hoạ, vì đường mạng ở đây là loopback trong cùng VM. Qua mạng vật lý thật (LAN cơ quan, chưa nói tới WAN), mỗi con số này còn phải cộng thêm round-trip time thật — có thể gấp hàng chục lần.

Tạo 200 file nhỏ: nơi NFS lộ rõ điểm yếu, và đảo ngược thứ hạng

Tạo và xoá 200 file 1 dòng, đo bằng date +%s%N trước/sau vòng lặp for, chạy 3 lần:

Đích Thời gian tạo 200 file So với volume thường
Volume thường 2 ms mốc
SMB (samba) 42 ms 21×
NFS (unfs3) 85 ms 42×

Ở phép đo trước, NFS nhanh hơn SMB. Ở phép đo này, NFS chậm gấp đôi SMB — thứ hạng đảo ngược hoàn toàn. Nguyên nhân nằm ở giao thức: NFSv3 xử lý mỗi thao tác (CREATE, GETATTR, SETATTR...) gần như một RPC round-trip riêng, còn SMB2/3 gộp được nhiều bước (tạo, ghi, đóng) vào một compound request. Không có "ổ mạng nào nhanh hơn" một cách tuyệt đối — chỉ có nhanh hơn ở kiểu tải nào.

Đây cũng là lý do thực dụng nhất bài viết này muốn nói: nếu container chạy npm install (hàng chục nghìn file nhỏ trong node_modules), build cache, hay bất kỳ thứ gì tạo nhiều file cùng lúc, đặt nó trên NFS hay SMB — dù là ổ mạng nhanh — vẫn chậm hơn hẳn đĩa cục bộ, và NFS ở đây chậm nhất.

Đo ngược lời khuyên phổ biến: O_DIRECT trên CIFS không đáng tin

Lời khuyên phổ biến khi muốn đo tốc độ ghi "thật": dùng O_DIRECT để bỏ qua cache của hệ điều hành. Thử ghi tuần tự 256MB với --direct=1:

Đích Băng thông ghi (O_DIRECT)
Volume thường 3355 MB/s
NFS 658 MB/s
SMB 3398 MB/s

NFS giảm đúng như dự đoán (khoảng 5 lần so với volume thường). Nhưng SMB ngang bằng volume thường — nhanh vô lý cho một đường ghi phải qua mạng. Không phải lỗi đo: mount | grep cifs cho thấy mount mặc định dùng cache=strict, và ở chế độ này cờ O_DIRECT từ ứng dụng không chắc buộc kernel CIFS client bỏ cache hoàn toàn như trên một khối thiết bị cục bộ. Nói cách khác — đừng tin số đo O_DIRECT trên CIFS mount mặc định; muốn đo ghi thật, ép fsync bằng --sync=1 như phép đo đầu bài, không dùng --direct=1.

Server chết giữa phiên: treo, không phải lỗi

Phần đáng chú ý nhất không phải tốc độ, mà là hành vi khi mất kết nối. Hai kịch bản, đo riêng.

Mount lần đầu khi server đã chết: docker run với volume NFS trỏ tới server không phản hồi thất bại sau khoảng 14 giây với lỗi rõ ràng — no route to host. Container không khởi động được, nhưng ít nhất biết ngay.

Mount đã sống, server chết giữa phiên: đây mới là cái bẫy. Volume dùng tuỳ chọn mặc định hard (thấy trong mount | grep nfs) — client sẽ không bao giờ tự bỏ cuộc, chỉ lặp lại RPC tới khi server trả lời. Đo thật: dừng container NFS server trong lúc một lệnh cat khác đang đọc file trên mount đó; kill -9 vào tiến trình đó không có tác dụng vì nó đang ở uninterruptible sleep trong syscall kernel. Khởi động lại NFS server, lệnh cat tự chạy tiếp và trả kết quả — tổng cộng treo đúng 37 giây, khớp thời gian server offline cộng thời gian khởi động lại.

Đây là lý do cần cân nhắc kỹ trước khi gắn ổ mạng cho service production: một request HTTP đọc file qua NFS mount hard, nếu NFS server phía sau rớt mạng, sẽ treo vô thời hạn thay vì trả lỗi. Muốn có lỗi thay vì treo, phải đổi sang tuỳ chọn soft khi mount — đổi lại là rủi ro mất dữ liệu nếu request bị cắt giữa chừng ghi dở, đúng đánh đổi kinh điển của NFS mà tài liệu chính thức luôn nhắc.

Chọn theo khối lượng công việc

  • Đọc file tĩnh (ảnh, asset, log tổng hợp) hay ghi tuần tự khối lớn ít khi cần fsync ngay: cả NFS và SMB đều dùng được, chênh lệch không đáng kể so với lợi ích chia sẻ được dữ liệu giữa nhiều host.
  • Thư mục dữ liệu của database: tránh cả hai. Ghi 4K kèm fsync liên tục vốn đã chậm hơn 20–50%, và một lần server rớt mạng là cả service treo theo, không có timeout nào cứu được nếu mount ở chế độ hard.
  • node_modules, build cache, hay bất kỳ thứ tạo nhiều file nhỏ: tránh, đặc biệt tránh NFS — chậm hơn đĩa cục bộ 20–40 lần trong phép đo ở đây.
  • Service không chấp nhận được treo vô thời hạn: nếu bắt buộc phải dùng ổ mạng, mount soft với timeo hợp lý còn hơn để một request treo mãi không rõ lý do.

Thử ba mươi giây

Kiểm tra ngay mount NFS/SMB hiện có đang ở chế độ hard hay soft — quyết định nó treo hay báo lỗi khi server phía sau rớt mạng:

docker run --rm -v ten-volume-cua-ban:/mnt/x alpine:3.20 sh -c "mount | grep /mnt/x"

Thấy hard mà không có soft hay timeo đặt riêng — mount đó sẽ treo vô thời hạn nếu server phía sau rớt mạng, không phải trả lỗi.

Bài viết liên quan