基于重写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)
})
相关推荐
码事漫谈8 分钟前
FDE 的起源、形态与真实边界
后端
行百里er11 分钟前
Spring Insight 里如何画出服务拓扑
spring boot·spring cloud·监控
ServBay21 分钟前
Grok Bot 还是 OpenClaw?聊聊真正能替人打杂的 AI 智能体
后端·aigc·grok
掘金挖土32 分钟前
前端手摸手跑路之 AI 应用开发(五)
前端·后端
弈栈录44 分钟前
LangGraph 入门:用状态机设计可靠的 Agent 工作流
后端·程序员
LEE2 小时前
前端转型全栈 00:AI 时代该学哪些,不该学哪些
前端·后端
Go_error2 小时前
上下文取消链:摧毁我们支付系统的 bug
后端·go
无限压榨切图仔3 小时前
一个优惠券需求改了三端,我才明白全栈不是多学一门语言
前端·后端
CRZZX3 小时前
阶段 0.1:为 AI Agent 项目建立敏感配置治理与安全基线
后端
Zane19943 小时前
用了Optional,为什么NPE还是防不住?
java·后端