一、蓝绿部署概述
蓝绿部署是一种可以保证系统在不间断提供服务的情况下上线的部署方式。
蓝组集群A和绿组集群B通过网关同时对外提供服务,正常情况下两组的版本是一致的,当需要进行服务升级时,将其中一组服务(蓝组)从网关中移除,另一组服务(绿组)正常对外提供服务,对蓝组进行升级,升级测试通过后,修改网关的配置,将对外提供服务的集群切换为升级后的蓝组,绿组不再对外提供服务。
对更新后的线上服务保留三天左右的观察期,在观察期内如果出现问题,可以直接通过网关切换退回到更新前的版本,增加容错。观察期过后系统正常运行,则将剩下的绿组进行升级,保证升级后蓝绿两组的版本号一致。
二、部署流程
1、原理
使用eureka的元数据信息metadataMap和ribbon的路由,在网关实现蓝绿分组部署
2、部署过程
2.1、网关zuul
引入第三方依赖
<dependency>
<groupId>io.jmnarloch</groupId>
<artifactId>ribbon-discovery-filter-spring-cloud-starter</artifactId>
<version>2.1.0</version>
</dependency>
自定义全局过滤器,读取配置的蓝绿分组路由,设置调用参数
import com.netflix.zuul.ZuulFilter;
import io.jmnarloch.spring.cloud.ribbon.support.RibbonFilterContextHolder;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.stereotype.Component;
/**
* 蓝绿部署支撑
*/
@Component
//@RefreshScope
public class BlueGreenFilter extends ZuulFilter {
private String currentGroup = "all";
private boolean shoudFilter = false;
// 读取配置的蓝绿路由
@Value("${current-group}")
public void setCurrentGroup(String currentGroup) {
//RibbonFilterContextHolder.getCurrentContext().remove("group");
if(!"green".equals(currentGroup)&&!"blue".equals(currentGroup)){
this.currentGroup = "all";
shoudFilter = false;
}else {
this.currentGroup = currentGroup;
shoudFilter = true;
//RibbonFilterContextHolder.getCurrentContext().add("group",currentGroup);
}
}
@Override
public String filterType() {
return FilterConstants.PRE_TYPE;
}
@Override
public int filterOrder() {
return FilterConstants.PRE_DECORATION_FILTER_ORDER - 5;
}
@Override
public boolean shouldFilter() {
return shoudFilter;
}
// 将配置的蓝绿信息添加到上下文中,ribbon进行负载均衡时会和metadataMap中的配置进行匹配,获取匹配一致的服务链接
@Override
public Object run() {
RibbonFilterContextHolder.getCurrentContext().add("group",currentGroup);
return null;
}
}
启动zuul网关服务,通过动态修改配置current-group来决定对外提供的是蓝组还是绿组
2.2、具体服务(order)
properties配置文件中增加配置:eureka.instance.metadata-map.group = green (blue)
蓝组服务配置为blue,绿组服务配置为green
部署后的服务具有蓝绿特性,网关zuul在会根据上下文中配置的group来进行过滤,选择配置的蓝组或绿组来对外提供服务。
三、蓝绿部署导致的服务调用问题解决
3.1、问题描述
A、B两个服务,A服务通过feign调用B服务,在对A、B服务升级时,由于是蓝绿部署,升级过程中对蓝组进行升级,绿组为了容错,暂时不进行升级处理,网关zuul切换成蓝组对外提供服务。由于蓝绿组两个版本同时运行,虽然绿组已经不对外进行提供服务了,但是仍然运行,且蓝绿两组的服务在eureka上都有注册信息。当A服务通过feign调用B服务时,从eureka上获取B服务的地址,B服务新版本的蓝组和旧版本的绿组都可能被获取到,因此会导致服务之间调用存在多次请求结果不一致问题。
3.2、解决
A、B服务之间的服务调用也是通过ribbon来实现,原理等同于网关,在一个全局的过滤器中,拦截请求,将蓝绿组的状态信息封装到RibbonFilterContextHolder上下文中,在实际调用时,通过被调用者eureka的metadataMap中的蓝绿特性来进行区分实际调用的服务是蓝组还是绿组。
调用者服务A引入第三方依赖
<dependency>
<groupId>io.jmnarloch</groupId>
<artifactId>ribbon-discovery-filter-spring-cloud-starter</artifactId>
<version>2.1.0</version>
</dependency>
编写全局过滤器,在过滤器中注入配置的蓝绿组,添加蓝绿特性
import io.jmnarloch.spring.cloud.ribbon.support.RibbonFilterContextHolder;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.servlet.*;
import java.io.IOException;
@Component
public class CustomFilter implements Filter {
private String currentGroup;
@Value("${current-group:all}")
public void setCurrentGroup(String currentGroup){
this.currentGroup = currentGroup;
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
try {
if (currentGroup.equals("blue") || currentGroup.equals("green")){
RibbonFilterContextHolder.getCurrentContext().add("group",currentGroup);
}
}catch (Exception e){
e.printStackTrace();
}
filterChain.doFilter(servletRequest, servletResponse);
}
@Override
public void destroy() {
}
}
四、原理解析
4.1、feign调用
开启feign调用 -- 启动类添加 @EnableFeignClients -- 导入类 FeignClientsRegistrar
org.springframework.cloud.openfeign.FeignClientsRegistrar#registerFeignClients 扫描所有添加了@FeignClient注解的类注册到BeanDefinitionRegistry中
注册的实例对象为FeignClientFactoryBean,spring容器在实例化时通过id实际调用其getObject方法,通过动态代理来进行实例化
public Object getObject() throws Exception {
return this.getTarget();
}
<T> T getTarget() {
// TraceFeignContext
FeignContext context = (FeignContext)this.applicationContext.getBean(FeignContext.class);
Builder builder = this.feign(context);
String url;
if (!StringUtils.hasText(this.url)) {
if (!this.name.startsWith("http")) {
url = "http://" + this.name;
} else {
url = this.name;
}
url = url + this.cleanPath();
return this.loadBalance(builder, context, new HardCodedTarget(this.type, this.name, url));
} else {
if (StringUtils.hasText(this.url) && !this.url.startsWith("http")) {
this.url = "http://" + this.url;
}
url = this.url + this.cleanPath();
Client client = (Client)this.getOptional(context, Client.class);
if (client != null) {
if (client instanceof LoadBalancerFeignClient) {
client = ((LoadBalancerFeignClient)client).getDelegate();
}
builder.client(client);
}
Targeter targeter = (Targeter)this.get(context, Targeter.class);
return targeter.target(this, builder, context, new HardCodedTarget(this.type, this.name, url));
}
}
protected <T> T loadBalance(Builder builder, FeignContext context, HardCodedTarget<T> target) {
Client client = (Client)this.getOptional(context, Client.class);
if (client != null) {
builder.client(client);
// HystrixTarget
Targeter targeter = (Targeter)this.get(context, Targeter.class);
return targeter.target(this, builder, context, target);
} else {
throw new IllegalStateException("No Feign Client for loadBalancing defined. Did you forget to include spring-cloud-starter-netflix-ribbon?");
}
}
// Feign.Build
public <T> T target(Target<T> target) {
return build().newInstance(target);
}
public Feign build() {
SynchronousMethodHandler.Factory synchronousMethodHandlerFactory =
new SynchronousMethodHandler.Factory(client, retryer, requestInterceptors, logger,
logLevel, decode404, closeAfterDecode);
ParseHandlersByName handlersByName =
new ParseHandlersByName(contract, options, encoder, decoder, queryMapEncoder,
errorDecoder, synchronousMethodHandlerFactory);
return new ReflectiveFeign(handlersByName, invocationHandlerFactory, queryMapEncoder);
}
}
// ReflectiveFeign#newInstance
@Override
public <T> T newInstance(Target<T> target) {
Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target);
Map<Method, MethodHandler> methodToHandler = new LinkedHashMap<Method, MethodHandler>();
List<DefaultMethodHandler> defaultMethodHandlers = new LinkedList<DefaultMethodHandler>();
for (Method method : target.type().getMethods()) {
if (method.getDeclaringClass() == Object.class) {
continue;
} else if(Util.isDefault(method)) {
DefaultMethodHandler handler = new DefaultMethodHandler(method);
defaultMethodHandlers.add(handler);
methodToHandler.put(method, handler);
} else {
methodToHandler.put(method, nameToHandler.get(Feign.configKey(target.type(), method)));
}
}
InvocationHandler handler = factory.create(target, methodToHandler);
T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(), new Class<?>[]{target.type()}, handler);
for(DefaultMethodHandler defaultMethodHandler : defaultMethodHandlers) {
defaultMethodHandler.bindTo(proxy);
}
return proxy;
}
创建LoadBalancerFeignClient并包装成TraceLoadBalancerFeignClient,其中包含SpringClientFactory--RibbonClientConfiguration对象,加载ribbon相关配置信息,获得FeignLoadBalancer负载均衡器
public class LoadBalancerFeignClient implements Client {
static final Options DEFAULT_OPTIONS = new Options();
private final Client delegate;
private CachingSpringLoadBalancerFactory lbClientFactory;
private SpringClientFactory clientFactory;
public LoadBalancerFeignClient(Client delegate, CachingSpringLoadBalancerFactory lbClientFactory, SpringClientFactory clientFactory) {
this.delegate = delegate;
this.lbClientFactory = lbClientFactory;
this.clientFactory = clientFactory;
}
public Response execute(Request request, Options options) throws IOException {
try {
URI asUri = URI.create(request.url());
String clientName = asUri.getHost();
URI uriWithoutHost = cleanUrl(request.url(), clientName);
RibbonRequest ribbonRequest = new RibbonRequest(this.delegate, request, uriWithoutHost);
IClientConfig requestConfig = this.getClientConfig(options, clientName);
return ((RibbonResponse)this.lbClient(clientName).executeWithLoadBalancer(ribbonRequest, requestConfig)).toResponse();
} catch (ClientException var8) {
IOException io = this.findIOException(var8);
if (io != null) {
throw io;
} else {
throw new RuntimeException(var8);
}
}
}
IClientConfig getClientConfig(Options options, String clientName) {
Object requestConfig;
if (options == DEFAULT_OPTIONS) {
requestConfig = this.clientFactory.getClientConfig(clientName);
} else {
requestConfig = new LoadBalancerFeignClient.FeignOptionsClientConfig(options);
}
return (IClientConfig)requestConfig;
}
protected IOException findIOException(Throwable t) {
if (t == null) {
return null;
} else {
return t instanceof IOException ? (IOException)t : this.findIOException(t.getCause());
}
}
public Client getDelegate() {
return this.delegate;
}
static URI cleanUrl(String originalUrl, String host) {
String newUrl = originalUrl.replaceFirst(host, "");
StringBuffer buffer = new StringBuffer(newUrl);
if (newUrl.startsWith("https://") && newUrl.length() == 8 || newUrl.startsWith("http://") && newUrl.length() == 7) {
buffer.append("/");
}
return URI.create(buffer.toString());
}
private FeignLoadBalancer lbClient(String clientName) {
return this.lbClientFactory.create(clientName);
}
static class FeignOptionsClientConfig extends DefaultClientConfigImpl {
public FeignOptionsClientConfig(Options options) {
this.setProperty(CommonClientConfigKey.ConnectTimeout, options.connectTimeoutMillis());
this.setProperty(CommonClientConfigKey.ReadTimeout, options.readTimeoutMillis());
}
public void loadProperties(String clientName) {
}
public void loadDefaultValues() {
}
}
}
调用者通过feign进行微服务调用,通过Eureka客户端获取对应的服务列表
// DynamicServerListLoadBalancer
@VisibleForTesting
public void updateListOfServers() {
List<T> servers = new ArrayList<T>();
if (serverListImpl != null) {
servers = serverListImpl.getUpdatedListOfServers();
LOGGER.debug("List of Servers for {} obtained from Discovery client: {}",
getIdentifier(), servers);
if (filter != null) {
servers = filter.getFilteredListOfServers(servers);
LOGGER.debug("Filtered List of Servers for {} obtained from Discovery client: {}",
getIdentifier(), servers);
}
}
updateAllServerList(servers);
}
// RibbonLoadBalancerClient#execute
@Override
public <T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException {
ILoadBalancer loadBalancer = getLoadBalancer(serviceId);
// 获取服务列表
Server server = getServer(loadBalancer);
if (server == null) {
throw new IllegalStateException("No instances available for " + serviceId);
}
RibbonServer ribbonServer = new RibbonServer(serviceId, server, isSecure(server,
serviceId), serverIntrospector(serviceId).getMetadata(server));
return execute(serviceId, ribbonServer, request);
}
// BaseLoadBalancer#chooseServer 选择需要的服务
public Server chooseServer(Object key) {
if (counter == null) {
counter = createCounter();
}
counter.increment();
if (rule == null) {
return null;
} else {
try {
return rule.choose(key);
} catch (Exception e) {
logger.warn("LoadBalancer [{}]: Error choosing server for key {}", name, key, e);
return null;
}
}
}
//PredicateBasedRule
@Override
public Server choose(Object key) {
ILoadBalancer lb = getLoadBalancer();
Optional<Server> server = getPredicate().chooseRoundRobinAfterFiltering(lb.getAllServers(), key);
if (server.isPresent()) {
return server.get();
} else {
return null;
}
}
第三方依赖(ribbon-discovery-filter-spring-cloud-starter)自定义规则,进行服务的甄选判别
/**
* Copyright (c) 2015 the original author or authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.jmnarloch.spring.cloud.ribbon.rule;
import com.netflix.loadbalancer.AbstractServerPredicate;
import com.netflix.loadbalancer.AvailabilityPredicate;
import com.netflix.loadbalancer.CompositePredicate;
import com.netflix.loadbalancer.IRule;
import com.netflix.loadbalancer.PredicateBasedRule;
import io.jmnarloch.spring.cloud.ribbon.predicate.DiscoveryEnabledPredicate;
import org.springframework.util.Assert;
/**
* A simple {@link IRule} for matching the discovered server instances. The actual matching is being performed by the
* registered instance of {@link DiscoveryEnabledPredicate} allowing to adjust the actual matching strategy.
*
* @author Jakub Narloch
* @see DiscoveryEnabledPredicate
*/
public abstract class DiscoveryEnabledRule extends PredicateBasedRule {
private final CompositePredicate predicate;
/**
* Creates new instance of {@link DiscoveryEnabledRule} class with specific predicate.
*
* @param discoveryEnabledPredicate the discovery enabled predicate, can't be null
* @throws IllegalArgumentException if {@code discoveryEnabledPredicate} is {@code null}
*/
public DiscoveryEnabledRule(DiscoveryEnabledPredicate discoveryEnabledPredicate) {
Assert.notNull(discoveryEnabledPredicate, "Parameter 'discoveryEnabledPredicate' can't be null");
this.predicate = createCompositePredicate(discoveryEnabledPredicate, new AvailabilityPredicate(this, null));
}
/**
* {@inheritDoc}
*/
@Override
public AbstractServerPredicate getPredicate() {
return predicate;
}
/**
* Creates the composite predicate with fallback strategies.
*
* @param discoveryEnabledPredicate the discovery service predicate
* @param availabilityPredicate the availability predicate
* @return the composite predicate
*/
private CompositePredicate createCompositePredicate(DiscoveryEnabledPredicate discoveryEnabledPredicate, AvailabilityPredicate availabilityPredicate) {
return CompositePredicate.withPredicates(discoveryEnabledPredicate, availabilityPredicate)
.build();
}
}
public class CompositePredicate extends AbstractServerPredicate {
private AbstractServerPredicate delegate;
private List<AbstractServerPredicate> fallbacks = Lists.newArrayList();
private int minimalFilteredServers = 1;
private float minimalFilteredPercentage = 0;
@Override
public boolean apply(@Nullable PredicateKey input) {
return delegate.apply(input);
}
...
}
自定义断言继承CompositePredicate
/**
* Copyright (c) 2015 the original author or authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.jmnarloch.spring.cloud.ribbon.predicate;
import com.netflix.loadbalancer.AbstractServerPredicate;
import com.netflix.loadbalancer.PredicateKey;
import com.netflix.niws.loadbalancer.DiscoveryEnabledServer;
import javax.annotation.Nullable;
/**
* A template method predicate to be applied to service discovered server instances. The concreate implementation of
* this class need to implement the {@link #apply(DiscoveryEnabledServer)} method.
*
* @author Jakub Narloch
*/
public abstract class DiscoveryEnabledPredicate extends AbstractServerPredicate {
/**
* {@inheritDoc}
*/
@Override
public boolean apply(@Nullable PredicateKey input) {
return input != null
&& input.getServer() instanceof DiscoveryEnabledServer
&& apply((DiscoveryEnabledServer) input.getServer());
}
/**
* Returns whether the specific {@link DiscoveryEnabledServer} matches this predicate.
*
* @param server the discovered server
* @return whether the server matches the predicate
*/
protected abstract boolean apply(DiscoveryEnabledServer server);
}
/**
* Copyright (c) 2015 the original author or authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.jmnarloch.spring.cloud.ribbon.predicate;
import com.netflix.niws.loadbalancer.DiscoveryEnabledServer;
import io.jmnarloch.spring.cloud.ribbon.api.RibbonFilterContext;
import io.jmnarloch.spring.cloud.ribbon.support.RibbonFilterContextHolder;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
/**
* A default implementation of {@link DiscoveryEnabledServer} that matches the instance against the attributes
* registered through
*
* @author Jakub Narloch
* @see DiscoveryEnabledPredicate
*/
public class MetadataAwarePredicate extends DiscoveryEnabledPredicate {
/**
* {@inheritDoc}
*/
@Override
protected boolean apply(DiscoveryEnabledServer server) {
// 获取上下文中的group参数(该参数为在全局拦截器中添加进去的,如group:blue)
final RibbonFilterContext context = RibbonFilterContextHolder.getCurrentContext();
final Set<Map.Entry<String, String>> attributes = Collections.unmodifiableSet(context.getAttributes().entrySet());
// 获取eureka中发现的目标服务列表的metadata参数(在目标服务中设置metadataMap参数group设置成blue或green,区分蓝绿)
final Map<String, String> metadata = server.getInstanceInfo().getMetadata();
// 进行断言过滤,将目标服务器中配置的参数和当前调用服务器全局过滤器中的参数进行比较,一致的放行,不一致的过滤,实现指定蓝绿过滤
return metadata.entrySet().containsAll(attributes);
}
}