spring-cloud-gateway负载普通web项目
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
用来做gateway -
spring-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
官方文档进行配置。
上一篇: 迦罗封神记
下一篇: 运用jieba库分词