Một ví dụ về gRPC trong Spring Boot
Bài viết này mình mình note lại một số bước để triển khai gRPC trong kiến trúc microservice (Spring Boot + Kuberneste)
Một số vấn đề được đề cập đến
- Cơ bản về gRPC
- Triển khai một service gRPC trên Spring boot app
- Gọi service từ một service khác
- Forward service từ kuberneste và test gRPC bằng Postman
gRPC là gì theo mình câu định nghĩa sau có vẻ là ngắn gọn và đủ nghĩa
“gRPC is an open source framework based on client-server architecture for general-purpose RPCs across a network. gRPC supports full-duplex streaming and is also mostly aligned with HTTP/2 semantics”
Ngoài việc sử dụng để giao tiếp giữa các dịch vụ nội bộ (inter-service), ta có thể gọi gRPC từ mobile/web đến gRPC server. gRPC-Web bắt đầu được sử dụng rộng rái trong các ứng dụng IoT ngày nay.
Để tìm hiểu sâu hơn về gRPC ta cũng nên tìm hiểu thêm các vấn đề về
- Protobuf -> định nghĩa cấu trúc message
- Kiến trúc: Sub, Channel, Transport
- Giao thức truyền dữ liệu
- Yếu tố ảnh hưởng đến vòng đời của một gRPC
Sau đây là cách triển khai chi tiết
Hệ thống có hai microservices được deploye lên k8s
- organizatin-service và
- task-services
organizatin-service chứa thông tin cấu hình của tổ chức: countryCode, timezone, currency
Để tính toán các giá trị nghiệp vụ của của task-services cho một tổ chức ta cần có thông tin cầu hình của tổ chức đó.
Có nhiều cách làm, gọi qua internal APIs cũng là một cách. Để tận dụng tính năng độ trễ thấp của gRPC ta có thể nghĩ đến việc áp dụng nó.
Cấu hình các plugin và dependency
plugin
id 'com.google.protobuf' version '0.9.4'gencode
protobuf {
protoc {
artifact = "com.google.protobuf:protoc:${protobufVersion}"
}
plugins {
grpc {
artifact = "io.grpc:protoc-gen-grpc-java:${grpcVersion}"
}
}
generateProtoTasks {
all().each { task ->
task.plugins {
grpc {}
}
}
}
}dependency cho gRPC
// gRPC server (inter-service locale lookups)
implementation platform("io.grpc:grpc-bom:${grpcVersion}")
implementation platform("com.google.protobuf:protobuf-bom:${protobufVersion}")
implementation "net.devh:grpc-server-spring-boot-starter:${grpcSpringBootVersion}"
implementation 'io.grpc:grpc-protobuf'
implementation 'io.grpc:grpc-stub'
implementation 'com.google.protobuf:protobuf-java'
runtimeOnly 'io.grpc:grpc-netty-shaded'
// generated gRPC stubs use @javax.annotation.Generated, which is absent from JDK 21
compileOnly 'javax.annotation:javax.annotation-api:1.3.2'Trong phần trên ta để ý
Trong phần trên ta để ý
implementation “net.devh:grpc-server-spring-boot-starter:${grpcSpringBootVersion}”
Đây là thư viện khởi chạy grpc độc lập với cổng của http, cấu hình cổng này sẽ thông qua
# gRPC server (inter-service API, separate port from HTTP)
grpc.server.port: ${GRPC_SERVER_PORT:9090}Lưu ý các version khi làm việc với Spring Boot 3, các version khác có thể phải cập nhật
ext {
set('springCloudVersion', "2025.0.0")
set('grpcVersion', "1.75.0")
set('protobufVersion', "3.25.5")
set('grpcSpringBootVersion', "3.1.0.RELEASE")
}Tiếp theo ta cần định nghĩa cấu trúc dữ liệu và các service mà hệ thống hỗ trợ
File organization_locale.proto
syntax = "proto3";
package etask.notification.locale.v1;
option java_multiple_files = true;
option java_package = "com.etask.notification.grpc.locale.v1";
option java_outer_classname = "OrganizationLocaleProto";
// Locale settings (countryCode, timeZone, currency) of an organization.
// Mirrors GET /internal/v1/organizations/{organizationId}/locale.
service OrganizationLocaleService {
// Returns NOT_FOUND when the organization is not registered (REST: 404).
rpc GetLocale (GetLocaleRequest) returns (GetLocaleResponse);
}
message GetLocaleRequest {
string organization_id = 1;
}
message GetLocaleResponse {
string country_code = 1;
string time_zone = 2;
string currency = 3;
}Khi tiến hành build app, gRPC plugin sẽ tiến hành generate tại build/generated/source/proto
Lưu ý mặc định file proto nằm ở thư mục src/main/prote trong cấu hình plugin của build.gradle ta có thể thay đổi
Starter (grpc-server-spring-boot-starter) sẽ tìm kiếm các @GrpcService beans chính là các implement service ImplBase/BindableService được sinh ra tại code generated.
Đây là đoạn code mẫu, ta không cần đi chi tiết cách để lấy dữ liệu của cấu hình tổ chức, để ý đến cách implement và cấu trúc của file.
Hàm trả về void. nhưng StreamObserver responseObserver là dữ liệu trả về cho hàm gọi gRPC.
Ta có thể thấy cách implement timeout, cách lỗi trả về khi dữ liệu không đầy đủ.
package com.etask.notification.domain.organization.application;
import com.etask.notification.domain.organization.infrastructure.cache.OrganizationLocaleCache;
import com.etask.notification.domain.organization.infrastructure.cache.OrganizationLocaleCache.OrganizationLocale;
import com.etask.notification.grpc.locale.v1.GetLocaleRequest;
import com.etask.notification.grpc.locale.v1.GetLocaleResponse;
import com.etask.notification.grpc.locale.v1.OrganizationLocaleServiceGrpc;
import io.grpc.Status;
import io.grpc.stub.StreamObserver;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import net.devh.boot.grpc.server.service.GrpcService;
import java.time.Duration;
/**
* gRPC counterpart of {@link InternalOrganizationLocaleController}.
* Reads from the same Caffeine cache. Runs on the gRPC executor, so blocking on the reactive lookup is safe.
*/
@Slf4j
@GrpcService
@RequiredArgsConstructor
public class OrganizationLocaleGrpcService extends OrganizationLocaleServiceGrpc.OrganizationLocaleServiceImplBase {
private static final Duration LOOKUP_TIMEOUT = Duration.ofSeconds(5);
private final OrganizationLocaleCache localeCache;
@Override
public void getLocale(GetLocaleRequest request, StreamObserver<GetLocaleResponse> responseObserver) {
String organizationId = request.getOrganizationId();
log.info("gRPC GetLocale request received for org {}", organizationId);
if (organizationId.isBlank()) {
log.warn("gRPC GetLocale rejected: organization_id is blank");
responseObserver.onError(Status.INVALID_ARGUMENT
.withDescription("organization_id is required")
.asRuntimeException());
return;
}
try {
OrganizationLocale locale = localeCache.get(organizationId).block(LOOKUP_TIMEOUT);
if (locale == null) {
log.warn("gRPC GetLocale: org {} not registered", organizationId);
responseObserver.onError(Status.NOT_FOUND
.withDescription("Organization not registered: " + organizationId)
.asRuntimeException());
return;
}
responseObserver.onNext(GetLocaleResponse.newBuilder()
.setCountryCode(locale.countryCode())
.setTimeZone(locale.timeZone())
.setCurrency(locale.currency())
.build());
log.info("gRPC GetLocale success for org {}: countryCode={}, timeZone={}, currency={}",
organizationId, locale.countryCode(), locale.timeZone(), locale.currency());
responseObserver.onCompleted();
} catch (Exception e) {
log.error("gRPC GetLocale failed for org {}", organizationId, e);
responseObserver.onError(Status.INTERNAL
.withDescription("Failed to load locale")
.withCause(e)
.asRuntimeException());
}
}
}OrganizationLocaleGrpcService ta đã implement xong một gRPC và có thể gọi qua các service trong namespace của K8s.
task-service muốn gọi đến organization sẽ làm cách nào
1. Copy file organization_locale.proto
Copy src/main/proto/organization_locale.proto tới thư mục src/main/proto/ của task-service
2. Thêm dependencies
plugins {
id 'com.google.protobuf' version '0.9.4'
}
dependencies {
implementation 'net.devh:grpc-client-spring-boot-starter:3.1.0.RELEASE'
implementation 'io.grpc:grpc-netty-shaded:1.75.0'
implementation 'io.grpc:grpc-protobuf:1.75.0'
implementation 'io.grpc:grpc-stub:1.75.0'
implementation 'com.google.protobuf:protobuf-java:3.25.5'
implementation 'jakarta.annotation:jakarta.annotation-api:2.1.1'
}
protobuf {
protoc { artifact = 'com.google.protobuf:protoc:3.25.5' }
plugins {
grpc { artifact = 'io.grpc:protoc-gen-grpc-java:1.75.0' }
}
generateProtoTasks {
all()*.plugins { grpc {} }
}
}3. Cấu hình địa chỉ của organization-service
Ở đây ta cấu hình etask-org-active.etask.svc.cluster.local do các dịch vụ đang chạy trong K8s
grpc:
client:
oranization-locale:
address: static://etask-org-active.etask.svc.cluster.local:9090
negotiationType: PLAINTEXT4. Tạo client service để gọi
@Service
public class LocaleClient {
@GrpcClient("oranization-locale")
private OrganizationLocaleServiceGrpc.OrganizationLocaleServiceBlockingStub localeStub;
public GetLocaleResponse getLocale(String organizationId) {
return localeStub.withDeadlineAfter(3, TimeUnit.SECONDS)
.getLocale(GetLocaleRequest.newBuilder()
.setOrganizationId(organizationId)
.build());
}
}Ta có thể public organization-service để có thể test trên Postman
gRPC Locale là một service đang chạy trong cụm K8s do vậy ta cần forward dịch vụ này ra ngoài
kubectl port-forward svc/etask-org-active 9090:9090 -n etaskTrên giao diện của Postman ta chọn import a .proto file
URL test là localhost:9090
Body
{
"organization_id": "323d1cec-34f2-45af-8942-2347a5d27xxx"
}Response
{
"country_code": "GB",
"time_zone": "Europe/London",
"currency": "GBP"
}Qua bài này chúng ta đã thực hiện xong việc tạo một gRPC service trong một Spring Boot project, hiểu cách để một service gọi đến một gRPC server trong cùng một cụm kuberneste và cũng hiểu cách để public một service để có thể thực hiện test thông qua Postman.

Để lại một bình luận