asp.net core 從單機到集群
- 2019 年 10 月 3 日
- 筆記
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
- https://github.com/WeihanLi/ActivityReservation
- https://docs.microsoft.com/en-us/aspnet/core/host-and-deploy/proxy-load-balancer?view=aspnetcore-2.2
- https://en.wikipedia.org/wiki/X-Forwarded-For