Một con số giá, ba chỗ ở
Ở VNTrip, bảo hiểm trễ chuyến đi kèm đơn vé máy bay. Điều đó cho một con số mà khách hàng nhìn thấy — đơn này giá bao nhiêu, và bảo hiểm che những gì — tận ba chỗ ở riêng biệt: các row bảo hiểm trong service bảo hiểm, các chặng bay trong dữ liệu chuyến bay của đơn, và tổng tiền trong service đơn hàng. Đổi một chặng thì bảo hiểm phải đổi theo, vì hợp đồng che những chuyến bay cụ thể. Đổi bảo hiểm thì tổng tiền phải đổi theo, vì khách trả đúng một con số. Ba sự thật, ba service, ba code path do những người khác nhau viết vào những thời điểm khác nhau.
Path nào cũng cẩn thận cả. Service bảo hiểm validate row của nó. Service đơn hàng tính tổng từ input của chính nó. Dữ liệu chuyến bay đúng y những gì phía hàng không đã xác nhận. Chạy bất kỳ test nào lên bất kỳ một trong ba cái, nó đều pass, vì từng service đều nhất quán nội bộ.
Vậy mà một đơn vẫn có thể mang row bảo hiểm cho một chặng không còn tồn tại, hoặc một tổng tiền không còn bao gồm một hợp đồng vẫn đang có. Không có gì sai ở bất cứ đâu; cái sai nằm trọn vẹn ở giữa các service.
Con bug không có địa chỉ
Đây là đoạn đáng dừng lại lâu nhất. Khi một case support rơi xuống — khách bị tính tiền cho phần bảo hiểm không khớp với chuyến bay của họ — không có service nào để gán case vào cả. Team bảo hiểm chứng minh được row của họ hợp lệ. Team đơn hàng chứng minh được tổng tiền khớp với input của họ. Ai cũng đúng, và chính vì thế mà cái case cứ bị đá qua đá lại. Một chỗ lệch nằm trong một phép join mà không service nào tính, là một con bug không có địa chỉ: chủ nào cũng có thể nói thật lòng "không phải của tôi".
Nguyên nhân thường gặp thì vẫn là mấy nghi phạm quen mặt — một chặng đổi sau khi hợp đồng đã gắn vào, một partial failure giữa hai lần ghi lẽ ra phải đi cùng nhau, một cú retry đẩy được một bên mà không đẩy bên kia. Nhưng ngồi liệt kê nguyên nhân là lạc đề, vì vấn đề sâu hơn nằm ở chỗ sẽ không có gì báo cho mình biết cả. Mỗi ca như vậy để lại sau lưng ba service xanh lè. Có bao nhiêu đơn đang ở trạng thái lệch trước khi mình bắt đầu đo — mình không nói được, và sự vắng mặt của con số đó không phải một câu văn hoa: nó chính là cái phát hiện.
Cái đã thay đổi
Mình viết ra phép join. Một job đối soát tính cả ba sự thật, mỗi cái từ đúng nguồn có thẩm quyền của nó — row bảo hiểm đúng như service bảo hiểm đang lưu, các chặng đúng như dữ liệu chuyến bay của đơn đang nói, tổng tiền đúng như service đơn hàng báo — rồi diff chúng với nhau. Nó không tin bản cache mà service này giữ về service kia; nó hỏi thẳng từng bên và so sánh. Đầu ra là một danh sách delta: đơn này có bảo hiểm trỏ vào một chặng không nằm trong đơn; đơn kia có tổng tiền đang thiếu phí của hợp đồng này.
Hai thứ xung quanh cái job đó gánh phần lớn giá trị:
Re-sync theo yêu cầu cho một đơn lẻ. Khi báo cáo, hoặc support, chỉ ra một đơn, một lệnh duy nhất tính lại các sự thật phụ thuộc cho đúng đơn đó và chỉ đơn đó. Việc sửa thôi không còn là một câu UPDATE gõ tay dựng vội dưới áp lực, mà thành cùng một code path cho mọi lần.
Export CSV. Báo cáo phủ một giai đoạn và export ra được, vì người dùng dữ liệu này không phải lúc nào cũng là engineer. Một người bên tài chính hay vận hành có thể mở nguyên một tháng, thấy từng chỗ lệch, và audit lại các lần sửa — điều đó cũng có nghĩa là bản đối soát được nhìn tới, vì một báo cáo không ai đọc chỉ là phiên bản chậm hơn của việc không có báo cáo.
Quy tắc
Một con số tồn tại trong ba service thì chưa có chủ, cho tới khi bạn viết ra cái thứ so sánh chúng. Sở hữu một sự thật không phải là "bản copy của tôi hợp lệ" — service nào trên cái đơn đó cũng tuyên bố được câu ấy. Sở hữu là làm cái đoạn code có thể nói rằng các bản copy đang khớp nhau, và ngay khi một con số như vậy tồn tại, đoạn code đó không phải hạ tầng tuỳ chọn; nó chính là nơi tính đúng của con số thật sự sống. Nếu vẽ sơ đồ hệ thống ra mà thấy cùng một giá trị nằm trong nhiều hơn một cái hộp, thì mũi tên giữa các hộp là một component bạn phải xây — vì chừng nào chưa xây, nó là một component đang lặng lẽ không tồn tại.