spring-cloud-gateway负载普通web项目

/ Spring Cloud / 没有评论 / 192浏览

对于普通的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负载思路

这样,我们能保证用户在本次会话内访问的是同一个节点,就不需要帆软9.0的集群机制了。

实现

基于spring cloud 2.x

依赖

我们需要使用spring-cloud-starter-gatewayspring-cloud-starter-netflix-ribbon

其中:

<?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🇼🇸//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官方文档进行配置。