ในยุคที่ Microservices กลายเป็นสถาปัตยกรรมมาตรฐานของซอฟต์แวร์สมัยใหม่ การสื่อสารระหว่าง Service ต่างๆ เป็นสิ่งที่สำคัญมาก REST API ที่ใช้ JSON เป็นที่นิยมมานาน แต่เมื่อระบบมีขนาดใหญ่ขึ้น ปัญหาเรื่องประสิทธิภาพก็ตามมา นั่นคือจุดที่ gRPC เข้ามาแก้ปัญหา
gRPC เป็น Framework สำหรับ Remote Procedure Call ที่พัฒนาโดย Google ใช้ HTTP/2 เป็น Transport Layer และ Protocol Buffers เป็นรูปแบบการ Serialize ข้อมูล ทำให้การสื่อสารระหว่าง Service เร็วกว่า REST แบบดั้งเดิมหลายเท่า ในบทความนี้เราจะเรียนรู้ gRPC ตั้งแต่แนวคิดพื้นฐานจนถึงการนำไปใช้งานจริงในระบบ Microservices
gRPC คืออะไร?
gRPC ย่อมาจาก gRPC Remote Procedure Call (ตัว g เปลี่ยนความหมายทุกเวอร์ชัน เช่น Google, Good, Green) เป็น Open Source RPC Framework ที่ Google เปิดตัวในปี 2015 หลังจากใช้ระบบ RPC ภายในชื่อ Stubby มานานกว่า 15 ปี
แนวคิดของ RPC คือการทำให้การเรียกใช้ฟังก์ชันบนเครื่องอื่น (Remote) เหมือนกับการเรียกฟังก์ชันในเครื่องของเราเอง (Local) โดย gRPC จัดการเรื่อง Network Communication, Serialization และ Deserialization ให้ทั้งหมด นักพัฒนาแค่กำหนด Interface ด้วยไฟล์ .proto แล้ว gRPC จะ Generate Code สำหรับทั้ง Client และ Server ให้อัตโนมัติ
คุณสมบัติหลักของ gRPC มีดังนี้:
- ใช้ HTTP/2 — รองรับ Multiplexing, Header Compression, Server Push ทำให้เร็วกว่า HTTP/1.1 มาก
- Protocol Buffers — Binary Serialization ที่เล็กกว่า JSON 3-10 เท่า และ Parse เร็วกว่า 20-100 เท่า
- Strongly Typed — มี Type Safety จาก .proto file ป้องกันข้อผิดพลาดตั้งแต่ Compile Time
- Code Generation — Generate Client/Server Code ได้มากกว่า 12 ภาษา
- Bidirectional Streaming — ส่งข้อมูลแบบ Stream ได้ทั้งสองทาง
- Deadline/Timeout — มีระบบ Timeout ในตัว ป้องกัน Cascading Failure
- Pluggable — รองรับ Authentication, Load Balancing, Logging แบบ Plugin ได้
ทำไมต้อง gRPC? ข้อจำกัดของ REST
REST API ที่ใช้ JSON over HTTP/1.1 มีข้อจำกัดหลายประการเมื่อใช้ในระบบ Microservices ขนาดใหญ่:
ปัญหาของ REST ในระบบ Microservices:
- JSON มีขนาดใหญ่ — Text-based format ใช้ Bandwidth มากกว่า Binary format อย่าง Protobuf หลายเท่า
- Parsing ช้า — การ Parse JSON ต้องใช้ CPU มากกว่า Binary deserialization
- ไม่มี Type Safety — JSON ไม่มี Schema ที่บังคับ ทำให้เกิด Bug ง่าย
- HTTP/1.1 ไม่รองรับ Multiplexing — หนึ่ง Connection ส่งได้ทีละ Request
- ไม่มี Streaming ในตัว — ต้องใช้ WebSocket หรือ SSE แยกต่างหาก
- ต้องเขียน Client Code เอง — ไม่มี Code Generation อัตโนมัติ
gRPC แก้ปัญหาเหล่านี้ทั้งหมด ด้วย HTTP/2 + Protobuf + Code Generation ทำให้การสื่อสารระหว่าง Service เร็วขึ้น เล็กลง และปลอดภัยจาก Bug มากขึ้น
HTTP/2 — Transport Layer ของ gRPC
gRPC ใช้ HTTP/2 เป็น Transport Protocol ซึ่งมีข้อดีเหนือกว่า HTTP/1.1 อย่างมากมาย HTTP/2 ถูกพัฒนาจาก SPDY ของ Google และกลายเป็นมาตรฐานในปี 2015 คุณสมบัติหลักของ HTTP/2 ที่ gRPC ใช้ประโยชน์ได้แก่:
Multiplexing: HTTP/2 สามารถส่งหลาย Request/Response พร้อมกันผ่าน Connection เดียว ไม่ต้องรอ Response ก่อนถึงจะส่ง Request ถัดไป เรื่อง Head-of-Line Blocking ที่เป็นปัญหาของ HTTP/1.1 จึงหมดไป
Binary Framing: HTTP/2 ส่งข้อมูลเป็น Binary Frame แทน Text ทำให้ Parse ได้เร็วกว่าและใช้ Bandwidth น้อยกว่า
Header Compression (HPACK): HTTP/2 บีบอัด Header ด้วย HPACK Algorithm ลดขนาด Header ที่ส่งซ้ำๆ ได้มาก โดยเฉพาะ Cookie และ Header มาตรฐานที่มีขนาดใหญ่
Server Push: Server สามารถส่งข้อมูลไปยัง Client ก่อนที่ Client จะร้องขอ ซึ่ง gRPC ใช้ความสามารถนี้ในการทำ Server Streaming
Flow Control: HTTP/2 มี Flow Control ในระดับ Stream ทำให้ควบคุมอัตราการส่งข้อมูลได้ละเอียด ป้องกัน Receiver ถูก Overwhelm
Protocol Buffers (Protobuf) — การ Serialize ข้อมูล
Protocol Buffers (หรือ Protobuf) คือ Language-neutral, Platform-neutral Mechanism สำหรับ Serializing Structured Data ที่พัฒนาโดย Google เป็น Binary Format ที่เล็กกว่า JSON 3-10 เท่า และ Serialize/Deserialize ได้เร็วกว่า JSON 20-100 เท่า
ไฟล์ .proto — กำหนด Schema
ทุกอย่างใน gRPC เริ่มต้นที่ไฟล์ .proto ซึ่งเป็น Interface Definition Language (IDL) ที่กำหนดโครงสร้างข้อมูลและ Service ที่ต้องการ
// user.proto
syntax = "proto3";
package user;
option go_package = "github.com/myapp/proto/user";
// Message — กำหนดโครงสร้างข้อมูล
message User {
int32 id = 1; // Field number 1
string name = 2; // Field number 2
string email = 3; // Field number 3
UserRole role = 4; // Enum type
repeated string tags = 5; // Array of strings
Address address = 6; // Nested message
// Oneof — เลือกได้แค่ field เดียว
oneof contact {
string phone = 7;
string line_id = 8;
}
// Map type
map<string, string> metadata = 9;
}
// Enum
enum UserRole {
USER_ROLE_UNSPECIFIED = 0;
USER_ROLE_ADMIN = 1;
USER_ROLE_EDITOR = 2;
USER_ROLE_VIEWER = 3;
}
// Nested Message
message Address {
string street = 1;
string city = 2;
string province = 3;
string zip_code = 4;
}
// Timestamp ใช้ Well-Known Types
import "google/protobuf/timestamp.proto";
message UserWithTimestamp {
int32 id = 1;
string name = 2;
google.protobuf.Timestamp created_at = 3;
google.protobuf.Timestamp updated_at = 4;
}
กฎสำคัญของ Protobuf
- Field Number 1-15 ใช้ 1 byte encode — ใส่ field ที่ใช้บ่อยไว้ในช่วงนี้
- Field Number 16-2047 ใช้ 2 bytes encode
- ห้ามลบ Field — ใช้
reservedแทน - Default Values — int = 0, string = "", bool = false, enum = ค่าแรก (0)
- Repeated = Array/List, Map = Key-Value pairs
// การใช้ reserved เมื่อต้องการลบ field
message User {
reserved 6, 8; // field numbers ที่ลบไปแล้ว
reserved "old_field", "deprecated_field"; // ชื่อ field ที่ลบ
int32 id = 1;
string name = 2;
string email = 3;
}
Code Generation ด้วย protoc
หลังจากเขียนไฟล์ .proto แล้ว ใช้ protoc (Protocol Buffer Compiler) เพื่อ Generate Code สำหรับภาษาที่ต้องการ
# ติดตั้ง protoc
# macOS
brew install protobuf
# Ubuntu
sudo apt install -y protobuf-compiler
# ตรวจสอบเวอร์ชัน
protoc --version
# Generate Go code
protoc --go_out=. --go-grpc_out=. proto/user.proto
# Generate Python code
pip install grpcio grpcio-tools
python -m grpc_tools.protoc -I. --python_out=. --grpc_python_out=. proto/user.proto
# Generate Node.js code (static)
npm install @grpc/proto-loader grpc
npx grpc_tools_node_protoc --js_out=import_style=commonjs,binary:. \
--grpc_out=grpc_js:. proto/user.proto
# Generate TypeScript
npm install ts-proto
protoc --plugin=./node_modules/.bin/protoc-gen-ts_proto \
--ts_proto_out=. proto/user.proto
gRPC Service Definition
หลังจากกำหนด Message แล้ว ขั้นตอนต่อไปคือกำหนด Service ซึ่งเป็น Interface ที่กำหนดว่ามี Method อะไรบ้างที่ Client สามารถเรียกใช้ได้
// service.proto
syntax = "proto3";
package userservice;
import "user.proto";
service UserService {
// 1. Unary RPC — Request เดียว Response เดียว
rpc GetUser(GetUserRequest) returns (GetUserResponse);
rpc CreateUser(CreateUserRequest) returns (CreateUserResponse);
rpc UpdateUser(UpdateUserRequest) returns (UpdateUserResponse);
rpc DeleteUser(DeleteUserRequest) returns (DeleteUserResponse);
// 2. Server Streaming — Client ส่ง Request เดียว Server ส่ง Response หลายตัว
rpc ListUsers(ListUsersRequest) returns (stream User);
// 3. Client Streaming — Client ส่ง Request หลายตัว Server ตอบ Response เดียว
rpc UploadUsers(stream User) returns (UploadUsersResponse);
// 4. Bidirectional Streaming — ทั้ง Client และ Server ส่ง Stream ได้
rpc SyncUsers(stream UserEvent) returns (stream UserEvent);
}
message GetUserRequest {
int32 id = 1;
}
message GetUserResponse {
User user = 1;
}
message CreateUserRequest {
string name = 1;
string email = 2;
UserRole role = 3;
}
message CreateUserResponse {
User user = 1;
}
message ListUsersRequest {
int32 page_size = 1;
string page_token = 2;
string filter = 3;
}
message UploadUsersResponse {
int32 count = 1;
}
message UserEvent {
string event_type = 1;
User user = 2;
}
4 ประเภทของ gRPC Service
gRPC รองรับ 4 รูปแบบการสื่อสาร แต่ละแบบเหมาะกับ Use Case ที่แตกต่างกัน:
1. Unary RPC
เป็นรูปแบบที่ง่ายที่สุด เหมือน REST ปกติ — Client ส่ง Request หนึ่งตัว Server ตอบ Response หนึ่งตัว เหมาะกับการดึงข้อมูล, สร้างข้อมูล, อัปเดตข้อมูลทั่วไป
2. Server Streaming RPC
Client ส่ง Request หนึ่งตัว Server ส่ง Response กลับมาเป็น Stream (หลายตัวต่อเนื่อง) เหมาะกับการดึงข้อมูลจำนวนมาก เช่น ดึงรายการสินค้า 10,000 รายการ, Real-time Feed, การส่ง Log หรือ Events จาก Server
3. Client Streaming RPC
Client ส่ง Request หลายตัวเป็น Stream Server ตอบ Response หนึ่งตัวหลังจากรับข้อมูลครบ เหมาะกับการ Upload ไฟล์ขนาดใหญ่, Batch Processing, การส่งข้อมูล Sensor หลายตัว
4. Bidirectional Streaming RPC
ทั้ง Client และ Server ส่ง Stream ได้พร้อมกัน แต่ละฝั่งอ่านและเขียนอิสระจากกัน เหมาะกับ Chat Application, Real-time Collaboration, Game State Synchronization, Interactive Data Processing
Implementing gRPC Server (Go)
ตัวอย่างการสร้าง gRPC Server ด้วยภาษา Go ซึ่งเป็นภาษาที่นิยมใช้กับ gRPC มากที่สุดเพราะ Go เป็น First-class Citizen ของ gRPC
// server/main.go
package main
import (
"context"
"fmt"
"log"
"net"
"sync"
pb "github.com/myapp/proto/user"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
type userServer struct {
pb.UnimplementedUserServiceServer
mu sync.RWMutex
users map[int32]*pb.User
nextID int32
}
func NewUserServer() *userServer {
return &userServer{
users: make(map[int32]*pb.User),
nextID: 1,
}
}
// Unary RPC
func (s *userServer) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.GetUserResponse, error) {
s.mu.RLock()
defer s.mu.RUnlock()
user, ok := s.users[req.Id]
if !ok {
return nil, status.Errorf(codes.NotFound, "user %d not found", req.Id)
}
return &pb.GetUserResponse{User: user}, nil
}
// Unary RPC
func (s *userServer) CreateUser(ctx context.Context, req *pb.CreateUserRequest) (*pb.CreateUserResponse, error) {
s.mu.Lock()
defer s.mu.Unlock()
user := &pb.User{
Id: s.nextID,
Name: req.Name,
Email: req.Email,
Role: req.Role,
}
s.users[s.nextID] = user
s.nextID++
return &pb.CreateUserResponse{User: user}, nil
}
// Server Streaming RPC
func (s *userServer) ListUsers(req *pb.ListUsersRequest, stream pb.UserService_ListUsersServer) error {
s.mu.RLock()
defer s.mu.RUnlock()
for _, user := range s.users {
if err := stream.Send(user); err != nil {
return err
}
}
return nil
}
func main() {
lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
grpcServer := grpc.NewServer()
pb.RegisterUserServiceServer(grpcServer, NewUserServer())
fmt.Println("gRPC server listening on :50051")
if err := grpcServer.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
Implementing gRPC Server (Python)
# server.py
import grpc
from concurrent import futures
import user_pb2
import user_pb2_grpc
class UserService(user_pb2_grpc.UserServiceServicer):
def __init__(self):
self.users = {}
self.next_id = 1
def GetUser(self, request, context):
user = self.users.get(request.id)
if not user:
context.set_code(grpc.StatusCode.NOT_FOUND)
context.set_details(f"User {request.id} not found")
return user_pb2.GetUserResponse()
return user_pb2.GetUserResponse(user=user)
def CreateUser(self, request, context):
user = user_pb2.User(
id=self.next_id,
name=request.name,
email=request.email,
role=request.role,
)
self.users[self.next_id] = user
self.next_id += 1
return user_pb2.CreateUserResponse(user=user)
def ListUsers(self, request, context):
"""Server Streaming: ส่ง User ทีละตัว"""
for user in self.users.values():
yield user
def serve():
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
user_pb2_grpc.add_UserServiceServicer_to_server(
UserService(), server
)
server.add_insecure_port("[::]:50051")
server.start()
print("gRPC server listening on :50051")
server.wait_for_termination()
if __name__ == "__main__":
serve()
Implementing gRPC Server (Node.js)
// server.js
const grpc = require("@grpc/grpc-js");
const protoLoader = require("@grpc/proto-loader");
const packageDef = protoLoader.loadSync("proto/user.proto", {
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true,
});
const proto = grpc.loadPackageDefinition(packageDef).userservice;
const users = new Map();
let nextId = 1;
const server = new grpc.Server();
server.addService(proto.UserService.service, {
GetUser: (call, callback) => {
const user = users.get(call.request.id);
if (!user) {
return callback({
code: grpc.status.NOT_FOUND,
details: `User ${call.request.id} not found`,
});
}
callback(null, { user });
},
CreateUser: (call, callback) => {
const user = {
id: nextId++,
name: call.request.name,
email: call.request.email,
role: call.request.role,
};
users.set(user.id, user);
callback(null, { user });
},
ListUsers: (call) => {
// Server Streaming
for (const user of users.values()) {
call.write(user);
}
call.end();
},
});
server.bindAsync(
"0.0.0.0:50051",
grpc.ServerCredentials.createInsecure(),
() => {
console.log("gRPC server on :50051");
}
);
gRPC Client
หลังจากมี Server แล้ว การสร้าง Client ก็ง่ายมาก เพราะ gRPC Generate Code ให้เราอัตโนมัติจาก .proto file
# Python Client
import grpc
import user_pb2
import user_pb2_grpc
# สร้าง Channel
channel = grpc.insecure_channel("localhost:50051")
stub = user_pb2_grpc.UserServiceStub(channel)
# Unary RPC — สร้าง User
response = stub.CreateUser(user_pb2.CreateUserRequest(
name="สมชาย",
email="somchai@example.com",
role=user_pb2.USER_ROLE_ADMIN,
))
print(f"Created user: {response.user.id}")
# Unary RPC — ดึง User
response = stub.GetUser(user_pb2.GetUserRequest(id=1))
print(f"Got user: {response.user.name}")
# Server Streaming — List Users
for user in stub.ListUsers(user_pb2.ListUsersRequest(page_size=10)):
print(f"User: {user.name} ({user.email})")
# Client Streaming
def generate_users():
for i in range(100):
yield user_pb2.User(
name=f"User {i}",
email=f"user{i}@example.com",
)
response = stub.UploadUsers(generate_users())
print(f"Uploaded {response.count} users")
# Bidirectional Streaming
def event_stream():
for i in range(10):
yield user_pb2.UserEvent(
event_type="sync",
user=user_pb2.User(name=f"User {i}"),
)
for event in stub.SyncUsers(event_stream()):
print(f"Received event: {event.event_type}")
Error Handling — Status Codes
gRPC มีระบบ Error Handling ที่เป็นมาตรฐาน โดยใช้ Status Code ที่กำหนดไว้ชัดเจน ต่างจาก REST ที่ใช้ HTTP Status Code ซึ่งบางครั้งไม่ตรงกับสถานการณ์จริง
| gRPC Code | HTTP เทียบเท่า | ความหมาย |
|---|---|---|
| OK (0) | 200 | สำเร็จ |
| CANCELLED (1) | 499 | Client ยกเลิก |
| UNKNOWN (2) | 500 | ไม่ทราบสาเหตุ |
| INVALID_ARGUMENT (3) | 400 | Parameter ไม่ถูกต้อง |
| DEADLINE_EXCEEDED (4) | 504 | หมดเวลา |
| NOT_FOUND (5) | 404 | ไม่พบข้อมูล |
| ALREADY_EXISTS (6) | 409 | ข้อมูลซ้ำ |
| PERMISSION_DENIED (7) | 403 | ไม่มีสิทธิ์ |
| RESOURCE_EXHAUSTED (8) | 429 | ทรัพยากรหมด (Rate Limit) |
| UNIMPLEMENTED (12) | 501 | ยังไม่ได้ Implement |
| INTERNAL (13) | 500 | Server Error |
| UNAVAILABLE (14) | 503 | Service ไม่พร้อม |
| UNAUTHENTICATED (16) | 401 | ยังไม่ได้ Login |
# Python — Return Error with Details
from google.rpc import error_details_pb2
from grpc_status import rpc_status
def GetUser(self, request, context):
if request.id <= 0:
context.abort(
grpc.StatusCode.INVALID_ARGUMENT,
"User ID must be positive"
)
user = self.users.get(request.id)
if not user:
context.abort(
grpc.StatusCode.NOT_FOUND,
f"User {request.id} not found"
)
return user_pb2.GetUserResponse(user=user)
Metadata และ Interceptors
Metadata ใน gRPC เทียบเท่ากับ HTTP Headers ใช้ส่งข้อมูลเพิ่มเติมเช่น Authentication Token, Request ID, Tracing Information
# Python — ส่ง Metadata จาก Client
metadata = [
("authorization", "Bearer eyJhbGciOiJIUzI1NiIs..."),
("x-request-id", "req-12345"),
("x-client-version", "1.0.0"),
]
response = stub.GetUser(
user_pb2.GetUserRequest(id=1),
metadata=metadata,
)
# Python — อ่าน Metadata ใน Server
def GetUser(self, request, context):
metadata = dict(context.invocation_metadata())
token = metadata.get("authorization", "")
request_id = metadata.get("x-request-id", "unknown")
print(f"Request {request_id} with token: {token[:20]}...")
# ... process request
Interceptors — Middleware สำหรับ gRPC
Interceptors เป็น Middleware ที่ทำงานก่อนและหลัง RPC Call ใช้สำหรับ Logging, Authentication, Metrics, Rate Limiting ได้โดยไม่ต้องแก้ Business Logic
# Python Server Interceptor
class AuthInterceptor(grpc.ServerInterceptor):
def intercept_service(self, continuation, handler_call_details):
metadata = dict(handler_call_details.invocation_metadata)
token = metadata.get("authorization", "")
if not token.startswith("Bearer "):
return grpc.unary_unary_rpc_method_handler(
lambda req, ctx: ctx.abort(
grpc.StatusCode.UNAUTHENTICATED,
"Missing or invalid token"
)
)
# Validate JWT token here
return continuation(handler_call_details)
class LoggingInterceptor(grpc.ServerInterceptor):
def intercept_service(self, continuation, handler_call_details):
method = handler_call_details.method
print(f"[gRPC] {method} called")
return continuation(handler_call_details)
# ใช้ Interceptor
server = grpc.server(
futures.ThreadPoolExecutor(max_workers=10),
interceptors=[AuthInterceptor(), LoggingInterceptor()],
)
Authentication — TLS และ JWT
ในระบบ Production gRPC ต้องมี Authentication ที่เหมาะสม gRPC รองรับหลายวิธี ได้แก่ TLS/mTLS สำหรับ Transport Security และ Token-based (JWT) สำหรับ Application-level Authentication
# Python — gRPC with TLS
# Server
server_credentials = grpc.ssl_server_credentials(
[(private_key, certificate_chain)],
root_certificates=ca_cert,
require_client_auth=True, # mTLS
)
server.add_secure_port("[::]:50051", server_credentials)
# Client
channel_credentials = grpc.ssl_channel_credentials(
root_certificates=ca_cert,
private_key=client_key,
certificate_chain=client_cert,
)
channel = grpc.secure_channel("server:50051", channel_credentials)
# JWT Token — ใช้ Call Credentials
class JWTPlugin(grpc.AuthMetadataPlugin):
def __init__(self, token):
self.token = token
def __call__(self, context, callback):
callback([("authorization", f"Bearer {self.token}")], None)
call_creds = grpc.metadata_call_credentials(JWTPlugin(jwt_token))
composite_creds = grpc.composite_channel_credentials(
channel_credentials, call_creds
)
channel = grpc.secure_channel("server:50051", composite_creds)
gRPC-Web สำหรับ Browser
Browser ไม่สามารถเรียก gRPC โดยตรงได้ เพราะ Browser ไม่รองรับ HTTP/2 Trailers ที่ gRPC ต้องใช้ gRPC-Web จึงถูกสร้างขึ้นเป็น Bridge ระหว่าง Browser กับ gRPC Backend
วิธีใช้ gRPC-Web มี 2 แบบหลัก คือใช้ Envoy Proxy เป็น Reverse Proxy ที่แปลง gRPC-Web เป็น gRPC ปกติ หรือใช้ grpc-web npm package ร่วมกับ gRPC Server ที่รองรับ gRPC-Web โดยตรง
# Envoy Proxy Config สำหรับ gRPC-Web
# envoy.yaml
static_resources:
listeners:
- name: listener_0
address:
socket_address: { address: 0.0.0.0, port_value: 8080 }
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters...
http_filters:
- name: envoy.filters.http.grpc_web
- name: envoy.filters.http.cors
- name: envoy.filters.http.router
route_config:
virtual_hosts:
- name: local_service
domains: ["*"]
routes:
- match: { prefix: "/" }
route: { cluster: grpc_service }
cors:
allow_origin_string_match:
- prefix: "*"
allow_methods: GET, PUT, DELETE, POST, OPTIONS
allow_headers: "*"
expose_headers: grpc-status, grpc-message
clusters:
- name: grpc_service
connect_timeout: 0.25s
type: LOGICAL_DNS
lb_policy: ROUND_ROBIN
typed_extension_protocol_options:
envoy.extensions.upstreams.http.v3.HttpProtocolOptions:
"@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions
explicit_http_config:
http2_protocol_options: {}
load_assignment:
cluster_name: grpc_service
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address: { address: grpc-server, port_value: 50051 }
gRPC vs REST — เปรียบเทียบละเอียด
| คุณสมบัติ | gRPC | REST |
|---|---|---|
| Protocol | HTTP/2 | HTTP/1.1 (ส่วนใหญ่) |
| Data Format | Protocol Buffers (Binary) | JSON (Text) |
| Type Safety | Strong (จาก .proto) | Weak (ไม่บังคับ) |
| Code Generation | อัตโนมัติ 12+ ภาษา | ต้องทำเอง (OpenAPI) |
| Streaming | รองรับ 4 แบบ | ต้องใช้ WebSocket/SSE |
| Performance | เร็วกว่า 2-10x | ช้ากว่า |
| Payload Size | เล็กกว่า 3-10x | ใหญ่กว่า |
| Browser Support | ต้องใช้ gRPC-Web + Proxy | รองรับโดยตรง |
| Human Readable | ไม่ (Binary) | ใช่ (JSON) |
| Tooling | grpcurl, BloomRPC | curl, Postman |
| Caching | ไม่มีในตัว | HTTP Caching มาตรฐาน |
| Learning Curve | สูงกว่า | ต่ำกว่า |
gRPC vs GraphQL
GraphQL เป็นอีกทางเลือกหนึ่งนอกจาก REST แต่มีจุดประสงค์ต่างจาก gRPC โดย GraphQL เน้นการแก้ปัญหา Over-fetching และ Under-fetching ของ REST สำหรับ Frontend ขณะที่ gRPC เน้นประสิทธิภาพการสื่อสารระหว่าง Backend Services
| คุณสมบัติ | gRPC | GraphQL |
|---|---|---|
| เหมาะกับ | Service-to-Service | Client-to-Server (Frontend) |
| Flexibility | Fixed Schema | Flexible Query |
| Performance | สูงมาก | ปานกลาง |
| Real-time | Streaming | Subscriptions |
| Ecosystem | Backend-focused | Full-stack |
Load Balancing gRPC
การทำ Load Balancing กับ gRPC ซับซ้อนกว่า REST เพราะ gRPC ใช้ HTTP/2 ซึ่งเป็น Long-lived Connection ทำให้ L4 Load Balancer (TCP level) ไม่สามารถกระจายโหลดได้ดี เนื่องจาก Connection ถูกสร้างครั้งเดียวแล้วใช้ต่อไปเรื่อยๆ ต้องใช้ L7 Load Balancer ที่เข้าใจ HTTP/2 เช่น Envoy, Nginx (1.13.10+), HAProxy (2.0+), Linkerd, Istio
นอกจากนี้ gRPC ยังรองรับ Client-side Load Balancing ผ่าน Name Resolution และ Load Balancing Policy ในตัว ทำให้ Client สามารถกระจายโหลดไปยัง Backend หลายตัวได้โดยไม่ต้องมี Proxy ตรงกลาง วิธีนี้ลด Latency เพราะไม่มี Hop เพิ่มเติม แต่ต้องจัดการ Service Discovery เอง
gRPC ใน Kubernetes
Kubernetes เป็น Platform ที่นิยมใช้ gRPC มากที่สุด แต่มีปัญหาเรื่อง Load Balancing ที่ต้องระวัง Kubernetes Service (ClusterIP) ทำ L4 Load Balancing ซึ่งกระจาย Connection ไม่ใช่ Request ทำให้ Pod ตัวหนึ่งอาจรับ Request มากกว่าตัวอื่นเพราะ Connection ถูกสร้างแค่ครั้งเดียว วิธีแก้ปัญหามีหลายทาง ดังนี้:
ใช้ Headless Service + Client-side LB: ใช้ Headless Service ของ Kubernetes (clusterIP: None) ที่คืน IP ของ Pod ทุกตัว แล้วให้ gRPC Client ทำ Round-robin เอง
ใช้ Service Mesh (Istio/Linkerd): Service Mesh จะ Inject Sidecar Proxy ที่ทำ L7 Load Balancing ให้อัตโนมัติ เป็นวิธีที่ง่ายที่สุดและนิยมใน Production
ใช้ Ingress Controller ที่รองรับ gRPC: เช่น Nginx Ingress, Traefik, Ambassador ที่ทำ L7 Load Balancing สำหรับ gRPC Traffic
Health Checking และ Reflection
gRPC มีมาตรฐาน Health Checking Protocol ที่ใช้ตรวจสอบว่า Service ยังทำงานอยู่หรือไม่ ซึ่งสำคัญมากใน Kubernetes และ Load Balancer
// health.proto (มาตรฐานจาก gRPC)
syntax = "proto3";
package grpc.health.v1;
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;
SERVICE_UNKNOWN = 3;
}
ServingStatus status = 1;
}
Reflection ช่วยให้ Client สามารถค้นหา Service และ Method ที่มีอยู่บน Server ได้โดยไม่ต้องมีไฟล์ .proto ล่วงหน้า เหมือนกับ Swagger ของ REST ช่วยให้เครื่องมืออย่าง grpcurl สามารถเรียก Service ได้ทันทีโดยไม่ต้อง Compile proto file
# Python — เปิด Reflection
from grpc_reflection.v1alpha import reflection
SERVICE_NAMES = (
user_pb2.DESCRIPTOR.services_by_name['UserService'].full_name,
reflection.SERVICE_NAME,
)
reflection.enable_server_reflection(SERVICE_NAMES, server)
Testing gRPC
การทดสอบ gRPC Service มีเครื่องมือหลายตัว ทั้ง CLI และ GUI ที่ช่วยให้ทดสอบได้ง่ายเหมือน Postman สำหรับ REST
grpcurl — Command Line
# ติดตั้ง
brew install grpcurl # macOS
go install github.com/fullstorydev/grpcurl/cmd/grpcurl@latest # Go
# List services (ต้องเปิด Reflection)
grpcurl -plaintext localhost:50051 list
# Describe service
grpcurl -plaintext localhost:50051 describe userservice.UserService
# เรียก Unary RPC
grpcurl -plaintext -d '{"id": 1}' localhost:50051 userservice.UserService/GetUser
# เรียกพร้อม Metadata
grpcurl -plaintext \
-H "authorization: Bearer eyJ..." \
-d '{"name": "test", "email": "test@test.com"}' \
localhost:50051 userservice.UserService/CreateUser
BloomRPC / Evans — GUI Tools
BloomRPC เป็น GUI Client สำหรับ gRPC เหมือน Postman ของ REST ให้ Load ไฟล์ .proto แล้วสามารถเรียก RPC ได้ทันที มี UI สวยงาม ใช้งานง่าย เหมาะกับ Developer ที่ไม่คุ้นเคยกับ CLI ส่วน Evans เป็นอีกเครื่องมือที่เป็น Interactive CLI Client ที่รองรับ Reflection ได้ดี
Unit Testing gRPC
# Python — Testing gRPC with pytest
import grpc
import grpc_testing
import user_pb2
import user_pb2_grpc
class TestUserService:
def setup_method(self):
# สร้าง In-process gRPC server สำหรับ test
self.server = grpc.server(futures.ThreadPoolExecutor(max_workers=2))
self.service = UserService()
user_pb2_grpc.add_UserServiceServicer_to_server(self.service, self.server)
port = self.server.add_insecure_port("[::]:0")
self.server.start()
channel = grpc.insecure_channel(f"localhost:{port}")
self.stub = user_pb2_grpc.UserServiceStub(channel)
def teardown_method(self):
self.server.stop(0)
def test_create_user(self):
response = self.stub.CreateUser(user_pb2.CreateUserRequest(
name="Test User",
email="test@test.com",
role=user_pb2.USER_ROLE_ADMIN,
))
assert response.user.id == 1
assert response.user.name == "Test User"
def test_get_user_not_found(self):
with pytest.raises(grpc.RpcError) as exc_info:
self.stub.GetUser(user_pb2.GetUserRequest(id=999))
assert exc_info.value.code() == grpc.StatusCode.NOT_FOUND
เมื่อไหร่ควรใช้ gRPC?
ใช้ gRPC เมื่อ:
- สื่อสารระหว่าง Microservices ภายใน (Internal Communication)
- ต้องการ Low Latency และ High Throughput
- ต้องการ Streaming (Real-time data, Large file transfer)
- ทำงานกับหลายภาษา (Polyglot Architecture)
- ต้องการ Strong Type Safety และ Contract-first Design
- ระบบที่ Mobile App สื่อสารกับ Backend (Bandwidth สำคัญ)
ไม่ควรใช้ gRPC เมื่อ:
- ต้องการให้ Browser เรียกโดยตรง (ใช้ REST หรือ GraphQL แทน)
- ต้องการ HTTP Caching
- External API ที่ให้ Third-party ใช้ (ผู้ใช้คุ้นเคยกับ REST มากกว่า)
- ระบบเล็กที่ไม่ต้องการ Performance สูง
- ต้อง Debug ง่าย (JSON อ่านง่ายกว่า Binary)
Migration จาก REST ไปเป็น gRPC
การย้ายจาก REST ไป gRPC ไม่จำเป็นต้องทำทีเดียวทั้งหมด สามารถทำแบบค่อยเป็นค่อยไปได้ ขั้นตอนแนะนำมีดังนี้:
- เริ่มจาก Internal Services ก่อน — เลือก Service ที่สื่อสารกันภายในมากที่สุดและเป็น Bottleneck ด้านประสิทธิภาพ
- สร้าง .proto file จาก REST API เดิม — แปลง JSON Schema เป็น Protobuf Message
- รัน gRPC + REST คู่กัน — ใช้ gRPC-Gateway ที่แปลง gRPC เป็น REST API อัตโนมัติ ทำให้ Client เดิมยังใช้ REST ได้
- ย้าย Client ทีละตัว — เริ่มย้าย Client ไปใช้ gRPC Client ที่ Generate จาก .proto file
- ปิด REST Endpoint เมื่อย้ายครบ — เมื่อ Client ทุกตัวย้ายไป gRPC แล้วก็ปิด REST ได้
// ใช้ gRPC-Gateway เพื่อรัน REST + gRPC คู่กัน
// service.proto — เพิ่ม HTTP annotations
import "google/api/annotations.proto";
service UserService {
rpc GetUser(GetUserRequest) returns (GetUserResponse) {
option (google.api.http) = {
get: "/api/v1/users/{id}"
};
}
rpc CreateUser(CreateUserRequest) returns (CreateUserResponse) {
option (google.api.http) = {
post: "/api/v1/users"
body: "*"
};
}
}
Best Practices สำหรับ gRPC
การใช้ gRPC ให้ได้ประสิทธิภาพสูงสุดมีแนวปฏิบัติที่ควรยึดถือ ดังนี้:
- ใช้ Deadline/Timeout เสมอ — ทุก RPC Call ต้องกำหนด Deadline เพื่อป้องกัน Request ค้างตลอดกาล
- Reuse Channel/Connection — สร้าง Channel ครั้งเดียวแล้วใช้ซ้ำ อย่าสร้างใหม่ทุก Request
- ใช้ Keepalive — ตั้ง Keepalive Ping เพื่อรักษา Connection ที่ไม่ได้ใช้งาน
- กำหนด Max Message Size — ป้องกัน Out of Memory จาก Message ขนาดใหญ่เกินไป
- เวอร์ชัน .proto file — ใช้ Package versioning เช่น api.v1, api.v2
- ใช้ Well-Known Types — ใช้ Timestamp, Duration, Wrappers แทนการสร้างเอง
- ใส่ Interceptors — สำหรับ Logging, Metrics, Authentication ส่วนกลาง
- ทำ Graceful Shutdown — รอ Request ที่กำลังประมวลผลเสร็จก่อน Shutdown
สรุป
gRPC และ Protocol Buffers เป็นเทคโนโลยีที่เปลี่ยนวิธีการสื่อสารระหว่าง Microservices อย่างมาก ด้วยประสิทธิภาพที่เหนือกว่า REST หลายเท่า Type Safety จาก .proto file และ Code Generation อัตโนมัติ ทำให้การพัฒนาระบบ Microservices ง่ายขึ้นและเร็วขึ้น
ในปี 2026 gRPC กลายเป็นมาตรฐานสำหรับ Internal Communication ใน Microservices โดยเฉพาะในระบบที่ใช้ Kubernetes และ Service Mesh บริษัทระดับโลกอย่าง Google, Netflix, Square, Dropbox ล้วนใช้ gRPC เป็นหลัก
เริ่มต้นเรียนรู้ gRPC วันนี้ เริ่มจากเขียน .proto file ง่ายๆ Generate Code แล้วลองสร้าง Server และ Client ดู เมื่อเข้าใจแนวคิดแล้ว คุณจะเห็นว่า gRPC ไม่ได้ยากอย่างที่คิด และมันจะเปลี่ยนวิธีคิดเกี่ยวกับ API Design ของคุณไปตลอด
