微服務優雅上下線的實踐方法
導語
本文介紹了微服務優雅上下線的實踐方法及原理,包括適用於 Spring 應用的優雅上下線邏輯和服務預熱,以及使用 Docker 實現無損下線的 Demo。同時,本文還總結了優雅上下線的價值和挑戰。
作者簡介
顏松柏
騰訊雲微服務架構師
擁有超過 10 年的 IT 從業經驗,精通軟件架構設計,微服務架構,雲架構設計等多個領域,在泛互、金融、教育、出行等多個行業擁有豐富的微服務架構經驗。
前言
微服務優雅上下線的原理是指在微服務的發佈過程中,保證服務的穩定性和可用性,避免因爲服務的變更而造成流量的中斷或錯誤。
微服務優雅上下線的原理可以從三個角度來考慮:
-
服務端的優雅上線,即在服務啓動後,等待服務完全就緒後再對外提供服務,或者有一個服務預熱的過程。
-
服務端的無損下線,即在服務停止前,先從註冊中心註銷,拒絕新的請求,等待舊的請求處理完畢後再下線服務。
-
客戶端的容災策略,即在調用服務時,通過負載均衡、重試、黑名單等機制,選擇健康的服務實例,避免調用不可用的服務實例。
微服務優雅上下線可以提高微服務的穩定性和可靠性,減少發佈過程中的風險和損失。
優雅上線
優雅上線,也叫無損上線,或者延遲發佈,或者延遲暴露,或者服務預熱。
優雅上線的目的是爲了提高發布的穩定性和可靠性,避免因爲應用的變更而造成流量的中斷或錯誤。
優雅上線的方法
優雅上線的方法有以下幾種:
-
延遲發佈:即延遲暴露應用服務,比如應用需要一些初始化操作後才能對外提供服務,如初始化緩存,數據庫連接池等相關資源就位,可以通過配置或代碼來實現延遲暴露。
-
QoS 命令:即通過命令行或 HTTP 請求來控制應用服務的上線和下線,比如在應用啓動時不向註冊中心註冊服務,而是在服務健康檢查完之後再手動註冊服務。
-
服務註冊與發現:即通過註冊中心來管理應用服務的狀態和路由信息,比如在應用啓動時向註冊中心註冊服務,並監聽服務狀態變化事件,在應用停止時向註冊中心註銷服務,並通知其他服務更新路由信息。
-
灰度發佈:即通過分流策略來控制應用服務的流量分配,比如在發佈新版本的應用時,先將部分流量導入到新版本的應用上,觀察其運行情況,如果沒有問題再逐步增加流量比例,直到全部切換到新版本的應用上。
上面的方法核心思想都是一個,就是等服務做好了準備再把請求放行過去。
優雅上線的實現
大部分優雅上線都是通過註冊中心和服務治理能力來實現的。
對於初始化過流程較長的應用,由於註冊通常與應用初始化過程同步進行,因此可能出現應用還未完全初始化就已經被註冊到註冊中心供外部消費者調用,此時直接調用可能會導致請求報錯。
所以,通過服務註冊與發現來做優雅上線的基本思路是:
-
在應用啓動時,提供一個健康檢查接口,用於反饋服務的狀態和可用性。
-
應用啓動後,可以採用下列方法來使新的請求暫時不進入新版的服務實例。
-
暫時不向註冊中心註冊服務。
-
隔離服務,有些註冊中心支持隔離服務實例,比如北極星。
-
將權重配置爲 0。
-
將服務實例的 Enable 改爲 False。
-
讓健康檢查接口返回不健康的狀態。
-
在新版本的應用實例完成初始化操作後,確保了可用性後,再對應的將上述的方法取消,這樣就可以讓新的請求被路由到新版本的應用實例上。
-
如果需要預熱,就讓流量進入新版本的應用實例時按比例的一點點增加。
這樣,就可以實現優雅上線的過程,保證請求進來的時候,不會因爲新版本的應用實例沒有準備好而導致請求失敗。
優雅上線的北極星代碼 Demo
我們以 Spring Cloud 和 北極星 爲例,講一下如何通過服務註冊與發現來做優雅上線的過程。
首先,我們需要創建一個 Spring Cloud 項目,並添加北極星的依賴。
然後,我們需要在 application.properties 文件中配置北極星的相關信息,如註冊中心地址,服務名,分組名等,例如:
spring:
application:
name: ${application.name}
cloud:
polaris:
address: grpc://${修改爲第一步部署的 Polaris 服務地址}:8091
namespace: default
然後,我們需要創建一個 Controller 類,提供一個簡單的接口,用於返回服務的信息,例如:
@RestController
public class ProviderController {
@Value("${server.port}")
private int port;
@GetMapping("/hello")
public String hello() {
return "Hello, I am provider, port: " + port;
}
}
最後,如果需要我們可以重寫健康檢查接口,用於反饋服務的狀態和可用性。這裏我們需要引入 Actuator。
@Component
public class DatabaseHealthIndicator implements HealthIndicator {
@Override
public Health health() {
if (isDatabaseConnectionOK()) {
return Health.up().build();
} else {
return Health.down().withDetail("Error Code", "DB-001").build();
}
}
private boolean isDatabaseConnectionOK() {
// 檢查數據庫連接、緩存等
return true;
}
}
這樣,我們就完成了一個簡單的服務提供者應用,並且可以通過北極星來實現服務註冊與發現。
接下來,我們需要創建一個服務消費者應用,並且也添加北極星的依賴和配置信息。
然後,使用 RestTemplate 來調用服務提供者的接口,例如:
@SpringBootApplication
public class ConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(ConsumerApplication.class, args);
}
@Bean
@LoadBalanced // 開啓負載均衡
public RestTemplate restTemplate() {
return new RestTemplate();
}
@RestController
public class ConsumerController {
@Autowired
private RestTemplate restTemplate;
@GetMapping("/hello")
public String hello() {
// 使用服務名來調用服務提供者的接口
return restTemplate.getForObject("<http://provider/hello>", String.class);
}
}
}
這裏我們使用了 @LoadBalanced 註解來開啓負載均衡功能,並且使用服務名 provider 來調用服務提供者的接口。
這樣,我們就完成了一個簡單的服務消費者應用,並且可以通過北極星來實現服務註冊與發現。
接下來,我們就可以通過以下步驟來實現優雅上線的過程:
- 在發佈新版本的服務提供者應用時,先啓動新版本的應用實例,但是不向註冊中心註冊服務,或者讓健康檢查接口返回不健康的狀態,這樣就不會有新的請求進入新版本的應用實例。這可以通過配置或代碼來實現,例如:
# 不向註冊中心註冊服務
spring.cloud.polaris.discovery.register=false
// 讓健康檢查接口返回不健康的狀態
this.isHealthy = false;
- 在新版本的應用實例完成初始化操作後,再向註冊中心註冊服務,或者讓健康檢查接口返回健康的狀態,這樣就可以讓新的請求被路由到新版本的應用實例上。這可以通過配置或代碼來實現,例如:
# 向註冊中心註冊服務
spring.cloud.polaris.discovery.register=true
// 讓健康檢查接口返回健康的狀態
this.isHealthy = true;
這樣,就可以實現優雅上線的過程,保證正在處理的請求不會被中斷,而新的請求會被路由到新版本的應用上。
不過,如果對優雅上線的極致要求不高,北極星本身就是支持優雅上線的,無須做額外的操作。因爲北極星的邏輯是,當 Spring 的 Bean 全部加載完成後,Controller 能訪問後纔會去註冊服務。所以,在絕大多數的場景下,它已經滿足了優雅上線的要求。
服務預熱
服務預熱是指在服務上線之前,先讓服務處於一個運行狀態,讓其加載必要的資源、建立連接等,以便在服務上線後能夠快速響應請求。如下圖所示。
在流量較大情況下,剛啓動的服務直接處理大量請求可能由於應用內部資源初始化不徹底從而出現請求阻塞、報錯等問題。此時通過服務預熱,在服務剛啓動階段通過小流量幫助服務在處理大量請求前完成初始化,可以幫助發現服務上線後可能存在的問題,例如資源不足、連接數過多等,從而及時進行調整和優化,確保服務的穩定性和可靠性。
雲原生 API 網關實現服務預熱
雲原生 API 網關是騰訊雲基於開源微服務網關推出的一款高性能高可用的雲上網關託管產品。我們可以通過簡單的幾個配置就能實現服務預熱。
首先我們在網關新建後端服務的時候,可以打開下圖中的慢啓動開關。同時可以設置慢啓動的時間。
開啓後,服務端有新的服務節點上線後,會在設置的慢啓動的時間內,將新節點的權重從 1 逐步增加到目標值。這個新節點的流量會慢慢增加。
如果有多個新增節點,那所有新增的節點都會慢啓動。
針對後端來源是 K8S 服務 、註冊中心、IP 列表的服務都可以實現慢啓動,也就是服務預熱。
優雅下線
無損下線、優雅下線都是同一個意思。都是爲了避免服務下線的時候由於請求沒有處理完導致請求失敗的情況。
優雅下線的方法
無損下線的一些常用的工具或框架有:
-
Dubbo-go:支持多種註冊中心、負載均衡、容災策略等,可以實現優雅上下線的設計與實踐。
-
Spring Cloud:提供了多種組件來實現服務的配置、路由、監控、熔斷等,可以通過監聽 ContextClosedEvent 事件來實現優雅下線的邏輯。
-
Docker:可以通過 Docker Stop 或 Docker Kill 命令來停止容器,前者會發送 SIGTERM 信號給容器的 PID1 進程,後者會發送 SIGKILL 信號。如果程序能響應 SIGTERM 信號,就可以實現優雅下線的操作。
Spring Cloud 優雅下線的原理
ContextClosedEvent 是 Spring 容器在關閉時發佈的一個事件,可以通過實現 ApplicationListener 接口來監聽這個事件,並在 onApplicationEvent 方法中執行一些自定義的邏輯。
對於 Spring Cloud 中的微服務來說,當收到 ContextClosedEvent 事件時,可以做以下幾件事情:
-
從註冊中心註銷當前服務,這樣就不會再有新的請求進入。
-
拒絕或者延遲新的請求,這樣就可以保證正在處理的請求不會被中斷。
-
等待一段時間,讓舊的請求處理完畢,或者超時。
-
關閉服務,釋放資源。
這樣就可以實現優雅下線的邏輯,避免因爲服務的變更而造成流量的中斷或錯誤。
Spring Boot 優雅下線的 Demo
在舊版本里面,我們需要實現 TomcatConnectorCustomizer 和 ApplicationListener 接口,然後就可以在 Customize 方法中獲取到 Tomcat 的 Connector 對象,並在 onApplicationEvent 方法中監聽到 Spring 容器的關閉事件。
在 2.3 及以後版本,我們只需要在 application.yml 中添加幾個配置就能啓用優雅關停了。
# 開啓優雅停止 Web 容器,默認爲 IMMEDIATE:立即停止
server:
shutdown: graceful
# 最大等待時間
spring:
lifecycle:
timeout-per-shutdown-phase: 30s
這個開關的具體實現邏輯在我們在 GracefulShutdown 裏。
然後我們需要添加 Actuator 依賴,然後在配置中暴露 Actuator 的 Shutdown 接口。
# 暴露 shutdown 接口
management:
endpoint:
shutdown:
enabled: true
endpoints:
web:
exposure:
include: shutdown
這個時候,我們調用 http://localhost:8080/actuator/shutdown 就可以執行優雅關停了,它會返回如下內容:
{
"message": "Shutting down, bye..."
}
優缺點
我覺得這種方法有以下的優點和缺點:
優點:
-
簡單易用,只需要實現兩個接口,就可以實現優雅下線的邏輯。
-
適用於 Tomcat 作爲內嵌容器的 Spring Boot 應用,不需要額外的配置或依賴。
-
可以保證正在處理的請求不會被中斷,而新的請求不會進入,避免了服務的變更造成流量的中斷或錯誤。
缺點:
-
只適用於 Tomcat 作爲內嵌容器的 Spring Boot 應用,如果使用其他的容器或部署方式,可能需要另外的實現。
-
需要等待一定的時間,讓正在處理的請求完成或超時,這可能會影響服務的停止速度和資源的釋放。
-
如果正在處理的請求過多或過慢,可能會導致線程池無法優雅地關閉,或者超過系統的終止時間,造成強制關閉。
Docker 優雅下線的 Demo
這裏用一個簡單的 JS 應用來演示 Docker 實現無損下線的過程。
首先,我們需要創建一個 Dockerfile 文件,用於定義一個簡單的應用容器,代碼如下:
# 基於 node:14-alpine 鏡像
FROM node:14-alpine
# 設置工作目錄
WORKDIR /app
# 複製 package.json 和 package-lock.json 文件
COPY package*.json ./
# 安裝依賴
RUN npm install
# 複製源代碼
COPY . .
# 暴露 3000 端口
EXPOSE 3000
# 啓動應用
CMD [ "node", "app.js" ]
然後,我們需要創建一個 app.js 文件,用於定義一個簡單的 Web 應用,代碼如下:
// 引入 express 模塊
const express = require('express');
// 創建 express 應用
const app = express();
// 定義一個響應 /hello 路徑的接口
app.get('/hello', (req, res) => {
// 返回 "Hello, I am app" 字符串
res.send('Hello, I am app');
});
// 監聽 3000 端口
app.listen(3000, () => {
// 打印日誌信息
console.log('App listening on port 3000');
});
接下來,我們需要在終端中執行以下命令,來構建和運行我們的應用容器,並查看頁面結果。
# 構建鏡像,命名爲 app:1.0.0
docker build -t app:1.0.0 .
# 運行容器,命名爲 app-1,映射端口爲 3001:3000
docker run -d --name app-1 -p 3001:3000 app:1.0.0
# 查看容器運行狀態和端口映射信息
docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
a8a9f9f7c6c4 app:1.0.0 "docker-entrypoint.s…" 10 seconds ago Up 9 seconds 0.0.0.0:3001->3000/tcp app-1
# 在瀏覽器中訪問 <http://localhost:3001/hello> ,可以看到返回 "Hello, I am app" 字符串
這個時候假設我們要發佈一個新版本的應用,我們需要修改 app.js 文件中的代碼,把返回的字符串修改爲 “Hello, I am app v2”。
然後,我們需要在終端中執行以下命令,來構建和運行新版本的應用容器:
# 構建鏡像,命名爲 app:2.0.0
docker build -t app:2.0.0 .
# 運行容器,命名爲 app-2,映射端口爲 3002:3000
docker run -d --name app-2 -p 3002:3000 app:2.0.0
# 查看容器運行狀態和端口映射信息
docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
b7b8f8f7c6c4 app:2.0.0 "docker-entrypoint.s…" 10 seconds ago Up 9 seconds 0.0.0.0:3002->3000/tcp app-2
a8a9f9f7c6c4 app:1.0.0 "docker-entrypoint.s…" 2 minutes ago Up 2 minutes 0.0.0.0:3001->3000/tcp app-1
# 在瀏覽器中訪問 <http://localhost:3002/hello> ,可以看到返回 "Hello, I am app v2" 字符串
接下來,需要優雅地下線舊版本的應用容器,讓它完成正在處理的請求,然後停止接收新的請求,最後退出進程。
# 向舊版本的應用容器發送 SIGTERM 信號,讓它優雅地終止
docker stop app-1
# 查看容器運行狀態和端口映射信息
docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
b7b8f8f7c6c4 app:2.0.0 "docker-entrypoint.s…" 2 minutes ago Up 2 minutes 0.0.0.0:3002->3000/tcp app-2
# 在瀏覽器中訪問 <http://localhost:3001/hello> ,可以看到無法連接到服務器的錯誤
這樣,我們就實現了通過 Docker 來做優雅下線的過程,保證正在處理的請求不會被中斷,而新的請求會被路由到新版本的應用上。
這裏主要用到了 Docker Stop 命令。Docker Stop 命令會向容器發送 SIGTERM 信號,這是一種優雅終止進程的方式,它會給目標進程一個清理善後工作的機會,比如完成正在處理的請求,釋放資源等。如果目標進程在一定時間內(默認爲 10 秒)沒有退出,Docker Stop 命令會再發送 SIGKILL 信號,強制終止進程。
所以,使用 Docker Stop 命令能實現優雅下線的前提是,容器中的應用能夠正確地響應 SIGTERM 信號,並在收到該信號後執行清理工作。如果容器中的應用忽略了 SIGTERM 信號,或者在清理工作過程中出現異常,那麼 Docker Stop 命令就無法實現優雅下線的效果。
讓容器中的應用正確地響應 SIGTERM 信號的方法,主要取決於容器中的 1 號進程是什麼,以及它如何處理信號。如果容器中的 1 號進程就是應用本身,那麼應用只需要在代碼中爲 SIGTERM 信號註冊一個處理函數,用於執行清理工作和退出進程。例如,在 Node.js 中,可以這樣寫:
// 定義一個處理 SIGTERM 信號的函數
function termHandler() {
// 執行清理工作
console.log('Cleaning up...');
// 退出進程
process.exit(0);
}
// 爲 SIGTERM 信號註冊處理函數
process.on('SIGTERM', termHandler);
北極星的優雅下線
北極星的心跳默認是 5 秒維持一次,客戶端的緩存默認是 2 秒刷新一次。理論上,在極致情況下,服務下線會有 2 秒的不可用時間。但客戶端都有重試機制,且大部分客戶端的超時時間都是大於 2 秒的。因此大部分情況下,服務在北極星下線是不會造成業務感知的。
北極星的優雅下線有多種方式。其中上面的 Spring Boot 與 Docker 的方式是其中兩種。
另外一種是可以在服務下線的時候,在 PreStop 的時候去做服務隔離與反註冊。
這樣的隔離操作可以手動做,也可以通過腳本來自動做。
如上圖,被隔離的實例將不會被主調方發現,這樣就不會有新的需求進來,在處理完成現有的請求後,就可以執行下線操作了。
總結
優雅上下線的價值
在微服務實踐中,實現優雅上下線能給我們帶來以下好處:
-
最小化服務中斷:通過優雅上下線,可以最小化服務中斷的時間和影響範圍,從而確保服務的可用性和穩定性。
-
避免數據丟失:優雅下線可以確保正在處理的請求能夠完成,避免數據丟失和請求失敗。
-
提高用戶體驗:優雅上下線可以確保用戶在使用服務時不會遇到任何中斷或錯誤,從而提高用戶體驗和滿意度。
-
簡化部署流程:通過使用自動化工具和流程,可以簡化部署流程,減少人工干預和錯誤,提高部署效率和質量。
-
提高可維護性:通過使用監控和日誌記錄工具,可以及時發現和解決問題,提高服務的可維護性和可靠性。
這些好處可以幫助企業提高服務質量和效率,提升用戶滿意度和競爭力。
優雅上下線的挑戰
但同時,優雅上下線也面臨一些挑戰:
-
複雜性增加:微服務架構通常由多個服務組成,每個服務都有自己的生命週期和依賴關係,因此優雅上下線需要考慮多個服務之間的交互和協調,增加了系統的複雜性。
-
部署流程複雜:優雅上下線需要使用自動化工具和流程,這需要投入大量的時間和資源來構建和維護,增加了部署流程的複雜性。
-
數據一致性問題:優雅下線需要確保正在處理的請求能夠完成,但這可能會導致數據一致性問題,需要採取措施來解決這個問題。
-
人員技能要求高:微服務架構需要具備更高的技術水平和技能,需要擁有更多的開發和運維經驗,這對企業的人員要求較高。
綜上所述,企業需要認真考慮這些挑戰,並採取相應的措施來解決這些問題,以確保在微服務實踐中更好的落地優雅上下線。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/y0CnPy6r91bXzvCSH9s89w