基于重写ribbon负载实现灰度发布

项目结构如下

代码如下:

pom:

java 复制代码
<?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>
    <parent>
        <groupId>sca.pro</groupId>
        <artifactId>sca-parent</artifactId>
        <version>1.0.1</version>
    </parent>

    <groupId>sca.gary.publish</groupId>
    <artifactId>gray-spring-boot-starter</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <!-- Compile dependencies -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>com.netflix.ribbon</groupId>
            <artifactId>ribbon-loadbalancer</artifactId>
            <scope>provided</scope>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>transmittable-thread-local</artifactId>
            <version>2.14.2</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <dependency>
            <groupId>io.github.openfeign</groupId>
            <artifactId>feign-core</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-web</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <encoding>${project.build.sourceEncoding}</encoding>
                </configuration>
            </plugin>
        </plugins>
    </build>


</project>

spring.factories

java 复制代码
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
sca.gary.publish.graypublish.config.GrayConfig,\
sca.gary.publish.graypublish.feignInterceptor.FeignRequestInterceptor

GrayConfig:可以在对应服务启动类添加如下,可写多个服务

复制代码
@RibbonClients(value = {
        @RibbonClient(value = "nacos中的服务名称",configuration = GrayConfig.class)
})
java 复制代码
package sca.gary.publish.graypublish.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import sca.gary.publish.graypublish.GrayRule;

//@Configuration
之前理解有误,这里千万不能加它,否则服务调用会混乱
public class GrayConfig {
    @Bean
    public GrayRule grayRule(){
        return new GrayRule();
    }
}
复制代码
ContantsString:版本号请求头key
java 复制代码
package sca.gary.publish.graypublish.contans;

public class ContantsString {
    public static final String GRAY_KEY="version";

}
复制代码
FeignRequestInterceptor:feign拦截器,当远程调用时,将版本号保存到ttl中,供给服务负载使用,并把当前请求头中的版本号放在远程调用的请求头中,防止它仍需要远程调用
java 复制代码
package sca.gary.publish.graypublish.feignInterceptor;

import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.context.annotation.Import;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import sca.gary.publish.graypublish.contans.ContantsString;
import sca.gary.publish.graypublish.ttl.ThreadLocalUtils;

import javax.servlet.http.HttpServletRequest;
import java.util.Enumeration;
import java.util.LinkedHashMap;
import java.util.Map;

@Component
public class FeignRequestInterceptor  implements RequestInterceptor {
    @Override
    public void apply(RequestTemplate template) {
        ServletRequestAttributes sra = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = sra.getRequest();
        Map<String, String> headers = getHeaders(request);
        for (Map.Entry<String, String> entry : headers.entrySet()) {
            //② 设置请求头到新的Request中
            template.header(entry.getKey(), entry.getValue());
        }
    }

    /**
     * 获取原请求头
     */
    private Map<String, String> getHeaders(HttpServletRequest request) {
        Map<String, String> map = new LinkedHashMap<>();
        Enumeration<String> enumeration = request.getHeaderNames();
        if (enumeration != null) {
            while (enumeration.hasMoreElements()) {
                String key = enumeration.nextElement();
                String value = request.getHeader(key);
                //将灰度标记的请求头透传给下个服务
                if (ContantsString.GRAY_KEY.equals(key)){
                    //① 保存灰度发布的标记
                    ThreadLocalUtils.set(ContantsString.GRAY_KEY,value);
                    map.put(key, value);
                }
            }
        }
        return map;
    }
}
复制代码
ThreadLocalUtils:用于存储网关传递过来的版本号,实现当前服务的负载选择
java 复制代码
package sca.gary.publish.graypublish.ttl;

import com.alibaba.ttl.TransmittableThreadLocal;
import java.util.HashMap;
import java.util.Map;

public class ThreadLocalUtils {

    private static TransmittableThreadLocal<Map<String, Object>> cache = new TransmittableThreadLocal<>();

    /**
     * 设置对象到本地变量
     *
     * @param object
     */
    public static void set(String key, Object object) {
        if (!isCaheIsNull()) {
            cache.get().put(key, object);
        } else {
            Map<String, Object> map = new HashMap<>();
            map.put(key, object);
            cache.set(map);
        }
    }

    /**
     * 从本地变量中获取变量
     *
     * @return
     */
    public static Object get(String key) {
        if (!isCaheIsNull()) {
            return cache.get().get(key);
        } else {
            return null;
        }
    }


    /**
     * 根据KEY移除缓存里的数据
     *
     * @param key
     */
    public static void remove(String key) {
        if (isCaheIsNull()) {
            return;
        } else {
            cache.get().remove(key);
        }
    }

    /**
     * 释放本地线程资源
     */
    public static void clear() {
        cache.remove();
    }

    /**
     * 是否存在本地变量
     *
     * @return
     */
    private static boolean isCaheIsNull() {
        return cache.get() == null;
    }

}
复制代码
GrayRule:

最重要的就是这个类,重写了ribbon的负载策略,通过从网关传递过来的版本号,和每个服务中的元数据版本号进行对比,如果相同则调用它们的版本,如果没有找到对应版本的服务,则将获取到的所有服务按照原规则进行负载

java 复制代码
package sca.gary.publish.graypublish;

import com.alibaba.cloud.nacos.ribbon.NacosServer;
import com.google.common.base.Optional;
import com.netflix.client.config.IClientConfig;
import com.netflix.loadbalancer.Server;
import com.netflix.loadbalancer.ZoneAvoidanceRule;
import org.springframework.util.ObjectUtils;
import sca.gary.publish.graypublish.contans.ContantsString;
import sca.gary.publish.graypublish.ttl.ThreadLocalUtils;

import java.util.ArrayList;
import java.util.List;

public class GrayRule extends ZoneAvoidanceRule {

    @Override
    public void initWithNiwsConfig(IClientConfig clientConfig) {
    }

    @Override
    public Server choose(Object key) {
        try {
            //从ThreadLocal中获取灰度标记
            Object version = ThreadLocalUtils.get(ContantsString.GRAY_KEY);

            //获取所有可用服务
            List<Server> serverList = this.getLoadBalancer().getReachableServers();
            //灰度发布的服务
            List<Server> grayServerList = new ArrayList<>();
            if (ObjectUtils.isEmpty(version)){
                return originChoose(serverList,key);
            }
            for(Server server : serverList) {
                NacosServer nacosServer = (NacosServer) server;
                //从nacos中获取元素剧进行匹配
                if(nacosServer.getMetadata().containsKey(ContantsString.GRAY_KEY)
                        && nacosServer.getMetadata().get(ContantsString.GRAY_KEY).equals(version.toString()) ){
                    grayServerList.add(server);
                }
            }
            if (!ObjectUtils.isEmpty(grayServerList)){
                return originChoose(grayServerList,key);
            }
            return originChoose(serverList,key);
        } finally {
            //清除灰度标记
            ThreadLocalUtils.clear();
        }
    }

    private Server originChoose(List<Server> noMetaServerList, Object key) {
        Optional<Server> serverOptional = getPredicate().chooseRoundRobinAfterFiltering(noMetaServerList, key);
        if (serverOptional.isPresent()) {
            return serverOptional.get();
        } else {
            return null;
        }
    }


}

使用方式:

1.首先保证每个服务都有feign的依赖

2.添加依赖如下

复制代码
<dependency>
    <groupId>sca.gary.publish</groupId>
    <artifactId>gray-spring-boot-starter</artifactId>
    <version>1.0.1</version>
    <exclusions>
        <exclusion>
            <groupId>io.github.openfeign</groupId>
            <artifactId>feign-core</artifactId>
        </exclusion>
    </exclusions>
</dependency>

3.yaml添加配置

spring:

cloud:

nacos:

discovery:

metadata:

version: 2.0

3.在启动类上添加下边的,需要远程调用哪个服务,就加哪几个

复制代码
@RibbonClients(value = {
        @RibbonClient(value = "服务名",configuration = GrayConfig.class)
})
相关推荐
柏油5 小时前
MySQL InnoDB 行锁
数据库·后端·mysql
咖啡调调。5 小时前
使用Django框架表单
后端·python·django
白泽talk5 小时前
2个小时1w字| React & Golang 全栈微服务实战
前端·后端·微服务
摆烂工程师5 小时前
全网最详细的5分钟快速申请一个国际 “edu教育邮箱” 的保姆级教程!
前端·后端·程序员
一只叫煤球的猫5 小时前
你真的会用 return 吗?—— 11个值得借鉴的 return 写法
java·后端·代码规范
Asthenia04126 小时前
HTTP调用超时与重试问题分析
后端
颇有几分姿色6 小时前
Spring Boot 读取配置文件的几种方式
java·spring boot·后端
AntBlack6 小时前
别说了别说了 ,Trae 已经在不停优化迭代了
前端·人工智能·后端
@淡 定6 小时前
Spring Boot 的配置加载顺序
java·spring boot·后端