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

75 mã nhà cung cấp trên 31 module — và một cuộc di trú giữa lúc đang chạy

Lớp gom nhiều wholesaler về một hợp đồng API duy nhất: mỗi supplier là một module, khác biệt giữa thương hiệu đẩy xuống dữ liệu, trạng thái giữa các bước nằm trong chính id.

Thời gian
05/2024 – nay
Vai trò
Người thiết kế contract cho module supplier và mô hình di trú, kiêm chủ release train của gateway khách sạn

Công nghệ

  • Node.js
  • Koa 2
  • Express 4
  • MySQL + Knex
  • Redis (ioredis)
  • AWS SQS
  • BullMQ
  • camaro + xmlbuilder
  • Docker
  • Kubernetes
  • DataDog APM

Vấn đề

Mỗi wholesaler một giao thức, một hệ mã khách sạn, một mô hình phòng và giá, một cách viết chính sách huỷ — nhưng tất cả phải trả lời chung một hợp đồng API.

Thêm hai chuyện: cùng một tích hợp phải bán lại dưới nhiều thương hiệu, mỗi thương hiệu có credential và cột mã khách sạn riêng; và hai thế hệ của stack khách sạn đang chạy song song trên production, chia trên hai repo, phần lớn supplier tồn tại ở cả hai.

Cách làm

Bốn quyết định gánh toàn bộ thiết kế. Quyết định nào cũng viết theo cùng một khung: lúc đó có gì trên bàn, đã chọn gì, và trả giá bằng gì.

Quyết định — mỗi supplier một module, chung một contract cố định. Trên bàn: một code path dùng chung, rẽ nhánh theo từng tính năng của từng supplier — lối mòn ai cũng đi khi wholesaler thứ hai xuất hiện, và cũng là cách biến mỗi khác biệt giao thức thành thêm một cái if. Đã chọn: mỗi supplier là một module với contract cố định, đăng ký bằng mã trong một registry duy nhất. Trả giá: hành vi viết theo từng module, nên fix supplier là fix từng module một — còn đổi chính cái contract thì đụng cả 31 module cùng lúc.

Quyết định — hai pattern tạo offer có tài liệu, không phải mỗi supplier một luồng riêng. Trên bàn: để mỗi wholesaler giữ nguyên mô hình phòng-giá của nó, mỗi bên bọc một luồng riêng. Đã chọn: mọi supplier được xếp vào một trong hai pattern đã viết thành tài liệu — pattern A khi supplier trả một token đặt được N phòng, pattern B khi mỗi phòng là một giá độc lập và danh sách offer là ma trận các tổ hợp hợp lệ. Trả giá: pattern B trả trước cho sự đều đặn — ma trận được dựng sẵn ngay vào danh sách offer thay vì để sau mới ráp; và supplier nào không khớp pattern nào là một buổi bàn thiết kế, không phải một bản vá nhanh.

Quyết định — trạng thái giữa các bước nằm trong id, không nằm trong session server. Trên bàn: session phía server — thứ ai cũng với tới trước tiên, và cũng là thứ hết hạn khi client chậm, lệch nhau giữa các instance. Đã chọn: offer id ghép có version, encode source · contract · hotel · uuid, prebook decode ra là biết dùng credential nào — không có gì để hết hạn, không có gì để lệch giữa các instance. Trả giá: bản thân cái id thành một contract riêng — phải decode được qua các version, và bước sau cần gì thì thứ đó phải nhét vừa vào id.

Quyết định — deadline tuyệt đối 30 giây thay vì chờ theo phần trăm tiến độ. Search fan-out bằng hàng đợi: mỗi supplier một job có Redis lock, worker ghi vào một hash chung kèm hash meta đánh dấu supplier nào xong; tầng HTTP gom lại và trả phần trăm tiến độ. Trên bàn: trả phần trăm đó cho tới khi supplier cuối cùng trả lời — tức là giao cho supplier chậm nhất quyền quyết định mọi lượt search kết thúc lúc nào, và client thì poll mãi. Đã chọn: một deadline tuyệt đối 30 giây đè lên phần trăm — quá hạn thì báo xong để client thôi poll. Trả giá: supplier trả lời muộn đơn giản là vắng mặt trong result set lượt đó; offer của nó bị bỏ khỏi lượt search đó chứ không được chờ.

Chính deadline đó biến mỗi module thành một failure domain riêng:

các module supplier module — kịp hạn module — kịp hạn module — kịp hạn module — quá 30 giây deadline tuyệt đối 30 giây bị loại — chỉ lượt search này một result set gộp chung offer của các supplier còn lại vẫn trả về vùng tô — failure domain, gói trong một module nét liền — biên deploy, và vạch cắt deadline
Supplier quá deadline 30 giây chỉ bị cắt khỏi result set của lượt search đó — không gì khác xảy ra với nó hay với ai. Biên nằm theo từng module nên failure domain cũng theo từng module. Con số duy nhất trong hình là 30 giây; còn lại là hình khối.

Quanh bốn quyết định là hai nước đi đỡ. Phần khác nhau giữa các thương hiệu bị đẩy xuống dữ liệu: một bảng setting thưa chỉ chứa key khác default, và một map từ mã supplier sang cột mã khách sạn tương ứng.

Còn migration thì không big-bang: dòng mới là dòng duy nhất viết code mới, dòng cũ read-only, chuyển từng supplier một — và quy tắc ghi rõ là fix supplier phải áp cả hai repo, vì sửa một bên sẽ để bên kia hỏng.

Kết quả

Mình chạy cả release train chứ không chỉ viết code — đưa nhánh integration dùng chung lên production trên cả 8 service, tức là chịu trách nhiệm cả chuyện cái gì được lên và lên lúc nào.

75 mã supplier hiện chạy trên 31 module nhờ alias, mirror giữa service search và service booking. Tính năng cross-cutting được ship ở cả năm gateway trong cùng một ngày.