將新版本軟體交付給用戶時,擁有不同的部署策略對於保證以高效率且穩健的方式是非常重要的。 在閱讀完其他文章後,我們可以整理出如下總結(如果您對部署策略不熟悉,請參閱baeldung blog或plutora blog複習一下):
Recreate Deployment 最簡單,但可能會導致服務停機並向所有使用者暴露潛在的 Bug。 其他(Blue/Green, Rolling, A/B測試, Shadow, Canary…)可以保證零停機時間,其中一些使用更多的資源(Memory, CPU 等硬體資源…)及複雜的設定來實現在同一時間運行兩個版本的應用程式,來達到同時為 Release 提供更多信心或更容易回滾到舊版本。
然而,我們不應該將硬體資源視為免費或無限的,尤其像現在整個軟體行業陷入困難時。正如Pete Hodgson在他的文章中所說(Feature Toggle),我們可以使用Feature Toggle系統來執行部署策略,這可以節省一些資源。此外,還可以消除為策略設置CD工具或網路相關元件(像是Load Balancer等)的繁雜工作(對於一些不熟悉DevOps或SRE知識的開發人員)。唯一剩下的工作就是設置Toggle和寫一些程式(可使用if/else
或switch
輕鬆完成)。
在這篇文章中,我們將介紹:
- Feature Toggle需要哪些功能來實現這一點?
- 如何使用Feature Toggle執行不同的部署策略(Blue/Green部署、A/B測試、Canary Release、Shadow部署…)?
- 如何最小化Feature Toggle的維護工作?
這是為本文所寫的範例GitHub Repository open-feature-openflagr-example,歡迎訪問並留下任何評論。
Feature Toggle 系統的需求
在考慮採用Feature Toggle而不是複雜的發佈策略時,可以先看看網路上提供的開源版或企業級Feature Toggle(Unleash,Flagsmith,Flagr,LaunchDarkly 等),並選擇一個具有以下特點和最低要求的Feature Toggle:
- 動態評估API並支持高RPS:Feature Toggle在通過其API評估Toggle狀態時應有效處理高RPS負載(通過API獲取Toggle是否打開/關閉),因為Toggle應該在最小程度上影響核心業務性能。
- 動態設定和持久化:Feature Toggle系統應允許通過UI或API動態調整設定。另外它也得確保設定更改後能持久存在,保證系統在重新啟動前後有一致的行為。
- Toggle評估API應提供以下功能:
- 支援請求鍵值 (ID):基於請求中的鍵值去決定 Toggle 的評估結果(例如使用 Hash 算法來做到機率分布式的 Toggle),並確保相同的鍵值總是獲得相同的結果。
- 支援評估請求的 Body 內容:可以設置規則以決定 Toggle 的評估結果(例如:當請求的Body中有個屬性
region
=Asia時Toggle開啟;=Europe時Toggle關閉)。
以上是將用Feature Toggle來替代部 署策略的最低系統要求。如此一來,我們可以將流量設作業轉移到我們的程式開發工作中,還能與功能一起發出 Pull Request (PR) 讓 同事一起進行 Code Review。
使用Feature Toggle的部署策略
在這部分中,我們將展示如何設定Toggle(以Flagr為例),並demo在簡單情況下程式碼片段的實作方式(我在demo中使用最單純的if/else
或switch
,在實際專案中可以考慮用策略模式或其他更優雅的方式去寫)。首先,我們將從最簡單的Toggle開啟/關閉開始,執行 Blue/Green 部署或 Shadow 部署。接著應用基於百分比的滾動設置在Toggle上以實現 Canary Release。最後加上條件規則去根據請求的Body內容決定結果以實現 A/B 測試。
給定以下的程式碼片段(v1 會印出藍色的o; v2 會印出綠色的x)共用於接下來的 demo:
public static String v1Feature() {
return BLUE + "o" + RESET;
}
public static String v2Feature() {
return GREEN + "x" + RESET;
}
Blue/Green 部署 (toggle: 啟用/停用)
首先要達成 Blue/Green部署 ,Toggle的設定如下非常簡單:
而對應的程式碼如下所示:
boolean toggleOn = client.getBooleanValue(FLAG_KEY, false, ctx);
String message;
if (toggleOn) {
message = v2Feature();
v2++;
} else {
message = v1Feature();
v1++;
}
System.out.print(message);
一開始我將開關設定為關閉,然後在程式執行期間將其打開。我們可以看到應用程式正如我們預期的那樣,在兩個功能之間順利切換。
這樣一來,我們就可以節省很多硬體資源,因為我們無需維護完全不同的兩個環境(藍色和綠色) 來運行不同版本的應用程式。
Shadow 部署 (toggle: 啟用/停用)
在這個例子中,我們可以與 Blue/Green 佈署共享相同的 Toggle 設定,但首先將 Toggle 預設開啟。以下是Shadow部署的程式碼:
String version = client.getStringValue(FLAG_KEY, "off", ctx);
String message = "";
message = v1Feature();
v1++;
if (version.equalsIgnoreCase("on")) {
Thread newThread = new Thread(() -> {
atomicString.accumulateAndGet(v2Feature(), String::concat);
v2.getAndIncrement();
});
newThread.start();
}
System.out.print(message);
一開始,我們將同時調用 v1 和 v2 功能,假設在 v2 功能中發現了一些問題,就可以在程式執行期間將 Toggle 關閉。我們可以看到在 toggle 關閉後 v2 功能就不會再被呼叫。
使用Toggle系統執行Shadow部署是一種有效且高度靈活的方式。只要我們在程式碼中增加一些複雜性,稍微小心一點處理非同步程式的部分即可。
Canary Release (toggle: 基於百分比的滾動部署)
讓我們將機率分佈功能加入 Toggle 的設定以進行Canary Release。
以下是Canary部署的程式碼:
...
UUID userId = UUID.randomUUID();
MutableContext ctx = new MutableContext(userId.toString());
String version = client.getStringValue(FLAG_KEY, "v1", ctx);
String message = "";
switch (version) {
case "v1" -> {
message = v1Feature();
v1++;
}
case "v2" -> {
message = v2Feature();
v2++;
}
}
System.out.print(message);
...
給定一開始設定的分佈比例為 3:1(v1=75%; v2=25%),並且由於我們給每個請求分配了不同的鍵值 (ID) targetKey
,我們將獲得一個非常接近給定分佈的結果。
若是我們程式都給定相同的 targetKey=tester
,結果將保持不變,因為被Hash到相同的結果(在這個例子中是v2)。
因此,我們可以說使用Toggle系統進行 Canary Release 非常簡單易懂。只要我們認為新功能足夠穩定,我們隨時可以更改百分比,以便進行下一個階段的部署分佈。
A/B Testing (toggle: 條件規則根據請求的 Body內容)
最後,我們來實作 A/B 測試。 透過對 Toggle 系統加上條件去根據請求的 Body 內容決定結果,如下所示。
而程式碼則需多給出 context
帶有 region
這個屬性,如下所示:
UUID userId = UUID.randomUUID();
MutableContext ctx = new MutableContext(userId.toString());
ctx.add("region", region);
String version = client.getStringValue(FLAG_KEY, "v1", ctx);
String message = "";
switch (version) {
case "v1" -> {
message = v1Feature();
v1++;
}
case "v2" -> {
message = v2Feature();
v2++;
}
}
System.out.print(message);
預期所有來自亞洲的使用者應該使用 v1 功能,而來自歐洲的使用者應該使用 v2,而其他地區使用者應該分一半一半的使用功能。正如我們在 console log 中所看到的,結果就如同前面所預期的。