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服务器来演示实现过程。

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

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

1
2
iptables -F
getenforce 0

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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服务器都要执行如下操作:

1
vim /apps/tomcat/conf/server.xml

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

1
&ltEngine name="Catalina" defaultHost="myapp"&gt

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

1
2
3
&ltHost name="myapp"  appBase="/data/myapp"
unpackWARs="true" autoDeploy="true"&gt
&lt/Host&gt

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

1
mkdir -p /data/myapp/ROOT

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

1
vim /data/myapp/ROOT/index.jsp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
&lt%@ page import="java.util.*" %&gt
&lt!DOCTYPE html&gt
&lthtml lang="en"&gt
&lthead&gt
&ltmeta charset="UTF-8"&gt
&lttitle&gtlbjsptest&lt/title&gt
&lt/head&gt
&ltbody&gt
&ltdiv&gtOn &lt%=request.getServerName() %&gt&lt/div&gt
&ltdiv&gt&lt%=request.getLocalAddr() + ":" + request.getLocalPort() %&gt&lt/div&gt
&ltdiv&gtSessionID = &ltspan style="color:blue"&gt&lt%=session.getId() %&gt&lt/span&gt&lt/div&gt
&lt%=new Date()%&gt
&lt/body&gt
&lt/html&gt

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

  每一次的sessionID都没有重复过,这肯定不满足我们的需要。所以我们将nginx配置中#ip_hash;的#注释去掉,重启nginx服务,再看效果,如下图所示:
在这里插入图片描述
  可以看到,每次刷新,主机IP和sessionID都不再变化,说明绑定session成功。

session复制集群

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
&ltCluster className="org.apache.catalina.ha.tcp.SimpleTcpCluster"
channelSendOptions="8"&gt

&ltManager className="org.apache.catalina.ha.session.DeltaManager"
expireSessionsOnShutdown="false"
notifyListenersOnReplication="true"/&gt

&ltChannel className="org.apache.catalina.tribes.group.GroupChannel"&gt
&ltMembership className="org.apache.catalina.tribes.membership.McastService"
address="228.0.0.4"
port="45564"
frequency="500"
dropTime="3000"/&gt
&ltReceiver className="org.apache.catalina.tribes.transport.nio.NioReceiver"
address="auto"
port="4000"
autoBind="100"
selectorTimeout="5000"
maxThreads="6"/&gt

&ltSender className="org.apache.catalina.tribes.transport.ReplicationTransmitter"&gt
&ltTransport className="org.apache.catalina.tribes.transport.nio.PooledParallelSender"/&gt
&lt/Sender&gt
&ltInterceptor className="org.apache.catalina.tribes.group.interceptors.TcpFailureDetector"/&gt
&ltInterceptor className="org.apache.catalina.tribes.group.interceptors.MessageDispatchInterceptor"/&gt
&lt/Channel&gt

&ltValve className="org.apache.catalina.ha.tcp.ReplicationValve"
filter=""/&gt
&ltValve className="org.apache.catalina.ha.session.JvmRouteBinderValve"/&gt

&ltDeployer className="org.apache.catalina.ha.deploy.FarmWarDeployer"
tempDir="/tmp/war-temp/"
deployDir="/tmp/war-deploy/"
watchDir="/tmp/war-listen/"
watchEnabled="false"/&gt

&ltClusterListener className="org.apache.catalina.ha.session.ClusterSessionListener"/&gt
&lt/Cluster&gt

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

1
2
3
4
5
6
7
8
9
10
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文件中最后一行&lt/web-app&gt标签的上面一行插入子标签&ltdistributable/&gt表示可分配。
  操作如下:

1
2
3
mkdir /data/myapp/ROOT/WEB-INF
cp /apps/tomcat/conf/web.xml /data/myapp/ROOT/WEB-INF/
sed -i '/&lt\/web-app&gt/i&ltdistributable/&gt' /data/myapp/ROOT/WEB-INF/web.xml

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

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

sticky模式

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

1
2
3
4
5
&ltt1&gt   &ltt2&gt
. \ / .
. X .
. / \ .
&ltm1&gt &ltm2&gt

  实现原理:当请求结束时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标签结尾就可以了

1
2
3
4
5
6
&ltManager 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"
/&gt

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

1
tail -n 20 /apps/tomcat/logs/catalina.out
1
2
3
4
5
6
7
8
9
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节点。
tomcatsticky

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标签结尾插入代码略有不同,具体代码如下

1
2
3
4
5
6
7
8
&ltManager 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"
/&gt

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

1
tail -n 20 /apps/tomcat/logs/catalina.out
1
2
3
4
5
6
7
8
9
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高可用配置)。

1
2
3
4
5
6
7
8
&ltManager 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"
/&gt

一个低调的男人