灰階釋出
什麼是灰階釋出,
概念請參考,我們來簡單的通過下圖來看下,通俗的講: 為了保證服務更新過程的平滑過渡提高客戶體驗,會一部分使用者 一部分使用者遞進更新,這樣生産中會同時出現多個版本的用戶端,為了保證多個版本用戶端的可用需要對應的多個版本的服務端版本。灰階釋出就是通過一定政策保證 多個版本用戶端、服務端間能夠正确對應。
所謂灰階釋出,即某個服務存在多個執行個體時,并且執行個體版本間的版本并不一緻,通過
實作方案
nginx + lua (openresty)
Netflix Zuul
隻需要自定義ribbon 的斷言即可,核心是通過TTL 擷取上下請求header中的版本号
@Slf4j
public class MetadataCanaryRuleHandler extends ZoneAvoidanceRule {
@Override
public AbstractServerPredicate getPredicate() {
return new AbstractServerPredicate() {
@Override
public boolean apply(PredicateKey predicateKey) {
String targetVersion = RibbonVersionHolder.getContext();
RibbonVersionHolder.clearContext();
if (StrUtil.isBlank(targetVersion)) {
log.debug("用戶端未配置目标版本直接路由");
return true;
}
DiscoveryEnabledServer server = (DiscoveryEnabledServer) predicateKey.getServer();
final Map<String, String> metadata = server.getInstanceInfo().getMetadata();
if (StrUtil.isBlank(metadata.get(SecurityConstants.VERSION))) {
log.debug("目前微服務{} 未配置版本直接路由");
return true;
}
if (metadata.get(SecurityConstants.VERSION).equals(targetVersion)) {
return true;
} else {
log.debug("目前微服務{} 版本為{},目标版本{} 比對失敗", server.getInstanceInfo().getAppName()
, metadata.get(SecurityConstants.VERSION), targetVersion);
return false;
}
}
};
}
}
維護請求中的版本号
public class RibbonVersionHolder {
private static final ThreadLocal<String> context = new TransmittableThreadLocal<>();
public static String getContext() {
return context.get();
}
public static void setContext(String value) {
context.set(value);
}
public static void clearContext() {
context.remove();
}
}
Spring Cloud Gateway 中實作
第一反應,參考zuul 的實作,自定義斷言,然後從上下中擷取版本資訊即可。但由于 spring cloud gateway 是基于webflux 的反應式程式設計,是以傳統的TTL或者 RequestContextHolder 都不能正确的維護上下文請求。
先來看 spring clou的 gateway 預設的lb 政策實作 LoadBalancerClientFilter
public class LoadBalancerClientFilter implements GlobalFilter, Ordered {
@Override
public int getOrder() {
return LOAD_BALANCER_CLIENT_FILTER_ORDER;
}
@Override
@SuppressWarnings("Duplicates")
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
return chain.filter(exchange);
}
protected ServiceInstance choose(ServerWebExchange exchange) {
return loadBalancer.choose(
((URI) exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR)).getHost());
}
}
我們隻需要重寫 choose 方法,把上下文請求傳遞到路由斷言中即可,如下
@Override
protected ServiceInstance choose(ServerWebExchange exchange) {
HttpHeaders headers = exchange.getRequest().getHeaders();
return loadBalancer.choose(((URI) exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR)).getHost(), headers);
}
然後在路由斷言中通過 PredicateKey擷取到即可
public abstract class AbstractDiscoveryEnabledPredicate extends AbstractServerPredicate {
/**
* {@inheritDoc}
*/
@Override
public boolean apply(@Nullable PredicateKey input) {
return input != null
&& input.getServer() instanceof NacosServer
&& apply((NacosServer) input.getServer(), (HttpHeaders) input.getLoadBalancerKey());
}
}
最後根據版本來計算
public class GrayMetadataAwarePredicate extends AbstractDiscoveryEnabledPredicate {
@Override
protected boolean apply(NacosServer server, HttpHeaders headers) {
PigxRibbonRuleProperties ribbonProperties = SpringContextHolder.getBean(PigxRibbonRuleProperties.class);
if (!ribbonProperties.isGrayEnabled()) {
log.debug("gray closed,GrayMetadataAwarePredicate return true");
return true;
}
final Map<String, String> metadata = server.getMetadata();
String version = metadata.get(CommonConstants.VERSION);
// 判斷Nacos服務是否有版本标簽
if (StrUtil.isBlank(version)) {
log.debug("nacos server tag is blank ,GrayMetadataAwarePredicate return true");
return true;
}
// 判斷請求中是否有版本
String target = headers.getFirst(CommonConstants.VERSION);
if (StrUtil.isBlank(target)) {
log.debug("request headers version is blank,GrayMetadataAwarePredicate return true");
return true;
}
log.debug("請求版本:{} ,目前服務版本:{}", target, version);
return target.equals(version);
}
}
整合nacos
結合nacos的動态配置可以非常友善的實作灰階