Я пытаюсь реализовать службу шлюза с использованием 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-запрос:
Например, запрос с путем: /other-service/api/v1/hello
URI будет lb://other-service:8080/other-service/api/v1/hello
если это запрос grpc:
Например: /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
и изменил порт в соответствии с заголовком типа контента, и это сработало, но это не идеально.
Мне удалось решить эту проблему, используя 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 запроса.