REST + JSON là lựa chọn mặc định cho API: ai cũng biết, chạy trên mọi ngôn ngữ, debug chỉ cần curl và đọc bằng mắt. Vậy tại sao các hệ thống lớn — Google, Netflix, Uber — lại dùng gRPC cho giao tiếp giữa các service nội bộ? Câu trả lời không phải "gRPC tốt hơn REST" (sai) mà là "gRPC đánh đổi khác, hợp với bài toán khác". Khi hai service gọi nhau hàng triệu lần mỗi giây trong một cluster, hai thứ trở nên quan trọng: kích thước mỗi message trên mạng và tốc độ mã hoá/giải mã — và đây là nơi gRPC + protobuf vượt trội. Bài này (phần 1 loạt gRPC nâng cao) đo thật khác biệt đó bằng con số, thay vì nói chung chung.

gRPC là gì: hợp đồng + codegen + HTTP/2

gRPC gồm ba thành phần ghép lại:

  • Protocol Buffers (protobuf): định dạng mã hoá nhị phân và một ngôn ngữ khai báo hợp đồng (file .proto). Message được mô tả bằng các trường có số thứ tự (field number), không phải tên.
  • Codegen: từ .proto, công cụ protoc sinh code cho server và client — struct, hàm marshal/unmarshal, stub RPC — đảm bảo hai bên type-safe, không parse tay.
  • HTTP/2: giao thức truyền, hỗ trợ multiplexing (nhiều request trên một kết nối), streaming hai chiều, nén header.

Kết quả: client gọi một RPC như gọi hàm cục bộ, trong khi bên dưới là mã hoá nhị phân gọn truyền qua HTTP/2.

// order.proto — hợp đồng type-safe
message Order {
  int64 id = 1;          // field number, KHÔNG phải tên
  int64 amount = 2;
  repeated string items = 3;
  string currency = 4;
}
service OrderService {
  rpc GetOrder(OrderReq) returns (Order);
}
# sinh code Go từ .proto:
protoc --go_out=. --go-grpc_out=. order.proto
// client gọi như hàm cục bộ (type-safe):
o, _ := client.GetOrder(ctx, &OrderReq{Id: 1001})

Ảnh chụp đoạn mã nền tối gRPC là RPC nhị phân trên HTTP2 cộng protobuf, order.proto hợp đồng type-safe message Order int64 id bằng 1 field number không phải tên int64 amount bằng 2 repeated string items bằng 3 string currency bằng 4 service OrderService rpc GetOrder trả về Order, sinh code Go protoc go_out go-grpc_out order.proto, client gọi như hàm cục bộ GetOrder ctx OrderReq Id 1001, so sánh REST JSON text dễ đọc mọi nơi hỗ trợ debug bằng mắt gRPC protobuf nhị phân gọn nhanh type-safe HTTP2 nhưng cần codegen khó đọc bằng mắt

Hình 1: gRPC khai hợp đồng trong .proto (trường dùng field number thay tên), protoc sinh code type-safe, và client gọi RPC như hàm cục bộ. So với REST/JSON: JSON text dễ đọc và phổ biến; gRPC/protobuf nhị phân gọn, nhanh, type-safe nhưng cần codegen.

Đo thật: protobuf 26 byte vs JSON 70 byte

Mình định nghĩa một Order và serialize nó hai cách — protobuf (proto.Marshal) và JSON (json.Marshal) — rồi đo kích thước byte thật trên go-lab, cộng một unary gRPC call thật để đo độ trễ:

Ảnh chụp output thật nền tối protobuf gọn hơn JSON call nhanh Go proto.Marshal vs json.Marshal cộng unary gRPC, cùng một Order id amount items currency, protobuf bằng 26 byte hex 08e907 1090a10f 1a0663612d706865 1a0462616e68 2203564e44, JSON bằng 70 byte id 1001 amount 250000 items ca-phe banh currency VND, protobuf nhỏ hơn 62.9 phần trăm tiết kiệm 44 byte vì nhị phân cộng dùng field number 1 2 3 thay tên text varint nén số không dấu ngoặc dấu phẩy thừa, unary gRPC call thật server client Go 1000 call trong 71.6 ms bằng 13967 call mỗi giây 0.072 ms mỗi call, gọn hơn bằng ít byte trên mạng cộng parse nhanh đổi lại byte nhị phân không đọc được bằng mắt cần proto cộng codegen

Hình 2: Kết quả thật. Cùng một Order: protobuf = 26 byte (hex 08e907...), JSON = 70 byte → protobuf nhỏ hơn 62.9% (tiết kiệm 44 byte). Unary gRPC call: 1000 call trong 71.6ms → 13.967 call/s, 0.072 ms/call.

Đọc kết quả:

  • Protobuf 26 byte vs JSON 70 byte — gọn hơn 62.9%: cùng một dữ liệu, protobuf chỉ chiếm hơn một phần ba. Lý do nằm ở cách mã hoá: JSON lưu tên trường dạng text ("amount", "currency") lặp lại trong mỗi message, cộng dấu ngoặc, dấu phẩy, dấu nháy. Protobuf lưu số thứ tự trường (1 byte cho tag) thay cho tên, dùng varint nén số nguyên (250000 chỉ vài byte), và không có ký tự phân cách thừa. Nhìn hex 08e907: 08 = tag của field 1 (id), e907 = varint của 1001. Mỗi byte đều có nghĩa, không byte nào phí.
  • Gần 14.000 call/s, 0.072 ms/call: unary gRPC call (server và client trong cùng process, qua localhost) đạt throughput cao và độ trễ dưới 1/10 mili-giây. Con số này gồm cả mã hoá protobuf, truyền HTTP/2, giải mã — toàn bộ vòng call. Tất nhiên đây là localhost không có độ trễ mạng thật, nhưng nó cho thấy overhead của chính gRPC rất thấp.

Ở quy mô nhỏ, 44 byte tiết kiệm nghe không đáng. Nhưng nhân với hàng tỉ message mỗi ngày giữa các service, 63% ít byte hơn nghĩa là ít băng thông, ít chi phí mạng, và parse nhanh hơn — đó là lý do thực tế các hệ thống quy mô lớn chọn gRPC cho giao tiếp nội bộ.

Vì sao field number quan trọng hơn bạn nghĩ

Chi tiết "dùng field number thay tên" không chỉ để tiết kiệm byte — nó là nền tảng của schema evolution (như bài schema của loạt Message Queue). Vì wire format chỉ chứa số (1, 2, 3) chứ không chứa tên, bạn có thể đổi tên một trường trong .proto mà không phá vỡ tương thích wire (số không đổi). Ngược lại, không bao giờ được đổi số thứ tự của một trường đang dùng — đó mới là thứ phá vỡ. Đây là điểm đảo ngược hoàn toàn so với JSON (nơi tên là tất cả): với protobuf, số là hợp đồng, tên chỉ là nhãn cho người đọc. Bài sau (wire format) sẽ mổ xẻ chính xác từng byte này.

Đánh đổi cần cân nhắc

Không đọc được bằng mắt — cái giá của nhị phân. JSON debug bằng curl và đọc trực tiếp; protobuf là byte nhị phân, bạn không thể nhìn 08e907 mà biết đó là gì nếu không có .proto. Điều này làm việc debug, log, và khám phá API khó hơn hẳn. gRPC bù lại bằng công cụ (grpcurl, reflection — bài sau), nhưng rào cản vẫn có thật. Với API công khai cho bên thứ ba, REST/JSON thường vẫn là lựa chọn đúng vì dễ tiếp cận; gRPC toả sáng ở giao tiếp nội bộ nơi cả hai bên bạn kiểm soát.

Cần codegen và .proto — thêm bước build. REST chỉ cần gửi JSON, không cần sinh code. gRPC bắt buộc: viết .proto, chạy protoc, quản lý code sinh ra, đồng bộ .proto giữa các team. Đây là chi phí quy trình thật, đặc biệt khi nhiều ngôn ngữ/team. Bù lại bạn được type-safe (sai kiểu bị bắt lúc compile, không phải lúc chạy) — một đánh đổi đáng giá cho hệ lớn nhưng thừa thãi cho một script nhỏ.

gRPC khó dùng từ trình duyệt. Trình duyệt không nói gRPC trực tiếp (cần gRPC-Web + proxy). Nếu client là web frontend, REST/JSON hoặc GraphQL đơn giản hơn nhiều. gRPC hợp nhất cho service-to-service (backend gọi backend, microservice), nơi không có trình duyệt ở giữa. Chọn gRPC theo vị trí trong kiến trúc, không phải vì nó "nhanh hơn".

Ba ý mang về

  1. Protobuf gọn hơn JSON đáng kể: đo thật cùng một Order, protobuf 26 byte vs JSON 70 byte (nhỏ hơn 62.9%) — vì dùng field number thay tên text, varint nén số, không ký tự phân cách thừa; nhân với tỉ message/ngày là tiết kiệm băng thông lớn.
  2. gRPC call nhanh và type-safe: đo thật unary call đạt ~14.000 call/s, 0.072 ms/call; client gọi RPC như hàm cục bộ nhờ codegen từ .proto, sai kiểu bị bắt lúc compile.
  3. Đánh đổi: gọn/nhanh/type-safe đổi lấy khó đọc + cần codegen: protobuf nhị phân không debug bằng mắt được, cần .proto và protoc, khó dùng từ trình duyệt — gRPC hợp giao tiếp nội bộ service-to-service, còn API công khai/web thường vẫn nên REST/JSON.

Nguồn

Phần sau ta mổ xẻ từng byte của protobuf wire format: giải mã thật chuỗi hex 08e907... thành field number, wire type và giá trị — để hiểu vì sao nó gọn và vì sao field number là hợp đồng thật.