はじめに
gRPC (Google リモート プロシージャ コール) は、バイナリのシリアル化に プロトコル バッファ (Protobuf) を使用し、トランスポートに HTTP/2 を使用します。 REST/JSON と比較して、gRPC はシリアル化と帯域幅の点で 5 ~ 10 倍高速です。 Quarkus は、gRPC と自動コード生成を統合します。 .proto ファイル。
REST の代わりに gRPC を使用するのはどのような場合ですか?
| 基準 | REST/JSON | gRPC/プロトバッファ |
|---|---|---|
| パフォーマンス | 遅い (テキストベース) | 5 ~ 10 倍高速 |
| ペイロードサイズ | 大 (JSON 冗長) | 小 (バイナリ) |
| 契約 | OpenAPI (オプション) | プロトバッファ (必須) |
| ストリーミング | WebSocket/SSE | 内蔵(4パターン)| |
| ブラウザのサポート | ネイティブ | gRPC-Web プロキシが必要 |
| デバッグ | 簡単(カール、ポストマン) | 難しい(別途ツールが必要) |
| 適切 | パブリック API、Web アプリ | 内部サービス |
電子商取引プロジェクトの場合: 内部サービス間では gRPC、外部/フロントエンドでは REST。
Quarkus で gRPC をセットアップする
依存関係
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-grpc</artifactId>
</dependency>
プロトバッファの定義
// src/main/proto/product_service.proto
syntax = "proto3";
option java_multiple_files = true;
option java_package = "com.xdev.ecommerce.product.grpc";
package product;
service ProductGrpcService {
// Unary RPC
rpc GetProduct (GetProductRequest) returns (ProductResponse);
rpc CheckStock (CheckStockRequest) returns (StockResponse);
rpc ReserveStock (ReserveStockRequest) returns (StockResponse);
// Server Streaming
rpc ListProducts (ListProductsRequest)
returns (stream ProductResponse);
// Client Streaming (batch import)
rpc BatchUpdateStock (stream UpdateStockRequest)
returns (BatchStockResponse);
}
message GetProductRequest {
int64 product_id = 1;
}
message ProductResponse {
int64 id = 1;
string name = 2;
string description = 3;
string price_amount = 4; // BigDecimal as string
string currency = 5;
int32 stock_available = 6;
string category_name = 7;
string status = 8;
}
message CheckStockRequest {
int64 product_id = 1;
int32 quantity = 2;
}
message StockResponse {
bool available = 1;
int32 current_stock = 2;
string message = 3;
}
message ReserveStockRequest {
int64 product_id = 1;
int32 quantity = 2;
string order_id = 3;
}
message ListProductsRequest {
string category = 1;
int32 page = 2;
int32 size = 3;
}
message UpdateStockRequest {
int64 product_id = 1;
int32 quantity_change = 2; // positive = add, negative = reduce
}
message BatchStockResponse {
int32 success_count = 1;
int32 failure_count = 2;
repeated string errors = 3;
}
gRPC サーバーの実装
import io.quarkus.grpc.GrpcService;
import io.smallrye.mutiny.Multi;
import io.smallrye.mutiny.Uni;
@GrpcService
public class ProductGrpcServiceImpl
implements ProductGrpcService {
@Inject
ProductRepository productRepo;
@Override
public Uni<ProductResponse> getProduct(
GetProductRequest request) {
return Uni.createFrom().item(() -> {
Product product = productRepo
.findByIdOptional(request.getProductId())
.orElseThrow(() -> new StatusRuntimeException(
Status.NOT_FOUND.withDescription(
"Product " + request.getProductId()
+ " not found")));
return toResponse(product);
});
}
@Override
public Uni<StockResponse> checkStock(
CheckStockRequest request) {
return Uni.createFrom().item(() -> {
Product product = productRepo
.findByIdOptional(request.getProductId())
.orElseThrow(() -> new StatusRuntimeException(
Status.NOT_FOUND));
boolean available =
product.stock.available() >= request.getQuantity();
return StockResponse.newBuilder()
.setAvailable(available)
.setCurrentStock(product.stock.available())
.setMessage(available
? "Stock available"
: "Insufficient stock")
.build();
});
}
@Override
@Transactional
public Uni<StockResponse> reserveStock(
ReserveStockRequest request) {
return Uni.createFrom().item(() -> {
Product product = productRepo
.findByIdOptional(request.getProductId())
.orElseThrow(() -> new StatusRuntimeException(
Status.NOT_FOUND));
product.reserveStock(request.getQuantity());
return StockResponse.newBuilder()
.setAvailable(true)
.setCurrentStock(product.stock.available())
.setMessage("Stock reserved for order "
+ request.getOrderId())
.build();
});
}
@Override
public Multi<ProductResponse> listProducts(
ListProductsRequest request) {
return Multi.createFrom().items(() -> {
var query = productRepo.findActive(
request.getCategory(), null,
Sort.by("createdAt").descending());
return query.page(Page.of(
request.getPage(), request.getSize()))
.list().stream()
.map(this::toResponse);
});
}
private ProductResponse toResponse(Product p) {
return ProductResponse.newBuilder()
.setId(p.id)
.setName(p.name)
.setDescription(
p.description != null ? p.description : "")
.setPriceAmount(
p.price != null
? p.price.amount().toPlainString() : "0")
.setCurrency(
p.price != null ? p.price.currency() : "VND")
.setStockAvailable(
p.stock != null ? p.stock.available() : 0)
.setCategoryName(
p.category != null ? p.category.name : "")
.setStatus(p.status)
.build();
}
}
gRPC サーバーを構成する
# gRPC server port
quarkus.grpc.server.port=9000
# Cùng host với REST (dev)
quarkus.grpc.server.use-separate-server=true
# TLS (production)
%prod.quarkus.grpc.server.ssl.certificate=server.crt
%prod.quarkus.grpc.server.ssl.key=server.key
オーダーサービスの gRPC クライアント
依存関係
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-grpc</artifactId>
</dependency>
プロトファイルをコピー
まとめる .proto ファイルに入れる order-service/src/main/proto/。
クライアント構成
# application.properties
quarkus.grpc.clients.product-grpc.host=localhost
quarkus.grpc.clients.product-grpc.port=9000
gRPC クライアントを使用する
import io.quarkus.grpc.GrpcClient;
@ApplicationScoped
public class OrderService {
@GrpcClient("product-grpc")
ProductGrpcService productGrpc;
@Transactional
public OrderDTO createOrder(String customerId,
CreateOrderRequest request) {
Order order = new Order();
order.customerId = customerId;
for (var item : request.items()) {
// gRPC call thay REST
ProductResponse product = productGrpc
.getProduct(GetProductRequest.newBuilder()
.setProductId(item.productId())
.build())
.await().indefinitely();
// Check stock qua gRPC
StockResponse stock = productGrpc
.checkStock(CheckStockRequest.newBuilder()
.setProductId(item.productId())
.setQuantity(item.quantity())
.build())
.await().indefinitely();
if (!stock.getAvailable()) {
throw new BusinessException(400,
stock.getMessage());
}
order.addItem(product.getId(), product.getName(),
Money.vnd(new BigDecimal(
product.getPriceAmount())),
item.quantity());
}
// Reserve stock
for (OrderItem oi : order.items) {
productGrpc.reserveStock(
ReserveStockRequest.newBuilder()
.setProductId(oi.productId)
.setQuantity(oi.quantity)
.setOrderId(order.orderNumber)
.build())
.await().indefinitely();
}
order.persist();
return OrderDTO.from(order);
}
}
gRPC 例外処理
import io.grpc.StatusRuntimeException;
import io.grpc.Status;
try {
ProductResponse product = productGrpc
.getProduct(request)
.await().indefinitely();
} catch (StatusRuntimeException e) {
switch (e.getStatus().getCode()) {
case NOT_FOUND ->
throw new ResourceNotFoundException(
"Product", productId);
case UNAVAILABLE ->
throw new ServiceUnavailableException(
"Product Service unavailable");
case DEADLINE_EXCEEDED ->
throw new BusinessException(504,
"Product Service timeout");
default ->
throw new RuntimeException(
"gRPC error: " + e.getStatus());
}
}
gRPC 期限 (タイムアウト)
import java.time.Duration;
import io.smallrye.mutiny.Uni;
ProductResponse product = productGrpc
.getProduct(request)
.ifNoItem().after(Duration.ofSeconds(5))
.fail()
.onFailure(TimeoutException.class)
.recoverWithItem(() -> {
// Fallback hoặc cached response
return ProductResponse.getDefaultInstance();
})
.await().indefinitely();
演習
1.作成 product_service.proto GetProduct、CheckStock、ReserveStock RPC を使用
2.実装する ProductGrpcServiceImpl 製品サービスで
3. Order Service に Product Service という名前の gRPC クライアントを作成します。
4. ListProducts のサーバー ストリーミングを実装する
5. ステータス コードを使用した gRPC 例外処理を追加します。
6. REST 呼び出しと gRPC 呼び出しの間の応答時間を比較します (ベンチマーク)
概要
- gRPC は Protobuf バイナリ シリアル化を使用します - REST/JSON より 5 ~ 10 倍高速です
.protofile — コントラクトファーストの自動コード生成@GrpcService— アノテーションサーバー実装@GrpcClient— gRPC クライアント スタブを挿入する- 4 パターン: 単項、サーバー ストリーミング、クライアント ストリーミング、双方向
- 内部通信には gRPC、外部/パブリック API には REST
次の記事: Apache Kafka — イベント駆動型アーキテクチャ。