วันที่ครั้งแรกได้ยิน gRPC
ตอนได้ยิน gRPC ครั้งแรก แรกๆ นึกว่า “อะไรเนี่ย มาทำให้ชีวิตซับซ้อนอีกแล้ว” 😅
เพราะตอนนั้นใช้ REST API แล้วก็พอใจแล้วนะ:
- GET, POST, PUT, DELETE เข้าใจง่าย
- JSON response อ่านง่าย
- HTTP status codes คุ้นเคย
แต่พอเจอปัญหาเรื่อง performance กับ type safety ใน microservices หลายตัวที่ติดต่อกันบ่อยๆ เราก็เริ่มมองหา alternative
ความท้าทายแรก: Protocol Buffers
วันแรกที่ต้องมาเรียนเขียน .proto files รู้สึกเหมือนกลับไปเรียนภาษาใหม่:
// user.proto - ไฟล์แรกของผม (ดู basic มาก)
syntax = "proto3";
package user;
service UserService {
rpc GetUser (GetUserRequest) returns (User);
rpc CreateUser (CreateUserRequest) returns (User);
}
message User {
int32 id = 1;
string name = 2;
string email = 3;
}
message GetUserRequest {
int32 id = 1;
}
message CreateUserRequest {
string name = 1;
string email = 2;
}
ข้อสงสัยแรกๆ:
- ทำไมต้องใส่
= 1, = 2, = 3 - syntax = “proto3” คืออะไร
- package กับ namespace ต่างกันยังไง
เรียนรู้ว่าตัวเลขพวกนั้นคือ field tags สำคัญมากในการทำ backward compatibility
เคสจริง: การ Migrate จาก REST
มีระบบ user management ที่ใช้ REST API อยู่:
// REST API - เก่า
app.get('/api/users/:id', async (req, res) => {
const user = await getUserById(req.params.id);
res.json(user);
});
app.post('/api/users', async (req, res) => {
const user = await createUser(req.body);
res.status(201).json(user);
});
ปัญหาที่เจอ:
- API calls เยอะมาก ระหว่าง services
- Type safety ไม่มี (ส่งผิด format บ่อย)
- JSON serialization/deserialization ช้า
- API documentation ไม่ sync กับ code
ลอง Migrate เป็น gRPC:
// gRPC Server
const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');
const packageDefinition = protoLoader.loadSync('./protos/user.proto');
const userProto = grpc.loadPackageDefinition(packageDefinition).user;
const server = new grpc.Server();
server.addService(userProto.UserService.service, {
async getUser(call, callback) {
try {
const user = await getUserById(call.request.id);
callback(null, user);
} catch (error) {
callback({
code: grpc.status.NOT_FOUND,
details: 'User not found'
});
}
},
async createUser(call, callback) {
try {
const user = await createUser(call.request);
callback(null, user);
} catch (error) {
callback({
code: grpc.status.INVALID_ARGUMENT,
details: error.message
});
}
}
});
ปัญหาใหญ่แรก: Error Handling
การจัดการ error ใน gRPC ต่างจาก HTTP มาก:
// ❌ ความเข้าใจผิดตอนแรก
callback(new Error('Something went wrong')); // ได้ UNKNOWN error
// ✅ วิธีที่ถูกต้อง
callback({
code: grpc.status.INVALID_ARGUMENT,
details: 'Invalid input provided',
metadata: new grpc.Metadata() // สำหรับข้อมูลเพิ่มเติม
});
Status codes ที่ใช้บ่อย:
OK- สำเร็จINVALID_ARGUMENT- input ผิดNOT_FOUND- ไม่เจอข้อมูลINTERNAL- server errorUNAUTHENTICATED- ไม่ได้ loginPERMISSION_DENIED- ไม่มีสิทธิ์
ความผิดพลาดที่เจอจริง
1. Field Numbers ซ้ำกัน
// ❌ ผิดพลาดที่ทำจริง
message User {
int32 id = 1;
string name = 1; // ซ้ำกับ id!
string email = 2;
}
Error: Field number 1 has already been used
เรียนรู้ว่า: Field numbers ต้องไม่ซ้ำใน message เดียวกัน
2. Breaking Changes โดยไม่รู้ตัว
// Version 1
message User {
int32 id = 1;
string name = 2;
}
// Version 2 - Breaking change!
message User {
int32 id = 1;
int32 name = 2; // เปลี่ยนจาก string เป็น int32
}
ผลคือ: Old clients อ่านข้อมูลไม่ได้!
เรียนรู้เรื่อง Backward Compatibility:
- ห้ามเปลี่ยน field numbers
- ห้ามเปลี่ยน field types (ส่วนใหญ่)
- ใช้
reservedสำหรับ deprecated fields
message User {
reserved 2; // field number 2 ห้ามใช้แล้ว
reserved "old_name"; // field name ห้ามใช้แล้ว
int32 id = 1;
string full_name = 3; // ใช้ field number ใหม่
}
3. Streaming ที่งงงวย
แรกๆ งงมากกับ streaming:
service ChatService {
// Server streaming - server ส่งหลาย response
rpc GetMessages (GetMessagesRequest) returns (stream Message);
// Client streaming - client ส่งหลาย request
rpc SendMessages (stream Message) returns (SendMessagesResponse);
// Bidirectional streaming - สองทางพร้อมกัน
rpc Chat (stream Message) returns (stream Message);
}
Server streaming example:
// Server ส่งข้อความทีละข้อความ
getMessages(call) {
const messages = getMessagesFromDB();
messages.forEach(message => {
call.write(message);
});
call.end();
}
// Client รับทีละข้อความ
const call = client.getMessages({ roomId: 123 });
call.on('data', (message) => {
console.log('Received:', message);
});
call.on('end', () => {
console.log('Stream ended');
});
Bidirectional streaming (Chat):
// Server
chat(call) {
call.on('data', (message) => {
// broadcast ไปให้ clients อื่น
broadcastMessage(message);
// ส่งกลับไป client ที่ส่งมา
call.write({
id: generateId(),
text: `Echo: ${message.text}`,
timestamp: Date.now()
});
});
}
// Client
const call = client.chat();
// ส่งข้อความ
call.write({ text: 'Hello from client' });
// รับข้อความ
call.on('data', (message) => {
console.log('Received:', message.text);
});
Performance Gains ที่ได้จริง
หลังจาก migrate จาก REST เป็น gRPC:
Before (REST API):
Requests: 1000/sec
Average latency: 85ms
P95 latency: 150ms
CPU usage: 65%
Memory usage: 1.2GB
After (gRPC):
Requests: 1000/sec
Average latency: 12ms
P95 latency: 25ms
CPU usage: 35%
Memory usage: 800MB
สาเหตุ:
- Binary serialization เร็วกว่า JSON
- HTTP/2 multiplexing
- Connection reuse
- Smaller payload size
Debugging gRPC ที่ปวดหัว
1. ดูได้ยากกว่า REST
# REST - ง่าย
curl -X GET "http://api.example.com/users/123"
# gRPC - ต้องใช้เครื่องมือพิเศษ
grpcurl -plaintext localhost:50051 user.UserService/GetUser -d '{"id": 123}'
2. Browser ดูไม่ได้ตรงๆ
ต้องใช้ tools:
- grpcurl - command line
- BloomRPC/Kreya - GUI client
- Postman - รองรับ gRPC แล้ว
- gRPC-Web - สำหรับ browser
3. Load Balancing ที่ซับซ้อน
// Client-side load balancing
const client = new userProto.UserService(
'dns:///user-service:50051', // DNS resolver
grpc.credentials.createInsecure(),
{
'grpc.lb_policy_name': 'round_robin',
'grpc.keepalive_time_ms': 30000,
'grpc.keepalive_timeout_ms': 5000,
'grpc.keepalive_permit_without_calls': true
}
);
Advanced Features ที่เรียนรู้
1. Interceptors (Middleware)
// Server interceptor - สำหรับ authentication
const authInterceptor = (call, metadata, next) => {
const token = metadata.get('authorization')[0];
if (!verifyToken(token)) {
return next({
code: grpc.status.UNAUTHENTICATED,
details: 'Invalid token'
});
}
call.metadata.set('user_id', getUserIdFromToken(token));
next();
};
server.use(authInterceptor);
// Client interceptor - สำหรับ retry logic
const retryInterceptor = (options, nextCall) => {
return new grpc.InterceptingCall(nextCall(options), {
start: function(metadata, listener, next) {
let retryCount = 0;
const maxRetries = 3;
const retryListener = {
onReceiveStatus: function(status, next) {
if (status.code !== grpc.status.OK && retryCount < maxRetries) {
retryCount++;
// Exponential backoff
setTimeout(() => {
this.retry();
}, Math.pow(2, retryCount) * 1000);
} else {
next(status);
}
}
};
next(metadata, retryListener);
}
});
};
2. Health Checking
// health.proto (standard gRPC health checking)
service Health {
rpc Check(HealthCheckRequest) returns (HealthCheckResponse);
rpc Watch(HealthCheckRequest) returns (stream HealthCheckResponse);
}
message HealthCheckRequest {
string service = 1;
}
message HealthCheckResponse {
enum ServingStatus {
UNKNOWN = 0;
SERVING = 1;
NOT_SERVING = 2;
}
ServingStatus status = 1;
}
// Health check implementation
server.addService(healthProto.Health.service, {
check: (call, callback) => {
callback(null, {
status: healthProto.HealthCheckResponse.ServingStatus.SERVING
});
},
watch: (call) => {
// Send periodic health updates
setInterval(() => {
call.write({
status: healthProto.HealthCheckResponse.ServingStatus.SERVING
});
}, 5000);
}
});
3. Reflection API
// Enable server reflection
const reflection = require('@grpc/reflection');
reflection.addReflectionToServer(server);
ได้อะไร:
- Tools สามารถ discover services ได้อัตโนมัติ
- ไม่ต้องแชร์ .proto files
- grpcurl ใช้ได้โดยไม่ต้องระบุ proto
gRPC กับ REST: เมื่อไหร่ใช้อะไร
หลังจากใช้ gRPC มาสักพัก เรียนรู้ว่า ไม่ใช่ silver bullet
ใช้ gRPC เมื่อ:
- Internal microservices ที่ติดต่อกันบ่อย
- Performance critical applications
- Strongly typed contracts ต้องการ
- Streaming data เยอะ
- Multiple languages ใน ecosystem
ใช้ REST เมื่อ:
- Public APIs สำหรับ third party
- Browser applications (ยังไงต้องใช้ gRPC-Web)
- Simple CRUD operations
- Caching ต้องการใช้ HTTP cache
- Legacy systems integration
Production Lessons Learned
1. Connection Management
// ❌ สร้าง client ใหม่ทุกครั้ง (ช้า)
function callUserService(id) {
const client = new userProto.UserService(
'localhost:50051',
grpc.credentials.createInsecure()
);
return new Promise((resolve, reject) => {
client.getUser({ id }, (err, user) => {
if (err) reject(err);
else resolve(user);
});
});
}
// ✅ Reuse connection (เร็ว)
const userClient = new userProto.UserService(
'localhost:50051',
grpc.credentials.createInsecure()
);
function callUserService(id) {
return new Promise((resolve, reject) => {
userClient.getUser({ id }, (err, user) => {
if (err) reject(err);
else resolve(user);
});
});
}
2. Error Handling ระหว่าง Services
// Service A เรียก Service B
async function getUserWithProfile(userId) {
try {
// Get user basic info
const user = await userService.getUser({ id: userId });
// Get user profile (อาจจะ fail)
try {
const profile = await profileService.getProfile({ userId });
return { ...user, profile };
} catch (profileError) {
if (profileError.code === grpc.status.NOT_FOUND) {
// Profile ไม่มีก็ไม่เป็นไร return user อย่างเดียว
return user;
}
throw profileError; // Error อื่นๆ ให้ fail
}
} catch (userError) {
// User ไม่มีถือว่า critical error
throw userError;
}
}
3. Monitoring & Observability
// Prometheus metrics
const promClient = require('prom-client');
const grpcRequestsTotal = new promClient.Counter({
name: 'grpc_requests_total',
help: 'Total gRPC requests',
labelNames: ['method', 'status_code']
});
const grpcDuration = new promClient.Histogram({
name: 'grpc_duration_seconds',
help: 'gRPC request duration',
labelNames: ['method']
});
// Interceptor สำหรับเก็บ metrics
const metricsInterceptor = (call, metadata, next) => {
const start = Date.now();
const method = call.methodName;
next((err, result) => {
const duration = (Date.now() - start) / 1000;
const statusCode = err ? err.code : 'OK';
grpcRequestsTotal.inc({ method, status_code: statusCode });
grpcDuration.observe({ method }, duration);
});
};
เครื่องมือที่ช่วยให้ชีวิต gRPC ง่ายขึ้น
1. Code Generation
# Generate JavaScript code จาก proto
protoc --js_out=import_style=commonjs:. \
--grpc_out=grpc_js:. \
user.proto
# หรือใช้ buf (modern tool)
buf generate
2. Testing
// Jest + gRPC testing
describe('UserService', () => {
let server;
let client;
beforeAll(async () => {
server = new grpc.Server();
server.addService(userProto.UserService.service, userServiceImpl);
const port = await new Promise((resolve) => {
server.bindAsync('localhost:0', grpc.ServerCredentials.createInsecure(), (err, port) => {
resolve(port);
});
});
server.start();
client = new userProto.UserService(
`localhost:${port}`,
grpc.credentials.createInsecure()
);
});
afterAll(() => {
server.tryShutdown(() => {});
});
test('should get user by id', async () => {
const response = await new Promise((resolve, reject) => {
client.getUser({ id: 1 }, (err, user) => {
if (err) reject(err);
else resolve(user);
});
});
expect(response.id).toBe(1);
expect(response.name).toBe('John Doe');
});
});
3. API Documentation
# buf.gen.yaml - generate docs
version: v1
plugins:
- plugin: buf.build/protocolbuffers/js
out: gen/js
- plugin: buf.build/grpc/grpc-web
out: gen/grpc-web
- plugin: buf.build/bufbuild/connect-web
out: gen/connect-web
สรุป: gRPC ที่รักแล้วเกลียด แล้วรักอีกครั้ง
ช่วงแรก (รัก): เห็น performance benefits เจ๋งๆ
ช่วงกลาง (เกลียด): Debug ยาก, tooling ไม่ครบ, learning curve สูง
ตอนนี้ (รักอีกครั้ง): เข้าใจ trade-offs และรู้ว่าเมื่อไหร่ควรใช้
ข้อดีที่ได้จริง:
- Performance ดีกว่า REST มาก (latency ลด 70%)
- Type safety ลด bugs จาก API contract
- Streaming ทำ real-time features ได้ง่าย
- Multi-language support ดีเยี่ยม
ข้อเสียที่ต้องยอมรับ:
- Learning curve สูงกว่า REST
- Debugging ยากกว่า (แต่มี tools ช่วยแล้ว)
- Browser support ต้องใช้ gRPC-Web
- Caching ซับซ้อนกว่า HTTP caching
คำแนะนำ:
- เริ่มจาก internal services ก่อน
- เรียนรู้ Protocol Buffers ให้เข้าใจ
- ตั้ง monitoring ตั้งแต่แรก
- มี fallback plan ไว้ (circuit breaker)
- Test thoroughly โดยเฉพาะ error scenarios
gRPC มันเหมือน รถสปอร์ตคันแรง - powerful มาก แต่ต้องรู้วิธีขับ 🏎️
ถ้าใช้เป็น มันจะทำให้ระบบเร็วขึ้นมาก แต่ถ้าใช้ผิด อาจจะชนกำแพงได้ 😅