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

 更新时间:2025年10月27日 09:58:13   作者:遥远_  
微服务架构是一项在云中部署应用和服务的新技术,这篇文章主要介绍了Java微服务无损发布生产实战及验证的相关资料,文中通过代码介绍的非常详细,需要的朋友可以参考下

1. 背景

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

2. 排查

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

3. 方案

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

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

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

3.1.1. 优化前的sh脚本逻辑

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

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 "查询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 "找到应用进程,PID: $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. 测试结果

脚本运行结果:

应用日志:

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.springframework.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. 测试结果

4. 验证

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

5. 上线

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

6. 参考资料

nacos官方open api

nacos官方FAQ

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

相关文章

  • Java 仿天猫服装商城系统的实现流程

    Java 仿天猫服装商城系统的实现流程

    读万卷书不如行万里路,只学书上的理论是远远不够的,只有在实战中才能获得能力的提升,本篇文章手把手带你用java+SSM+jsp+mysql+maven实现一个仿天猫服装商城系统,大家可以在过程中查缺补漏,提升水平
    2021-11-11
  • Java Predicate接口源码使用示例

    Java Predicate接口源码使用示例

    Java8引入了许多函数式接口(Functional Interface),Predicate(断言)就是其中一个,它的主要作用可以简单描述为:向其传入一个对象(可以理解为参数),将得到一个布尔值作为输出,这篇文章主要介绍了Java Predicate接口源码使用示例,需要的朋友可以参考下
    2025-04-04
  • Java通过数据库表生成实体类详细过程

    Java通过数据库表生成实体类详细过程

    这篇文章主要介绍了Java通过数据库表生成实体类,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习吧
    2023-02-02
  • Java中String字符串常量池和intern方法源码分析

    Java中String字符串常量池和intern方法源码分析

    在之前的文章中,小编给大家介绍了String字符串的不可变性及其实现原理,其中给大家提到了字符串常量池的概念,那么什么是常量池,String字符串与常量池有什么关系,本文给大家唠唠字符串常量池及String#intern()方法的作用,需要的朋友可以参考下
    2023-05-05
  • 微服务SpringConfig配置中心详解

    微服务SpringConfig配置中心详解

    这篇文章主要介绍了微服务SpringConfig配置中心,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2025-04-04
  • 轻量级声明式的Http库——Feign的独立使用

    轻量级声明式的Http库——Feign的独立使用

    这篇文章主要介绍了轻量级声明式的Http库——Feign的使用教程,帮助大家更好的理解和学习使用feign,感兴趣的朋友可以了解下
    2021-04-04
  • Java guava框架LoadingCache及CacheBuilder本地小容量缓存框架总结

    Java guava框架LoadingCache及CacheBuilder本地小容量缓存框架总结

    Guava Cache本地缓存框架主要是一种将本地数据缓存到内存中,但数据量并不能太大,否则将会占用过多的内存,本文给大家介绍Java guava框架 LoadingCache及CacheBuilder 本地小容量缓存框架总结,感兴趣的朋友一起看看吧
    2023-12-12
  • Spring MVC文件配置以及参数传递示例详解

    Spring MVC文件配置以及参数传递示例详解

    这篇文章主要给大家介绍了关于Spring MVC文件配置以及参数传递的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2021-03-03
  • spring声明式事务 @Transactional 不回滚的多种情况以及解决方案

    spring声明式事务 @Transactional 不回滚的多种情况以及解决方案

    本文主要介绍了spring声明式事务 @Transactional 不回滚的多种情况以及解决方案,文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2021-11-11
  • spring中ApplicationListener的使用小结

    spring中ApplicationListener的使用小结

    ApplicationListener是spring提供的一个监听器,本文主要介绍了spring中ApplicationListener的使用小结,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2024-07-07

最新评论