How recurring network maintenance exposed 6 bugs

Bài tóm tắt Jane Street 2026 3. Quy mô lớn Độ khó: Khó

Jane Street · 2026 · 3. Quy mô lớn · Độ khó: Khó — bài gốc

Bối cảnh

  • Jane Street bảo trì mạng nhiều vào cuối tuần; mỗi lần một tủ rack có thể mất kết nối 15–25 phút. Hạ tầng Kafka nội bộ lẽ ra phải chịu được và tự nối lại. Nhiều tháng năm 2026, nó không chịu được: process segfault, rò hàng trăm nghìn socket, hoặc sập trong một đợt nửa triệu kết nối TCP.
  • Nhân vật: relay (chép message giữa các cụm Kafka) và consumer-group monitor, mỗi host chạy hàng chục cái (79 trên một host), mỗi cái giữ pool kết nối tới nhiều broker và liên tục tra DNS. Có hai đường DNS: glibc qua Async_unix (đặt sau một throttle tối đa nửa thread pool, thêm vào sau một sự cố cũ) và dns-lib tự viết bằng OCaml (không qua throttle). Ephemeral port (28.232 cổng) dùng chung cả host.

Thay đổi chính

Sáu bug, mỗi cái nấp sau cái trước:

  1. glibc (bug 11 năm, từ 2015): res_init thất bại giữa chừng (ví dụ EMFILE khi hết file descriptor) để lại resolver nửa khởi tạo; lần tra DNS tiếp theo trên cùng thread segfault. Tái hiện bằng setrlimit và Async một thread; bản vá đã lên upstream glibc 2.44. Cùng chữ ký crash đã làm sập hệ giao dịch, job nghiên cứu, cả Chrome trong nhiều năm.
  2. Thư viện socket: bind UDP thất bại (hết ephemeral port) thì socket không được đóng; một process giữ 30.000 socket UDP chưa bind. Tái hiện bằng shim LD_PRELOAD.
  3. Async (hồi quy 14 năm, từ 2012): timeout của Tcp.connect_sock không còn bao phần tra DNS, nên lần kết nối kẹt ở DNS không bao giờ timeout.
  4. Vòng retry của Kafka client báo timeout nhưng không huỷ lần thử bên dưới.
  5. Bootstrap của Kafka client nhân bản các lần tra cùng một endpoint đồng thời (một comment "CR-someday" đã ghi sẵn rủi ro này).
  6. Migration job runner sang systemd user service lặng lẽ bỏ override giới hạn FD: từ 262.144 xuống 32.768, giảm 8 lần, bắt đầu rải 10% host cuối tháng 3.
  • Thundering herd khi mạng trở lại: trong 1.435 giây mất mạng, mỗi relay khoảng 1.500 consumer group × khoảng 18 chu kỳ retry 80 giây ≈ 27.000 lần kết nối bị "bỏ" nhưng vẫn xếp hàng DNS; khi throttle xả, tất cả mở kết nối cùng lúc (TCP từ 429 lên 474.033 trong nửa phút). Log ghi 27.898 và 27.786 lần thử trên hai relay; kết nối tự tan sau khoảng 10 phút, khớp connections.max.idle.ms 600 giây của Kafka.
  • Sửa ở từng tầng: glibc, socket library, Async (timeout bao DNS), Kafka client (huỷ lần thử hết hạn, cache lookup đang chạy), khôi phục giới hạn FD cho mọi job bị ảnh hưởng.

Vì sao

  • Mất ba lần điều tra vì mỗi lần đều "vào đồ thị từ một lá": crash chỉ nói "có áp lực FD", rò rỉ chỉ nói "có đợt cầu tăng vọt"; tới lần ba mới thấy cơn bão retry ở gốc. Phần nguồn gốc của đợt UDP tác giả ghi là giải thích có khả năng cao nhất, chưa kiểm chứng trực tiếp.
  • Hệ có dự phòng nên mỗi sự cố đều sống sót được, cho phép điều tra không vội.

Bài học cho SWE

  • Timeout không có nghĩa là huỷ. Kiểm xem timeout có bao đúng thao tác có thể treo, và việc hoàn thành muộn có còn tốn tài nguyên không.
  • Thiết lập nhiều bước phải failure-atomic; test thẳng đường lỗi bằng cách ép lỗi (setrlimit, LD_PRELOAD) thay vì dựng lại cả sự cố mạng.
  • Để giả thuyết dự đoán một con số rồi đối chiếu log.
  • Mô hình hoá tài nguyên ở đúng phạm vi chia sẻ (ephemeral port là của cả host, FD là của process).
  • Code cũ, dùng rộng rãi (glibc, hàm được gọi nhiều nhất) vẫn có thể sai.

Liên quan