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

目录

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失败了:

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

遥远_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值