欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

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

程序员文章站 2022-04-28 10:25:48
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-gatewayspring-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官方文档进行配置。