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-gatewayspring-cloud-starter-netflix-ribbon

其中:

  • spring-cloud-starter-gateway用来做gateway
  • spring-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
                    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官方文档进行配置。

你可能感兴趣的:(spring-cloud-gateway负载普通web项目)