You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring Cloud Gateway+K8s服务发现:HTTP与GRPC转发端口冲突解决方案问询

问题:Spring Cloud Gateway + Kubernetes服务发现同时支持HTTP/1.1和GRPC转发的端口冲突问题

场景与配置

我要实现一个基于Spring Cloud Gateway结合Kubernetes服务发现的网关,同时转发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)
          .replacePath(newPath)
          .build(true)
          .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会转发到lb://other-service:8080/other-service/api/v1/hello
  • GRPC请求:把路径首段作为目标服务名,移除前2段路径,使用9090端口。比如/other-grpc-service/grpc/HelloService/sayHello会转发到lb://other-grpc-service:9090/HelloService/sayHello

当前问题

  • 默认配置下,HTTP请求能正常转发,但GRPC请求报错Connection prematurely closed BEFORE response,原因是负载均衡器默认用了HTTP服务的8080端口
  • 取消注释port-name: grpc-port后,GRPC请求正常,但HTTP请求报相同错误,此时负载均衡器强制用GRPC服务的9090端口
  • 排查后发现是ReactiveLoadBalancerClientFilter覆盖了自定义过滤器设置的端口,它只会读取全局配置的端口参数

优化解决方案

方案1:手动拆分HTTP/GRPC路由(最直观)

关闭自动路由发现,手动定义两组路由,通过请求特征区分HTTP和GRPC,分别指定对应端口:

spring:
  cloud:
    gateway:
      routes:
        # HTTP请求路由
        - id: http-service-route
          uri: lb://{serviceId}:8080
          predicates:
            - Path=/{serviceId}/**
            - Header=Content-Type, application/json|application/x-www-form-urlencoded|text/plain
          filters:
            # 可选:如果需要移除路径中的服务名前缀
            - RewritePath=/(?<serviceId>.*?)(?<path>/.*), $\{path}
        # GRPC请求路由
        - id: grpc-service-route
          uri: lb://{serviceId}:9090
          predicates:
            - Path=/{serviceId}/grpc/**
            - Header=Content-Type, application/grpc*
          filters:
            - RewritePath=/(?<serviceId>.*?)/grpc(?<path>/.*), $\{path}
      discovery:
        locator:
          enabled: false # 关闭自动路由

这种方式直接通过请求头和路径匹配不同路由,端口区分明确,不需要修改过滤器逻辑,维护起来简单。

方案2:自定义服务实例端口选择(保留自动路由)

如果想保留Discovery Locator的自动路由能力,可以扩展服务实例供应商,根据请求动态选端口:

  1. 实现自定义ServiceInstanceListSupplier,根据请求标记选择端口:
@Component
class PortAwareServiceInstanceListSupplier(
    private val delegate: ServiceInstanceListSupplier
) : ServiceInstanceListSupplier by delegate {

    override fun get(request: Request): Flux<List<ServiceInstance>> {
        val isGrpc = request.context.attributes["isGrpc"] as? Boolean ?: false
        return delegate.get(request).map { instances ->
            instances.map { instance ->
                val targetPort = if (isGrpc) 9090 else 8080
                DefaultServiceInstance(
                    instance.instanceId,
                    instance.serviceId,
                    instance.host,
                    targetPort,
                    instance.metadata,
                    instance.uri
                )
            }
        }
    }
}
  1. 在自定义过滤器中标记请求类型:
// 在RewritePathAndPortGatewayFilterFactory的apply方法中添加
val isGrpc = path.contains("/grpc/") || 
             exchange.request.headers.getFirst("Content-Type")?.startsWith("application/grpc") == true
exchange.attributes["isGrpc"] = isGrpc
  1. 配置使用自定义供应商:
spring:
  cloud:
    kubernetes:
      discovery:
        service-instance-list-supplier: com.your.package.PortAwareServiceInstanceListSupplier

这种方式保留了自动路由的便利性,同时动态适配端口,适合服务数量较多的场景。

方案3:调整过滤器执行顺序(快速临时修复)

让自定义过滤器在ReactiveLoadBalancerClientFilter之后执行,覆盖它设置的端口:

@Component
class RewritePathAndPortGatewayFilterFactory : ... {
    override fun getOrder(): Int {
        // 比负载均衡过滤器优先级低,后执行
        return ReactiveLoadBalancerClientFilter.LOAD_BALANCER_CLIENT_FILTER_ORDER + 1
    }
    // 其他代码不变
}

注意:这种方式可能会干扰负载均衡的原有逻辑,比如如果服务实例的端口不是固定8080/9090,会出问题,仅适合端口固定的场景。

方案对比

之前复制ReactiveLoadBalancerClientFilter修改端口的方案虽然能解决问题,但需要维护Spring Cloud内部代码,版本升级时容易出兼容问题。上面的方案1和方案2更符合Spring Cloud的扩展规范,长期维护性更好。

内容的提问来源于stack exchange,提问作者Lior Derei

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.20 20:43:11