实际是该项目的延申 cclient/elasticsearch-multi-cluster-compat-proxy: 网关代理兼容ES6 es7 proxy and compat elasticsearch version 7 and elasticsearch version 6's _search and _bulk request api (github.com)
项目用spring-boot-starter-webflux 实现了es6和es7的兼容层网关,但只是做可行性验证
真正生产使用的网关还需要更多功能,最基本的,限流,熔断,负载均衡,这些都独立往项目里加,每个点都需要花精力整合
实际这方面应用已经有了很成熟的方案,服务治理相关,k8s,ingress,istio,kong,nginx...
但对es这类bare服务,套用云服务的方案并不合适,kong/nginx因为是c+lua的技术栈,定制的成本较高
nodejs已经过气了,go和java,考虑生态,选择java
java方面的网关,很少单独提及,更多是做为java服务治理的一个组件来使用
java类网关,早些年大家都使用netty/mina原生实现
但后期有基于netty的各种封装好的http框架,再完全用netty开发http类网关就比较少见了,当然tcp/udp类,自定义rpc的还是免不了直接和netty打交道
整合netty的 http高性能网关类服务早些年个人用过 jersey,后来用vert.x,再后来就直接上spring-boot-starter-webflux了,抽空把历史代码扒出来
现在为了省去在webflux自行添加限流,熔断,降级等功能,直接使用spring-boot-gateway
实际spring-boot-gateway,本身集成mvn/webflux的两种方案,webflux的底层就是netty
本地代理其实可以理解为替代nginx,并实现一些和业务产品深度结合的功能
因为nginx c+lua的技术栈和开发成本较高
首先spring-cloud-gateway 原生是 spring-cloud的组件,应用场景和spring-cloud深耦合
例如,loadbanlance,依赖spring-cloud的服务发现组件,consule,nacos等
https://docs.spring.io/spring-cloud-commons/docs/current/reference/html/#spring-cloud-loadbalancer
Spring Cloud Commons provides the @EnableDiscoveryClient annotation. This looks for implementations of the DiscoveryClient and ReactiveDiscoveryClient interfaces with META-INF/spring.factories. Implementations of the discovery client add a configuration class to spring.factories under the org.springframework.cloud.client.discovery.EnableDiscoveryClient key. Examples of DiscoveryClient implementations include Spring Cloud Netflix Eureka, Spring Cloud Consul Discovery, and Spring Cloud Zookeeper Discovery.
spring-cloud-gateway 基础支持
spring:
cloud:
gateway:
routes:
- id: before_route
uri: https://example.org
predicates:
- Before=2017-01-20T17:42:47.789-07:00[America/Denver]
以该项为例,关键是uri 这个参数
网关单点1:1 uri 可以写一个http/https的访问地址
如果需要实现负载均衡gateway: server 1:n 则需要实现 uri("lb://backing-service:8088")
@Bean
public RouteLocator routes(RouteLocatorBuilder builder) {
return builder.routes()
.route("circuitbreaker_route", r -> r.path("/consumingServiceEndpoint")
.filters(f -> f.circuitBreaker(c -> c.name("myCircuitBreaker").fallbackUri("forward:/inCaseOfFailureUseThis").addStatusCode("INTERNAL_SERVER_ERROR"))
.rewritePath("/consumingServiceEndpoint", "/backingServiceEndpoint")).uri("lb://backing-service:8088")
.build();
}
es3
这里实际依赖了spring-cloud生态的服务发现组件,注册服务 backing-service 至注册中心,spring-cloud从注册中心获取真实的服务地址host:port,再通过客户端负载均衡lb 路由至真实服务
这是服务治理的基本原理流程
但是对目前的场景不适用,目前的场景,基本可以理解为把spring-cloud-gateway 当nginx用
路由到后端静态的几个地址即可
以es为例 3个client节点
es-client-01 192.168.10.11:9200
es-client-02 192.168.10.12:9200
es-client-03 192.168.10.13:9200
并不存在一个有效的注册中心,实际多一个注册中心组件,项目的复杂度就更高了,对es 这类服务组件,并不需要和spring的生态完全结合
我们需要定制lb://
的解析,即,使lb://
不通过注册中心,而是完全静态配置在本地(先搞静态吧,以后再考虑搞动态,此动态非彼动态)
需要注意withHealthChecks()
顾名思义 添加了withHealthChecks() 则会对 后端server进行健康检查,检查方式为验证对应的后端server /actuator/health
是否可达。这个路径是spring-boot/spring-cloud 组件的默认路径,但后端的es服务,这个路径并不可达。.withHealthChecks() 会返回Service Unavailable
{"timestamp":"2021-06-02T09:04:00.596+00:00","path":"/","status":503,"error":"Service Unavailable","requestId":"d3d01e2e-1"}
@Bean
public ServiceInstanceListSupplier discoveryClientServiceInstanceListSupplier(
ConfigurableApplicationContext context) {
return ServiceInstanceListSupplier.builder()
.withBase(new CustomServiceInstanceListSupplier())
// .withHealthChecks()
.build(context);
}
健康检查部分代码,关键注意/actuator/health
package org.springframework.cloud.loadbalancer.core;
public class HealthCheckServiceInstanceListSupplier extends DelegatingServiceInstanceListSupplier implements InitializingBean, DisposableBean {
private static final Log LOG = LogFactory.getLog(HealthCheckServiceInstanceListSupplier.class);
private final HealthCheck healthCheck;
private final String defaultHealthCheckPath;
private final Flux<List<ServiceInstance>> aliveInstancesReplay;
private Disposable healthCheckDisposable;
private final BiFunction<ServiceInstance, String, Mono<Boolean>> aliveFunction;
public HealthCheckServiceInstanceListSupplier(ServiceInstanceListSupplier delegate, HealthCheck healthCheck, BiFunction<ServiceInstance, String, Mono<Boolean>> aliveFunction) {
super(delegate);
this.defaultHealthCheckPath = (String)healthCheck.getPath().getOrDefault("default", "/actuator/health");
this.aliveFunction = aliveFunction;
this.healthCheck = healthCheck;
Repeat<Object> aliveInstancesReplayRepeat = Repeat.onlyIf((repeatContext) -> {
return this.healthCheck.getRefetchInstances();
}).fixedBackoff(healthCheck.getRefetchInstancesInterval());
Flux<List<ServiceInstance>> aliveInstancesFlux = Flux.defer(delegate).repeatWhen(aliveInstancesReplayRepeat).switchMap((serviceInstances) -> {
return this.healthCheckFlux(serviceInstances).map((alive) -> {
return Collections.unmodifiableList(new ArrayList(alive));
});
});
this.aliveInstancesReplay = aliveInstancesFlux.delaySubscription(healthCheck.getInitialDelay()).replay(1).refCount(1);
}
}
我们先不采用withHealthChecks,等后期有时间再自定义或配置withHealthChecks实现
package org.springframework.cloud.loadbalancer.core;
public class DiscoveryClientServiceInstanceListSupplier implements ServiceInstanceListSupplier {
public static final String SERVICE_DISCOVERY_TIMEOUT = "spring.cloud.loadbalancer.service-discovery.timeout";
private static final Log LOG = LogFactory.getLog(DiscoveryClientServiceInstanceListSupplier.class);
private Duration timeout = Duration.ofSeconds(30L);
private final String serviceId;
private final Flux<List<ServiceInstance>> serviceInstances;
public DiscoveryClientServiceInstanceListSupplier(DiscoveryClient delegate, Environment environment) {
this.serviceId = environment.getProperty("loadbalancer.client.name");
this.resolveTimeout(environment);
this.serviceInstances = Flux.defer(() -> {
return Flux.just(delegate.getInstances(this.serviceId));
}).subscribeOn(Schedulers.boundedElastic()).timeout(this.timeout, Flux.defer(() -> {
this.logTimeout();
return Flux.just(new ArrayList());
})).onErrorResume((error) -> {
this.logException(error);
return Flux.just(new ArrayList());
});
}
public DiscoveryClientServiceInstanceListSupplier(ReactiveDiscoveryClient delegate, Environment environment) {
this.serviceId = environment.getProperty("loadbalancer.client.name");
this.resolveTimeout(environment);
this.serviceInstances = Flux.defer(() -> {
return delegate.getInstances(this.serviceId).collectList().flux().timeout(this.timeout, Flux.defer(() -> {
this.logTimeout();
return Flux.just(new ArrayList());
})).onErrorResume((error) -> {
this.logException(error);
return Flux.just(new ArrayList());
});
});
}
public String getServiceId() {
return this.serviceId;
}
public Flux<List<ServiceInstance>> get() {
return this.serviceInstances;
}
private void resolveTimeout(Environment environment) {
String providedTimeout = environment.getProperty("spring.cloud.loadbalancer.service-discovery.timeout");
if (providedTimeout != null) {
this.timeout = DurationStyle.detectAndParse(providedTimeout);
}
}
private void logTimeout() {
if (LOG.isDebugEnabled()) {
LOG.debug(String.format("Timeout occurred while retrieving instances for service %s.The instances could not be retrieved during %s", this.serviceId, this.timeout));
}
}
private void logException(Throwable error) {
LOG.error(String.format("Exception occurred while retrieving instances for service %s", this.serviceId), error);
}
}