Spring Cloud如何實(shí)現(xiàn)服務(wù)的平滑發(fā)布

一、問題描述

微服務(wù)之間的調(diào)用或者網(wǎng)關(guān)轉(zhuǎn)發(fā)請求到服務(wù),是通過向服務(wù)注冊中心請求來獲取可用的服務(wù)列表。為了提高性能,每個微服務(wù)都會緩存一份獲取到的服務(wù)列表。這樣就導(dǎo)致在一個微服務(wù)容器關(guān)閉時,即使微服務(wù)注冊中心及時的注銷了這個服務(wù)節(jié)點(diǎn),但是還是會有一部分請求被分發(fā)到這個已關(guān)閉的節(jié)點(diǎn)。ribbon調(diào)用是不平滑的,shutdown請求到后服務(wù)就馬上關(guān)閉了,服務(wù)消費(fèi)此時未感應(yīng)到服務(wù)下線了,會仍然往這個服務(wù)發(fā)送請求,從而導(dǎo)致報錯。

二、方案

建議:無論用那種方式,重要接口最好做降級處理。

2.1 方案一、啟用服務(wù)的重試功能

這個方案需要保證所有接口的處理都是冪等的,對接口處理的設(shè)計要求比較高,而且也可能會帶來額外的問題。

1、引入pom

<dependency>
    <groupId>org.springframework.retry</groupId>
    <artifactId>spring-retry</artifactId>
</dependency>

2、加入配置

ribbon.OkToRetryOnAllOperations:true
#(是否所有操作都重試,若false則僅get請求重試)
ribbon.MaxAutoRetriesNextServer:3
#(重試負(fù)載均衡其他實(shí)例最大重試次數(shù),不含首次實(shí)例)
ribbon.MaxAutoRetries:1
#(同一實(shí)例最大重試次數(shù),不含首次調(diào)用)
ribbon.ReadTimeout:30000
ribbon.ConnectTimeout:3000
ribbon.retryableStatusCodes:404,500,503
#(那些狀態(tài)進(jìn)行重試)
spring.cloud.loadbalancer.retry.enable:true
# (重試開關(guān))
2.2 方案二、在服務(wù)關(guān)閉前從注冊中心注銷后,仍然保留一段時間服務(wù)響應(yīng)能力,之后再關(guān)閉
2.2.1 注銷服務(wù)說明

1、注銷Eureka服務(wù)
默認(rèn)當(dāng)Eureka Server連續(xù)3次(默認(rèn)心跳間隔是30s)沒有收到該服務(wù)的心跳時,會自動將該實(shí)例注銷(進(jìn)入自我保護(hù)模式時除外)。但是也可以通過手動發(fā)送 DELETE 請求到 Eureka Server 來注銷服務(wù)實(shí)例。

curl -v -X DELETE http://{Eureka Server 地址}/eureka/apps/{Application 名}/{Eureka 實(shí)例的 ID}

Eureka 實(shí)例的 ID 可以在 Eureka Server 頁面上查看到。

2、注銷Nacos服務(wù)
Nacos默認(rèn)心跳時間是30秒

curl -X DELETE '127.0.0.1:8848/nacos/v1/ns/instance?serviceName=nacos.test.1&ip=1.1.1.1&port=8888&clusterName=TEST1'

注銷實(shí)例沒什么特別的處理,我們可以簡單理解成Nacos是將請求下線的節(jié)點(diǎn)從持有的服務(wù)列表中刪除該節(jié)點(diǎn)。

2.2.2 自定義了一個EndPoint在其中注銷Nacos服務(wù)

之所以通過 Actuator 暴露接口來調(diào)用是因?yàn)橥ㄟ^設(shè)置來保證其安全性,具體的方法可以參考這篇博客:修改 Actuator 路徑和端口。

這種方式通過在微服務(wù)中調(diào)用 Nacos 提供的接口來注銷,注銷之后就不會再發(fā)送心跳。之后通過 Sleep 一定的時間來阻止服務(wù)的關(guān)閉,使其仍然可以保持響應(yīng)一段時間。這里需要配合阿里云容器服務(wù)( Kubernetes 版) 的 停止前處理 功能來觸發(fā)。

1、添加自定義的 EndPoint

import com.alibaba.cloud.nacos.NacosDiscoveryProperties;
import com.alibaba.nacos.api.exception.NacosException;
import com.alibaba.nacos.api.naming.NamingService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.actuate.endpoint.annotation.DeleteOperation;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
import org.springframework.boot.actuate.endpoint.annotation.Selector;
import org.springframework.stereotype.Component;

import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.HashMap;
import java.util.Map;

@Component
@Endpoint(id = "nacos")
@RequiredArgsConstructor
@Slf4j
public class NacosEndpoint {

    private final NacosDiscoveryProperties nacosDiscoveryProperties;

    @Value("${spring.application.name}")
    private String applicationName;

    @Value("${server.port}")
    private Integer port;

    @DeleteOperation
    public Map<String, String> deregister() {
        return deregisterAndSleep(0);
    }

    @DeleteOperation
    public Map<String, String> deregisterAndSleep(@Selector int seconds) {
        Map<String, String> result = new HashMap<>();

        InetAddress addr = null;
        try {
            addr = InetAddress.getLocalHost();
        } catch (UnknownHostException e) {
            log.error(e.getMessage(), e);
            result.put("code", "failure");
            result.put("message", e.getMessage());
            return result;
        }

        log.info(String.format("Deregister nacos instance (%s - %s:%d)", applicationName, addr.getHostAddress(), port));

        NamingService namingService = nacosDiscoveryProperties.namingServiceInstance();
        try {
            namingService.deregisterInstance(applicationName, addr.getHostAddress(), port);
        } catch (NacosException e) {
            log.error(e.getMessage(), e);
            result.put("code", "failure");
            result.put("message", e.getMessage());
            return result;
        }

        log.info("Deregister nacos instance success.");

        if (seconds > 0) {
            try {
                log.info(String.format("Thread sleep %d seconds.", seconds));
                Thread.sleep(seconds * 1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
                log.error(e.getMessage(), e);
                result.put("code", "failure");
                result.put("message", e.getMessage());
                return result;
            }
        }

        result.put("code", "ok");
        return result;
    }
}

2、配置 K8S
在 阿里云容器服務(wù)(Kubernetes版)的停止前處理 中調(diào)用該 EndPoint ["curl","-X","DELETE","http://localhost:5678/customize-actuator/nacos/30"]

2.3 方案三、本公司方案

K8S配一個探針,每隔1秒去訪問服務(wù)的端口,判斷服務(wù)是否可用,當(dāng)發(fā)布版本更新服務(wù),該服務(wù)端口可用30秒后,才把流量分配給該服務(wù)。

資料來源:
Spring Cloud 微服務(wù)平滑更新問題
SpringCloud服務(wù)的平滑上下線

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容