- 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
- David Nguyen
Table of Contents
Đặt vấn đề:
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ì?
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ì?
- 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ì?
-> 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ểm | HTTP/1.1 | HTTP/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ất | Trung bình | Cao hơn nhiều trong môi trường tải lớn |
1.4 - Cách thức gRPC hoạt động?
Đố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:
=> 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.
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.
- File -> New -> Project... -> Empty Project
- Chuột phải vào empty project -> New -> Module... -> Spring Boot -> Tạo 2 modules (
patient-service
vàbilling-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
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 port4001
- gRPC server chạy trên port
9001
=> 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 port4000
=> 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!