spring cloud实战总结(优雅下线、灰度发布)
1、nacos优雅下线
@Endpoint(id = "shutdownRegister"
)
@Configuration
@Slf4j
public class ShutdownRegisterEndpoint implements ApplicationContextAware {private static final Map<String, String> NO_CONTEXT_MESSAGE = Collections.singletonMap("message", "No context to shutdownRegister.");private static final Map<String, String> SHUTDOWN_MESSAGE = Collections.singletonMap("message", "Register Shutting down, bye...");@Autowired(required = false)NacosServiceRegistry nacosServiceRegistry;@Autowired(required = false)NacosRegistration registration;private ConfigurableApplicationContext context;@Autowired(required = false)private ApplicationContext applicationContext;public ShutdownRegisterEndpoint() {log.info("[组件init]注销服务组件");}@WriteOperationpublic Map<String, String> shutdownRegister() {if (this.context == null) {return NO_CONTEXT_MESSAGE;}if (nacosServiceRegistry != null && registration != null) {log.info("注销nacos");nacosServiceRegistry.deregister(registration);}if (applicationContext != null) {applicationContext.publishEvent(new EleServerDownEvent(new Object()));}return SHUTDOWN_MESSAGE;}public void setApplicationContext(ApplicationContext context) throws BeansException {if (context instanceof ConfigurableApplicationContext) {this.context = (ConfigurableApplicationContext) context;}}
}public class EleServerDownEvent extends ApplicationEvent {private static final long serialVersionUID = 1305017141473336210L;public EleServerDownEvent(Object source) {super(source);}
}
使用方式:
# 健康监控
management:server:port: 18001endpoints:web:exposure:include: "info,health,shutdown,shutdownRegister"endpoint:shutdown:enabled: truehealth:show-details: always然后脚本启动命令如下:
curl --connect-timeout 5 -X POST http://$host_ip:18000/actuator/shutdownRegister
sleep 10
curl --connect-timeout 5 -X POST http://$host_ip:18000/actuator/shutdown
sleep 30
2、流量灰度发布
灰度发布通过在网关层根据特定规则将部分流量路由到新版本的服务实例上,同时保持大部分流量仍路由到旧版本服务实例,从而实现逐步过渡到新版本的目的。
主要步骤是:将应用部署到灰度实例上,在nacos元数据标识灰度服务,然后通过网关获取到url灰度标识,通过拦截器路由算法路由到灰度实例上
1、在url中header加入灰度标识,例如: version: gray
2、网关匹配路由
/*** @Description 灰度负载均衡**/
public class GrayGatewayLoadBalancerFilter extends LoadBalancerClientFilter {@Resourceprivate DiscoveryClient discoveryClient;public GrayGatewayLoadBalancerFilter(LoadBalancerClient loadBalancer, LoadBalancerProperties properties) {super(loadBalancer, properties);}@Overrideprotected ServiceInstance choose(ServerWebExchange exchange) {//所有服务数据URI originalUrl = exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR);if (originalUrl == null) {return super.choose(exchange);}String targetServerId = originalUrl.getHost();List<ServiceInstance> serverList = discoveryClient.getInstances(targetServerId);ServiceInstance choose = GrayGatewayGrayRouteUtil.choose(exchange, serverList);if (choose != null) {return choose;}return super.choose(exchange);}@Overridepublic int getOrder() {return Ordered.LOWEST_PRECEDENCE;}}
@Slf4j
public class GrayGatewayGrayRouteUtil {private static final AtomicInteger ATOMIC_INTEGER = new AtomicInteger(0);private static final AtomicInteger GRAY_ATOMIC_INTEGER = new AtomicInteger(0);private GrayGatewayGrayRouteUtil() {}public static ServiceInstance choose(ServerWebExchange exchange, List<ServiceInstance> serverList) {//所有服务数据URI originalUrl = exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR);if (originalUrl == null) {log.error("error originalUrl is null");return null;}//没有服务返回空if (CollUtil.isEmpty(serverList)) {log.error("error serverList is isEmpty.");return null;}String grayHeader = exchange.getRequest().getHeaders().getFirst("version");List<ServiceInstance> instanceList = filterInstanceList(serverList, grayHeader);if (Objects.nonNull(instanceList) && CollUtil.isNotEmpty(instanceList)) {ServiceInstance serviceInstance = null;if(StringUtils.isNotBlank(grayHeader)){log.info("gray ServiceInstance grayHeader: {}", grayHeader);serviceInstance = instanceList.get(getGrayAndIncrement() % instanceList.size());} else {serviceInstance = instanceList.get(getAndIncrement() % instanceList.size());}return serviceInstance;}/*** 兜底* 1、不容错:所有正常的机器都在重启or 都宕机了 不可以请求到灰度服务上(第一版本的灰度发布策略只有1台灰度机器,这种极端情况下如果允许分发请求,1台机器承受不住所有的请求压力)* 2、灰度机器不存在,随机一台实例*/return StringUtils.isNotBlank(grayHeader) ? serverList.get(getAndIncrement() % serverList.size()) : null;}private static List<ServiceInstance> filterInstanceList(List<ServiceInstance> serverList, String grayHeader) {return serverList.stream().filter(item -> {Map<String, String> metadata = item.getMetadata();String serverGrayValue = metadata.get(GrayRouterConstants.GRAY_REGISTRY_TAG);//非灰度请求返回非灰度机器if (StringUtils.isBlank(grayHeader) && StringUtils.isBlank(serverGrayValue)) {return true;}//灰度请求返回灰度机器return StringUtils.isNotBlank(grayHeader) && StringUtils.isNotBlank(serverGrayValue) && serverGrayValue.equals(grayHeader);}).collect(Collectors.toList());}/*** 计算得到当前调用次数** @return*/public static int getAndIncrement() {int current;int next;do {current = ATOMIC_INTEGER.get();next = current >= Integer.MAX_VALUE ? 0 : current + 1;} while (!ATOMIC_INTEGER.compareAndSet(current, next));return next;}/*** 计算得到当前调用次数** @return*/public static int getGrayAndIncrement() {int current;int next;do {current = GRAY_ATOMIC_INTEGER.get();next = current >= Integer.MAX_VALUE ? 0 : current + 1;} while (!GRAY_ATOMIC_INTEGER.compareAndSet(current, next));return next;}
}
3、nacos灰度配置
spring:cloud:nacos:discovery:metadata:version: gray
4、应用层保存灰度信息,放到ThreadLocal中
/*** web 根据header的标识,设置当前线程走灰度服务参数*/
@Slf4j
public class GrayRouteFilter implements Filter {@Overridepublic void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {HttpServletRequest httpRequest = (HttpServletRequest) request;String grayName = httpRequest.getHeader(GrayRouterConstants.REQUEST_HEADER);if (StringUtils.isBlank(grayName)) {chain.doFilter(request, response);return;}GrayThreadLocal.setGrayName(grayName);try {log.info(" gray request grayName:{} ",grayName);chain.doFilter(request, response);} finally {GrayThreadLocal.remove();}}
}
@Beanpublic FilterRegistrationBean<GrayRouteFilter> registerGrayRouteFilter() {FilterRegistrationBean<GrayRouteFilter> registration = new FilterRegistrationBean<>();registration.setFilter(new GrayRouteFilter());registration.addUrlPatterns("/*");registration.setName("grayRouteFilter");// 值越小,Filter越靠前。registration.setOrder(3);return registration;}
5、dubbo、feign配置
@Activate(group = {CommonConstants.CONSUMER, CommonConstants.PROVIDER}, order = 5)
@Slf4j
public class DubboGrayCodeFilter implements Filter {@Overridepublic Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {if (RpcContext.getContext().isConsumerSide()) {// Consumerreturn doConsumerFilter(invoker, invocation);}if (RpcContext.getContext().isProviderSide()) {// Providerreturn doProviderFilter(invoker, invocation);}return invoker.invoke(invocation);}/*** 服务提供方过滤** @param invoker* @param invocation* @return*/public Result doProviderFilter(Invoker<?> invoker, Invocation invocation) {GrayThreadLocal.remove();String grayName = RpcContext.getContext().getAttachment(GrayRouterConstants.REQUEST_HEADER);if (StringUtils.isNotBlank(grayName)) {log.info("gray doProviderFilter invocation grayName: {}", grayName);GrayThreadLocal.setGrayName(grayName);}try {return invoker.invoke(invocation);} finally {// 调用完成后移除线程变量属性GrayThreadLocal.remove();}}/*** 服务消费方过滤** @param invoker* @param invocation* @return*/public Result doConsumerFilter(Invoker<?> invoker, Invocation invocation) {String grayName = GrayThreadLocal.getGrayName();if (StringUtils.isNotBlank(grayName)) {String address = invoker.getUrl().getAddress();log.info("gray doConsumerFilter invocation grayName: {}, {}", grayName, address);RpcContext.getContext().setAttachment(GrayRouterConstants.REQUEST_HEADER,grayName);}return invoker.invoke(invocation);}}
//feign负载均衡
@Slf4j
public class GrayLoadBalanceRule implements IRule {private static final AtomicInteger ATOMIC_INTEGER = new AtomicInteger(0);private static final AtomicInteger GRAY_ATOMIC_INTEGER = new AtomicInteger(0);@Autowiredprivate NacosDiscoveryProperties nacosDiscoveryProperties;@Autowiredprivate NacosServiceManager nacosServiceManager;/*** 计算得到当前调用次数*/public static int getAndIncrement() {int current;int next;do {current = ATOMIC_INTEGER.get();next = current >= Integer.MAX_VALUE ? 0 : current + 1;} while (!ATOMIC_INTEGER.compareAndSet(current, next));return next;}public static int getGrayAndIncrement() {int current;int next;do {current = GRAY_ATOMIC_INTEGER.get();next = current >= Integer.MAX_VALUE ? 0 : current + 1;} while (!GRAY_ATOMIC_INTEGER.compareAndSet(current, next));return next;}private static List<Instance> filterNacosInstanceList(List<Instance> serverList, String grayHeader) {return serverList.stream().filter(item -> {String serverGrayValue = item.getMetadata().get(GrayRouterConstants.GRAY_REGISTRY_TAG);
// 非灰度请求返回非灰度机器if (StringUtils.isBlank(grayHeader) && StringUtils.isBlank(serverGrayValue)) {return true;}
// 灰度请求返回灰度机器return StringUtils.isNotBlank(grayHeader) && StringUtils.isNotBlank(serverGrayValue) && serverGrayValue.equals(grayHeader);}).collect(Collectors.toList());}@Overridepublic Server choose(Object key) {Server nacosServer = nacosFeignChoose();return nacosServer;}private ILoadBalancer lb;@Overridepublic void setLoadBalancer(ILoadBalancer iLoadBalancer) {this.lb = iLoadBalancer;}@Overridepublic ILoadBalancer getLoadBalancer() {return lb;}private Server nacosFeignChoose() {ILoadBalancer lb = getLoadBalancer();if (lb == null) {return null;}String grayName = GrayThreadLocal.getGrayName();String serverName = FeignServerNameThreadLocal.getServerName();try {String group = this.nacosDiscoveryProperties.getGroup();DynamicServerListLoadBalancer loadBalancer = (DynamicServerListLoadBalancer) getLoadBalancer();String name = loadBalancer.getName();NamingService namingService = nacosServiceManager.getNamingService(nacosDiscoveryProperties.getNacosProperties());List<Instance> instances = namingService.selectInstances(serverName, group, true);if (CollectionUtils.isEmpty(instances)) {log.info("no instance in service {}", name);return null;}List<Instance> serverList = filterNacosInstanceList(instances, grayName);if (Objects.nonNull(serverList) && CollUtil.isNotEmpty(serverList)) {NacosServer nacosServer = null;if(StringUtils.isNotBlank(grayName)){nacosServer = new NacosServer(serverList.get(getGrayAndIncrement() % serverList.size()));Instance instance = nacosServer.getInstance();String instanceId = instance.getInstanceId();log.info("gray instance in service {} ,serverName: {} ,instanceId:{}", name,serverName,instanceId);} else {nacosServer = new NacosServer(serverList.get(getAndIncrement() % serverList.size()));}return nacosServer;}} catch (Exception e) {log.warn("NacosRule error", e);}List<Server> allServers = lb.getAllServers();/*** 兜底* 1、不容错:所有正常的机器都在重启or 都宕机了 不可以请求到灰度服务上(第一版本的灰度发布策略只有1台灰度机器,这种极端情况下如果允许分发请求,1台机器承受不住所有的请求压力)* 2、灰度机器不存在,随机一台实例*/return StringUtils.isNotBlank(grayName) && CollUtil.isNotEmpty(allServers) ? allServers.get(getAndIncrement() % allServers.size()) : null;}}
@Beanpublic IRule grayLoadBalanceRule() {return new GrayLoadBalanceRule();}