本文首发于我的个人网站: https://hewanyue.com/
本文作者: Hechao
本文链接: https://hewanyue.com/blog/6254cc16.html

会话保持起源

  tomcat作为一个应用服务器,单机性能上都是无法满足生产中需要的,而想要解决高并发场景,光靠提升单机性能,成本与效果肯定都是无法让人接受的,而此时我们一般都采用tomcat集群的方式,用多台tomcat服务器来共同支撑我们的业务。
  但这时就出现了一个新的问题,那就是会话保持。因为每台tomcat服务器的session是独立的,当客户端被调度到一个新的tomcat服务器时,他无法识别之前一台的tomcat服务器分配的sessionID,于是对于此次访问,之前的会话信息就都没有了,这表现在用户的客户端就相当于,点开一个新的链接,就发现需要重新登陆,或者之前的购物车里的商品都不见了等等。这样的客户访问体验绝对不是我们想要的,所以我们需要实现会话保持功能!

会话保持实现

  一般tomcat的会话保持有三种方案实现:

  • nginx、httpd或者haproxy的调度实现session绑定,一般是源地址哈希方式实现
    优点:简单易配置
    缺点:
    ①如果目标服务器故障后,没有做持久化的话就会丢失session;
    ②即便做了持久化,当服务器故障后,nginx或者haproxy会不得不重新分配一个tomcat服务器,而这时因为新的tomcat服务器上没有原来的sessionID,所以无法找到相应会话信息,会重新分配一个sessionID给客户端,就算原来的tomcat服务器重新上线,又被分配到原来的tomcat服务器中,可此时客户端已经有了新的sessionID,也不会去读取最开始的session信息,那些会话信息就相当于永远丢失了。
  • session复制集群,官方给出的tomcat会话共享解决方案
      tomcat自己提供的多播集群,通过多播将任何一台的session同步到其他节点。
    缺点:
    ①tomcat的同步节点不宜过多,互相即时通信session需要太多带宽;
    ②每一台tomcat服务器都拥有全部session信息,内存占用太多。
  • session server
      session 共享服务器,一般使用memcached、redis做共享的session服务器。
    目前最理想的解决方案,不过会需要额外的机器来配置共享服务器。

反向代理的session绑定

  这种实现session保持的方案,一般是使用的不多,一般用于公司内部中的会话保持场景。只需在haproxy或者nginx中的调度算法中,加入基于源地址hash即可(调度算法参考之前博客)。于是,用户每次访问都会被调度到同一台tomcat服务器上,上面已有他的session信息,便实现了会话保持。

配置实现

  我们用一台nginx服务器做反向代理,两台tomcat服务器来演示实现过程。

nginx主机ip:192.168.32.207
tomcat主机1ip:192.168.32.231
tomcat主机2ip:192.168.32.232

  将3台机子中的nginx或tomcat服务启动之后,分别检查80端口和8080端口是否都已监听,确保服务启动。

iptables -F
getenforce 0

  确保防火墙和SELinux设置不会干扰我们几台主机间的相互通信。
  nginx主机反向代理的配置

upstream tomcat {#ip_hash; #先关闭原地址iphash,观察效果server 192.168.32.231:8080 weight=1 fail_timeout=5s max_fails=3;server 192.168.32.232:8080 weight=1 fail_timeout=5s max_fails=3;
}server {listen 80;index index.jsp
#    server_name www.example.net;
#    location ~* \.(jsp|do)$ {location / {proxy_pass http://tomcat;}
}

  可启用主机名,也可不启用直接用端口访问。用域名访问还需要改DNS或者hosts设置,比较麻烦,我们就直接通过IP+端口访问就可以了。
  tomcat服务器中可以新建一个host,也可使用原先的localhost默认主机。我们这次不用之前的localhost,而是自己新创建一个host标签,指定appBase在/data/myapp目录下。两个tomcat服务器都要执行如下操作:

vim /apps/tomcat/conf/server.xml

  找到Engine标签,将默认主机修改为``myapp`

    <Engine name="Catalina" defaultHost="myapp">

  找到localhost</Host>标签,在下面创建新的主机myapp,指定appBase为/data/myapp

      <Host name="myapp"  appBase="/data/myapp"unpackWARs="true" autoDeploy="true"></Host>

  PS:Host name一般为主机域名,当一个tomcat中有多个host服务时,就是通过http报文请求头部的host信息来判断去访问哪个host服务的,当找不到对应的主机之后才访问defaultHost,我们这里因为只有启用了一个host,且为defaultHost,所以就无所谓,可以任意命名了,只需对应上就好。
  之后我们要在/data/myapp目录下创建一个ROOT目录来作为tomcat访问的默认目录,注意ROOT是大写的。

mkdir -p /data/myapp/ROOT

  为了方便我们看到效果,我们编写index.jsp时,调用一些函数,方便我们看到我们访问的tomcat主机的IP和端口、sessionID、访问时间。

vim /data/myapp/ROOT/index.jsp
<%@ page import="java.util.*" %>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>lbjsptest</title>
</head>
<body>
<div>On <%=request.getServerName() %></div>
<div><%=request.getLocalAddr() + ":" + request.getLocalPort() %></div>
<div>SessionID = <span style="color:blue"><%=session.getId() %></span></div>
<%=new Date()%>
</body>
</html>

  此时,我们访问nginx的80端口,就可以被反向代理到后端的两个tomcat服务器上去。而这时,是轮询的调度方式,也就是一边一次,可以看到访问的主机和sessionID都是一直在变化的。
效果如下图所示:

  每一次的sessionID都没有重复过,这肯定不满足我们的需要。所以我们将nginx配置中#ip_hash;的#注释去掉,重启nginx服务,再看效果,如下图所示:

  可以看到,每次刷新,主机IP和sessionID都不再变化,说明绑定session成功。

session复制集群

  这是tomcat官方提供的解决方案,所有tomcat上都有全量的session,不过同步session信息会消耗带宽,而且所有服务器保存所有session信息也比较占用资源,对于tomcat这种本身就处于效率瓶颈的服务来说,高并发场景下超过若五个tomcat服务器,就不再建议使用。
  配置详细可以参见官网配置说明。

        <Cluster className="org.apache.catalina.ha.tcp.SimpleTcpCluster"channelSendOptions="8"><Manager className="org.apache.catalina.ha.session.DeltaManager"expireSessionsOnShutdown="false"notifyListenersOnReplication="true"/><Channel className="org.apache.catalina.tribes.group.GroupChannel"><Membership className="org.apache.catalina.tribes.membership.McastService"address="228.0.0.4"port="45564"frequency="500"dropTime="3000"/><Receiver className="org.apache.catalina.tribes.transport.nio.NioReceiver"address="auto"port="4000"autoBind="100"selectorTimeout="5000"maxThreads="6"/><Sender className="org.apache.catalina.tribes.transport.ReplicationTransmitter"><Transport className="org.apache.catalina.tribes.transport.nio.PooledParallelSender"/></Sender><Interceptor className="org.apache.catalina.tribes.group.interceptors.TcpFailureDetector"/><Interceptor className="org.apache.catalina.tribes.group.interceptors.MessageDispatchInterceptor"/></Channel><Valve className="org.apache.catalina.ha.tcp.ReplicationValve"filter=""/><Valve className="org.apache.catalina.ha.session.JvmRouteBinderValve"/><Deployer className="org.apache.catalina.ha.deploy.FarmWarDeployer"tempDir="/tmp/war-temp/"deployDir="/tmp/war-deploy/"watchDir="/tmp/war-listen/"watchEnabled="false"/><ClusterListener className="org.apache.catalina.ha.session.ClusterSessionListener"/></Cluster>

  将官网上的这段集群配置,插入到两个tomcat服务器中我们创建的host标签中(也可插入引擎标签中,就相当于所有主机都生效),即<Host name标签与</Host>标签的中间。
  配置说明:

Cluster 集群配置
Manager 会话管理器配置
Channel 信道配置Membership 成员判定。使用什么多播地址、端口多少、间隔时长ms、超时时长ms。同一个多播地址和端口认为同属一个组。使用时修改这个多播地址,以防冲突Receiver 接收器,多线程接收多个其他节点的心跳、会话信息。默认会从4000到4100依次尝试可用端口。address="auto",auto可能绑定到127.0.0.1上,所以一定要改为可以用的IP上去Sender 多线程发送器,内部使用了tcp连接池。Interceptor 拦截器
ReplicationValve 检测哪些请求需要检测Session,Session数据是否有了变化,需要启动复制过程
ClusterSessionListener 集群session侦听器

  复制完官方文档里的配置,我们还需要修改接收器的ip为本机ip,不能使用auto,否则会无法同步session信息。
  此外,还需要在应用的web.xml文件中最后一行</web-app>标签的上面一行插入子标签<distributable/>表示可分配。
  操作如下:

mkdir /data/myapp/ROOT/WEB-INF
cp /apps/tomcat/conf/web.xml /data/myapp/ROOT/WEB-INF/
sed -i '/<\/web-app>/i<distributable/>' /data/myapp/ROOT/WEB-INF/web.xml

  此时,如果我们将前端Nginx或者其他反向代理服务器中的源地址hash去掉,我们就可以看到,虽然访问的tomcat服务器在变化(服务器ip变化),而sessionID却是不变的,因为每个tomcat服务器上都有全量的相同的session信息。

session 共享服务器

  这种实现方式其实才是本文标题中真正的session共享,毕竟共享经济炒的很热,我们都知道,所谓共享,共用才叫共享,前面提到的那种tomcat集群里会话信息人手一份可不能称作是共享。于是乎,我们想到用将session放到外部存储,所有的tomcat服务器都去外部存储中去查找sessionID。
  储存session信息肯定不能存储在磁盘文件中,这样的读取写入性能都会很慢,放在mysql数据库中或者以硬盘文件的方式保存,高并发场景下的读取写入速度都将会大打折扣。所以我们要使用类似memcached或者redis这种Key/Value的非关系型数据库里,也被称作NoSQL。
  memcached和redis这种键值对型数据库的数据信息都是存储在内存中的,读写效率都很高,而且由于没有复杂的表关系,采用的是哈希算法,他们对于信息的查找都是O(1),而不是类似mysql等数据库的O(n),意思就是说耗时/耗空间与总数据量大小无关,不会随着数据量的增大导致查找时间几何倍数的增长。
  但也因为memcached和redis的数据都是储存在内存中的,而且memcached还不支持持久化,所以我们一定要做好高可用,一旦发生故障,会话信息将立即丢失,几乎没有恢复的可能。
  要实现tomcat共享session服务器,首先,我们要让tomcat将session储存到memcached或者redis等外部存储上,这就需要我们对tomcat进行配置,其次我们要将session信息序列化为变为字节流以便能储存在session服务器中,还要能将session服务器中的数据反序列化为可以识别的session信息,最后,当然我们还需要一个客户端来跟后端的session服务器通信,才能将数据写入和读取。
  这想实现确实也比较复杂,不过在github已有开源解决方案(网址是https://github.com/magro/memcached-session-manager),memcached-session-manager,简称msm,后端采用memcached或者redis都可以(之前只支持memcached,因而得名msm,后来支持redis后,人们还是习惯叫它msm),且已经完成了对tomcat的session共享的配置支持(支持tomcat6.X、7.X、8.X、9.X),我们直接去下载对应版本的去使用就可以了。
  根据项目的介绍文档,我们知道,想实现tomcat的session共享,我们至少需要配套的工具有:

  • tomcat的session管理工具 memcached-session-manager
  • 与session服务器通信的客户端
    如果是memcached,则建议使用spymemcached.jar
    如果是redis,则建议使用jedis.jar
  • 将session信息序列化的工具,作者推荐使用kryo。

  这些工具官网上也都提供了下载链接,我们直接下载下来即可。
  kryo如下图所示

  其他工具包如下图所示

  将这些jar包统统拷贝到tomcat服务器的lib目录下(改变lib目录下的jar包需重启tomcat服务才能生效)
  使用msm搭建session共享服务器,如果后端为memcached,则有两种模式可以选,分别是sticky模式和non-sticky模式,后端为redis,则使用类似non-sticky模式。

sticky模式

  以两台服务器为例,将tomcat1和memcached1部署在一台服务器上(简称为t1、m1),tomcat2和memcached2部署在另一台服务器上(简称为t2、m2)为例,结构图如下图所示。

<t1>   <t2>. \ / ..  X  .. / \ .
<m1>   <m2>

  实现原理:当请求结束时Tomcat的session会送给memcached备份。即Tomcat session为主session,memcached session为备session,使用memcached相当于备份了一份Session。查询Session时Tomcat会优先使用自己内存的Session,Tomcat通过jvmRoute发现不是自己的Session,便从memcached中找到该Session,更新本机Session,请求完成后更新memcached。
  可能有的朋友看的一头雾水,这到底是个什么结构。其实很简单,sticky模式就是t1的session信息还是储存在t1上,不过以m2为备用数据库,t2的session信息也是放在t2中储存,以m1服务器为备用服务器。这就意味着,用户在访问t1时,是从t1获取session信息,当t1挂掉或者整个节点1服务器挂掉之后,用户会被调度到t2上,而t2本地中没有session信息时,就会去m2中上找相关sessionID,而m2因为是t1的备用存储,所以有跟t1完全相同的session信息,于是用户的sessionID就可以被t2识别;而当m2备用存储服务挂掉之后,t1服务会通过检测发现自己没有备用存储,就会自动将m1也指定为自己的备用存储,将备份信息也同步至m1中,于是用户若再从t2访问时,虽然因为m2挂掉,其中的数据都无法访问,但t2就可以从m1上读取到对应的sessionID并同步到t2本身的存储中,也可以保持之前的会话信息。
  修改的配置也很简单,依照官网说明,将下面的代码标签插入/conf/context.xml文件中的context标签结尾就可以了

<Manager className="de.javakaffee.web.msm.MemcachedBackupSessionManager"
memcachedNodes="n1:192.168.32.231:11211,n2:192.168.32.232:11211"
failoverNodes="n1"
requestUriIgnorePattern=".*\.(ico|png|gif|jpg|css|js)$"
transcoderFactoryClass="de.javakaffee.web.msm.serializer.kryo.KryoTranscoderFactory"
/>

  n1、n2只是memcached的节点别名,可以重新命名。
failoverNodes是指故障转移节点,也就是发生故障之后的备用节点,所以在n1节点上,n1是备用节点,n2是主存储节点。另一台Tomcat中配置将failoverNodes改为n2,意思是其主节点是n1,备用节点是n2。
若配置成功,在/apps/tomcat/log/catalina.out文件中看到如下信息。

tail -n 20 /apps/tomcat/logs/catalina.out
23-Nov-2019 13:35:54.187 INFO [myapp-startStop-1] de.javakaffee.web.msm.MemcachedSessionService.startInternal --------
-  finished initialization:
- sticky: true
- operation timeout: 1000
- node ids: [n1]
- failover node ids: [n2]
- storage key prefix: null
- locking mode: null (expiration: 5s)
--------

此时在访问我们的前端代理(取消ipHASH绑定)就会看到下面界面,将节点2 关机,可以看到访问ip固定为192.168.32.231,而使用的memcached变成了n1节点。

non-sticky模式

  从msm 1.4.0之后开始支持non-sticky模式。
Tomcat session为中转Session,如果n1为主session,n2为备session,则产生的新的Session会发送给主、备memcached,并清除本地Session,也就是说tomcat本身不储存session信息,只负责产生session。
  需要注意的是,如果n1下线,n2转换为主节点。n1再次上线,n2依然是主Session存储节点。
  配置方法与sticky大致相同,不过在/conf/context.xml文件中的context标签结尾插入代码略有不同,具体代码如下

<Manager className="de.javakaffee.web.msm.MemcachedBackupSessionManager"
memcachedNodes="n1:192.168.32.231:11211,n2:192.168.32.232:11211"
sticky="false"
sessionBackupAsync="false"
lockingMode="uriPattern:/path1|/path2"
requestUriIgnorePattern=".*\.(ico|png|gif|jpg|css|js)$"
transcoderFactoryClass="de.javakaffee.web.msm.serializer.kryo.KryoTranscoderFactory"
/>

  重启tomcat服务后生效。此时在/apps/tomcat/log/catalina.out文件中看到如下信息。

tail -n 20 /apps/tomcat/logs/catalina.out
23-Nov-2019 13:43:05.863 INFO [myapp-startStop-1] de.javakaffee.web.msm.MemcachedSessionService.startInternal --------
-  finished initialization:
- sticky: false
- operation timeout: 1000
- node ids: [n1, n2]
- failover node ids: []
- storage key prefix: null
- locking mode: uriPattern:/path1|/path2 (expiration: 5s)
--------

  再次尝试访问负载代理服务器,发现同样实现了访问tomcatIP变化,sessionID不变,说明配置成功。
  而后端使用redis作为session共享服务器时,仅支持non-stricky模式。建议用另外的服务器安装redis服务,并修改监听IP后启动,tomcat服务器中将jedis.jarjar包拷贝至tomcat安装路径下lib目录,同样在/conf/context.xml文件中的context标签结尾插入下面的代码即可(例如redis服务器IP端口为192.168.32.233:6379,可配置redis集群,可参考我之前博客redis高可用配置)。

<Manager className="de.javakaffee.web.msm.MemcachedBackupSessionManager"
memcachedNodes="redis://192.168.32.233:6379"
sticky="false"
sessionBackupAsync="false"
lockingMode="uriPattern:/path1|/path2"
requestUriIgnorePattern=".*\.(ico|png|gif|jpg|css|js)$"
transcoderFactoryClass="de.javakaffee.web.msm.serializer.kryo.KryoTranscoderFactory"
/>

MSM实现tomcat集群的session共享相关推荐

  1. Nginx+Tomcat+Memcached实现tomcat集群和session共享

    Nginx+Tomcat+Memcached实现tomcat集群和session共享 [http://blog.csdn.net/shimiso/article/details/8979044] 为什 ...

  2. Tomcat集群及Session共享

    Tomcat集群 由于单台Tomcat的承载能力是有限的,当我们的业务系统用户量比较大,请求压力比较大时,单台Tomcat是扛不住的,这个时候,就需要搭建Tomcat的集群,而目前比较流行的做法就是通 ...

  3. Nginx+tomcat集群的session共享问题

    请求负载过程中会话信息不能丢失.那么需要在多个tomcat中session需要共享.所以需要进行相关问题的解决 配置Tomcat的session共享可以有三种解决方案: 第一种:是以负载均衡服务器本身 ...

  4. 基于tomcat集群做session共享

    前端代理服务器nginx:192.168.223.136 tomcat服务器:采用的一台多实例192.168.223.146:8081,192.168.223.146:8082(如何构建多实例tomc ...

  5. Tomcat集群和Session共享的配置方法

    Tomcat集群配置其实很简单,在Tomcat自带的文档中有详细的说明( /docs/cluster-howto.html ),只不过是英语的,对我这样的人来说很难懂 . 下面根据说下怎么配置吧: 第 ...

  6. 关于 tomcat 集群中 session 共享的三种方法

    前两种均需要使用 memcached 或 redis 存储 session ,最后一种使用 terracotta 服务器共享. 建议使用 redis ,不仅仅因为它可以将缓存的内容持久化,还因为它支持 ...

  7. nginx tomcat集群实现session共享

    tomcat配置文件 项目中引入如下4个jar包,就搞定啦

  8. 通过memcached来实现对tomcat集群中Session的共享策略 .

    近期在做一套集群的实现,实现的方案是在Linux下完成对Apache + Tomcat 负载均衡的功能. 但是实现了该集群后,发现登陆系统后,每次都会被拦截回登录页面,造成该现象的原因是Session ...

  9. Redis + Tomcat + Nginx 集群实现 Session 共享

    点击上方"方志朋",选择"设为星标" 回复"666"获取新整理的面试文章 作者 | 蕃薯耀 链接 | www.cnblogs.com/fan ...

最新文章

  1. python中的 if __name__ == '__main_'的作用和原理
  2. leetcode117. 填充每个节点的下一个右侧节点指针 II
  3. HashMap30连问,彻底搞懂HashMap
  4. python聚类分析案例_深度解读|如何构建用户分级体系实现精细化运营?附案例实操...
  5. 终于有人把YOLO讲明白了
  6. 7种主流案例,告诉你调度器架构设计通用法则(干货!)
  7. postgresql分割字符串_postgresql 实现字符串分割字段转列表查询
  8. 物联卡可以持续使用的时间有多久?
  9. ElementUI:使用nav报错Invalid prop: custom validator check failed for prop “index“.
  10. axure 8 表格合并_Excel表格制作出库过磅单,详细步骤讲解,一起来制作吧
  11. 查看DELL服务器保修期
  12. html 百度地图坐标拾取,百度拾取坐标系统坐标反查是什么意思
  13. 微信群发工具,纯Python编写~
  14. Blender 2.8 白色背景的处理方式
  15. 雨听 | 英语学习笔记(十一)~作文范文:公园的免费入口
  16. 经营者思维—赢在战略人力资源管理
  17. [iOS] Xcode 5 + iOS 7免证书(iDP)真机调试与生成IPA全攻略
  18. 什么是AES对称加密算法
  19. ROC曲线绘制(Python)
  20. 北京 少儿编程 java_Greenfoot【java学习】-创建新世界

热门文章

  1. c语言用户自定义类型,c语言用户自定义数据类型.ppt
  2. 针对 store_name 字段
  3. xe5 TStyleManager 界面风格
  4. 计算机医学参考文献,热门医学计算机论文参考文献 医学计算机论文参考文献哪里找...
  5. 单片机自动调光C语言,基于51单片机的DS18B20水温PID控制调节系统设计C语言程序...
  6. ES6 数组some()和every()使用
  7. EasyPlayerPro RTMP播放器助力远程娃娃机直播抓娃娃技术方案
  8. Mac电脑键盘(ujmiko)不能使用问题记录
  9. 破解某助手刺探功能---第二篇smali代码实现
  10. android+qq+4.6.2,AndroidQQ通讯录4.6闪亮登场 让你领略“超快感”