Khi mở một ứng dụng ngân hàng để kiểm tra số dư, đặt một đơn hàng trên Shopee hay theo dõi giá cổ phiếu thay đổi liên tục, chúng ta đang sử dụng những hệ thống có khả năng trao đổi dữ liệu giữa nhiều thành phần khác nhau. Một thao tác tưởng chừng đơn giản trên giao diện có thể khiến ứng dụng gửi request đến Backend, Backend truy vấn database, gọi một service khác, xử lý kết quả rồi trả dữ liệu về cho người dùng.
Tuy nhiên, không phải hệ thống nào cũng giao tiếp theo cùng một cách. Có những ứng dụng chỉ cần gửi request và chờ response, có những hệ thống phải liên tục cập nhật dữ liệu theo thời gian thực, trong khi một số nghiệp vụ lại cần xử lý bất đồng bộ để tránh phụ thuộc vào tốc độ của các service khác.
Đó là lý do trong quá trình phát triển Backend, chúng ta thường gặp những công nghệ như RESTful API, gRPC, WebSocket, Server-Sent Events (SSE), Kafka, RabbitMQ hay Redis Pub/Sub. Mỗi công nghệ có những đặc điểm riêng, nhưng nếu chỉ học cách sử dụng thư viện hoặc ghi nhớ ưu nhược điểm, chúng ta rất dễ nhầm lẫn giữa những khái niệm vốn thuộc các tầng khác nhau của hệ thống.
Chẳng hạn, tại sao REST API thường sử dụng HTTP nhưng WebSocket cũng có thể bắt đầu bằng một HTTP request? Tại sao gRPC thường được nhắc đến cùng HTTP/2? Nếu HTTP/2 đã hỗ trợ multiplexing thì có cần WebSocket nữa không? Và tại sao một hệ thống đã có gRPC vẫn cần Kafka hoặc một message broker?
Để trả lời những câu hỏi ấy, chúng ta cần hiểu cách dữ liệu thực sự di chuyển giữa Client và Server, từ kết nối mạng ở tầng thấp đến cách ứng dụng tổ chức việc trao đổi thông tin. Trong bài đầu tiên của series Backend Communication này, mình sẽ đi từ TCP, TLS, HTTP/1.1, HTTP/2 và HTTP/3 đến các mô hình request–response, polling, streaming, server push và asynchronous messaging. Sau đó, chúng ta sẽ đặt REST, gRPC, WebSocket, SSE và Messaging vào đúng vị trí của chúng, trước khi tìm hiểu cách lựa chọn công nghệ phù hợp cho từng bài toán thực tế.
1. Điều gì thực sự xảy ra khi Client gọi một API?Link to this heading
Hãy bắt đầu bằng một tình huống quen thuộc. Người dùng mở ứng dụng giao dịch và muốn xem số dư tài khoản. Ứng dụng gửi một HTTP request đến Backend:
GET /api/v1/accounts/balance HTTP/1.1
Host: api.example.com
Authorization: Bearer <access_token>
Accept: application/jsonNếu request hợp lệ và được xử lý thành công, Server có thể trả về:
HTTP/1.1 200 OK
Content-Type: application/json
{
"currency": "VND",
"available_balance": 15000000,
"total_balance": 20000000
}Nhìn ở góc độ lập trình ứng dụng, quá trình này khá đơn giản: Client gửi request, Server xử lý và trả response. Nhưng bên dưới một lời gọi HTTP là nhiều bước mà các thư viện networking thường thực hiện thay chúng ta.
Đầu tiên, Client cần xác định địa chỉ mạng của Server. Nếu ứng dụng sử dụng tên miền api.example.com, hệ thống thường phải phân giải DNS để tìm địa chỉ IP tương ứng. Tiếp theo, Client thiết lập đường giao tiếp với Server. Với HTTPS truyền thống chạy trên TCP, quá trình này bao gồm thiết lập kết nối TCP và thực hiện TLS handshake để tạo kênh truyền được mã hóa.
Khi kết nối đã sẵn sàng, Client gửi HTTP request. Server nhận request, kiểm tra thông tin xác thực, xác định người dùng có quyền truy cập tài khoản hay không, thực hiện logic nghiệp vụ và trả HTTP response. Cuối cùng, Client đọc response, giải mã JSON và cập nhật giao diện.
Có thể hình dung quá trình này như sau:
Client (Web/Mobile)
|
| 1. DNS lookup
v
IP Address
|
| 2. TCP connection
| 3. TLS handshake
v
HTTPS Server
|
| 4. HTTP request
v
Authentication
|
v
Business Logic
|
v
Database
|
| 5. HTTP response
v
Client cập nhật giao diệnSơ đồ này đã được đơn giản hóa để tập trung vào luồng xử lý chính. Trong thực tế, DNS có thể đã được cache, kết nối TCP/TLS có thể được tái sử dụng, request có thể đi qua CDN, load balancer hoặc API Gateway, và Server cũng có thể gọi nhiều service khác trước khi trả kết quả. Đặc biệt, với HTTP/3, kết nối không sử dụng TCP mà dựa trên QUIC chạy trên UDP.
Điểm quan trọng cần hiểu là HTTP không tự giải quyết toàn bộ việc truyền dữ liệu trên mạng. HTTP định nghĩa cách trao đổi request và response, trong khi các tầng bên dưới đảm nhận việc vận chuyển dữ liệu, thiết lập kết nối và bảo vệ thông tin.
Để hiểu vì sao các công nghệ giao tiếp có hành vi khác nhau, chúng ta cần bắt đầu từ những tầng này.
2. TCP, UDP và TLS: Nền tảng bên dưới giao tiếp Client–ServerLink to this heading
2.1. TCP là gì và tại sao nhiều ứng dụng sử dụng TCP?Link to this heading
TCP, viết tắt của Transmission Control Protocol, là giao thức truyền tải hướng kết nối. Nhiệm vụ quan trọng của TCP là cung cấp một luồng byte đáng tin cậy, có thứ tự giữa hai đầu kết nối.
Giả sử Client cần gửi một thông điệp có kích thước lớn. TCP có thể chia dữ liệu thành nhiều segment để truyền qua mạng. Trong quá trình truyền, một số segment có thể đến chậm, bị mất hoặc đến không đúng thứ tự. TCP sử dụng sequence number, acknowledgement, cơ chế retransmission cùng các thuật toán flow control và congestion control để cung cấp dữ liệu cho ứng dụng theo đúng thứ tự.
Điều này rất hữu ích đối với những nghiệp vụ yêu cầu dữ liệu đầy đủ và chính xác, chẳng hạn như gửi thông tin đặt hàng, truy vấn tài khoản hoặc trao đổi dữ liệu giữa các microservice.
Tuy nhiên, có một điểm rất dễ nhầm lẫn: TCP cung cấp một byte stream, không cung cấp ranh giới message ở tầng ứng dụng. Nếu Client thực hiện hai lần ghi dữ liệu, Server không được phép giả định rằng sẽ có đúng hai lần đọc tương ứng. Một lần đọc có thể chỉ nhận được một phần message hoặc nhận nhiều message được ghép lại.
Ví dụ, Client gửi hai message:
Message 1: HELLO
Message 2: WORLDServer có thể nhận dữ liệu qua các lần đọc như sau:
Read 1: HEL
Read 2: LOWOR
Read 3: LDTổng dữ liệu vẫn đúng thứ tự là HELLOWORLD, nhưng TCP không tự cho Server biết đâu là điểm kết thúc của từng message.
Vì vậy, những giao thức ứng dụng như HTTP, gRPC hay WebSocket cần có cơ chế framing để xác định ranh giới thông điệp. Đây là kiến thức rất quan trọng khi tự xây dựng một giao thức trên TCP bằng Go.
Một đặc điểm khác của TCP là kết nối thường được thiết lập bằng cơ chế three-way handshake:
Client Server
| |
| -------- SYN ---------> |
| <----- SYN-ACK -------- |
| -------- ACK ---------> |
| |
| Connection ready |Quá trình thiết lập kết nối tạo ra chi phí ban đầu. Vì thế, các hệ thống hiện đại thường sử dụng connection reuse hoặc connection pooling để tránh phải liên tục thiết lập kết nối mới cho từng request.
2.2. UDP khác TCP như thế nào?Link to this heading
UDP, hay User Datagram Protocol, là giao thức truyền tải theo datagram. Khác với TCP, UDP không tự cung cấp cơ chế bảo đảm dữ liệu đến nơi, đúng thứ tự hoặc được truyền lại khi mất.
Điều này khiến UDP có mô hình đơn giản hơn, nhưng không có nghĩa UDP luôn nhanh hơn TCP trong mọi tình huống. Hiệu năng thực tế còn phụ thuộc vào ứng dụng, chất lượng mạng và các cơ chế được xây dựng bên trên.
UDP phù hợp với những hệ thống có khả năng tự quản lý độ tin cậy hoặc chấp nhận mất một phần dữ liệu. Một số ứng dụng thời gian thực có thể ưu tiên dữ liệu mới hơn dữ liệu cũ, chẳng hạn như truyền âm thanh hoặc video trực tiếp.
Một ví dụ quan trọng là QUIC, giao thức được HTTP/3 sử dụng. QUIC chạy trên UDP nhưng tự triển khai các cơ chế như truyền dữ liệu đáng tin cậy, kiểm soát tắc nghẽn, multiplexing và bảo mật bằng TLS 1.3. Vì vậy, không nên kết luận rằng HTTP/3 thiếu độ tin cậy chỉ vì nó sử dụng UDP.
2.3. TLS bảo vệ dữ liệu như thế nào?Link to this heading
Nếu TCP chủ yếu giải quyết việc vận chuyển dữ liệu đáng tin cậy, TLS, viết tắt của Transport Layer Security, giải quyết bài toán bảo mật kênh truyền.
Khi sử dụng HTTPS, Client và Server thiết lập một phiên TLS để bảo vệ dữ liệu trao đổi. TLS hiện đại cung cấp ba thuộc tính quan trọng: tính bí mật thông qua mã hóa, tính toàn vẹn để phát hiện dữ liệu bị thay đổi trong quá trình truyền và khả năng xác thực Server thông qua chứng chỉ số được kiểm tra hợp lệ.
Ví dụ, khi người dùng đăng nhập vào một ứng dụng, request có thể chứa access token hoặc thông tin nhạy cảm. Nếu truyền qua HTTP không mã hóa trên một mạng không đáng tin cậy, dữ liệu có nguy cơ bị quan sát hoặc can thiệp. Với HTTPS được cấu hình đúng, nội dung HTTP được bảo vệ trên đường truyền giữa hai đầu TLS.
Tuy nhiên, TLS không có nghĩa toàn bộ hệ thống đã an toàn. Nếu Backend trả dữ liệu tài khoản của người dùng A cho người dùng B vì kiểm tra quyền truy cập sai, TLS vẫn có thể mã hóa response ấy một cách hoàn hảo. Vấn đề lúc này không nằm ở kênh truyền mà ở logic authorization.
Tương tự, TLS không ngăn được việc token bị đánh cắp từ một thiết bị đã nhiễm mã độc, dữ liệu nhạy cảm bị ghi vào log hoặc Server bị khai thác lỗ hổng ứng dụng.
Vì vậy, cần phân biệt rõ: TLS bảo vệ dữ liệu trong quá trình truyền, còn authentication, authorization và các biện pháp bảo mật ứng dụng quyết định ai được truy cập dữ liệu nào và được thực hiện hành động gì.
3. HTTP/1.1, HTTP/2 và HTTP/3 khác nhau ở đâu?Link to this heading
Khi làm Backend, chúng ta thường nhìn thấy HTTP như một giao thức duy nhất. Nhưng qua nhiều phiên bản, HTTP đã thay đổi đáng kể cách truyền request và response trên mạng.
Điều thú vị là cả HTTP/1.1, HTTP/2 và HTTP/3 đều giữ lại những khái niệm ứng dụng quen thuộc như method, URL, status code, header và response body. Phần thay đổi lớn nằm ở cách biểu diễn và vận chuyển dữ liệu.
3.1. HTTP/1.1: Đơn giản, phổ biến và dễ sử dụngLink to this heading
HTTP/1.1 hỗ trợ persistent connection, nghĩa là nhiều request có thể tái sử dụng cùng một kết nối TCP thay vì phải tạo kết nối mới cho từng request.
Tuy nhiên, HTTP/1.1 không có cơ chế multiplexing thực sự trên một kết nối như HTTP/2. Trong mô hình thông thường, các request trên cùng kết nối được xử lý theo thứ tự; HTTP/1.1 pipelining từng được định nghĩa nhưng gặp nhiều hạn chế thực tế và không được sử dụng rộng rãi.
Để xử lý nhiều request đồng thời, trình duyệt thường mở nhiều kết nối TCP đến cùng một origin.
Giả sử một trang web cần tải HTML, CSS, JavaScript và nhiều hình ảnh. Nếu tất cả request phải tuần tự chờ nhau trên một kết nối, thời gian tải trang có thể tăng đáng kể. Việc mở nhiều kết nối giúp cải thiện concurrency, nhưng cũng tạo thêm chi phí quản lý kết nối.
Dù vậy, HTTP/1.1 vẫn được sử dụng rộng rãi vì đơn giản, tương thích tốt và được hỗ trợ bởi gần như mọi công cụ HTTP.
3.2. HTTP/2: Multiplexing nhiều request trên một kết nốiLink to this heading
HTTP/2 được thiết kế để cải thiện hiệu quả truyền dữ liệu so với HTTP/1.1. Một thay đổi quan trọng là khả năng multiplexing, cho phép nhiều HTTP stream hoạt động đồng thời trên cùng một kết nối TCP.
Thay vì phải chờ toàn bộ response trước hoàn thành rồi mới nhận response tiếp theo, HTTP/2 có thể xen kẽ các frame thuộc nhiều stream khác nhau.
HTTP/1.1 - Một kết nối, xử lý tuần tự điển hình
Request A ───── Response A
Request B ─── Response B
HTTP/2 - Multiplexing trên một kết nối
Stream A: [A1] [A2] [A3]
Stream B: [B1] [B2]
Stream C: [C1] [C2]HTTP/2 còn sử dụng binary framing và HPACK để nén header, giúp giảm overhead trong nhiều trường hợp.
Tuy nhiên, HTTP/2 vẫn thường chạy trên TCP. Nếu một TCP segment bị mất, TCP phải khôi phục phần dữ liệu bị thiếu trước khi có thể tiếp tục cung cấp luồng byte theo thứ tự cho tầng phía trên. Điều này có thể khiến nhiều HTTP/2 stream cùng bị ảnh hưởng, dù chúng độc lập ở tầng HTTP.
Hiện tượng ấy thường được gọi là TCP-level head-of-line blocking.
Một hiểu lầm phổ biến là HTTP/2 luôn nhanh hơn HTTP/1.1. Thực tế, multiplexing và header compression có thể cải thiện hiệu năng, đặc biệt khi có nhiều request đồng thời, nhưng kết quả còn phụ thuộc vào network latency, packet loss, kích thước payload, cấu hình Server và cách ứng dụng sử dụng kết nối.
3.3. HTTP/3: Sử dụng QUIC thay vì TCPLink to this heading
HTTP/3 tiếp tục duy trì mô hình HTTP quen thuộc nhưng sử dụng QUIC làm giao thức truyền tải. QUIC chạy trên UDP và tích hợp TLS 1.3 để bảo vệ kết nối.
Một lợi ích quan trọng của QUIC là khả năng quản lý nhiều stream độc lập. Khi dữ liệu của một stream bị mất, những stream khác không nhất thiết phải chờ dữ liệu ấy được truyền lại như trong trường hợp nhiều HTTP/2 stream cùng phụ thuộc vào một luồng byte TCP.
QUIC còn hỗ trợ connection migration, cho phép kết nối có khả năng tiếp tục hoạt động khi đường mạng thay đổi, chẳng hạn như thiết bị di động chuyển từ Wi-Fi sang mạng di động, nếu các điều kiện triển khai cho phép.
Điều này đặc biệt hữu ích với những ứng dụng hoạt động trên mạng di động có chất lượng không ổn định. Tuy nhiên, HTTP/3 cũng không phải giải pháp mặc định tốt nhất cho mọi hệ thống. Việc triển khai cần xem xét khả năng hỗ trợ của client, proxy, load balancer, firewall, hạ tầng quan sát và đặc điểm lưu lượng thực tế.
3.4. Bảng so sánh HTTP/1.1, HTTP/2 và HTTP/3Link to this heading
| Tiêu chí | HTTP/1.1 | HTTP/2 | HTTP/3 |
|---|---|---|---|
| Giao thức truyền tải | TCP | TCP | QUIC trên UDP |
| Mã hóa | TLS khi dùng HTTPS | TLS khi dùng HTTPS | TLS 1.3 tích hợp trong QUIC |
| Multiplexing | Không thực sự | Có | Có |
| Header compression | Không có cơ chế chuẩn tương đương | HPACK | QPACK |
| Head-of-line blocking giữa các stream | Có giới hạn ở mô hình tuần tự | Có thể xảy ra ở tầng TCP | Tránh phụ thuộc liên stream do mất gói ở tầng truyền tải |
| Độ phổ biến | Rất rộng | Rất rộng | Ngày càng được hỗ trợ |
| Điểm mạnh | Đơn giản, tương thích | Hiệu quả với nhiều request đồng thời | Phù hợp với nhiều điều kiện mạng hiện đại |
Cần lưu ý rằng HTTP/2 và HTTP/3 không tự biến một API thành hệ thống real-time, cũng không tự giải quyết authentication, authorization hay độ tin cậy nghiệp vụ. Chúng chủ yếu thay đổi cách dữ liệu HTTP được truyền đi.
Đây là điểm dẫn chúng ta đến một câu hỏi quan trọng hơn: ngoài việc lựa chọn phiên bản HTTP, ứng dụng nên tổ chức việc trao đổi dữ liệu theo mô hình nào?
4. Request–Response, Polling, Streaming và Messaging: Các mô hình giao tiếp phổ biếnLink to this heading
Để lựa chọn công nghệ đúng, trước tiên cần hiểu ứng dụng muốn trao đổi dữ liệu theo cách nào. Có những nghiệp vụ cần kết quả ngay lập tức, có những nghiệp vụ cần liên tục nhận dữ liệu mới, và cũng có những công việc không cần hoàn thành trong cùng một request.
4.1. Request–Response: Gửi yêu cầu và chờ kết quảLink to this heading
Request–response là mô hình quen thuộc nhất. Client gửi một yêu cầu đến Server, Server xử lý và trả kết quả tương ứng.
Client Server
| |
| ------ Request --------> |
| |
| Processing |
| |
| <----- Response -------- |Mô hình này phù hợp với những thao tác như đăng nhập, lấy hồ sơ người dùng, truy vấn số dư, tạo đơn hàng hoặc cập nhật thông tin tài khoản.
REST API thường sử dụng mô hình request–response qua HTTP. gRPC unary cũng có cách tương tác tương tự, dù sử dụng định dạng dữ liệu và hợp đồng dịch vụ khác.
Ưu điểm lớn nhất của request–response là dễ hiểu, dễ debug và phù hợp với phần lớn nghiệp vụ CRUD. Client biết mình đã gửi yêu cầu gì và nhận được kết quả tương ứng.
Tuy nhiên, một hạn chế xuất hiện khi công việc xử lý kéo dài. Nếu Server cần nhiều phút để tạo báo cáo, giữ request mở trong toàn bộ thời gian có thể gây timeout hoặc tiêu tốn tài nguyên không cần thiết. Trong trường hợp này, một thiết kế tốt hơn có thể là nhận yêu cầu, tạo job, trả về job ID và xử lý công việc bất đồng bộ.
Một điểm cần đặc biệt chú ý là request timeout không đồng nghĩa với việc Server chưa xử lý thành công. Ví dụ, Client gửi lệnh đặt hàng, Server đã ghi dữ liệu vào database nhưng response bị mất trên đường truyền. Client chỉ thấy timeout và không biết kết quả cuối cùng.
Nếu Client retry mà không có cơ chế idempotency, hệ thống có thể tạo ra hai đơn hàng hoặc hai lệnh giao dịch. Đây là vấn đề thuộc về tính đúng đắn nghiệp vụ, không thể giải quyết chỉ bằng cách đổi từ REST sang gRPC.
4.2. Polling: Chủ động hỏi Server theo chu kỳLink to this heading
Polling là cách Client liên tục gửi request để kiểm tra xem dữ liệu có thay đổi hay không.
Ví dụ, một ứng dụng muốn cập nhật trạng thái đơn hàng mỗi năm giây:
Client Server
| |
| ---- GET /orders/123 -----> |
| <------- PENDING ---------- |
| |
| Chờ 5 giây |
| |
| ---- GET /orders/123 -----> |
| <------- PENDING ---------- |
| |
| Chờ 5 giây |
| |
| ---- GET /orders/123 -----> |
| <------- COMPLETED -------- |Polling rất dễ triển khai vì có thể sử dụng REST API thông thường. Nó cũng phù hợp với những dữ liệu không cần cập nhật ngay lập tức, chẳng hạn như tiến độ xử lý báo cáo hoặc trạng thái của một tác vụ nền.
Nhược điểm là nhiều request có thể không mang lại thông tin mới. Nếu một triệu client cùng polling mỗi năm giây, hệ thống có thể phải tiếp nhận trung bình khoảng 200.000 request mỗi giây, ngay cả khi dữ liệu gần như không thay đổi.
Ngoài ra, polling còn tạo ra độ trễ cập nhật. Nếu trạng thái thay đổi ngay sau lần kiểm tra, Client có thể phải chờ gần hết chu kỳ tiếp theo mới nhận được dữ liệu mới.
Một biến thể là long polling. Thay vì trả response ngay khi chưa có dữ liệu mới, Server giữ request trong một khoảng thời gian cho đến khi có sự kiện hoặc đạt timeout. Sau khi nhận response, Client tiếp tục gửi request mới.
Long polling có thể giảm số lượng request vô ích so với polling thông thường, nhưng vẫn cần quản lý timeout, reconnect và tài nguyên kết nối.
4.3. Streaming: Truyền dữ liệu liên tục trong một phiên giao tiếpLink to this heading
Streaming cho phép dữ liệu được truyền thành nhiều phần theo thời gian, thay vì phải đợi toàn bộ kết quả được tạo xong mới gửi một response hoàn chỉnh.
Ví dụ, một hệ thống phân tích dữ liệu có thể trả kết quả từng phần khi xử lý xong từng nhóm dữ liệu. Một dịch vụ AI có thể gửi nội dung câu trả lời dần dần, giúp người dùng bắt đầu đọc trước khi quá trình sinh nội dung hoàn tất.
Client Server
| |
| ------ Request ------------> |
| |
| <------ Chunk 1 ------------ |
| <------ Chunk 2 ------------ |
| <------ Chunk 3 ------------ |
| <------ End ---------------- |Streaming không đồng nghĩa với WebSocket. HTTP cũng có thể truyền response theo từng phần, SSE sử dụng HTTP response stream, còn gRPC hỗ trợ server streaming, client streaming và bidirectional streaming.
Điểm quan trọng là cần xác định hướng truyền dữ liệu. Có trường hợp Client gửi một request và nhận nhiều message từ Server. Có trường hợp Client gửi nhiều message trong một phiên RPC. Cũng có trường hợp cả hai phía đều có thể gửi dữ liệu độc lập theo thời gian.
Streaming phù hợp khi dữ liệu được tạo ra liên tục hoặc việc chờ toàn bộ kết quả gây ra trải nghiệm không tốt. Tuy nhiên, kết nối kéo dài cũng làm phát sinh những vấn đề mới như quản lý tài nguyên, flow control, backpressure, timeout và xử lý mất kết nối.
4.4. Server Push: Server chủ động gửi dữ liệu khi có thay đổiLink to this heading
Trong nhiều ứng dụng, dữ liệu thay đổi không theo chu kỳ cố định. Ví dụ, giá thị trường có thể thay đổi nhiều lần mỗi giây, một lệnh giao dịch có thể được khớp bất kỳ lúc nào, hoặc một tin nhắn có thể xuất hiện khi người dùng đang mở ứng dụng.
Nếu sử dụng polling, Client phải liên tục hỏi Server xem có thông tin mới hay không. Với mô hình server push ở tầng ứng dụng, sau khi thiết lập cơ chế đăng ký nhận dữ liệu, Server có thể chủ động gửi cập nhật khi sự kiện xảy ra.
WebSocket và SSE là hai lựa chọn phổ biến cho nhu cầu này, nhưng có sự khác biệt quan trọng. SSE chủ yếu phục vụ luồng sự kiện một chiều từ Server đến Client, trong khi WebSocket cung cấp kênh giao tiếp hai chiều để cả Client và Server có thể gửi message.
Cần phân biệt server push theo nghĩa ứng dụng với tính năng HTTP/2 Server Push. HTTP/2 từng định nghĩa một cơ chế cho phép Server chủ động gửi tài nguyên liên quan, nhưng tính năng này không được hỗ trợ rộng rãi trong các trình duyệt hiện đại. Khi nói đến cập nhật real-time trong ứng dụng, chúng ta thường đang đề cập đến SSE, WebSocket hoặc một cơ chế streaming khác.
4.5. Asynchronous Messaging: Không cần chờ xử lý hoàn tấtLink to this heading
Không phải mọi hoạt động trong hệ thống đều cần trả kết quả ngay lập tức. Giả sử sau khi một lệnh giao dịch được khớp, hệ thống cần cập nhật lịch sử, gửi thông báo, tính toán báo cáo và chuyển sự kiện sang hệ thống phân tích dữ liệu.
Nếu Order Service gọi đồng bộ từng service và chờ tất cả hoàn thành, thời gian xử lý sẽ tăng lên. Chỉ cần Notification Service gặp sự cố, luồng xử lý chính cũng có nguy cơ bị ảnh hưởng.
Một giải pháp là sử dụng asynchronous messaging. Order Service phát một sự kiện vào message broker, sau đó những consumer phù hợp xử lý sự kiện theo trách nhiệm của mình.
Order Service
|
| OrderMatched Event
v
Message Broker
|
+------------+------------+
| | |
v v v
Position Notification Analytics
Service Service ServiceMô hình này giúp giảm sự phụ thuộc trực tiếp giữa các service và cho phép xử lý công việc ở những thời điểm khác nhau. Tuy nhiên, nó cũng khiến hệ thống phức tạp hơn vì phải quản lý việc gửi và nhận message, retry, duplicate delivery, ordering, dead-letter queue và eventual consistency.
Một điều rất quan trọng là không phải message broker nào cũng cung cấp cùng một mức độ bền vững dữ liệu. Redis Pub/Sub thông thường không lưu lại message cho subscriber đã mất kết nối, trong khi Kafka, RabbitMQ hoặc các dịch vụ messaging có khả năng lưu trữ và xác nhận message theo những cơ chế khác nhau.
Vì vậy, asynchronous messaging không đơn giản là gửi dữ liệu qua một hàng đợi. Nó là một lựa chọn kiến trúc, đòi hỏi thiết kế rõ ràng về độ tin cậy và tính nhất quán của nghiệp vụ.
5. REST, gRPC, WebSocket, SSE và Messaging có thực sự cùng một loại công nghệ?Link to this heading
Đây có lẽ là phần quan trọng nhất của bài viết. Chúng ta thường nhìn thấy những câu hỏi như “Nên chọn REST hay gRPC?”, “WebSocket có nhanh hơn REST không?” hoặc “Kafka có thể thay thế gRPC không?”. Những câu hỏi ấy có thể hữu ích trong một bối cảnh cụ thể, nhưng dễ gây hiểu lầm nếu xem tất cả các công nghệ như những lựa chọn ngang hàng.
Để hiểu rõ hơn, cần phân biệt ít nhất bốn khía cạnh: giao thức truyền tải, giao thức ứng dụng, mô hình giao tiếp và định dạng dữ liệu.
Giao thức truyền tải quyết định cách dữ liệu được vận chuyển giữa hai đầu kết nối. TCP, UDP và QUIC thuộc nhóm này, dù QUIC cung cấp nhiều chức năng vượt xa một giao thức datagram đơn giản.
Giao thức ứng dụng xác định cách các bên biểu diễn và trao đổi thông điệp ở mức ứng dụng. HTTP và WebSocket là những ví dụ. gRPC là một framework và giao thức RPC với các quy tắc về message framing, service contracts và cách ánh xạ RPC lên transport, thường là HTTP/2.
Mô hình giao tiếp mô tả cách các thành phần tương tác, chẳng hạn như request–response, polling, streaming, publish–subscribe hoặc asynchronous messaging. Một mô hình có thể được triển khai bằng nhiều công nghệ khác nhau.
Định dạng dữ liệu quyết định nội dung message được biểu diễn như thế nào. JSON, XML, Protocol Buffers, Avro và MessagePack là những ví dụ. JSON thường được sử dụng với REST API, nhưng REST không bắt buộc phải sử dụng JSON. Tương tự, WebSocket có thể truyền text hoặc binary message, còn gRPC mặc định thường sử dụng Protocol Buffers nhưng có thể hỗ trợ cơ chế serialization khác.
Có thể tóm tắt như sau:
| Khái niệm | Vai trò | Ví dụ |
|---|---|---|
| Transport | Vận chuyển dữ liệu | TCP, UDP, QUIC |
| Security | Bảo vệ kênh truyền | TLS, mTLS |
| Application protocol | Quy tắc trao đổi thông điệp | HTTP, WebSocket |
| Communication model | Cách các bên tương tác | Request–response, streaming, pub/sub |
| API style/framework | Cách thiết kế và triển khai API | REST, gRPC |
| Serialization | Biểu diễn dữ liệu | JSON, Protobuf, Avro |
| Messaging infrastructure | Trung gian phân phối message | Kafka, RabbitMQ, Redis Pub/Sub |
Các nhóm này không phải những tầng hoàn toàn tách biệt trong mọi triển khai. Một công nghệ có thể kết hợp nhiều chức năng, nhưng cách phân loại trên giúp chúng ta tránh nhầm lẫn khi thiết kế hệ thống.
Chẳng hạn, một REST API có thể chạy trên HTTP/1.1, HTTP/2 hoặc HTTP/3. Một gRPC service thường sử dụng HTTP/2 và Protocol Buffers. WebSocket có thể được thiết lập bằng HTTP/1.1 Upgrade hoặc cơ chế phù hợp trên những phiên bản HTTP mới hơn. SSE sử dụng một HTTP response với kiểu nội dung text/event-stream, trong khi Kafka là nền tảng event streaming với giao thức và cơ chế lưu trữ riêng.
Điều quan trọng là không nên mặc định công nghệ này thay thế hoàn toàn công nghệ kia. Một hệ thống thực tế có thể sử dụng nhiều công nghệ cùng lúc, miễn là mỗi công nghệ giải quyết đúng vấn đề.
6. RESTful API: Khi sự đơn giản và tính tương thích là lợi thếLink to this heading
REST, viết tắt của Representational State Transfer, là một phong cách kiến trúc cho các hệ thống phân tán. Trong thực tế, khi nói đến RESTful API, chúng ta thường đề cập đến những API sử dụng HTTP methods để thao tác trên các resource được xác định bằng URI.
Ví dụ, một hệ thống quản lý đơn hàng có thể cung cấp các endpoint:
GET /api/v1/orders
GET /api/v1/orders/123
POST /api/v1/orders
PATCH /api/v1/orders/123
DELETE /api/v1/orders/123Mỗi HTTP method mang một ý nghĩa nhất định. GET thường dùng để đọc dữ liệu, POST để tạo hoặc thực hiện một thao tác, PATCH để cập nhật một phần resource, còn DELETE để yêu cầu xóa resource.
Một đặc điểm quan trọng của REST là stateless interaction: mỗi request cần mang đủ ngữ cảnh để Server hiểu và xử lý, thay vì phụ thuộc vào trạng thái hội thoại chỉ được lưu ngầm giữa các request. Điều này không có nghĩa Server không được lưu dữ liệu hay không được sử dụng session, mà là cách xử lý request không nên phụ thuộc vào một trạng thái giao tiếp ẩn không được thể hiện trong request.
Ví dụ REST API bằng GoLink to this heading
Để minh họa, chúng ta có thể xây dựng một endpoint đơn giản trả về số dư tài khoản bằng thư viện net/http của Go.
package main
import (
"encoding/json"
"log"
"net/http"
)
type BalanceResponse struct {
Currency string `json:"currency"`
AvailableBalance int64 `json:"available_balance"`
TotalBalance int64 `json:"total_balance"`
}
func getBalance(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
w.Header().Set("Allow", http.MethodGet)
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
response := BalanceResponse{
Currency: "VND",
AvailableBalance: 15000000,
TotalBalance: 20000000,
}
w.Header().Set("Content-Type", "application/json")
if err := json.NewEncoder(w).Encode(response); err != nil {
log.Printf("encode response: %v", err)
}
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/api/v1/accounts/balance", getBalance)
log.Fatal(http.ListenAndServe(":8080", mux))
}Đây là ví dụ tối giản để minh họa request–response. Dữ liệu đang được hardcode và endpoint chưa có authentication hoặc authorization, vì vậy không nên triển khai nguyên trạng cho dữ liệu tài khoản thật. Trong production, Server cần xác thực người dùng, xác định tài khoản mà người dùng có quyền truy cập, lấy số dư từ nguồn dữ liệu đáng tin cậy và chạy sau một kênh HTTPS được cấu hình đúng.
Sau khi chạy chương trình, chúng ta có thể gọi API bằng curl:
curl http://localhost:8080/api/v1/accounts/balanceKết quả:
{
"currency": "VND",
"available_balance": 15000000,
"total_balance": 20000000
}Điểm mạnh của RESTful API là dễ phát triển, dễ kiểm thử và có khả năng tương thích rất rộng. Trình duyệt, ứng dụng di động, hệ thống Backend và các công cụ như curl hoặc Postman đều có thể làm việc với HTTP.
REST cũng phù hợp với các API công khai vì dễ tài liệu hóa bằng OpenAPI, dễ tích hợp với API Gateway và có thể tận dụng nhiều cơ chế sẵn có của HTTP như caching, conditional requests và status codes.
Tuy nhiên, REST không phải lựa chọn tối ưu cho mọi trường hợp. Khi một hệ thống có hàng trăm lời gọi giữa các microservice, việc quản lý hợp đồng dữ liệu, serialization và các API thay đổi thường xuyên có thể trở nên phức tạp. Với những nghiệp vụ cần truyền dữ liệu liên tục, request–response truyền thống cũng không phải lúc nào là cách tổ chức hiệu quả nhất.
Đó là một trong những lý do gRPC được sử dụng phổ biến trong các hệ thống microservices.
7. gRPC: Giao tiếp giữa các service bằng hợp đồng rõ ràngLink to this heading
gRPC là một framework Remote Procedure Call cho phép một ứng dụng gọi phương thức được cung cấp bởi một ứng dụng khác, dù chúng chạy trên những máy chủ hoặc tiến trình khác nhau.
Thay vì thiết kế API chủ yếu xoay quanh resource và HTTP methods như REST, gRPC thường định nghĩa các service và phương thức thông qua một hợp đồng .proto.
Ví dụ, chúng ta có một Account Service cung cấp phương thức lấy số dư:
syntax = "proto3";
package account.v1;
option go_package = "example.com/project/gen/account/v1;accountv1";
service AccountService {
rpc GetBalance(GetBalanceRequest) returns (GetBalanceResponse);
}
message GetBalanceRequest {
string account_id = 1;
}
message GetBalanceResponse {
string currency = 1;
int64 available_balance = 2;
int64 total_balance = 3;
}Từ hợp đồng này, các công cụ Protobuf và gRPC có thể sinh code client và server cho những ngôn ngữ được hỗ trợ. Nhờ đó, các service có thể sử dụng những kiểu dữ liệu và phương thức được định nghĩa thống nhất.
Ở phía Go client, lời gọi có thể có dạng:
ctx, cancel := context.WithTimeout(
context.Background(),
2*time.Second,
)
defer cancel()
response, err := client.GetBalance(
ctx,
&accountv1.GetBalanceRequest{
AccountId: "account-123",
},
)
if err != nil {
return err
}
fmt.Println(response.AvailableBalance)Đoạn code trên minh họa lời gọi RPC sau khi client đã được khởi tạo và cấu hình kết nối. Trong production, kết nối cần được bảo vệ bằng TLS phù hợp, request phải có thông tin xác thực và Server phải kiểm tra quyền truy cập đối với account_id; không được tin rằng Client có quyền xem mọi tài khoản chỉ vì đã truyền một ID hợp lệ.
Điểm đáng chú ý của gRPC là khả năng hỗ trợ bốn kiểu giao tiếp: unary RPC, server streaming, client streaming và bidirectional streaming.
Unary RPC tương tự request–response: Client gửi một request và nhận một response. Server streaming cho phép Client gửi một request rồi nhận nhiều message từ Server. Client streaming cho phép Client gửi nhiều message trước khi nhận response cuối cùng. Bidirectional streaming cho phép cả hai phía gửi nhiều message độc lập trong cùng một RPC.
Vì sử dụng Protocol Buffers theo mặc định và thường chạy trên HTTP/2, gRPC có thể mang lại lợi thế về kích thước payload, hợp đồng dữ liệu và hiệu quả giao tiếp trong nhiều tình huống. Tuy nhiên, không nên kết luận rằng gRPC luôn nhanh hơn REST. Với payload nhỏ, số lượng request thấp hoặc những hệ thống có bottleneck nằm ở database, sự khác biệt về serialization có thể không phải yếu tố quyết định.
Ngoài ra, gRPC cũng có những đánh đổi. Việc debug trực tiếp bằng trình duyệt không thuận tiện như JSON API, hệ thống cần quản lý code generation và schema evolution, còn việc sử dụng gRPC từ trình duyệt thường cần một lớp hỗ trợ như gRPC-Web hoặc một gateway tương thích. Native gRPC trên HTTP/2 không thể mặc nhiên sử dụng trực tiếp bằng mọi API networking của trình duyệt.
Vì vậy, gRPC thường phù hợp với giao tiếp service-to-service trong môi trường có khả năng kiểm soát cả client và server, đặc biệt khi cần hợp đồng dữ liệu rõ ràng hoặc streaming. REST vẫn là lựa chọn rất tốt cho nhiều public API và các ứng dụng cần khả năng tích hợp rộng.
8. WebSocket: Khi Client và Server cần giao tiếp hai chiều liên tụcLink to this heading
Giả sử chúng ta đang xây dựng một ứng dụng giao dịch. Người dùng mở màn hình theo dõi giá bạc và muốn nhìn thấy giá mới ngay khi thị trường thay đổi.
Nếu Client gọi REST API mỗi giây để lấy giá, hệ thống sẽ tạo ra rất nhiều request, kể cả khi giá không thay đổi. Nếu tăng chu kỳ polling lên năm hoặc mười giây, dữ liệu trên giao diện có thể không còn đủ mới đối với một số nhu cầu giao dịch.
WebSocket giải quyết bài toán này bằng cách thiết lập một kết nối giao tiếp hai chiều, cho phép Client và Server trao đổi message trong suốt vòng đời kết nối.
Với WebSocket truyền thống trên HTTP/1.1, Client bắt đầu bằng một HTTP Upgrade request. Khi Server chấp nhận, kết nối chuyển sang giao thức WebSocket. Với WebSocket chạy trên các phiên bản HTTP mới hơn, cơ chế thiết lập có thể khác.
Sau khi kết nối được thiết lập, Server có thể gửi cập nhật giá mà không cần chờ Client gửi một HTTP request mới cho từng lần cập nhật. Client cũng có thể gửi message để đăng ký hoặc hủy đăng ký các mã hàng hóa.
Client Server
| |
| ----- WebSocket Connect ---> |
| <---- Connection Ready ----- |
| |
| ---- Subscribe SILVER -----> |
| |
| <---- Price Update --------- |
| <---- Price Update --------- |
| <---- Price Update --------- |
| |
| ---- Unsubscribe ----------> |WebSocket phù hợp với những hệ thống như ứng dụng chat, dashboard real-time, cập nhật giá thị trường, trạng thái đơn hàng hoặc các ứng dụng cần tương tác hai chiều thường xuyên.
Tuy nhiên, việc duy trì kết nối lâu dài làm phát sinh nhiều vấn đề mà REST API thông thường ít gặp hơn. Server phải quản lý số lượng kết nối đồng thời, phát hiện kết nối đã chết, xử lý client chậm, giới hạn kích thước message và giải quyết việc kết nối bị ngắt khi mạng thay đổi.
Một vấn đề quan trọng khác là WebSocket không tự bảo đảm rằng Client sẽ nhận được mọi sự kiện nghiệp vụ trong thời gian mất kết nối.
Ví dụ, người dùng đang theo dõi một lệnh giao dịch và WebSocket bị ngắt đúng lúc lệnh được khớp. Nếu Server chỉ gửi sự kiện qua kết nối hiện tại mà không có cơ chế lưu trữ hoặc đồng bộ lại trạng thái, Client có thể bỏ lỡ cập nhật quan trọng.
Với những hệ thống yêu cầu tính đúng đắn cao, WebSocket nên được xem là kênh phân phối cập nhật, không phải nguồn dữ liệu có thẩm quyền duy nhất. Khi reconnect, Client có thể cần gọi REST API để lấy trạng thái mới nhất hoặc sử dụng sequence number, cursor và cơ chế replay nếu Backend hỗ trợ.
Đây là lý do trong một ứng dụng giao dịch, chúng ta có thể sử dụng REST để truy vấn trạng thái lệnh và WebSocket để nhận cập nhật nhanh. Hai công nghệ bổ sung cho nhau thay vì loại trừ nhau.
Về bảo mật, WebSocket nên sử dụng wss://, xác thực kết nối, kiểm tra quyền đăng ký từng loại dữ liệu và kiểm soát Origin đối với những client chạy trong trình duyệt. Một kết nối đã được xác thực không có nghĩa Client được phép subscribe mọi tài khoản hoặc mọi luồng dữ liệu.
9. Server-Sent Events: Khi Server chỉ cần gửi dữ liệu một chiềuLink to this heading
Không phải ứng dụng real-time nào cũng cần giao tiếp hai chiều. Trong nhiều trường hợp, Client chỉ cần nhận dữ liệu liên tục từ Server, còn các thao tác gửi yêu cầu vẫn có thể thực hiện bằng REST API thông thường.
Đây là trường hợp SSE, viết tắt của Server-Sent Events, trở thành một lựa chọn đáng cân nhắc.
SSE sử dụng HTTP để truyền một luồng sự kiện từ Server đến Client. Response thường có Content-Type: text/event-stream, và mỗi sự kiện được biểu diễn bằng những trường văn bản theo định dạng SSE.
Ví dụ:
event: price
id: 101
data: {"symbol":"SILVER","price":52.35}
event: price
id: 102
data: {"symbol":"SILVER","price":52.40}Trong trình duyệt, chúng ta có thể sử dụng EventSource để nhận sự kiện:
const events = new EventSource("/api/v1/prices/stream");
events.addEventListener("price", (event) => {
const price = JSON.parse(event.data);
console.log(price.symbol, price.price);
});
events.onerror = () => {
console.log("SSE connection interrupted");
};Đoạn code trên phù hợp với SSE sử dụng cơ chế xác thực tương thích với EventSource, chẳng hạn như cookie được cấu hình an toàn. Native EventSource không cho phép tùy ý thiết lập header Authorization như một lời gọi fetch thông thường, vì vậy cách xác thực cần được thiết kế ngay từ đầu.
Ở phía Backend, một SSE handler tối giản bằng Go có thể được triển khai như sau:
func streamEvents(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("X-Content-Type-Options", "nosniff")
flusher, ok := w.(http.Flusher)
if !ok {
http.Error(
w,
"streaming not supported",
http.StatusInternalServerError,
)
return
}
ticker := time.NewTicker(2 * time.Second)
defer ticker.Stop()
eventID := 0
for {
select {
case <-r.Context().Done():
return
case <-ticker.C:
eventID++
_, err := fmt.Fprintf(
w,
"event: heartbeat\nid: %d\ndata: {\"status\":\"ok\"}\n\n",
eventID,
)
if err != nil {
return
}
flusher.Flush()
}
}
}Ví dụ này chỉ minh họa cách Server giữ HTTP response mở và gửi sự kiện định kỳ. Trong production, endpoint cần authentication, authorization, giới hạn kết nối và xử lý buffering ở reverse proxy. Nếu sử dụng event ID để hỗ trợ resume, Server phải có cơ chế lưu trữ và replay sự kiện thực sự; chỉ tăng biến đếm trong bộ nhớ như ví dụ không bảo đảm khôi phục sự kiện sau khi restart.
Ưu điểm của SSE là đơn giản, tận dụng HTTP và được trình duyệt hỗ trợ thông qua EventSource. Nó phù hợp với notification feed, tiến độ xử lý công việc, AI response streaming hoặc những dashboard chỉ cần nhận cập nhật từ Server.
Nhược điểm là SSE chủ yếu truyền dữ liệu một chiều, sử dụng định dạng text và chịu ảnh hưởng của các giới hạn kết nối, timeout hoặc buffering trong hạ tầng HTTP. Khi cần Client và Server gửi message hai chiều thường xuyên, WebSocket có thể phù hợp hơn.
Vì vậy, câu hỏi không nên là SSE có tốt hơn WebSocket hay không, mà là ứng dụng có thực sự cần một kênh giao tiếp hai chiều hay chỉ cần Server liên tục gửi dữ liệu mới.
10. Messaging: Khi các service không cần giao tiếp đồng bộLink to this heading
Hãy quay lại hệ thống giao dịch. Khi một lệnh được khớp, Order Service cần ghi nhận kết quả và thông báo cho những thành phần khác. Position Service cần cập nhật vị thế, Notification Service cần gửi thông báo, còn Analytics Service có thể sử dụng dữ liệu để xây dựng báo cáo.
Nếu Order Service gọi trực tiếp từng service bằng REST hoặc gRPC và chờ tất cả hoàn thành, hệ thống sẽ hình thành sự phụ thuộc đồng bộ. Khi một service phản hồi chậm hoặc gặp lỗi, toàn bộ luồng xử lý có thể bị ảnh hưởng.
Messaging giúp giảm sự phụ thuộc ấy bằng cách đưa message vào một hệ thống trung gian. Producer phát message, còn consumer nhận và xử lý theo cơ chế của message broker.
Ví dụ, Order Service có thể phát một sự kiện:
{
"event_id": "evt-1001",
"event_type": "OrderMatched",
"order_id": "ORD-123",
"account_id": "ACC-456",
"matched_quantity": 2,
"matched_price": "52.35"
}Position Service nhận sự kiện và cập nhật vị thế. Notification Service có thể sử dụng cùng thông tin để gửi thông báo cho người dùng.
Tuy nhiên, có một vấn đề rất quan trọng: nếu producer ghi trạng thái lệnh vào database thành công nhưng thất bại khi publish event, các service khác có thể không biết rằng lệnh đã được khớp. Ngược lại, nếu message được gửi thành công nhưng producer không nhận được acknowledgement, việc retry có thể khiến cùng một sự kiện được phát nhiều lần.
Đây là lý do những hệ thống yêu cầu độ tin cậy cao thường cần các cơ chế như transactional outbox, idempotent consumer, retry policy, dead-letter queue và reconciliation.
Một consumer xử lý sự kiện OrderMatched phải tính đến khả năng nhận cùng một event nhiều lần. Nếu mỗi lần nhận đều cộng thêm vị thế mà không kiểm tra event_id hoặc một khóa idempotency phù hợp, số lượng vị thế có thể bị cập nhật sai.
Messaging cũng không tự bảo đảm thứ tự xử lý trên toàn hệ thống. Kafka có thể bảo đảm thứ tự trong phạm vi partition, nhưng không có nghĩa tất cả sự kiện ở nhiều partition đều có một thứ tự toàn cục. Tương tự, việc nhiều consumer xử lý song song có thể làm thay đổi thứ tự hoàn thành nghiệp vụ nếu ứng dụng không thiết kế cơ chế kiểm soát.
Điểm mạnh của messaging là khả năng tách rời các service, hấp thụ lưu lượng đột biến và xử lý bất đồng bộ. Nhưng đổi lại, hệ thống phải chấp nhận độ phức tạp lớn hơn trong việc theo dõi trạng thái, xử lý lỗi và bảo đảm tính nhất quán dữ liệu.
Vì vậy, messaging phù hợp với event-driven architecture, background jobs, notification processing và những luồng nghiệp vụ có thể xử lý bất đồng bộ. Nó không nhất thiết thay thế REST hoặc gRPC trong những trường hợp Client cần kết quả ngay lập tức.
11. Bảo mật giao tiếp Client–Server: Không chỉ là HTTPS và JWTLink to this heading
Khi lựa chọn công nghệ giao tiếp, hiệu năng và trải nghiệm người dùng thường được quan tâm đầu tiên. Tuy nhiên, với những hệ thống xử lý dữ liệu cá nhân, tài chính hoặc thông tin nhạy cảm, bảo mật phải được xem là một phần của thiết kế ngay từ đầu.
Một hệ thống có thể sử dụng HTTPS, gRPC với mTLS hoặc WebSocket qua wss:// nhưng vẫn tồn tại lỗ hổng nghiêm trọng nếu không kiểm soát đúng quyền truy cập.
Để hiểu rõ hơn, chúng ta cần phân biệt ba câu hỏi.
Thứ nhất, dữ liệu có được bảo vệ trên đường truyền hay không? Đây là trách nhiệm của TLS và các cấu hình liên quan. Với hệ thống production, cần sử dụng TLS hiện đại, xác minh chứng chỉ hợp lệ, bảo vệ private key và tránh vô hiệu hóa certificate verification chỉ để giải quyết lỗi kết nối.
Thứ hai, ai đang gửi request? Đây là bài toán authentication. Hệ thống có thể sử dụng session cookie, access token, OAuth 2.0 hoặc các cơ chế phù hợp với từng loại client. JWT là một định dạng token phổ biến, nhưng bản thân JWT không phải một giao thức xác thực hoàn chỉnh và không tự giải quyết việc cấp quyền.
Thứ ba, người gửi request được phép làm gì? Đây là bài toán authorization. Ví dụ, một người dùng đã đăng nhập hợp lệ không có nghĩa họ được xem số dư của tài khoản khác. Backend phải kiểm tra quyền truy cập đối với từng tài nguyên hoặc hành động quan trọng, thay vì chỉ kiểm tra token có hợp lệ hay không.
Trong hệ thống giao dịch, điều này càng quan trọng vì một số hành động có thể ảnh hưởng trực tiếp đến tài sản của người dùng. Một API đặt lệnh không chỉ cần xác thực người gửi mà còn phải kiểm tra quyền giao dịch, trạng thái tài khoản, giới hạn nghiệp vụ và tính hợp lệ của yêu cầu.
Bên cạnh đó, từng công nghệ còn có những điểm cần lưu ý riêng. REST API cần quan tâm đến CORS, CSRF khi sử dụng cookie authentication, rate limiting, input validation và object-level authorization. gRPC cần bảo vệ metadata, xác thực service identity, áp dụng deadline và kiểm soát quyền gọi từng RPC. WebSocket cần xác thực kết nối, kiểm tra Origin khi phù hợp, phân quyền từng subscription, giới hạn message và xử lý token hết hạn. SSE cần bảo vệ endpoint, kiểm soát quyền nhận sự kiện và tránh để proxy cache hoặc chia sẻ nhầm dữ liệu nhạy cảm. Messaging cần authentication giữa producer, consumer và broker, phân quyền topic hoặc queue, bảo vệ dữ liệu trong message và kiểm soát những consumer được phép đọc dữ liệu.
Một nguyên tắc nên ghi nhớ là mã hóa bảo vệ dữ liệu trên đường truyền, nhưng không thể thay thế kiểm tra quyền truy cập, tính đúng đắn nghiệp vụ và khả năng kiểm toán hệ thống.
Trong các bài chuyên sâu tiếp theo, chúng ta sẽ phân tích kỹ hơn từng nhóm rủi ro và cách triển khai biện pháp bảo vệ bằng Go.
12. Những vấn đề chỉ thực sự xuất hiện khi hệ thống chạy ProductionLink to this heading
Khi học REST, gRPC hay WebSocket, chúng ta thường bắt đầu bằng một ví dụ đơn giản: Client gửi request và Server trả response. Nhưng trong production, phần khó nhất nhiều khi không nằm ở việc gửi được dữ liệu, mà là xử lý đúng khi mạng hoặc một thành phần trong hệ thống gặp sự cố.
Hãy xem một tình huống điển hình. Client gửi yêu cầu đặt lệnh, Server xử lý thành công và ghi nhận lệnh vào database. Tuy nhiên, ngay trước khi response được gửi về, kết nối bị ngắt. Client chỉ nhận được timeout nên không biết lệnh đã được tạo hay chưa.
Nếu Client tự động retry, hệ thống có thể nhận cùng một yêu cầu hai lần. Nếu Backend không có cơ chế idempotency hoặc kiểm tra trùng lặp phù hợp, một thao tác của người dùng có thể dẫn đến hai lệnh giao dịch.
Một tình huống khác xảy ra với WebSocket. Người dùng đang theo dõi trạng thái lệnh thì Server restart. Kết nối bị ngắt và Client tự động reconnect sau vài giây. Trong khoảng thời gian ấy, lệnh đã được khớp. Nếu Client chỉ tiếp tục chờ các message mới, giao diện có thể vẫn hiển thị trạng thái cũ.
Cách xử lý an toàn hơn là kết hợp reconnect với đồng bộ trạng thái từ nguồn dữ liệu có thẩm quyền. Nếu hệ thống hỗ trợ sequence number và replay, Client có thể yêu cầu những sự kiện bị bỏ lỡ. Nếu không, Client nên gọi API lấy trạng thái hiện tại trước khi tiếp tục nhận cập nhật real-time.
Với messaging, tình huống tương tự có thể xảy ra khi consumer restart sau khi xử lý thành công một sự kiện nhưng trước khi xác nhận message với broker. Broker có thể phân phối lại message, khiến consumer nhận cùng một sự kiện lần nữa. Đây không nhất thiết là lỗi của broker, mà có thể là hành vi đúng theo delivery semantics được cấu hình.
Những ví dụ này cho thấy một nguyên tắc quan trọng trong hệ thống phân tán: việc truyền dữ liệu thành công, xử lý nghiệp vụ thành công và Client nhận được xác nhận thành công là ba sự kiện khác nhau.
Khi thiết kế giao tiếp giữa các service, chúng ta cần xác định rõ hệ thống phải làm gì nếu một trong ba sự kiện ấy không xảy ra như mong đợi.
Ngoài ra, những vấn đề như timeout, retry, circuit breaker, connection pooling, backpressure và observability cũng ảnh hưởng trực tiếp đến độ ổn định của hệ thống. Một API có latency trung bình thấp nhưng p99 rất cao vẫn có thể gây trải nghiệm không tốt. Một cơ chế retry thiếu kiểm soát có thể biến sự cố nhỏ thành retry storm, khiến service vốn đã quá tải càng khó phục hồi.
Đây là lý do việc lựa chọn công nghệ giao tiếp không thể chỉ dựa trên benchmark hoặc số lượng request mỗi giây. Chúng ta còn phải hiểu đặc điểm nghiệp vụ, mức độ quan trọng của dữ liệu và cách hệ thống phục hồi khi có lỗi.
13. Bảng lựa chọn REST, gRPC, WebSocket, SSE và Messaging theo use caseLink to this heading
Sau khi hiểu các tầng giao tiếp và đặc điểm của từng công nghệ, chúng ta có thể tổng hợp thành một bảng lựa chọn thực tế.
| Use case | Công nghệ nên cân nhắc | Lý do |
|---|---|---|
| Đăng nhập, lấy hồ sơ người dùng | REST/HTTPS | Đơn giản, tương thích rộng, dễ tích hợp |
| CRUD dữ liệu ứng dụng | REST | Phù hợp với resource-based API |
| Public API cho đối tác | REST, đôi khi gRPC | Phụ thuộc khả năng hỗ trợ và yêu cầu tích hợp |
| Giao tiếp nội bộ giữa microservices | gRPC hoặc REST | gRPC phù hợp với typed contracts; REST đơn giản và linh hoạt |
| Truy vấn số dư tài khoản | REST hoặc gRPC unary | Cần request–response rõ ràng và kiểm soát quyền truy cập |
| Đặt lệnh giao dịch | REST hoặc gRPC unary | Cần xác nhận, idempotency và xử lý lỗi nghiệp vụ |
| Cập nhật giá thị trường liên tục | WebSocket, gRPC streaming hoặc SSE | Tùy client và nhu cầu truyền một chiều hay hai chiều |
| Chat hai chiều | WebSocket | Hai phía cần gửi message liên tục |
| Notification feed từ Server | SSE hoặc WebSocket | SSE đơn giản nếu chỉ cần nhận dữ liệu một chiều |
| Streaming nội dung AI | SSE hoặc HTTP streaming | Có thể gửi kết quả từng phần |
| Upload dữ liệu dạng stream giữa service | gRPC client streaming | Hỗ trợ nhiều message trong một RPC |
| Xử lý background job | Queue hoặc message broker | Không cần giữ request chờ toàn bộ công việc |
| Phát sự kiện nghiệp vụ | Kafka, RabbitMQ, Pub/Sub | Tách producer và consumer |
| Đồng bộ vị thế sau khớp lệnh | Messaging kết hợp API đối soát | Cần xử lý sự kiện và kiểm tra tính nhất quán |
| Dashboard cập nhật vài phút một lần | REST polling | Đơn giản, không cần kết nối lâu dài |
| Dashboard cập nhật mỗi giây | SSE hoặc WebSocket | Giảm nhu cầu polling liên tục |
| Tích hợp hệ thống cần replay sự kiện | Durable messaging/event streaming | Có khả năng lưu và đọc lại sự kiện theo thiết kế |
Bảng trên là gợi ý lựa chọn ban đầu, không phải quy tắc tuyệt đối. Một hệ thống có thể sử dụng REST để nhận lệnh, gRPC để kiểm tra thông tin giữa các service, Kafka để phát sự kiện nghiệp vụ và WebSocket để thông báo kết quả cho người dùng.
Để lựa chọn chính xác hơn, chúng ta nên đặt ra một số câu hỏi trước khi quyết định: Client có cần kết quả ngay lập tức không? Dữ liệu có thay đổi liên tục không? Giao tiếp một chiều hay hai chiều? Nếu kết nối bị ngắt, dữ liệu có cần được phát lại không? Có yêu cầu về thứ tự xử lý không? Hệ thống có cần lưu trữ sự kiện không? Ai là người kiểm soát client và server? Cuối cùng, nếu một service gặp lỗi, những thành phần khác có được phép tiếp tục hoạt động hay không?
Câu trả lời cho những câu hỏi ấy thường quan trọng hơn việc công nghệ nào đang phổ biến hoặc được cho là có hiệu năng cao nhất.
14. Một kiến trúc thực tế: Kết hợp nhiều công nghệ trong ứng dụng giao dịchLink to this heading
Để kết nối toàn bộ kiến thức trong bài, hãy hình dung chúng ta đang thiết kế một ứng dụng giao dịch hàng hóa với các chức năng đăng nhập, xem số dư, theo dõi giá thị trường, đặt lệnh và nhận thông báo khi lệnh được khớp.
Ở phía Client, người dùng đăng nhập và lấy thông tin tài khoản thông qua REST API. Khi mở màn hình giao dịch, ứng dụng thiết lập WebSocket để nhận dữ liệu giá thị trường liên tục. Khi người dùng đặt lệnh, Client gửi request đến Order API. Backend xác thực người dùng, kiểm tra quyền giao dịch, xác minh điều kiện nghiệp vụ và xử lý yêu cầu theo luồng đặt lệnh được thiết kế.
Trong quá trình xử lý, Order Service có thể gọi Account Service bằng gRPC để kiểm tra những thông tin cần thiết. Tuy nhiên, việc kiểm tra số dư hoặc hạn mức bằng một RPC riêng không tự bảo đảm tính nhất quán khi có nhiều lệnh đồng thời. Những điều kiện tài chính quan trọng vẫn cần được bảo vệ bằng cơ chế kiểm soát trạng thái, đặt giữ tài sản hoặc giao dịch phù hợp tại thành phần có thẩm quyền.
Khi nhận được kết quả khớp lệnh từ hệ thống giao dịch, Backend ghi nhận trạng thái nghiệp vụ và phát sự kiện để những service liên quan cập nhật vị thế, lịch sử và thông báo. Message broker giúp các service xử lý sự kiện theo trách nhiệm của mình mà không cần tất cả cùng hoàn thành trong một request đồng bộ.
Cuối cùng, Client nhận cập nhật trạng thái qua WebSocket. Nếu kết nối bị ngắt, ứng dụng có thể truy vấn lại trạng thái lệnh qua REST API hoặc sử dụng cơ chế replay nếu hệ thống hỗ trợ.
Web / Mobile Client
|
+--------------+--------------+
| |
REST/HTTPS WebSocket
| |
v v
API Gateway Realtime Gateway
| ^
v |
Order Service |
| |
| gRPC |
v |
Account Service |
| |
+--------------+--------------+
|
Event Broker
|
+--------------+--------------+
| | |
v v v
Position Notification Analytics
Service Service ServiceĐây chỉ là kiến trúc minh họa ở mức khái niệm. Trong hệ thống thực tế, luồng xử lý đặt lệnh, nguồn dữ liệu có thẩm quyền, vị trí của broker, cơ chế cập nhật tài sản và cách phân phối sự kiện cần được thiết kế theo yêu cầu nghiệp vụ cụ thể.
Điểm đáng chú ý là không có công nghệ nào trong sơ đồ phải giải quyết toàn bộ bài toán. REST xử lý những yêu cầu phù hợp với request–response, gRPC hỗ trợ giao tiếp giữa các service, WebSocket cung cấp cập nhật real-time, còn messaging giúp phân phối sự kiện bất đồng bộ.
Một kiến trúc tốt không nhất thiết sử dụng nhiều công nghệ. Nếu ứng dụng nhỏ và chỉ có vài chức năng CRUD, REST API có thể đã đủ. Việc bổ sung WebSocket, gRPC hoặc Kafka khi chưa có nhu cầu thực tế có thể làm tăng độ phức tạp mà không mang lại lợi ích tương xứng.
Ngược lại, khi yêu cầu nghiệp vụ thực sự cần streaming, giao tiếp hai chiều hoặc xử lý bất đồng bộ, việc cố sử dụng một công nghệ cho mọi tình huống cũng có thể khiến hệ thống khó mở rộng và vận hành.
15. Những điều quan trọng cần ghi nhớLink to this heading
Sau khi đi qua TCP, TLS, các phiên bản HTTP và những mô hình giao tiếp phổ biến, điều mình muốn nhấn mạnh nhất là chúng ta không nên bắt đầu thiết kế hệ thống bằng câu hỏi “Nên dùng công nghệ nào?”. Thay vào đó, hãy bắt đầu bằng việc hiểu dữ liệu cần được trao đổi như thế nào và điều gì sẽ xảy ra nếu quá trình trao đổi gặp lỗi.
TCP cung cấp một luồng byte đáng tin cậy và có thứ tự, nhưng không tự xác định ranh giới message. TLS bảo vệ kênh truyền, nhưng không thay thế authentication hoặc authorization. HTTP/2 và HTTP/3 cải thiện cách truyền HTTP, nhưng không tự biến API thành hệ thống real-time.
REST phù hợp với nhiều nghiệp vụ request–response và có khả năng tích hợp rộng. gRPC hữu ích khi cần hợp đồng dịch vụ rõ ràng, typed messages và streaming giữa những hệ thống có khả năng kiểm soát client. WebSocket phù hợp với giao tiếp hai chiều liên tục, còn SSE là lựa chọn đơn giản khi Server chủ yếu cần gửi sự kiện một chiều. Messaging giúp các thành phần xử lý bất đồng bộ và giảm phụ thuộc trực tiếp, nhưng yêu cầu thiết kế cẩn thận về độ tin cậy, thứ tự và tính nhất quán dữ liệu.
Quan trọng hơn, không có công nghệ giao tiếp nào tự giải quyết được mọi vấn đề của hệ thống phân tán. Dù sử dụng REST, gRPC, WebSocket hay Kafka, chúng ta vẫn cần xử lý timeout, retry, duplicate delivery, authorization, observability và khả năng phục hồi sau sự cố.
Kết luậnLink to this heading
Khi mới học Backend, chúng ta thường bắt đầu bằng việc xây dựng một REST API, gửi request từ Postman và nhận JSON response. Sau đó, khi làm việc với những hệ thống phức tạp hơn, chúng ta gặp gRPC, WebSocket, Kafka và nhiều công nghệ khác. Nếu chỉ học từng công nghệ một cách riêng lẻ, rất dễ có cảm giác rằng mỗi công nghệ là một thế giới hoàn toàn khác nhau.
Nhưng khi nhìn từ bản chất, tất cả đều đang giải quyết những biến thể của cùng một bài toán: làm thế nào để các thành phần phần mềm trao đổi thông tin đúng cách, hiệu quả, an toàn và đáng tin cậy.
Một Backend Engineer giỏi không nhất thiết phải sử dụng công nghệ mới nhất hoặc phức tạp nhất. Điều quan trọng hơn là hiểu yêu cầu nghiệp vụ, nhận diện những rủi ro có thể xảy ra và lựa chọn giải pháp đủ đơn giản để vận hành nhưng vẫn đáp ứng được nhu cầu phát triển của hệ thống.
Trong bài tiếp theo của series, chúng ta sẽ đi sâu vào RESTful API, từ cách HTTP thực sự hoạt động, thiết kế resource và status code đến idempotency, caching, pagination, authentication, authorization và những sai lầm thường gặp khi xây dựng API cho production bằng Go.
---
