Tomcat集群部署

初步了解了单机Tomcat的配置. 接下来我们就来看一下Tomcat负载均衡集群的配置吧.

概述

说到了集群就一定会说到的问题就是关于会话保持的问题了, 我们也可以把这个问题叫做会话黏性, 常规的方式例如: 进行源地址IP绑定, 还有基于cookie的方式. 这两种方式的一个显而易见的问题就是关于当我们的后端服务器重启或者是宕机的时候就会导致所有的session丢失. 这个时候我们就要做session的持久化或者是使用session集群和session服务器, 常见的是基于kv的数据库比如memcached和redis.

我们先来使用nginx做前端load balancer和tomcat配合搭建LB Tomcat, 接着使用Apache加上tomcat实现一样的效果.

接着我们使用tomcat自己的LB实现来搭建tomcat负载均衡集群, 最后再加上一个memcache session服务器.

Nginx和Apache实现负载均衡的TC

这次一共使用3台主机, 其中一个作为负载均衡器, 安装httpd和nginx, 后端放两台tomcat主机, 配置上基本和上一个保持一致. 唯一的不同点是作为测试的jsp页面一个写的是node1一个写的是node2, 并且将主机名设定成了web1.wyx.com和web2.wyx.com.

接着, 将默认的主机设置成为这两个我们自定义主机.

tomcat的配置到这里就基本结束了, 接下来来配置一下nginx, 其实就是我们之前配置的反向代理, 很简单的. 直接贴下配置:

1
2
3
4
5
6
7
8
9
location ~* \.(jsp|do)$ {
proxy_pass http://tcservers;
}

upstream tcservers {
server web1.wyx.com:8080;
server web2.wyx.com:8080;
}

默认的负载均衡算法就是轮询所以当我们进行访问测试的时候就是一个接一个的出现.

但是我们说, 一个稍微好一点的架构是在每一个tomcat的前面加上一个httpd , 接着通过nginx的负载均衡到这些Apache主机上, 有这些Apache主机进行tomcat的访问, 最后返回结果.

如果是通过apache来构建tomcat集群的话, 我们有三种方案, 一种是使用http协议进行访问, 一种是使用之前说的ajp协议. 上面的这两种都需要使用到mod_proxy模块, 而子模块不同而已. 第三种方案叫做使用一个叫做mod_jk的模块来实现. 不过这个模块仅仅支持AJP协议并且httpd没有自行携带. 需要我们到官方的站点上进行源代码的下载自行编译. 另外, 我们之前提到过, httpd其实也是可以做负载均衡的, 这依赖一个模块叫做proxy_balancer_module. 这个模块一般来说也是自行携带的, 不需要再另行安装.

对于上面的第三种, 我们就暂时忽略了, 主要来看看前两的使用.

我们在上一次的Tomcat配置学习的时候, 曾经使用httpd做前端服务器进行了代理, 但是当时只是跟着配置了一下, 对于一些代理的apache指令并不知道是什么东西, 现在来说明一下.

ProxyPreserve {On|Off}: 该选项表示是否将请求报文中的Host发送给后端的代理服务器, 这个选项在之前的实验中我也尝试过了.

ProxyRequests {On|Off}: 这个选项表示是否开启Apache的正向代理功能, 要求mod_proxy_http模块必须启用.

ProxyPassReverse: 用于让apacje调整HTTP重定向报文中的Location, Content-Location和URI标签所对应的URL. 在反向代理环境中需要打开这个选项来避免重定向报文绕过proxy服务器.

ProxyVia {On|Off|Full|Block}: 用来控制是否在请求的首部中加上Via头部, 默认的值是Off. Full表示每一个Via的头部都会加上Apache服务器的版本号信息, Block表示每一个代理的请求报文中的Via都会被移除.

ProxyPass [Path] !|url [key=value key=value]: 这个指令是用来将后端服务器某个URL与当前服务器的虚拟路径关联起来作为提供服务的路径. 说白了就是进行反向代理, 这也就是说, 当这个选项打开的时候, ProxyRequests必须关闭(废话啦). 需要注意的一点是, 如果这个地方的路径后面写上了/那么URL的结尾也必须加上/. 或者都不写

ProxyPass常用的一些属性有min, max分别表示后端服务器的最小和最大连接池容量. loadfactor定义负载均衡的权重相当于是Nginx的weight. retry表示apache得到错误响应之后等到的重试时长, 单位是s.

如果上面的Proxy是使用balance://开头的 ( 这说明是一个负载均衡器, 这种感觉就好像我们在使用Ngixn反向代理功能的时候写的upstream服务器组 ), 那么我们还可以加上以下的属性:

lbmethod: 很好理解吧. 就是负载均衡使用的调度方法, 默认的方法是byrequests, 也就是根据权重将统计请求个数进行调度, 还有bytraffic, 执行基于权重的流量计数调度, bybusyness通过考量每个后端服务器的当前负载进行调度.

maxattempts: 放弃请求执行故障转移的次数, 最大值不能超过节点的个数.

nofailover: 是否进行故障转移, 如果在我们后端服务器有进行SESSION绑定的话, 就不应该进行转移, 因为这样会损坏用户的session.

stickysession: 设置调度器session名字, 根据web程序语言的不同, 设置成为JSESSIONID或者PHPSESSIONID.

比如一个示例:

1
2
3
4
5
<Proxy balancer://hotcluster>
BalancerMember http://web1.wyx.com loadfactor=1
BalancerMember http://web2.wyx.com loadfactor=2
ProxySet lbmethod=bytraffic
</Proxy>

这里就定义好了一个负载均衡集群, 那么怎么让他们生效呢? 我们在虚拟主机的配置文件中这样定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<proxy balancer://lbcluster>
BalancerMember ajp://192.168.16.100:8009 loadfactor=10
BalancerMember ajp://192.168.16.101:8009 loadfactor=10
</proxy>

<VirtualHost *:80>
ServerName www.wyx.com
ProxyVia On
ProxyRequests Off
ProxyPreserveHost On
<Proxy *>
Require all granted
</Proxy>
ProxyPass / balancer://lbcluster/
ProxyPassReverse / balancer://lbcluster/
<Location />
Require all granted
<Location>
</VirtualHost>

这样配置完成之后我们可以就先启动试试效果了, 和Nginx之前演示的效果类似, 现在 我们加上关于Session相关的配置:

1
2
3
4
5
6
<proxy balancer://lbcluster>
BalancerMember ajp://192.168.16.100:8009 loadfactor=10 route=node1
BalancerMember ajp://192.168.16.101:8009 loadfactor=10 route=node2
ProxySet stickysession=JSESSIONID
</proxy>
...(后面一致)

这样再次访问几次, 就会出现访问到同一个后端服务器的现象了.

另外, 我们的mod_proxy_http其实也内置了一个负载均衡的web管理页面, 只需要简单的配置就可以使用了, 来看一下吧.

加上一个访问路径:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<VirtualHost *:80>
ServerName www.wyx.com
ProxyVia On
ProxyRequests Off
ProxyPreserveHost On
<Proxy *>
Require all granted
</Proxy>
ProxyPass / balancer://lbcluster/
ProxyPassReverse / balancer://lbcluster/
<Location />
Require all granted
</Location>
<Location /balancer-manager>
SetHandler balancer-manager
ProxyPass !
Require ip 192.168.16.1
Require all denied
</Location>
</VirtualHost>

这里加上了一个Location, 访问的效果就想这样:

balancer-manager_example

可以在这个页面进行一些简单的管理和状态监视.

Session Cluster

tomcat提供了一些会话管理组件, 主要有两种, 一个是标准会话管理器, 一个是持久会话管理器.

对于标准会话管理器, 使用的类是org.apache.catalina.session.StandardManager, 常用的三个属性是:

  • maxActiveSessions: 最大允许的活动会话数量, 默认是-1, 也就是不限制
  • maxInactiveInterval: 非活动的会话超时时长, 默认是60s
  • pathname: 会话文件的保存目录 默认保存在webapp下的SESSIONS.ser文件中
1
2
<Manager classname=“org.apache.catalina.session.StandardManager”
maxInactiveInterval=7200>

但是大部分的情况下, 我们使用的都是另外一个可以进行持久化保存的会话管理器(PersistantManager). 这个会话管理器可以将会话数据保存在持久存储中, 并且可以在服务器意外终止的时候重启启动的时候加载这些会话信息, 持久会话管理器支持将会话保存在文件存储或者JDBC中.

例如一个保存在文件中的示例:

1
2
3
4
5
<Manager className="org.apache.catalina.session.PersistantManager"
saveOnRestart="true">
<Store className="org.apache.catalina.session.FileStore"
directory="/data/tomcat-sessions"/>
</Manager>

每一个用户的会话都会保存到上面制定的目录位置中, 命名为session_id.session, 并且后台线程会每隔一定时间进行检查(默认是60s).

上面是保存在文件中的, 刚刚说是可以使用JDBC存储的, 也就是说我们可以把上面的Store区域改成:

1
2
3
4
5
6
<Manager className="org.apache.catalina.session.PersistantManager"
saveOnRestart="true">
<Store className="org.apache.catalina.session.JDBCStore"
driverName="com.mysql.jdbc.driver"
connectionURL="jdbc:mysql//localhost:3306/mydb?user=user_name;password=pw"/>
</Manager>

另外还有一个叫做DeltaManager的会话管理器, 这一种是将每一个tomcat节点通过一条总线连接起来, 接着通过多播方式进行session同步, 每一台主机上面都包含所有的session数据, 这样哪怕一台节点宕机了, session数据也依然在, 只要前端调度进行一次故障转移就行了. 但是由于使用的是多播的方式, 所以这就必然限制了集群的规模.

除了这些, 还有一个叫做BackupManager的东西.

官方文档上关于构建会话复制集群的说明在这里: cluster-howto. 但是要看好版本号, 不同版本的tomcat配置起来会有出入的.

直接使用官方网站上的配置说明就可以了: (可以根据情况修改一下组播地址和接受的IP地址)

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
<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>

注意: 为了使得webapp可以被管理器进行分发, 必须要在web.xml中添加distributable元素

这里还是像之前的实验一样, 只有两个节点. 配置结束之后查看一下两个节点的日志, 可以看到”:

1
04-Oct-2018 17:28:24.520 INFO [Membership-MemberAdded.] org.apache.catalina.ha.tcp.SimpleTcpCluster.memberAdded Replication member added:[org.apache.catalina.tribes.membership.MemberImpl[tcp://{192, 168, 16, 101}:4000,{192, 168, 16, 101},4000, alive=1036, securePort=-1, UDP Port=-1, id={-8 8 14 -62 107 -126 71 94 -107 116 88 49 9 -83 -91 -105 }, payload={}, command={}, domain={}]]

已经加入了.

接着我们写一个JSP页面来方便观察实验效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<%@ page language="java" %>
<html>
<head><title>TomcatA</title></head>
<body>
<h1><font color="red">web1.wyx.com</font></h1>
<table align="centre" border="1">
<tr>
<td>Session ID</td>
<% session.setAttribute("wyx.com","wyx.com"); %>
<td><%= session.getId() %></td>
</tr>
<tr>
<td>Created on</td>
<td><%= session.getCreationTime() %></td>
</tr>
</table>
</body>
</html>

另外, apache也已经配置了最基本的负载均衡, 没有配置stickysession.

接着访问就会出现神奇的事情了, 虽然我们的主机在进行调度, 但是得到的session是一样的, 也就是说不同的主机处理了同一个会话.

Memcached

我们接下来引入memcached, 这是一个基于内存的使用key/value高性能的数据缓存. 和redis最大的不同是, redis是store, 而memcached是cache. 也就是说, 这玩意不能用来进行存储, 不能持久化, 但是可以进行高性能的键值对缓存.

之前说过我们软件系统存储数据有三种类型的, 分别是:

  • 结构化数据, 例如我们的RDBMS所管理的数据
  • 半结构化数据, JSON, XML都是这一类数据
  • 非结构化数据, 文件

而我们的memcached只能存取最简单的数据类型, 那就是K-V存储. 他的应用场景在哪里呢? 比如说, 在一个存储百万条数据的MySQL数据库中进行查询, 会经过很多次的磁盘IO, 这就大大降低了查询的效率, 但是如果我们在查询之后能够把查询的结果缓存下来, 就可以直接在下一次的访问时直接返回结果而不需要再进行IO了.

那么这个数据我们该怎么使用memcached来进行缓存呢? 其实很简单, 计算出这一次查询SQL语句的哈希值, 然后将这个哈希值作为存储的键, 而查询的结果集也就是一个数据流作为存储的值就可以了.

其实, 我们的MySQL系统本身就是具有缓存系统的, 只不过当我们存在多个MySQL的时候, 就需要一个公共的缓存系统了.

memcached有这些特点:

  • 协议十分简单, 因为只需要进行数据的get和push, 并不需要一些复杂的操作
  • 记录libevent事件处理
  • 基于内存完成数据的存储, 基于的算法就是我们熟悉的LRU算法
  • memcacahed互不通信的集群

至于安装也是十分简单的了, 直接使用yum进行安装就可以了, 并且看一下生成的文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
[root@VM-master ~]# rpm -ql memcached
/etc/sysconfig/memcached
/usr/bin/memcached
/usr/bin/memcached-tool
/usr/lib/systemd/system/memcached.service
/usr/share/doc/memcached-1.4.15
/usr/share/doc/memcached-1.4.15/AUTHORS
/usr/share/doc/memcached-1.4.15/CONTRIBUTORS
/usr/share/doc/memcached-1.4.15/COPYING
/usr/share/doc/memcached-1.4.15/ChangeLog
/usr/share/doc/memcached-1.4.15/NEWS
/usr/share/doc/memcached-1.4.15/README.md
/usr/share/doc/memcached-1.4.15/protocol.txt
/usr/share/doc/memcached-1.4.15/readme.txt
/usr/share/doc/memcached-1.4.15/threads.txt
/usr/share/man/man1/memcached-tool.1.gz
/usr/share/man/man1/memcached.1.gz

就连省略都不需要了, 可以说是很简单了. 一个简单的配置文件, 一个主程序, 管理程序再加上服务脚本和文档就没了.

甚至说就连配置文件也是十分的简单:

1
2
3
4
5
6
7
[root@VM-master ~]# cat /etc/sysconfig/memcached 
PORT="11211"
USER="memcached"
MAXCONN="1024"
CACHESIZE="64"
OPTIONS=""

其实就是一些环境变量.

直接启动服务就可以看到tcp/udp的11211端口已经监听了.

我们可以使用telnet进行一些memcached的尝试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[root@VM-master ~]# telnet 127.0.0.1 11211
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
set mykey 0 60 11
hello world
STORED
get mykey
VALUE mykey 0 11
hello world
END
(60s later)
get mykey
END

简单的说明一下吧. set操作就是无条件的设置一个键, 当然也可以进行修改, get操作就是在获取值. 上面的意思是set一个键, 键的名字叫做mykey, 标号是0, 存活时间是60s, 长度是11个字节, 这个地方如果你设定的字节长度和实际你要存储的数据的长度不符合的话是会报错的.

接着一些其他的操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
set mykey 0 60 11     
hello world
STORED
append mykey 0 60 1
!
STORED
get mykey
VALUE mykey 1 12
hello world!
END
prepend mykey 0 60 2
a
STORED
get mykey
VALUE mykey 1 14
a hello world!
END

通过使用appendprepend可以在键的后面和前面添加值. 另外使用delete来删除, stats来查看当前的状态, flush_all来清除所有的, incrdecr来增加和减少. 基本的命令就这些了.

但是需要提醒一点的是, 可能在你删除了一个键之后发现似乎stats显示的总数没有变少, 但是当前数目变少了, 这是因为memcached使用的是惰性的存储模式, 也就是说, 删除的时候没有真正的删除而是加上了一个不可用的标识符.

这个地方使用的是命令行的形式, 在应用程序中, 使用memcached也很简单, 因为协议简单, 所以对接起来就也容易.

接下来我们来说说memcached对内存存储的工作模式. 它使用的是叫做slab allocation的也就是整理内存来进行复用的模式. 由于存储的内容大小不一样, 所以就先按照一些提前订好的大小分成组. 举个例子, 我先准备好2字节的内存空间(很多, 一大把), 4字节的(也有很多), 8字节的(还是很多)等等. 接着假设来了一个3字节的数据, 我们就把他丢进4字节的组中. 这一个4字节的内存空间, 我们叫做chunk, 而这一整个组我们叫做slab class.

这种感觉有点类似我们内存的分页(4K). 所以其实一个4Kb的内存页的内部, 分配给slab用于再次分割, 成为一个个chunk. 这种模式对于内存的回收重新利用就很方便. memcached规定最大的单个不能超过1M. 那么这些内存空间的大小该怎么去增长呢? 这个概念在memcached中叫做factor, 也就是增长因子, 默认的大小是1.25. 我们可以手动启动加上-vv参数来验证一下:

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
[root@VM-master ~]# memcached -u memcached -vv
slab class 1: chunk size 96 perslab 10922
slab class 2: chunk size 120 perslab 8738
slab class 3: chunk size 152 perslab 6898
slab class 4: chunk size 192 perslab 5461
slab class 5: chunk size 240 perslab 4369
slab class 6: chunk size 304 perslab 3449
slab class 7: chunk size 384 perslab 2730
slab class 8: chunk size 480 perslab 2184
slab class 9: chunk size 600 perslab 1747
slab class 10: chunk size 752 perslab 1394
slab class 11: chunk size 944 perslab 1110
slab class 12: chunk size 1184 perslab 885
slab class 13: chunk size 1480 perslab 708
slab class 14: chunk size 1856 perslab 564
slab class 15: chunk size 2320 perslab 451
slab class 16: chunk size 2904 perslab 361
slab class 17: chunk size 3632 perslab 288
slab class 18: chunk size 4544 perslab 230
slab class 19: chunk size 5680 perslab 184
slab class 20: chunk size 7104 perslab 147
slab class 21: chunk size 8880 perslab 118
slab class 22: chunk size 11104 perslab 94
slab class 23: chunk size 13880 perslab 75
slab class 24: chunk size 17352 perslab 60
slab class 25: chunk size 21696 perslab 48
slab class 26: chunk size 27120 perslab 38
slab class 27: chunk size 33904 perslab 30
slab class 28: chunk size 42384 perslab 24
slab class 29: chunk size 52984 perslab 19
slab class 30: chunk size 66232 perslab 15
slab class 31: chunk size 82792 perslab 12
slab class 32: chunk size 103496 perslab 10
slab class 33: chunk size 129376 perslab 8
slab class 34: chunk size 161720 perslab 6
slab class 35: chunk size 202152 perslab 5
slab class 36: chunk size 252696 perslab 4
slab class 37: chunk size 315872 perslab 3
slab class 38: chunk size 394840 perslab 2
slab class 39: chunk size 493552 perslab 2
slab class 40: chunk size 616944 perslab 1
slab class 41: chunk size 771184 perslab 1
slab class 42: chunk size 1048576 perslab 1
<26 server listening (auto-negotiate)
<27 server listening (auto-negotiate)
<28 send buffer was 212992, now 268435456
<29 send buffer was 212992, now 268435456
<28 server listening (udp)
<29 server listening (udp)
<28 server listening (udp)
<29 server listening (udp)
<28 server listening (udp)
<29 server listening (udp)
<28 server listening (udp)
<29 server listening (udp)

通过增加-f参数来指定, 可以修改这个增长因子.

接下来来看一下memcached的管理工具, 还是之前的hello world这个11字节的数据, 存储结束之后我们退出telnet然后使用memcached-tools来查看一下:

1
2
3
4
5
6
7
[root@VM-master ~]# memcached-tool 127.0.0.1
# Item_Size Max_age Pages Count Full? Evicted Evict_Time OOM
1 96B 10s 1 1 yes 0 0 0
[root@VM-master ~]# memcached-tool 127.0.0.1
# Item_Size Max_age Pages Count Full? Evicted Evict_Time OOM
1 96B 84s 1 1 yes 0 0 0

可以看到有数据了, 其中第一个#表示slab class的编号, 往上看, 1编号的slab大小就是96字节, 而11字节的数据就进入这个里面了, 后面的Max_age表示当前缓存对象的生存时间, Pages表示分配给slab的内存页数, Count表示slab内的记录数, Full?表示是否还有空闲的chunks.