Mercury và hai triệu dòng Haskell: Bài học về kỹ thuật sản xuất ở quy mô lớn

03 tháng 5, 2026·9 phút đọc

Ian Duncan chia sẻ kinh nghiệm vận hành hệ thống Haskell khổng lồ với 2 triệu dòng mã tại Mercury, một công ty fintech phục vụ hàng trăm nghìn doanh nghiệp. Bài viết đề cập đến triết lý về độ tin cậy, việc sử dụng hệ thống kiểu để mã hóa kiến thức vận hành, và các bài học thực tế về việc duy trì, mở rộng và quan sát hệ thống phức tạp.

Mercury và hai triệu dòng Haskell: Bài học về kỹ thuật sản xuất ở quy mô lớn

Tại Mercury, chúng tôi có khoảng 2 triệu dòng mã Haskell. Đây là câu chuyện về cách chúng tôi biến một ngôn ngữ lập trình thường bị coi là "học thuật" thành xương sống cho một hệ thống tài chính quy mô lớn, xử lý hàng trăm tỷ đô la giao dịch mỗi năm.

Haskell trong chiến thực: Từ đam mê tuổi trẻ đến hạ tầng tài chính

Tôi lần đầu tiên nghe đến Haskell khi mới 16 tuổi, trong giờ học khoa học máy tính nơi chúng tôi đang vật lộn với Java và những lỗi NullPointerException kinh điển. Khi tình cờ đọc thấy một bài viết về một ngôn ngữ mà lỗi con trỏ null là không thể tồn tại, nơi hệ thống kiểu có thể ngăn chặn toàn bộ một loại lỗi lập trình, tôi đã bị mê hoặc ngay lập tức.

Gần hai thập kỷ trôi qua, tôi vẫn tin rằng giá trị cốt lõi của Haskell là đúng đắn. Nhưng điều tôi mất nhiều thời gian hơn để học hỏi là: Lời hứa đó trông như thế nào khi cơ sở mã (codebase) mở rộng lớn, công ty phát triển nhanh hơn tài liệu hướng dẫn, và hệ thống thực sự bắt đầu xử lý tiền bạc? Tại Mercury, một công ty fintech cung cấp dịch vụ ngân hàng cho hơn 300.000 doanh nghiệp và xử lý 248 tỷ USD giao dịch trong năm 2025, chúng tôi đã tìm ra câu trả lời.

Tư duy về Độ tin cậy: Khả năng thích ứng

Có một cách truyền thống để nghĩ về độ tin cậy của hệ thống: tập trung vào việc ngăn chặn sự cố. Bạn liệt kê mọi thứ có thể sai, thêm các kiểm tra và viết bài test. Tuy nhiên, chúng tôi tiếp cận vấn đề differently. Một hệ thống hoạt động tin cậy是因为 nó có khả năng hấp thụ sự thay đổi (variation): nó giảm hiệu suất một cách êm đẹp (degrades gracefully), người vận hành có thể hiểu và điều chỉnh nó.

Độ tin cậy không phải là sự vắng mặt của lỗi. Đó là sự hiện diện của khả năng thích ứng (adaptive capacity). Khi bạn có hàng trăm kỹ sư làm việc trên một cơ sở mã nhiều triệu dòng, trong đó nhiều người mới chỉ học Haskell được 6 tháng, "khả năng thích ứng" không còn là một khái niệm trừu tượng mà là mối quan tâm hàng ngày.

Trong bối cảnh này, tôi ngày càng coi hệ thống kiểu (type system) của Haskell là một công cụ hỗ trợ vận hành hơn là một bằng chứng về tính đúng đắn (correctness proof). Giá trị của nó không chỉ là loại bỏ các lớp lỗi, mà là mã hóa kiến thức tổ chức dưới một hình thức tồn tại ngay cả khi người viết nó đã rời đi.

Sự tinh khiết (Purity) là một ranh giới, không phải thuộc tính

Một sự hiểu lầm phổ biến về Haskell là coi sự tinh khiết (purity) là một thuộc tính của ngôn ngữ. Thực tế, đó là một cái gì đó mà các giao diện (interfaces) của bạn áp đặt. Dưới "mặt nạ" của các thư viện chuẩn như bytestring hay text là một "địa ngục nhỏ" của việc cấp phát bộ nhớ thay đổi (mutable allocation) và các thao tác không an toàn. Điều làm cho nó có thể chấp nhận được là các tác động phụ (side effects) được đóng gói (encapsulated) kỹ lưỡng sao cho ranh giới không thể bị vi phạm.

Trong sản xuất, mục tiêu thường không phải là tránh hoàn toàn việc thay đổi trạng thái (mutation), mà là chứa chặt nó, làm cho sự chứa chặn đó dễ đọc và xác minh rằng nó vẫn nằm trong phạm vi. Câu hỏi đúng không phải là "đây có tinh khiết không?" mà là "sự không tinh khiết ở đâu, và bao nhiêu phần của codebase được phép biết về nó?".

Làm cho việc đúng trở nên dễ dàng

Trong các cơ sở mã lớn, tính chính xác thường phụ thuộc vào việc thực hiện các thao tác theo một thứ tự cụ thể hoặc bao gồm một bước nhất định mà không liên quan trực tiếp đến công việc chính. Ví dụ: "Hãy luôn nhớ ghi nhật ký kiểm tra (audit log) sau mỗi giao dịch". Những câu thần chú này thường sống trong các trang wiki hoặc ký ức của các kỹ sư cấp cao.

Haskell cung cấp các công cụ để mã hóa những câu thần chú này vào các kiểu (types) để chúng không thể bị quên. Ví dụ, thay vì để kỹ sư nhớ phải gọi hàm xuất bản sự kiện sau khi ghi giao dịch, bạn có thể thiết kế kiểu dữ liệu sao cho cách duy nhất để hoàn thành một giao dịch là đi qua đường dẫn bao gồm việc xuất bản sự kiện đó.

Điều này biến quy trình vận hành đúng đắn thành con đường ít sức kháng nhất (path of least resistance). Trình biên dịch ở đây không chỉ kiểm tra logic, mà còn bảo vệ trí nhớ của tổ chức và biến nó thành một giao diện cứng nhắc.

Thực thi bền vững (Durable Execution) với Temporal

Các hệ thống tài chính chứa đầy các quy trình spanning nhiều bước, nhiều dịch vụ và nhiều chế độ lỗi. Trước đây, Mercury điều phối các quy trình này bằng các máy trạng thái (state machines) dựa trên cơ sở dữ liệu và các công việc cron. Nó hoạt động, nhưng rất mong manh và khó lý luận.

Chúng tôi đã chuyển sang sử dụng Temporal, một khung thực thi bền vững. Bạn viết quy trình làm việc (workflow) của mình dưới dạng mã tuần tự thông thường, và nền tảng ghi lại từng bước trong lịch sử sự kiện. Nếu một worker bị sập giữa chừng, worker khác sẽ phát lại lại phần đầu tiên để tái tạo trạng thái, sau đó tiếp tục từ nơi nó dừng lại.

Tôi coi Temporal như một vật thể nhân tạo (prosthetic) cho những ngôn ngữ không có khả năng xử lý sự cố bẩm sinh như Erlang. Nó cung cấp các đức tính vận hành tương tự thông qua các phương tiện hiệu quả nhưng hơi điên rồ.

Thiết kế cho miền (Domain) của bạn, không phải cho lớp vận chuyển (Transport)

Một sai lầm phổ biến khi hệ thống phát triển là để cho hệ thống gọi (invoking system) rò rỉ vào mô hình miền (domain model). Ví dụ, mã ném ra các ngoại lệ mã trạng thái HTTP (ví dụ: 409 Conflict) trực tiếp từ logic nghiệp vụ. Điều này có ý nghĩa khi mã chạy trong trình xử lý yêu cầu HTTP, nhưng sau này khi mã đó được tái sử dụng trong cron jobs hoặc background workers, việc một cron job ném ra lỗi 409 là vô nghĩa.

Giải pháp là mô hình hóa các lỗi miền dưới dạng các kiểu miền (domain types). Một khoản thanh toán thất bại vì thiếu tiền nên là InsufficientFunds, không phải lỗi 402. Sau đó, bạn viết các lớp chuyển đổi mỏng tại mỗi ranh giới để chuyển đổi lỗi miền thành lỗi HTTP hoặc chiến lược worker tương ứng.

Sự đánh đổi trong mã hóa Kiểu

Mã hóa các bất biến (invariants) vào hệ thống kiểu rất mạnh mẽ, nhưng cũng đắt đỏ. Nó không tốn chi phí chạy thời gian (runtime), nhưng tốn chi phí nhận thức (cognitive overhead). Mọi bất biến bạn đưa vào hệ thống kiểu là một ràng buộc đối với mọi kỹ sư trong tương lai chạm vào đoạn mã đó.

Nếu vi phạm ràng buộc gây ra mất dữ liệu hoặc lỗi tài chính, chi phí là đáng giá. Nhưng nếu ràng buộc chỉ là "chúng tôi hiện đang làm cách này", bạn có thể đã vô tình làm cho codebase khó thay đổi hơn mà không mang lại lợi ích vận hành.

Điểm ngọt (sweet spot) nằm ở giữa: Mã hóa các bất biến bảo vệ chống lại sự hỏng hóc thầm lẻ (silent corruption), sử dụng kiểm tra thời gian chạy cho các lỗi gây ra tiếng ồn lớn, và nhớ rằng các kiểu là dành cho nhóm, không chỉ dành cho trình biên dịch.

Thiết kế để có khả năng nội soi (Introspection)

Độ tin cậy là về khả năng thích ứng, và nội soi là một cách bạn đạt được điều đó. Người vận hành không thể hiểu những gì họ không thể nhìn thấy.

Haskell không có "monkey patching" (sửa mã runtime). Nếu một thư viện exposing chức năng dưới dạng một tập hợp các hàm cụ thể, các tùy chọn của bạn để đo lường nó (instrumentation) là rất hạn chế. Giải pháp tôi thường sử dụng là "records of functions" (các bản ghi chứa hàm). Thay vì exposing một module đầy các hàm cụ thể, bạn exposing một bản ghi có các trường là các hàm. Người gọi có thể gói (wrap), đo lường, giả lập (mock) hoặc thay thế bất kỳ hàm riêng lẻ nào mà không cần chạm vào phần còn lại.

Mẫu này cho phép kết hợp (compose) các mối quan tâm chéo (cross-cutting concerns) như tracing, logging, và timeout một cách chính xác nhờ vào các thuộc tính đại số như Monoid. Bạn không cần viết plumbing tùy chỉnh để kết hợp tracing interceptor với timeout interceptor; bạn chỉ cần mconcat chúng.

Lời nhắn cho các tác giả thư viện

Nếu bạn đang viết một thư viện Haskell, hãy để lại các lối thoát (escape hatches). Hãy cung cấp các bản ghi hàm, các kiểu hiệu ứng (effect types), hoặc callbacks để người dùng có thể chèn hành vi mà không cần sửa mã của bạn. Hãy cân nhắc thêm instrumentation OpenTelemetry vào gói của bạn. Và hãy xin đừng ghi log trực tiếp từ mã thư viện. Hãy cung cấp một callback logging hoặc chấp nhận một logger dưới dạng tham số; để ứng dụng quyết định cách nó được vận hành.

Hai triệu dòng Haskell听起来 là một cơn ác mộng bảo trì, nhưng với cách tiếp cận đúng đắn—coi hệ thống kiểu là công cụ vận hành, tách biệt logic nghiệp vụ khỏi chi tiết vận chuyển, và ưu tiên khả năng quan sát—nó có thể là nền tảng cho một hệ thống tài chính cực kỳ mạnh mẽ và đáng tin cậy.

Bài viết được tổng hợp và biên soạn bằng AI từ các nguồn tin tức công nghệ. Nội dung mang tính tham khảo. Xem bài gốc ↗