关注可以查看更多粉丝专享blog~
公司产品线采用的是Spring Cloud(Dalston.SR1)、Spring Boot(1.5.x)、Spring MVC、Mybatis、Redis构建的微服务、服务数量60+,之前规定是每周二中午12点-2点发布,由于用户访问量的上升这样用户体验特别差,之前为了解决这个问题做过一次不停机发布方案,采用的是Spring Cloud优雅停机,具体方式如下:
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
endpoints.shutdown.enabled: true #启用shutdown端点,以便支持优雅停机
#endpoints.shutdown.sensitive: false #禁用密码验证(可选)
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接口确定启动成功,则灰度剩下的机器,由刚发布完的机器提供服务以此来避免服务器错误的情况。做到随时发布,其实里面还有一些问题,比如接口涉及到版本问题这么简单粗暴是不可行的,后面的路还很长,本次先记录我的不停机发布之旅。
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);
}
}
}
核心实现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 = 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()));
}
}
定义访问接口用于操作灰度服务,我将这些接口定义在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());
}
}
基本开发已经完成了,后面就开始测试效果啦,测试工具使用的是JMeter,策略是挑选10个核心服务和5个非核心服务用脚本控制滚动发布,构建启动完成之后间歇10分钟然后又发持续24小时,期间JMeter对这些服务中的高频,且涉及到内部调用的接口进行压力测试200线程/s,观察error情况,24H 0 error。棒棒棒!由于本人太懒了,过了好久了才来写这篇博客,当时的JMeter截图没有保存,所以还是要及时记笔记,大家一起努力!