Khi hai service cần nói chuyện với nhau, lựa chọn mặc định của hầu hết lập trình viên là REST + JSON: định nghĩa vài endpoint, trả về JSON, parse ở phía kia. Nó hoạt động, nhưng khi hệ thống lớn lên — nhiều service gọi nhau hàng triệu lần, cần tốc độ, cần hợp đồng (contract) rõ ràng để không gọi nhầm — REST/JSON bắt đầu lộ chi phí: payload text cồng kềnh, không có contract bắt buộc nên dễ lệch, và không có streaming sẵn. gRPC là câu trả lời của Google cho bài toán này: bạn định nghĩa dịch vụ trong một file .proto, công cụ protoc sinh code type-safe cho cả hai phía, và lời gọi mạng trông y như gọi một hàm nội bộ. Bài mở màn sê-ri này (phần 1/12) dựng server + client gRPC thật bằng Go và đo thật những gì nó mang lại.
gRPC là gì: contract-first, code sinh tự động
Khác biệt cốt lõi của gRPC so với REST là contract-first. Bạn không viết code xử lý request trước; bạn định nghĩa hợp đồng trong .proto — các message và các RPC — rồi để protoc sinh ra stub cho server và client.
message GetOrderRequest { int64 id = 1; }
message Order { int64 id = 1; string customer = 2; double amount = 3; string status = 4; string region = 5; }
service OrderService {
rpc GetOrder(GetOrderRequest) returns (Order);
}
Chạy protoc --go_out=. --go-grpc_out=. pb/order.proto sinh ra code Go: interface server để bạn cài logic, và client stub để gọi như hàm thường. Không parse URL, không marshal JSON tay, không đoán kiểu dữ liệu — tất cả type-safe và sinh tự động.

Hình 1: Quy trình gRPC — định nghĩa .proto, protoc sinh stub, viết server + client, gọi cli.GetOrder(...) như hàm Go. Bảng so sánh gRPC (HTTP/2, Protobuf nhị phân, contract bắt buộc, streaming sẵn) với REST/JSON.
So với REST/JSON, gRPC khác ở bốn điểm chính: chạy trên HTTP/2 (nhiều request song song trên một kết nối), payload là Protobuf nhị phân (nhỏ hơn JSON), contract .proto bắt buộc (type-safe, không lệch), và streaming sẵn 4 kiểu (bài kafka... à, bài grpc-03). Đổi lại, trình duyệt không gọi thẳng gRPC được (cần grpc-web proxy) — đây là lý do gRPC chủ yếu dùng cho giao tiếp service-to-service phía backend, còn REST vẫn hợp cho API công khai ra trình duyệt.
Đo thật: unary call, kích thước và latency
Mình dựng server OrderService và client trong go-lab, gọi một unary RPC (một request → một response, kiểu RPC cơ bản nhất), rồi đo hai thứ: kích thước message trên dây và latency.
cli := pb.NewOrderServiceClient(conn)
o, err := cli.GetOrder(context.Background(), &pb.GetOrderRequest{Id: 1001})
// o.Customer == "nguyen-van-a", o.Amount == 250000 — type-safe, không cast

Hình 2: Đo thật trong go-lab. Cùng message Order: Protobuf 41 byte, JSON 88 byte (~2,1× nhỏ hơn). Unary call GetOrder(1001) trả về đúng Order; 1000 call hết 42 ms, trung bình 0,042 ms/call, ~23.800 call/giây trên localhost.
Kết quả thật:
- Protobuf 41 byte vs JSON 88 byte cho cùng một message Order. Protobuf nhỏ hơn ~2,1 lần vì mã hoá nhị phân theo field number (số 1, 2, 3... trong
.proto) thay vì nhúng tên trường như JSON. Bài grpc-02 sẽ mổ xẻ wire format này. - Unary call hoạt động type-safe:
cli.GetOrder(ctx, &GetOrderRequest{Id:1001})trả về*Ordervới các trường đã có kiểu — không cast, không parse. Stub doprotocsinh lo toàn bộ việc mã hoá sang Protobuf, gửi qua HTTP/2, và giải mã. - Latency rất thấp: 1000 call hết 42 ms, trung bình 0,042 ms/call (42 micro-giây round-trip trên localhost), tức ~23.800 call/giây một luồng tuần tự. Trên localhost con số này chủ yếu đo chi phí mã hoá + HTTP/2 framing, không phải mạng; nhưng nó cho thấy gRPC "nhẹ" cỡ nào ở tầng gọi.
Lưu ý trung thực: 23.800 call/giây là tuần tự một luồng trên localhost — không phải giới hạn throughput của gRPC (gọi song song nhiều luồng sẽ cao hơn nhiều). Và latency thật trên mạng data center sẽ bị cộng thêm round-trip mạng (thường 0,1–1 ms), lớn hơn hẳn 42 micro-giây localhost. Số ở đây để thấy chi phí tầng gRPC, không phải để suy ra hiệu năng production.
Đánh đổi cần cân nhắc
gRPC mạnh cho service-to-service, không cho API ra trình duyệt. Vì trình duyệt không nói HTTP/2 trailers kiểu gRPC cần, muốn gọi từ JavaScript phải qua grpc-web + proxy — thêm một tầng. Với API công khai ra web/mobile, REST/JSON (hoặc GraphQL) thường gọn hơn. Chọn gRPC khi hai đầu đều là service của bạn.
Contract-first là kỷ luật, không phải gánh nặng — nếu quản lý tốt. .proto bắt buộc đồng bộ giữa client và server; đổi contract phải sinh lại code cả hai phía. Đây là điểm mạnh (không lệch âm thầm như JSON) nhưng cần quy trình: lưu .proto ở nơi dùng chung, sinh code trong CI, versioning rõ ràng. Thiếu kỷ luật này, nhiều team sinh code lệch nhau là một mớ hỗn độn.
Khó debug hơn REST. Payload nhị phân không đọc được bằng mắt như JSON; không curl thẳng được. Cần công cụ riêng (grpcurl, server reflection — bài grpc-11) để gọi thử. Đổi lấy hiệu năng và type-safety là mất đi sự tiện lợi "mở trình duyệt xem JSON". Cân nhắc điều này cho trải nghiệm phát triển của đội.
Ba ý mang về
- gRPC là contract-first: định nghĩa
.proto, sinh code, gọi như hàm nội bộ.protocsinh stub type-safe cho cả server lẫn client;cli.GetOrder(ctx, req)trông như một lời gọi hàm Go bình thường mà thực chất đi qua mạng. Không parse URL, không marshal tay. - Protobuf nhỏ hơn JSON đáng kể. Đo thật: cùng message Order, Protobuf 41 byte vs JSON 88 byte (~2,1× nhỏ hơn) nhờ mã hoá nhị phân theo field number. Trên khối lượng lớn, chênh lệch này là băng thông thật.
- gRPC nhẹ ở tầng gọi, nhưng chọn đúng ngữ cảnh. Đo thật: unary call ~0,042 ms trên localhost (~23.800 call/giây một luồng). gRPC toả sáng cho giao tiếp service-to-service hiệu năng cao; cho API ra trình duyệt hay cần debug bằng mắt thì REST/JSON vẫn hợp hơn.
Nguồn
- gRPC — Introduction to gRPC: https://grpc.io/docs/what-is-grpc/introduction/
- gRPC — Quick start (Go): https://grpc.io/docs/languages/go/quickstart/
- Protocol Buffers — Overview: https://protobuf.dev/overview/
Phần sau ta mổ xẻ Protocol Buffers ở mức byte: wire format nhị phân, varint và field number mã hoá thế nào, và vì sao Protobuf vừa nhỏ vừa nhanh hơn JSON — đo thật từng byte.