前言:雖說公司app后端使用的是.net core+Redis+docker+k8s部署的,但是微信公眾號后端使用的是IIS部署的,雖說公眾號並發量不大,但領導還是使用了負載均衡,所以在介紹docker+k8s實現分布式Session共享之前,就先介紹一下IIS+nginx實現Session共享的方案,兩者其實區別不大,所以這篇着重介紹方案,下篇介紹測試的區別以及填坑的方式。
1、環境准備
操作系統:Windows10
IIS:需要安裝模塊
VS2019、本地Redis數據庫、ngnix(windows版)
2、Session共享的簡易說明
下圖簡要說明了負載均衡通過輪詢方式,將同一個客戶端請求發送到不同的站點下,操作的Session應該是同一個。
3、添加測試項目
雖然個人認為本來WebApi中使用Session本身就是一種不合理的設計,但這是舊項目遷移需要保留的歷史邏輯,所以只能硬着頭皮尋找對應的解決方案了。
在VS2019中添加一個.net core 的WebApi項目,使用Session的話需要添加以下配置。
Startup.cs類中,ConfigureServices方法添加services.AddSession(); Configure方法中添加app.UseSession(); 注意要放到UseMVC方法前面。
測試代碼如下,添加testController類,在Get1方法中設置Session,記錄當前時間,Get2方法中讀取Session
[Route("[action]")] [ApiController] public class testController : ControllerBase { // GET: api/test [HttpGet] public IEnumerable<string> Get() { return new string[] { "value1", "value2", HttpContext.Connection.LocalIpAddress.ToString(), HttpContext.Connection.LocalPort.ToString()}; } // GET: api/test/5 [HttpGet] public string Get1(int temp1) { if (string.IsNullOrEmpty(HttpContext.Session.GetString("qqq"))) { HttpContext.Session.SetString("qqq", DateTime.Now.ToString()); } return HttpContext.Connection.LocalIpAddress.ToString() + "|" + HttpContext.Connection.LocalPort.ToString(); } [HttpGet] public string Get2(int temp1, int temp2) { return HttpContext.Connection.LocalIpAddress.ToString() + "|" + HttpContext.Connection.LocalPort.ToString() + "|" + HttpContext.Session.GetString("qqq"); } }
4、發布.net core項目到IIS
(1)右鍵項目點擊發布
(2)記錄下發布路徑,並在IIS中新增兩個站點,指向該路徑,並設置不同的端口號
記得把應用程序池中改為無托管代碼
5、nginx配置負載均衡
下載地址:http://nginx.org/
配置方式:
(1)找到nginx的安裝路徑,打開nginx.conf文件
(2)添加upstream配置,配置用於負載均衡輪詢的站點,即上一步驟中添加的兩個站點
(3)配置location節點,注意proxy_pass與upstream中配置的名稱保持一致。
(4)啟動ngnix,用cmd命令指定nginx的安裝目錄,然后start nginx
6、在沒有做Session共享方案的情況下進行測試
瀏覽器分別輸入http://localhost:7665/Get1與http://localhost:7665/Get2,由於ip是一樣的,所以沒有參考必要,不停刷新http://localhost:7665/Get1,最后看到的端口號在7666與7667之間不停的來回切換,說明nginx的輪詢是成功的。當然這里只是為了實現session共享做的負載均衡,所以把負載均衡放在了同一台服務器上進行配置,感興趣的同學可以使用不同服務器配置負載均衡,並用壓力測試工具測試站點在配置負載均衡的吞吐能力,后面有機會我可以單獨介紹這部分內容。
測試結果:
1、過程: 請求http://localhost:7665/Get1,請求分發到站點7667,設置了Session;
請求http://localhost:7665/Get2,請求分發到站點7666,讀取不到該Session;
再次請求Get2,請求分發到站點7667,可以讀取到Session。
結論:說明負載均衡的兩個站點之間不會讀取同一個Session,也就是說Session不會共享。
2、 過程: 再次請求Get1,請求分發到站點7666;
再次請求Get2,請求分發到站點7667,讀取不到該Session;
再次請求Get2,請求分發到站點7666,可以讀取到Session,且session值被刷新。
結論:因為nginx每次都將請求分發到另外一站點,且session沒有共享,所以string.IsNullOrEmpty(HttpContext.Session.GetString("qqq"))總是true,然后session都會被刷新,另一個站點的session就丟失了(這里的丟失應該是刷新session后產生了新的cookie值,導致原來的session無法讀取,感興趣的同學還可以用Fiddler跟蹤記錄下Get1產生cookie值,然后在Get2請求時帶上cookie進行驗證)。
7、使用Redis將Session存放在Redis服務器
(1)在Startup.cs文件中,ConfigureServices方法加入以下代碼
services.Configure<CookiePolicyOptions>(options => { // This lambda determines whether user consent for non-essential cookies is needed for a given request. options.CheckConsentNeeded = context => false; //這里要改為false,默認是true,true的時候session無效 options.MinimumSameSitePolicy = Microsoft.AspNetCore.Http.SameSiteMode.None; }); services.AddDataProtection(configure => { configure.ApplicationDiscriminator = "wxweb"; }) .SetApplicationName("wxweb") .AddKeyManagementOptions(options => { //配置自定義XmlRepository options.XmlRepository = new SessionShare(); }); #region 使用Redis保存Session // 這里取連接字符串 services.AddDistributedRedisCache(option => { //redis 連接字符串 option.Configuration = Configuration.GetValue<string>("RedisConnectionStrings"); //redis 實例名 option.InstanceName = "Wx_Session"; }); //添加session 設置過期時長分鍾 //var sessionOutTime = con.ConnectionConfig.ConnectionRedis.SessionTimeOut; services.AddSession(options => { options.IdleTimeout = TimeSpan.FromSeconds(Convert.ToDouble(8 * 60 * 60)); //session活期時間 options.Cookie.HttpOnly = true;//設為httponly }); #endregion
簡要說明:
services.Configure<CookiePolicyOptions>是為了可以使用cookie
SetApplicationName("wxweb")是為了保證不同站點下的應用程序名稱是一致的。
options.XmlRepository = new SessionShare();是為了保證不同站點下應用程序使用的machinekey是一樣的,詳情見https://www.cnblogs.com/newP/p/6518918.html
AddDistributedRedisCache是一個官方的拓展組件,用戶將session保存在redis中。
RedisConnectionStrings是Redis連接字符串
(2)SessionShare的實現如下
public class SessionShare : IXmlRepository { private readonly string keyContent = @"自己的machinekey"; public virtual IReadOnlyCollection<XElement> GetAllElements() { return GetAllElementsCore().ToList().AsReadOnly(); } private IEnumerable<XElement> GetAllElementsCore() { yield return XElement.Parse(keyContent); } public virtual void StoreElement(XElement element, string friendlyName) { if (element == null) { throw new ArgumentNullException(nameof(element)); } StoreElementCore(element, friendlyName); } private void StoreElementCore(XElement element, string filename) { } }
(3)再次進行發布
8、添加Session共享方案以后進行測試
測試結果:無論Get1刷新多少次,Get2都能拿到Session值,且Session沒有被刷新(當前時間沒有變化),即Session共享成功。
總結:以前總是看別人的文章了解Session共享,這次自己從配置負載均衡到解決Session共享從頭到尾走了一遍,所以記錄了下來。下篇文章我會介紹AddDistributedRedisCached在並發量較高時timeout的解決方案。
參考文章(同時感謝這些大佬的文章提供的幫助)