负载均衡,很直白的说,就是:某一个服务,由多台服务器均摊后由多台机器作为服务器对该服务做出响应。其中最为困扰就是Session共享了,处理起来特麻烦。尤其让人吐血的是,透明替换啊,简直颠覆了我对程序认识。通过一番痛苦挣扎,终于解决!于是乎,就来简单记录下吧。一共三步:使用Session->Redis存储Session->统一不同机器之间的MachineKey。不同于缓存的是,Session记住需要保证是当前会话的一个状态,而缓存就相对比较简单了,如我需要一个key值,那我直接去缓存服务器读取就ok,对吧。假设这样一个场景:在成功使用负载均衡的情况下,用户的第一个操作,是服务器A响应,在未登录的情况下进行登录并成功,可就在登录成功后,由于负载均衡,接下来的操作,由服务器B响应,可是服务器B的Session与服务器A的Session是独立在两台物理机的。这时候用户在已经登录过的情况下,还得重新登录一下来保持状态,是不是很烦,这种糟糕的情况,是不是程序中严禁允许的,对吧。这是不是比单纯的缓存要复杂的多,原理图大致如下:
在上述描述的情景中,如果我们能实现Session共享,即:Session被透明替换,缓存的数据,被存储到第三方库,在使用Session的时候,其实调用的就是用来存储Session的Redis,这样的话问题不就解决了,对吧。那么我们首先就来使用Session吧。
NuGet : Microsoft.AspNetCore.Session
在Startup.cs的ConfigureServices方法中进行注入,并在Configure方法中,添加使用Session的标识。代码如下:
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration {
get; }
/// <summary>
/// 此方法由运行时调用,使用此方法可将服务添加到容器中
/// </summary>
public void ConfigureServices(IServiceCollection services)
{
//注入Session
services.AddSession(options =>
{
options.IdleTimeout = TimeSpan.FromMinutes(30);
});
//注入自启动
services.AddControllersWithViews().AddRazorRuntimeCompilation();
}
/// <summary>
/// 此方法由运行时调用,使用此方法配置HTTP请求管道
/// </summary>
/// <param name="app"></param>
/// <param name="env"></param>
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseStaticFiles();
app.UseRouting();
//session
app.UseSession();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name:"WX",
pattern:"{area:exists}/{controller=WeChat}/{action=GetValid}/{id?}");
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
}
这时在HomeController.cs文件中就可以使用Session了。示例如下:
public class HomeController : Controller
{
public IActionResult Index()
{
HttpContext.Session.SetString("key","key")
var json=new
{
key=HttpContext.Session.GetString("key")
};
return Json(json);
}
}
使用Redis透明存储Session,至于如何搭建Redis参见我的上篇文章Redis汇总。NuGet :
1、Microsoft.Extensions.Caching.StackExchangeRedis
2、Microsoft.AspNetCore.DataProtection.StackExchangeRedis
再去Startup.cs的ConfigureServices方法中注入一下Redis即可实现。这里我是用的Redis集群,代码如下:
public void ConfigureServices(IServiceCollection services)
{
//注入Session
services.AddSession(options =>
{
options.IdleTimeout = TimeSpan.FromMinutes(30);
});
//使用Redis作为缓存,存储Session等
var redis_Con = "42.193.5.237:6379,password=123@456,defaultdatabase=0,8.135.26.156:6379,password=123@456,defaultdatabase=0,154.8.183.144:6379,password=123@456,defaultdatabase=0,39.107.65.223:6379,password=123@456,defaultdatabase=0,183.60.104.198:6379,password=123@456,defaultdatabase=0,124.71.148.52:6379,password=123@456,defaultdatabase=0";
services.AddStackExchangeRedisCache(option =>
{
option.Configuration = redis_Con;
});
//注入自启动
services.AddControllersWithViews().AddRazorRuntimeCompilation();
}
这时重新启动项目,运行时我们发现Redis已经存储了我们想要的数据。这个过程是透明的,所以不好理解,就算看到了数据,直观上也感受不到有什么直接联系,但是此时的Session确确实实已经在Redis进行了数据加密存储。
虽然上述操作已经完整实现了Redis存储Sesssion,实质上也就是说现在Redis已经透明替换掉Session了,记得我在Java的SpringBoot中,实现到这一步,已经可以实现Session共享了,但在.NET Core还远未实现啊,这到底是怎么回事。通过查阅资料,发现是不同机器之间的 MachineKey导致的,于是通过一番痛苦的折腾,终于发现了一个第三方包来解决这个问题。NuGet : Microsoft.AspNetCore.DataProtection.StackExchangeRedis
继续在Startup.cs的ConfigureServices方法中添加如下代码,配置不同机器之间使用MachineKey的统一标识即可:
//为不同机器的作MachineKey的统一标识
var redis = ConnectionMultiplexer.Connect(redis_Con);
services.AddDataProtection()
.SetApplicationName("session_application_name")
.PersistKeysToStackExchangeRedis(redis, "SNK-DataProtection-Keys");
再来改造下HomeController.cs的Index方法,注意这里的Machine是一个类就两个属性ip,key,自行创建这个类不难吧。ip分别用于部署在两台服务器的标识,key使用从Session读取来验证是Session共享的内容。39.107.65.223服务器代码:
public IActionResult Index()
{
var key = HttpContext.Session.Get("key");
Machine m = new Machine()
{
ip = "39.107.65.223",
key = "key"
};
if (key == null)
{
HttpContext.Session.Set("key", Encoding.UTF8.GetBytes(JsonConvert.SerializeObject(m)));
}
byte[] by = HttpContext.Session.Get("key");
string str = Encoding.UTF8.GetString(by);
return Json(JsonConvert.DeserializeObject<Machine>(str));
}
打包发布完后,另一台服务器154.8.183.144代码如下,其余部分完全一样:
public IActionResult Index()
{
var key = HttpContext.Session.GetString("key");
if (key == null)
{
HttpContext.Session.SetString("key", "154.8.183.144设置的key");
}
Machine m = new Machine()
{
ip = "154.8.183.144",
key = HttpContext.Session.GetString("key")
};
byte[] by = HttpContext.Session.Get("key");
string str = Encoding.UTF8.GetString(by);
return Json(JsonConvert.DeserializeObject<Machine>(str));
}
接下来在第三台服务器42.193.5.237反下载nginx,官方下载连接,作为负载均衡反向代理服务器。
打开nginx.conf添加如下内容来配置负载均衡:
upstream servers1{
server 39.107.65.223 weight=1;
server 154.8.183.144 weight=1;
}
server {
listen 81;
server_name _;
location / {
#反向代理地址
proxy_pass http://servers1;
#设置主机头和客户端真实地址,以便服务器获取客户端真实IP
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
响应结果:ip是用来标识进行响应的服务器,可以直观的感受到,在同一个会话中,分别由不同的服务器进行响应,但使用的Session 数据却是共享了。到此,大功告成!