Java微服務(wù)無損發(fā)布生產(chǎn)實(shí)戰(zhàn)案例及驗(yàn)證
1. 背景
項(xiàng)目為Spring Cloud微服務(wù),注冊中心使用nacos。在上線發(fā)布過程中發(fā)現(xiàn)存在feign調(diào)用失敗的情況,即發(fā)布過程沒有做到無損。上線發(fā)布采用灰度發(fā)布策略,即先發(fā)布一臺機(jī)器,這臺機(jī)器成功發(fā)布后再接著發(fā)布其他機(jī)器,理論上應(yīng)該無損。
2. 排查
在上線發(fā)布微服務(wù)a第xx臺機(jī)器過程中,a應(yīng)該處于下線狀態(tài),不應(yīng)該再請求到這臺機(jī)器,但是從日志看有流量進(jìn)來并調(diào)用a失敗了:

3. 方案
借助AI及經(jīng)驗(yàn)排查項(xiàng)目代碼和停機(jī)sh腳本后發(fā)現(xiàn)代碼層面及運(yùn)維腳本層面都沒有實(shí)現(xiàn)優(yōu)雅停機(jī),基于盡量少改動代碼考慮,決定采用修改停機(jī)sh腳本的方案實(shí)現(xiàn)優(yōu)雅停機(jī)。
3.1. 外部腳本控制(運(yùn)維層面)
這是本案例采用的優(yōu)雅停機(jī)方案,代碼無侵入,只需完善停機(jī)sh腳本邏輯以支持優(yōu)雅停機(jī)。
3.1.1. 優(yōu)化前的sh腳本邏輯
停止實(shí)例時腳本沒有先下線nacos注冊中心服務(wù)實(shí)例,存在被調(diào)用方服務(wù)B剛停止但nacos還未感知到,此時調(diào)用方A從nacos獲取到這個已停止的服務(wù)B的實(shí)例,導(dǎo)致調(diào)用失敗。

3.1.2. 優(yōu)化后的sh腳本邏輯
采取主動標(biāo)記nacos實(shí)例下線 + 休眠等待 + 停止java進(jìn)程方案:先標(biāo)記nacos實(shí)例下線(主動下線而不是被動等待nacos自動檢測下線),再設(shè)置一個休眠時間一是為了等待nacos更新,二是讓正在處理的業(yè)務(wù)請求有時間完成。最后再停止java進(jìn)程,避免請求丟失,實(shí)現(xiàn)微服務(wù)優(yōu)雅停機(jī)。
#!/bin/bash
# stop.sh - 優(yōu)雅停機(jī)腳本
APP_NAME="provider"
APP_PORT=8082
NACOS_SERVER="127.0.0.1:8848"
echo "開始優(yōu)雅停止 $APP_NAME..."
# 1. 獲取服務(wù)ip
get_nacos_registered_ip() {
# 調(diào)用Nacos查詢接口
local response=$(curl -s "http://${NACOS_SERVER}/nacos/v1/ns/instance/list?serviceName=${APP_NAME}")
if [ $? -ne 0 ] || [ -z "$response" ]; then
echo "查詢Nacos服務(wù)失敗" >&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響應(yīng)中解析IP" >&2
return 1
}
LOCAL_IP=$(get_nacos_registered_ip)
echo "檢測到服務(wù)注冊IP: $LOCAL_IP"
# 2. 從Nacos下線服務(wù)
echo "從Nacos注銷服務(wù)..."
curl -X DELETE "${NACOS_SERVER}/nacos/v1/ns/instance?serviceName=${APP_NAME}&ip=${LOCAL_IP}&port=${APP_PORT}"
# 3. 等待流量切換
echo "等待流量切換(15秒)..."
sleep 15
# 4. 根據(jù)jar名稱查找應(yīng)用進(jìn)程,比如provider2
echo "查找應(yīng)用進(jìn)程..."
# 更精確的進(jìn)程查找方式
APP_PID=$(ps aux | grep "[j]ava.*provider2" | awk '{print $2}')
if [ -n "$APP_PID" ]; then
echo "找到應(yīng)用進(jìn)程,PID: $APP_PID"
# 5. 發(fā)送優(yōu)雅停止信號
echo "發(fā)送優(yōu)雅停止信號..."
kill -15 $APP_PID
# 6. 等待進(jìn)程停止,最多30秒
for i in {1..30}; do
if kill -0 $APP_PID 2>/dev/null; then
echo "等待應(yīng)用停止...($i/30)"
sleep 1
else
echo "應(yīng)用已優(yōu)雅停止"
exit 0
fi
done
# 7. 強(qiáng)制停止
echo "優(yōu)雅停止超時,強(qiáng)制停止"
kill -9 $APP_PID
else
echo "應(yīng)用未運(yùn)行"
fi
echo "停止腳本執(zhí)行完成"另外,springboot項(xiàng)目配置文件application.yml配置優(yōu)雅停機(jī):
server: shutdown: graceful
3.1.3. 測試結(jié)果
腳本運(yùn)行結(jié)果:

應(yīng)用日志:

3.2. 應(yīng)用內(nèi)優(yōu)雅停機(jī)(代碼層面)
3.2.1. 優(yōu)雅停機(jī)實(shí)現(xiàn)類
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優(yōu)雅停機(jī) ===");
startGracefulShutdown();
}
public void startGracefulShutdown() {
try {
// 步驟1: Nacos注銷
deregisterFromNacos();
// 步驟2: 等待注冊中心傳播
waitForRegistryPropagation();
// 步驟3: 暫停Tomcat接收新請求
pauseTomcat();
// 步驟4: 等待活躍請求完成
waitForActiveRequests();
logger.info("=== Tomcat+Nacos優(yōu)雅停機(jī)完成 ===");
} catch (Exception e) {
logger.error("優(yōu)雅停機(jī)失敗", e);
}
}
private void deregisterFromNacos() {
if (nacosServiceRegistry != null && nacosRegistration != null) {
try {
logger.info("從Nacos注銷服務(wù)實(shí)例...");
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("等待請求超時,強(qiáng)制關(guān)閉線程池");
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. 測試結(jié)果

4. 驗(yàn)證
在測試環(huán)境用jemeter做接口測試,觀察在執(zhí)行停機(jī)腳本發(fā)布期間有無失敗用例。

5. 上線
測試環(huán)境驗(yàn)證沒問題后上線,生產(chǎn)發(fā)布時通過可觀測平臺查看發(fā)布服務(wù)的請求列表,發(fā)現(xiàn)錯誤數(shù)為0,成功率100%,且發(fā)布過程中沒有日志告警,說明問題已修復(fù)。
6. 參考資料
到此這篇關(guān)于Java微服務(wù)無損發(fā)布生產(chǎn)實(shí)戰(zhàn)案例及驗(yàn)證的文章就介紹到這了,更多相關(guān)Java微服務(wù)無損發(fā)布生產(chǎn)內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java 仿天貓服裝商城系統(tǒng)的實(shí)現(xiàn)流程
讀萬卷書不如行萬里路,只學(xué)書上的理論是遠(yuǎn)遠(yuǎn)不夠的,只有在實(shí)戰(zhàn)中才能獲得能力的提升,本篇文章手把手帶你用java+SSM+jsp+mysql+maven實(shí)現(xiàn)一個仿天貓服裝商城系統(tǒng),大家可以在過程中查缺補(bǔ)漏,提升水平2021-11-11
Java通過數(shù)據(jù)庫表生成實(shí)體類詳細(xì)過程
這篇文章主要介紹了Java通過數(shù)據(jù)庫表生成實(shí)體類,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)吧2023-02-02
Java中String字符串常量池和intern方法源碼分析
在之前的文章中,小編給大家介紹了String字符串的不可變性及其實(shí)現(xiàn)原理,其中給大家提到了字符串常量池的概念,那么什么是常量池,String字符串與常量池有什么關(guān)系,本文給大家嘮嘮字符串常量池及String#intern()方法的作用,需要的朋友可以參考下2023-05-05
Java?guava框架LoadingCache及CacheBuilder本地小容量緩存框架總結(jié)
Guava?Cache本地緩存框架主要是一種將本地數(shù)據(jù)緩存到內(nèi)存中,但數(shù)據(jù)量并不能太大,否則將會占用過多的內(nèi)存,本文給大家介紹Java?guava框架?LoadingCache及CacheBuilder?本地小容量緩存框架總結(jié),感興趣的朋友一起看看吧2023-12-12
spring聲明式事務(wù) @Transactional 不回滾的多種情況以及解決方案
本文主要介紹了spring聲明式事務(wù) @Transactional 不回滾的多種情況以及解決方案,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2021-11-11
spring中ApplicationListener的使用小結(jié)
ApplicationListener是spring提供的一個監(jiān)聽器,本文主要介紹了spring中ApplicationListener的使用小結(jié),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2024-07-07

