NGINX и gRPC теперь настоящие друзья

Несколько дней назад зарелизилась новая версия Nginx — 1.13.10. Главная фича данного релиза — это нативная поддержка проксирования HTTP/2, и, как следствие, gRPC.

Наверное, сейчас, когда мир заполонили микросервисы, а также гетерогенные стеки технологий, каждый знает, что такое gRPC. Если нет, то это, как protobuf (который gRPC в том числе может использовать для сериализации), или Apache Thrift на стероидах. Технология позволяет организовать взаимодействие множества сервисов друг с другом в крайне эффективной манере.

Высокая производительность gRPC достигается благодаря нескольким вещам: использованию HTTP/2 для мультиплексирования, сжатию данных. Кроме того, фреймворк побуждает программистов разрабатывать свои сервисы в неблокирующем стиле (a-ka NIO), используя внутри себя такие библиотеки, как Netty.

image

Изображение взято с https://www.slideshare.net/borisovalex/enabling-googley-microservices-with-grpc-at-jdkio-2017

Другая важная фишка gRPC — это нативная поддержка backpressure. Это свойство реализовано с помощью абстракции deadline: инитный таймаут клиента экспозится через всю чепочку сервисов. Если следующий вызов не влезет в заданный дедлайн (таймаут), то вся чепочка вызовов будет зафейлена. Это защищает систему от цепной реакции. Подробнее об этом рассказывал Александр Борисов из Google. Можно посмотреть этот доклад на youtube.

Вернёмся к нашим баранам: Nginx и gRPC. С первого взгляда может показаться, что это две несовместимые технологии. Nginx используется, как точка входа в систему. В то время, как gRPC — это инстурмент для взаимодействие микросервисов внутри системы. Однако это не всегда так.

Рассмотрим компанию, которая разрабатывает API. У этой компании могут быть мобильные приложения, которые потребляют это же API. Приложения обычно не могут ходить напрямую в микросервисы, которые не доступны из публичной сети. Поэтому требуется некоторый Gateway, который будет принимать запросы из вне и проксировать их во внутренние микросервисы.

Функцию Gateway могут выполняться несколько классов систем. Во-первых, это может быть честное приложение на каком-нибудь языке программирования. Из плюсов такого подхода — большая гибкость. Из минусов — часто, пониженная производительность. Кроме того, программируя свой Gateway, достаточно просто наделать багов, которые могут повлиять на безопастность системы.

Другой вариант реализации Gateway — использование готовое решение из класса Reverse Proxy. Это может быть знакомый всем Nginx. Но есть и другие современные альтернативы. Это и Envoy, это и Træfik, это и Caddy. Наверное, преимущества Прокси всем понятны: это быстро, это надежно. Мы получаем из коробки балансировку трафика. Мы получаем из коробки SSL-терминирование. Кроме того, в любом Прокси обычно реализована очень гибкая система роутинга, которая позволяет по разным URL маршутизовать трафик в разные приложения.

Итак, мы поняли, что иногда нам нужно заэкспозить gRPC наружу системы, видимо, используя какой-то Reverse Proxy. Но вот незадача. У нас на проекте Nginx, ничего модного мы не хотим, а в старичке бага — нет возможности проксировать HTTP/2. Решение есть — обновиться до 1.13.10! Ребята наконец-то сделали нативную поддержку проксирования HTTP/2, а также gRPC.

image

Из коробки вы получите целый пакет благ. TLS-терминирование, балансировка трафика по нодам, мощный роутинг, а также ряд других известных вам Nginx фич.

Всё, что нужно сделать, чтобы начать использовать проксирование gRPC трафика — это подщаманить конфиг (и, возможно, собрать Nginx с парой новых модулей, если вы собираете Прокси сами). HelloWorld конфиг описывается так:

server {
    listen 80 http2;
    charset utf-8;
    access_log logs/access.log;

    location / {
        grpc_pass grpc://movie:6565;
    }
}


Я сам человек простой: пока не увижу — не поверю. Поэтому накидал для демонстрации Демку, где есть Сервер, который отдаёт список лучших фильмов (набор заданных строк), и есть Клиент, который читает эти фильмы. Клиент и сервер работают через Nginx.

Пишим фильмы мы так:

@Override
public void getRating(Moviesrating.GetRatingRequest request,
                      StreamObserver responseObserver) {
    log.info("getRating(): request={}", request);

    List bestMovies = Arrays.asList(
            "The Shawshank Redemption",
            "The Godfather",
            "The Dark Knight",
            "Interstellar"
    );

    responseObserver.onNext(Moviesrating.GetRatingResponse.newBuilder()
            .addAllMovie(bestMovies)
            .build());
    responseObserver.onCompleted();
}


А так мы читаем фильмы:

@GetMapping("/top")
Mono> top() {
    log.info("top()");

    ListenableFuture ratingFuture
            = moviesRatingStub.getRating(
                    Moviesrating.GetRatingRequest.newBuilder().build());

    CompletableFuture> completable = new CompletableFuture>() {
        @Override
        public boolean cancel(boolean mayInterruptIfRunning) {
            boolean result = ratingFuture.cancel(mayInterruptIfRunning);
            super.cancel(mayInterruptIfRunning);
            return result;
        }
    };

    ratingFuture.addListener(() -> {
        try {
            completable.complete(ratingFuture.get().getMovieList().stream()
                    .map(Movie::new)
                    .collect(Collectors.toList()));
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
    }, executor);

    return Mono.fromFuture(completable);
}


Всё работает, мужики из Nginx не обманули, можно верить. А если не верите — https://github.com/Hixon10/grpc-nginx — проверьте сами.

© Habrahabr.ru