Пошук уроків, статей та іншого контенту
Покаже, як створювати типізовані gRPC-сервіси, описувати контракти та виконувати міжсервісні виклики.
gRPC — фреймворк для виконання віддалених викликів між сервісами. Один сервіс викликає метод іншого майже так само, як звичайний метод локального об’єкта.
Для опису контрактів gRPC використовує Protocol Buffers, або скорочено protobuf.
Контракт визначає:
доступні сервіси;
методи цих сервісів;
типи аргументів;
типи відповідей;
числові ідентифікатори полів повідомлень.
На основі .proto-файлу генеруються клієнтські та серверні класи для конкретної мови програмування.
Типовий процес має такий вигляд:
Розробник описує контракт у .proto-файлі.
Компілятор protobuf генерує класи повідомлень і код клієнта та сервера.
Сервер реалізує згенерований інтерфейс.
Клієнт створює канал і викликає метод через згенерований stub.
gRPC серіалізує повідомлення у бінарний формат protobuf та передає його через HTTP/2.
Сервер десеріалізує запит і повертає типізовану відповідь.
На відміну від REST, клієнту не потрібно вручну формувати URL, JSON і HTTP-методи для кожної операції. Ці деталі описуються контрактом і реалізуються gRPC.
Створимо файл greeter.proto:
syntax = "proto3";
package greeting;
service Greeter {
rpc SayHello (HelloRequest) returns (HelloResponse);
}
message HelloRequest {
string name = 1;
}
message HelloResponse {
string message = 1;
}syntax = "proto3";Вказує версію синтаксису protobuf.
package greeting;Задає логічний простір імен. У gRPC повне ім’я методу буде таким:
/greeting.Greeter/SayHelloservice Greeter {
rpc SayHello (HelloRequest) returns (HelloResponse);
}Описує сервіс Greeter з методом SayHello.
Метод:
приймає повідомлення HelloRequest;
повертає повідомлення HelloResponse;
є unary-викликом, тобто один запит відповідає одній відповіді.
message HelloRequest {
string name = 1;
}Описує структуру повідомлення. Число 1 — це номер поля, який використовується у бінарному форматі protobuf.
Номер поля є частиною контракту. Його не слід змінювати після публікації схеми.
Найпоширеніші типи protobuf:
string;
bool;
bytes;
int32, int64;
uint32, uint64;
float, double;
enum;
інші message;
повторювані поля через repeated.
Приклад складнішого повідомлення:
message User {
int64 id = 1;
string email = 2;
bool active = 3;
repeated string roles = 4;
}Поле roles може містити декілька значень:
["admin", "editor"]У protobuf повідомлення не повинно залежати від порядку полів. Ідентифікатори полів використовуються для сумісної серіалізації та десеріалізації.
Для прикладу використаємо Python і бібліотеки grpcio та grpcio-tools.
Створіть віртуальне середовище та встановіть залежності:
python -m venv .venv
source .venv/bin/activate
pip install grpcio grpcio-toolsДля Windows активація середовища має такий вигляд:
.venv\Scripts\Activate.ps1Покладіть greeter.proto у корінь проєкту та запустіть генерацію:
python -m grpc_tools.protoc \
-I. \
--python_out=. \
--grpc_python_out=. \
greeter.protoУ результаті з’являться два файли:
greeter_pb2.py — класи protobuf-повідомлень;
greeter_pb2_grpc.py — клієнтський stub і базовий клас сервера.
Ці файли генеруються автоматично. Зазвичай їх не редагують вручну.
Створіть файл server.py:
from concurrent import futures
import grpc
import greeter_pb2
import greeter_pb2_grpc
class GreeterService(greeter_pb2_grpc.GreeterServicer):
def SayHello(
self,
request: greeter_pb2.HelloRequest,
context: grpc.ServicerContext,
) -> greeter_pb2.HelloResponse:
if not request.name.strip():
context.abort(
grpc.StatusCode.INVALID_ARGUMENT,
"Ім'я не може бути порожнім",
)
return greeter_pb2.HelloResponse(
message=f"Привіт, {request.name}!"
)
def serve() -> None:
server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
greeter_pb2_grpc.add_GreeterServicer_to_server(
GreeterService(),
server,
)
server.add_insecure_port("[::]:50051")
server.start()
print("gRPC-сервер запущено на порту 50051")
try:
server.wait_for_termination()
except KeyboardInterrupt:
# Коректно зупиняємо сервер під час завершення процесу
server.stop(grace=1)
if __name__ == "__main__":
serve()Клас GreeterService успадковує GreeterServicer, згенерований із .proto:
class GreeterService(greeter_pb2_grpc.GreeterServicer):Метод сервера повинен мати назву, яка точно збігається з назвою методу в контракті:
rpc SayHello (HelloRequest) returns (HelloResponse);Результатом є екземпляр згенерованого класу HelloResponse:
return greeter_pb2.HelloResponse(
message=f"Привіт, {request.name}!"
)Параметр context дає змогу:
повернути gRPC-статус помилки;
додати деталі помилки;
перевірити метадані;
реагувати на скасування виклику.
У прикладі для некоректного запиту використовується:
context.abort(
grpc.StatusCode.INVALID_ARGUMENT,
"Ім'я не може бути порожнім",
)Це припиняє обробку методу та повертає клієнту статус INVALID_ARGUMENT.
Створіть файл client.py:
import grpc
import greeter_pb2
import greeter_pb2_grpc
def main() -> None:
with grpc.insecure_channel("localhost:50051") as channel:
stub = greeter_pb2_grpc.GreeterStub(channel)
request = greeter_pb2.HelloRequest(name="Олено")
try:
response = stub.SayHello(request, timeout=3)
print(response.message)
except grpc.RpcError as error:
print(
f"gRPC-помилка: {error.code().name} — "
f"{error.details()}"
)
if __name__ == "__main__":
main()Запустіть сервер в одному терміналі:
python server.pyВ іншому терміналі запустіть клієнта:
python client.pyОчікуваний результат:
Привіт, Олено!Канал відповідає за з’єднання з gRPC-сервером:
channel = grpc.insecure_channel("localhost:50051")У production-середовищі для захищеного з’єднання використовують TLS і grpc.secure_channel. insecure_channel підходить для локального прикладу та внутрішнього тестування.
Stub — це згенерований клієнтський об’єкт:
stub = greeter_pb2_grpc.GreeterStub(channel)Після цього віддалений метод викликається як звичайний метод:
response = stub.SayHello(request, timeout=3)Завдяки згенерованим класам клієнт використовує HelloRequest, а відповідь має тип HelloResponse.
У прикладі використано unary RPC:
rpc SayHello (HelloRequest) returns (HelloResponse);Це найпростіший тип gRPC-виклику:
клієнт → один запит → сервер
клієнт ← одна відповідь ← серверUnary-виклики підходять для:
отримання одного ресурсу;
створення або зміни одного ресурсу;
команд;
перевірок і розрахунків.
gRPC також підтримує потокові виклики, але для них використовуються інші типи методів у .proto. У цьому прикладі вони не потрібні.
gRPC має стандартизовані коди статусів. Найчастіше використовують:
OK — успішний виклик;
INVALID_ARGUMENT — некоректні аргументи;
NOT_FOUND — ресурс не знайдено;
ALREADY_EXISTS — ресурс уже існує;
UNAUTHENTICATED — відсутня або некоректна автентифікація;
PERMISSION_DENIED — недостатньо прав;
DEADLINE_EXCEEDED — завершився час очікування;
UNAVAILABLE — сервіс тимчасово недоступний;
INTERNAL — внутрішня помилка сервера.
Код статусу потрібно вибирати за змістом помилки, а не повертати INTERNAL для будь-якої проблеми.
На клієнті статус можна перевірити через grpc.RpcError:
try:
response = stub.SayHello(request, timeout=3)
except grpc.RpcError as error:
if error.code() == grpc.StatusCode.INVALID_ARGUMENT:
print("Клієнт передав некоректні дані")
elif error.code() == grpc.StatusCode.UNAVAILABLE:
print("Сервіс тимчасово недоступний")Кожен міжсервісний виклик повинен мати обмеження часу:
response = stub.SayHello(request, timeout=3)Без тайм-ауту клієнт може чекати невизначено довго, якщо сервер завис, мережа стала недоступною або залежний сервіс не відповідає.
У реальній системі значення тайм-ауту обирають відповідно до операції:
швидкі локальні запити мають короткий дедлайн;
складні операції можуть отримати більше часу;
дедлайн не повинен бути необмеженим.
Якщо час вичерпано, клієнт отримує статус DEADLINE_EXCEEDED.
Контракти між сервісами змінюються, тому важливо зберігати зворотну сумісність.
Безпечні зміни:
додавання нового поля з новим номером;
додавання нового RPC-методу;
додавання нового значення enum за потреби.
Наприклад:
message HelloResponse {
string message = 1;
string language = 2;
}Старий клієнт проігнорує невідоме поле language, а новий клієнт зможе працювати зі старою відповіддю без цього поля.
Не слід:
змінювати тип уже наявного поля без перевірки сумісності;
повторно використовувати номер видаленого поля;
змінювати значення поля так, щоб його зміст став іншим;
перейменовувати поле, якщо це змінює логіку серіалізації або згенерований API.
Для видалених полів можна залишити номери зарезервованими:
message HelloRequest {
reserved 2;
reserved "old_field";
string name = 1;
}Це допомагає випадково не використати старий номер або назву повторно.
Типовий виклик між сервісами виглядає так:
Order Service → Payment ServiceOrder Service має клієнтський stub для Payment Service. Контракт платежів зберігається у .proto-файлі та використовується обома сервісами.
Переваги такого підходу:
контракт є явним і версіонованим;
типи повідомлень генеруються автоматично;
помилки структури виявляються під час компіляції або генерації;
protobuf зазвичай передає компактні бінарні повідомлення;
gRPC підтримує дедлайни, статуси та потокові виклики.
Водночас сервіси все одно повинні коректно обробляти:
недоступність залежностей;
повторні спроби;
тайм-аути;
несумісні версії контрактів;
помилки авторизації.
gRPC не усуває мережеві проблеми, а лише надає стандартизований спосіб їх обробки.
Погано:
response = stub.SayHello(request)Краще:
response = stub.SayHello(request, timeout=3)Без дедлайну один проблемний виклик може утримувати потік або процес надто довго.
Не варто повертати INTERNAL для помилки вхідних даних:
context.abort(
grpc.StatusCode.INTERNAL,
"Некоректне ім'я",
)Для цього підходить INVALID_ARGUMENT:
context.abort(
grpc.StatusCode.INVALID_ARGUMENT,
"Ім'я не може бути порожнім",
)Неправильно:
return {"message": "Привіт"}Метод повинен повертати повідомлення згенерованого типу:
return greeter_pb2.HelloResponse(message="Привіт")Номер поля є частиною wire-формату. Не можна безпечно змінювати:
string name = 1;на:
string name = 2;для вже використовуваного контракту.
Після зміни .proto потрібно знову запустити генерацію. Інакше сервер і клієнт можуть використовувати різні версії контракту або згенерованого API.
gRPC дає змогу викликати методи віддалених сервісів через типізований API.
Protocol Buffers описує структуру повідомлень і сервісів у .proto-файлі.
З .proto генеруються класи повідомлень, клієнтські stub-и та базові класи серверів.
Unary-виклик приймає один запит і повертає одну відповідь.
gRPC має стандартизовані статуси помилок і підтримує дедлайни.
У міжсервісних викликах потрібно явно обробляти тайм-аути та недоступність залежностей.
Для сумісної еволюції контрактів не можна повторно використовувати номери видалених полів.