1. 程式人生 > >asp.net core 從單機到叢集

asp.net core 從單機到叢集

asp.net core 從單機到叢集

Intro

這篇文章主要以我的活動室預約的專案作為示例,看一下一個 asp.net core 應用從單機應用到分散式應用需要做什麼。

示例專案

活動室預約提供了兩個版本,叢集版
和 單機版

單機版方便部署,不依賴其他環境,資料庫使用的是 sqlite,詳細部署文件可以參考:https://github.com/WeihanLi/ActivityReservation/blob/dev/docs/deploy/standalone.md

叢集版,目前依賴的元件有 mysql(資料庫)/redis(快取)/elasticsearch(日誌)

日誌

日誌原來是輸出到檔案中的,單機部署沒有什麼問題,可以直接 ssh 到機器上檢視檔案內容,但是如果部署到叢集上,日誌再輸出到檔案的話,排查起來可就有點麻煩了,日誌是分散在多臺機器上,只看某一臺機器上的日誌可能並不能解決問題。

基於日誌這個痛點讓我把日誌遷移到 elasticsearch 上,日誌統一輸出到 es,並通過 kibana 來搜尋/分析日誌。

日誌元件一直用的 log4net,日誌輸出到 es ,自己寫了一個 es 的 Appender, 但是後來越來越覺得 log4net使用起來不夠靈活,後來日誌元件換成了 serilog,使用 serilog 就可以方便的擴充套件,增加日誌要記錄的資訊,關於自定義 serilog enricher 可以參考 Serilog 自定義 Enricher 來增加記錄的資訊

使用 es 來儲存日誌還有一個好處,就是搜尋日誌非常的快,而且藉助 kibana 可以很方便的進行統計分析

拿上篇文章的圖來借用一下,下面是 kibana 基於日誌的 RequestIP 來繪製的前十個訪問最多的 IP 地址

快取

單機部署為了不增加系統複雜度,不引入外部依賴,單機版使用的是 MemoryCache
叢集部署,就需要引入分散式快取,我選擇的是 redis,redis 元件是基於 StackExchange.Redis 的,自己在其基礎上封裝了一些功能。

在我的這個示例應用中 redis 不僅僅做快取,我還用 redis 的 hash 實現了一個類似於 asp.net 裡 Application 的服務,還有 redis 的釋出訂閱來實現一個 eventBus 來非同步處理公告的瀏覽記錄。

單機環境下,我們用 lock 或者用訊號量來實現資源在某一段時間內只能被一個請求拿到

多臺機器環境下,我們需要一個分散式鎖,上面引入了 redis,就用 redis 來實現一個分散式鎖,分散式鎖詳細實現可以參考:
RedLock

使用方式如下:

using (var redisLock = RedisManager.GetRedLockClient($"reservation:{reservation.ReservationPlaceId:N}:{reservation.ReservationForDate:yyyyMMdd}"))
{
    if (redisLock.TryLock())
    {
        var reservationForDate = reservation.ReservationForDate;
        if (!IsReservationForDateAvailable(reservationForDate, isAdmin, out msg))
        {
            return false;
        }

        // ...
        return true;
    }
    else
    {
        msg = "系統繁忙,請稍後重試!";
        return false;
    }
}

DataProtection

微軟在 .net core 下引入了 DataProtection 來保護網站的資料,你也可以用它做一些資料保護,之前做了一個簡單資料保護擴充套件,通過 Filter 來自動實現資料的加密/解密,詳細資訊可以參考 asp.net core 引數保護

預設 DataProtection 的key 是儲存到檔案的,可能你也注意到過在 asp.net core 應用啟動的時候預設會有一條日誌資訊如下:

多臺機器同時部署的話,key 基本上就是不一樣的,這樣資料就不會被認為是安全的。

舉個栗子,我的應用有一個後臺使用 cookie 認證,cookie 會使用 DataProtection 的 key 進行加密,使用預設的 DataProtection 時,多臺機器上(實際是k8s的多個pod) 的key 是不一樣的,這就導致我在後臺登入了之後,進入後臺之後重新整理一下可能就又跳轉到登入介面,這是因為生成的 cookie ,對於一個服務來說是有效的,但是對於其他服務來說是無效的(key 不同,沒有辦法解密成功,認證失敗),所以叢集部署的時候,DataProection 是必須要設定的,放在一個統一的地方管理,我們上面已經引入了 redis,所以就把 DataProtection 的 key 放在 redis 中去儲存(redis 服務可以做高可用,即使 redis 服務掛了也會重新生成一個 key,不會有什麼影響)

使用到的包 Microsoft.AspNetCore.DataProtection.StackExchangeRedis,配置方式:

// DataProtection persist in redis
services.AddDataProtection()
    .SetApplicationName(ApplicationHelper.ApplicationName)
    .PersistKeysToStackExchangeRedis(() => DependencyResolver.Current.ResolveService<IConnectionMultiplexer>().GetDatabase(5), "DataProtection-Keys")
    ;

獲取使用者IP

叢集部署的時候,會有閘道器/反向代理去轉發請求,這時候直接通過 HttpContext.Connection.RemoteIpAddress 獲取到的 ip 地址就會是閘道器/反向代理的地址,並不是實際使用者的地址,一般的反向代理軟體會將真實的使用者IP放在 X-Forwarded-For 請求頭中,轉發到下游真正的伺服器地址,你可以從請求中直接獲取 X-Forwarded-For 請求頭的值,也可以使用微軟提供的 ForwardedHeaders 中介軟體,配置方式:

public void ConfigureServices(IServiceCollection services)
{
    // ...
    services.Configure<ForwardedHeadersOptions>(options =>
    {
        options.KnownNetworks.Clear();
        options.KnownProxies.Clear();
        options.ForwardLimit = null;
        options.ForwardedHeaders = Microsoft.AspNetCore.HttpOverrides.ForwardedHeaders.All;
    });
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{    
    app.UseForwardedHeaders();
    // ...
}

具體引數配置可以參考文件:https://docs.microsoft.com/en-us/aspnet/core/host-and-deploy/proxy-load-balancer?view=aspnetcore-2.2

Memo

其他還有一些上面並未提到,

比較常用的如 Session,如果要上叢集的話,也應該有相應的分散式 session,這個應用沒有用到 session,所以上面沒有提(之前用極驗驗證碼的時候有用,後來換成騰訊的驗證碼服務之後去掉了session)

檔案上傳,如果是存在本地的話,也不太合適,可能需要存在一個集中的檔案伺服器或者雲端儲存如 Azure Blob。。(網站裡的公告模組的圖片上傳還沒改,,,打算基於 github 或者 開源中國實現一個 storage )

其他暫時沒想到了,想到了再補充吧。

Reference