背景
我們編寫的Web項目部署之后楚昭,經(jīng)常會因為需要進(jìn)行配置變更或功能迭代而重啟服務(wù)沼侣,單純的kill -9 pid
的方式會強(qiáng)制關(guān)閉進(jìn)程耕陷,這樣就會導(dǎo)致服務(wù)端當(dāng)前正在處理的請求失敗付秕,那有沒有更優(yōu)雅的方式來實現(xiàn)關(guān)機(jī)或重啟呢?
優(yōu)雅停機(jī)
在項目正常運行的過程中温亲,如果直接不加限制的重啟可能會發(fā)生一下問題
- 項目重啟(關(guān)閉)時,調(diào)用方可能會請求到已經(jīng)停掉的項目杯矩,導(dǎo)致拒絕連接錯誤(503)栈虚,調(diào)用方服務(wù)會緩存一些服務(wù)列表導(dǎo)致,服務(wù)列表依然還有已經(jīng)關(guān)閉的項目實例信息
- 項目本身存在一部分任務(wù)需要處理史隆,強(qiáng)行關(guān)閉導(dǎo)致這部分?jǐn)?shù)據(jù)丟失魂务,比如內(nèi)存隊列、線程隊列、MQ 關(guān)閉導(dǎo)致重復(fù)消費
為了解決上面出現(xiàn)的問題粘姜,提供以下解決方案:
- 關(guān)于問題 1 采用將需要重啟的項目實例鬓照,提前 40s 從 nacos 上剔除,然后再重啟對應(yīng)的項目孤紧,保證有 40s 的時間可以用來服務(wù)發(fā)現(xiàn)刷新實例信息豺裆,防止調(diào)用方將請求發(fā)送到該項目
- 使用 Spring Boot 提供的優(yōu)雅停機(jī)選項,再次預(yù)留一部分時間
- 使用 shutdonwhook 完成自定的關(guān)閉操作
一号显、主動將服務(wù)剔除
該方案主要考慮因為服務(wù)下線的瞬間留储,如果 Nacos 服務(wù)剔除不及時,導(dǎo)致仍有部分請求轉(zhuǎn)發(fā)到該服務(wù)的情況
在項目增加一個接口咙轩,同時在準(zhǔn)備關(guān)停項目前執(zhí)行 stop 方法获讳,先主動剔除這個服務(wù),shell 改動如下:
run.sh
function stop()
{
echo "Stop service please waiting...."
echo "deregister."
curl -X POST "127.0.0.1:${SERVER_PORT}/discovery/deregister"
echo ""
echo "deregister [${PROJECT}] then sleep 40 seconds."
# 這里 sleep 40 秒活喊,因為 Nacos 默認(rèn)的拉取新實例的時間為 30s, 如果調(diào)用方不修改的化丐膝,這里應(yīng)該最短為 30s
# 考慮到已經(jīng)接收的請求還需要一定的時間進(jìn)行處理,這里預(yù)留 10s, 如果 10s 還沒處理完預(yù)留的請求钾菊,調(diào)用方肯定也超時了
# 所以這里是 30 + 10 = 40sleep 40
kill -s SIGTERM ${PID}
if [ $? -eq 0 ];then
echo "Stop service done."
else
echo "Stop service failed!"
fi
}
在項目中增加 /discovery/deregister
接口
Spring Boot MVC 版本
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.serviceregistry.Registration;
import org.springframework.cloud.client.serviceregistry.ServiceRegistry;
import org.springframework.context.annotation.Lazy;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@SuppressWarnings("SpringJavaAutowiredFieldsWarningInspection")
@RestController
@RequestMapping("discovery")
@Slf4j
public class DeregisterInstanceController {
@Autowired
@Lazy
private ServiceRegistry serviceRegistry;
@Autowired
@Lazy
private Registration registration;
@PostMapping("deregister")
public ResultVO<String> deregister() {
log.info("deregister serviceName:{}, ip:{}, port:{}",
registration.getServiceId(),
registration.getHost(),
registration.getPort());
try {
serviceRegistry.deregister(registration);
} catch (Exception e) {
log.error("deregister from nacos error", e);
return ResultVO.failure(e.getMessage());
}
return ResultVO.success();
}
}
Spring Cloud Gateway
通過使用 GatewayFilter
方式來處理
package com.br.zeus.gateway.filter;
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.isAlreadyRouted;
import com.alibaba.fastjson.JSON;
import com.br.zeus.gateway.entity.RulesResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.serviceregistry.Registration;
import org.springframework.cloud.client.serviceregistry.ServiceRegistry;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.context.annotation.Lazy;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@SuppressWarnings("SpringJavaAutowiredFieldsWarningInspection")
@Component
@Slf4j
public class DeregisterInstanceGatewayFilter implements GatewayFilter, Ordered {
@Autowired
@Lazy
private ServiceRegistry serviceRegistry;
@Autowired
@Lazy
private Registration registration;
public DeregisterInstanceGatewayFilter() {
log.info("DeregisterInstanceGatewayFilter 啟用");
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
if (isAlreadyRouted(exchange)) {
return chain.filter(exchange);
}
log.info("deregister serviceName:{}, ip:{}, port:{}",
registration.getServiceId(),
registration.getHost(),
registration.getPort());
RulesResult result = new RulesResult();
try {
serviceRegistry.deregister(registration);
result.setSuccess(true);
} catch (Exception e) {
log.error("deregister from nacos error", e);
result.setSuccess(false);
result.setMessage(e.getMessage());
}
ServerHttpResponse response = exchange.getResponse();
response.getHeaders().setContentType(MediaType.APPLICATION_JSON_UTF8);
DataBuffer bodyDataBuffer = response.bufferFactory().wrap(JSON.toJSONBytes(result));
response.setStatusCode(HttpStatus.OK);
return response.writeWith(Mono.just(bodyDataBuffer));
}
@Override
public int getOrder() {
return 0;
}
}
在路由配置時帅矗,增加接口和過濾器的關(guān)系
.route("DeregisterInstance", r -> r.path("/discovery/deregister")
.filters(f -> f.filter(deregisterInstanceGatewayFilter))
.uri("https://example.com"))
二、Spring Boot 自帶的優(yōu)雅停機(jī)方案
要求 Spring Boot 的版本大于等于 2.3
在配置文件中增加如下配置:
application.yaml
server:
shutdown: graceful
spring:
lifecycle:
timeout-per-shutdown-phase: 10s
當(dāng)使用 server.shutdown=graceful
啟用時煞烫,在 web 容器關(guān)閉時浑此,web 服務(wù)器將不再接收新請求,并將等待活動請求完成的緩沖期滞详。使用 timeout-per-shutdown-phase
配置最長等待時間凛俱,超過該時間后關(guān)閉
三、使用 ShutdownHook
public class MyShutdownHook {
public static void main(String[] args) {
// 創(chuàng)建一個新線程作為ShutdownHook
Thread shutdownHook = new Thread(() -> {
System.out.println("ShutdownHook is running...");
// 執(zhí)行清理操作或其他必要的任務(wù)
// 1. 關(guān)閉 MQ
// 2. 關(guān)閉線程池
// 3. 保存一些數(shù)據(jù)
});
// 注冊ShutdownHook
Runtime.getRuntime().addShutdownHook(shutdownHook);
// 其他程序邏輯
System.out.println("Main program is running...");
// 模擬程序執(zhí)行
try {
Thread.sleep(5000); // 假設(shè)程序運行5秒鐘
} catch (InterruptedException e) {
e.printStackTrace();
}
// 當(dāng)程序退出時料饥,ShutdownHook將被觸發(fā)執(zhí)行
}
}