spring cloud gateway負載普通web項目 對於普通的web項目,也是可以通過 進行負載的,只是無法通過服務發現。 背景 不知道各位道友有沒有使用過 "帆軟" ,帆軟是國內一款報表工具,這裡不做過多介紹。 它是通過war包部署到 ,預設是單台服務。如果想做集群,需要配置 ,帆軟會將當 ...
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
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>xxx</groupId>
<artifactId>yyy</artifactId>
<version>1.0.0</version>
<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<project.build.sourceEncoding>utf-8</project.build.sourceEncoding>
<spring.boot.version>2.1.2.RELEASE</spring.boot.version>
<spring.cloud.version>2.1.0.RELEASE</spring.cloud.version>
<slf4j.version>1.7.25</slf4j.version>
</properties>
<repositories>
<repository>
<id>aliyun</id>
<name>aliyun maven</name>
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
<version>${spring.cloud.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
<version>${spring.cloud.version}</version>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.5</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>2.9.8</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.9.8</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.9.8</version>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring.boot.version}</version>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
核心配置
主要是通過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<Server> 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<Server> 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<Server> 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
官方文檔進行配置。