Spring Cloud 框架優(yōu)雅關(guān)機和重啟

我們編寫的Web項目部署之后,經(jīng)常會因為需要進(jìn)行配置變更或功能迭代而重啟服務(wù),單純的kill -9 pid的方式會強制關(guān)閉進(jìn)程,這樣就會導(dǎo)致服務(wù)端當(dāng)前正在處理的請求失敗,那有沒有更優(yōu)雅的方式來實現(xiàn)關(guān)機或重啟呢?
在項目正常運行的過程中,如果直接不加限制的重啟可能會發(fā)生一下問題
-
項目重啟(關(guān)閉)時,調(diào)用方可能會請求到已經(jīng)停掉的項目,導(dǎo)致拒絕連接錯誤(503),調(diào)用方服務(wù)會緩存一些服務(wù)列表導(dǎo)致,服務(wù)列表依然還有已經(jīng)關(guān)閉的項目實例信息
-
項目本身存在一部分任務(wù)需要處理,強行關(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)雅停機選項,再次預(yù)留一部分時間
-
使用 shutdonwhook 完成自定的關(guān)閉操作
該方案主要考慮因為服務(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)雅停機方案
要求 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)閉
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í)行
????}
}
