Đảm bảo toàn vẹn dữ liệu với gRPC streams

Trước khi bắt đầu tìm hiểu về toàn vẹn dữ liệu với gRPC chúng ta hãy điểm qua những yêu cầu tối thiểu cho một lập trình viên web trong những năm 2000. Ngày xưa, chúng ta có thể xây dựng một website chủ với những ngôn ngữ lập trình backend (như PHP, Python, Ruby). Chúng ta cần một chút javascript/jQuery và khả năng cắt HTML từ psd. Vầy là đủ. Mã nguồn thì được triển khai chủ yếu sử dụng FTP. CI/CP, Pipelines, DevOps, SRE và những khái niêm khác không nằm trong danh sách những yêu cầu tối thiểu phải biết với những lập trình viên thời bấy giờ.

Tổng quan về cuộc cách mạng trong lập trình web

Vâng, và công nghệ thay đổi nhanh chóng cả mặt. Số lượng người dùng gia tăng mỗi ngày, trong khi chúng ta phải đối mặt với vấn đề mở rộng ngày càng gia tăng. Hầu hết những dịch vụ web phổ biến ngày nay đều là kiến trúc phân tán. CQRS hay Event Sourcing, SOACAP theorem trở nên không thể thiếu đối với lập trình viên. Yêu cầu tối thiểu nay đã mở rộng và khó khăn hơn xưa nhiều.

Ngày nay, những lập trình viên web nên quan tâm đến tương tác với thiết bị di động nữa. Responsive, SPA, adaptive layout đã được giới thiệu cách đây khá lâu và đã trở thành xu hướng, chuẩn chung cho lập trình web. Lập trình viên được chia làm hai phe: Frontend và Backend. Lập trình web đã trở nên thử thách hơn trước đây rất nhiều rồi. Frontend đã trở thành một phần độc lập trong dự án và tích hợp với Backend sử dụng API.

REST hay RPC?

Để giao tiếp giữa Frontend và Backend, chúng ta có 2 lựa chọn cơ bản nhất đó là tạo một HTTP API: RPC hoặc REST. RPC viết tắt của “Remote Procedure Call”. Nó tương tự như cách gọi hàm của các ngôn ngữ lập trình, với tên phương thức và một tập tham số. Các bạn có quyền thoải mái đặt tên cho phương thức và rất tiện lợi khi tạo APIs.

REST viết tắt cho ” REpresentational State Transfer”. REST là đại diện cho dữ liệu có cấu trúc đơn giản (hầu hết là JSON) và thường dùng cho resources. Chúng ta sử dụng phương thức HTTP để lấy (GET), cập nhật (POST/PUT/PATCH) và xóa resources (DELETE). Không may là sức sáng tạo của con người là vô hạn, dẫn đến việc bất đồng và hoang mang trong team nói riêng và cộng đồng lập trình nói chung về vấn đề REST thật sự là gì. Yup, một số tiên phong lớn cố gắng định nghĩa một hướng dẫn phổ cập nhất cho việc phát triển REST API. Theo ý kiến của mình, Zalando REST API guidelines là hướng dẫn toàn diện, chi tiết và tốt nhất về REST API cho tới ngày nay.

Đồng bộ và bất đồng bộ

Một vài năm trước, Facebook đã cho ra đời GraphQL – ngôn ngữ truy vấn cho API của bạn. Và không kém cạnh, Google cũng phát hành gRPC. Tương tự như RPC hay SOAP với một số điểm khác biệt. Nó sử dụng HTTP/2 cho giao thức truyền tải và protocol buffer cho data serialization. Khá là nhiều sự lựa chọn phải không các bạn?

Okay, vẫn chưa đến lúc cho toàn vẹn dữ liệu, hãy điểm thêm về kiến trúc microservices . Nginx có một hướng dẫn cụ thể về việc giao tiếp giữa các microservices. Nhìn chung lập trình viên có 2 cách để thiết lập giao tiếp giữa các microservices, đó là đồng bộ và bất đồng bộ. Giao tiếp đồng bộ thì thường sử dụng HTTP API và bất đồng bộ sử dụng message queues. Kafka, NATS hay RabbitMQ là những ứng cử viên tuyệt vời. Vâng, gRPC thay thế cho REST/GraphQL có thể là bất đồng bộ. Chúng ta có thể sử dụng để giao tiếp bất đồng bộ giữa các services với nhau.

Giao tiếp bất đồng bộ sử dụng gRPC và không có MQ

Một trong những dự án của mình, team cần phải đảm bảo toàn vẹn dữ liệu truyền tải giữa các services. Tuy nhiên, sử dụng MQ hơi quá mức cần thiết. Yêu cầu dự án chỉ là xử lý các sự kiện và phân phối các sự kiện đó theo thứ tự trước sau.

Mình có 2 microservices:

Phần Core rất đơn giản. Nó mở ra một REST API bên ngoài cho người dùng. Hệ thống lưu tất cả những sự kiện được chấp nhận vào trong cơ sở dữ liệu PostgreSQL và gửi nó qua Processor. Sau đó sẽ lưu kết quả sau khi xử lý xong sự kiện.

Processor còn đơn giản hơn. Nó chỉ cần xử lý dữ liệu sử dụng thuật toán cho mỗi sự kiện nhận được từ Core.

Trong phiên bản đầu tiên, mình đã sử dụng gRPC để giao tiếp giữa các microservices. Mình có phương thức AddEvent ở Core. Nó chấp nhận tất cả sự kiên, ghi nó vào trong Postgres và gửi nó vào một Go channel.

Go channel sẽ truyền sự kiện đó qua Processor sử dụng sự kiện stream. Processor mở một stream đến Core để lấy dữ liệu từ Postgres và sau đó nhận dữ liệu được gửi từ channel.

Đảm bảo toàn vẹn dữ liệu trên gRPC streams

Chúng ta có một hệ thống phân tán khi có nhiều hơn 1 service giao tiếp với service khác. Nó gây ra rất nhiều vấn đề

Chúng ta không thể chắc chắn rằng hệ thống sẽ luôn làm việc bình thường nếu không có đảm bảo toàn vẹn dữ liệu. Hệ thống luôn không đáng tin cây, đôi lúc thứ tự truyền dữ liệu sẽ bị phá vỡ. Tệ hơn nữa, chúng ta có thể đối mặt với việc thất thoát dữ liệu, trùng lặp gói tin…

Chúng ta cần phải giải quyết những vấn đề của hệ thống:

  1. Chúng ta cần truyền lại gói tin một khi chúng ta không nhận được message.
  2. Có thể bị trùng lặp dữ liệu nếu chúng ta gửi lại gói tin.

Mình đi đến một giải pháp dưới đây:

enum InnerOrderResponseCode {
  INIT = 0;         // Khởi tạo Stream 
  ACK = 1;          // Chấp nhận message thành công để xử lý
  DUPLICATE = 2;    // Message trùng lặp
  ERROR = 3;        // Lỗi, ngững xử lý sự kiện
 }

Tuy nhiên chúng ta vẫn có thể có những messages trùng lặp trong hệ thống.

Để phòng tránh việc này, chúng ta cần phải map với mutex trên Processor. Gửi sự kiện với cấp số nhân ở phía Core.

Kết luận

Thông thường, mình không thích phát minh lại cái bánh xe. Tuy nhiên, đôi khi nó lại dễ dàng để thực hiện giải pháp của bạn hơn và chỉ cần mất một khoảng thời gian ngắn. Bạn có thể không cần sử dụng bất kỳ một MQ nào nếu bạn chọn gRPC để liên lạc giữa các microservices.

Nếu thấy hữu ích hãy theo dõi blog của mình nhé!

Write a comment