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的自动路由能力,可以扩展服务实例供应商,根据请求动态选端口:
- 实现自定义
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 ) } } } }
- 在自定义过滤器中标记请求类型:
// 在RewritePathAndPortGatewayFilterFactory的apply方法中添加 val isGrpc = path.contains("/grpc/") || exchange.request.headers.getFirst("Content-Type")?.startsWith("application/grpc") == true exchange.attributes["isGrpc"] = isGrpc
- 配置使用自定义供应商:
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
相关产品推荐
相关产品推荐

