Khi REST API bắt đầu hụt hơi trong hệ thống Microservices
Hồi mới làm hệ thống phân tán, team mình mặc định chọn REST qua HTTP/1.1 kèm payload JSON. Khi cụm chỉ có 2-3 service, mọi thứ vẫn ổn định. Nhưng khi hệ thống chạm mốc hơn 15 microservices gọi chéo liên tục, độ trễ p99 vọt từ 12ms lên gần 180ms.
Thủ phạm không ở đâu xa. JSON là text format nên CPU tốn rất nhiều chu kỳ để parse chuỗi. Ngoài ra, HTTP/1.1 còn bị nghẽn bởi head-of-line blocking. Mỗi request đều phải tranh nhau socket TCP rảnh hoặc tốn chi phí bắt tay 3 bước mở kết nối mới. Đó là lúc team quyết định đưa gRPC, Go và Protocol Buffers vào toàn bộ luồng mạng nội bộ.
Bản chất: gRPC và Protocol Buffers vận hành thế nào?
1. Protocol Buffers (Protobuf) giải quyết bài toán gì?
Protobuf là cơ chế tuần tự hóa dữ liệu nhị phân (binary serialization) của Google. Thay vì gửi chuỗi JSON dài dòng như {"order_id": 101, "status": "PAID"} (tốn khoảng 34 bytes), Protobuf mã hóa toàn bộ thành mảng byte chỉ vỏn vẹn 6-8 bytes dựa trên schema định sẵn trong file .proto.
2. Vì sao gRPC lại vượt trội hơn REST truyền thống?
- Multiplexing trên HTTP/2: Hàng trăm request và response có thể truyền song song trên duy nhất một kết nối TCP. Tình trạng nghẽn hàng đợi kết nối biến mất.
- Tải trọng mạng giảm 60-80%: Dữ liệu nhị phân kèm nén header (HPACK) giúp tiết kiệm băng thông đáng kể, đặc biệt khi throughput đạt hàng chục nghìn RPS.
- Hợp đồng API chặt chẽ (Strict Contract): Code client và server tự động sinh ra từ file
.proto. Compiler sẽ bắt lỗi sai kiểu dữ liệu ngay từ lúc build thay vì đợi runtime văng lỗi 500.
3. Bốn mô hình giao tiếp trong gRPC
- Unary RPC: Request – Response một-một quen thuộc, tương tự như gọi REST API.
- Server Streaming RPC: Client bắn 1 request, Server giữ kết nối và đẩy liên tục chuỗi message về theo thời gian thực.
- Client Streaming RPC: Client stream nhiều message lên Server rồi nhận lại đúng 1 kết quả tổng hợp.
- Bidirectional Streaming RPC: Cả Client và Server đều đọc ghi dữ liệu độc lập hai chiều cùng lúc.
Thực hành: Xây dựng Order Service với Go
Chúng ta sẽ dựng một service đơn hàng thực tế gồm hai luồng: Unary để tạo đơn và Server Streaming để cập nhật trạng thái vận chuyển theo thời gian thực.
Bước 1: Cài đặt công cụ
Hãy cài trình biên dịch protoc và hai plugin sinh mã Go cho terminal:
# Cai dat plugin sinh ma Go cho protoc
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
# Dua GOPATH/bin vao PATH
export PATH="$PATH:$(go env GOPATH)/bin"
Bước 2: Khởi tạo project và viết file contract
Thiết lập thư mục dự án:
mkdir -p grpc-order-demo/proto
cd grpc-order-demo
go mod init grpc-order-demo
Định nghĩa schema trong file proto/order.proto:
syntax = "proto3";
package order;
option go_package = "grpc-order-demo/proto/orderpb";
message CreateOrderRequest {
string customer_id = 1;
double amount = 2;
string item_name = 3;
}
message CreateOrderResponse {
string order_id = 1;
string status = 2;
}
message TrackOrderRequest {
string order_id = 1;
}
message OrderStatusUpdate {
string order_id = 1;
string step = 2;
string timestamp = 3;
}
service OrderService {
// Unary RPC: Tao don hang moi
rpc CreateOrder (CreateOrderRequest) returns (CreateOrderResponse);
// Server Streaming RPC: Stream trang thai don theo thoi gian thuc
rpc TrackOrder (TrackOrderRequest) returns (stream OrderStatusUpdate);
}
Bước 3: Biên dịch file .proto sang Go
Chạy lệnh protoc từ thư mục gốc dự án:
mkdir -p proto/orderpb
protoc --go_out=. --go_opt=paths=source_relative \
--go-grpc_out=. --go-grpc_opt=paths=source_relative \
proto/order.proto
mv proto/order.pb.go proto/order_grpc.pb.go proto/orderpb/
go mod tidy
Sau bước này, bạn sẽ có hai file sinh tự động trong proto/orderpb chứa toàn bộ struct và interface để server/client giao tiếp.
Bước 4: Triển khai gRPC Server
Tạo file server/main.go. Server sẽ mở cổng :50051 và hiện thực hóa logic nghiệp vụ:
package main
import (
"context"
"fmt"
"log"
"net"
"time"
pv "grpc-order-demo/proto/orderpb"
"google.golang.org/grpc"
)
type orderServer struct {
pv.UnimplementedOrderServiceServer
}
func (s *orderServer) CreateOrder(ctx context.Context, req *pv.CreateOrderRequest) (*pv.CreateOrderResponse, error) {
orderID := fmt.Sprintf("ORD-%d", time.Now().UnixNano()%100000)
log.Printf("[Server] Nhan don tu %s: %s ($%.2f)", req.GetCustomerId(), req.GetItemName(), req.GetAmount())
return &pv.CreateOrderResponse{
OrderId: orderID,
Status: "CONFIRMED",
}, nil
}
func (s *orderServer) TrackOrder(req *pv.TrackOrderRequest, stream pv.OrderService_TrackOrderServer) error {
stages := []string{"Da tiep nhan", "Dang dong goi", "Dang ban giao van chuyen", "Giao thanh cong"}
for _, stage := range stages {
update := &pv.OrderStatusUpdate{
OrderId: req.GetOrderId(),
Step: stage,
Timestamp: time.Now().Format(time.RFC3339),
}
if err := stream.Send(update); err != nil {
return err
}
time.Sleep(800 * time.Millisecond)
}
return nil
}
func main() {
lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalf("Khong the bind port: %v", err)
}
s := grpc.NewServer()
pv.RegisterOrderServiceServer(s, &orderServer{})
log.Println("gRPC Server dang chay tai :50051...")
if err := s.Serve(lis); err != nil {
log.Fatalf("Loi chay server: %v", err)
}
}
Bước 5: Viết gRPC Client
Tạo file client/main.go để giả lập một microservice khác gọi tới Order Service:
package main
import (
"context"
"io"
"log"
"time"
pv "grpc-order-demo/proto/orderpb"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
func main() {
// Su dung NewClient cho cac phien ban Go gRPC moi
conn, err := grpc.NewClient("localhost:50051", grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
log.Fatalf("Ket noi that bai: %v", err)
}
defer conn.Close()
client := pv.NewOrderServiceClient(conn)
// 1. Goi Unary RPC co timeout
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
res, err := client.CreateOrder(ctx, &pv.CreateOrderRequest{
CustomerId: "CUST-9921",
Amount: 129.50,
ItemName: "Ban phim Keychron K2 Pro",
})
if err != nil {
log.Fatalf("Loi tao don: %v", err)
}
log.Printf("[Client] Tao thanh cong! Ma: %s | Trang thai: %s", res.GetOrderId(), res.GetStatus())
// 2. Goi Server Streaming RPC
log.Println("[Client] Dang stream tien do don hang...")
stream, err := client.TrackOrder(context.Background(), &pv.TrackOrderRequest{OrderId: res.GetOrderId()})
if err != nil {
log.Fatalf("Loi bat stream: %v", err)
}
for {
update, err := stream.Recv()
if err == io.EOF {
log.Println("[Client] Don hang da giao xong. Ket thuc stream.")
break
}
if err != nil {
log.Fatalf("Loi doc stream: %v", err)
}
log.Printf("-> [%s] %s", update.GetTimestamp(), update.GetStep())
}
}
Kinh nghiệm thực chiến: Debug và kiểm thử payload nhị phân
Vì gRPC truyền binary, bạn không thể dùng curl kiểm tra endpoint như REST. Thay vào đó, bạn nên cài grpcurl để tương tác trực tiếp qua CLI.
Khi cần chuyển payload Protobuf sang JSON để trao đổi với frontend hoặc soi log debug, việc định dạng dữ liệu thủ công rất dễ sót dấu ngoặc hay sai kiểu số. Bạn có thể dùng nhanh các tiện ích trực tuyến như JSON Formatter trên toolcraft.app để format, validate cú pháp chuẩn xác mà không cần cài thêm plugin IDE cồng kềnh.
Khi nào nên chọn gRPC, khi nào giữ lại REST?
gRPC và REST bổ trợ cho nhau chứ không triệt tiêu nhau. Kinh nghiệm thực tế từ các dự án backend lớn cho thấy:
- Nên chọn gRPC khi: Xây dựng giao tiếp nội bộ giữa các microservices (East-West traffic), hệ thống IoT/Streaming cần độ trễ dưới 10ms, hoặc các team đa ngôn ngữ (Go, Java, Python) cần chung một schema nhất quán.
- Nên giữ lại REST khi: Làm Public API cho đối tác thứ ba, API trực tiếp cho trình duyệt Web (tránh overhead cài thêm gRPC-Web proxy), hoặc các service CRUD nhỏ cần release nhanh.
Sở hữu Protobuf và gRPC trong tech stack Go giúp bạn tự tin giải quyết triệt để bài toán thắt nút cổ chai băng thông khi hệ thống scale lên hàng chục ngàn kết nối đồng thời.
