gRPC — Khi REST không còn đủ nhanh cho microservices

Phong Hy

Khi anh em build microservices, câu hỏi đầu tiên thường là: "Dùng REST hay gRPC?".

Hồi mới làm backend, mình cũng mặc định REST cho mọi thứ — đơn giản, dễ hiểu, ai cũng biết. Nhưng khi hệ thống lớn dần, mình bắt đầu thấy REST có những chỗ không ổn: response JSON quá verbose, không có contract rõ ràng, không support streaming tốt, và latency cao hơn mức cần thiết.

Đó là lúc mình tìm đến gRPC — và từ đó tới giờ không quay lại.

gRPC là gì?

gRPC là framework RPC (Remote Procedure Call) của Google, dùng Protobuf (Protocol Buffers) để serialize dữ liệu và HTTP/2 để vận chuyển. Thay vì gửi JSON text qua HTTP/1.1 như REST, gRPC gửi binary Protobuf qua HTTP/2 — nhỏ hơn, nhanh hơn, và support multiplexing + streaming mặc định.

Code thực tế với Go

Trước tiên, mình define service trong file .proto:

syntax = "proto3";

service UserService {
  rpc GetUser (GetUserRequest) returns (User);
  rpc ListUsers (ListUsersRequest) returns (stream User);
}

message GetUserRequest { string id = 1; }
message User {
  string id = 1;
  string name = 2;
  string email = 3;
}

Generate code xong, implement server:

type userServer struct {
  pb.UnimplementedUserServiceServer
}

func (s *userServer) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.User, error) {
  // query database thực tế ở đây
  return &pb.User{
    Id:    req.Id,
    Name:  "Phong Hy",
    Email: "phong@hy.dev",
  }, nil
}

func main() {
  lis, _ := net.Listen("tcp", ":50051")
  s := grpc.NewServer()
  pb.RegisterUserServiceServer(s, &userServer{})
  s.Serve(lis)
}

Client gọi:

conn, _ := grpc.Dial("localhost:50051", grpc.WithInsecure())
client := pb.NewUserServiceClient(conn)

user, _ := client.GetUser(context.Background(), &pb.GetUserRequest{Id: "1"})
fmt.Printf("User: %s <%s>\n", user.Name, user.Email)

Kinh nghiệm thực tế từ dự án của mình

Mình đã migrate một hệ thống gồm 12 microservices từ REST qua gRPC. Kết quả:

  • Latency giảm 40-60% — Protobuf binary nhỏ hơn JSON 3-5 lần, parse nhanh hơn.
  • Bandwidth giảm rõ rệt — request/response gọn hơn, đặc biệt quan trọng khi call internal service hàng triệu lần/ngày.
  • Contract chặt chẽ — file .proto là single source of truth, generate code tự động cho cả server lẫn client. Không còn chuyện "quên update docs" hay "response thay đổi không ai biết".
  • Streaming real-time — với REST, muốn stream dữ liệu phải dùng WebSocket hoặc SSE riêng. gRPC support streaming mặc định, chỉ cần thêm stream trong proto.

Tuy nhiên, gRPC cũng có caveat:

  1. Không dùng được browser trực tiếp — browser không support gRPC natively, cần thêm gRPC-Web proxy.
  2. Debug khó hơn — không thể curl request như REST. Phải dùng grpcurl hoặc Postman mới nhất.
  3. Load balancing phức tạp hơn — HTTP/2 multiplexing làm LB truyền thống khó xử lý, cần L7 LB support gRPC.

Khi nào nên dùng gRPC?

  • Internal microservices communication — đây là use case chính. Giữa services với nhau, gRPC là lựa chọn tối ưu.
  • Real-time streaming — cần push data real-time giữa các services.
  • Polyglot environment — team dùng nhiều ngôn ngữ (Go, Python, Java, Rust...), Protobuf generate code cho tất cả.

Còn REST vẫn tốt cho public-facing API vì universal, dễ debug và browser-friendly.

Kết luận

gRPC không phải "kẻ hủy diệt REST" — mỗi cái có chỗ đứng riêng. Nhưng nếu bạn đang build internal microservices, đặc biệt với Go, gRPC là thứ bạn không nên bỏ qua. Latency giảm một nửa, contract rõ ràng, code generation tự động — đó là những thứ mình trân trọng nhất ở gRPC.

Hãy thử migrate một endpoint nhỏ của bạn qua gRPC và tự đo speed — bạn sẽ bất ngờ đấy.