Khi các service cần gọi nhau, REST/JSON là mặc định quen thuộc. Nhưng ở quy mô lớn — hàng triệu lời gọi nội bộ mỗi giây — chi phí của JSON (văn bản dài dòng, parse chậm, không có hợp đồng chặt) trở nên đáng kể. gRPC là câu trả lời của Google: một khung RPC dựa trên protobuf (mã hóa nhị phân) và HTTP/2, với hợp đồng định nghĩa trước và mã sinh tự động. Go là ngôn ngữ hạng nhất của gRPC. Bài này dựng một unary service (kiểu gọi đơn giản nhất: một request, một response) từ đầu, và đo thật vì sao gRPC nhanh và gọn hơn REST/JSON.

Bước 1: định nghĩa hợp đồng bằng .proto

gRPC là contract-first: bạn định nghĩa service và message trong một file .proto, và đó là nguồn sự thật duy nhất. Sinh mã từ đó cho cả client lẫn server:

syntax = "proto3";
service MayTinh {
  rpc Cong(YeuCau) returns (KetQua); // unary: 1 request -> 1 response
}
message YeuCau { int64 a = 1; int64 b = 2; }
message KetQua { int64 tong = 1; }

Mỗi trường có một số thứ tự (a = 1) — số này, không phải tên, là thứ đi trên dây. Chạy protoc với plugin Go sinh ra hai file: .pb.go (message + marshal) và _grpc.pb.go (interface client/server):

protoc --go_out=. --go-grpc_out=. tinhtoan.proto

Kết quả sinh ra các interface MayTinhClient và MayTinhServer — bạn không viết tay phần transport, marshal, hay stub.

Ảnh chụp đoạn mã Go nền tối minh hoạ gRPC unary service từ đầu contract-first bằng protobuf sinh mã, bước 1 định nghĩa hợp đồng bằng proto syntax proto3 service MayTinh rpc Cong YeuCau returns KetQua unary 1 req 1 res message YeuCau int64 a bằng 1 int64 b bằng 2 message KetQua int64 tong bằng 1 sinh mã Go client cộng server protoc go_out go-grpc_out tinhtoan proto, bước 2 server hiện thực interface sinh ra type server struct tinhtoan UnimplementedMayTinhServer func s con trỏ server Cong ctx context Context r con trỏ YeuCau con trỏ KetQua error return KetQua Tong r A cộng r B nil lis bằng net Listen tcp 50051 gs bằng grpc NewServer tinhtoan RegisterMayTinhServer gs server gs Serve lis, bước 3 client gọi RPC như hàm cục bộ conn bằng grpc NewClient 50051 insecure c bằng tinhtoan NewMayTinhClient conn res bằng c Cong ctx YeuCau A 20 B 22 qua mạng trông như gọi hàm res Tong 42, vì sao gRPC hợp đồng proto là nguồn sự thật type-safe đa ngôn ngữ protobuf nhị phân nhỏ và nhanh hơn JSON HTTP 2 multiplexing streaming header nén sinh mã lo phần nhàm chán marshal transport stub

Hình 1: .proto định nghĩa service MayTinh với RPC unary Cong. protoc sinh client + server. Server hiện thực interface (Cong trả tổng); client gọi c.Cong(ctx, req) như một hàm cục bộ — gRPC lo phần transport.

Bước 2 và 3: server và client

Server chỉ cần hiện thực interface sinh ra (nhúng UnimplementedMayTinhServer để tương thích tiến), đăng ký và phục vụ:

type server struct{ tinhtoan.UnimplementedMayTinhServer }
func (s *server) Cong(ctx context.Context, r *tinhtoan.YeuCau) (*tinhtoan.KetQua, error) {
	return &tinhtoan.KetQua{Tong: r.A + r.B}, nil
}
gs := grpc.NewServer()
tinhtoan.RegisterMayTinhServer(gs, &server{})
gs.Serve(lis)

Client quay số tới server và gọi RPC như một hàm cục bộ — gRPC che giấu toàn bộ việc marshal, gửi qua HTTP/2, unmarshal:

c := tinhtoan.NewMayTinhClient(conn)
res, _ := c.Cong(ctx, &tinhtoan.YeuCau{A: 20, B: 22}) // qua mạng

Đo thật (Go 1.23, grpc 1.67): server + client chạy end-to-end qua cổng 50051 — Cong(20, 22) = 42, Cong(100, -30) = 70. Lời gọi đi qua TCP + HTTP/2 thật, nhưng ở tầng code trông y hệt gọi hàm.

Đo thật: protobuf nhỏ và nhanh hơn JSON

Đây là lý do kỹ thuật chọn gRPC. So cùng một message {a: 20, b: 22}:

Ảnh chụp bảng kết quả đo thật nền tối gRPC unary chạy end-to-end protobuf nhỏ hơn JSON 3,75x và marshal nhanh 2x, protoc cộng go run cộng go test bench Go 1.23 grpc 1.67 arm64 10 core, server cộng client chạy thật qua cổng 50051 gRPC Cong 20 22 bằng 42 client gọi RPC server tính trả về gRPC Cong 100 âm 30 bằng 70 qua mạng thật TCP cộng HTTP 2 client gọi c Cong ctx req trông như hàm cục bộ gRPC lo marshal gửi qua HTTP 2 unmarshal ở server và chiều ngược lại, kích thước wire protobuf vs JSON cùng a 20 b 22 protobuf 4 byte 08141016 field cộng varint không tên trường JSON 15 byte kèm dấu ngoặc tên dấu protobuf nhỏ hơn 3,75x mã hóa nhị phân theo số field bỏ tên trường chúng nằm trong proto không đi trên dây, benchmark marshal protobuf vs JSON protobuf Marshal 33,56 ns 4 byte 1 alloc JSON Marshal 70,89 ns 32 byte 2 alloc protobuf nhanh hơn 2,1x và ít cấp phát hơn 4B 1 vs 32B 2, cốt lõi contract-first proto là nguồn sự thật protoc sinh client cộng server unary 1 request 1 response gọi như hàm cục bộ nhỏ hơn protobuf 4 byte vs JSON 15 byte 3,75x nhanh hơn marshal 33,56 vs 70,89 ns 2,1x ít alloc đánh đổi cần bước sinh mã nhị phân khó debug cần HTTP 2

Hình 2: Server + client gRPC chạy thật (Cong(20,22)=42). Wire size: protobuf 4 byte (08141016) so với JSON 15 byte ({"a":20,"b":22}) — nhỏ hơn ~3,75x. Marshal: protobuf 33,56 ns (4 B, 1 alloc) so với JSON 70,89 ns (32 B, 2 alloc) — nhanh hơn ~2,1x.

  • Kích thước dây: protobuf 4 byte (08141016) so với JSON 15 byte ({"a":20,"b":22}) — nhỏ hơn ~3,75x.
  • Tốc độ marshal: protobuf 33,56 ns/op (4 B, 1 cấp phát) so với JSON 70,89 ns/op (32 B, 2 cấp phát) — nhanh hơn ~2,1x.

protobuf nhỏ hơn vì nó mã hóa theo số thứ tự trường (varint) và bỏ hoàn toàn tên trường — tên nằm trong .proto, không đi trên dây. JSON phải mang cả tên trường, dấu ngoặc, dấu nháy trong mỗi message. Ở quy mô hàng triệu message, chênh lệch này là băng thông và CPU thật.

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

gRPC cần bước sinh mã và tooling nặng hơn REST. Bạn phải cài protoc, các plugin Go, và chạy sinh mã mỗi khi đổi .proto. So với REST (chỉ cần encoding/json có sẵn), đây là ma sát thật lúc bắt đầu. gRPC trả cổ tức khi bạn có nhiều service gọi nhau và cần hợp đồng chặt; cho một API công khai đơn giản, REST/JSON thường đủ và dễ hơn.

protobuf nhị phân khó debug bằng mắt. JSON đọc được trực tiếp trong log, curl, trình duyệt; protobuf là byte thô cần công cụ giải mã. Khi gỡ lỗi giao tiếp, bạn mất khả năng "nhìn là hiểu" của JSON. Có grpcurl và reflection để bù, nhưng vẫn thêm một lớp.

gRPC cần HTTP/2 và không chạy thẳng trên trình duyệt. Trình duyệt không cho JavaScript kiểm soát HTTP/2 đủ mức để nói gRPC trực tiếp — cần một lớp proxy grpc-web. Với API mà client là trình duyệt, đây là phức tạp thêm; gRPC hợp nhất cho giao tiếp service-to-service phía sau, nơi cả hai đầu bạn kiểm soát.

Ba ý mang về

  1. gRPC là contract-first: định nghĩa service và message trong .proto (mỗi trường có số thứ tự), protoc sinh ra interface client và server — bạn không viết tay transport hay marshal, chỉ hiện thực interface ở server và gọi RPC như hàm cục bộ ở client (đo thật Cong(20,22)=42 qua HTTP/2).
  2. protobuf nhỏ và nhanh hơn JSON: đo thật cùng message, protobuf 4 byte so với JSON 15 byte (~3,75x nhỏ hơn) vì mã hóa theo số trường và bỏ tên trường; marshal 33,56 ns so với 70,89 ns (~2,1x nhanh hơn, ít cấp phát) — quan trọng ở quy mô triệu lời gọi.
  3. Đổi lại là tooling và khả năng debug: gRPC cần bước sinh mã và protoc, protobuf nhị phân khó đọc bằng mắt, và cần HTTP/2 (trình duyệt phải qua grpc-web) — hợp nhất cho giao tiếp service-to-service, còn API công khai đơn giản thì REST/JSON dễ hơn.

Phần sau ta mở rộng gRPC sang kiểu gọi mạnh hơn unary: Phần sau mổ xẻ gRPC streaming — server stream, client stream, và bidirectional stream, cùng khi nào mỗi kiểu vượt trội so với unary lặp lại.