Реализуем .NET сервис на gRPC. Тонкости, о которых нужно знать

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті.

Всем привет, я Максим Усатенко, .NET разработчик в компании TELEMART.UA и сегодня я поделюсь своим опытом создания gRPC сервиса на ASP.NET core 5.0, расскажу про трудности и нюансы, с которыми мне пришлось столкнуться во время реализации сервиса.

Для начала, давайте вкратце разберёмся, что такое gRPC и зачем он вообще нужен. Статей на эту тему уже много, и я не буду останавливаться подробно на деталях. Если вы ранее не слышали об этой технологии, то лучше сначала прочтите обзорные статьи. От себя могу порекомендовать статью Романа Махныка: «Фреймворк gRPC: как он упрощает задачи разработчика и почему лучше, чем REST API».

RPC — remote procedure call, технология вызова удаленных процедур, созданная более 30 лет назад. Основная идея состоит в том, чтоб вызывать код удаленного сервиса так, будто он расположен локально, и не думать о сетях и прочих низкоуровневых проблемах.

gRPC — фреймворк от Google, созданный в 2015 году и активно набирающий обороты в наши дни. Именно он вдохнул новую жизнь в RPC подход, который на тот момент уже считался пережитком прошлого и не имел особых перспектив. Основное преимущество gRPC — это протокол HTTP/2, который сам по себе быстрее, чем HTTP/1.1. Не стоит забывать и про бинарный обмен данными по протоколу protobuf, что также добавляет скорость взаимодействия по сети.

Также важно упомянуть двунаправленную передачу данных и стримы. Ранее для реализации этих фишек требовались Web-сокеты, в частности, в .NET есть мощный фреймворк SignalR, который с лихвой способен решить эти задачи, но сегодня речь пойдет не о нем.

Почему я выбрал именно gRPC

Задача стояла написать микросервис и вынести в него несколько методов из старого монолита. Все остальные сервисы были REST, и намного проще было бы по готовому образцу сделать ещё один, но я всегда за то, чтоб код был написан на последних современных технологиях. Современные технологии интересно учить, приятно поддерживать и они предоставляют множество новых фич. Выбор был между GraphQL и gRPC, но в конечном счете я остановился на последнем, в виду сетевой скорости, а сервис планировался высоконагруженный.

Основная идея gRPC — .proto файл, в котором описан контракт взаимодействия клиента с сервером, на основании которого генерируются классы самого сервиса на C#. Синтаксис .proto файлов интуитивный и читаемый, написать его не составит проблем любому разработчику, он чем то напоминает язык C:

syntax = "proto3";

message HelloRequest {
    string message = 1;
}

message HelloResponse {
     string message = 1;
}

service MyRPCService {
    rpc SayHello (HelloRequest) returns (HelloResponse) {}
}

В .NET существует два основных подхода в реализации gRPC:

  1. Описание .proto файла и после реализация готового сгенерированного класса на C# (классический вариант). В качестве контракта выступает сам файл, который описывает взаимодействия клиента с сервером. В момент сборки проекта появляется класс сервиса с расширением .g.cs, а уже от этого сгенерированного базового класса необходимо реализовать свой собственный.
  2. Code-first подход и фреймворк protobuf-net. Декорируем атрибутами сервис и объекты взаимодействия (DTO), после чего .proto файл генерируется неявно с помощью рефлексии. В качестве контракта взаимодействия будет выступать интерфейс самого сервиса:
[ServiceContract(Name = "Services.HelloService")]
public interface IHelloService
{
     [OperationContract]
     Task<HelloResponse> SayHelloAsync(HelloRequest request, CallContext context = default);
}

[DataContract]
public class HelloRequest
{
      [DataMember(Order = 1)]
      public string Message { get; set; }
 }

 [DataContract]
 public class HelloResponse
 {
      [DataMember(Order = 1)]
      public string ResponseMessage { get; set; }
 }

Я попробовал оба варианта и выбрал второй, так как это более высокая абстракция. Клиент и сервер, в моем случае, написаны на С#, да и писать родные классы приятнее, чем учить синтаксис .proto файла. Фактически, мне вообще в итоге не пришлось знать .proto синтаксис.

Также стоит отметить: если у вас клиент и сервер описаны на разных языках, вам в любом случае придется использовать классический вариант с .proto файлом.

Ход реализации сервиса

Порты

Первым делом принял решение сделать так, чтобы сервис поддерживал и gRPC и REST. Нужно это для случая отказа: если вдруг что то пойдёт не так, я должен иметь возможность быстро переключиться на годами отточенный REST. Изначально я поднял два порта: один обычный, а второй для gRPC с настройкой под HTTP/2. Так советовали многие и называли это best practice для gRPC, но в конце, перед релизом, пришлось отказаться от двух портов и пустить все по одному, так как я не смог настроить наш сервер IIS под использование двух портов на одном приложении. NGINX с этим справляется без проблем, но вот IIS... Возможно, у кого-то это получалось, пишите в коментариях, а я все больше смотрю в сторону переезда всей инфраструктуры на Linux.

 .UseKestrel(x =>
                    {
                        x.Listen(IPAddress.Loopback, 8700, listenOptions =>
                        {
                            listenOptions.Protocols = HttpProtocols.Http1AndHttp2;
                            listenOptions.UseHttps();
                        });
                    })

Первые грабли, на которые я наступил, — маленькое комьюнити. Если что-то не работает, очень трудно найти решение. Надеюсь, в будущем, с ростом популярности, с этим проблем не будет.

Многие гиганты, такие как Netflix, Twitter, Spotify уже приняли gRPC как стандарт своих внутренних сервисов, так как благодаря высокой пропускной способности это помогает им экономить бюджет на инфраструктуре, а на их объемах, это цифры с шестью нулями.

Архитектура сервиса

Тут вообще все туманно. Я не нашёл ни одного примера как красиво и лаконично организовать код в сервисе. В итоге, всю бизнес логику вынес в хендлеры, а контроллеры и gRPC сервис вызывали эти хендлеры. Фактически, мой gRPC сервис стал контроллером со своей стороны. Это позволило не дублировать бизнес-логику и сделать что-то наподобие «Чистой архитектуры»:

    [Authorize]
    public class GrpcService : IGrpcService
    {
        private readonly IMediator mediator;
        public GrpcService(IMediator mediator)
        {
            this.mediator = mediator;
        }

       Task<HelloResponse> SayHelloAsync(HelloRequest request, CallContext context = default)
       {
            if (request is null)
            {
                throw new RpcException(new Status(StatusCode.InvalidArgument, "Request is null"), "Bad request");
            }
            HelloResponse response = await mediator.Send(request);
            return response;
        }
    }

Безопасность

Долго останавливаться не буду, отмечу лишь то, что с ней проблем не было. Все, что поддерживает стандартный ASP.NET core в gRPC, реализовано из коробки: JWT токены, куки, заголовки, и прочие стандарты безопасности на ваш вкус.

Клиент

С клиентом вышло все красиво и лаконично. С помощью интерфейса-контракта клиент сгенерировал методы запросов. Вызов методов действительно оправдал название фреймворка RPC — удаленный сервис вызывался так, будто это внутренние методы клиента из соседнего класса.

Написал я клиент следующим образом: фабрика создает объект самого сервиса, и через DI подключается в места, где нужен этот сервис. Можно было бы и напрямую, но из за асинхронного запроса токена решил всю эту кухню инкапсулировать следующим образом:

    public class GrpcServiceFactory : IGrpcServiceFactory
    {
        private readonly IGrpcServiceSettings settings;
        private readonly IAuthenticationManager authenticationManager;

        public GrpcServiceFactory(
        IGrpcServiceSettings settings,
        IAuthenticationManager authenticationManager)
        {
            this.settings = settings;
            this.authenticationManager = authenticationManager;
        }

        public async Task<IGrpcService> BuildAsync()
        {
            authenticationManager.ThrowIfNotAuthenticated();
            await authenticationManager.RefreshTokensAsync();

            CallCredentials credentials = CallCredentials.FromInterceptor((_, metadata) =>
            {
                if (!string.IsNullOrEmpty(authenticationManager.AccessToken))
                {
                    metadata.Add("Authorization", $"Bearer {authenticationManager.AccessToken}");
                    metadata.Add("User-Agent", "my-agent");
                }
                return Task.CompletedTask;
            });

            GrpcChannel channel = GrpcChannel.ForAddress(
            settings.BaseAddress,
            new GrpcChannelOptions
            {
                Credentials = ChannelCredentials.Create(ChannelCredentials.SecureSsl, credentials)
            });
            return channel.CreateGrpcService<IGrpcService>();
        }
    }

Тестирование

Тестировать gRPC — это боль , мучение и страдание. Нет нормальных решений по примеру Postman. Fiddler вообще ломает запросы, потому что он пытается превратить HTTP/2 в HTTP/1.1, следовательно, его вообще надо выключать при работе с gRPC. Обдумайте эту проблему сразу, перед тем как решитесь на gRPC.

Есть консольное приложение grpcurl для отправки запросов, и его обертка grpc UI, которая работает через рефлексию внутри сервиса и на основании .proto файла генерирует методы для дебага.

Консольное приложение достаточно мощное, но не совсем комфортное в использовании, да и не каждый тестировщик захочет в нем разбираться. А UI обертка, к сожалению, работает только напрямую с .proto файлом, и не умеет считывать атрибуты с библиотеки protobuf-net. Впрочем, если вы выбрали подход через написание .proto файла, то это достаточно хорошее решение.

Лично я выбрал вариант с логированием всех запросов и ответов на уровне Information. Не скажу, что этот вариант хороший, да и он фактически обязывает выключать уровень логирования information на продакшене, но зато в реализации это оказалось максимально быстро, да и тестировщик, при надобности, всегда сможет посмотреть что и куда улетело.

    public class LoggerInterceptor : Interceptor
    {
        private const string MessageTemplate =
            "{RequestMethod} request: {Request} response: {Response} status code: {StatusCode} in {Elapsed:0.0000} ms";
        public override async Task<TResponse> UnaryServerHandler<TRequest, TResponse>(TRequest request, ServerCallContext context, UnaryServerMethod<TRequest, TResponse> continuation)
        {
            Stopwatch sw = Stopwatch.StartNew();

            TResponse response = await base.UnaryServerHandler(request, context, continuation);
            sw.Stop();
            Log.Logger.Information(MessageTemplate,
                context.Method,
                JsonConvert.SerializeObject(request),
                JsonConvert.SerializeObject(response),
                context.Status.StatusCode,
                sw.Elapsed.TotalMilliseconds);

            return response;
        }
    }

Вывод: тестировать и дебажить возможно, но не удобно, трафик перехватить сложно, но с каждым днем появляется все больше интересных решений, и в скором времени с этим проблем не будет. Убедитесь в том, что ваш QA-отдел сможет должным образом протестировать сервис и готов разбираться в чем-то новом.

Тонкости и нюансы

  1. При классической реализации с .proto файлом сталкиваешься сразу с проблемой разрастания самого файла, ведь несколько объектов запроса и сам метод могут занимать немало строк кода. Сразу появляется желание разделить на несколько файлов, как классы на C#. Импортирование файлов возможно (механизм похож на using), но лично у меня все время были проблемы со связями этих файлов, либо я не смог до конца вникнуть и разобраться, либо это действительно так. В любом случае надо на это обратить внимание, потому что руками прописывать связи в .csproj лично для меня выглядит как моветон.
  2. При code first реализации, порядок полей имеет значение, а decimal и double — это разные типы. Таких нюансов немало, и в идеале надо просто копировать класс с сервера на клиент или использовать через общую библиотеку, чтоб не сталкиваться с подобными проблемами.
  3. Убедитесь, что ваша инфраструктура поддерживает HTTP/2 и конкретно gRPC. После реализации задачи я уперся в то, что наш Windows Server 2016 просто не поддерживает сетевой функционал gRPC, так как его давно не обновляли, и пришлось на время отложить задачу до переезда на Linux. Рекомендую посоветоваться с DevOps. Если бы он был на моем проекте, то возможно, удалось бы избежать подобных проблем еще на этапе «фундамента» сервиса.
  4. На сегодняшний день браузеры не полностью поддерживают HTTP/2, следовательно и gRPC. Как известно, эта технология изначально затачивалась под внутрисервисное общение, и только с ростом ее популярности появилась потребность в использовании gRPC на веб фронте. Могу упомянуть gRPC Web — JS библиотеку для поддержки совместимости между HTTP/1.1 и HTTP/2. С помощью нее вы сможете поддерживать на клиентской стороне браузера коммуникацию с сервисом на gRPC, вот только надо ли это? Лично для меня этот функционал еще сырой и я подожду нативную поддержку HTTP/2 во всех основных браузерах.

Итог

gRPC — это новый и стремительно набирающий популярность фреймворк, который поддерживает очень много современных и интересных фишек. В будущем он будет вытеснять REST из мира серверной архитектуры, но этот процесс очень медлительный, и займёт не один десяток лет.

Повторюсь, будьте готовы к тому, что на ваши вопросы еще нет ответов, а на ошибки, с которыми вы столкнетесь, висят открытые issue на GitHub. Так может продлиться еще пару лет.

Стоит ли на него переходить?

Вопрос многогранный и спорный. Все таки фреймворк ещё сыроват и не совсем комфортен в разработке. Старый работающий REST менять однозначно не стоит, но если вы пишите новый сервис, и у вас есть возможность и желание попробовать что-то новое, то возможно gRPC — это именно то, что вам нужно. Также обратите внимание на производительность, и если ваша сеть — это узкое место во взаимодействии сервисов, то gRPC повысит пропускную способность и сможет решить вашу проблему.

А какой у вас опыт внедрения gRPC в .NET? Пишите в комментариях, мне будет очень интересно прочитать :)

👍ПодобаєтьсяСподобалось5
До обраногоВ обраному3
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Лучше поздно чем никогда: начиная с 13 января 2022 Postman тоже поддерживает gRPC

Задача стояла написать микросервис и вынести в него несколько методов из старого монолита. Все остальные сервисы были REST, и намного проще было бы по готовому образцу сделать ещё один, но я всегда за то, чтоб код был написан на последних современных технологиях. Современные технологии интересно учить, приятно поддерживать и они предоставляют множество новых фич. Выбор был между GraphQL и gRPC, но в конечном счете я остановился на последнем, в виду сетевой скорости, а сервис планировался высоконагруженный.

Просто колекція антипаттернів і buzzword-driven development.
REST- ні грама не застарів.
Виносити в мікросервіс пару методів — це як відрубувати руки, бо так зручніше, окремо від тіла.
Розводити зоопарк технологій, без стратегічного плану, мати більш-менш уніфікований стек технологій — це прирікати себе на високі витрати по підтримці. А потім від складності наш розробник «перегорить», компанія стане погана, бо змушує працювати і підтримувати то будуть інші люди.
А за півроку цей зоопарк підтримувати буде заважко і переписувати треба.
Замкнутий цикл.

Може й була серйозніша аргументація, але її в статті не згадано.

он фактически обязывает выключать уровень логирования information на продакшене

По идее можно же сделать для логера запросов конкретную категорию и в настройках логирования выключить только её. Ну или для этой категории поставить уровень логирования выше Information. В результате вы сохраните Information логи для остального.

Первое не стыкуется со вторым

Современные технологии интересно учить, приятно поддерживать и они предоставляют множество новых фич
написаны на С#, да и писать родные классы приятнее, чем учить синтаксис .proto файла. Фактически, мне вообще в итоге не пришлось знать .proto синтаксис.

Ну и да, хотелось бы больше узнать «тонкостей» из заголовка. Что такое grpc — большинство и так уже знает и имело с этим дело.

В других статьях как-то было повеселее. Каких-то пятнадцать скринов с кодом и вуаля. База данных для проекта ToDo List почти готова. Здесь сходу увидел несколько скринов с фабриками ООП но так и не понял полезную нагрузку этого приложения.
Потом вернулся и увидел скрин в самом начале, теперь все нормально. У простынь кода размерами с ткацкую фабрику есть полезная нагрузка. Оно говорит Say Hello

Не вижу смысла в примерах оставлять бизнес логику. Кусок кода от этого понятнее не станет.

Идея описывать контракты на .net вместо proto мне кажется странной. Почему добровольно отказываться от платформонезависимого формата?

Также не затронута та самая мощь HTTP2 в gRPC — стриминговые методы.

Кстати сказать, он используется не только для взаимодействия сервисов, но и для IPC.
В частности, я на проекте настроил kestrel для общения между процессами используя, unix sockets и gRPC (gRPC это может и на винде), один кор процесс стримит данные на анализатор и UI клиент, вроде неплохо работает.
Изначально, я хотел использовать виндовые пайпы, но не знал на каких стеках будут собраны разные процессы, поэтому использовал относительно универсальное gRPC решение.

В пример могу привести EF core, фактически он тоже уходит от платформонезависимого формата SQL. Для меня это схожая аналогия.

Ну а вообще я не говорю что подход code-first лучше. Я просто попробовал его и написал об этом в статье.

Хоть какое-то решение для удобной отправки сообщений есть — BloomRPC. Для стандартных задач хватает.
В топ2 маркетплейсе рф rpc тоже стал стандартом.

BloomRPC имеет правда проблему — он форматирует и отображает даты в респонсе в своем формате, который отличается от того, что выдает JsonFormatter из gRpc

А еще у него проблемы с бесплатными сертификатами. Недавно для себя открыл еще один неплохой grpc клиент для тестирования — Kreya. Он более замудренный, но тоже неплох.

Очікував від статті більшого, хоча б посилання на репозиторій з кодом.

Замість gRPC-web спробуйте gRPC-Gateway тоді буде доступно згенерувати /api/doc/index.html як в Swagger та тестувати через Postman.

Предоставить репозиторий к сожалению не мог, так как пример кода взят из коммерческого проекта. Если остались какие-либо вопросы — обращайтесь, с удовольствием отвечу. За рекомендацию gRPC-Gateway спасибо!

Підписатись на коментарі