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
<?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官方文檔進行配置。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM