У розподілених мікросервісних архітектурах мережевий транспорт безпосередньо визначає загальну пропускну здатність, споживання обчислювальних ресурсів (CPU/RAM) та затримку розповсюдження викликів (p99 latency). Традиційний підхід на базі REST/JSON через HTTP/1.1 має суттєві накладні витрати: текстова серіалізація вимагає парсингу рядків, імена полів дублюються в кожному корисному навантаженні, а протокол HTTP/1.1 змушує постійно відкривати нові TCP-сокети або керувати громіздкими пулами з'єднань.
Фреймворк gRPC (Google Remote Procedure Call) вирішує ці проблеми шляхом об'єднання бінарного протоколу серіалізації Protocol Buffers v3 та транспортного рівня HTTP/2.
Порівняння накладних витрат REST/JSON та gRPC/Protobuf у провідному форматі (Wire Format):
1. REST / JSON через HTTP/1.1:
[HTTP/1.1 Headers: ~500-800 байтів текстових метаданих CRLF]
{"userId": "usr_9921", "amount": 1500.50, "currency": "USD", "isExecuted": true}
- Розмір корисного навантаження: ~620 байтів (включаючи заголовки)
- Навантаження на CPU: Парсинг рядків, декодування UTF-8, інспекція типів у V8 runtime
2. gRPC / Protocol Buffers v3 через HTTP/2:
[HTTP/2 HPACK HEADERS Frame: ~12-25 байтів стиснутих індексів]
[5 байтів gRPC Frame Header: 1 байт Compressed Flag + 4 байти Message Length]
[Protobuf Binary Payload: 18 байтів]
08 eb 4d 11 00 00 00 00 00 d2 97 40 1a 03 55 53 44 20 01
- Розмір повідомлення: ~40 байтів (скорочення мережевого трафіку на 80-90%)
- Навантаження на CPU: Побітові зсуви та пряме читання типізованих буферів без алокації рядківProtocol Buffers не передає назви полів у мережевому пакеті. Кожне поле ідентифікується компактним цілочисельним номером тегу (Field Tag). У бінарному потоці дані пакуються в пари "ключ-значення" (Key-Value), де ключ є упакованим побітовим числом:
Key = (field_number << 3) | wire_typeСпецифікація визначає базові типи проводів (Wire Types):
Поля з номерами від 1 до 15 кодуються рівно в 1 байт (номер тегу плюс 3 біти типу). Саме тому найбільш навантажені поля в схемах .proto завжди нумеруються в діапазоні 1–15.
Кожен виклик віддаленої процедури в gRPC транслюється в окремий мультиплексований потік (Stream) усередині постійного TCP-з'єднання HTTP/2:
HEADERS із псевдозаголовками :method = POST, :path = /[package].[Service]/[Method], content-type = application/grpc, te = trailers.DATA. Перед кожним Protobuf-повідомленням додається 5-байтовий префікс кадру gRPC: 1 байт прапорця стиснення (Deflate/Gzip) та 4 байти довжини повідомлення у форматі Big-Endian.HEADERS із прапорцем END_STREAM, що містить HTTP-трейлери: grpc-status (цілочисельний код від 0 до 16, де 0 — OK) та grpc-message (діагностичний рядок опису помилки).gRPC трансформує розподілену мікросервісну топологію в строго типізоване середовище виконання віддалених процедур на базі контрактів, абстрагуючи низькорівневі мережеві сокети в локальні типізовані сигнатури функцій із наскрізною передачею контексту.
Protocol Buffers v3 (.proto) gRPC gRPC Deadline gRPC Metadata gRPC Interceptor Service Mesh4 типи патернів комунікації в gRPC:
1. Unary RPC (Одиночний запит - Одиночна відповідь):
Client ──[Request: UserRequest]──> Server
Client <──[Response: UserProfile]── Server
2. Server Streaming RPC (Одиночний запит - Потік відповідей):
Client ──[Request: SubscribeEvents]──> Server
Client <──[Stream: EventChunk 1]────── Server
Client <──[Stream: EventChunk 2]────── Server
Client <──[Stream: EventChunk N]────── Server
3. Client Streaming RPC (Потік запитів - Одиночна відповідь):
Client ──[Stream: MetricPoint 1]────> Server
Client ──[Stream: MetricPoint 2]────> Server
Client ──[Stream: MetricPoint N]────> Server
Client <──[Response: AggregationSummary]── Server
4. Bidirectional Streaming RPC (Двосторонній незалежний потік):
Client ──[Stream: AudioChunk 1]─────> Server
Client <──[Stream: SubtitleChunk 1]── Server
Client ──[Stream: AudioChunk 2]─────> Server
Client <──[Stream: SubtitleChunk 2]── Servertraceparent та жорстких дедлайнів (Deadlines) через ланцюжки з 5–10 мікросервісів для запобігання каскадним збоям та марним обчисленням.syntax = "proto3";
package billing.v1;
// Визначення сервісу білінгу
service BillingService {
// 1. Unary RPC: Створення платежу
rpc ProcessPayment (PaymentRequest) returns (PaymentResponse);
// 2. Server Streaming RPC: Отримання виписки транзакцій
rpc StreamTransactions (TransactionFilter) returns (stream Transaction);
// 3. Client Streaming RPC: Пакетне завантаження білінгових метрик
rpc IngestMetrics (stream MetricRecord) returns (IngestSummary);
// 4. Bidirectional Streaming: Інтерактивна звірка балансу в реальному часі
rpc LiveReconciliation (stream BalanceAuditRequest) returns (stream BalanceAuditResponse);
}
// Зарезервовані теги та ідентифікатори для безпечної еволюції схеми
message PaymentRequest {
reserved 4, 10 to 15;
reserved "internal_salt";
string account_id = 1;
double amount = 2;
string currency = 3;
PaymentMethod method = 5;
map<string, string> metadata = 6;
}
enum PaymentMethod {
PAYMENT_METHOD_UNSPECIFIED = 0;
PAYMENT_METHOD_CREDIT_CARD = 1;
PAYMENT_METHOD_BANK_TRANSFER = 2;
PAYMENT_METHOD_CRYPTO = 3;
}
message PaymentResponse {
string transaction_id = 1;
PaymentStatus status = 2;
int64 timestamp_epoch_ms = 3;
}
enum PaymentStatus {
PAYMENT_STATUS_UNSPECIFIED = 0;
PAYMENT_STATUS_SUCCESS = 1;
PAYMENT_STATUS_DECLINED = 2;
PAYMENT_STATUS_PENDING = 3;
}
message TransactionFilter {
string account_id = 1;
int64 from_timestamp = 2;
}
message Transaction {
string id = 1;
double amount = 2;
string status = 3;
}
message MetricRecord {
string metric_name = 1;
double value = 2;
int64 timestamp = 3;
}
message IngestSummary {
int32 processed_count = 1;
int64 elapsed_ms = 2;
}
message BalanceAuditRequest {
string audit_session_id = 1;
double reported_balance = 2;
}
message BalanceAuditResponse {
bool is_matched = 1;
double discrepancy = 2;
}Сучасна екосистема Node.js використовує офіційну бібліотеку @grpc/grpc-js (чиста реалізація на TypeScript/JavaScript, яка замінила застарілий бінарний модуль grpc).
У продакшені рекомендовано компілювати схеми .proto у статичний код TypeScript за допомогою ts-proto, що усуває оверхед на динамічну рефлексію під час запуску програми.
import * as grpc from "@grpc/grpc-js";
import * as protoLoader from "@grpc/proto-loader";
import path from "node:path";
// Завантаження контракту Protobuf
const PROTO_PATH = path.resolve(process.cwd(), "proto/billing.proto");
const packageDefinition = protoLoader.loadSync(PROTO_PATH, {
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true,
});
const protoDescriptor = grpc.loadPackageDefinition(packageDefinition) as any;
const billingPackage = protoDescriptor.billing.v1;
const server = new grpc.Server({
// Тюнінг мережевих параметрів HTTP/2 у gRPC
"grpc.max_receive_message_length": 4 * 1024 * 1024, // 4 MB
"grpc.max_send_message_length": 4 * 1024 * 1024,
"grpc.http2.max_pings_without_data": 0,
"grpc.keepalive_time_ms": 30000,
"grpc.keepalive_timeout_ms": 10000,
});
// 1. Unary Call: ProcessPayment
function processPayment(
call: grpc.ServerUnaryCall<any, any>,
callback: grpc.sendUnaryData<any>
) {
const metadata = call.metadata;
const traceId = metadata.get("x-trace-id")[0] || "no-trace";
console.log(`[gRPC ProcessPayment] Trace: ${traceId}, Account: ${call.request.account_id}`);
// Перевірка терміну дедлайну (Deadline / Timeout)
if (call.getDeadline() && new Date(call.getDeadline()).getTime() <= Date.now()) {
return callback({
code: grpc.status.DEADLINE_EXCEEDED,
message: "Час очікування виконання запиту вичерпано на сервері",
});
}
// Бізнес-логіка обробки платежу
const response = {
transaction_id: "tx_" + Math.random().toString(36).substring(2, 11),
status: "PAYMENT_STATUS_SUCCESS",
timestamp_epoch_ms: Date.now(),
};
// Повернення відповіді та метаданих трейлера
const trailer = new grpc.Metadata();
trailer.set("x-server-execution-time", "1.4ms");
callback(null, response, trailer);
}
// 2. Server Streaming: StreamTransactions
function streamTransactions(call: grpc.ServerWritableStream<any, any>) {
const { account_id } = call.request;
console.log(`[gRPC Server Stream] Запит транзакцій для ${account_id}`);
let count = 0;
const interval = setInterval(() => {
count++;
call.write({
id: `tx_${count}`,
amount: Math.round(Math.random() * 500 * 100) / 100,
status: "COMPLETED",
});
if (count >= 5) {
clearInterval(interval);
call.end(); // Завершення потоку
}
}, 100);
call.on("cancelled", () => {
console.warn("[gRPC Server Stream] Клієнт скасував потік");
clearInterval(interval);
});
}
// Реєстрація реалізацій сервісу
server.addService(billingPackage.BillingService.service, {
ProcessPayment: processPayment,
StreamTransactions: streamTransactions,
IngestMetrics: (call: any, cb: any) => { /* Client stream handler */ },
LiveReconciliation: (call: any) => { /* Bidi stream handler */ },
});
server.bindAsync(
"0.0.0.0:50051",
grpc.ServerCredentials.createInsecure(), // У продакшені: grpc.ServerCredentials.createSsl()
(err, port) => {
if (err) {
console.error("[gRPC Server Bind Error]:", err);
return;
}
console.log(`[gRPC Server] Запущено на порту ${port}`);
}
);import * as grpc from "@grpc/grpc-js";
import * as protoLoader from "@grpc/proto-loader";
import path from "node:path";
const PROTO_PATH = path.resolve(process.cwd(), "proto/billing.proto");
const packageDefinition = protoLoader.loadSync(PROTO_PATH);
const protoDescriptor = grpc.loadPackageDefinition(packageDefinition) as any;
const BillingService = protoDescriptor.billing.v1.BillingService;
// Interceptor для автоматичного логування та наскрізного трасування
const telemetryInterceptor: grpc.Interceptor = (options, nextCall) => {
return new grpc.InterceptingCall(nextCall(options), {
start: (metadata, listener, next) => {
// Додаємо розподілений W3C Trace ID
metadata.set("x-trace-id", "trace_" + Date.now());
const customListener: grpc.Listener = {
onReceiveMetadata: (receivedMetadata, nextMeta) => {
nextMeta(receivedMetadata);
},
onReceiveMessage: (message, nextMsg) => {
nextMsg(message);
},
onReceiveStatus: (status, nextStatus) => {
if (status.code !== grpc.status.OK) {
console.error(`[gRPC Error Status ${status.code}]: ${status.details}`);
}
nextStatus(status);
},
};
next(metadata, customListener);
},
});
};
const client = new BillingService(
"localhost:50051",
grpc.credentials.createInsecure(),
{
interceptors: [telemetryInterceptor],
"grpc.keepalive_time_ms": 15000,
}
);
// Виклик Unary методу зі строгим дедлайном (2000 мс)
async function executePayment() {
const deadline = new Date();
deadline.setMilliseconds(deadline.getMilliseconds() + 2000);
const request = {
account_id: "acc_77189",
amount: 250.0,
currency: "USD",
method: "PAYMENT_METHOD_CREDIT_CARD",
metadata: { orderId: "ord_100" },
};
client.ProcessPayment(
request,
{ deadline },
(error: grpc.ServiceError | null, response: any) => {
if (error) {
if (error.code === grpc.status.DEADLINE_EXCEEDED) {
console.error("[gRPC Client] Запит перервано: дедлайн вичерпано!");
} else {
console.error("[gRPC Client] Помилка RPC:", error.message);
}
return;
}
console.log("[gRPC Client] Платіж успішно виконано:", response);
}
);
}
executePayment();Класичне балансування навантаження на транспортному рівні L4 (наприклад, стандартний Kubernetes ClusterIP Service або AWS NLB) не працює коректно з gRPC:
Для коректного розподілу трафіку потрібне L7-балансування на рівні окремих RPC-потоків (Stream-level balancing). Це реалізується за допомогою Service Mesh (Envoy / Istio) або gRPC Client-Side Load Balancing.
Клієнт відправляє сотні паралельних викликів у локальний gRPC-канал.
Envoy Sidecar перехоплює трафік, демультиплексує HTTP/2 стрими та зчитує заголовок :path.
Envoy розподіляє кожен окремий стрім по пулу бекенд-подів за алгоритмом Least Request.
Бекенд-поди обробляють свої стрими паралельно, забезпечуючи рівномірне навантаження ядер CPU.
static_resources:
listeners:
- name: grpc_listener
address:
socket_address: { address: 0.0.0.0, port_value: 10000 }
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
stat_prefix: grpc_ingress
codec_type: HTTP2
http2_protocol_options:
max_concurrent_streams: 1024
initial_stream_window_size: 1048576
route_config:
name: grpc_routes
virtual_hosts:
- name: billing_service
domains: ["*"]
routes:
- match: { prefix: "/billing.v1.BillingService/" }
route:
cluster: billing_cluster
timeout: 2.0s
max_stream_duration:
grpc_timeout_header_max: 5.0s
http_filters:
- name: envoy.filters.http.router
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router
clusters:
- name: billing_cluster
connect_timeout: 0.25s
type: STRICT_DNS
lb_policy: LEAST_REQUEST
http2_protocol_options: {}
load_assignment:
cluster_name: billing_cluster
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address: { address: billing-backend-1, port_value: 50051 }
- endpoint:
address:
socket_address: { address: billing-backend-2, port_value: 50051 }Розгортання gRPC-сервісів за стандартним Kubernetes Service (ClusterIP / iptables) або TCP-балансувальником без Service Mesh. Через постійне HTTP/2 з'єднання всі клієнти тримають один TCP-сокет до одного поду. Нові репліки подів не отримують трафік, а поточний вузол падає від перевантаження. Рішення: використання L7-проксі (Envoy, Traefik) або клієнтського балансування через Headless Services (grpc.dns_resolver).
Видалення застарілого поля (наприклад, int32 old_status = 4;) з файлу .proto з наступним призначенням тегу 4 новому полю іншого типу. Старі клієнти або сервіси при десеріалізації зазнають фатального збою або отримають пошкоджені дані. Рішення: видалені номери та імена полів повинні обов'язково оголошуватися директивою reserved 4; reserved "old_status";.
Виклик віддаленого мікросервісу без вказання параметра deadline. Якщо downstream-сервіс зазнає перевантаження або дедлоку бази даних, сервіс, що викликав його, блокує потік до системного TCP-таймауту. Це призводить до вичерпання пулу воркерів та каскадного падіння всієї системи. Дедлайн має ініціалізуватися на кореневому шлюзі та зменшуватися на кожному кроці RPC.
Використання protoLoader.loadSync під час холодного старту serverless-функцій чи контейнерів. Парсинг файлів .proto на льоту витрачає ресурси CPU та сповільнює старт програми. У продакшені необхідно використовувати статичну AOT-генерацію (ts-proto), що генерує чистий і типізований код моделей.
ts-proto серіалізує типізований об'єкт у бінарний буфер Protobuf. gRPC додає 5-байтовий заголовок кадру.
Запит пакується в кадри HEADERS (стиснення HPACK, W3C Trace ID, Deadline) та DATA, відправляючись у відкритий TCP-сокет.
Envoy Sidecar аналізує Stream ID і направляє виклик на найменш завантажену репліку сервісу за алгоритмом Least Request.
Сервер зчитує бінарний буфер, перевіряє залишок дедлайну та передає типізовані параметри в бізнес-обробник.
Сервер повертає відповідь та фінальний кадр HEADERS із трейлером grpc-status: 0, вивільняючи Stream ID.