Java微服务无损发布生产案例

目录

[1. 背景](#1. 背景)

[2. 排查](#2. 排查)

[3. 方案](#3. 方案)

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

[3.1.1. 优化前的sh脚本逻辑](#3.1.1. 优化前的sh脚本逻辑)

[3.1.2. 优化后的sh脚本逻辑](#3.1.2. 优化后的sh脚本逻辑)

[3.1.3. 测试结果](#3.1.3. 测试结果)

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

[3.2.1. 优雅停机实现类](#3.2.1. 优雅停机实现类)

[3.2.2. 测试结果](#3.2.2. 测试结果)

[4. 验证](#4. 验证)

[5. 上线](#5. 上线)

[6. 参考资料](#6. 参考资料)


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进程,避免请求丢失,实现微服务优雅停机。

bash 复制代码
#!/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配置优雅停机:

bash 复制代码
server:
  shutdown: graceful

3.1.3. 测试结果

脚本运行结果:

应用日志:

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

3.2.1. 优雅停机实现类

java 复制代码
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

相关推荐
kkkkk0211063 小时前
黑马微服务保险(一)
笔记·微服务·架构
遥远_3 小时前
Spring Boot微服务健康检测:保障系统稳定性的关键实践
spring boot·微服务·1024程序员节·健康检测
苹果醋33 小时前
学习札记-Java8系列-1-Java8新特性简介&为什么要学习Java8
java·运维·spring boot·mysql·nginx
Tony Bai3 小时前
【Go 网络编程全解】13 从 HTTP/1.1 到 gRPC:Web API 与微服务的演进
开发语言·网络·http·微服务·golang
青鱼入云3 小时前
TraceId如何在Spring-Cloud微服务的REST调用中传递
微服务·架构·链路追踪
武子康3 小时前
Java-159 MongoDB 副本集容器化 10 分钟速查卡|keyfile + –auth + 幂等 init 附 docker-compose
java·数据库·mongodb·docker·性能优化·nosql·1024程序员节
m0_748233644 小时前
C++ 模板初阶:从函数重载到泛型编程的优雅过渡
java·c++·算法·1024程序员节
以己之4 小时前
11.盛最多水的容器
java·算法·双指针·1024程序员节
摇滚侠4 小时前
全面掌握PostgreSQL关系型数据库,设置远程连接,笔记05,笔记06
java·数据库·笔记·postgresql