SpringCloud之蓝绿部署

一、蓝绿部署概述

      蓝绿部署是一种可以保证系统在不间断提供服务的情况下上线的部署方式。

      蓝组集群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);
    }
}

 

 

  • 1
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值