Почему современные микросервисы предпочитают gRPC REST

Почему современные микросервисы предпочитают 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, используя три ключевые технологии:

3 архитектурных столпа gRPC: буферы протоколов, транспорт HTTP/2 и генерация кода

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 является то, что он генерирует для клиента два типа заглушек:

  1. BlockingStub: синхронное, блокирующее выполнение (аналогично стандартным вызовам REST).
  2. Заглушка: асинхронное, неблокирующее выполнение с использованием наблюдателей обратного вызова.
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 →