12 KiB
title, tags, categories
| title | tags | categories | |||
|---|---|---|---|---|---|
| Почему gRPC это лучший выбор для back-to-back взаимодействия |
|
|
gRPC - это высокопроизводительный фреймворк удаленных вызовов процедур от Google, построенный на HTTP/2 и Protocol Buffers. Он быстрее REST, строго типизирован и отлично масштабируется в микросервисных архитектурах.
Что такое gRPC?
Google Remote Procedure Call - это современный open-source RPC-фреймворк, позволяющий сервисам вызывать методы друг друга так, как если бы они были локальными функциями. Вместо того чтобы думать о HTTP-запросах и JSON-сериализации, разработчик просто вызывает метод на удаленном сервисе.
gRPC был разработан и открыт компанией Google в 2015 году. Он вырос из внутренней системы Stubby, которую Google использовал более десяти лет для связи между своими микросервисами, обрабатывающими миллиарды запросов в секунду.
Сегодня проект находится под управлением Cloud Native Computing Foundation (CNCF) - той же организации, что курирует Kibernetes и Prometheus. Это гарантирует его нейтральное развитие и широкую поддержку индустрии.
Ключевые особенности
Protobuf вместо JSON
gRPC использует Protobuf - бинарный формат сериализации данных. В отличие от текстового JSON:
- Данные занимают сильно меньше места
- Сериализация и десериализация происходят также быстрее
- Схема данных строго типизирована и версионируется через
.proto-файлы - Автоматическая кодогенерация клиентов и серверов на 10+ языках
HTTP/2 под капотом
Это дает следующие возможности:
- Мультиплексирование
- Двунаправленный стриминг
- Сжатие заголовков
- Server Push
Четыре режима взаимодействия
Unary RPC → клиент шлёт запрос, сервер возвращает ответ (как REST)
Server Streaming → сервер стримит поток ответов на один запрос
Client Streaming → клиент стримит данные, сервер возвращает один ответ
Bidirectional → оба шлют потоки данных друг другу одновременно
Строгий контракт через .proto
.proto-файл — это единственный источник правды для всей коммуникации. Если один сервис меняет API, компилятор Protobuf немедленно сломает несовместимых клиентов ещё на этапе сборки.
Встроенные инструменты
- Deadline / Timeout — встроенный контроль времени ожидания
- Cancellation — отмена запросов по цепочке вызовов
- Load Balancing — клиентская балансировка нагрузки из коробки
- Health Checking — стандартный протокол проверки готовности сервиса
- Interceptors — аналог middleware для аутентификации, логирования, трассировки
Пример: сервис пользователей на gRPC
Разберём полный цикл: от .proto-контракта до рабочего клиента и сервера на Go.
Шаг 1. Определяем контракт (user.proto)
syntax = "proto3";
package user;
option go_package = "github.com/example/user-service/proto";
// Запрос на получение пользователя
message GetUserRequest {
int64 id = 1;
}
// Ответ с данными пользователя
message GetUserResponse {
int64 id = 1;
string name = 2;
string email = 3;
string created_at = 4;
}
// Запрос на создание пользователя
message CreateUserRequest {
string name = 1;
string email = 2;
}
// Сервис с двумя методами
service UserService {
rpc GetUser (GetUserRequest) returns (GetUserResponse);
rpc CreateUser (CreateUserRequest) returns (GetUserResponse);
}
Шаг 2. Генерируем код
# Устанавливаем плагины
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
# Генерируем Go-код из .proto
protoc --go_out=. --go-grpc_out=. proto/user.proto
После этого компилятор создаст два файла:
user.pb.go— структуры данных (GetUserRequest, GetUserResponse и т.д.)user_grpc.pb.go— интерфейсы сервера и заглушки клиента
Шаг 3. Реализуем сервер (server/main.go)
package main
import (
"context"
"fmt"
"log"
"net"
"time"
pb "github.com/example/user-service/proto"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
// userServer реализует интерфейс pb.UserServiceServer
type userServer struct {
pb.UnimplementedUserServiceServer
// В реальности здесь была бы БД
users map[int64]*pb.GetUserResponse
}
// GetUser — обработчик метода GetUser
func (s *userServer) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.GetUserResponse, error) {
user, ok := s.users[req.Id]
if !ok {
// gRPC-статус коды вместо HTTP-кодов
return nil, status.Errorf(codes.NotFound, "пользователь с id=%d не найден", req.Id)
}
return user, nil
}
// CreateUser — обработчик метода CreateUser
func (s *userServer) CreateUser(ctx context.Context, req *pb.CreateUserRequest) (*pb.GetUserResponse, error) {
if req.Email == "" {
return nil, status.Error(codes.InvalidArgument, "email обязателен")
}
newID := int64(len(s.users) + 1)
user := &pb.GetUserResponse{
Id: newID,
Name: req.Name,
Email: req.Email,
CreatedAt: time.Now().Format(time.RFC3339),
}
s.users[newID] = user
return user, nil
}
func main() {
// Слушаем порт
lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalf("не удалось запустить listener: %v", err)
}
// Создаём gRPC-сервер
grpcServer := grpc.NewServer()
// Регистрируем наш сервис
pb.RegisterUserServiceServer(grpcServer, &userServer{
users: make(map[int64]*pb.GetUserResponse),
})
fmt.Println("gRPC-сервер запущен на :50051")
if err := grpcServer.Serve(lis); err != nil {
log.Fatalf("ошибка сервера: %v", err)
}
}
Шаг 4. Пишем клиент (client/main.go)
package main
import (
"context"
"fmt"
"log"
"time"
pb "github.com/example/user-service/proto"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
func main() {
// Подключаемся к серверу
conn, err := grpc.Dial(
"localhost:50051",
grpc.WithTransportCredentials(insecure.NewCredentials()),
)
if err != nil {
log.Fatalf("не удалось подключиться: %v", err)
}
defer conn.Close()
// Создаём клиента — это та самая "магия" gRPC:
// вызов метода выглядит как локальная функция
client := pb.NewUserServiceClient(conn)
// Контекст с таймаутом — хорошая практика для каждого запроса
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// Создаём пользователя
created, err := client.CreateUser(ctx, &pb.CreateUserRequest{
Name: "Алиса",
Email: "alice@example.com",
})
if err != nil {
log.Fatalf("ошибка CreateUser: %v", err)
}
fmt.Printf("Создан пользователь: ID=%d, Name=%s\n", created.Id, created.Name)
// Получаем пользователя по ID
user, err := client.GetUser(ctx, &pb.GetUserRequest{Id: created.Id})
if err != nil {
log.Fatalf("ошибка GetUser: %v", err)
}
fmt.Printf("Получен пользователь: %s <%s>\n", user.Name, user.Email)
}
Шаг 5. Запускаем
# Терминал 1: запускаем сервер
go run server/main.go
# gRPC-сервер запущен на :50051
# Терминал 2: запускаем клиент
go run client/main.go
# Создан пользователь: ID=1, Name=Алиса
# Получен пользователь: Алиса <alice@example.com>
gRPC vs REST: когда что выбирать?
| Критерий | REST | gRPC |
|---|---|---|
| Скорость | Хорошая | Отличная (бинарный протокол) |
| Типизация | Слабая (JSON) | Строгая (Protobuf) |
| Стриминг | Ограниченный (SSE) | Полноценный (4 режима) |
| Браузерная поддержка | Нативная | Нужен gRPC-Web |
| Отладка | Легко (curl, Postman) | Сложнее (нужен grpcurl) |
| Контракт API | OpenAPI/Swagger | .proto (строже) |
| Кодогенерация | Опционально | Встроена |
Выбирайте gRPC, когда:
- Сервисы общаются только между собой (backend-to-backend)
- Важна производительность и объём трафика
- Нужен стриминг данных в реальном времени
- Команда большая и строгий контракт критичен
Выбирайте REST, когда:
- API потребляется браузером напрямую
- Важна простота и человекочитаемость
- Команда небольшая и overhead Protobuf не оправдан
Итог
gRPC — это не просто «быстрый REST». Это принципиально другой подход к межсервисному взаимодействию, где строгий контракт, бинарная эффективность и встроенный стриминг делают его идеальным фундаментом для микросервисных архитектур. Google, Netflix, Cloudflare и десятки других компаний используют его в продакшне под нагрузками, которые REST с JSON попросту не выдержал бы.
Если вы строите систему из нескольких сервисов — gRPC стоит рассмотреть как первый выбор для их взаимодействия.