Spring Cloud Gateway с клиентом обнаружения Kubernetes для запросов http и grpc

Я пытаюсь реализовать службу шлюза с использованием Spring Cloud Gateway с обнаружением клиента k8s, который будет перенаправлять запросы http/1.1 и http/2 (GRPC).

Я установил следующие конфигурации:

server:
  port: 9995
  shutdown: graceful
  http2:
    enabled: true

logging:
  level:
    org.springframework.cloud.gateway: TRACE

spring:
  application:
    name: api-gateway
  lifecycle:
    timeout-per-shutdown-phase: 30s
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true
          include-expression: "metadata['gateway.enabled']=='true'"
          predicates:
            - name: Path
              args:
                pattern: "'/'+serviceId+'/**'"
          filters:
            - name: RewritePathAndPort
              args:
                grpcPort: 9090
                httpPort: 8080
    kubernetes:
      discovery:
        enabled: true
        primary-port-name: app-port
      loadbalancer:
        enabled: true
        mode: service
#        port-name: grpc-port

management:
  endpoint:
    health:
      enabled: true
      show-details: always
    gateway:
      enabled: true
  endpoints:
    web:
      exposure:
        include: '*'

Я также реализовал собственный фильтр RewritePathAndPort:

@Component
class RewritePathAndPortGatewayFilterFactory :
  AbstractGatewayFilterFactory<RewritePathAndPortGatewayFilterFactory.Config>(Config::class.java) {
  val log = KotlinLogging.logger {}

  class Config {
    var grpcPort: Int = 9090
    var httpPort: Int = 8080
  }

  override fun apply(config: Config): GatewayFilter =
    GatewayFilter { exchange, chain ->
      val req = exchange.request
      ServerWebExchangeUtils.addOriginalRequestUrl(exchange, req.uri)
      val path = req.uri.rawPath

      val pathSegments = path.split("/")
      log.info { "pathSegments: $pathSegments" }
      val host = pathSegments[1]
      log.info { "new host: $host" }

      val (newPath, newPort) =
        if (path.contains("/grpc/")) {
          stripPathSegments(pathSegments = pathSegments, segmentsToStrip = 3) to config.grpcPort
        } else {
          path to config.httpPort
        }

      log.info { "new port: $newPort" }

      log.info { "new path: $newPath" }

      val newUri =
        UriComponentsBuilder
          .fromUri(req.uri)
          .scheme("lb")
          .host(host)
          .port(newPort) // Set the new port
          .replacePath(newPath) // Set the new path
          .build(true) // true for encoding the path
          .toUri()
      log.info { "new Uri: $newUri" }
      val request =
        req
          .mutate()
          .uri(newUri)
          .path(newPath)
          .build()

      log.info { "new request: $request" }

      exchange.attributes[ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR] = request.uri
      log.info { "new exchange: $exchange" }
      chain.filter(exchange.mutate().request(request).build())
    }

  override fun shortcutFieldOrder(): List<String> = listOf("regexp", "replacement", "port")

  private fun stripPathSegments(
    pathSegments: List<String>,
    segmentsToStrip: Int,
  ): String =
    pathSegments
      .subList(segmentsToStrip, pathSegments.size)
      .joinToString("/")
      .let { "/$it" }
}

объяснение:

если это http-запрос:

  • переопределить хост по первому сегменту пути
  • оставь путь как есть
  • используйте порт 8080

Например, запрос с путем: /other-service/api/v1/hello URI будет lb://other-service:8080/other-service/api/v1/hello

если это запрос grpc:

  • переопределить хост по первому сегменту пути
  • удалить первые два сегмента пути
  • используйте порт 9090

Например: /other-grpc-service/grpc/HelloService/sayHello uri будет lb://other-grpc-service:9090/HelloService/sayHello

В приведенной выше конфигурации HTTP-запросы работают, а grpc — нет (сбой при Connection prematurely closed BEFORE response). поскольку балансировщик нагрузки использует порт http по умолчанию, настроенный в службе http, который равен 8080.

если я раскомментирую port-name: grpc-port, который сообщит балансировщику нагрузки использовать порт grpc-port, запросы grpc будут работать, но запросы http завершатся с ошибкой из-за той же ошибки. Это происходит потому, что балансировщик нагрузки переопределяет порт uri с помощью grpc-port, настроенного в службе grpc, который 9090.

Что вызывает это: ReactiveLoadBalancerClientFilter.

Есть ли способ обойти это?

редактировать: я нашел обходной путь после нескольких часов попыток. По сути, я скопировал ReactiveLoadBalancerClientFilter и изменил порт в соответствии с заголовком типа контента, и это сработало, но это не идеально.

Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
0
0
90
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Мне удалось решить эту проблему, используя GlobalFilter и Ordered вместо AbstractGatewayFilterFactory:

@Component
class EnhanceReactiveLoadBalancerClientFilter(
  private val gatewayProperties: GatewayProperties,
) : GlobalFilter,
  Ordered {
  private val log = KotlinLogging.logger { }

  override fun filter(
    exchange: ServerWebExchange,
    chain: GatewayFilterChain,
  ): Mono<Void> {
    val attributeUri = exchange.attributes[GATEWAY_REQUEST_URL_ATTR] as URI
    val originalPath = exchange.request.path.value()
    val newPath = originalPath.substringAfter(GRPC_PATH_INDICATOR)

    val newPort =
      if (originalPath == newPath) {
        gatewayProperties.ports.http
      } else {
        gatewayProperties.ports.grpc
      }

    val newAttributeUri =
      UriComponentsBuilder
        .fromUri(attributeUri)
        .port(newPort)
        .replacePath(newPath)
        .build()
        .toUri()

    log.debug { "reconstructed new attribute uri: $newAttributeUri" }
    exchange.attributes[GATEWAY_REQUEST_URL_ATTR] = newAttributeUri

    return chain.filter(exchange)
  }

  override fun getOrder(): Int = ReactiveLoadBalancerClientFilter.LOAD_BALANCER_CLIENT_FILTER_ORDER + 3
}

объяснение: Я не смог найти способ упорядочить RewritePathAndPortGatewayFilterFactory после ReactiveLoadBalancerClientFilter, создав упорядоченный глобальный фильтр, я смог установить его сразу после него, а также изменить порт и путь, поскольку ReactiveLoadBalancerClientFilter берет порт из самого сервиса k8s.

Мне все еще хотелось бы сделать это с AbstractGatewayFilterFactory, потому что это кажется правильным решением. но вышеперечисленного вполне достаточно

редактировать: изменение только атрибута GATEWAY_REQUEST_URL_ATTR, а не URI запроса, работает, потому что фильтр Spring Gateway, который отвечает за фактическую отправку запроса (/org/springframework/cloud/gateway/filter/NettyRoutingFilter.java), просматривает этот атрибут, а не URI запроса.

Другие вопросы по теме

Похожие вопросы

Невозможно создать массив функций в Java 8
Понимание порядка выполнения с помощью .then(Mono{})
Изменение часового пояса не приводит к тому, что LocalTime.now() возвращает другое значение, почему?
Может ли один и тот же код, использующий HttpClient, скомпилироваться с Spring-5.x и 6.x?
Улучшите качество подписи, извлеченной с помощью OpenCV из отсканированного листа бумаги
Переверните последовательные нули в единицы за k операций, чтобы получить максимальное количество единиц, найдите максимальное количество единиц
Влияет ли расстояние между операторами, отступами и комментариями в коде на его временную и пространственную сложность?
Почему мой код не удаляет zip-файл, созданный моим модульным тестом?
Приложение Spring Boot не запускается после добавления интеграции с клавиатурой
Исключение в потоке «main» java.time.format.DateTimeParseException: текст «1 апреля 2022 г., 12:00:00:000AM» не удалось проанализировать по индексу 19