SpringCloud灰度发布设计和实现
灰度发布
什么是灰度发布呢?要想了解这个问题就要先明白什么是灰度。灰度从字面意思理解就是存在于黑与白之间的一个平滑过渡的区域,所以说对于互联网产品来说,上线和未上线就是黑与白之分,而实现未上线功能平稳过渡的一种方式就叫做灰度发布。
在一般情况下,升级服务器端应用,需要将应用源码或程序包上传到服务器,然后停止掉老版本服务,再启动新版本。但是这种简单的发布方式存在两个问题,一方面,在新版本升级过程中,服务是暂时中断的,另一方面,如果新版本有BUG,升级失败,回滚起来也非常麻烦,容易造成更长时间的服务不可用。
在了解了什么是灰度发布的定义以后,就可以来了解一下灰度发布的具体操作方法了。可以通过抽取一部分用户,比如说选择自己的测试用户,使这些用户的请求全部访问灰度发布的服务,正式用户请求走正常上线的服务,那么就能够将需要灰度发布的版本也连接到正常上线服务中进行测试,没有问题之后将其设置为正常服务即完成版本的上线测试和发布。
实现设计
实现重点主要在:
- 利用 ThreadLocal+Feign 实现 http head 中实现信息的传递
- 使用Nacos的元数据,定义需要的灰度服务
- 自定义Ribbon的路由规则,根据Nacos的元数据选择服务节点
公共配置
ThreadLocal
public class PassParameters {private static final Logger log = LoggerFactory.getLogger(PassParameters.class);private static final ThreadLocal localParameters = new ThreadLocal();public static PassParametersModel get(){PassParametersModel model = (PassParametersModel) localParameters.get();log.info("ThreadID:{}, threadLocal {}", Thread.currentThread().getId(), model.toString());return model;}public static void set(PassParametersModel model){log.info("ThreadID:{}, threadLocal set {}", Thread.currentThread().getId(), model.toString());localParameters.set(model);}}
携带灰度发布相关数据的类
public class PassParametersModel {private String token;private String grayPublish;}
请求头常量
public class HttpConstants {public static final String AUTHORIZATION_HEADER = "Authorization";public static final String GRAY_PUBLISH = "Gray-Publish";public static final String GRAY_PUBLISH_MEAT_KEY = "grayPublish";
}
AOP请求拦截处理
@Aspect
@Order(1)
@Component
public class PermissionsValidationAspect {private final Logger logger = LoggerFactory.getLogger(getClass());/* 权限校验以及一些参数的赋值O* @param joinPoint* @return* @throws Throwable*/@Around(value = "需要拦截的方法")public Object check(ProceedingJoinPoint joinPoint) throws Throwable {// 获取一些参数ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();HttpServletRequest request = attributes.getRequest();String token = request.getHeader(HttpConstants.AUTHORIZATION_HEADER);// FIXME:// token权限校验,不要去查数据库!!!!// 来这里的token肯定都是有用的,因为在网关那里就会进行token有效的校验// 校验成功之后进行用户身份的赋值,比如uid、username等关键信息// 本次请求涉及到的灰度发布// FIXME: 通过用户所属的用户组或其他标识信息,专门指定某些用户是灰度发布测试用户,来设置灰度发布的字段String grayPublish = request.getHeader(HttpConstants.GRAY_PUBLISH);PassParametersModel model = new PassParametersModel();model.setToken(token);model.setGrayPublish(grayPublish);PassParameters.set(model);return joinPoint.proceed();}
}
Feign配置
Ribbon请求规则
将会从Nacos中获取元服务器的信息,并根据这个信息选择服务器。
public class GrayPublishRibbonRule extends AbstractLoadBalancerRule {private ILoadBalancer lb;private static Logger log = LoggerFactory.getLogger(GrayPublishRibbonRule.class);private AtomicInteger nextServerCyclicCounter;public GrayPublishRibbonRule() {nextServerCyclicCounter = new AtomicInteger(0);}@Overridepublic Server choose(Object key) {lb = getLoadBalancer();List<Server> servers = lb.getReachableServers(); // 获取所有UP的服务if (servers.isEmpty()) {return null;}List<Server> normalServerList = new ArrayList<>();Map<String, List<Server>> grayPublishMap = new HashMap<>();for (Server s : servers){NacosServer nacosServer = (NacosServer) s;Map<String, String> metadata = nacosServer.getMetadata();String grayPublishKey = metadata.get(HttpConstants.GRAY_PUBLISH_MEAT_KEY);if (grayPublishKey == null) {normalServerList.add(s);} else {List<Server> grayPublishList = grayPublishMap.computeIfAbsent(grayPublishKey, k -> new ArrayList<>());grayPublishList.add(s);}}PassParametersModel model = PassParameters.get();// 不是灰度发布请求,走正常的服务if (model.getGrayPublish() == null) {return defaultLoadBalanceChoose(normalServerList, key);}List<Server> currentServerList = grayPublishMap.get(model.getGrayPublish());// 是灰度发布,但是没有找到对应处理,也走正常的if (currentServerList == null || currentServerList.isEmpty()) {return defaultLoadBalanceChoose(normalServerList, key);}// 灰度发布,走对应的服务return defaultLoadBalanceChoose(currentServerList, key);}@Overridepublic void initWithNiwsConfig(IClientConfig iClientConfig) {}// 负载均衡public Server defaultLoadBalanceChoose(List<Server> reachableServers, Object key) {if (reachableServers == null || reachableServers.isEmpty()) {log.warn("No up servers available from load balancer: " + lb);return null;}Server server = null;int count = 0;while (count++ < 10) {int upCount = reachableServers.size();int nextServerIndex = incrementAndGetModulo(upCount);server = reachableServers.get(nextServerIndex);if (server == null) {/* Transient. */Thread.yield();continue;}if (server.isAlive() && (server.isReadyToServe())) {return (server);}// Next.server = null;}if (count >= 10) {log.warn("No available alive servers after 10 tries from load balancer: "+ lb);}return server;}/* Inspired by the implementation of {@link AtomicInteger#incrementAndGet()}. @param modulo The modulo to bound the value of the counter.* @return The next value.*/private int incrementAndGetModulo(int modulo) {for (;;) {int current = nextServerCyclicCounter.get();int next = (current + 1) % modulo;//求余法if (nextServerCyclicCounter.compareAndSet(current, next))return next;}}
}
Feign拦截器
将需要传递给下个微服务中的数据进行赋值
public class GrayPublishInterceptor implements RequestInterceptor {private Logger log = LoggerFactory.getLogger(this.getClass());@Overridepublic void apply(RequestTemplate requestTemplate) {PassParametersModel model = PassParameters.get();// tokenif (StringUtils.isNotEmpty(model.getToken())) {requestTemplate.header(HttpConstants.AUTHORIZATION_HEADER, model.getToken());}// grayPublishif (StringUtils.isNotEmpty(model.getGrayPublish())) {requestTemplate.header(HttpConstants.GRAY_PUBLISH, model.getGrayPublish());}// FIXME: 补充其他需要传递的内容}
}
配置使用
这里是全局配置使用
@Configuration
public class FeignConfig {// 灰度发布Ribbon规则@Beanpublic IRule getRule(){return new GrayPublishRibbonRule();}// header传递@Beanpublic GrayPublishInterceptor getInterceptor(){return new GrayPublishInterceptor();}}
Nacos元数据配置
通过在application.yml进行配置
# 服务的元数据
spring:cloud:nacos:discovery:metadata.grayPublish: HAHAHA_TEST