最近用户有個需求:更新 ASP.NET Core 應用時,要讓訪問不中斷且用户無感知,部署環境為 Windows Server + IIS。自然想到了藍綠部署,之前沒有應用過 URL Rewrite + ARR,就趁此實踐一下。
原本想着很簡單:對 URL 重寫規則不熟,直接問 ai。結果反倒被 ai 誤導,折騰了好一陣子才搞好,在此分享配置過程、重寫規則以及相關代碼。
1 概念簡介
開始之前,簡要説明一下本文涉及的三個關鍵概念及其在本方案中的作用:
-
URL Rewrite
IIS 的 URL 重寫模塊,可根據設置的規則匹配並處理請求。在本方案中,它承擔關鍵的請求分發工作:通過在一個 Switch 站點中配置重寫規則,將請求按需轉發到 Blue 站點或 Green 站點,實現版本之間的快速切換。 -
ARR(Application Request Routing)
IIS 的反向代理擴展,提供代理與轉發能力。需要説明的是本方案沒有使用 ARR 的 Server Farms 功能,而是依賴其反向代理能力以便支持 URL Rewrite 的 “代理式重寫”:有了 ARR 重寫才能有反代效果。 -
藍綠部署(Blue-Green Deployment)
一種應用程序發佈策略,即準備兩套功能一致的環境(藍/綠),在同一時間只有一個環境(如藍)承載線上流量。新版本部署到閒置環境(綠),測試通過後,通過切換流量瞬間完成發佈,實現零停機和快速回滾。
2 最終部署結構
先説結果:

示例裏的部署目錄結構(本文裏會以此目錄為例):

即:在 IIS 裏創建 3 個站點:Switch 站點、Blue 站點以及 Green 站點,需要共享的配置、緩存、附件等位於站外。數據庫自然也用同一數據庫。
用户通過 Switch 站點 http://192.168.0.116:9080 訪問系統(各站點端口可根據你的實際情況設置),Switch 站點再根據設置的重寫規則,將用户請求導向 Blue 站點或 Green 站點。藍綠站點同時只有其中的一個為用户提供服務。
初始部署時(v1.0.0),應用發佈到 Blue 站點,並讓 Switch 站點將請求導向 Blue 站點,用户開始正常訪問。
第一次更新(v1.0.1),新版本發佈到 Green 站點並進行測試、預熱,然後讓 Switch 站點將請求導向 Green 站點,用户訪問不中斷。
第二次更新(v1.0.2),Blue 站點此時處於空閒狀態,因此可以安全地將其停掉,並刪除舊版本、放入新版本進行測試、預熱,然後讓 Switch 站點再將請求導向 Blue 站點,用户訪問仍不會中斷。
如此重複,滾動更新。
這裏 有個藍綠部署示例應用,可分別用兩個瀏覽器去登錄切換試試:在瀏覽器A裏打開一個列表或編輯頁面,然後在瀏覽器B裏切換一下站點,再回到瀏覽器A裏繼續進行分頁查詢或點擊保存按鈕,響應將依然正常。如果只有一個瀏覽器,可分別用正常模式和無痕模式去登錄。
3 環境準備
先確保 IIS 與 ASP.NET Core 運行環境已安裝好,運行環境版本要與你發佈應用時指定的一致。
3.1 安裝 URL Rewrite
下載地址:https://www.iis.net/downloads/microsoft/url-rewrite,到頁面底部下載適用自己的安裝包。
要確認是否安裝:打開 IIS 管理器,在左側選中一個站點,看看右側功能列表裏有沒有 "URL 重寫"。
3.2 ARR 安裝與配置
下載地址:https://www.iis.net/downloads/microsoft/application-request-routing。
安裝好後,打開 IIS 管理器,在左側選中計算機名或服務器名,在右側功能列表裏找到 "Application Request Routing Cache":

雙擊打開,在右側找到並點擊 "Server Proxy Settings":

然後按照下圖配置:

為何取消選中 "Reverse rewrite host in response headers":因為選中後響應頭中的 Host 會被強行替換成 Switch 的 Host,這在跨域回調時可能會有問題。
至此 ARR 就配置好了,因為我們不需要使用其 Server Farms 功能。
4 藍綠站點創建與配置
創建 Blue 站點和 Green 站點,路徑分別指向 QAdminAppBlue 目錄和 QAdminAppGreen 目錄,將用來放置應用程序文件。
讓兩個站點分別監聽 5001 和 5002 端口(端口號你可自行調整),各自使用獨立的應用程序池並把應用程序池的 .NET CLR 版本均置為 "無託管代碼"。
另外,把藍綠站點均綁定到 IP 地址 127.0.0.1 上:因為你不應允許用户繞過 Switch 站點直接訪問 Blue 站點和 Green 站點。
當然也可通過其它途徑達到此目的,比如用 Windows 防火牆。
5 Switch 站點創建與配置
這裏是本方案裏最關鍵的配置部分。
5.1 創建 Switch 站點
創建 Switch 站點,路徑指向 QAdminAppSwitch目錄,該目錄下將只有個 web.config 文件,內容為 URL 重寫規則。
讓 Switch 站點監聽 9080 端口(我本機 80 已被佔用,你按實際情況設置),也使用獨立的應用程序池並把其 .NET CLR 版本置為 "無託管代碼"。
將 Switch 站點綁定到對外使用的一個 IP 地址上(比如 192.168.0.116),如果是要通過域名訪問,綁定時再設置一下主機名為你的域名。
用户將通過你設置的 IP 或域名訪問應用系統。
5.2 書寫 URL 重寫規則
在 IIS 管理器 => Switch 站點 => URL 重寫 裏,可進行重寫規則的配置,配置將存到站點根目錄下的 web.config 文件裏。
以下是所需要的完整的重寫規則,你可直接拷貝到 Switch 站點的 web.config 裏使用。其中的藍綠站點端口你按實際情況修改。
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<system.webServer>
<rewrite>
<rules useOriginalURLEncoding="false">
<clear />
<!-- 藍站點 https 規則 -->
<rule name="RouteToBlueHttps" enabled="true" stopProcessing="true">
<match url="(.*)" />
<conditions>
<add input="{HTTPS}" pattern="on" ignoreCase="true" />
</conditions>
<serverVariables>
<set name="HTTP_X_FORWARDED_HOST" value="{HTTP_HOST}" />
<set name="HTTP_X_FORWARDED_PROTO" value="https" />
<set name="HTTP_X_FORWARDED_FOR" value="{REMOTE_ADDR}" />
</serverVariables>
<action type="Rewrite" url="http://127.0.0.1:5001{UNENCODED_URL}" appendQueryString="false" />
</rule>
<!-- 藍站點 http 規則 -->
<rule name="RouteToBlueHttp" enabled="true" stopProcessing="true">
<match url="(.*)" />
<conditions>
<add input="{HTTPS}" pattern="off" ignoreCase="true" />
</conditions>
<serverVariables>
<set name="HTTP_X_FORWARDED_HOST" value="{HTTP_HOST}" />
<set name="HTTP_X_FORWARDED_PROTO" value="http" />
<set name="HTTP_X_FORWARDED_FOR" value="{REMOTE_ADDR}" />
</serverVariables>
<action type="Rewrite" url="http://127.0.0.1:5001{UNENCODED_URL}" appendQueryString="false" />
</rule>
<!-- 綠站點 https 規則 -->
<rule name="RouteToGreenHttps" enabled="false" stopProcessing="true">
<match url="(.*)" />
<conditions>
<add input="{HTTPS}" pattern="on" ignoreCase="true" />
</conditions>
<serverVariables>
<set name="HTTP_X_FORWARDED_HOST" value="{HTTP_HOST}" />
<set name="HTTP_X_FORWARDED_PROTO" value="https" />
<set name="HTTP_X_FORWARDED_FOR" value="{REMOTE_ADDR}" />
</serverVariables>
<action type="Rewrite" url="http://127.0.0.1:5002{UNENCODED_URL}" appendQueryString="false" />
</rule>
<!-- 綠站點 http 規則 -->
<rule name="RouteToGreenHttp" enabled="false" stopProcessing="true">
<match url="(.*)" />
<conditions>
<add input="{HTTPS}" pattern="off" ignoreCase="true" />
</conditions>
<serverVariables>
<set name="HTTP_X_FORWARDED_HOST" value="{HTTP_HOST}" />
<set name="HTTP_X_FORWARDED_PROTO" value="http" />
<set name="HTTP_X_FORWARDED_FOR" value="{REMOTE_ADDR}" />
</serverVariables>
<action type="Rewrite" url="http://127.0.0.1:5002{UNENCODED_URL}" appendQueryString="false" />
</rule>
</rules>
</rewrite>
</system.webServer>
</configuration>
5.3 添加允許的服務器變量
規則裏寫的轉發相關服務器變量需要添加進來才能正常使用。
打開 IIS 管理器,選中 Switch 站點,在右側功能列表裏找到 "URL 重寫":

雙擊打開 "URL 重寫",然後點擊右側的 "查看服務器變量":

在該界面將 "HTTP_X_FORWARDED_HOST"、"HTTP_X_FORWARDED_PROTO"、"HTTP_X_FORWARDED_FOR" 添加進來:

5.4 規則的簡要解釋
-
最關鍵的要求是:用户在瀏覽器裏輸入的 URL,能夠完整的、不被做任何改動的轉給藍綠站點裏的 App
這個費了點周折,比如 URL:"/TestPage/aa%2Fbb",本意是請求 "/TestPage" 頁面,路由參數為 "aa/bb",因為該參數裏有斜槓,因此用編碼後的 "aa%2Fbb" 傳遞。但測試時發現轉給 App 的請求是 "/TestPage/aa/bb",造成 404。
最終在 這裏 找到了答案:使用{UNENCODED_URL}並設置useOriginalURLEncoding為false。 -
裏邊的服務器變量設置用來確保傳遞正確的 host、scheme 以及客户端 IP 地址給 App
比如 App 裏拿到的 host 將是 "192.168.0.116:9080" 而不是 "127.0.0.1:5001" 或 "127.0.0.1:5002"。
要與代碼配合實現,見後邊章節。 -
為何給藍綠分別設置了兩條規則
因為 URL 重寫沒法自適應 http/https,只有個{HTTPS}變量(值為 "on"/"off"),為了讓 http、https 均能正常訪問,只能各自寫兩條規則。
如果你的應用只需要 http/https 中的一種訪問,可以刪掉不需要的規則。 -
藍綠的切換
藍綠的切換就是對應規則的啓用與停用,哪個站點規則啓用(enabled="true"),就導向哪個站點。不能同時都啓用。
不能在 IIS 裏手動去啓用、禁用規則,這會造成訪問中斷,而是要通過代碼去實現,見後邊章節。
6 應用調整
應用也需要加入一些初始化代碼,以及做出一些相應的調整才能適應藍綠部署環境。
6.1 應用初始化中的兩項必要配置
- 配置數據保護(Data Protection)以共享密鑰
必須使用 AddDataProtection() 指定藍綠站點使用同一套密鑰存儲,不然會出現 Cookie 無法識別等問題。
比如用共享文件夾:
builder.Services.AddDataProtection()
.SetApplicationName("myApp")
// 應用上一級目錄的 myAppKeys 目錄下
.PersistKeysToFileSystem(new DirectoryInfo($"{AppContext.BaseDirectory}../myAppKeys"));
或存於 Redis:
var redis = ConnectionMultiplexer.Connect("<URI>");
builder.Services.AddDataProtection()
.SetApplicationName("myApp")
.PersistKeysToStackExchangeRedis(redis, "DataProtection-Keys");
- 配置轉發頭中間件(Forwarded Headers)
必須使用 UseForwardedHeaders() 配置轉發頭中間件以確保 App 夠獲取真實的 host、scheme 以及客户端 IP 地址,比如 App 裏拿到的 host 將是 "192.168.0.116:9080" 而不是 "127.0.0.1:5001" 或 "127.0.0.1:5002",拿到的 scheme 則是實際的 scheme(http/https)。
// 在 builder.Build() 後立即調用:
app.UseForwardedHeaders(new ForwardedHeadersOptions
{
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedHost
});
6.2 其它事項
- 識別自己所在環境
藍綠部署環境下,應用通常需要知道自己運行在 Blue 還是 Green 裏,比如日誌要增加ServerNode項,以便記錄在哪個登錄、在哪個執行的操作等等。
至於如何識別自己所在的環境,可直接根據應用自己所在的目錄名稱來判斷:
private static string _whoami()
{
string dirName = new DirectoryInfo(AppContext.BaseDirectory).Name;
bool isBlue = dirName.Contains("blue", StringComparison.OrdinalIgnoreCase);
bool isGreen = dirName.Contains("green", StringComparison.OrdinalIgnoreCase);
if (isBlue && isGreen)
return "Ambiguous";
if ((!isBlue) && (!isGreen))
return "Unknown";
return isBlue ? "Blue" : "Green";
});
- 配置共享
把配置文件放到一個共享目錄下,比如應用上一級目錄下的 configs 目錄。
就是説應用目錄下不要有發佈後需要更改的配置文件,這樣更新時就可放心地刪除舊版、拷貝新版了,不然一旦疏忽會造成混亂或異常。
以下代碼將使應用使用其上一級目錄下的 configs 目錄下的 appConfig.json 配置文件,供你參考:
string appConfigFile = $"{AppContext.BaseDirectory}../configs/appConfig.json";
string appConfigFileEnv = $"{AppContext.BaseDirectory}../configs/appConfig.{builder.Environment.EnvironmentName}.json";
builder.Configuration.AddJsonFile(appConfigFile, false, true);
builder.Configuration.AddJsonFile(appConfigFileEnv, true, true);
如果藍綠下的 App 需要不同的配置:在共享配置文件裏書寫兩套配置,App 裏則用一套代碼就可讓藍綠各自讀取自己的:
配置:
{
"MyApp_Blue": {
"Foo": "abc",
},
"MyApp_Green": {
"Foo": "def",
},
}
讀取:
// 參見前邊的 _whoami()
string foo= builder.Configuration[$"MyApp_{_whoami()}:Foo"];
-
分佈式緩存
如果有需要共享的緩存,則需要改用分佈式緩存。比如用到了 Session。 -
文件上傳
若有附件上傳,則同樣要使用同一個共享目錄。 -
後台任務/定時任務
若有後台任務或定時任務,藍綠將都在執行。
如果任務允許藍綠同時運行,或允許一前一後運行,或者不允許同時運行但可中斷,就沒什麼問題。否則需要將任務獨立出來,並獨立運行(比如用 Windows Service)。 -
向後兼容
應用需要考慮向後兼容性。
如果新版本使用了與舊版不兼容的會話結構、加密格式、字段結構等等,就無法進行平滑切換,因此需要考慮向後的兼容性,比如新增的字段要確保允許 NULL 或設有默認值等。
如果確實無法兼容,就只能短時中斷訪問了,根據實際情況可採取提前通知、低峯操作等方式升級。
7 藍綠如何切換
藍綠的切換過程實際上就是啓用/禁用 Switch 站點裏的對應規則。
但是不能在 IIS 裏手動去啓用、禁用規則,這會造成訪問中斷,而是通過用腳本或代碼修改 Switch 站點裏的 web.config 文件來進行切換:修改對應規則的 enabled 為 true/false,比如用 PowerShell 腳本。
我是在應用裏設計了一個只有超級管理員用户訪問的頁面,在其中進行切換操作。
以下是用來獲取當前啓用的環境以及進行藍綠切換的 C# 方法,你可直接使用。
/// <summary>
/// 獲取當前 web.config 裏啓用的環境。
/// </summary>
/// <param name="webConfigPath">Switch 站點的 web.config 文件完整路徑。</param>
/// <returns></returns>
private static string _getCurrentEnvironmentInConfiguration(string webConfigPath)
{
if (!System.IO.File.Exists(webConfigPath))
throw new FileNotFoundException("未找到 Switch 站點的 web.config 文件。", webConfigPath);
XDocument doc = XDocument.Load(webConfigPath);
// 所有 Blue 規則節點
var blueRules = doc
.Descendants("rule")
.Where(r => ((string)r.Attribute("name")).StartsWith("RouteToBlue", StringComparison.OrdinalIgnoreCase))
.ToList();
// 所有 Green 規則節點
var greenRules = doc
.Descendants("rule")
.Where(r => ((string)r.Attribute("name")).StartsWith("RouteToGreen", StringComparison.OrdinalIgnoreCase))
.ToList();
if (blueRules.Count == 0 || greenRules.Count == 0)
throw new InvalidOperationException("未找到 RouteToBlue 或 RouteToGreen 規則,請檢查 web.config。");
bool blueEnabled = blueRules.All(r => (string)r.Attribute("enabled") != "false");
bool greenEnabled = greenRules.All(r => (string)r.Attribute("enabled") != "false");
if (blueEnabled && greenEnabled)
return "All";
if ((!blueEnabled) && (!greenEnabled))
return "NoneOrAmbiguous";
return blueEnabled ? "Blue" : "Green";
}
/// <summary>
/// 切換藍綠環境。
/// </summary>
/// <param name="webConfigPath">Switch 站點的 web.config 文件完整路徑。</param>
/// <returns>返回已啓用的環境。</returns>
private static string _toggleEnvironment(string webConfigPath)
{
if (!System.IO.File.Exists(webConfigPath))
throw new FileNotFoundException("未找到 Switch 站點的 web.config 文件。", webConfigPath);
XDocument doc = XDocument.Load(webConfigPath);
// 所有 Blue 規則節點
var blueRules = doc
.Descendants("rule")
.Where(r => ((string)r.Attribute("name")).StartsWith("RouteToBlue", StringComparison.OrdinalIgnoreCase))
.ToList();
// 所有 Green 規則節點
var greenRules = doc
.Descendants("rule")
.Where(r => ((string)r.Attribute("name")).StartsWith("RouteToGreen", StringComparison.OrdinalIgnoreCase))
.ToList();
if (blueRules.Count == 0 || greenRules.Count == 0)
throw new InvalidOperationException("未找到 RouteToBlue 或 RouteToGreen 規則,請檢查 web.config。");
bool blueEnabled = blueRules.All(r => (string)r.Attribute("enabled") != "false");
bool greenEnabled = greenRules.All(r => (string)r.Attribute("enabled") != "false");
string targetEnv;
if (blueEnabled && !greenEnabled)
{
// 當前是藍 → 切換到綠
foreach (var r in blueRules)
r.SetAttributeValue("enabled", "false");
foreach (var r in greenRules)
r.SetAttributeValue("enabled", "true");
targetEnv = "Green";
}
else if (greenEnabled && !blueEnabled)
{
// 當前是綠 → 切換到藍
foreach (var r in blueRules)
r.SetAttributeValue("enabled", "true");
foreach (var r in greenRules)
r.SetAttributeValue("enabled", "false");
targetEnv = "Blue";
}
else
{
// 若都已啓用、都已停用或狀態混雜,則切換到藍
foreach (var r in blueRules)
r.SetAttributeValue("enabled", "true");
foreach (var r in greenRules)
r.SetAttributeValue("enabled", "false");
targetEnv = "Blue";
}
// UTF-8 編碼保存,並確保不寫入 BOM,以防止 IIS 讀取出錯
using (var writer = new StreamWriter(webConfigPath, false, new System.Text.UTF8Encoding(false)))
{
doc.Save(writer);
}
return targetEnv;
}
8 切換測試
用 k6 分別對 Windows Server 2012 R2 + IIS8.5 和 Win11 + IIS10 下的藍綠部署進行了切換測試,尚未出現訪問中斷的情況。
作者:木南W
出處:https://www.cnblogs.com/munanwang/p/19234857
轉載請註明作者並在頁面明顯位置給出原文鏈接。