gRPC Deep Dive

Từ Protobuf wire format đến streaming patterns, interceptors và production deployment. gRPC = HTTP/2 transport + Protocol Buffers serialization + code generation.

Application Code (Go, Java, Python, Node.js, ...) gRPC Framework (generated stubs + runtime) HTTP/2 Transport (streams, multiplexing, HPACK) TLS 1.3 / TCP / IP Protocol Buffers (serialization / deserialization) .proto → protoc → generated message + service code encode/decode
01

gRPC là gì?

gRPC (Google Remote Procedure Call) bắt đầu từ hệ thống nội bộ "Stubby" của Google, được open-source năm 2016. Thiết kế cho microservices: strongly-typed contracts, binary wire format, streaming built-in, và code generation cho 10+ ngôn ngữ.

gRPC dùng HTTP/2 làm transport (multiplexed streams, header compression, bidirectional) và Protocol Buffers làm serialization format mặc định (có thể thay bằng JSON, Thrift, v.v.).

Tính nănggRPCREST/JSONGraphQL
SchemaBắt buộc (.proto)Tùy chọn (OpenAPI)Bắt buộc (SDL)
TransportHTTP/2HTTP/1.1 hoặc HTTP/2HTTP/1.1 hoặc HTTP/2
PayloadBinary (Protobuf)Text (JSON)Text (JSON)
Streaming✓ (4 patterns)SSE (server-only)Subscriptions (WS)
Browser supportgRPC-Web (proxy cần)✓ Native✓ Native
Code generation✓ First-classOptional (OpenAPI)Optional
Human readable✗ Binary
PerformanceTốt nhất (binary + H2)Trung bìnhTrung bình
Use cases phù hợp nhất
  • Microservices inter-service: typed contracts, low latency, code gen cho nhiều ngôn ngữ
  • Mobile clients: binary payload nhỏ hơn JSON 3-10x, tiết kiệm bandwidth
  • Real-time streaming: telemetry, log streaming, live feeds — bidirectional streaming built-in
  • Internal APIs: developer experience tốt hơn với generated stubs
02

Protocol Buffers — Proto3 Syntax

Protocol Buffers (Protobuf) là ngôn ngữ định nghĩa schema và serialization format của Google. Proto3 là phiên bản hiện đại nhất, đơn giản hơn proto2 (bỏ required fields, default values, extensions).

syntax = "proto3";

package users.v1;

option go_package = "github.com/myorg/users/v1;usersv1";

import "google/protobuf/timestamp.proto";

// Scalar types: double, float, int32, int64, uint32, uint64,
// sint32, sint64, fixed32, fixed64, sfixed32, sfixed64, bool, string, bytes

message User {
  uint64 id           = 1;   // field number 1-15 = 1-byte tag (hot fields)
  string name         = 2;
  string email        = 3;
  UserStatus status   = 4;
  repeated string tags = 5;  // repeated = array/list
  map<string, string> metadata = 6;

  oneof contact {             // only one of these can be set
    string phone = 7;
    string slack = 8;
  }

  google.protobuf.Timestamp created_at = 9;

  reserved 10, 11;            // never reuse these numbers
  reserved "old_field";       // never reuse this name
}

enum UserStatus {
  USER_STATUS_UNSPECIFIED = 0;   // proto3: enum must start with 0
  USER_STATUS_ACTIVE = 1;
  USER_STATUS_SUSPENDED = 2;
}

Field Numbers

Field numbers là duy nhất và vĩnh viễn. Chúng xác định field trong wire format — không phải field name. Điều này cho phép rename field mà không breaking.

  • 1–15: encode bằng 1 byte tag → dùng cho fields hot/phổ biến
  • 16–2047: encode bằng 2 bytes tag
  • 19000–19999: reserved for Protobuf internal
  • Không bao giờ tái sử dụng số đã xóa → dùng reserved

Default Values trong Proto3

TypeDefault value
Numeric (int, float, double)0
boolfalse
string"" (empty string)
bytesempty bytes
enumfirst value (must be 0)
messagenull / không set
Proto3 không có null

Không phân biệt được "field không set" và "field set về giá trị default (0, false, '')". Để distinguish, dùng optional keyword (proto3 optional, tạo synthetic oneof) hoặc google.protobuf.Int32Value (wrapper types).

03

Protobuf Wire Format

Protobuf serialize thành binary — compact và nhanh. Hiểu wire format giúp debug và hiểu tại sao Protobuf nhỏ hơn JSON.

Varint Encoding — số 300 (binary: 100101100) 1 010 1100 (bits 0–6 of 300) more 0 000 0010 (bits 7–13 of 300) last = 0xAC 0x02 (2 bytes vs 4 bytes fixed int32) MSB = continuation bit (1=more bytes follow, 0=last byte) · 7 bits payload per byte · little-endian group order Field Tag = (field_number << 3) | wire_type field_number=1, wire_type=2 (LEN) → tag = (1<<3)|2 = 10 = 0x0A field_number (bits 3–31+) wire_type (bits 0–2) Wire Types 0: Varint 1: 64-bit fixed 2: LEN (string/bytes/msg) 5: 32-bit fixed (wire types 3, 4 = deprecated)

Ví dụ thực tế — decode hexdump

Proto message: { name: "Alice", id: 1 }

Serialized hex: 0a 05 41 6c 69 63 65 10 01

0a        → tag: field_number=1 (name), wire_type=2 (LEN)
05        → varint: length = 5 bytes
41 6c 69 63 65  → "Alice" (UTF-8)
10        → tag: field_number=2 (id), wire_type=0 (Varint)
01        → varint: value = 1

So sánh JSON: {"name":"Alice","id":1} = 21 bytes  vs  Protobuf: 9 bytes

Varint Types

Proto typeWire typeEncodingKhi nào dùng
int32/int640 VarintVarint (negative = 10 bytes!)Numbers thường non-negative
sint32/sint640 VarintZigZag + VarintNumbers có thể negative thường xuyên
fixed32/sfixed325 32-bitLittle-endian fixedValues > 2²⁸ (fixed thường nhỏ hơn varint)
fixed64/sfixed641 64-bitLittle-endian fixedValues > 2⁵⁶
ZigZag Encoding (sint32/sint64)

Varint thông thường encode số âm rất tệ: -1 → 10 bytes (vì treated as large uint64). ZigZag map: 0→0, -1→1, 1→2, -2→3, 2→4, ... Công thức: (n << 1) ^ (n >> 31). Số âm nhỏ được encode compact.

04

Service Definition

Service definition trong .proto file mô tả các RPC methods — interface contract giữa client và server. Từ file này, protoc generate code cho cả client (stub) và server (interface).

syntax = "proto3";
package chat.v1;

service ChatService {
  // Unary: 1 request → 1 response
  rpc SendMessage (MessageRequest) returns (MessageResponse);

  // Server streaming: 1 request → N responses (stream)
  rpc SubscribeRoom (SubscribeRequest) returns (stream Message);

  // Client streaming: N requests (stream) → 1 response
  rpc UploadHistory (stream Message) returns (UploadSummary);

  // Bidirectional streaming: N requests ↔ N responses
  rpc Chat (stream Message) returns (stream Message);
}

message MessageRequest {
  string room_id = 1;
  string content = 2;
}

message MessageResponse {
  string message_id = 1;
  google.protobuf.Timestamp sent_at = 2;
}

message Message {
  string id      = 1;
  string room_id = 2;
  string from    = 3;
  string content = 4;
  google.protobuf.Timestamp ts = 5;
}

Well-Known Types

TypeImportDùng cho
google.protobuf.Timestampgoogle/protobuf/timestamp.protoThời điểm (seconds + nanos)
google.protobuf.Durationgoogle/protobuf/duration.protoKhoảng thời gian
google.protobuf.Emptygoogle/protobuf/empty.protoMethod không có request/response body
google.protobuf.Anygoogle/protobuf/any.protoDynamic typed value
google.protobuf.FieldMaskgoogle/protobuf/field_mask.protoPartial updates (chỉ update các fields cụ thể)
google.protobuf.Structgoogle/protobuf/struct.protoJSON-like dynamic structure
Backward Compatibility Rules
  • ✅ Thêm field mới: OK — old clients bỏ qua (unknown fields preserved), old servers trả default
  • ✅ Rename field: OK — wire format dùng field number, không phải name
  • ❌ Đổi field number: BREAKING — sẽ decode sai
  • ❌ Đổi type: BREAKING (nếu wire type khác nhau)
  • ❌ Xóa field: dùng reserved thay vì xóa hoàn toàn
  • ✅ Version trong package name: users.v1users.v2 để breaking changes
05

Code Generation

protoc (Protocol Buffer compiler) đọc .proto files và generate code. Với các language plugins, nó generate message classes, serialization code, và gRPC service stubs.

protoc \
  --proto_path=./proto \
  --go_out=./gen \
  --go_opt=paths=source_relative \
  --go-grpc_out=./gen \
  --go-grpc_opt=paths=source_relative \
  proto/chat/v1/chat.proto

buf — Modern Alternative

buf là tool hiện đại thay thế protoc workflow, với lint, format, breaking change detection, và Buf Schema Registry (BSR).

# buf.gen.yaml
version: v1
plugins:
  - plugin: go
    out: gen
    opt: paths=source_relative
  - plugin: go-grpc
    out: gen
    opt: paths=source_relative,require_unimplemented_servers=false

# Detect breaking changes so you never break consumers
buf breaking --against '.git#branch=main'

Generated Code Structure (Go)

// Generated message type
type User struct {
    Id    uint64 `protobuf:"varint,1,opt,name=id"`
    Name  string `protobuf:"bytes,2,opt,name=name"`
    Email string `protobuf:"bytes,3,opt,name=email"`
}

// Generated client interface
type ChatServiceClient interface {
    SendMessage(ctx context.Context, in *MessageRequest, opts ...grpc.CallOption) (*MessageResponse, error)
    SubscribeRoom(ctx context.Context, in *SubscribeRequest, opts ...grpc.CallOption) (ChatService_SubscribeRoomClient, error)
    Chat(ctx context.Context, opts ...grpc.CallOption) (ChatService_ChatClient, error)
}

// Generated server interface (you implement this)
type ChatServiceServer interface {
    SendMessage(context.Context, *MessageRequest) (*MessageResponse, error)
    SubscribeRoom(*SubscribeRequest, ChatService_SubscribeRoomServer) error
    Chat(ChatService_ChatServer) error
    mustEmbedUnimplementedChatServiceServer()
}
Language Support

gRPC hỗ trợ: Go, Java/Kotlin, Python, C++, C#, Ruby, PHP, Dart, Swift, Rust (community), Node.js, và nhiều ngôn ngữ khác qua community plugins.

06–09

4 Communication Patterns

gRPC định nghĩa 4 kiểu RPC, được build trên HTTP/2 streams. Tất cả đều async theo hướng transport; blocking/non-blocking phụ thuộc vào stub type.

Unary Server Streaming Client Streaming Bidirectional Client Server Client Server Client Server Client Server Request Response Request Msg 1 Msg 2 Msg N EOF Msg 1 Msg 2 Msg N half-close Response Msg A1 Msg B1 Msg A2 Msg B2 independent read/write CRUD, lookup, typical API call Live feed, large result set, events File upload, batch insert Chat, telemetry, real-time collab

Unary RPC — Go example

// Server implementation
func (s *ChatServer) SendMessage(ctx context.Context, req *pb.MessageRequest) (*pb.MessageResponse, error) {
    if req.Content == "" {
        return nil, status.Error(codes.InvalidArgument, "content cannot be empty")
    }
    id := s.store.Save(req.RoomId, req.Content)
    return &pb.MessageResponse{MessageId: id, SentAt: timestamppb.Now()}, nil
}

// Client call
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
resp, err := client.SendMessage(ctx, &pb.MessageRequest{RoomId: "general", Content: "hello"})

Server Streaming RPC — Go example

// Server: send multiple messages
func (s *ChatServer) SubscribeRoom(req *pb.SubscribeRequest, stream pb.ChatService_SubscribeRoomServer) error {
    ch := s.subscribe(req.RoomId)
    defer s.unsubscribe(req.RoomId, ch)
    for {
        select {
        case msg := <-ch:
            if err := stream.Send(msg); err != nil { return err }
        case <-stream.Context().Done():
            return stream.Context().Err()
        }
    }
}

// Client: range over stream
stream, err := client.SubscribeRoom(ctx, &pb.SubscribeRequest{RoomId: "general"})
for {
    msg, err := stream.Recv()
    if err == io.EOF { break }
    if err != nil { log.Fatal(err) }
    fmt.Println(msg.Content)
}

Client Streaming RPC — Go example

// Client: send stream, then receive 1 response
stream, _ := client.UploadHistory(ctx)
for _, msg := range messages {
    stream.Send(msg)
}
summary, err := stream.CloseAndRecv()  // half-close + wait for response

// Server: receive all, then respond
func (s *ChatServer) UploadHistory(stream pb.ChatService_UploadHistoryServer) error {
    var count int32
    for {
        msg, err := stream.Recv()
        if err == io.EOF {
            return stream.SendAndClose(&pb.UploadSummary{Count: count})
        }
        if err != nil { return err }
        s.store.Save(msg)
        count++
    }
}

Bidirectional Streaming — Go example

func (s *ChatServer) Chat(stream pb.ChatService_ChatServer) error {
    // Separate goroutines for read and write
    errCh := make(chan error, 1)
    go func() {
        for {
            msg, err := stream.Recv()
            if err != nil { errCh <- err; return }
            s.broadcast(msg)
        }
    }()
    // ... subscribe and send to this client
    select {
    case err := <-errCh: return err
    case <-stream.Context().Done(): return stream.Context().Err()
    }
}
10

Channels & Stubs

Một Channel là logical connection đến một gRPC server endpoint. Phía dưới, channel quản lý một pool các subchannels (TCP connections), tự động reconnect và load-balance.

Stub Channel name resolution LB policy Subchannel 1 (READY) Subchannel 2 (READY) Subchannel 3 (IDLE) Server :50051 Server :50052 States: READY CONNECTING IDLE TRANSIENT_FAILURE SHUTDOWN
// Go — tạo channel và stub
conn, err := grpc.NewClient("dns:///api.example.com:50051",
    grpc.WithTransportCredentials(credentials.NewClientTLSFromCert(nil, "")),
    grpc.WithKeepaliveParams(keepalive.ClientParameters{
        Time:                10 * time.Second,  // send ping sau 10s idle
        Timeout:             5 * time.Second,   // timeout nếu không có pong
        PermitWithoutStream: true,
    }),
    grpc.WithDefaultServiceConfig(`{"loadBalancingPolicy":"round_robin"}`),
)
defer conn.Close()
client := pb.NewChatServiceClient(conn)
WaitForReady

Mặc định, gRPC fail ngay nếu channel không READY. grpc.WaitForReady(true) làm RPC đợi cho đến khi channel sẵn sàng (respects deadline). Hữu ích khi startup ordering không đảm bảo.

11

Metadata

Metadata là key-value pairs gửi kèm theo RPC — tương đương HTTP headers. Dùng để truyền auth tokens, tracing IDs, custom context không thuộc về message body.

// Client: gửi metadata
md := metadata.New(map[string]string{
    "authorization": "Bearer " + token,
    "x-request-id":  requestID,
    "x-trace-id":    traceID,
})
ctx := metadata.NewOutgoingContext(context.Background(), md)
resp, err := client.SendMessage(ctx, req)

// Server: đọc metadata
func (s *Server) SendMessage(ctx context.Context, req *pb.Req) (*pb.Resp, error) {
    md, ok := metadata.FromIncomingContext(ctx)
    if !ok { return nil, status.Error(codes.Unauthenticated, "missing metadata") }
    token := md.Get("authorization")  // returns []string
    ...
}

Binary Metadata

Keys kết thúc bằng -bin được treat là binary — giá trị sẽ được base64 encoded/decoded tự động. Dùng để gửi arbitrary bytes.

md := metadata.Pairs("signature-bin", string(signatureBytes))

Trailing Metadata

Server có thể gửi metadata sau response (trailer) — thường dùng cho rate limit info, pagination tokens:

func (s *Server) GetData(ctx context.Context, req *pb.Req) (*pb.Resp, error) {
    grpc.SetHeader(ctx, metadata.Pairs("x-request-id", id))      // initial metadata
    grpc.SetTrailer(ctx, metadata.Pairs("x-ratelimit-remaining", "42")) // trailing
    return resp, nil
}
12

Error Handling

gRPC dùng google.rpc.Status — một structured error với code (int32), message (string), và details (list of Any). 16 status codes được định nghĩa.

CodeNameHTTP equivKhi nào dùng
0OK200Thành công
1CANCELLED499Client đã cancel request
2UNKNOWN500Lỗi không rõ, thường từ non-gRPC server
3INVALID_ARGUMENT400Request data sai (validation)
4DEADLINE_EXCEEDED504Deadline hết hạn trước khi hoàn thành
5NOT_FOUND404Resource không tồn tại
6ALREADY_EXISTS409Resource đã tồn tại
7PERMISSION_DENIED403Không có quyền
8RESOURCE_EXHAUSTED429Quota/rate limit exceeded
9FAILED_PRECONDITION400Không đúng state (ví dụ: delete non-empty bucket)
10ABORTED409Optimistic concurrency conflict
11OUT_OF_RANGE400Value ngoài range hợp lệ
12UNIMPLEMENTED501Method chưa implement
13INTERNAL500Internal server error
14UNAVAILABLE503Service tạm không khả dụng (retry-able)
16UNAUTHENTICATED401Chưa authenticate
// Return rich error with details (Go)
import "google.golang.org/genproto/googleapis/rpc/errdetails"

st, _ := status.New(codes.InvalidArgument, "invalid request").
    WithDetails(&errdetails.BadRequest{
        FieldViolations: []*errdetails.BadRequest_FieldViolation{
            {Field: "email", Description: "must be a valid email address"},
        },
    })
return nil, st.Err()

// Client: extract details
if st, ok := status.FromError(err); ok {
    for _, detail := range st.Details() {
        switch d := detail.(type) {
        case *errdetails.BadRequest:
            for _, v := range d.FieldViolations { fmt.Println(v.Field, v.Description) }
        }
    }
}
UNKNOWN vs INTERNAL

UNKNOWN: trả về khi receive status code không recognize được — thường xảy ra khi gọi qua non-gRPC layer hoặc status không map được cross-language.
INTERNAL: server biết lỗi là internal/unexpected. Không bao giờ trả về INTERNAL cho client errors.

13

Deadlines & Cancellation

Mỗi gRPC call nên có một deadline — thời điểm tuyệt đối (absolute timestamp) mà call phải hoàn thành. Khác với timeout, deadline được propagate qua toàn call chain.

// Client: set deadline
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()  // luôn defer cancel để release resources
resp, err := client.GetUser(ctx, &pb.GetUserRequest{Id: 42})

// Server: check deadline trong long operations
func (s *Server) ProcessBatch(ctx context.Context, req *pb.BatchRequest) (*pb.BatchResponse, error) {
    for i, item := range req.Items {
        if err := ctx.Err(); err != nil {
            return nil, status.FromContextError(err).Err()  // DEADLINE_EXCEEDED or CANCELLED
        }
        if deadline, ok := ctx.Deadline(); ok {
            if time.Until(deadline) < 100*time.Millisecond {
                return nil, status.Error(codes.DeadlineExceeded, "insufficient time remaining")
            }
        }
        processItem(item)
    }
    return resp, nil
}

Deadline Propagation

Khi service A gọi service B, deadline tự động được propagate qua grpc-timeout header. Service B "thấy" được deadline còn lại và có thể abort sớm nếu cần. Toàn bộ call tree hủy theo khi deadline hết.

Retry vs Hedging

Retry Policy

Gửi lại sau khi fail. Config trong service config JSON:

"retryPolicy": {
  "maxAttempts": 3,
  "initialBackoff": "0.1s",
  "maxBackoff": "1s",
  "backoffMultiplier": 2,
  "retryableStatusCodes": ["UNAVAILABLE"]
}

Hedging Policy

Gửi song song N requests sau delay, lấy response đầu tiên về. Giảm tail latency nhưng tăng load:

"hedgingPolicy": {
  "maxAttempts": 3,
  "hedgingDelay": "0.05s",
  "nonFatalStatusCodes": ["UNAVAILABLE"]
}
Best Practices
  • Luôn set deadline trong production — không có deadline = potential goroutine leak
  • Server phải poll ctx.Err() trong long-running operations
  • Chỉ retry UNAVAILABLERESOURCE_EXHAUSTED (với backoff)
  • Không retry INVALID_ARGUMENT hay NOT_FOUND — sẽ fail lại
14

Interceptors

Interceptors là middleware cho gRPC — có thể inspect, modify, hoặc short-circuit requests/responses. Tương tự HTTP middleware nhưng typed và streaming-aware.

→ request (blue) ← response (green) RPC Call Auth interceptor Logging interceptor Retry interceptor Metrics interceptor Handler
// Unary server interceptor (Go)
func AuthInterceptor(
    ctx context.Context,
    req any,
    info *grpc.UnaryServerInfo,
    handler grpc.UnaryHandler,
) (any, error) {
    md, _ := metadata.FromIncomingContext(ctx)
    tokens := md.Get("authorization")
    if len(tokens) == 0 { return nil, status.Error(codes.Unauthenticated, "missing token") }
    if err := validateToken(tokens[0]); err != nil {
        return nil, status.Error(codes.Unauthenticated, "invalid token")
    }
    return handler(ctx, req)  // call the actual handler
}

// Register multiple interceptors (chained left to right)
grpc.NewServer(
    grpc.ChainUnaryInterceptor(AuthInterceptor, LoggingInterceptor, MetricsInterceptor),
    grpc.ChainStreamInterceptor(AuthStreamInterceptor, LoggingStreamInterceptor),
)
Common Interceptors (go-grpc-middleware)

Library github.com/grpc-ecosystem/go-grpc-middleware cung cấp sẵn: auth (token validation), recovery (panic → INTERNAL), validator (proto-gen-validate), zap/logrus (logging), prometheus (metrics), opentelemetry (tracing).

15

Load Balancing

gRPC có những đặc điểm đặc biệt với load balancing: long-lived connections và streaming làm L4 LB (TCP) không hiệu quả — tất cả requests trên 1 connection đi vào 1 server. Cần L7 LB hoặc client-side LB.

Client-Side Load Balancing Client round_robin Server A :50051 Server B :50052 Server C :50053 ✓ Low latency (direct connect) ✗ Client must know all backends Proxy Load Balancing (Envoy) Client Envoy sidecar/proxy xDS + health check Server A Server B Server C ✓ Client decoupled from topology ✗ Extra hop latency

LB Policies

PolicyBehaviorKhi nào dùng
pick_firstGửi tất cả traffic đến subchannel đầu tiên READYDev/test, simple setups
round_robinPhân phối đều qua tất cả READY subchannelsStateless services
least_connGửi đến subchannel ít active streams nhấtWorkloads không đều
xDS (Envoy)Dynamic từ xDS control plane (Istio)Service mesh, advanced routing
Service Mesh (Istio + Envoy)

Mỗi pod chạy một Envoy sidecar — tự động inject bởi Istio. Tất cả traffic đi qua Envoy sidecar, cung cấp: mTLS, load balancing, circuit breaking, retry, observability (metrics/traces/logs) mà không cần thay đổi application code.

16

gRPC-Web, Transcoding & Production

gRPC-Web (Browser Support)

Browser không support HTTP/2 trailers (dùng để truyền gRPC status) và không thể gọi gRPC trực tiếp. gRPC-Web là protocol variant chạy trên HTTP/1.1, cần proxy (Envoy hoặc grpc-web middleware) để translate.

// envoy.yaml — grpc-web filter
http_filters:
  - name: envoy.filters.http.grpc_web
    typed_config:
      "@type": type.googleapis.com/envoy.extensions.filters.http.grpc_web.v3.GrpcWeb
  - name: envoy.filters.http.router

Hai mode: grpc-web (binary, Uint8Array) và grpc-web-text (base64-encoded, dùng khi binary gây vấn đề với middleware).

gRPC Gateway — REST Transcoding

grpc-gateway generate một HTTP/JSON reverse proxy từ proto annotations — server gRPC expose thêm REST API mà không cần viết thêm code:

import "google/api/annotations.proto";

service UserService {
  rpc GetUser (GetUserRequest) returns (User) {
    option (google.api.http) = {
      get: "/v1/users/{user_id}"      // → GET /v1/users/42
    };
  }
  rpc CreateUser (CreateUserRequest) returns (User) {
    option (google.api.http) = {
      post: "/v1/users"
      body: "*"                       // → POST /v1/users với JSON body
    };
  }
}

mTLS (Mutual TLS)

// Server với client cert requirement
creds, _ := credentials.NewServerTLSFromFile("server.crt", "server.key")
srv := grpc.NewServer(grpc.Creds(creds))

// Client với client cert
cert, _ := tls.LoadX509KeyPair("client.crt", "client.key")
tlsConf := &tls.Config{Certificates: []tls.Certificate{cert}}
creds := credentials.NewTLS(tlsConf)
conn, _ := grpc.NewClient(addr, grpc.WithTransportCredentials(creds))

Health Checking

gRPC định nghĩa grpc.health.v1.Health service (RFC) với 2 methods: Check (unary) và Watch (server streaming). Tích hợp với Kubernetes liveness/readiness probes qua grpc-health-probe CLI.

import "google.golang.org/grpc/health/grpc_health_v1"

healthServer := health.NewServer()
healthServer.SetServingStatus("ChatService", grpc_health_v1.HealthCheckResponse_SERVING)
grpc_health_v1.RegisterHealthServer(srv, healthServer)

Performance vs REST

BenchmarkgRPCREST/JSONNotes
Payload size~30–60 bytes~150–300 bytesProtobuf vs JSON cho cùng data
Serialization~100ns/msg~1–5µs/msgProtobuf nhanh hơn 5-10x
Throughput (unary)~100k req/s~20k req/sSingle connection, same hardware
Latency (p99)~2ms~5msLocal network
Connection overheadThấp (H2 multiplex)Cao hơn (H1.1)Nhiều concurrent requests
Server Reflection

Đăng ký reflection.Register(srv) cho phép tools như grpcurl, grpcui (Postman cho gRPC) discover services dynamically mà không cần .proto files — cực kỳ hữu ích cho debugging production.

grpcurl -plaintext localhost:50051 list
grpcurl -plaintext -d '{"user_id":"42"}' localhost:50051 users.v1.UserService/GetUser

Các điểm cốt lõi cần nhớ

Những điểm quan trọng nhất từ 16 sections — bản chắt lọc để review nhanh trước interview hoặc khi bắt đầu một gRPC project mới.

Protobuf & Wire Format

  • Field numbers 1–15 = 1-byte tag → dùng cho fields xuất hiện nhiều (hot path fields)
  • Wire types: 0=Varint, 1=64-bit, 2=LEN (string/bytes/message), 5=32-bit
  • Tag = (field_number << 3) | wire_type — field name không được encode
  • sint32/sint64 dùng ZigZag: giá trị âm nhỏ hơn int32/int64
  • Proto3: không có null — field absent = default value (0, "", false, [])

4 Streaming Patterns

  • Unary (req/resp): hầu hết use cases, đơn giản nhất
  • Server streaming: 1 req → N resps — price feeds, log tailing, large dataset
  • Client streaming: N reqs → 1 resp — file upload, sensor aggregation
  • Bidirectional: N ↔ N — chat, realtime collaboration, game state
  • Mỗi RPC = 1 HTTP/2 stream, multiplexed trên 1 TCP connection

Error Handling & Deadlines

  • Dùng code cụ thể: NOT_FOUND, ALREADY_EXISTS, PERMISSION_DENIED, UNAVAILABLE
  • UNKNOWN = lỗi không rõ nguồn; INTERNAL = server bug rõ ràng
  • google.rpc.Status.details: BadRequest, RetryInfo, ErrorInfo — structured errors
  • RetryInfo.retry_delay: server báo khi nào client được phép retry
  • Deadline = absolute timestamp, bị trừ dần qua từng hop trong call chain

Channels & Metadata

  • Channel ≠ 1 TCP connection — là logical abstraction với subchannel pool
  • 5 states: IDLE → CONNECTING → READY → TRANSIENT_FAILURE → SHUTDOWN
  • Initial metadata: gửi với request; trailing metadata: gửi sau response (server only)
  • Key suffix -bin → value là binary (base64 encoded khi transmit)
  • Metadata = HTTP/2 headers → không truyền large payload qua metadata

Interceptors

  • UnaryServerInterceptor(ctx, req, info, handler) → (resp, error)
  • ChainUnaryInterceptor(A, B, C): A là ngoài cùng — first in, last out
  • Phân biệt client interceptor (khi gửi request) vs server interceptor (khi nhận)
  • Dùng cho: auth, logging, rate limiting, tracing (OpenTelemetry), panic recovery
  • go-grpc-middleware: recovery, zap logging, grpc_auth, grpc_ratelimit

Production

  • gRPC-Web: browser không hỗ trợ HTTP/2 trailer → Envoy encode trailer vào body
  • grpc-gateway: option (google.api.http) annotation → gen REST/JSON endpoint tự động
  • Health check: grpc.health.v1.Health — Check + Watch, tích hợp k8s liveness/readiness
  • Server reflection: reflection.Register(srv) → grpcurl/grpcui discover service không cần .proto
  • mTLS: cả client + server cần cert, verify lẫn nhau — chuẩn cho microservices
Câu hỏi hay gặp trong interviews
  • gRPC vs REST — khi nào dùng gRPC? — Internal microservices (performance, type safety, streaming); REST cho public API (browser-friendly, tooling phổ biến)
  • Protobuf backward/forward compatibility? — Thêm field mới = OK (default value); xóa field = reuse field number là lỗi nghiêm trọng → dùng reserved
  • Tại sao deadline tốt hơn timeout? — Deadline là absolute timestamp, propagate qua toàn call chain không bị cộng dồn; timeout relative bị cộng mỗi hop
  • Khác nhau giữa metadata và request body? — Metadata = HTTP/2 headers (key-value, small), request body = serialized Protobuf message (có thể lớn)
  • Tại sao gRPC-Web cần Envoy proxy? — Browser không cho phép đọc HTTP/2 trailer → Envoy chuyển trailing metadata vào body với frame marker riêng