spring-cloud-gateway负载普通web项目
对于普通的web项目,也是可以通过
spring-cloud-gateway
进行负载的,只是无法通过服务发现。
背景
不知道各位道友有没有使用过帆软,帆软是国内一款报表工具,这里不做过多介绍。
它是通过war包部署到tomcat
,默认是单台服务。如果想做集群,需要配置cluster.xml
,帆软会将当前节点的请求转发给主节点(一段时间内)。
在实际工作中,部署四个节点时,每个节点启动需要10分钟以上(单台的情况下,则需要一两分钟)。而且一段时间内其他节点会将请求转发给主节点,存在单点压力。
于是,通过spring-cloud-gateway
来负载帆软节点。
帆软集群介绍
在帆软9.0,如果部署A、B两个节点,当查询A节点后,正确返回结果;如果被负载到B,那么查询是无法拿到结果的。可以认为是session(此session非web中的session)不共享的,帆软是B通过将请求转发给A执行来解决共享问题的。
gateway负载思路
- 对于非登录的用户(此时我们是用不了帆软的),直接采用随机请求转发到某个节点即可
- 对于登录的用户,根据sessionId去hash,在本次会话内一直访问帆软的同一个节点
这样,我们能保证用户在本次会话内访问的是同一个节点,就不需要帆软9.0的集群机制了。
实现
基于spring cloud 2.x
依赖
我们需要使用spring-cloud-starter-gateway
、spring-cloud-starter-netflix-ribbon
。
其中:
spring-cloud-starter-gateway
用来做gatewayspring-cloud-starter-netflix-ribbon
做客户端的LoadBalancer
4.0.0
xxx
yyy
1.0.0
1.8
1.8
utf-8
2.1.2.RELEASE
2.1.0.RELEASE
1.7.25
aliyun
aliyun maven
http://maven.aliyun.com/nexus/content/groups/public/
org.springframework.cloud
spring-cloud-starter-gateway
${spring.cloud.version}
org.springframework.cloud
spring-cloud-starter-netflix-ribbon
${spring.cloud.version}
org.slf4j
slf4j-api
${slf4j.version}
org.apache.httpcomponents
httpclient
4.5.5
com.fasterxml.jackson.core
jackson-annotations
2.9.8
com.fasterxml.jackson.core
jackson-core
2.9.8
com.fasterxml.jackson.core
jackson-databind
2.9.8
org.apache.maven.plugins
maven-compiler-plugin
3.8.0
1.8
org.springframework.boot
spring-boot-maven-plugin
${spring.boot.version}
repackage
核心配置
主要是通过lb
指定服务名,ribbon
指定多个服务实例(微服务是从注册中心中获取的)来进行负载。
spring:
cloud:
gateway:
routes:
# http
- id: host_route
# lb代表服务名,后面从ribbon的服务列表中获取(其实微服务是从注册中心中获取的)
# 这里负载所有的http请求
uri: lb://xx-http
predicates:
- Path=/**
filters:
# 请求限制5MB
- name: RequestSize
args:
maxSize: 5000000
# ws
- id: websocket_route
# lb代表服务名,后面从ribbon的服务列表中获取(其实微服务是从注册中心中获取的)
# 这里负载所有的websocket
uri: lb:ws://xx-ws
predicates:
- Path=/websocket/**
xx-http:
ribbon:
# 服务列表
listOfServers: http://172.16.242.156:15020, http://172.16.242.192:15020
# 10s
ConnectTimeout: 10000
# 10min
ReadTimeout: 600000
# 最大的连接
MaxTotalHttpConnections: 500
# 每个实例的最大连接
MaxConnectionsPerHost: 300
xx-ws:
ribbon:
# 服务列表
listOfServers: ws://172.16.242.156:15020, ws://172.16.242.192:15020
# 10s
ConnectTimeout: 10000
# 10min
ReadTimeout: 600000
# 最大的连接
MaxTotalHttpConnections: 500
# 每个实例的最大连接
MaxConnectionsPerHost: 300
之后,我们需要自定义负载均衡过滤器、以及规则。
自定义负载均衡过滤器
主要是通过判断请求是否携带session,如果携带说明登录过,则后面根据sessionId去hash,在本次会话内一直访问帆软的同一个节点;否则默认随机负载即可。
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.cloud.gateway.config.LoadBalancerProperties;
import org.springframework.cloud.gateway.filter.LoadBalancerClientFilter;
import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
import org.springframework.cloud.netflix.ribbon.RibbonLoadBalancerClient;
import org.springframework.http.HttpCookie;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
import java.net.URI;
import java.util.Objects;
/**
* 自定义负载均衡过滤器
*
* @author 奔波儿灞
* @since 1.0
*/
public class CustomLoadBalancerClientFilter extends LoadBalancerClientFilter {
private static final String COOKIE = "SESSIONID";
public CustomLoadBalancerClientFilter(LoadBalancerClient loadBalancer, LoadBalancerProperties properties) {
super(loadBalancer, properties);
}
@Override
protected ServiceInstance choose(ServerWebExchange exchange) {
// 获取请求中的cookie
HttpCookie cookie = exchange.getRequest().getCookies().getFirst(COOKIE);
if (cookie == null) {
return super.choose(exchange);
}
String value = cookie.getValue();
if (StringUtils.isEmpty(value)) {
return super.choose(exchange);
}
if (this.loadBalancer instanceof RibbonLoadBalancerClient) {
RibbonLoadBalancerClient client = (RibbonLoadBalancerClient) this.loadBalancer;
Object attrValue = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR);
Objects.requireNonNull(attrValue);
String serviceId = ((URI) attrValue).getHost();
// 这里使用session做为选择服务实例的key
return client.choose(serviceId, value);
}
return super.choose(exchange);
}
}
自定义负载均衡规则
核心就是实现choose
方法,从可用的servers列表中,选择一个server去负载。
import com.netflix.client.config.IClientConfig;
import com.netflix.loadbalancer.AbstractLoadBalancerRule;
import com.netflix.loadbalancer.Server;
import org.apache.commons.lang.math.RandomUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.CollectionUtils;
import java.util.List;
/**
* 负载均衡规则
*
* @author 奔波儿灞
* @since 1.0
*/
public class CustomLoadBalancerRule extends AbstractLoadBalancerRule {
private static final Logger LOG = LoggerFactory.getLogger(CustomLoadBalancerRule.class);
private static final String DEFAULT_KEY = "default";
private static final String RULE_ONE = "one";
private static final String RULE_RANDOM = "random";
private static final String RULE_HASH = "hash";
@Override
public void initWithNiwsConfig(IClientConfig iClientConfig) {
}
@Override
public Server choose(Object key) {
List servers = this.getLoadBalancer().getReachableServers();
if (CollectionUtils.isEmpty(servers)) {
return null;
}
// 只有一个服务,则默认选择
if (servers.size() == 1) {
return debugServer(servers.get(0), RULE_ONE);
}
// 多个服务时,当cookie不存在时,随机选择
if (key == null || DEFAULT_KEY.equals(key)) {
return debugServer(randomChoose(servers), RULE_RANDOM);
}
// 多个服务时,cookie存在,根据cookie hash
return debugServer(hashKeyChoose(servers, key), RULE_HASH);
}
/**
* 随机选择一个服务
*
* @param servers 可用的服务列表
* @return 随机选择一个服务
*/
private Server randomChoose(List servers) {
int randomIndex = RandomUtils.nextInt(servers.size());
return servers.get(randomIndex);
}
/**
* 根据key hash选择一个服务
*
* @param servers 可用的服务列表
* @param key 自定义key
* @return 根据key hash选择一个服务
*/
private Server hashKeyChoose(List servers, Object key) {
int hashCode = Math.abs(key.hashCode());
if (hashCode < servers.size()) {
return servers.get(hashCode);
}
int index = hashCode % servers.size();
return servers.get(index);
}
/**
* debug选择的server
*
* @param server 具体的服务实例
* @param name 策略名称
* @return 服务实例
*/
private Server debugServer(Server server, String name) {
LOG.debug("choose server: {}, rule: {}", server, name);
return server;
}
}
Bean配置
自定义之后,我们需要激活Bean,让过滤器以及规则生效。
import com.netflix.loadbalancer.IRule;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.cloud.gateway.config.LoadBalancerProperties;
import org.springframework.cloud.gateway.filter.LoadBalancerClientFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 负载均衡配置
*
* @author 奔波儿灞
* @since 1.0
*/
@Configuration
public class LoadBalancerConfiguration {
/**
* 自定义负载均衡过滤器
*
* @param client LoadBalancerClient
* @param properties LoadBalancerProperties
* @return CustomLoadBalancerClientFilter
*/
@Bean
public LoadBalancerClientFilter customLoadBalancerClientFilter(LoadBalancerClient client,
LoadBalancerProperties properties) {
return new CustomLoadBalancerClientFilter(client, properties);
}
/**
* 自定义负载均衡规则
*
* @return CustomLoadBalancerRule
*/
@Bean
public IRule customLoadBalancerRule() {
return new CustomLoadBalancerRule();
}
}
启动
这里是标准的spring boot程序启动。
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* 入口
*
* @author 奔波儿灞
* @since 1.0
*/
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
补充
请求头太长错误
由于spring cloud gateway
使用webflux
模块,底层是netty
。如果超过netty
默认的请求头长度,则会报错。
默认的最大请求头长度配置reactor.netty.http.server.HttpRequestDecoderSpec
,目前我采用的是比较蠢的方式直接覆盖了这个类。哈哈。
断路器
由于是报表项目,一个报表查询最低几秒,就没用hystrix
组件了。可以参考spring cloud gateway
官方文档进行配置。