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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章