<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
            <title type="text">March_CD Blog | 一个程序员的个人博客，心之所向，无所不能</title>
    <updated>2026-04-12T11:43:32+08:00</updated>
        <id>https://blog.238168.xyz</id>
        <link rel="alternate" type="text/html" href="https://blog.238168.xyz" />
        <link rel="self" type="application/atom+xml" href="https://blog.238168.xyz/atom.xml" />
    <rights>Copyright © 2026, March_CD Blog | 一个程序员的个人博客，心之所向，无所不能</rights>
    <generator uri="https://halo.run/" version="1.3.2">Halo</generator>
            <entry>
                <title><![CDATA[Spring Cloud 爬坑记---Zuul灰度发布]]></title>
                <link rel="alternate" type="text/html" href="https://blog.238168.xyz/archives/springcloud1" />
                <id>tag:https://blog.238168.xyz,2020-06-03:springcloud1</id>
                <published>2020-06-03T17:42:40+08:00</published>
                <updated>2020-06-03T19:07:42+08:00</updated>
                <author>
                    <name>March_CD</name>
                    <uri>https://blog.238168.xyz</uri>
                </author>
                <content type="html">
                        <![CDATA[<h3 id="灰度发布">灰度发布</h3><blockquote><p>灰度发布是通过切换线上并存版本之间的路由权重，逐步从一个版本切换为另一个版本的过程。虽然有很多人包括专业大牛认为灰度发布与金丝雀发布是等同的，但是在具体的操作和目的上面个还是有些许差别的。金丝雀发布更倾向于获取快速的反馈，而灰度发布更倾向于从一个版本到另一个版本平稳的切换。</p></blockquote><blockquote><p>环境支持：</p><ul><li>Spring Boot 2.x</li><li>Maven 3.x</li><li>Java 8</li><li>Lombok</li><li>transmittable-thread-local</li><li>spring-cloud-starter-netflix-zuul</li></ul></blockquote><ol><li>微服务客户端添加配置</li></ol><pre><code class="language-java">eureka.instance.metadata-map.version=v1 #当前微服务版本</code></pre><ol start="2"><li>zuul添加配置</li></ol><pre><code class="language-java">zuul.ribbon.canary.enabled=true</code></pre><ol start="3"><li>自定义ribbon负载均衡server选择</li></ol><pre><code class="language-java">import com.frdscm.gateway.SecurityConstants;import com.frdscm.gateway.util.RibbonVersionHolder;import com.google.common.base.Optional;import com.netflix.loadbalancer.Server;import com.netflix.loadbalancer.ZoneAvoidanceRule;import com.netflix.niws.loadbalancer.DiscoveryEnabledServer;import com.xiaoleilu.hutool.util.StrUtil;import lombok.extern.slf4j.Slf4j;import java.util.List;import java.util.Map;import java.util.stream.Collectors;@Slf4jpublic class MetadataCanaryRuleHandler extends ZoneAvoidanceRule {    @Override    public Server choose(Object key) {        List&lt;Server&gt; eligibleServers = this.getPredicate().getEligibleServers(this.getLoadBalancer().getAllServers(), key);        if (eligibleServers == null || eligibleServers.size() &lt; 1) {            return null;        }        String targetVersion = RibbonVersionHolder.getContext();        if (StrUtil.isBlank(targetVersion)) {            log.info(&quot;Client Not config version&quot;);            List&lt;Server&gt; roundRobinNotVersionServer = eligibleServers.stream().filter(server -&gt; {                Map&lt;String, String&gt; metadata = ((DiscoveryEnabledServer) server).getInstanceInfo().getMetadata();                String metaVersion = metadata.get(&quot;version&quot;);                return StrUtil.isBlank(metaVersion);            }).collect(Collectors.toList());            if (roundRobinNotVersionServer.size() &lt; 1) {                return eligibleServers.get(0);            }            if (roundRobinNotVersionServer.size() == 1) {                return roundRobinNotVersionServer.get(0);            }            Optional&lt;Server&gt; server = this.getPredicate().chooseRoundRobinAfterFiltering(roundRobinNotVersionServer, key);            return server.isPresent() ? server.get() : null;        } else {            List&lt;Server&gt; roundRobinVersionServer = eligibleServers.stream().filter(server -&gt; {                Map&lt;String, String&gt; metadata = ((DiscoveryEnabledServer) server).getInstanceInfo().getMetadata();                String metaVersion = metadata.get(&quot;version&quot;);                return targetVersion.equals(metaVersion);            }).collect(Collectors.toList());            if (roundRobinVersionServer.size() &lt; 1) {                return null;            }            if (roundRobinVersionServer.size() == 1) {                return roundRobinVersionServer.get(0);            }            Optional&lt;Server&gt; server = this.getPredicate().chooseRoundRobinAfterFiltering(roundRobinVersionServer, key);            return server.isPresent() ? server.get() : null;        }    }}</code></pre><ol start="4"><li>版本上下文Alibaba TTL</li></ol><pre><code class="language-java">import com.alibaba.ttl.TransmittableThreadLocal;public class RibbonVersionHolder {    private static final ThreadLocal&lt;String&gt; context = new TransmittableThreadLocal&lt;&gt;();    public static String getContext() {        return context.get();    }    public static void setContext(String value) {        context.set(value);    }    public static void clearContext() {        context.remove();    }}</code></pre><ol start="5"><li>初始化灰度发布</li></ol><pre><code class="language-java">import com.frdscm.gateway.handler.MetadataCanaryRuleHandler;import com.netflix.loadbalancer.ZoneAvoidanceRule;import com.netflix.niws.loadbalancer.DiscoveryEnabledNIWSServerList;import org.springframework.beans.factory.config.ConfigurableBeanFactory;import org.springframework.boot.autoconfigure.AutoConfigureBefore;import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;import org.springframework.cloud.netflix.ribbon.RibbonClientConfiguration;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.context.annotation.Scope;@Configuration@ConditionalOnClass(DiscoveryEnabledNIWSServerList.class)@AutoConfigureBefore(RibbonClientConfiguration.class)@ConditionalOnProperty(value = &quot;zuul.ribbon.canary.enabled&quot;)public class RibbonMetaFilterAutoConfiguration {    @Bean    @ConditionalOnMissingBean    @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)    public ZoneAvoidanceRule metadataAwareRule() {        return new MetadataCanaryRuleHandler();    }}</code></pre><ol start="6"><li>zuul中添加AccessFilter</li></ol><pre><code class="language-java">import com.netflix.zuul.ZuulFilter;import com.netflix.zuul.context.RequestContext;import com.smartcomma.scaffolding.gateway.util.RibbonVersionHolder;import com.xiaoleilu.hutool.util.StrUtil;import org.springframework.beans.factory.annotation.Value;import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;import org.springframework.stereotype.Component;import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.FORM_BODY_WRAPPER_FILTER_ORDER;@Componentpublic class AccessFilter extends ZuulFilter {    @Value(&quot;${zuul.ribbon.canary.enabled:false}&quot;)    private boolean canary;    @Override    public String filterType() {        return FilterConstants.PRE_TYPE;    }    @Override    public int filterOrder() {        return FORM_BODY_WRAPPER_FILTER_ORDER - 1;    }    @Override    public boolean shouldFilter() {        return true;    }    @Override    public Object run() {        RequestContext requestContext = RequestContext.getCurrentContext();        String version = requestContext.getRequest().getHeader(&quot;version&quot;);        if (canary &amp;&amp; StrUtil.isNotBlank(version)) {            RibbonVersionHolder.setContext(version);        } else {            RibbonVersionHolder.clearContext();        }        return null;    }}</code></pre><ol start="7"><li>客户端调用时header传入版本，以axios为例</li></ol><pre><code class="language-javascript">axios.defaults.headers.common['Authorization'] = AUTH_TOKEN;axios.defaults.headers.common['version'] = 'v1';  #对应eureka metadata 配置</code></pre>]]>
                </content>
            </entry>
            <entry>
                <title><![CDATA[Spring Boot 爬坑记---Spring Boot 2.x 优雅关闭]]></title>
                <link rel="alternate" type="text/html" href="https://blog.238168.xyz/archives/springboot1" />
                <id>tag:https://blog.238168.xyz,2020-06-03:springboot1</id>
                <published>2020-06-03T14:53:01+08:00</published>
                <updated>2020-06-03T19:10:49+08:00</updated>
                <author>
                    <name>March_CD</name>
                    <uri>https://blog.238168.xyz</uri>
                </author>
                <content type="html">
                        <![CDATA[<h3 id="tomcat优雅关闭">Tomcat优雅关闭</h3><blockquote><p>服务的开启和关闭是非常频繁的操作步骤， 所以在默认的情况下， 如果请求没有处理完，这个时候给springboot发起了关闭信号，会导致正在处理的请求失败，所以最好要对于这种情况进行处理。</p></blockquote><blockquote><p>环境支持：</p><ul><li>Spring Boot 2.x</li><li>Maven 3.x</li><li>Java 8</li><li>Lombok</li></ul></blockquote><pre><code class="language-java">import lombok.extern.slf4j.Slf4j;import org.apache.catalina.connector.Connector;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.boot.web.embedded.tomcat.TomcatConnectorCustomizer;import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;import org.springframework.boot.web.servlet.server.ServletWebServerFactory;import org.springframework.context.ApplicationListener;import org.springframework.context.annotation.Bean;import org.springframework.context.event.ContextClosedEvent;import java.util.concurrent.Executor;import java.util.concurrent.ThreadPoolExecutor;import java.util.concurrent.TimeUnit;@SpringBootApplication@Slf4jpublic class Application {    public static void main(String[] args) {        SpringApplication.run(Application.class, args);    }    @Bean    public GracefulShutdown gracefulShutdown() {        return new GracefulShutdown();    }    @Bean    public ServletWebServerFactory webServerFactory(final GracefulShutdown gracefulShutdown) {        TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory();        tomcat.addConnectorCustomizers(gracefulShutdown);        return tomcat;    }    /**     * graceful shutdown Spring Boot     */    private class GracefulShutdown implements TomcatConnectorCustomizer, ApplicationListener&lt;ContextClosedEvent&gt; {        private volatile Connector connector;        private static final int TIMEOUT = 15;        @Override        public void customize(Connector connector) {            this.connector = connector;        }        @Override        public void onApplicationEvent(ContextClosedEvent contextClosedEvent) {            this.connector.pause();            Executor executor = this.connector.getProtocolHandler().getExecutor();            if (executor instanceof ThreadPoolExecutor) {                try {                    log.warn(&quot;Tomcat Container ready shutdown&quot;);                    ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) executor;                    threadPoolExecutor.shutdown();                    if (!threadPoolExecutor.awaitTermination(TIMEOUT, TimeUnit.SECONDS)) {                        log.warn(&quot;Tomcat thread pool did not shut down gracefully within &quot;                                + TIMEOUT + &quot; seconds. Proceeding with forceful shutdown&quot;);                        threadPoolExecutor.shutdownNow();                        if (!threadPoolExecutor.awaitTermination(TIMEOUT, TimeUnit.SECONDS)) {                            log.error(&quot;Tomcat thread pool did not terminate&quot;);                        }                    }                } catch (InterruptedException ex) {                    Thread.currentThread().interrupt();                }            }        }    }}</code></pre>]]>
                </content>
            </entry>
</feed>
