Spring Cloud使用Zuul和Ribbon实现灰度发布

Spring Cloud使用Zuul和Ribbon做灰度发布

公司产品线采用的是Spring Cloud(Dalston.SR1)、Spring Boot(1.5.x)、Spring MVC、Mybatis、Redis构建的微服务、服务数量60+,之前规定是每周二中午12点-2点发布,由于用户访问量的上升这样用户体验特别差,之前为了解决这个问题做过一次不停机发布方案,采用的是Spring Cloud优雅停机,具体方式如下:

  • 1、maven添加actuator依赖

      <dependency>
      	<groupId>org.springframework.boot</groupId>
      	<artifactId>spring-boot-starter-actuator</artifactId>
      </dependency>
    
  • 2、application.yml中配置

      endpoints.shutdown.enabled:  true  #启用shutdown端点,以便支持优雅停机
      #endpoints.shutdown.sensitive:  false  #禁用密码验证(可选)
    
  • 3、在任意一台服务器上利用curl发送shutdown命令

      curl -X POST http://ip:端口/shutdown
      或者
      curl -d "" http://ip:端口/shutdown
    

回忆过去

在Jenkins脚本中配置打包完毕之后先对要发布的那台服务器发送shutdown命令,会收到{“message”:“Shutting down, bye…”},问题就出在这里,服务器在收到该指令之后会从eureka退出,有时候是从eureka直接注销,但是有时候会出现(down)标识,这个时候服务器还可以接收到请求,但是已经开始发布了,所以用户请求如果负载到该机器就会出现服务器错误,体验很差。难道要告诉用户我们在发布,现在不能用吗?NO!要不凌晨再发布吧?NONONO!!!所以开始了探索之路。

展望未来

网上有很多关于Spring Cloud灰度发布的策略,包括K8S,Apollo、Ribbon等,K8S成本太高了,pass!我们配置中心采用的是Spring Cloud Config所以Apollo pass!最终采用的是zuul和Ribbon来做灰度发布,其实主要方式就是网关拦截请求,通过识别请求头中的特定标识来识别是否灰度用户,从而将用户路由到灰度服务上面。我们没有灰度测试流程所以本次我只做了后半部分,在灰度发布的时候将用户路由到正常服务上面,待发布完成一台之后调用服务的/health接口确定启动成功,则灰度剩下的机器,由刚发布完的机器提供服务以此来避免服务器错误的情况。做到随时发布,其实里面还有一些问题,比如接口涉及到版本问题这么简单粗暴是不可行的,后面的路还很长,本次先记录我的不停机发布之旅。

大致流程分为以下几步

1、定义自定义灰度策略配置文件

GrayRibbonConfig(在GrayRibbonConfig类中指定我们的灰度发布规则类)

import com.*.*.*.ExcludeFromComponetScan;
import com.netflix.loadbalancer.IRule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import lombok.extern.slf4j.Slf4j;

/**
 * ribbon要排除在ComponentScan之外故新建此包 by david
 */
@Slf4j
@Configuration
@ExcludeFromComponetScan
public class GrayRibbonConfig {

    @Bean
    public IRule grayRule() {
        log.info("---customize gray publish---");
        return new GrayRule();
    }


}

RibbonClientConfig(使用@RibbonClients(defaultConfiguration = GrayRibbonConfig.class)指定客户端路由配置为GrayRibbonConfig类,该注解为全局配置,如果有服务需要特殊处理则需要使用@RibbonClient(configuration = Xxx.class)自定义配置)

import com.netflix.client.config.IClientConfig;
import com.netflix.loadbalancer.ConfigurationBasedServerList;

import org.springframework.cloud.netflix.ribbon.RibbonClients;

import lombok.extern.slf4j.Slf4j;

/**
 * 自定义灰度策略
 */
@Slf4j
@RibbonClients(defaultConfiguration = GrayRibbonConfig.class)
public class RibbonClientConfig {

    public static class BazServiceList extends ConfigurationBasedServerList {
        public BazServiceList(IClientConfig config) {
            super.initWithNiwsConfig(config);
        }
    }
}

2、重写Ribbon的Rule

在这里插入图片描述
核心实现GrayRule,各微服务需要依赖此包这样Ribbon就可以控制内部接口的访问,在zuul中也要放置同样的代码三个类,zuul控制外部访问,我们代码的模式是@FeignClient(value = 具体的服务)如果我们只想网关的话那么各微服务就不用了拉包了,由网关去分发所有请求,我们指向的是具体服务所以微服务需要一来到这三个类,这样就能加载配置了,由于这些配置卸载common包里面,common包里面各种依赖太多了,不想让zuul变得臃肿所以就在网关又放置了一份,当然也可以专门配置一个jar来做gray。

import com.*.*.*.GrayConstants;
import com.netflix.loadbalancer.ILoadBalancer;
import com.netflix.loadbalancer.Server;
import com.netflix.loadbalancer.ZoneAvoidanceRule;
import com.*.*.*.*.redis.JedisClusterUtils;
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 = new ArrayList<>();
        for (Server server : serverList) {
            // 过滤正在发布的服务
            if (grayServers.contains(server.getHostPort())) {
                continue;
            }
            noGrayServerList.add(server);
        }
        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()));
    }

}

3、定义访问接口用于操作灰度服务,我将这些接口定义在zuul里面了GrayController,将灰度服务放入redis的hash对象中,然后getAll如果灰度服务数量特别大,慎用!如果灰度服务忘记关闭则24小时之后自动关闭,这个根据实际情况而定,为了做压力测试,所以设置的过期时间比较长

import com.*.*.StringUtils;
import com.*.*.constant.GrayConstants;
import com.*.*.*.redis.JedisClusterUtils;
import org.springframework.http.HttpStatus;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@RestController
@RequestMapping("gray")
public class GrayController {

    private static final Integer GRAY_TIME_OUT = 24 * 60 * 60;
    private static final String GRAY_OPEN = "1";

    /**
     * 开启灰度发布开关
     */
    @GetMapping("openGray")
    public String openGray() {
        log.info("openGray start");
        JedisClusterUtils.set(GrayConstants.GRAY_SWITCH, GRAY_OPEN, GRAY_TIME_OUT);
        log.info("openGray end");
        return HttpStatus.OK.getReasonPhrase();
    }

    /**
     * 关闭灰度发布开关
     */
    @GetMapping("closeGray")
    public String closeGray() {
        log.info("closeGray start");
        JedisClusterUtils.del(GrayConstants.GRAY_SWITCH);
        JedisClusterUtils.del(GrayConstants.GRAY_ADDRESS);
        log.info("closeGray end");
        return HttpStatus.OK.getReasonPhrase();
    }

    /**
     * 设置灰度发布服务
     */
    @GetMapping("setGrayServer")
    public String setGrayServer(@RequestParam("grayHostPort") String grayHostPort) {
        log.info("setGrayServer start grayHostPort:{}", grayHostPort);
        String grayStatus = JedisClusterUtils.get(GrayConstants.GRAY_SWITCH);
        if (StringUtils.isEmpty(grayStatus) || !GRAY_OPEN.equals(grayStatus)) {
            JedisClusterUtils.set(GrayConstants.GRAY_SWITCH, GRAY_OPEN, GRAY_TIME_OUT);
        }
        JedisClusterUtils.hset(GrayConstants.GRAY_ADDRESS, grayHostPort, GRAY_OPEN);
        JedisClusterUtils.expire(GrayConstants.GRAY_ADDRESS, GRAY_TIME_OUT);
        log.info("setGrayServer end grayHostPort:{}", grayHostPort);
        return HttpStatus.OK.getReasonPhrase();
    }

    /**
     * 移除灰度服务
     */
    @GetMapping("removeGrayServer")
    public String removeGrayServer(@RequestParam("grayHostPort") String grayHostPort) {
        log.info("removeGrayServer start grayHostPort:{}", grayHostPort);
        JedisClusterUtils.hdel(GrayConstants.GRAY_ADDRESS, grayHostPort);
        log.info("removeGrayServer end grayHostPort:{}", grayHostPort);
        return HttpStatus.OK.getReasonPhrase();
    }

    /**
     * 获取灰度发布状态
     */
    @GetMapping("getGrayStatus")
    public String getGrayStatus() {
        log.info("getGrayStatus start");
        String status = JedisClusterUtils.get(GrayConstants.GRAY_SWITCH);
        log.info("getGrayStatus end :{}", status);
        return GRAY_OPEN.equals(status) ? "open" : "close";
    }

    /**
     * 获取灰度发布中的服务
     */
    @GetMapping("getGrayServer")
    public List<String> getGrayServer() {
        log.info("getGrayServer start");
        Map<String, String> grayServer = JedisClusterUtils.hgetAll(GrayConstants.GRAY_ADDRESS);
        log.info("getGrayServer end grayServer:{}", grayServer);
        return CollectionUtils.isEmpty(grayServer) ? new ArrayList<>() : new ArrayList<>(grayServer.keySet());
    }

}

4、基本开发已经完成了,后面就开始测试效果啦!

测试工具使用的是JMeter,策略是挑选10个核心服务和5个非核心服务用脚本控制滚动发布,构建启动完成之后间歇10分钟然后又发持续24小时,期间JMeter对这些服务中的高频,且涉及到内部调用的接口进行压力测试200线程/s,观察error情况,24H 0 error。棒棒棒!由于本人太懒了,过了好久了才来写这篇博客,当时的JMeter截图没有保存,所以还是要及时记笔记,大家一起努力!

猜你喜欢

转载自blog.csdn.net/Davids_/article/details/105990030