Session revocations at scale

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

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

Bối cảnh

  • Session của hàng trăm triệu người dùng nằm trong cookie mã hoá (user ID, quyền, vai trò), nên gateway tin được cookie mà không cần gọi kho dữ liệu cho mỗi request (hàng trăm nghìn lần mỗi giây).
  • Đăng xuất hoặc đổi quyền thì phải thu hồi gần như tức thời. Mỗi gateway giữ danh sách thu hồi trong bộ nhớ cho 12 giờ gần nhất (cookie tự làm mới trong 12 giờ; lúc làm mới thì tra MySQL).
  • Vấn đề: hàng trăm pod gateway, mỗi pod khi khởi động kéo hơn một triệu bản ghi thu hồi từ MySQL. Mỗi lần deploy thành một đợt dồn dập vào database. Tạm chữa bằng nhiều read replica.

Thay đổi chính

  • Không chọn Redis làm cache trung gian: theo Canva, Redis thường không chạy cấu hình bền vững hoàn toàn, vẫn phải vận hành cụm, và chỉ là dời vấn đề sang một kho khác cộng thêm việc giữ nhất quán.
  • Chọn S3: bền, rẻ, hợp để tải file lớn. Chia cửa sổ trượt 12 giờ thành khối 30 phút, mỗi khối một object; tên object chứa thời điểm bắt đầu nên liệt kê theo thứ tự là tìm được các khối cần.
  • Định dạng nhị phân: mỗi thu hồi (principal + mốc thời gian đăng nhập, vài bit cờ cho các loại thu hồi khác) gói vào 16 byte; mỗi khối là mảng phẳng đã sắp xếp, tra bằng binary search ngay trên byte tải về, không cần chuyển đổi. Bộ nhớ giảm 87,5% (8 lần) so với giữ nhiều object Java mỗi bản ghi.
  • Worker bất đồng bộ quét DB, lấy lô thu hồi mới, chèn vào khối mới nhất rồi ghi lại lên S3.
    • Chạy nhiều bản worker để sẵn sàng cao; tránh mất cập nhật bằng PUT có điều kiện (optimistic concurrency): chỉ ghi nếu khối chưa đổi từ lúc đọc.
    • Bầu leader bằng ZooKeeper chỉ là tối ưu để bớt xung đột, không phải cơ chế đúng đắn: một node có thể dừng tạm lâu rồi tỉnh dậy ghi đè, chỉ có điều kiện PUT chặn được.
    • Độ phức tạp O(N²) trên lý thuyết, nhưng gom vài trăm bản ghi mỗi lô thì vẫn đạt hơn 2.000 thu hồi/giây, dư so với nhu cầu; nút thắt thực tế là latency mạng.
  • Gateway cập nhật liên tục bằng GET có điều kiện (chỉ tải lại khối nếu đã đổi): kể cả 1 triệu thu hồi trong 30 phút cũng chỉ 16 MB, vài lần mỗi phút. Khối cũ hơn 12 giờ thì bỏ.
  • Kết quả: deploy nhanh hơn; chỉ còn 2 read replica (để dự phòng); tải DB giờ theo tốc độ ghi và traffic, không còn theo số pod khởi động cùng lúc.

Vì sao

  • Tải lúc khởi động tỉ lệ với số instance × kích thước dữ liệu, không tỉ lệ với traffic người dùng; thêm replica chỉ là chạy theo. Chuyển phần "tải hàng loạt" sang kho được thiết kế cho việc đó (object storage) thì DB chỉ còn phục vụ ghi.
  • Theo Canva, bài học lớn nhất là khác biệt giữa scale trên lý thuyết và thực tế: hằng số rất quan trọng, mảng đặc xử lý cực nhanh. Họ dựng thử vài phương án ở quy mô thật trước khi chọn (bài nhắc việc dùng AI coding agent để làm nhanh các bản thử).

Bài học cho SWE

  • Deploy là một nguồn thundering herd: mọi instance cùng nạp cache lúc khởi động (Hot partition và thundering herd).
  • Cache cục bộ cần đường nạp và đường cập nhật riêng khỏi database nguồn; object storage + GET có điều kiện là một lựa chọn rẻ (Cache và invalidation).
  • Trước khi thêm read replica, hỏi tải đọc đó đến từ người dùng hay từ chính hệ thống (Scale dọc trước khi scale ngang).
  • Leader election không thay được fencing; tính đúng đắn phải nằm ở thao tác ghi (điều kiện, version).

Liên quan