Mô phỏng mạng kém với tc netem trên Linux: Kiểm thử độ trễ, mất gói và jitter trước khi lên production

Networking tutorial - IT technology blog
Networking tutorial - IT technology blog

Bug Chỉ Xuất Hiện Trên Production

Sáu tháng trước, team tôi đẩy một bản cập nhật microservices qua staging mà không gặp bất kỳ lỗi nào. Load test qua. Integration test qua. Rồi chúng tôi deploy lên production — và chỉ trong vòng 20 phút, người dùng ở Đông Nam Á bắt đầu gặp timeout và các API call bị rớt.

Nguyên nhân? Ứng dụng của chúng tôi chưa bao giờ được kiểm thử trong điều kiện mạng thực tế. Staging chạy trên LAN gigabit. Traffic production thì vượt đại dương. RTT giữa các node Singapore và backend Tokyo của chúng tôi luôn dao động quanh 80ms, đôi khi tăng vọt lên hơn 200ms, cộng thêm tỷ lệ mất gói 1–2% trên một đường upstream.

Sự cố đó buộc chúng tôi phải đặt một câu hỏi khó: làm thế nào để tái tạo điều kiện mạng tệ ngay tại local, một cách nhất quán, trước khi ship code? Câu trả lời tôi cứ quay lại là tc netem — bộ mô phỏng mạng Linux Traffic Control. Tôi đã tích hợp nó vào pipeline pre-deployment từ đó đến nay. Tỷ lệ timeout sau deploy giảm hơn 80% chỉ trong hai sprint.

tc netem Thực Ra Là Gì

tc là lệnh Linux dùng để thao tác với các thiết lập kiểm soát lưu lượng mạng. Nó được đóng gói trong gói iproute2, nên hầu như có sẵn trên mọi Linux server hiện đại. netem (Network Emulator) là một queueing discipline (qdisc) mà bạn gắn vào một network interface để tạo ra các sự cố mạng giả lập.

Hãy hình dung nó như một lớp làm suy giảm lập trình được, nằm giữa ứng dụng của bạn và network stack. Bạn nói với kernel: “Khi các gói tin rời khỏi interface này, hãy bỏ ngẫu nhiên 2% trong số chúng, thêm 80ms độ trễ với 20ms jitter, và đôi khi sắp xếp lại thứ tự chúng.” Kernel thực hiện chính xác như vậy — hoàn toàn trong suốt, không cần thay đổi gì trong ứng dụng của bạn.

Các công cụ như iperf3 hay mtr thì đo lường điều kiện mạng hiện có. tc netem thì tạo ra những điều kiện đó theo yêu cầu. Đó là điểm khác biệt then chốt.

Bốn Loại Suy Giảm Cần Biết

  • Delay — thêm độ trễ cố định hoặc biến đổi vào các gói tin gửi đi
  • Packet loss — ngẫu nhiên bỏ một tỷ lệ phần trăm gói tin
  • Jitter — tạo ra sự biến động trong độ trễ (kẻ thù của các ứng dụng thời gian thực)
  • Packet reordering và duplication — mô phỏng các đường kết nối không đáng tin cậy

Thiết Lập Rule Netem Đầu Tiên

Trước tiên, kiểm tra xem tc đã có sẵn chưa:

tc -V

Hầu hết các distro đều có sẵn. Nếu không: apt install iproute2 hoặc yum install iproute.

Cú pháp cơ bản để thêm netem qdisc vào một interface:

tc qdisc add dev eth0 root netem delay 100ms

Toàn bộ traffic gửi đi trên eth0 giờ sẽ bị thêm độ trễ một chiều giả lập 100ms. Để xác nhận đã áp dụng:

tc qdisc show dev eth0

Bạn sẽ thấy output như sau:

qdisc netem 8001: root refcnt 2 limit 1000 delay 100ms

Để xóa khi đã xong:

tc qdisc del dev eth0 root

Luôn dọn dẹp sau khi kiểm thử. Một rule netem bị quên đã gây ra không ít buổi debug bối rối trong team tôi.

Thực Hành: Các Kịch Bản Kiểm Thử Thực Tế

Kịch bản 1: Mô phỏng độ trễ cross-region (80ms + Jitter)

Khớp chính xác với đường Đông Nam Á đến Tokyo đã khiến chúng tôi bị bất ngờ:

tc qdisc add dev eth0 root netem delay 80ms 20ms distribution normal

Giá trị 20ms thứ hai là phạm vi jitter. distribution normal làm cho sự biến động tuân theo đường cong chuẩn thay vì hoàn toàn ngẫu nhiên — gần hơn với cách mạng thực tế hoạt động. Hầu hết các gói tin rơi vào khoảng 80ms; các đợt tăng đột biến thỉnh thoảng đạt 100ms hoặc cao hơn.

Xác nhận bằng một lệnh ping nhanh đến host mục tiêu:

ping -c 20 your-backend-host.example.com

Kịch bản 2: Mất gói tin

Chỉ 1% mất gói cũng có thể phá hủy hiệu suất ứng dụng nếu logic retry không đủ chắc. Hãy bắt đầu thận trọng:

tc qdisc add dev eth0 root netem loss 1%

Để mô phỏng mất gói có tương quan — khi các gói bị rớt xuất hiện thành cụm, giống như trong sự cố suy giảm đường kết nối thực:

tc qdisc add dev eth0 root netem loss 2% 25%

Con số 25% đó là hệ số tương quan. Nếu một gói bị rớt, có 25% khả năng gói tiếp theo cũng bị rớt. Mất gói có tương quan tiêu hao ngân sách retry nhanh hơn nhiều so với mất gói ngẫu nhiên. Đây chính xác là điều xảy ra trong các sự cố upstream thực tế.

Kịch bản 3: Đường Kết Nối Suy Giảm Toàn Diện

Người dùng di động trên 4G tắc nghẽn, hoặc VPN tunnel qua ISP đang quá tải — kết hợp cả ba loại suy giảm:

tc qdisc add dev eth0 root netem \
  delay 150ms 50ms distribution normal \
  loss 3% \
  duplicate 0.5% \
  reorder 5% 50%

Kết hợp: độ trễ trung bình 150ms với jitter cao, 3% mất gói, 0.5% gói bị nhân đôi, và 5% sắp xếp lại thứ tự. Chạy ứng dụng của bạn trong điều kiện này mười phút và các giá trị timeout sai sẽ tự lộ diện.

Tôi đã áp dụng kịch bản này cho môi trường mô phỏng đường Singapore-Tokyo của chúng tôi. Chỉ trong vòng 10 phút integration testing, chúng tôi phát hiện ba gRPC client riêng biệt không cấu hình retry policy nào. Những lỗi đó đã âm thầm xảy ra trên production trong nhiều tuần.

Kịch bản 4: Giới hạn băng thông với netem + tbf

Netem xử lý độ trễ và mất gói, nhưng giới hạn băng thông cần kết hợp thêm một qdisc thứ hai — tbf (Token Bucket Filter):

# Đầu tiên, thêm netem làm root qdisc
tc qdisc add dev eth0 root handle 1: netem delay 80ms loss 1%

# Sau đó thêm tbf làm child để giới hạn ~1Mbit/s (mô phỏng mạng di động)
tc qdisc add dev eth0 parent 1:1 handle 10: tbf rate 1mbit burst 32kbit latency 400ms

Kết hợp hai qdisc này cho bạn độ trễ và mất gói kèm giới hạn băng thông cứng — xấp xỉ tốt tình huống người dùng di động với tín hiệu yếu.

Chỉnh Sửa Rule Mà Không Cần Xóa

Trong quá trình kiểm thử, bạn thường muốn điều chỉnh tham số mà không phải tháo toàn bộ. Dùng change:

# Tăng mất gói lên 5% trong khi giữ nguyên rule delay hiện tại
tc qdisc change dev eth0 root netem delay 80ms 20ms loss 5%

Xây Dựng Script Kiểm Thử cho Pre-Deployment

Sau vài tuần dùng netem theo kiểu thủ công, tôi gói các kịch bản phổ biến vào một shell script nhỏ nằm trong repo và chạy như một phần của checklist trước khi deploy:

#!/bin/bash
# network-chaos.sh — Áp dụng/xóa các suy giảm netem để kiểm thử trước khi deploy
# Cách dùng: ./network-chaos.sh [apply|remove] [scenario]

IFACE="eth0"

apply_scenario() {
  case "$1" in
    regional)
      echo "Đang áp dụng: độ trễ 80ms + jitter 20ms + mất gói 1%"
      tc qdisc add dev $IFACE root netem delay 80ms 20ms distribution normal loss 1%
      ;;
    degraded)
      echo "Đang áp dụng: độ trễ 150ms + jitter 50ms + mất gói 3% + reorder"
      tc qdisc add dev $IFACE root netem \\
        delay 150ms 50ms distribution normal \\
        loss 3% reorder 5% 50%
      ;;
    mobile)
      tc qdisc add dev $IFACE root handle 1: netem delay 120ms 40ms loss 2%
      tc qdisc add dev $IFACE parent 1:1 handle 10: tbf rate 2mbit burst 32kbit latency 400ms
      echo "Đang áp dụng: mô phỏng mạng di động (2Mbit, 120ms, mất gói 2%)"
      ;;
    *)
      echo "Kịch bản không hợp lệ: $1"
      exit 1
      ;;
  esac
}

remove_all() {
  tc qdisc del dev $IFACE root 2>/dev/null && echo "Đã xóa các rule netem" || echo "Không có gì để xóa"
}

case "$1" in
  apply) apply_scenario "$2" ;;
  remove) remove_all ;;
  *) echo "Cách dùng: $0 [apply|remove] [regional|degraded|mobile]" ;;
esac

Cần quyền root hoặc capability CAP_NET_ADMIN. Trong Docker, truyền --cap-add NET_ADMIN vào container chạy môi trường kiểm thử.

Xác Nhận Thiết Lập

Sau khi áp dụng các rule, hãy kiểm tra nhanh trước khi tin vào kết quả:

# Xem qdisc hiện tại
tc qdisc show dev eth0

# Đo độ trễ thực tế qua thống kê ping
ping -c 50 -q 8.8.8.8

# Hoặc dùng hping3 để xem thống kê ở mức TCP (thực tế hơn cho kiểm thử ứng dụng)
hping3 -c 100 -S -p 80 your-backend.example.com

Thực Tế Phát Hiện Được Gì

Sáu tháng chạy mọi candidate deploy qua ít nhất kịch bản regional đã tạo ra một danh sách nhất quán các lỗi bắt được trước production:

  • HTTP client không cấu hình timeout — chúng sẽ treo vô thời hạn khi gặp mất gói
  • Database connection pool không xử lý reconnect sau khi jitter đột biến kích hoạt TCP RST
  • WebSocket client âm thầm ngừng nhận message khi các gói bị sắp xếp lại thứ tự trúng vào edge case trong logic reassembly message
  • Queue consumer bị đói tài nguyên dưới độ trễ cao vì prefetch window quá nhỏ

Không cái nào lộ ra trong kiểm thử trên LAN. Tất cả đều xuất hiện trong vài phút dưới kịch bản mạng suy giảm.

Một Số Lưu Ý Thực Tế

Netem chỉ ảnh hưởng đến traffic gửi đi trên interface. Để suy giảm cả hai chiều, bạn cần rule trên cả hai phía của kết nối, hoặc dùng một network namespace hoặc VM riêng làm trung gian.

Với các thiết lập dựa trên container, chạy netem bên trong network namespace của container, hoặc dùng một sidecar container làm network proxy với các suy giảm áp dụng lên interface của nó.

Thêm một điều nữa: các rule netem tồn tại qua các lần khởi động lại process nhưng không tồn tại qua reboot. Nếu VM kiểm thử của bạn khởi động lại giữa chừng, các rule sẽ biến mất. Hãy để script apply/remove ở nơi dễ tìm.

Biến Điều Này Thành Quy Trình Chuẩn

Sự thay đổi thực sự không phải ở công cụ — mà là ở việc coi điều kiện mạng suy giảm là một phần bình thường của ma trận kiểm thử, không phải edge case. Mọi thay đổi liên quan đến mạng không tầm thường trong codebase của chúng tôi giờ đều phải qua ít nhất kịch bản regional trước khi được phê duyệt deploy.

Lần thiết lập đầu tiên mất khoảng năm phút. Các bug mà nó phát hiện sẽ tốn hàng giờ để chẩn đoán trên production — nếu bạn có thể tái tạo chúng. Một giờ kiểm thử trước deploy luôn tốt hơn phòng chiến tranh lúc 2 giờ sáng.

Hãy bắt đầu với một rule delay 80ms đơn giản, chạy ứng dụng của bạn, theo dõi log, và xem cái gì vỡ. Bạn sẽ tìm thấy điều gì đó — và bạn sẽ mừng vì đã tìm thấy nó ngay lúc này.

Share: