博客 / 詳情

返回

實踐筆記:IIS + URL Rewrite + ARR 實現 ASP.NET Core 藍綠部署

最近用户有個需求:更新 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 藍綠部署動畫

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

Folders

即:在 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"

ARR1

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

ARR2

然後按照下圖配置:

ARR3

為何取消選中 "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 重寫"

URLRewrite

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

Role1

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

Role2

5.4 規則的簡要解釋

  • 最關鍵的要求是:用户在瀏覽器裏輸入的 URL,能夠完整的、不被做任何改動的轉給藍綠站點裏的 App
    這個費了點周折,比如 URL:"/TestPage/aa%2Fbb",本意是請求 "/TestPage" 頁面,路由參數為 "aa/bb",因為該參數裏有斜槓,因此用編碼後的 "aa%2Fbb" 傳遞。但測試時發現轉給 App 的請求是 "/TestPage/aa/bb",造成 404。
    最終在 這裏 找到了答案:使用 {UNENCODED_URL} 並設置 useOriginalURLEncodingfalse

  • 裏邊的服務器變量設置用來確保傳遞正確的 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 文件來進行切換:修改對應規則的 enabledtrue/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

轉載請註明作者並在頁面明顯位置給出原文鏈接。

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.