Courier: Dropbox migration to gRPC

Bài tóm tắt Dropbox 2019 3. Quy mô lớn Độ khó: Khó

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

Bối cảnh

  • Dropbox chạy hàng trăm service bằng nhiều ngôn ngữ, trao đổi hàng triệu request mỗi giây (2019). Trước Courier có giao thức tự serialize, Thrift cho pipeline log, và "legacy RPC" chính là HTTP/1.1 + protobuf.
  • Chọn gRPC chủ yếu vì giữ được protobuf sẵn có, cộng HTTP/2 multiplex và streaming hai chiều. Theo Dropbox, nếu fbthrift đã có thì có thể đã xét Thrift kỹ hơn.

Thay đổi chính

  • Courier không phải giao thức mới, mà là gRPC gắn vào hạ tầng riêng: identity, phân quyền, service discovery, stats, log, tracing. Phần lớn service gọi nhau trực tiếp, không qua proxy, để giảm latency.
  • Bảo mật: mọi server/client có cert do CA nội bộ cấp, identity nằm trong cert, mTLS bắt buộc, PFS bắt buộc, TLS 1.2+. ACL và rate limit theo service và theo method, cập nhật qua hệ config phân tán (AFS) trong vài giây mà không restart, nên chủ service shed load nhanh. Identity là khoá chung cho ACL, rate limit, stats.
  • Observability: stats phía client (availability, latency) và server (request rate, queue size), histogram theo method, tracing theo một phần OpenTracing (server trên Cassandra + Jaeger) cho ra đồ thị phụ thuộc lúc chạy. Codegen khởi tạo tĩnh cấu trúc stats để giảm overhead.
  • Độ tin cậy, thường là action item từ postmortem:
    • Deadline bắt buộc khai báo trong định nghĩa service; stub tự truyền deadline qua ranh giới API. Context còn đi ra ngoài RPC: ORM MySQL cũ ghi deadline vào comment SQL, SQLProxy đọc comment và KILL query quá hạn.
    • Circuit breaking bằng hàng đợi LIFO giới hạn cả kích thước lẫn thời gian chờ giữa listener và workpool; muốn giữ thứ tự thì dùng CoDel.
  • Hiệu năng: handshake TLS tốn CPU khi restart hàng loạt → đổi RSA 2048 sang ECDSA P-256 cho ký, sửa gRPC-core/gRPC-python để có session resumption. Mã hoá đối xứng rẻ (khoảng 40 Gbps một core) nên khi tốc độ mã hoá ngang tốc độ memcpy thì phải giảm số lần copy. Giới hạn BDP 16 MB cứng trong grpc-go làm nghẽn link liên vùng. Tách HTTP/1.1 và gRPC ra hai server riêng trong Go; dùng gogo/protobuf giảm CPU.
  • Codegen stub riêng thay vì interceptor: panic handler gom lỗi và dừng process khi panic (net/http mặc định bỏ qua rồi tiếp tục với trạng thái có thể hỏng), stats theo client tạo động, lỗi ứng dụng có mã riêng ánh xạ sang mã gRPC. Stub Python có tham số ctx tường minh và kiểu mypy (mã nguồn mở thành mypy-protobuf).
  • Migration 4 bước: (0) đóng băng tính năng của legacy RPC, tính năng mới như tracing và streaming chỉ có trên Courier; (1) codegen sinh một interface chung cho cả hai; (2) chuyển từng service sang interface mới nhưng vẫn chạy legacy RPC, bước dễ lỗi nhất nên chỉ đổi một biến; (3) chạy cả hai server trong cùng binary trên hai port, client đổi bằng diff một dòng; (4) chứng minh legacy không còn được dùng (đọc code và stats của server cũ) rồi xoá.

Vì sao

  • Framework RPC chung là chỗ đặt cải tiến độ tin cậy cho toàn hạ tầng một lần (deadline, chống quá tải), thay vì mỗi client tự viết backoff và jitter.

Bài học cho SWE

  • Theo nhóm tác giả: observability là tính năng; chuẩn hoá giảm tải nhận thức; codegen để giảm boilerplate; migration tốn thời gian hơn phát triển và chỉ xong khi đã dọn; gom báo cáo sự cố theo quý để tìm lỗi lặp lại đáng sửa ở tầng RPC.
  • Đóng băng hệ cũ và chỉ thêm tính năng ở hệ mới tạo động lực chuyển mà không cần ép.
  • Hàng đợi chờ phải có giới hạn thời gian, không chỉ kích thước; LIFO phục vụ request mới (client còn chờ) trước request cũ (client có thể đã bỏ).
  • TLS nội bộ còn bắt được lỗi phần cứng (CPU, DMA, mạng làm hỏng dữ liệu).

Liên quan