1.1 应用服务器的无状态特性
应用层服务器(这里一般指Web服务器)处理网站应用的业务逻辑,应用的一个最显著的特点是:应用的无状态性。
PS:提到无状态特性,不得不说下Http协议。我们常常听到说,Http是一个无状态协议,同一个会话的连续两个请求互相不了解,他们由最新实例化的环境进行解析,除了应用本身可能已经存储在全局对象中的所有信息外,该环境不保存与会话有关的任何信息。之所以我们在使用ASP.NET WebForm开发中会感觉不到Http的无状态特性,完全是因为Microsoft帮我们实现了ViewState,它是ASP.NET WebForm中保存页面信息的基本单位,本质是一个HTML中的隐藏域,回调时会将这个隐藏域中的数据提交到服务器端。
在很多场景中,用户都需要和我们的网站系统进行多次的信息交互,这时就需要一种解决方案来克服无状态特性所带来的困境。还好,在巨人的肩膀上,我们已经有了很好的解决方案,那就是浏览器端的Cookie和服务器端的Session。在一般的单机开发中(这里一般是指只有一台Web服务器的情况),服务器端我们通常使用Session来存储用户登录状态(一般是一个自定义对象实例),在多数的管理信息系统开发中(毕竟内部系统用户量不多,一台Web服务器既提供Web服务又存储Session对象内存还算是够用的)这是很常见的。
但是,在大用户量下,单机版的Session就会显得效率低下,甚至会拖累Web服务器的性能。这是因为:每个用户的Http请求发到服务器端后,每台Web服务器的服务器软件(例如:IIS、Tomcat等)都会为该请求创建一个线程来进行处理和响应,但是一台服务器同一时间可以接收的请求数毕竟是有限的(这个根据服务器的配置而定,例如CPU中i3、i5和i7类型分别可以创建的线程数都各不相同),当某个时间段出现高并发请求数的时候(比如:网购秒杀系统中经常同一时间会出现海量的并发数),那这台应用服务器将会接收前所未有的请求负载,最终可能会因为承受不了高负载而导致宕机,网站不得不停止服务。
这时,又想起了那句话:当一头牛拉不动车的时候,不要去寻找一头更强壮的牛,而是用两头牛来拉车。于是,我们可以采用服务器集群的技术来对Web服务器进行改进,增加N台Web服务器部署相同的Web应用构成Web服务器集群来对外提供服务,通过负载均衡设备或软件将海量的并发请求数平均地分摊到每台Web服务器,例如:假设某系统在促销活动期间同一时刻涌入了10万个请求,而服务器集群中有5台Web服务器同时提供服务,这时负载均衡设备就将这个10万请求通过某种算法较为均衡地分配给其中的Web服务器,平均下来每台服务器最多就只承担2万个请求。
通过服务器集群,已经较好地解决了请求负载问题,这时新的问题又来了:由于Session默认是属于进程内(InProc)的,也就是说它是存储在Web服务器的内存里边的。当构建好集群之后,用户的Session会建立在负载均衡设备所分配的其中一台Web服务器里边。但是当用户下一次访问或者访问系统中的其他子系统(比如:我首先在百度百科进行登录了,然后访问百度贴吧),由于Session会话还存储在上一次提供服务的Web服务器里边,系统校验规则(现在这台Web服务器里边检测到没有该用户的Session)会造成用户的重复登录(比如:都是在百度的网页,它却让你登录好几次,你爽吗?很明显,不爽吧)。这时,就需要我们解决Web服务器集群的Session管理,下面我们就来看看如何进行Web服务器集群的Session管理。
1.2 应用服务器集群的Session管理
我们现在来看看在集群环境中,Session管理的几种常见手段:
①Session复制:该方案简单易行,集群中的几台服务器之间同步Session对象,任何一台服务器宕机都不会导致Session对象的丢失,服务器也只需要从本机获取即可。但是,该方案只适合集群规模较小的情况下。当规模较大时,大量的Session复制操作会占用服务器和网络的大量资源,系统不堪重负。
②Session绑定:利用负载均衡的源地址Hash算法,总是将源于同一IP地址的请求分发到同一台服务器上。这样的话,在整个会话期间,用户所有的请求都在同一台服务器上进行处理,即Session绑定在某台特定服务器上,保证Session总能在这台服务器上获取。(这种方案又叫做会话粘滞)。
但是,这种方案不符合高可用的需求。因为一旦某台服务器宕机,那么该机器上得Session也就不复存在了,用户请求切换到其他机器后因为没有Session而无法完成业务处理。因此,很少有网站采用此方案进行Session管理。
③Cookie记录Session:利用浏览器支持的Cookie记录Session简单易行,可用性高,并且支持服务器的线性伸缩,因此,许多网站都或多或少地使用了Cookie来记录Session。但是Cookie记录Session有缺点:比如受Cookie大小限制、每次请求响应都要传输Cookie影响性能、用户关闭了Cookie会造成访问不正常等。
④Session服务器:利用独立部署的Session服务器(集群)统一管理Session,应用服务器每次读写Session时,都访问Session服务器。这种方案实际上是将应用服务器的状态分离,分为无状态的应用服务器和有状态的Session服务器。
从上面的几种方式来看,各有利弊,但Session服务器是最符合高可用需求的方案,也是企业中经常用到的方案。那么,对于有状态的Session服务器,一种较简单的方法是利用分布式缓存(如Memcached、Redis等,有关Redis的简单介绍可以阅读我的博文:NoSQL初探之人人都爱Redis)、数据库等,在这些产品的基础上进行封装,使其符合Session的存储和访问要求。综合上述介绍,我们今天就采用Memcached来构建我们的Session服务器,解决Web服务器集群的Session的共享访问。
PS:为什么要采用分布式缓存方案而不采用数据库来存储Session?这个就得要分析一下数据访问的性能瓶颈了,一般来说,磁盘IO读写的速度是最慢的,因为数据库数据其实是存储在文件中的,虽然目前大多数的数据库都采用了B+树结构,读取一条数据最多都还是需要4次的数据读写(三次磁盘访问获得数据索引及行ID,一次数据文件读操作,终于知道数据库操作多麻烦了)。而分布式缓存例如Memcached是以Key/Value这种简单的形式存储在服务器的内存里边的,内存的随机读写速度是完爆磁盘IO的,因此内网+内存的双内模式是比较完美的方案。
磁盘又分为两种类型:
①机械硬盘:通过马达驱动磁头臂,带动磁头到指定的磁盘位置访问数据。它能够实现快速顺序读写,慢速随机读写。
②固态硬盘(又称SSD):无机械装置,数据存储在可持久记忆的硅晶体上,因此可以像内存一样快速随机访问。
在目前的网站应用中,大部分应用访问数据都是随机的,这种情况下SSD具有更好的性能表现,但是性价比有待提升(蛮贵的,么么嗒)。
二、Memcached实现Session的分布式存储
2.0 案例总体预览
(1)模拟的登录案例场景
假设我们有一个基于ASP.NET的信息系统,这个系统使用一个统一的系统登录页面进行用户登录,登陆后默认跳转到一个用户中心主页,并显示:欢迎您,{用户账号名称}。
①系统登录页面效果:
②用户主页效果:
(2)模拟的技术体系选择
ASP.Net MVC+EF Code First+MySQL+Memcached
2.1 初始准备工作
(1)新建一个ASP.NET MVC4的空项目,视图引擎选择为“Razor”即可;
(2)在项目中新建一个文件夹,取名为“Lib”,主要存放一些必要的DLL文件;
(3)在项目中添加对这几个DLL的引用,注意这里引入EntityFramework.dll是为了支持后面的CodeFirst开发方式,EF版本必须在4.1及以上。PS:你也可以通过Package Manager来安装EntityFramework。到此,我们的准备工作就做好了,接下来就可以开始正式的工作了。
2.2 借助EF CodeFirst生成MySQL数据库
首先,EF是一种ORM(Object-relational mapping)框架,它能把我们在编程时使用对象映射到底层的数据库结构。ORM框架负责把从数据库传回的记录集转换为对象,也可以依据对象当前所处的具体状态生成相应的SQL命令发给数据库,完成数据的存取工作(常见的数据存取操作可简称为CRUD:Create、Read、Update、Delete)。
EF给数据库应用系统开发带来了更高的效率,使用它能更容易地写出易维护、易扩展的系统,而且性能虽然比不上ADO.NET,但也足够好,能满足大多数开发场景的需求。与ADO.NET不一样,EF的抽象层次较高:它把数据库映射为DbContext,把数据库中存取的数据直接映射为实体(Entity)对象,屏蔽了底层的数据库内部结构,无需直接使用下层数据存取引擎所提供的底层对象(比如ADO.NET所提供的DbConnection,DbCommands等)完成CRUD。
EF支持三种开发模式:Code First、Database First和Model First。这里我们使用Code First模式,它能帮助我们实现快速开发迭代的目标。最后,EF不是本文的重点,如果你还不了解EF或者Code First,可以参阅金旭亮老师的《EF走马观花》系列文章,这里就不再赘述了。
(1)在Models文件夹新建一个类,取名为“UserInfo”。它作为我们的实体类,映射到MySQL数据库中的UserInfo表(这里MySQL数据库中还未创建这样的数据表)
[Serializable] public class UserInfo { [Key] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] public int UserId { get; set; } [Required] [MaxLength(128)] public string UserName { get; set; } [Required] [MaxLength(32)] public string UserPwd { get; set; } }