开发者

Java微服务无损发布生产实战案例及验证

开发者 https://www.devze.com 2025-10-30 10:23 出处:网络 作者: 遥远_
目录1. 背景2. 排查3. 方案3.1. 外部脚本控制(运维层面)3.1.1. 优化前的sh脚本逻辑3.1.2. 优化后的sh脚本逻辑3.1.3. 测试结果3.2. 应用内优雅停机(代码层面)3.2.1. 优雅停机实现类3.2.2. 测试结果4. 验
目录
  • 1. 背景
  • 2. 排查
  • 3. 方案
    • 3.1. 外部脚本控制(运维层面)
      • 3.1.1. 优化前的sh脚本逻辑
      • 3.1.2. 优化后的sh脚本逻辑
      • 3.1.3. 测试结果
    • 3.2. 应用内优雅停机(代码层面)
      • 3.2.1. 优雅停机实现类
      • 3.2.2. 测试结果
  • 4. 验证
    • 5. 上线
      • 6. 参考资料

        1. 背景

        项目为Spring Cloud微服务,注册中心使用nacos。在上线发布过程中发现存在feign调用失败的情况,即发布过程没有做到无损。上线发布采用灰度发布策略,即先发布一台机器,这台机器成功发布后再接着发布其他机器,理论上应该无损。

        2. 排查

        在上线发布微服务a第xx台机器过程中,a应该处于下线状态,不应该再请求到这台机器,但是从日志看有流量进来并调用a失败了:

        Java微服务无损发布生产实战案例及验证

        3. 方案

        借助AI及经验排查项目代码和停机sh脚本后发现代码层面及运维脚本层面都没有实现优雅停机,基于尽量少改动代码考虑,决定采用修改停机sh脚本的方案实现优雅停机。

        3.1. 外部脚本控制(运维层面)

        这是本案例采用的优雅停机方案,代码无侵入,只需完善停机sh脚本逻辑以支持优雅停机。

        3.1.1. 优化前的sh脚本逻辑

        停止实例时脚本没有先下线nacos注册中心服务实例,存在被调用方服务B刚停止但nacos还未感知到,此时调用方A从nacos获取到这个已停止的服务B的实例,导致调用失败。

        Java微服务无损发布生产实战案例及验证

        3.1.2. 优化后的sh脚本逻辑

        采取主动标记nacos实例下线 + 休眠等待 + 停止Java进程方案:先标记nacos实例下线(主动下线而不是被动等待nacos自动检测下线),再设置一个休眠时间一是为了等待nacos更新,二是让正在处理的业务请求有时间完成。最后再停止java进程,避免请求丢失,实现微服务优雅停机。

        #!/bin/bash
        # stop.sh - 优雅停机脚本
        
        APP_NAME="provider"
        APP_PORT=8082
        NACOS_SERVER="127.0.0.1:8848"
        
        echo "开始优雅停止 $APP_NAME..."
        
        # 1. 获取服务ip
        get_nacos_registered_ip() {
            
            # 调用Nacos查询接口
            local response=$(curl -s "http://${NACOS_SERVER}/nacos/v1/ns/instance/list?serviceName=${APP_NAME}")
            
            if [ $? -ne 0 ] || [ -z "$response" ]; then
                echo http://www.devze.com"查询Nacos服务失败" >&2
                return 1
            fi
            
            # 解析jsON获取IP(需要jq工具)
            if command -v jq >/dev/null 2>&1; then
                local ip=$(echo "$response" | jq -r '.hosts[0].ip' 2>/dev/null)
                if [ "$ip" != "null" ] && [ -n "$ip" ]; then
                    echo $ip
                    return 0
                fi
            fi
            
            # 如果没有jq,使用grep/awk解析
            local ip=$(echo "$response" | grep -o '"ip":"[^"]*' | cut -d'"' -f4 | head -1)
            if [ -n "$ip" ]; then
                echo $ip
                return 0
            fi
            
            echo "无法从Nacos响应中解析IP" >&2
            return 1
        }
        
        LOCAL_IP=$(get_nacos_registered_ip)
        echo "检测到服务注册IP: $LOCAL_IP"
        
        # 2. 从Nacos下线服务
        echo "从Nacos注销服务..."
        curl -X DELETE "${NACOS_SERVER}/nacos/v1/ns/instance?serviceName=${APP_NAME}&ip=${LOCAL_IP}&port=${APP_PORT}"
        
        # 3. 等待流量切换
        echo "等待流量切换(15秒)..."
        sleep 15
        
        # 4. 根据jar名称查找应用进程,比如provider2
        echo "查找应用进程..."
        # 更精确的进程查找方式
        APP_PID=$(ps aux | grep "[j]ava.*provider2" | awk '{print $2}')
        
        if [ -n "$APP_PID" ]; then
            echo "找到应用进程,PI编程D: $APP_PID"
            
            # 5. 发送优雅停止信号
            echo "发送优雅停止信号..."
            kill -15 $APP_PID
            
            # 6. 等待进程停止,最多30秒
            for i in {1..30}; do
                if kill -0 $APP_PID 2>/dev/null; then
                    echo "等待应用停止...($i/30)"
                    sleep 1
                else
                    echo "应用已优雅停止"
                    exit 0
                fi
            done
            
            # 7. 强制停止
            echo "优雅停止超时,强制停止"
            kill -9 $APP_PID
        else
            echo "应用未运行"
        fi
        
        echo "停止脚本执行完成"

        另外,springboot项目配置文件application.yml配置优雅停机:

        server:
          shutdown: graceful
        

        3.1.3. 测试结果

        脚本运行结果:

        Java微服务无损发布生产实战案例及验证

        应用日志:

        Java微服务无损发布生产实战案例及验证

        3.2. 应用内优雅停机(代码层面)

        3.2.1. 优雅停机实现类

        package com.example.provider;
        
        import com.alibaba.cloud.nacos.registry.NacosRegistration;
        import com.alibaba.cloud.nacos.registry.NacosServiceRegistry;
        import org.apache.catalina.connector.Connector;
        import org.slf4j.Logger;
        import org.slf4j.LoggerFactory;
        import org.springframework.boot.web.embedded.tomcat.TomcatConnectorCustomizer;
        import org.springframework.context.ApplicationListener;
        import org.sp编程客栈ringframework.context.annotation.Lazy;
        import org.springframework.context.event.ContextClosedEvent;
        import org.springframework.stereotype.Component;
        
        import javax.annotation.Resource;
        import java.util.concurrent.Executor;
        import java.util.concurrent.ThreadPoolExecutor;
        import java.util.concurrent.TimeUnit;
        
        @Component
        public class TomcatNacosGracefulShutdown implements TomcatConnectorCustomizer,
                ApplicationListener<ContextClosedEvent> {
        
            private static final Logger logger = LoggerFactory.getLogger(TomcatNacosGracefulShutdown.class);
        
            private volatile Connector connector;
        
            @Lazy
            @Resource
            private NacosServiceRegistry nacosServiceRegistry;
        
            @Lazy
            @Resource
            private NacosRegistration nacosRegistration;
        
            @Override
            public void customize(Connector connector) {
                this.connector = connector;
                logger.info("Tomcat连接器自定义配置完成");
            }
        
            @Override
            public void onApplicationEvent(ContextClosedEvent event) {
                logger.info("=== 开始Tomcat+Nacos优雅停机 ===");
                startGracefulShutdown();
            }
        
            public void startGracefulShutdown() {
                try {
                    // 步骤1: Nacos注销
                    deregisterFromNacos();
        
                    // 步骤2: 等待注册中心传播
            编程客栈        waitForRegistryPropagation();
        
                    // 步骤3: 暂停Tomcat接收新请求
                    pauseTomcat();
        
                    // 步骤4: 等待活跃请求完成
                    waitForActiveRequests();
        
                    logger.info("=== Tomcat+Nacos优雅停机完成 ===");
        
                } catch (Exception e) {
                    logger.error("优雅停机失败", e);
                }
            }
        
            private void deregisterFromNacos() {
                if (nacosServiceRegistry != null && nacosRegistration != null) {
                    try {
                        logger.info("从Nacos注销服务实例...");
                        nacosServiceRegistry.deregister(nacosRegistration);
                        logger.info("Nacos注销成功: {}", nacosRegistration.getServiceId());
                    } catch (Exception e) {
                        logger.error("Nacos注销失败", e);
                    }
                } else {
                    logger.warn("Nacos注册组件未找到,跳过注销");
                }
            }
        
            private void waitForRegistryPropagation() {
                try {
                    long waitTime = 15000;
                    logger.info("等待注册中心传播: {}ms", waitTime);
                    Thread.sleep(waitTime);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    logger.warn("等待过程被中断");
                }
            }
        
            private void pauseTomcat() {
                if (connector != null) {
                    logger.info("暂停Tomcat连接器,停止接收新请求");
                    connector.pause();
                } else {
                    logger.warn("Tomcat连接器未找到,无法暂停");
                }
            }
        
            private void waitForActiveRequests() {
                if (connector == null) {
                    logger.warn("Tomcat连接器未找到,跳过等待");
                    return;
                }
        
                Executor executor = connector.getProtocolHandler().getExecutor();
                if (executor instanceof ThreadPoolExecutor) {
                    ThreadPoolExecutor threadPool = (ThreadPoolExecutor) executor;
        
                    logger.info("等待活跃请求完成...");
                    threadPool.shutdown();
        
                    try {
                        long maxWaitTime = 30000;
                        if (!threadPool.awaitTermination(maxWaitTime, TimeUnit.MILLISECONDS)) {
                            logger.warn("等待请求超时,强制关闭线程池");
                            threadPool.shutdownNow();
        
                            if (!threadPool.awaitTermination(5000, TimeUnit.MILLISECONDS)) {
                                logger.error("线程池未能正常终止");
                            }
                        } else {
                            logger.info("所有活跃请求处理完成");
                        }
                    } catch (InterruptedException e) {
                        threadPool.shutdownNow();
                        Thread.currentThread().interrupt();
                        logger.warn("等待过程被中断");
                    }
                } else {
                    logger.warn("无法获取Tomcat线程池,跳过等待");
                }
            }
        
        }

        3.2.2. 测试结果

        Java微服务无损发布生产实战案例及验证

        4. 验证

        在测试环境用jemeter做接口测试,观察在执行停机脚本发布期间有无失败用例。

        Java微服务无损发布生产实战案例及验证

        5. 上线

        测试环境验证没问题后上线,生产发布时通过可观测平台查看发布服务的请php求列表,发现错误数为0,成功率100%,且发布过程中没有日志告警,说明问题已修复。

        6. 参考资料

        nacos官方open api

        nacos官方FAQ

        到此这篇关于Java微服务无损发布生产实战案例及验证的文章就介绍到这了,更多相关Java微服务无损发布生产内容请搜索编程客栈(www.devze.com)以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程客栈(www.devze.com)!

        0

        精彩评论

        暂无评论...
        验证码 换一张
        取 消

        关注公众号