Загрузка
Все статьи

gRPC vs REST — что выбрать для межсервисного взаимодействия на Kotlin

REST — привычный, универсальный, работает через JSON. gRPC — бинарный протокол, контракты через Protobuf, стриминг из коробки. На реальном проекте с 12 микросервисами сравниваю latency, размер полезной нагрузки, DX и почему выбор протокола зависит не только от производительности.

Когда микросервисов больше пяти, межсервисное общение становится архитектурной проблемой. REST/HTTP с JSON — стандарт de facto, но gRPC активно набирает популярность. Оба подхода решают одну задачу — один сервис должен вызвать другой. Но философия, производительность и developer experience у них совершенно разные.

На нашем проекте — маркетплейс с 12 микросервисами, 3 млн запросов в час между сервисами — мы запустили оба протокола параллельно и сравнили. Вот результаты.

REST + JSON: привычный путь

REST (REpresentational State Transfer) — архитектурный стиль, который стал синонимом HTTP API. JSON — формат данных по умолчанию. Каждый микросервис предоставляет HTTP-эндпоинты, другие сервисы вызывают их через HTTP-клиент.

// REST endpoint (Ktor)
routing {
    route("/api/v1/orders") {
        get("/{id}") {
            val id = call.parameters["id"]?.toLongOrNull()
                ?: return@get call.respond(HttpStatusCode.BadRequest)
            val order = orderService.findById(id)
                ?: return@get call.respond(HttpStatusCode.NotFound)
            call.respond(OrderDto.fromDomain(order))
        }

        post("/{id}/items") {
            val id = call.parameters["id"]?.toLongOrNull()!!
            val request = call.receive<AddItemRequest>()
            val updated = orderService.addItem(id, request)
            call.respond(HttpStatusCode.OK, OrderDto.fromDomain(updated))
        }
    }
}

// HTTP-клиент (в другом микросервисе)
class OrderServiceClient(private val httpClient: HttpClient) {
    suspend fun getOrder(orderId: Long): OrderDto {
        val response = httpClient.get("http://order-service:8080/api/v1/orders/$orderId")
        if (response.status != HttpStatusCode.OK) {
            throw OrderServiceException("Order $orderId not found")
        }
        return response.body<OrderDto>()
    }

    suspend fun addItem(orderId: Long, request: AddItemRequest): OrderDto {
        val response = httpClient.post("http://order-service:8080/api/v1/orders/$orderId/items") {
            contentType(ContentType.Application.Json)
            setBody(request)
        }
        if (response.status != HttpStatusCode.OK) {
            throw OrderServiceException("Failed to add item: ${response.status}")
        }
        return response.body<OrderDto>()
    }
}

REST прост в понимании и отладке. HTTP-запрос можно прочитать в Wireshark, Chrome DevTools, curl. JSON — человекочитаемый формат. Любой инструмент может вызвать REST API — от браузера до Postman и bash-скрипта.

Минусы REST: JSON — текстовый формат, избыточный (каждый ключ повторяется, нет схемы типов). Нет обязательного контракта между клиентом и сервером — контракт — это «документация», которая часто устаревает. HTTP/1.1 — одно соединение = один запрос (мультиплексирование только в HTTP/2). Нет стриминга «из коробки» (SSE работает, но ограниченно).

gRPC + Protobuf: контрактный подход

gRPC — фреймворк от Google, использующий Protocol Buffers (Protobuf) для описания контракта и HTTP/2 для передачи. Контракт определяется в .proto-файле, из которого генерируются клиентские и серверные классы.

// order_service.proto
syntax = "proto3";

package com.example.orderservice;

option java_multiple_files = true;
option java_package = "com.example.orderservice.grpc";

service OrderService {
  rpc GetOrder(GetOrderRequest) returns (OrderResponse);
  rpc AddItem(AddItemRequest) returns (OrderResponse);
  rpc StreamOrderUpdates(StreamOrdersRequest) returns (stream OrderUpdate);
}

message GetOrderRequest {
  int64 id = 1;
}

message OrderResponse {
  int64 id = 1;
  repeated OrderItem items = 2;
  string status = 3;
  double total_amount = 4;
  string created_at = 5;
}

message OrderItem {
  string product_id = 1;
  int32 quantity = 2;
  double unit_price = 3;
}

message AddItemRequest {
  int64 order_id = 1;
  string product_id = 2;
  int32 quantity = 3;
}

message StreamOrdersRequest {
  repeated int64 order_ids = 1;
}

message OrderUpdate {
  int64 order_id = 1;
  string event_type = 2; // CREATED, UPDATED, CANCELLED
  OrderResponse order = 3;
  int64 timestamp = 4;
}
// gRPC-сервер (Ktor + grpc-kotlin)
class OrderGrpcService(
    private val orderService: OrderService
) : OrderServiceGrpcKt.OrderServiceCoroutineImplBase() {

    override suspend fun getOrder(request: GetOrderRequest): OrderResponse {
        val order = orderService.findById(request.id)
            ?: throw StatusException(Status.NOT_FOUND.withDescription("Order ${request.id} not found"))
        return order.toGrpcResponse()
    }

    override suspend fun addItem(request: AddItemRequest): OrderResponse {
        val updated = orderService.addItem(
            orderId = request.orderId,
            productId = request.productId,
            quantity = request.quantity
        )
        return updated.toGrpcResponse()
    }

    override fun streamOrderUpdates(request: StreamOrdersRequest): Flow<OrderUpdate> {
        return orderService.subscribeToUpdates(request.orderIdsList)
            .map { it.toGrpcUpdate() }
    }
}

// gRPC-клиент
class OrderServiceGrpcClient(private val channel: ManagedChannel) {
    private val stub = OrderServiceGrpcKt.OrderServiceCoroutineStub(channel)

    suspend fun getOrder(orderId: Long): OrderResponse {
        return stub.getOrder(GetOrderRequest.newBuilder().setId(orderId).build())
    }

    suspend fun addItem(orderId: Long, productId: String, quantity: Int): OrderResponse {
        return stub.addItem(AddItemRequest.newBuilder()
            .setOrderId(orderId)
            .setProductId(productId)
            .setQuantity(quantity)
            .build())
    }

    fun streamUpdates(orderIds: List<Long>): Flow<OrderUpdate> {
        val request = StreamOrdersRequest.newBuilder()
            .addAllOrderIds(orderIds)
            .build()
        return stub.streamOrderUpdates(request)
    }
}

gRPC генерирует типобезопасные клиентские и серверные классы из .proto-файла. Изменили контракт — код не скомпилируется. Это надёжнее, чем REST API со спецификацией OpenAPI, которую нужно вручную поддерживать.

Protobuf — бинарный формат, в 3–10 раз меньше JSON. HTTP/2 даёт мультиплексирование (одно TCP-соединение, множество потоков), сжатие заголовков (HPACK) и двунаправленный стриминг. Это особенно важно при высокочастотном межсервисном общении.

Производительность: реальные цифры

Замеры для OrderResponse с 10 позициями (типичный ответ сервиса заказов):

Метрика                    | REST + JSON   | gRPC + Protobuf
---------------------------|---------------|----------------
Размер полезной нагрузки   |    1840 Б     |     420 Б
Сериализация               |    0,12 мс    |   0,03 мс
Десериализация             |    0,18 мс    |   0,02 мс
Latency (внутри кластера)  |    2,4 мс     |   0,8 мс
Пропускная способность     |    3 200 rps  |  12 000 rps
(одно соединение)          |               |
P99 latency (100 соединений)|    8 мс      |   1,5 мс
Загрузка CPU               |    35%        |   12%
(десериализация)           |               |
Выделение памяти           |   45 КБ/запрос|   8 КБ/запрос

gRPC в 4 раза быстрее по latency и в 3 раза меньше по размеру полезной нагрузки. Сериализация Protobuf в 6 раз быстрее JSON. Для высокочастотного межсервисного общения (десятки тысяч запросов в секунду между сервисами) разница колоссальная.

Но есть нюанс: если вызов сервиса происходит раз в секунду, а сама бизнес-логика занимает 200 мс, разница между 2,4 мс и 0,8 мс latency незаметна. gRPC выигрывает при высокочастотном общении; при редких вызовах разница нивелируется.

Стриминг

Область, где gRPC безоговорочно выигрывает, — стриминг. gRPC поддерживает 4 режима:

Режим                 | REST                    | gRPC
----------------------|-------------------------|------------------
Unary (1 запрос)      | HTTP GET/POST           | Unary RPC
Server streaming      | SSE (ограниченно)        | server streaming RPC
Client streaming      | Нет                     | client streaming RPC
Bidirectional         | WebSocket (работает)     | bidirectional RPC

Real-time обновления заказов: вместо опроса (каждые 5 секунд REST-запрос за статусом) — server streaming в gRPC. Клиент подписывается на обновления и получает уведомления мгновенно. На продакшене это сократило количество межсервисных запросов на 60%.

Отладка и наблюдаемость

REST проще отлаживать: curl, Postman, Chrome DevTools — всё «просто работает». JSON читаемый, HTTP-статусы стандартные. gRPC бинарный — нужен gRPCurl или gRPC reflection для отладки. Логирование gRPC-запросов требует интерсепторов, текст логов содержит бинарно-закодированные данные.

// Интерсептор для логирования gRPC
class LoggingInterceptor : ServerInterceptor {
    private val logger = KotlinLogging.logger {}

    override fun <ReqT : Any?, RespT : Any?> interceptCall(
        call: ServerCall<ReqT, RespT>,
        headers: Metadata,
        next: ServerCallHandler<ReqT, RespT>
    ): ServerCall.Listener<ReqT> {
        val method = call.methodDescriptor.fullMethodName
        logger.info { "gRPC request: $method from ${call.attributes[Grpc.TRANSPORT_ATTR_REMOTE_ADDR]}" }
        return next.startCall(object : ForwardingServerCall.SimpleForwardingServerCall<ReqT, RespT>(call) {
            override fun close(status: Status, trailers: Metadata) {
                logger.info { "gRPC response: $method -> ${status.code} (${status.description ?: ""})" }
                super.close(status, trailers)
            }
        }, headers)
    }
}

Распределённая трассировка (Jaeger, Zipkin, OpenTelemetry) работает с обоими протоколами, но интеграция с gRPC сложнее — нужно передавать контекст трассировки через metadata gRPC (заголовки), а не через стандартные HTTP-заголовки.

Gateway: REST для внешних, gRPC для внутренних

На продакшене мы используем гибридный подход: внешние клиенты (frontend, mobile) общаются через REST/JSON, внутренние сервисы — через gRPC. API Gateway (Kong или Envoy) преобразует REST → gRPC и обратно.

Frontend  ──REST/JSON──>  API Gateway  ──gRPC──>  Order Service
Mobile   ──REST/JSON──>  (Envoy)      ──gRPC──>  Product Service
3rd Party ─REST/JSON──>               ──gRPC──>  Payment Service
                                        ──gRPC──>  Notification Service

Envoy + трансодирование gRPC-HTTP/1.1 — стандартный паттерн. Это даёт лучшее из обоих миров: фронтенд видит привычный REST API, а внутренние сервисы общаются через быстрый бинарный протокол.

Мой вердикт

Для внешних API (фронтенд, мобильные приложения, сторонние сервисы) — REST. Человекочитаемость, простота отладки, универсальность, HTTP-кэширование. Не заставляйте фронтенд-разработчиков устанавливать protobuf-компилятор.

Для внутреннего общения между микросервисами — gRPC. Контракт, производительность, стриминг, типобезопасность. На 10+ сервисах разница в задержках и пропускной способности ощущается.

Если у вас 2–3 микросервиса с низкой частотой общения — REST достаточен. Не усложняйте архитектуру gRPC ради «потенциальной» производительности. Добавляйте gRPC там, где есть реальная потребность: высокочастотные вызовы, стриминг, подход «контракт прежде всего».

Поделиться статьёй

Комментарии

0
0 / 2000

Пока нет комментариев

Будьте первым, кто оставит комментарий к этой статье

Читайте также

Micronaut vs Spring Boot — лёгкий фреймворк для Kotlin-микросервисов

Сравниваю Micronaut и Spring Boot для Kotlin-микросервисов: стартовое время, потребление памяти, холодный старт и реальный опыт миграции одного из сервисов.

12 июня 2026 11 мин

Kotlin: sealed interfaces и when-выражения — паттерн-матчинг как в Rust

Как sealed interfaces из Kotlin 1.5+ вместе с when-выражениями дают мощный паттерн-матчинг, похожий на Rust enum+match, и почему мы перешли на ADT для доменных событий.

11 июня 2026 9 мин

Kotlinx Serialization + Protobuf — быстрая сериализация для микросервисов

Разбираюсь с накладными расходами JSON-сериализации при высоких нагрузках и показываю, как интегрировать Protobuf с kotlinx-serialization в микросервисной архитектуре на Kotlin.

10 июня 2026 10 мин