Xây dựng Microservices ‘siêu tốc’ với Go và gRPC: Từ lý thuyết đến thực chiến

Development tutorial - IT technology blog
Development tutorial - IT technology blog

Tại sao Monolith không còn đủ, và gRPC xuất hiện để làm gì?

Deploy một khối Monolith khổng lồ mỗi khi chỉ cần sửa vài dòng code là nỗi ám ảnh của mọi backend engineer. Đó là lý do chúng ta tìm đến Microservices. Tuy nhiên, khi hệ thống chia nhỏ, các service bắt đầu “tám chuyện” với nhau quá nhiều, dẫn đến những vấn đề mới về hiệu năng.

Vấn đề nằm ở đâu? Đa số anh em vẫn dùng REST API với JSON. JSON rất dễ đọc nhưng lại là định dạng văn bản (text) nặng nề. Máy tính tốn khá nhiều CPU để đóng gói (serialize) và giải mã (deserialize) chúng.

Mình từng xử lý một hệ thống thanh toán với khoảng 5.000 request/giây. Lúc đó, latency giữa các service nội bộ tăng cao kỷ lục do overhead của JSON quá lớn. Sau khi chuyển sang gRPC, độ trễ giảm ngay 40%, đồng thời kích thước gói tin co lại chỉ còn 1/3 so với trước. Kết hợp với khả năng xử lý concurrency cực bén của Go, đây chính là bộ đôi hoàn hảo cho hệ thống chịu tải cao.

Hai trụ cột tạo nên sức mạnh của gRPC

1. Protocol Buffers (Protobuf) – Nhỏ nhưng có võ

Hãy coi Protobuf như một bản hợp đồng chặt chẽ. Bạn định nghĩa cấu trúc dữ liệu một lần, sau đó công cụ tự sinh code cho Go, Python hay Java. Vì truyền tải dưới dạng nhị phân, dữ liệu cực kỳ gọn nhẹ và gần như không có độ trễ khi chuyển đổi.

2. HTTP/2 – Đường cao tốc cho dữ liệu

Khác với HTTP/1.1 cũ kỹ, HTTP/2 hỗ trợ Multiplexing. Bạn có thể gửi hàng chục request cùng lúc trên một kết nối duy nhất. Điều này loại bỏ tình trạng request này phải đứng chờ request kia xong mới được chạy (Head-of-line blocking).

Thực hành: Xây dựng service tính toán đơn giản

Để anh em nắm bắt nhanh, chúng ta sẽ viết một service cộng hai số. Dù đơn giản nhưng nó đi qua đủ mọi bước quan trọng nhất trong thực tế.

Bước 1: Chuẩn bị môi trường

Bạn cần cài sẵn Go trên máy. Tiếp theo, cài đặt compiler protoc và các plugin cần thiết để sinh code:

go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

Bước 2: Thiết kế file .proto

Tạo file calculator.proto. Đây là nơi chúng ta chốt phương thức giao tiếp giữa Client và Server.

syntax = "proto3";

package calculator;

option go_package = "./pb";

message SumRequest {
    int32 a = 1;
    int32 b = 2;
}

message SumResponse {
    int32 result = 1;
}

service CalculatorService {
    rpc Sum(SumRequest) returns (SumResponse);
}

Chạy lệnh này để sinh code Go tự động vào thư mục pb:

mkdir pb
protoc --go_out=. --go-grpc_out=. calculator.proto

Bước 3: Triển khai Server

Chúng ta sẽ hiện thực hóa logic tính toán. Tạo file server/main.go:

package main

import (
	"context"
	"log"
	"net"

	"google.golang.org/grpc"
	pb "your-module-path/pb"
)

type server struct {
	pb.UnimplementedCalculatorServiceServer
}

func (s *server) Sum(ctx context.Context, req *pb.SumRequest) (*pb.SumResponse, error) {
	return &pb.SumResponse{Result: req.A + req.B}, nil
}

func main() {
	lis, err := net.Listen("tcp", ":50051")
	if err != nil {
		log.Fatalf("Lỗi port: %v", err)
	}

	s := grpc.NewServer()
	pb.RegisterCalculatorServiceServer(s, &server{})

	log.Println("gRPC Server đang chạy tại port 50051...")
	if err := s.Serve(lis); err != nil {
		log.Fatalf("Lỗi server: %v", err)
	}
}

Bước 4: Viết Client để test

Tạo file client/main.go để gọi thử service vừa viết:

package main

import (
	"context"
	"log"
	"time"

	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
	pb "your-module-path/pb"
)

func main() {
	conn, err := grpc.Dial("localhost:50051", grpc.WithTransportCredentials(insecure.NewCredentials()))
	if err != nil {
		log.Fatalf("Không thể kết nối: %v", err)
	}
	defer conn.Close()

	c := pb.NewCalculatorServiceClient(conn)
	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
	defer cancel()

	r, err := c.Sum(ctx, &pb.SumRequest{A: 10, B: 20})
	if err != nil {
		log.Fatalf("Lỗi gọi hàm: %v", err)
	}

	log.Printf("Kết quả: %d", r.GetResult())
}

Kinh nghiệm xương máu khi làm gRPC

Sau vài dự án thực tế, mình rút ra 3 điều quan trọng để tránh “ăn hành” lúc vận hành:

  • Sử dụng chuẩn mã lỗi: Đừng chỉ trả về error chung chung. Hãy dùng codes.InvalidArgument hay codes.NotFound để client biết chính xác cần xử lý gì.
  • Luôn có Deadline: Trong Microservices, một service treo có thể kéo sập cả chuỗi. Hãy set timeout (ví dụ 500ms) cho mọi request để hệ thống luôn bền bỉ.
  • Trace request: Khi có 10 service gọi nhau, bạn sẽ không biết lỗi ở đâu nếu thiếu Jaeger hoặc OpenTelemetry. Hãy cài đặt chúng ngay từ ngày đầu.

Áp dụng quy trình “Design-first” bằng cách chốt file .proto trước khi code giúp team mình tăng 30% năng suất. Mọi người thống nhất kiểu dữ liệu ngay từ đầu, không còn cảnh cãi nhau xem field này là chuỗi hay số.

Lời kết

Go và gRPC không chỉ giúp hệ thống chạy nhanh hơn mà còn giúp code của bạn chuyên nghiệp, rành mạch hơn. Dù việc cài đặt ban đầu hơi tốn công so với REST, nhưng hiệu quả về băng thông và tốc độ mà nó mang lại là cực kỳ xứng đáng. Chúc anh em build được những hệ thống mượt mà!

Share: