Bỏ qua tới nội dung
Kiên

Webhook là một tối ưu, không phải nguồn sự thật

· 3 phút đọc

callback + poll

Bên bảo hiểm xác nhận cấp hợp đồng theo kiểu bất đồng bộ. Callback là đường nhanh — và cũng là đường có thể mất, đến hai lần, hoặc đến trước khi transaction của chính mình kịp hiển thị. Cái poll định kỳ không phải phương án dự phòng; nó mới là câu trả lời thật. Callback chỉ làm câu trả lời đến sớm hơn.

  • integration
  • reliability
  • hệ phân tán
  • backend
Nội dung
  1. Bối cảnh
  2. Ba cách một callback nói dối
  3. Cái poll mới là thiết kế
  4. Quy tắc

Bối cảnh

Ở VNTrip, mình gắn bảo hiểm trễ chuyến vào đơn vé máy bay. Hợp đồng thật thì bên bảo hiểm cấp, theo kiểu bất đồng bộ: mình gửi yêu cầu, họ trả lời "đã nhận", còn xác nhận cấp hợp đồng thì đến sau. Tích hợp của họ có sẵn cái khuôn quen thuộc cho chuyện này — khi hợp đồng được cấp, họ gọi vào một webhook của mình.

Bản thiết kế đầu tiên coi cái callback đó là cơ chế chính. Callback về là hợp đồng confirmed; xong. Đó là thiết kế hiển nhiên nhất, là thiết kế mà tài liệu của bên bảo hiểm ngầm gợi ý, và nó sai theo ba cách rất cụ thể — cả ba đều vô hình vào cái ngày mình ship nó.

Ba cách một callback nói dối

Nó có thể mất. Cú gọi outbound của họ băng qua internet công cộng để vào ingress của mình. Một lần deploy phía mình, một cú giật mạng ở bất kỳ phía nào, một retry policy bên họ bỏ cuộc — và cái message đơn giản là biến mất. Phía mình không có gì báo lỗi, vì với phía mình thì chẳng có gì xảy ra cả. Đơn hàng nằm ở "pending" mãi mãi, và thứ khách hàng cuối cùng cảm nhận được là một cái bảo hiểm đã trả tiền mà không thấy đâu.

Nó có thể đến hai lần. Retry tồn tại chính vì việc giao message không đáng tin, nên cùng một xác nhận có thể rơi xuống hai lần hoặc hơn. Nếu áp nó hai lần mà gây ra bất kỳ điều gì — ghi đúp một row, bắn lại một cái mail cho khách — thì chính cái retry sinh ra để cứu bạn lại là thứ tạo ra incident.

Nó có thể đến trước khi write của chính mình kịp hiển thị. Đây là cái cắn cả những người đã xử lý xong hai cái trên. Mình tạo bản ghi hợp đồng local trong một transaction, gọi sang bên bảo hiểm, rồi commit. Vào một ngày mạng nhanh, callback của họ quay về trước khi cái commit đó hiển thị với connection đang xử lý webhook. Handler tra đơn hàng, không thấy gì, và thế là cái message đúng nhất, nhanh nhất mà bên bảo hiểm từng gửi cho mình bị vứt đi — bởi chính mình.

Từng trường hợp trên thật sự xảy ra bao nhiêu lần trên production, mình chưa bao giờ đếm, và đó chính là điểm mấu chốt: từ khi có thiết kế bên dưới, câu hỏi đó thôi không còn quan trọng nữa.

Cái poll mới là thiết kế

Cách sửa không phải là một webhook tốt hơn. Là giáng chức cái webhook. Một job định kỳ poll sang bên bảo hiểm cho mọi đơn còn đang chờ xác nhận và áp đúng trạng thái tìm thấy. Job đó là nguồn sự thật: callback không bao giờ đến thì poll lấy được câu trả lời; callback đến quá sớm và bị vứt thì poll lấy được câu trả lời; đến hai lần thì xem đoạn dưới. Webhook vẫn giữ, vì khách hàng thích nhanh — nhưng việc duy nhất của nó bây giờ là làm câu trả lời đến sớm hơn nhịp poll kế tiếp. Mất nó thì tốn độ trễ, không tốn tính đúng.

Cú lật ngược đó ép bước áp trạng thái phải có hai tính chất:

Idempotency. Callback và poll giờ đều có thể mang về cùng một xác nhận, nên áp nó hai lần phải an toàn. Bước áp là một bước chuyển trạng thái — pending sang confirmed kèm số hợp đồng của bên bảo hiểm — và một bước chuyển đã xảy ra rồi thì là no-op, không phải lỗi. Giữ được điều đó thì retry, message trùng, và cả cuộc đua callback-với-poll đều sập về cùng một trường hợp vô hại.

Đối soát. Poll trả lời câu "bên bảo hiểm nói gì"; nó không trả lời câu "hai bên có khớp nhau không". Một báo cáo riêng đi qua cả hai phía — row của mình, trạng thái hợp đồng của họ — và liệt kê mọi đơn mà hai bên lệch nhau, theo cả hai chiều. Nó không tin phía nào cả; nó so sánh. Chỗ lệch export ra CSV để một con người audit được cả một giai đoạn, và một đơn lẻ có thể re-sync theo yêu cầu thay vì chờ batch. Báo cáo đó tìm ra những chỗ lệch có thật; còn nó sẽ tìm ra bao nhiêu trong những tháng trước khi nó tồn tại thì mình không thể biết, vì khi đó không có gì đang nhìn cả.

Quy tắc

Nếu mất một message trong im lặng làm thay đổi một trạng thái mà khách hàng nhìn thấy, thì trạng thái đó cần một cái poll, không phải một cái webhook to mồm hơn. Cứ gia cố callback bao nhiêu tuỳ thích — retry, queue, chữ ký — nó vẫn là một cú push băng qua một đường biên bạn không kiểm soát, và chế độ hỏng của push là sự im lặng. Poll biến sự im lặng trở lại thành một câu hỏi mà bạn tự đặt, theo lịch của chính bạn. Webhook là một tối ưu. Một tối ưu tốt. Và nó chỉ là như vậy thôi.