gRPC cơ bản là kỹ năng cần thiết khi làm microservice nội bộ. Bài này giới thiệu protocol buffers, 4 mode streaming, kèm code Node.js minh hoạ — và quan trọng hơn: khi nào nên dùng gRPC, khi nào REST đủ.
gRPC là gì?
gRPC = Google Remote Procedure Call. Framework RPC mở do Google phát hành 2015, dựa trên:
- Protocol Buffers (protobuf): binary format có schema
- HTTP/2: multiplexing, server push, header compression
- Code generation: từ .proto file → client + server code cho 11+ ngôn ngữ
So với REST, gRPC tập trung vào: tốc độ, contract chặt, streaming. Đánh đổi: phức tạp hơn, browser không support native.
.proto file: schema-first design
// users.proto
syntax = "proto3";
package alodev.users.v1;
service UserService {
rpc GetUser (GetUserRequest) returns (User);
rpc ListUsers (ListUsersRequest) returns (ListUsersResponse);
rpc StreamUserUpdates (StreamRequest) returns (stream UserUpdate);
}
message User {
int64 id = 1;
string email = 2;
string name = 3;
Role role = 4;
google.protobuf.Timestamp created_at = 5;
}
enum Role {
ROLE_UNSPECIFIED = 0;
ROLE_USER = 1;
ROLE_ADMIN = 2;
}
message GetUserRequest {
int64 id = 1;
}
message ListUsersRequest {
int32 page_size = 1;
string page_token = 2;
}
message ListUsersResponse {
repeated User users = 1;
string next_page_token = 2;
}
Mỗi field có tag number (1, 2, 3...) — đây là khoá để binary encoding biết field nào là gì. Tag không bao giờ reuse — đây là rule cho backwards compat.
Implement server Node.js
npm install @grpc/grpc-js @grpc/proto-loader
// server.js
import grpc from '@grpc/grpc-js'
import protoLoader from '@grpc/proto-loader'
const def = protoLoader.loadSync('./users.proto', {
keepCase: true, longs: String, enums: String, defaults: true,
})
const proto = grpc.loadPackageDefinition(def).alodev.users.v1
const userImpl = {
GetUser: async (call, callback) => {
const user = await db.users.findById(call.request.id)
if (!user) {
return callback({
code: grpc.status.NOT_FOUND,
message: `User ${call.request.id} không tồn tại`,
})
}
callback(null, {
id: user.id,
email: user.email,
name: user.name,
role: user.role,
created_at: { seconds: Math.floor(user.created_at.getTime() / 1000) },
})
},
ListUsers: async (call, callback) => {
const limit = call.request.page_size || 50
const cursor = call.request.page_token ? parseInt(call.request.page_token) : 0
const rows = await db.users.list({ offset: cursor, limit })
callback(null, {
users: rows,
next_page_token: rows.length === limit ? String(cursor + limit) : '',
})
},
// Server streaming — push user updates real-time
StreamUserUpdates: (call) => {
const interval = setInterval(async () => {
const update = await getNextUpdate()
if (update) call.write(update)
}, 1000)
call.on('end', () => clearInterval(interval))
},
}
const server = new grpc.Server()
server.addService(proto.UserService.service, userImpl)
server.bindAsync('0.0.0.0:50051', grpc.ServerCredentials.createInsecure(), () => {
console.log('gRPC server :50051')
})
Implement client Node.js
// client.js
import grpc from '@grpc/grpc-js'
import protoLoader from '@grpc/proto-loader'
const def = protoLoader.loadSync('./users.proto', { /* ... */ })
const proto = grpc.loadPackageDefinition(def).alodev.users.v1
const client = new proto.UserService(
'localhost:50051',
grpc.credentials.createInsecure()
)
// Unary call
client.GetUser({ id: 42 }, (err, user) => {
if (err) {
if (err.code === grpc.status.NOT_FOUND) console.log('Not found')
else throw err
}
console.log(user)
})
// Server streaming
const stream = client.StreamUserUpdates({})
stream.on('data', (update) => console.log('update:', update))
stream.on('error', (err) => console.error(err))
stream.on('end', () => console.log('stream ended'))
4 mode streaming
1. Unary: client → server (1 req, 1 resp). Như REST.
2. Server stream: client → server (1 req, N resp). Push notification.
3. Client stream: client → server (N req, 1 resp). Upload file.
4. Bidirectional: client ⇄ server (N req, N resp). Chat, real-time.
Đây là điểm mạnh lớn so với REST — REST chỉ unary native. SSE giải được server stream nhưng không có chiều ngược.
Khi nào dùng gRPC?
✓ Dùng gRPC khi:
- Service-to-service nội bộ — không browser facing
- Latency-sensitive — payment, ad serving, real-time bidding
- Streaming là requirement (server push, bidirectional)
- Multi-language: Go service gọi Python service gọi Java service
- Schema strict bắt buộc (PCI, fintech)
✗ Không nên gRPC khi:
- Public API browser-facing
- Tooling đơn giản quan trọng (REST + Postman)
- Team mới — learning curve cao
- Caching HTTP cần thiết (gRPC không cache HTTP)
- Debug bằng curl/log dễ — protobuf binary khó đọc
gRPC vs REST so sánh nhanh
| Khía cạnh | gRPC | REST |
|---|---|---|
| Format | Binary (protobuf) | Text (JSON) |
| Transport | HTTP/2 | HTTP/1.1 hoặc 2 |
| Schema | .proto bắt buộc | OpenAPI optional |
| Streaming | 4 mode native | SSE / WebSocket |
| Browser | Cần gRPC-Web proxy | Native |
| Tooling | BloomRPC, grpcurl | Postman, curl |
| Code gen | Built-in cho 11+ lang | OpenAPI codegen |
| Learning curve | Cao | Thấp |
Kết luận
gRPC cơ bản dễ học — phức tạp ở tooling và operational. Nếu anh đang build microservice nội bộ với latency yêu cầu < 50ms, gRPC là lựa chọn xuất sắc. Public API hoặc team mới thì REST vẫn là default an toàn. Đọc thêm Microservices vs Monolith để hiểu khi nào việc tách service xứng đáng đầu tư gRPC.