名词解释
蓝绿发布:
优点:无缝的升级服务
缺点:消耗资源大,2倍的机器
滚动发布:
优点:也能实现无缝的升级服务,同时节约机器
缺点:发布当中,如果出现了问题,不好排查,到底是新系统的BUG还是老系统的BUG
灰度发布:
优点:新功能让一小部分人使用,相当于Beta版,不会影响主业务
如果该新功能反应效果好,再升级为所有人使用,如某信的“拍一拍”功能
实现新功能, a b testing,尽量减少用户使用的时延
节省了服务器,延时,和试错成本
发布方式
现在开发在日常的情况,大部分使用的是Spring Cloud,可以通过Eureka来注册服务,服务提供者和消费者都可以注册到Eureka中,
可以通过API来进行注册、下线、更新配置等操作,Eureka的操作文档地址为:
https://github.com/Netflix/eureka/wiki/Eureka-REST-operations。
在日常使用的情况时,在服务提供者和消费者都可以配置Eureka的元数据,也可以自定义元数据。可以通过:
PUT /eureka/v2/apps/appID/instanceID/metadata?key=value
来更新服务的元数据。
Zuul--> 服务
Zuul的依赖中添加下面的依赖:
<dependency>
<groupId>io.jmnarloch</groupId>
<artifactId>ribbon-discovery-filter-spring-cloud-starter</artifactId>
<version>2.1.0</version>
</dependency>
添加相应的filter文件:
package com.edu.cloudzuul.filter;
import com.edu.cloudzuul.dao.CommonGrayRuleDaoCustom;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import io.jmnarloch.spring.cloud.ribbon.support.RibbonFilterContextHolder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.context.annotation.FilterType;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
@Component
public class GrayFilter extends ZuulFilter {
@Override
public String filterType() {
return FilterConstants. ROUTE_TYPE;
}
@Override
public int filterOrder() {
return 0;
}
@Override
public boolean shouldFilter() {
return true;
}
@Autowired
private CommonGrayRuleDaoCustom commonGrayRuleDaoCustom;
@Override
public Object run() throws ZuulException {
RequestContext currentContext = RequestContext.getCurrentContext();
HttpServletRequest request = currentContext.getRequest();
int userId = Integer.parseInt(request.getHeader("userId"));
// 根据用户id 查 规则 查库 v1,meata
// 将数据从Eureka的meta-data、redis、数据库、guava 缓存 中获取数据,然后根据规则转发请求
// 数据库表可以这样设计: id | user_id | service_name | meta_version
// 这里只是简单实现了转发规则,userId == 1的 转发到 metadata version = v1的服务
// 金丝雀
if (userId == 1){
RibbonFilterContextHolder.getCurrentContext().add("version","v1");
// 普通用户
}else if (userId == 2){
RibbonFilterContextHolder.getCurrentContext().add("version","v2");
}
return null;
}
}
在进行数据库设计的时候:
CREATE TABLE `gray_release_config` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`server_name` varchar(255) DEFAULT NULL, //服务名
`path` varchar(255) DEFAULT NULL,//需要进行灰度发布的接口路径
`percent` int(11) DEFAULT NULL,//负载均衡策略,百分之percent的请求转发到forward上
`forward` int(11) DEFAULT NULL,//自定义元数据值
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
也可以从guava、redis、apollo、eureka、数据库中获取这些数据。
RibbonFilterContextHolder是基于InheritableThreadLocal来传输数据的工具类,为什么要用InheritableThreadLocal而不是ThreadLocal?
在Spring Cloud中我们用Hystrix来实现断路器,默认是用信号量来进行隔离的,信号量的隔离方式用ThreadLocal在线程中传递数据是没问题的,
当隔离模式为线程时,Hystrix会将请求放入Hystrix 的线程池中执行,这时候某个请求就由A线程变成B线程了,ThreadLocal必然没有效果了,
这时候就用InheritableThreadLocal来传递数据。
服务之间调用:Ribbon Rule
下面是一个自定义rule的例子:
import com.netflix.loadbalancer.ILoadBalancer;
import com.netflix.loadbalancer.Server;
import com.netflix.loadbalancer.ZoneAvoidanceRule;
import org.apache.commons.lang.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ThreadLocalRandom;
import lombok.extern.slf4j.Slf4j;
/**
* 自定义灰度发布规则 2019-11-19 by david
*/
@Slf4j
@Service
public class GrayRule extends ZoneAvoidanceRule {
/**
* 在choose方法中,自定义规则,返回的Server就是具体选择出来的服务
*
* @param key 服务key
* @return 可用server
*/
@Override
public Server choose(Object key) {
// 获取负载均衡接口
ILoadBalancer loadBalancer = this.getLoadBalancer();
// 获取到所有存活的服务
List<Server> allServers = loadBalancer.getAllServers();
// 获取到需要路由的服务
List<Server> serverList = this.getPredicate().getEligibleServers(allServers, key);
log.info("[gray choose] key:{}; allServers:{}; serverList:{}", key, allServers, serverList);
// 如果服务列表为空则返回null
if (CollectionUtils.isEmpty(serverList)) {
log.warn("=====GrayRule choose serverList isEmpty key:{}=====", key);
return null;
}
// 灰度开关,检查是否开启灰度服务开启时扫描灰度列表,避免每次扫描列表增大开销
String switchValue = JedisClusterUtils.get(GrayConstants.GRAY_SWITCH);
if (StringUtils.isBlank(switchValue) || "0".equals(switchValue)) {
return getRandom(serverList);
}
// 灰度服务列表
final Map<String, String> grayAddress = JedisClusterUtils.hgetAll(GrayConstants.GRAY_ADDRESS);
if (CollectionUtils.isEmpty(grayAddress)) {
log.info("[choose] : grayAddress isEmpty return serverList:{}", serverList);
return getRandom(serverList);
}
List<String> grayServers = new ArrayList<>(grayAddress.keySet());
// 查找非灰度服务并返回
List<Server> noGrayServerList = serverList.stream().filter(x -> !grayServers.contains(x.getHostPort())).collect(Collectors.toList());
return noGrayServerList.isEmpty() ? null : getRandom(noGrayServerList);
}
/**
* 随机返回一个可用服务
*
* @param serverList 服务列表
* @return 随机获取的服务
*/
private static Server getRandom(List<Server> serverList) {
return CollectionUtils.isEmpty(serverList) ? null : serverList.get(ThreadLocalRandom.current().nextInt(serverList.size()));
}
}
为了针对进来的请求进行灰度发布,需要使用AOP来获取请求中的一些数据,为了使用当前线程的数据,就会使用到ThreadLocal,来获取这个线程的数据,需要
使用方法来获取到:
/**
* 用于 保存、获取 每个线程中的 request header
*/
@Component
public class RibbonParameters {
private static final ThreadLocal local = new ThreadLocal();
public static <T> T get(){
return (T)local.get();
}
public static <T> void set(T t){
local.set(t);
}
}
/**
* 拦截请求,AOP实现,获取request header
*/
@Aspect
@Component
public class RequestAspect {
/**
* 定义切入点
*/
@Pointcut("execution(* com.edu.apipassenge.controller..*Controller*.*(..))")
private void anyMehtod(){
}
/**
* 在之前切入
* 此时IDEA中左侧栏能看到被拦截的方法
* @param joinPoint
*/
@Before(value = "anyMehtod()")
public void before(JoinPoint joinPoint){
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
String version = request.getHeader("version");
// Map<String,String> map = new HashMap<>();
// map.put("version",version);
// RibbonParameters.set(map); //写入ThreadLocal
//灰度规则 匹配的地方 查db, redis
if (version.trim().equals("v1")) {
RibbonFilterContextHolder.getCurrentContext().add("version", "v1");
} else if (version.trim().equals("v2")){
RibbonFilterContextHolder.getCurrentContext().add("version", "v2");
}
}
}
设置配置类:
/**
* 自定义Ribbon配置,用于启动类
*/
public class GrayRibbonConfiguration {
@Bean
public IRule ribbonRule(){
return new GreyRule();
}
}
在controller里面需要进行设置:
@SpringBootApplication
@RibbonClient(name = "service-sms" , configuration = GrayRibbonConfiguration.class)
public class ApiPassengeApplication {
@LoadBalanced
@Bean
public RestTemplate restTemplate(){
return new RestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(ApiPassengeApplication.class, args);
}
}
参考代码地址为:https://github.com/yishengxiaoyao/gray-publish
参考文献
Eureka REST operations
SpringCloud灰度发布实践(附源码)
Spring Cloud使用Zuul和Ribbon做灰度发布
微服务Zuul网关进行灰度发布
灰度发布的原理及实现
灰度发布落地实战2
灰度发布落地实战1
谈谈微服务平台之灰度发布
SpringCloud-灰度发布
网友评论