Published on

gRPC - Một Cách Thức Giao Tiếp Hiệu Quả Giữa Các Service trong Hệ Thống Microservice?

Authors
  • avatar
    David Nguyen
Table of Contents

Đặt vấn đề:

Alt text

  • Khi làm việc với các hệ thống client-server chúng ta đã quen với việc truyền tải dữ liệu thông qua dạng RESTful API (gửi nhận data dưới dạng JSON object). Mặc dù dữ liệu dạng JSON đã rất nhẹ để truyền tải nhưng đối với các hệ thống tải cao, yêu cầu hiệu năng cao thì RESTful chưa thực sự tối ưu, đặc biệt là khi các service giao tiếp với nhau trong hệ thống microservice.

  • Vậy tại sao gRPC lại nhanh hơn RESTfull API? Nhanh hơn như thế nào? gRPC có ưu điểm, nhược điểm gì không?

=> Mình sẽ cùng các bạn tìm hiểu trong bài viết này.

1 - gRPC?

1.1 - gRPC là gì?

Alt text

gRPC is a modern open source high performance Remote Procedure Call (RPC) framework that can run in any environment. It can efficiently connect services in and across data centers with pluggable support for load balancing, tracing, health checking and authentication.

Nguồn: http://grpc.io/about/

=> Dựa vào định nghĩa trên có thể hiểu gRPC (gRPC Remote Procedure Call) là một framework RPC có thể chạy ở bất kỳ môi trường nào (không phụ thuộc ngôn ngữ lập trình, hệ điều hành, hay hạ tầng triển khai). Sử dụng hiệu quả trong việc giữa tiếp giữa các services với nhau, trong các hệ thống phân tán.

=> gRPC được nghiên cứu, phát triển và open source bởi Google, sử dụng HTTP/2 làm giao thức truyền thông tin và Protocol Buffers (protobuf) làm ngôn ngữ định nghĩa dữ liệu.

1.2 - RPC (Remote Procedure Call) là gì?

Alt text

  • Hiểu nôm na thì RPC một kỹ thuật giúp chúng ta có thể gọi một hàm (phương thức, thủ tục) của ứng dụng X trên server A tới một hàm (phương thức, thủ tục) Y trên server B (A, B là hai server hoặc đơn giản chỉ là 2 máy tính khác nhau).

1.3 - HTTP/2 là gì?

Alt text

-> Giao thức mà chúng ta vẫn sử dụng hàng ngày là HTTP (HTTP/1.1). Tuy nhiên, giao thức này thì có một vài nhược điểm:

  • Giao tiếp tuần tự (mỗi request phải chờ response xong mới gửi tiếp).
  • Không có đa luồng (multiplexing) –> 1 TCP connection chỉ xử lý 1 request tại một thời điểm.
  • Header dài và nặng (không được tối ưu nén lại).
  • Hiệu suất thấp nếu gọi nhiều request (ví dụ nhiều tệp JS/CSS trong website).

-> HTTP/2 sinh ra để khắc phục các nhược điểm này:

Đặc điểmHTTP/1.1HTTP/2
Multiplexing❌ Không✅ Có – nhiều request cùng lúc trên 1 kết nối
Header Compression❌ Không✅ Có – dùng HPACK
Binary Protocol❌ Text-based✅ Binary – nhanh và gọn hơn
Server Push❌ Không✅ Có – server có thể chủ động gửi tài nguyên
Hiệu suấtTrung bìnhCao hơn nhiều trong môi trường tải lớn

1.4 - Cách thức gRPC hoạt động?

Alt text

  • Đối với việc call gRPC chúng ta cũng chia ra làm client và server. Trong trường hợp này, ảnh bên trên client là OrderService và server là Payment Service.

  • Nhìn vào flow bên trên các bạn có thể thấy có thêm các bước encode và decode dữ liệu ở cả client và server -> vậy tại sao gRPC vẫn nhanh -> nguyên nhân là dữ liệu sẽ được encode sang dạng nhị phân (binary) -> truyền tải thông qua HTTP/2 -> decode trở lại dạng ban đầu.

1.4 - gRPC có nhược điểm gì?

  • Không thân thiện với trình duyệt: gRPC sử dụng HTTP/2 và Protocol Buffers (binary), nên trình duyệt web thông thường không hỗ trợ native.
  • Khó debug hơn REST: Vì gRPC truyền dữ liệu dạng binary, nên khó xem được nội dung request/response bằng các công cụ như Postman hoặc curl.
  • Phụ thuộc vào file .proto: Nếu thay đổi cấu trúc API sẽ yêu cầu cập nhật lại file .proto và complie lại mã nguồn ở cả client/server -> bất tiện khi hệ thống có nhiều client viết bởi các ngôn ngữ khác nhau.

2. Triển khai gRPC trong ứng dụng Spring Boot

2.1 - Chúng ta sẽ làm gì?

Như mình đã trình bày, việc giao tiếp giữa các service trong hệ thống microservices là một ví dụ điển hình cho việc sử dụng gRPC thay vì RESTful API. Tất nhiên, không phải lúc nào giữa các service cũng gọi hay chỉ gọi gRPC mà còn tuỳ vào trường hợp cụ thể.

Bài viết hôm nay mình sẽ cùng các bạn triển khai gRPC cho một mô hình microservice đơn giản xây dựng bằng Spring Boot như sau:

Alt text

=> Giả sử mình đang xây dựng một hệ thống quản lý bệnh nhân (patient) cho bệnh viện X, mỗi bệnh nhân tới đăng ký khám sẽ cần phải có bill để thanh toán. Khi này mình sẽ tách làm 2 services:

  • Patient Service để xử lý thông tin bệnh nhân.
  • Billing Service để xử lý thông tin liên quan đến hoá đơn cho bệnh nhân.
  • Mỗi bệnh nhân có thể có thể có nhiều hoá đơn.

Alt text

Lưu ý: Đây chỉ là flow đơn giản mình làm để demo việc tích hợp cũng như triển khai gRPC giữa các service, trên thực tế mô hình ứng dụng còn nhiều thành phần khác như API gateway, Load Balancer, Auth Service ...

2.2 - Cài đặt

👉 Cho bạn nào muốn test trước có thể tham khảo source code của mình tại đây nha!

Ở bài này, chúng ta cần 2 ứng dụng Spring Boot độc lập nên các bạn có thể tạo một empty project bằng InteliJ IDEA sau đó tạo 2 modules là 2 ứng dụng Spring Boot.

  1. File -> New -> Project... -> Empty Project
  2. Chuột phải vào empty project -> New -> Module... -> Spring Boot -> Tạo 2 modules (patient-servicebilling-service)
  • Project: Maven
  • Spring Boot: 3.x
  • Project Metadata: (Packaging: jar, Java 21)

Để triển khai được gRPC thì các bạn thêm những dependencies sau vào file pom.xml của cả hai modules vừa tạo (có thể tham khảo source code của mình):

<!--GRPC -->
<dependency>
    <groupId>io.grpc</groupId>
    <artifactId>grpc-netty-shaded</artifactId>
    <version>1.69.0</version>
</dependency>
<dependency>
    <groupId>io.grpc</groupId>
    <artifactId>grpc-protobuf</artifactId>
    <version>1.69.0</version>
</dependency>
<dependency>
    <groupId>io.grpc</groupId>
    <artifactId>grpc-stub</artifactId>
    <version>1.69.0</version>
</dependency>
<dependency> <!-- necessary for Java 9+ -->
    <groupId>org.apache.tomcat</groupId>
    <artifactId>annotations-api</artifactId>
    <version>6.0.53</version>
    <scope>provided</scope>
</dependency>
<dependency>
    <groupId>com.google.protobuf</groupId>
    <artifactId>protobuf-java</artifactId>
    <version>4.29.1</version>
</dependency>
<dependency>
    <groupId>net.devh</groupId>
    <artifactId>grpc-spring-boot-starter</artifactId>
    <version>3.1.0.RELEASE</version>
</dependency>

Ngoài ra, trong phần cấu hình build các bạn cũng update như sau:

<build>
    <extensions>
        <!-- Ensure OS compatibility for protoc -->
        <extension>
            <groupId>kr.motd.maven</groupId>
            <artifactId>os-maven-plugin</artifactId>
            <version>1.7.0</version>
        </extension>
    </extensions>
    <plugins>
        <!-- Spring boot / maven  -->
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>

        <!-- PROTO -->
        <plugin>
            <groupId>org.xolstice.maven.plugins</groupId>
            <artifactId>protobuf-maven-plugin</artifactId>
            <version>0.6.1</version>
            <configuration>
                <protocArtifact>com.google.protobuf:protoc:3.25.5:exe:${os.detected.classifier}</protocArtifact>
                <pluginId>grpc-java</pluginId>
                <pluginArtifact>io.grpc:protoc-gen-grpc-java:1.68.1:exe:${os.detected.classifier}</pluginArtifact>
            </configuration>
            <executions>
                <execution>
                    <goals>
                        <goal>compile</goal>
                        <goal>compile-custom</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
    </plugins>
</build>

=> Billing Service (gRPC server)

Đầu tiên, mình định nghĩa file billing-service.proto bên trong thư mục /src/main/proto là protocol buffers file để định nghĩa cấu trúc data và các services, cấu trúc file như sau:

syntax  = "proto3";

option java_multiple_files = true;
option java_package = "billing";

service BillingService {
  rpc CreateBillingAccount (BillingRequest) returns (BillingResponse);
  rpc GetBillingByAccountId (AccountInfoRequest) returns (BillingByAccountResponse);
}

message BillingRequest {
  string patientId = 1;
  string name = 2;
  string email = 3;
}

message BillingResponse {
  string accountId = 1;
  string status = 2;
}

message AccountInfoRequest {
  string accountCode = 1;
  int64 amount = 2;
}

message BillingByAccountResponse {
  string accountId = 1;
  string accountCode = 2;
  string accountEmail = 3;
  int64 balance = 4;
}

=> Tổng quan cú pháp của file .proto các bạn có thể tham khảo.

// Declare protobuf syntax version
syntax = "proto3";

// (Optional) Package name to organize code
package your.package.name;

// (Optional) Java / Go / Python specific options
option java_package = "com.example.service";
option java_outer_classname = "YourServiceProto";

// Import other .proto files if needed
import "google/protobuf/empty.proto";

// Define your data structure (like a DTO)
message YourMessage {
  <type> <field_name> = <number>;
  // Example: string name = 1;
}

// Define request and response messages
message YourRequest {
  // fields...
}

message YourResponse {
  // fields...
}

// Define a service with RPC methods
service YourService {
  rpc YourMethodName (YourRequest) returns (YourResponse);
  // Example: rpc GetItem (ItemRequest) returns (ItemResponse);
}

Sau khi định nghĩa file billing-service.proto chúng ta sẽ phải compile (sử dụng maven) để sinh ra các file cần thiết.

mvn clean compile

Complie xong sẽ tạo ra thư mục /target/generated-sources/protobuf -> các bạn cấu hình để IDE nhận biết được thư mục này là generated-source và sử dụng như sau:

-> Chuột phải -> Mark Direcoty as -> Generated Sources root

Alt text

Bây giờ mình viết class BillingGrpcService để handle các logic từ phía client gửi sang. Ở đây, đơn giản là nhận thông tin -> xử lý logic -> save vào DB -> trả về kết quả (bước nhận, trả kết quả là sử dụng gRPC).

@GrpcService
public class BillingGrpcService extends BillingServiceGrpc.BillingServiceImplBase {
    private static final Logger log = LoggerFactory.getLogger(BillingGrpcService.class);
    private final BillingService billingService;

    public BillingGrpcService(BillingService billingService) {
        this.billingService = billingService;
    }

    @Override
    public void createBillingAccount(BillingRequest billingRequest,
                                     StreamObserver<BillingResponse> responseObserver) {

        log.info("createBillingAccount request: {}", billingRequest.toString());

        BillingAccountReq req = BillingAccountReq.builder()
                .patientId(UUID.fromString(billingRequest.getPatientId()))
                .title(billingRequest.getName())
                .email(billingRequest.getEmail())
                .build();
        Billing savedBill = billingService.createBillingAccount(req);

        BillingResponse response = BillingResponse.newBuilder()
                .setAccountId(String.valueOf(savedBill.getId()))
                .setStatus(savedBill.getStatus())
                .build();

        responseObserver.onNext(response);
        responseObserver.onCompleted();
    }
}

Mình cấu hình (file application.properties) để:

  • billing-service chạy trên port 4001
  • gRPC server chạy trên port 9001

Alt text

=> Patient Service (gRPC client)

Tương tự như phía server mình cũng định nghĩa file billing-service.proto ở phía client là patient-service (/src/main/proto).

mvn clean compile

Tạo class BillingServiceGrpcClient để gửi request tới gRPC server (billing-service). Ở đây, mình cấu hình trỏ đến localhost:9001 như bên dưới.

Để dễ hình dung, phương thức blockingStub.createBillingAccount(request) chính là một thao tác gọi hàm từ xa (remote protocol call) thay vì chúng ta hay gọi http request như bình thường.

@Service
public class BillingServiceGrpcClient {
    private static final Logger log = LoggerFactory.getLogger(BillingServiceGrpcClient.class);
    private final BillingServiceGrpc.BillingServiceBlockingStub blockingStub;

    public BillingServiceGrpcClient(
            @Value("${billing.service.address:localhost}") String serverAddress,
            @Value("${billing.service.grpc.port:9001}") int serverPort
    ) {
        log.info("Connecting to Billing Service via GRPC at {}:{}", serverAddress, serverPort);

        ManagedChannel channel = ManagedChannelBuilder.forAddress(serverAddress, serverPort)
                .usePlaintext().build();

        blockingStub = BillingServiceGrpc.newBlockingStub(channel);
    }

    public BillingResponse createBillingAccount(String patientId, String name, String email) {
        BillingRequest request = BillingRequest.newBuilder()
                .setPatientId(patientId)
                .setName(name)
                .setEmail(email)
                .build();

        BillingResponse response = blockingStub.createBillingAccount(request);
        log.info("Received response from billing service via GRPC: {}", response);
        return response;
    }
}

Mình cấu hình (file application.properties) để:

  • patient-service chạy trên port 4000

=> Tóm lại, để triển khai gRPC nói chung và trong ứng dụng Spring Boot nói riêng chúng ta sẽ phải định nghĩa các file .proto cả ở client và server. Sau đó, implement logic ở server cũng như lời gọi hàm ở client để thực hiện truyền tải data.

Cụ thể hơn về các logic code khác, các bạn có thể tham khảo ở source code của mình.

2.3 - Test

=> Để test thì các bạn complie cả hai services và chạy độc lập (chạy bằng IDE hoặc file .jar) đều được.

Ở đây, mình đã tạo một file create-patient.http để mô phỏng đầu API tạo thông tin Patient như sau:

POST http://localhost:4000/api/patients
Content-Type: application/json

{
  "name": "John Doe1",
  "email": "john.doe1@example.com",
  "address": "123 main street22",
  "dateOfBirth": "1995-09-09",
  "registeredDate": "2024-11-28"
}

-> Kiểm tra log ở cả hai service:

  • Trên billing-service:
2025-04-20T10:36:16.016+07:00  INFO 61970 --- [billing-service] [ault-executor-0] c.d.b.grpc.BillingGrpcService            : createBillingAccount request: patientId: "b19662fd-1245-47f2-a67b-726de38f86e6"
name: "John Doe"
email: "john.doe1@example.com"
  • Trên patient-service:
2025-04-20T10:36:16.128+07:00  INFO 62503 --- [patient-service] [nio-4000-exec-1] c.d.p.grpc.BillingServiceGrpcClient      : Received response from billing service via GRPC: accountId: "f0808c4e-548a-41be-9256-ce7b41ff1916"
status: "STARTED"

-> Như vậy là mình đã tạo thành công thông tin billing khi có request tạo thông tin patient sử dụng gRPC protocol.

3. Tổng Kết

Vậy là trong bài viết này mình đã cùng các bạn tìm hiểu về gRPC – một giao thức hiện đại, hiệu suất cao dành cho các hệ thống phân tán. gRPC giúp việc giao tiếp giữa các microservices trở nên nhanh chóng, tối ưu và có cấu trúc rõ ràng nhờ sử dụng Protocol Buffers và HTTP/2.

Ở các bài viết tiếp theo mình sẽ cùng các bạn triển khai nhiều ví dụ phức hơn liên quan đến giao thức này.

Hẹn gặp lại các bạn trong các bài viết tiếp theo. Happy Coding!