详解Session分布式共享(.NET CORE版)

一、前言&回顾


       在上篇文章Session分布式共享 = Session + Redis + Nginx中,好多同学留言问了我好多问题,其中印象深刻的有:nginx挂了怎么办?采用Redis的Session方案与微软Session方案相比,有什么优势呢?Cookie也可以取代Session的,采用Redis的Session方案优势在哪里?Nginx的iphash方式到底是什么?MachineKey有啥用?Net Core怎样实现?

       那会儿看到大家的提问,我的回答也只是从应用层面回答,基本上的回答可以总结为:“别人这么做了,解决了这个问题,我用这个方法也解决了这个问题,原理请看链接。”很惭愧的说,那时的我并没有完全理解他真正的优势在哪里,只是凭着直觉和经验知道这样做比较好,知道当一部分东西不可控时候,将其解耦、可视化、集群就可以让一个系统更加健壮,但没有一个理论支撑。经过最近一段时间的查阅资料和阅读书籍,对此有了深刻理解,本文将从网站架构的可用性角度对这种Session共享进行分析和讲解,并用.net core再次实现这种架构模式。(Session分布式共享的net core版,因为工作没有机会应用到生产环境,过往经验就更别提了,所以只是研究性的,请大家注意,但园子里早有大牛写出了相关文章,本文结束会将相关文章贴出)

二、网站可用性--Session管理


     可用性是网站架构中非常重要的一环,什么是可用性,说的简单些,就是用户随时随地打开这个网站,这个网站都能打开,并且里面的功能都能用。如果可用性不高会出现什么情况?大家想象一下春节在12306抢票的情景,网站各种崩溃,大家保准会想:要是有别的方式能买到票,我才不用12306这个破网站呢。这个例子有点极端,因为业务场景比较极端,当然,这种现象也不光是网站可用性这一环出了问题。但是一个网站三天两头打不开,要么是点开了里面的页面到处是报错页面和操作无反应,你还会用这个网站么?我相信我们在浏览网站时候,只要不像12306这种垄断业务的网站,出现不可用的情况,我们一定会离开寻找其他类似的网站。

     Session管理是网站可用性的内容之一,大家都知道Http是无状态请求,即无法追踪上次Http请求的相关信息,但是业务中大量需要将Http变为有状态请求,Session就随之产生了,可是在分布式网站设计中,无状态请求才能实现网站的横向拓展(增减应用服务器),因此又与Session相矛盾,因为Session信息如果存储在网站应用服务器的缓存中,加台服务器就不能用了,因此将Session解耦是解决此问题的关键,下面介绍网站常见的Session管理手段。

1、Session复制

     Session复制是最早企业应用系统使用较多的一种服务集群Session管理机制,开启Session复制功能,即是在集群中的几台服务器之间同步Session对象,Java中好像JBoss有这个功能,.Net暂不知道。

     优势:Session信息读取快,实现简单。

     缺点:集群规模较大时,服务器之间Session复制会占用服务器资源和网络资源,最后系统会不堪重负。

image

2、Session绑定 

     Session绑定的方式,一般软/硬均衡负载服务器都会提供此功能,例如:上篇文章Nginx的IPhash方式,均衡负载服务器利用Hash算法将同一IP分配到同一台服务器上,即Session绑定在某台特定服务器上,保证Session总能在这台服务器上获得,又称作为会话黏滞。

     缺点:如果某台服务器宕机,那么这台服务器上面的Session也就不存在了,用户请求切换到其他服务器上因为没有Session而出错。

image

 

3、利用Cookie记录Session

     通过Cookie记录Session信息是大部分网站采用的方法,这种方式只要Cookie不滥用,也是非常好非常成熟的方案。Cookie记录Session就是把一些状态信息放到了客户端,每次请求都要传输到服务器。

     优势:这种方法简单易实现,可用性高,支持服务器横向拓展,方案成熟

     缺点:安全性问题,Cookie有大小限制,而且每次请求传输Cookie会影响性能

image

 

4、Session服务器

     Session服务器的方式管理Session,是一种非常好的解决方案,因为Session是为了业务需要Http状态而产生,而分布式网站设计中提倡Http无状态,为了满足这一设计,Session服务器是将有状态的Session信息与无状态的应用服务器相分离,再针对不同服务器的不同特性进行设计。例如:我们将Session信息存入到Redis中,那么Redis的集群配置、稳定性设置都有很多好的解决方案,如果将Session存入到Memcache,那么Memcache的集群配置、稳定性设置也会有很多成熟案例。这样我们就将一些问题简单化,如果我们单独应用.Net的Session,我们需要了解更多.Net深层次的东西并加以改造来保证其可用和稳定,越深层的东西越需要时间和阅历,而如果将Session存储介质转移到Redis中,Redis集群方案、管理工具都非常成熟,只需要配置配置就解决了Session的问题,何乐而不为呢。

     优势:可用性高、安全性高、伸缩性好、性能高、信息大小无限制

image

 

三、.Net Core+Redis+Nginx实现Session分布式共享


1、前期准备&环境

      (1)Vs2017    (2).Net Core 1.1  (3) Win 7  (4)ubuntu 16.04

2、.Net Core简介

       随着互联网的发展,在当今中国市场(外国不大清楚)开源、跨平台是衡量一门语言、技术好坏的重要指标之一,微软为了推动.Net开源及跨平台,.Net Core随之诞生。

       详见大牛的文章:.NET Core与.NET Framework、Mono之间的关系

       下面说说.Net Core给我的初步的感受:

         1).Net Core并没有颠覆之前C#语法

          通俗讲就是之前说中国话(C#),现在还是说中国话,只是说话的环境变了。

         2).Net Core因为刚起步,API变了或者少了很多

          通俗讲就是说话环境变了,而且里面有好多你没见过的东西,你不知道用什么官方词语来描述,因为官方正在找相关词来描述这些新东西。

         3)脱离IIS,跨平台

          通俗讲就是微软老妈为了不让我们到了新环境饿着,怕离开现在这个环境(Windows+IIS)之后不知道怎么生存。于是,教会了我们语言(C#),给了我们挣钱的工具(.Net Core+Kestrel),说了一句“去吧孩子,自己奋斗去吧,稍等,别忘了把这张Visa卡带上(.Net Core SDK),我会定期给你打钱的。”

         4)NuGet越来越重要

          NuGet经过几年的发展,越来越成熟,.Net Core开源组件获取的主要方法,通过NuGet可以下载各种中间件和组件,而且方便快捷(除了有时候断网,但是可以使用国内镜像),NuGet就像微软老妈给咱们的一个通讯录,并告诉咱们,如果你在某些方面需要帮助的时候,可以通过NuGet找到你的七大姑八大姨来帮忙。

3、拓扑图

image

          根据之前文章中成功的经验,简单改造一下,中间一个Windows系统和一个Ubuntu系统承载着.Net Core程序,有人会问Windows那个咋不来个IIS啊,我要说的是.Net Core实行走出去的原则,基本脱离IIS,如果IIS上面想部署.Net Core程序的话,需要安装同样的应用程序,并且站点配置的应用程序池也要变成“无托管代码”。

4、开发.Net Core程序使用Session

4-1、创建一个Web程序

          用Vs2017创建一个.Net Core的Web应用程序,且这个应用程序不包含身份验证信息

image

image

         创建完如下

image

4-2、.Net Core调用Session

        .Net Core使用Session,需要引用相关Session的NuGet包,网上一查,发现.Net Core的官方Session组件类似一个中间件,并且官方支持Redis。

         注意:.Net Core的Mvc不能直接使用Session,如果你在程序里面写了个HttpContext.Session就会出现如下错误:Session has not been configured for this application or request.

image

4-2-1、Microsoft.AspNetCore.Session

         .Net Core使用Session必须安装Microsoft.AspNetCore.Session,他的NuGet包安装如下图:

image

4-2-2、修改Startup.cs让Session可用

          在相应位置加入高亮代码services.AddSession(); app.UseSession();

public void ConfigureServices(IServiceCollection services)
 {
     // Add framework services.
     services.AddMvc();
     services.AddSession();
 }
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
     loggerFactory.AddConsole(Configuration.GetSection("Logging"));
     loggerFactory.AddDebug();

     if (env.IsDevelopment())
     {
          app.UseDeveloperExceptionPage();
          app.UseBrowserLink();
     }
     else
     {
          app.UseExceptionHandler("/Home/Error");
     }

     app.UseStaticFiles();
     app.UseSession();
     app.UseMvc(routes =>
     {
          routes.MapRoute(
          name: "default",
          template: "{controller=Home}/{action=Index}/{id?}");
     });
}

4-2-3、Session写入和读取

          Session的读取方式,与.Net有所不同,写法如下,并且Session的HttpContext.Session.SetString或者HttpContext.Session.Set方法分别支持字符串和Byte数组,所以复杂实体需要转化成Json存入Session中。

【Session 写入方法】

HttpContext.Session.SetString("key", "strValue");

【Session 读取方法】

HttpContext.Session.GetString("key")

5、Session存储介质更换为Redis

5-1、首先配置Redis

详细配置方式见:Session分布式共享 = Session + Redis + Nginx

redis-server redis.windows.conf

详细配置方式见:Session分布式共享 = Session + Redis + Nginx

5-2、安装Microsoft.Extensions.Caching.Redis.Core

       NuGet中搜索Microsoft.Extensions.Caching.Redis.Core并安装,此NuGet包是对Caching的拓展,即可以更换Caching存储介质

image

5-3、appsettings.json配置Redis连接字符串

       appsettings.json配置Redis连接字符串(相当于web.config里面配置appsetting节点),注意:添加位置要在Logging上面,否则读不到,添加代码为下面的高亮部分

{

"Data": "RedisConnection",
"ConnectionStrings"
: {
"RedisConnection": "192.168.8.138:6379"
},
"Logging": {
    "IncludeScopes": false,
    "LogLevel": {
      "Default": "Warning"
    }
  }
}

5-4、Startup.cs的ConfigureServices方法中添加引用

public void ConfigureServices(IServiceCollection services)
        {
            // Add framework services.
            services.AddMvc();
            services.AddDistributedRedisCache(option =>
{
                   //redis 数据库连接字符串
                   option.Configuration = Configuration.GetConnectionString("RedisConnection");
                   //redis 实例名
                   
option.InstanceName = "master";
             
});
              
services.AddSession();

          }

         页面运行HttpContext.Session.GetString("key"),然后用Redis管理工具RedisDesktopManager查询Session是否入库。

4

5-5、发布前指定IP和端口(重要) 

         如果你没有看这个步骤,继续下面发布步骤,等你发布时候,你会发现一个尴尬的问题,就是你用IP访问不了你的网站,用localhost可以访问,.Net Core默认是5000端口,端口占用也会让你的网站访问不了。

         只需要在Program.cs中添加高亮代码即可,细心地人已经看到.UseUrls(new string[] { }) 传入的是个数组,那么这里定义多个网站,当你执行时候dotnet命令时候,多个网站都会启动。

image

public static void Main(string[] args)
        {
            var host = new WebHostBuilder()
                  //增加处,*号表示ip
                  .UseUrls(new string[] { "http://*:7201
})
                .UseKestrel()
                .UseContentRoot(Directory.GetCurrentDirectory())
                .UseIISIntegration()
                .UseStartup<Startup>()
                .UseApplicationInsights()
                .Build();

            host.Run();
        }

6、.Net Core 发布

6-1、Windows安装.Net Core发布环境[10.2.107.100]

         1)安装Windows Server Hosting (x64 & x86)相当于IIS,注意安装时候请联网(好像是自动下载sdk,具体没仔细研究)。

image

        2)输入dotnet命令验证,如果“报’dotnet’不是内部或者外部命令”请找到“C:\Program Files\dotnet”文件夹中的dotnet.exe,用cmd来调用dotnet.exe来运行,或者添加系统环境变量(window中cmd命令可以节省在编写命令时候可以.exe,即命令dotnet就是dotnet.exe)

image

       坑1】

         在win7下提示一下错误:Failed to load the dll from [C:\Program Files\dotnet\host\fxr\1.0.1\hostfxr.dll], HRESULT: 0x80070057

image

         解决方法:

         需要安装补丁:KB2533623

        下载地址如下:

         https://support.microsoft.com/en-us/kb/2533623 

      【坑2】

         注意.net Core版本,本文主要是用的.net Core 1.1.1开发的,下面两个截图是版本按错了出的错误信息

image

6-2、Ubuntu安装.Net Core发布环境[10.2.107.46]

         Ubuntu安装.Net Core官方写的很详细了,照着做即可,千万别抵触Linux系统,抵触的话那就别用.Net Core了,如果不知道Ubuntu和Linux的关系的话请百度。

image

         最后验证dotnet命令是否可以使用。

image

6-3、发布网站

       在项目上右键->发布…

image

image

       点击发布按钮,生成的文件如下(SessionTest为应用程序名)

image

        好了,有了这些文件,我们只需要把这些文件扔到服务器上就成了,但是怎么启动呢?通过查询,网上说只要用dotnet命令就成。继续实践…

        说明:我的项目叫做image生成了image这个为主要的dll,也是程序的入口。

        大家都知道.Net Core是跨平台的,不同系统的服务器环境配置好了,网上查询说是使用dotnet命令启动网站,那么可以推断出几个平台的dotnet命令是一样的。

6-3-1、Windows启动.Net Core网站[10.2.107.100:7201]

         启动.Net Core网站的命令很简单,安装好发布环境的应用程序,C:\Program Files\dotnet目录如下(如果dotnet命令不能用,可以直接调用dotnet.exe这个应用程序。)

image

         将生成好的网站复制到服务器上

image

 cmd命令找到PublishOutput

cd C:\PublishOutput

image

dotnet运行网站命令

dotnet SessionTest.dll

成功以后(之后再编译运行,会提示下面截图)

image

访问http://10.2.107.100:7201/(如果一台机子有多个网卡多个IP,其他IP的7201端口也是个独立网站)

image

 

6-3-2、Ubuntu启动.Net Core网站[10.2.107.46:7201]

想办法将发布的程序复制到Ubuntu上面去,我测试使用的VBox虚拟机。

具体方法传送门:virtualbox中ubuntu和windows共享文件夹设置

dotnet SessionTest.dll

image

访问http://10.2.107.46:7201/

image

7、Nginx配置

7-1、网站端口修改

        nginx.conf配置修改

image

        listen   80; 改成 listen   81; 因为一般都被80都被使用。

server {
        listen       81;
        ……
}

7-2、增加负载均衡

  nginx.conf中添加upstream节点

upstream Jq_one {
      server
10.2.107.100:7201
;
       server
10.2.107.46:7201;
} 
server {
.....
}

 

7-3、location节点修改

location / {
            root   html;
            index  index.aspx index.html index.htm;
            #其中jq_one 对应着upstream设置的集群名称
            proxy_pass         http://Jq_one; 
            #设置主机头和客户端真实地址,以便服务器获取客户端真实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;
}

7-4、Nginx启动命令

        C:\server\nginx-1.0.2>start nginx

        或

        C:\server\nginx-1.0.2>nginx.exe

7-5、Nginx重新载入命令

      C:\server\nginx-1.0.2>nginx.exe -s reload

 

四、黎明前的黑暗-MachineKey


 

      本以为做了上述准备和相关代码编写,就能够实现Session共享了,结果我想的太简单了,应用程序发布后并不能实现Session共享,难道分布式共享下Session需要特殊处理?.Net我是怎么实现的,它们的方法应该方法类似。我突然想到了MachineKey这个东西,之前在.Net版本分布式共享时候需要添加这个东西,评论也有人问我什么要加MachineKey。后来只能搜索.Net Core Machinekey关键词,找到了以下几篇文章做参考。

      搭建分布式 ASP.NET Core Web

      ASP.NET Core 数据保护(Data Protection)

      坎坷路:ASP.NET Core 1.0 Identity 身份验证(中集)

      net core 1.0 实现负载多服务器单点登录

      此问题属于数据安全问题,微软在开发.Net Core中延续了之前的设计,采用数据保护(Data Protection)方式对一些内部数据进行加密解密设计,如:Session、Cookie等(远不止这些)。这样可以保证数据的真实性、完整性、机密性、隔离性。数据安全必然离不开加解密算法,大家想一下之前.Net的WebFrom中的ViewState,它最终解析到Html页面是个hidden标签里面有一串很复杂的字符串,这个字符串是被数据保护(Data Protection)机制加密过的。Session也一样,大家可以看看Session存到Redis中啥样,见下图:

image

       数据保护(Data Protection)有个特性是隔离性,大家可以想象一下,数据保护核心是加密解密,常见的加密方式有对称加密和非对称加密,上一篇做分布式共享时候,两台机子拷贝了同样的MahcineKey,那么他的内部加密猜测好像是对称加密,MachineKey直译中文为“机器钥匙”在联想隔离性,那么可以推断出来不同机子密钥是不同的,那么MachineKey的作用是统一不同机子的密钥。(吐血中…….这个只是个猜测,详细原理请参考专业文章)

1、提取.Net Core的MachineKey

        .Net Core的MachineKey存储是以key-xxxx-xxxx-xxxx-xxxx.xml的形式存储的,那如何提取这个xml信息呢?

       Startup.cs的ConfigureServices添加下图高亮代码

public void ConfigureServices(IServiceCollection services)
        {
              //抽取key-xxxxx.xml
            services.AddDataProtection().PersistKeysToFileSystem(new DirectoryInfo(@"D:\XML"));
            services.AddSession();
            services.AddDistributedRedisCache(option =>
            {
                //redis 数据库连接字符串
                option.Configuration = Configuration.GetConnectionString("RedisConnection");

                //redis 实例名
                option.InstanceName = "master";
            });
            services.AddMvc();
        }

       查看D:\Xml里的xml文件

imageimage

 

2、重写IXmlRepository接口固定Key

       在项目中添加CustomXmlRepository.cs类,其中keyContent中填写key.xml内容,注意:里面的几个时间(现在还不能确定expirationDate对项目是否有影响),有人问我KeyContent能否从文件里读,回答是可以,但是ubuntu的文件路径保准不是Windows的d:\之类的,需要使用Linux的写法,所以干脆字符串来的快。

using Microsoft.AspNetCore.DataProtection.Repositories;
using Microsoft.AspNetCore.Http;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using System.Xml.Linq;

namespace SessionTest
{
    public class CustomXmlRepository : IXmlRepository
    {
        private readonly string keyContent =
@"<?xml version='1.0' encoding='utf-8'?>
<key id='9108538d-9ea4-45fb-a690-438c8d788619' version='1'>
  <creationDate>2017-04-27T06:15:07.2194692Z</creationDate>
  <activationDate>2017-04-27T06:15:07.1844647Z</activationDate>
  <expirationDate>2017-07-26T06:15:07.1844647Z</expirationDate>
  <descriptor deserializerType='Microsoft.AspNetCore.DataProtection.AuthenticatedEncryption.ConfigurationModel.AuthenticatedEncryptorDescriptorDeserializer, Microsoft.AspNetCore.DataProtection, Version=1.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60'>
    <descriptor>
      <encryption algorithm='AES_256_CBC' />
      <validation algorithm='HMACSHA256' />
      <masterKey p4:requiresEncryption='true' xmlns:p4='http://schemas.asp.net/2015/03/dataProtection'>
        <!-- Warning: the key below is in an unencrypted form. -->
      <value>HOz58FE6STtDHlMo2ZONoPgPTOOjRPikRWXmHOwNDS5o6NPb4hlgl/DxXUhat66soovBUFy1APXCQ4z30DDPyw==</value>
      </masterKey>
    </descriptor>
  </descriptor>
</key>";
         
        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)
        {
        }
    }
}

修改Startup.cs文件中的ConfigureServices方法加载自定义的CustomXmlRepository类

public void ConfigureServices(IServiceCollection services)
        {
            ////抽取key-xxxxx.xml
            //services.AddDataProtection()
            //        .PersistKeysToFileSystem(new DirectoryInfo(@"D:\XML"));

            services.AddSingleton<IXmlRepository, CustomXmlRepository>(); 
              services.AddDataProtection(configure =>
            {
                configure.ApplicationDiscriminator = "newP.Web";
            });

            services.AddSession();
            services.AddDistributedRedisCache(option =>
            {
                //redis 数据库连接字符串
                option.Configuration = Configuration.GetConnectionString("RedisConnection");

                //redis 实例名
                option.InstanceName = "master";
            });

            services.AddMvc();

        }

五、实现效果演示


         演示效果说明

         本机127.0.0.1也为10.2.107.100,因为电脑性能有限,没有弄windows虚拟机,只弄了10.2.107.46这台Linux虚拟机。

         MachineKey的这个实现思路也可以用到.Net Core的身份验证上。

         UNC文件也可以实现Session共享方式

         原理就是Windows和Linux通过文件共享和挂载的方式Key.xml共享一个文件,但是总觉得有点怪怪的,共享文件会不会被别人恶意篡改,所以最后采用重写的方式实现。

         对UNC方式感兴趣的请看:搭建分布式 ASP.NET Core Web

 

 

5 六、后记&感悟


         希望通过本文,让大家对网站的可用性中有个简单认识,并了解到Session存入Redis中的优势。

        个人观点,有可能因为知识和阅历的原因,分析片面,请多谅解。

 

七、参考文章


         ASP.NET Core 使用 Redis 和 Protobuf 进行 Session 缓存

        .Net Core Session使用

        Asp.net Core 使用Redis存储Session

        Using Sessions and HttpContext in ASP.NET Core and MVC Core

        .NET Core与.NET Framework、Mono之间的关系

        virtualbox中ubuntu和windows共享文件夹设置  

        搭建分布式 ASP.NET Core Web

        ASP.NET Core 数据保护(Data Protection)

        坎坷路:ASP.NET Core 1.0 Identity 身份验证(中集)

        net core 1.0 实现负载多服务器单点登录

 

    

posted @ 2017-05-02 09:01  傲翼飞寒  阅读(10433)  评论(42编辑  收藏  举报