Port monolith TypeScript sang Go mà không lệch hành vi
Tôi tự viết bản Node, rồi tự chuyển nó sang Go. Nghe thì dễ hơn — cho tới lúc nhận ra “tôi nhớ nó chạy thế nào” không phải một tiêu chí nghiệm thu.
- Go
- Migration
- Testing
Chuyển một service đang phục vụ khách hàng thật sang ngôn ngữ khác không phải bài toán cú pháp. Đến tuần thứ hai bạn sẽ thấy: phần khó là chứng minh bản mới cho ra đúng kết quả bản cũ, trong khi bản cũ vẫn đang chạy.
Trường hợp của tôi có một điểm tưởng là lợi thế: bản Node gốc do chính tôi viết. Không phải đọc code lạ, không phải đoán ý người khác. Nhưng đúng vì thế mà cái bẫy đổi hình dạng — ngữ nghĩa của hệ thống nằm trong đầu tôi, và trí nhớ của tác giả là thứ tiêu chí nghiệm thu tệ nhất mà bạn có thể chọn khi hệ thống ghi thẳng vào tiền của khách hàng.
Bắt đầu từ phần thuần tuý
Tôi không port endpoint trước. Tôi port phần logic không có I/O trước: định dạng dữ liệu, làm tròn số, mã hoá trường, phân tích công thức. Đó là phần duy nhất có thể so sánh tuyệt đối giữa hai ngôn ngữ.
Với mã hoá, tiêu chí là byte-identical:
// Ciphertext của Go phải trùng từng byte với createCipheriv của Node,
// kiểm bằng một known-answer vector sinh từ openssl.
func TestFieldEncryptorMatchesNode(t *testing.T) { /* ... */ }
Với lớp format, tôi chạy bản TypeScript qua ts-node và diff từng ca — kể cả thứ tự khoá trong JSON, vì phía dưới có hệ thống đọc theo vị trí.
Cron là nơi dễ sai nhất
Logic "job nào đến hạn" luôn phức tạp hơn vẻ ngoài: ưu tiên theo thời gian, ưu tiên theo giá trị, chuẩn hoá múi giờ, các trường hợp fallback. Đây là phần tôi viết test dày nhất, và cũng là phần duy nhất tôi tìm thấy khác biệt hành vi thật sự giữa hai bản.
Repository thì test bằng pgxmock, Redis bằng miniredis — CI không cần hạ tầng thật, nên vòng lặp phản hồi tính bằng giây.
Cutover phải đảo ngược được
Bản Go chạy song song bản Node trên cùng máy chủ, khác cổng, với một công tắc trong admin để chuyển backend. Nhờ vậy việc chuyển đổi được kiểm chứng bằng lưu lượng thật, và quay lại chỉ mất vài giây thay vì một lần rollback đầy lo lắng.
Điều tôi làm sai lúc đầu
Tôi từng để các handler trả về 501 kèm tên service sở hữu, coi như bản đồ ranh giới miền. Ý tưởng đúng, nhưng tôi giữ trạng thái đó hơi lâu: một route trả 501 trong nhiều tuần thì không ai biết nó thực sự đã sẵn sàng chưa. Lần sau tôi sẽ port theo chiều dọc từng miền một, cho tới khi miền đó phục vụ được lưu lượng thật, rồi mới mở miền kế tiếp.
Và một điều nữa: tôi đã bắt đầu bằng cách tin vào phần mình nhớ rõ nhất. Hoá ra những khác biệt hành vi duy nhất tôi tìm thấy đều nằm đúng ở đó — phần logic cron mà tôi tự tin là thuộc nằm lòng.