Почему современные микросервисы предпочитают gRPC REST
В монолитной архитектуре компоненты взаимодействуют посредством вызовов методов в памяти, которые являются мгновенными и очень надежными. Однако при переходе на архитектуру микросервисов эти компоненты разделяются сетевыми границами. Связь становится внепроцессным сетевым вызовом (межпроцессная связь или IPC).
В течение многих лет REST (передача репрезентативного состояния) через HTTP/1.1 с полезными нагрузками JSON был стандартом по умолчанию для создания веб-API. Хотя REST отлично подходит для общедоступных веб-сервисов и взаимодействия между клиентом и сервером, он создает серьезные узкие места при использовании для высокочастотного внутреннего взаимодействия между сервисами с низкой задержкой.
Вот почему современные микросервисы быстро переходят на gRPC (вызов удаленных процедур Google). В этой статье мы проанализируем ограничения REST, разберем основы архитектуры gRPC и рассмотрим полную реализацию службы gRPC на Java.
1. Узкие места REST в микросервисах
REST поддерживает Интернет уже более двух десятилетий. Однако его базовые технологии не оптимизированы для внутренних распределенных архитектур:
A. Накладные расходы при использовании обычного текста JSON
JSON удобен для чтения человеком, что упрощает отладку, но он крайне неэффективен для межмашинного взаимодействия:
- Стоимость сериализации/десериализации: анализ токенов текстовых строк и преобразование их в объекты потребляют значительные ресурсы процессора.
- Большой размер полезных данных: ключи JSON повторяются в каждом отдельном запросе (например,
{"transactionId": "123", "amount": 99.99}). Для систем, обрабатывающих миллионы запросов в секунду, эти избыточные метаданные тратят огромную пропускную способность сети.
B. Ограничения соединений HTTP/1.1
REST обычно работает по протоколу HTTP/1.1, который демонстрирует несколько структурных недостатков:
- Блокировка заголовка линии (HoL): при одном TCP-соединении клиент должен дождаться ответа на текущий запрос, прежде чем отправлять следующий.
- Исчерпание пула соединений: для обработки одновременных запросов клиенты должны открывать несколько TCP-соединений. Это приводит к значительным издержкам ресурсов ОС, исчерпанию сокетов и снижению производительности при медленном запуске, поскольку соединения постоянно открываются и закрываются.
C. Слабые контракты API
В API REST отсутствует встроенный контракт времени компиляции. Хотя OpenAPI/Swagger помогает документировать API, они отделены от базы кода. Разработчик серверной части может легко изменить имя поля JSON и случайно нарушить работу последующих служб без предупреждений во время компиляции.
2. Архитектурные основы gRPC
gRPC, представленный Google в 2015 году, представляет собой высокопроизводительную платформу RPC с открытым исходным кодом, разработанную специально для облачных приложений. Он устраняет узкие места REST, используя три ключевые технологии:
A. Буферы протоколов (Protobuf)
Вместо JSON gRPC использует буферы протокола в качестве языка определения интерфейса (IDL) и формата сериализации сообщений.
- Protobuf — это высокооптимизированный механизм двоичной сериализации.
- Поля сериализуются как небольшие числовые теги, а не как текстовые имена.
- Полезные данные на 70–80 % меньше, чем в JSON, а сериализация происходит до 10 раз быстрее, что значительно снижает нагрузку на ЦП и полосу пропускания.
B. Мультиплексирование HTTP/2
gRPC использует HTTP/2 в качестве транспортного протокола, что обеспечивает несколько оптимизаций производительности:
- Истинное мультиплексирование: несколько запросов и ответов могут чередоваться одновременно по одному долгоживущему TCP-соединению, что полностью устраняет блокировку начала линии.
- Сжатие заголовков (HPACK): заголовки HTTP сжимаются, что еще больше уменьшает размер запроса.
- Поддержка потоковой передачи: HTTP/2 изначально поддерживает однонаправленную потоковую передачу с клиента, потоковую передачу с сервера и полную двунаправленную потоковую передачу.
C. Генерация кода и строгие контракты
Определив структуру API в файле .proto, gRPC генерирует клиентские заглушки и базовые классы сервера на десятках языков (Java, Go, Python, C++ и т. д.). Контракт безопасен во время компиляции; если сервер обновляет свой интерфейс, клиентские сборки немедленно завершаются неудачей, если они не соответствуют новому контракту.
3. Реализация Java: создание службы gRPC
Давайте создадим практическое приложение Java gRPC. Мы внедрим высокопроизводительную Службу обработки платежей, где клиент отправляет запрос на оплату и получает подтверждение транзакции.
Шаг 1. Определите контракт API (payment.proto)
Мы начнем с определения нашего сервисного интерфейса и структуры сообщений в файле буфера протокола.
syntax = "proto3";
option java_multiple_files = true;
option java_package = "com.ghaznix.grpc.payment";
option java_outer_classname = "PaymentProto";
package payment;
// The Payment Service definition
service PaymentService {
// Unary RPC: Sends a single payment request and receives a confirmation
rpc ProcessPayment (PaymentRequest) returns (PaymentResponse);
}
// The Payment Request message
message PaymentRequest {
string transaction_id = 1;
string customer_id = 2;
double amount = 3;
string currency = 4;
}
// The Payment Response message
message PaymentResponse {
string transaction_id = 1;
string status = 2;
string message = 3;
string timestamp = 4;
}
Шаг 2. Реализация серверной логики
После компиляции файла .proto (обычно автоматически обрабатываемого плагинами gRPC Maven или Gradle) инструмент генерирует класс PaymentServiceImplBase. Мы расширяем этот класс для реализации бизнес-логики.
package com.ghaznix.grpc.payment;
import io.grpc.stub.StreamObserver;
import java.time.Instant;
public class PaymentServiceImpl extends PaymentServiceGrpc.PaymentServiceImplBase {
@Override
public void processPayment(PaymentRequest request, StreamObserver<PaymentResponse> responseObserver) {
System.out.println("Received payment request for customer: " + request.getCustomerId()
+ " with amount: " + request.getAmount() + " " + request.getCurrency());
// Process payment logic (simulation)
String status = request.getAmount() > 0 ? "SUCCESS" : "DECLINED";
String message = status.equals("SUCCESS") ? "Payment processed successfully." : "Invalid payment amount.";
PaymentResponse response = PaymentResponse.newBuilder()
.setTransactionId(request.getTransactionId())
.setStatus(status)
.setMessage(message)
.setTimestamp(Instant.now().toString())
.build();
// Send the response to the client
responseObserver.onNext(response);
// Signal that the RPC execution is complete
responseObserver.onCompleted();
}
}
Шаг 3. Загрузите сервер gRPC
Затем мы создаем класс сервера для запуска сервера gRPC на определенном порту и регистрируем реализацию нашей службы.
package com.ghaznix.grpc.payment;
import io.grpc.Server;
import io.grpc.ServerBuilder;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
public class PaymentServer {
private Server server;
public void start() throws IOException {
int port = 50051;
server = ServerBuilder.forPort(port)
.addService(new PaymentServiceImpl())
.build()
.start();
System.out.println("gRPC Server started, listening on port " + port);
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
System.err.println("*** Shutting down gRPC server since JVM is shutting down");
try {
PaymentServer.this.stop();
} catch (InterruptedException e) {
e.printStackTrace(System.err);
}
System.err.println("*** Server shut down");
}));
}
public void stop() throws InterruptedException {
if (server != null) {
server.shutdown().awaitTermination(30, TimeUnit.SECONDS);
}
}
public void blockUntilShutdown() throws InterruptedException {
if (server != null) {
server.awaitTermination();
}
}
public static void main(String[] args) throws IOException, InterruptedException {
final PaymentServer server = new PaymentServer();
server.start();
server.blockUntilShutdown();
}
}
Шаг 4. Реализация клиента (блокировка и асинхронные заглушки)
Основным преимуществом gRPC является то, что он генерирует для клиента два типа заглушек:
- BlockingStub: синхронное, блокирующее выполнение (аналогично стандартным вызовам REST).
- Заглушка: асинхронное, неблокирующее выполнение с использованием наблюдателей обратного вызова.
package com.ghaznix.grpc.payment;
import io.grpc.ManagedChannel;
import io.grpc.ManagedChannelBuilder;
import java.util.concurrent.TimeUnit;
public class PaymentClient {
private final ManagedChannel channel;
private final PaymentServiceGrpc.PaymentServiceBlockingStub blockingStub;
public PaymentClient(String host, int port) {
// Create a communication channel to the server
this.channel = ManagedChannelBuilder.forAddress(host, port)
.usePlaintext() // Disables TLS for local development
.build();
// Create the synchronous blocking stub
this.blockingStub = PaymentServiceGrpc.newBlockingStub(channel);
}
public void shutdown() throws InterruptedException {
channel.shutdown().awaitTermination(5, TimeUnit.SECONDS);
}
public void executePayment(String txId, String customerId, double amount) {
PaymentRequest request = PaymentRequest.newBuilder()
.setTransactionId(txId)
.setCustomerId(customerId)
.setAmount(amount)
.setCurrency("USD")
.build();
System.out.println("Sending payment request for: " + txId);
// Execute unary synchronous call
PaymentResponse response = blockingStub.processPayment(request);
System.out.println("Server Response: " + response.getStatus() + " | " + response.getMessage());
}
public static void main(String[] args) throws InterruptedException {
PaymentClient client = new PaymentClient("localhost", 50051);
try {
client.executePayment("TX-99081", "customer-abc-123", 250.75);
} finally {
client.shutdown();
}
}
}
4. Сравнение архитектур: gRPC и REST
В таблице ниже представлены основные характеристики обеих парадигм:
| Архитектурная особенность | REST (Представительская государственная передача) | gRPC (вызов удаленных процедур Google) |
|---|---|---|
| Протокол | HTTP/1.1 (стандартно), HTTP/2 (дополнительно) | HTTP/2 (строгое требование) |
| Формат полезной нагрузки | Обычный текст JSON, XML, HTML | Буферы двоичных протоколов (Protobuf) |
| Парадигма API | Ресурсы (пути URI + GET/POST/PUT/DELETE) | Удаленные процедуры (вызовы методов/функций) |
| Контрактное качество | Свободно (OpenAPI/Swagger является необязательным/внешним) | Строгий (определяется в файлах .proto при компиляции) |
| Мультиплексирование | Нет (блокировка заголовка строки в HTTP/1.1) | Да (несколько потоков в одном TCP-соединении) |
| Типы потоковой передачи | Только однонаправленные события, отправленные сервером (SSE) | Клиент, сервер и двунаправленная потоковая передача |
| Генерация кода | Необходимы внешние инструменты (например, Swagger Codegen) | Встроено через компилятор Protobuf (protoc) |
| Поддержка браузера | Универсальный (Прямой доступ к веб-клиенту) | Ограничено (требуется перевод прокси grpc-web) |
5. Когда лучше выбирать gRPC вместо REST?
Несмотря на огромные преимущества в производительности gRPC, это не панацея. Выбор протокола зависит от контекста внутри вашей системной архитектуры:
- Используйте gRPC для внутренней микросервисной сетки. Для межсервисного взаимодействия больших объемов данных низкая задержка, двоичная компактность и возможности потоковой передачи делают gRPC лучшим выбором. Это снижает нагрузку на внутренний процессор сервера и ускоряет общее время отклика.
- Используйте REST для Edge/публичных API. Поскольку веб-браузеры изначально не могут выполнять вызовы gRPC без слоев трансляции, REST остается лучшим выбором для общедоступных API, интеграции со сторонними веб-перехватчиками и прямого взаимодействия клиентских браузеров с периферией вашей архитектуры.
Объединив эти два фактора — предоставление REST на уровне API-шлюза и использование gRPC для внутренней связи микросервисов — архитекторы могут создавать системы, которые обладают высокой функциональной совместимостью и невероятно быстрыми действиями.
Узнайте больше о разработке программного обеспечения и серверной части в блоге Ghaznix →