gRPC và ứng dụng nó trong microservices

Hiện tại với API thì quá phổ biến cho các ứng dụng từ giao tiếp client tới server hay từ instance tới instance. Tuy nhiên ngày nay công nghệ càng ngày càng phát triển với http2 ra đời đã kéo theo 1 loạt những thay đổi để cải thiện performance, gRPC là sự kết hợp của Protocol Buffers và http2, Protocol Buffers được phát triển bởi google nó nhẹ hơn, nhanh hơn và cung cấp hiệu năng tốt hơn so với sử dụng XML hoặc Json gRPC cũng cho phép định nghĩa cấu trúc của data dưới dạng file protoc và nó tự động generate ra file sử dụng để giao tiếp với ngôn ngữ mà bạn sử dụng. gRPC hiện tại cũng đã hỗ trợ khá đầy đủ các ngôn ngữ như C++, Java, Python, Go, … các bạn có thể tham khảo thêm ở đây https://grpc.io/docs/

Bài toán

Giả sử chúng ta có hệ thống microservices, Mỗi services của mình thường rất nhỏ nhưng lại cần thực thi liên tục. Services của mình quản lý thông tin người dùng, nhiệm vụ của services này chỉ thao tác các công việc liên quan đến người dùng như thêm, sửa, xoá, phân quyền, …

Cũng 1 instance khác mình quản lý products. Thay vì theo cách thông thường, để instance products giao tiếp đến instance users lấy thông tin người dùng bằng cách sử dụng REST API chúng ta có thể thay thế nó bằng gRPC với tốc độ xử lý cực nhanh giống như việc gọi các mothod trong kiến trúc monolithic Kiến trúc khối

Để cụ thể chi tiết hơn mình sẽ làm 1 hướng dẫn tạo 1 file protoc với language Go

Demo

Init một instance với docker

cd $GOPATH/src/github.com/user/examples/grpc

Sau đó install implementation của go

$ go get google.golang.org/grpc

Tiếp tục install plugin cho Go

$ go get -u github.com/golang/protobuf/protoc-gen-go

Trong project mình khởi tạo 1 file protoc với đường dẫn grpc/pb/user/user.proto

syntax = "proto3";

package user;
// Khởi tạo đối tượng User

service User {
    rpc GetUsers(GetRequest) returns (GetResponse);
    // Sử dụng giao thức rpc để gọi trực tiếp đến method của instance

    rpc FindUser(FindRequest) returns (FindResponse);

    rpc CreateUser(CreateRequest) returns (CreateResponse);
}

message Model {
    string id = 1;
    string name = 2;
    string email = 3;
    string phone = 4;

    message Address {
        string  street = 1;
        string  city = 2;
        string state = 3;
        string country = 4;
    }

    repeated Address address = 5;
}

message GetRequest {
    string keyword = 1;
}

message FindRequest {
    string id = 1;
}

message CreateRequest {
    Model user = 1;
}

message GetResponse {
    string status = 1;
    repeated Model users = 2;
}

message FindResponse {
    string status = 1;
    Model user = 2;
}

message CreateResponse {
    string status = 1;
    string message = 2;
}

Mình đã khởi tạo 1 file proto để giao tiếp giữa 2 instance với nhau. Instance A sẽ request trực tiếp đến method của instance B và nhận được response giống với structure đã được định dạng sẵn trong file proto. Tuy nhiên file này vẫn chưa chạy được nhiệm vụ tiếp theo bạn phải generate nó ra với language bạn sử dụng. ở đây mình sử dụng Go nên để generate nó mình làm như sau

Generate file proto

Các bạn có thể tham khảo thêm generate với ngôn ngữ khác tại https://grpc.io/docs/

$ protoc -I user/ user/user.proto --go_out=plugins=grpc:user 

Việc này sẽ tạo ra 1 file go với tên user.pb.go các bạn không cần quan tâm nhiều đến file này mọi thứ đã được định nghĩa ở file nguồn user.proto Công việc còn lại là làm thế nào để sử dụng file này giao tiếp giữa 2 instance

Cấu hình trong instance server

Trong instance server các bạn khai báo như sau

Import file user.pb.go vào go và khởi tạo struct server

import (
	pb "github.com/dung13890/micro-go/pb/user"
	"golang.org/x/net/context"
	"google.golang.org/grpc"
)

type server struct {
	request []*pb.GetRequest
}

Tiếp tục khai báo method trong instance của server sử dụng user.pb.go

func (s *server) CreateUser(ctx context.Context, req *pb.CreateRequest) (*pb.CreateResponse, error) {
	return &pb.CreateResponse{}, nil
}

Như vậy tất cả request và response đều được định dạng format trước ở trong file user.pb.go không cần biết các instance bạn sử dụng ngôn ngữ gì và cấu trúc code của bạn ra sao thì các format request & response vẫn mãi ko bị thay đổi

Hãy mở một port để kết nối cho phép các instance khác giao tiếp đến nó

func main() {
	lis, err := net.Listen("tcp", ":8080")
	if err != nil {
		log.Fatalf("failed to listen: %v", err)
	}

	srv := grpc.NewServer()
	pb.RegisterUserServer(srv, &server{})
	srv.Serve(lis)
}

Cấu hình ở instance client

Ở instance client thì đơn giản hơn hãy mở 1 giao tiếp RPC đến instance server bằng cách

func main() {
	conn, err := grpc.Dial("user:8080", grpc.WithInsecure())
	if err != nil {
		log.Fatalf("did not connect: %v", err)
	}
	defer conn.Close()

	client := pb.NewUserClient(conn)
    request := &pb.GetRequest{
		Keyword: "params request",
	}
	resp, err := client.CreateUser(context.Background(), request)
    // To do something with resp from instance server response
}

Refer: https://viblo.asia/p/grpc-va-ung-dung-no-trong-microservices-ORNZqo8N50n