Tomcat
History
起始于SUN的一个Servlet的参考实现项目Java Web Server,作者是James Duncan Davidson,后将项目贡献给了ASF。和ASF现有的项目合并,并开源成为顶级项目,官网http://tomcat.apache.org/。
Tomcat仅仅实现了Java EE规范的与Servlet、JSP相关的类库,是JavaEE不完整实现。
著名图书出版商O’Reilly约稿该项目成员,Davidson希望使用一个公猫作为封面,但是公猫已经被另一本书使用,书出版后封面是一只雪豹。《Tomcat权威指南》
1999年发布初始版本是Tomcat 3.0,实现了Servlet 2.2和JSP1.1规范。
Tomcat 4.x发布时,内建了Catalina(Servlet容器)和Jasper(JSP engine)等。
商用的有IBM WebSphere、Oracle WebLogic(原属于BEA公司)、Oracle Oc4j、Glassfish、JBoss等。
开源实现有Tomcat、Jetty、Resin。

安装
可以使用Centos7 yum源自带的安装。yum源中是Tomcat 7.0版本。安装完通过浏览器可以观察一下首页。
# yum install tomcat tomcat-admin-webapps tomcat-webapps
# systemctl start tomcat.service
# ss -tanl
LISTEN 0 100 :::8009
LISTEN 0 100 :::8080
采用Apache官网下载,下载8.x.x
# tar xf apache-tomcat-8.5.42.tar.gz -C /usr/local
# cd /usr/local
# ln -sv apache-tomcat-8.5.42/ tomcat
"tomcat" -> "apache-tomcat-8.5.42/"
# cd tomcat
# cd bin
# ./catalina.sh --help
# ./catalina.sh version
# ./catalina.sh start
# ss -tanlp
# ./catalina.sh stop
# ./startup.sh
# ./shutdown.sh
useradd -r java 建立系统账号
上例中,启动身份是root,如果使用普通用户启动可以使用
# useradd -r java
# chown -R java.java ./*
# su - java -c '/usr/local/tomcat/bin/catalina.sh start'
# ps -aux | grep tomcat
目录结构

配置文件

组件分类
顶级组件
Server,代表整个Tomcat容器
服务类组件
Service,组织Engine和Connector,里面只能包含一个Engine
连接器组件
Connector,有HTTP、HTTPS、A JP协议的连接器
容器类
Engine、Host、Context都是容器类组件,可以嵌入其它组件,内部配置如何运行应用程序。
内嵌类
可以内嵌到其他组件内,valve、logger、realm、loader、manager等。以logger举例,在不同容器组件内定义。
集群类组件
listener、cluster
Tomcat内部组成
由上述组件就构成了Tomcat,如下图
一个Server可包含多个Service,一个Engine和多个Connector组合在一起,就是一个service。多个sevice中,Engine里的Connector中所使用的端口不能重复。每个Engine里可包含多个Host,每个Host包含多个Context,每个Context包含一个Web Application。

Apache的AJP Connecto接收客户端的二进制数据,比HTTP Connector接收字符串传输要效率高,通常情况下HTTPS Connector基本不使用。

A JP(Apache Jserv protocol)是一种基于TCP的二进制通讯协议。
核心组件
- Tomcat启动一个Server进程。可以启动多个Server,但一般只启动一个
- 创建一个Service提供服务。可以创建多个Service,但一般也只创建一个
- 每个Service中,是Engine和其连接器Connector的关联配置
- 可以为这个Server提供多个连接器Connector,这些Connector使用了不同的协议,绑定了不同的端口。其作用就是处理来自客户端的不同的连接请求或响应
- Service内部还定义了Engine,引擎才是真正的处理请求的入口,其内部定义多个虚拟主机Host
- Engine对请求头做了分析,将请求发送给相应的虚拟主机
- 如果没有匹配,数据就发往Engine上的defaultHost缺省虚拟主机
- Engine上的缺省虚拟主机可以修改
- Host定义虚拟主机,虚拟主机有name名称,通过名称匹配
- Context定义应用程序单独的路径映射和配置
<?xml version="1.0" encoding="UTF-8"?>
<Server port="8005" shutdown="SHUTDOWN">
<Service name="Catalina">
<Connector port="8080" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443" />
<Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />
<Engine name="Catalina" defaultHost="localhost">
<Host name="localhost" appBase="webapps"
unpackWARs="true" autoDeploy="true">
</Host>
</Engine>
</Service>
</Server>
举例说明:
- 假设来自客户的请求为:http://localhost:8080/test/index.jsp
- 浏览器端的请求被发送到服务端端口8080,Tomcat进程监听在此端口上。通过侦听的HTTP/1.1 Connector获得此请求。
- Connector把该请求交给它所在的Service的Engine来处理,并等待Engine的响应
- Engine获得请求localhost:8080/test/index.jsp,匹配它所有虚拟主机Host。
- Engine匹配到名为localhost的Host。即使匹配不到也把请求交给该Host处理,因为该Host被定义为该Engine的默认主机
- localhost Host获得请求/test/index.jsp,匹配它所拥有的所有Context
- Host匹配到路径为/test的Context
- path=/test的Context获得请求/index.jsp,在它的mapping table中寻找对应的servlet
- Context匹配到URL PATTERN为*.jsp 的servlet,对应于JspServlet类构造HttpServletRequest对象和
- HttpServletResponse对象,作为参数调用JspServlet的doGet或doPost方法。
- Context把执行完了之后的HttpServletResponse对象返回给Host
- Host把HttpServletResponse对象返回给Engine
- Engine把HttpServletResponse对象返回给Connector
- Connector把HttpServletResponse对象返回给浏览器端
应用部署
根目录
Tomcat中默认网站根目录是CATALINA_BASE/webapps/
在Tomcat中部署主站应用程序和其他应用程序,和之前WEB服务程序不同。
nginx
假设在nginx中部署2个网站应用myshop、bbs,假设网站根目录是/var/www/html,那么部署可以是这样的。
myshop解压缩所有文件放到/var/www/html/目录下。
bbs的文件放在/var/www/html/bbs下。
Tomcat
Tomcat中默认网站根目录是CATALINA_BASE/webapps/
在Tomcat的webapps目录中,有个非常特殊的目录ROOT,它就是网站默认根目录。
将eshop解压后的文件放到这个ROOT中。
bbs解压后文件都放在CATALINA_BASE/webapps/bbs目录下。
每一个虚拟主机的目录都可以使用appBase配置自己的站点目录,里面都可以使用ROOT目录作为主站目录。
JSP WebApp目录结构
主页配置:一般指定为index.jsp或index.html
WEB-INF/:当前WebApp的私有资源路径,通常存储当前应用使用的web.xml和context.xml配置文件
META-INF/:类似于WEB-INF
classes/:类文件,当前webapp需要的类
lib/:当前应用依赖的jar包
实验
默认情况下,/usr/local/tomcat/webapps/ROOT/下添加一个index.html文件,观察访问到了什么?
将/usr/local/tomcat/conf/web.xml中的下面
每个独立的app都可以有自己的配置文件,不配置,则使用缺省配置文件。
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version="3.1"
metadata-complete="true">
<display-name>Welcome to Tomcat</display-name>
<description>
Welcome to Tomcat
</description>
<welcome-file-list>
<welcome-file>index.jsp</welcome-file>
<welcome-file>index.htm</welcome-file>
<welcome-file>index.html</welcome-file>
</welcome-file-list>
</web-app>
配置修改后,观察首页变化
webapp归档格式
.war:WebApp打包
.jar:EJB类打包文件
.rar:资源适配器类打包文件
.ear:企业级WebApp打包
传统,应用开发测试后,通常打包为war格式,这种文件部署到了Tomcat的webapps下,还可以自动展开。
<Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="true">
部署Deploy
部署:将webapp的源文件放置到目标目录,通过web.xml和context.xml文件中配置的路径就可以访问该webapp,通过类加载器加载其特有的类和依赖的类到JVM上。
– 自动部署Auto Deploy:Tomcat发现多了这个应用就把它加载并启动起来
– 手动部署
– 冷部署:将webapp放到指定目录,才去启动Tomcat
– 热部署:Tomcat服务不停止,需要依赖工具manager、ant脚本、tcd(tomcat client deployer)等
反部署undeploy:停止webapp的运行,并从JVM上清除已经加载的类,从Tomcat实例上卸载掉webapp
启动start:是webapp能够访问
停止stop:webapp不能访问,不能提供服务,但是JVM并不清除它
实验部分:
1、添加一个文件,test.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>jsp例子</title>
</head>
<body>
后面的内容是服务器端动态生成字符串,最后拼接在一起
<%
out.println("hello jsp");
%>
</body>
</html>
先把test.jsp放到ROOT下去,试试看,访问http://YourIP:8080/test.jsp 。
立即可以看到,这是通过路径映射找到相应的test.jsp后,转换成test_jsp.java,在编译成test_jsp.class。
cat /usr/local/tomcat/work/Catalina/localhost/ROOT/org/apache/jsp/test_jsp.java转换后的文件。

添加一个应用
模拟部署一个应用myapp
# cd
常见开发项目目录组成
[Sun Jun 30 23:06
root@Centos7 ~]$ mkdir /projects/myapp/{WEN-INF,META-INF,classes,lib} -pv
mkdir: created directory ‘/projects’
mkdir: created directory ‘/projects/myapp’
mkdir: created directory ‘/projects/myapp/WEN-INF’
mkdir: created directory ‘/projects/myapp/META-INF’
mkdir: created directory ‘/projects/myapp/classes’
mkdir: created directory ‘/projects/myapp/lib’
手动复制项目目录到webapps目录下去
# cp -r /projects/myapp/ /usr/local/tomcat/webapps/
使用http://172.16.36.102:8080/myapp/访问试试看

配置详解
server.xml
<?xml version="1.0" encoding="UTF-8"?>
<Server port="8005" shutdown="SHUTDOWN">
<Service name="Catalina">
<Connector port="8080" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443" />
<Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />
<Engine name="Catalina" defaultHost="localhost">
<Host name="localhost" appBase="webapps"
unpackWARs="true" autoDeploy="true">
</Host>
</Engine>
</Service>
</Server>
<Server port="8005" shutdown="SHUTDOWN">
8005是Tomcat的管理端口,默认监听在127.0.0.1上。SHUTDOWN这个字符串接收到后就会关闭此Server。
# telnet 127.0.0.1 8005
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
SHUTDOWN
这个管理功能建议禁用,改shutdown为一串猜不出的字符串。
<Server port="8005" shutdown="44ba3c71d57f494992641b258b965f28">
<GlobalNamingResources>
<!-- Editable user database that can also be used by
UserDatabaseRealm to authenticate users
-->
<Resource name="UserDatabase" auth="Container"
type="org.apache.catalina.UserDatabase"
description="User database that can be updated and saved"
factory="org.apache.catalina.users.MemoryUserDatabaseFactory"
pathname="conf/tomcat-users.xml" />
</GlobalNamingResources>
用户认证,配置文件是conf/tomcat-users.xml。
打开tomcat-users.xml,我们需要一个角色manager-gui。
<tomcat-users xmlns="http://tomcat.apache.org/xml"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://tomcat.apache.org/xml tomcat-users.xsd"
version="1.0">
<role rolename="manager-gui"/>
<user username="wayne" password="wayne" roles="manager-gui"/>
</tomcat-users>
Tomcat启动加载后,这些内容是常驻内存的。如果配置了新的用户,需要重启Tomcat。
访问manager的时候告诉403,提示中告诉去manager的context.xml中修改

文件路径/usr/local/tomcat/webapps/manager/META-INF/context.xml
<Context antiResourceLocking="false" privileged="true" >
<Valve className="org.apache.catalina.valves.RemoteAddrValve"
allow="127\.\d+\.\d+\.\d+|::1|0:0:0:0:0:0:0:1|172\.\d+\.\d+.\d+" />
<Manager sessionAttributeValueClassNameFilter="java\.lang\.(?:Boolean|Integer|Long|Number|String)|org\.apache\.catalina\.filters\.CsrfPrevention
Filter\$LruCache(?:\$1)?|java\.util\.(?:Linked)?HashMap"/>
</Context>
看正则表达式就知道是本地访问了,由于当前访问地址是172.16.x.x,可以修改正则为
allow="127\.\d+\.\d+\.\d+|::1|0:0:0:0:0:0:0:1|172\.16\.\d+.\d+"
再次测试,输入上面认证用户登陆成功。
<Service name="Catalina">
一般情况下,一个Server实例配置一个Service,name属性相当于该Service的ID。
<Connector port="8080" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443" />
连接器配置。
redirectPort,如果访问HTTPS协议,自动转向这个连接器。但大多数时候,Tomcat并不会开启HTTPS,因为Tomcat往往部署在内部,HTTPS性能较差。
<Engine name="Catalina" defaultHost="localhost">
引擎配置。
defaultHost指向内部定义某虚拟主机。缺省虚拟主机可以改动,默认localhost。
<Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="true">
虚拟主机配置。
name必须是主机名,用主机名来匹配。
appBase,当期主机的网页根目录,相对于CATALINA_HOME,也可以使用绝对路径
unpackWARs是否自动解压war格式
autoDeploy 热部署,自动加载并运行应用
虚拟主机配置实验
尝试再配置一个虚拟主机,并将myapp部署到/data/webapps目录下
<Host name="node1.magedu.com" appBase="/data/webapps/" unpackWARs="True" autoDeploy="false" />
常见虚拟主机根目录
# mkdir /data/webapps -pv
mkdir: created directory "/data"
mkdir: created directory "/data/webapps"
# cp -r ~/projects/myapp/ /data/webapps/ROOT
# pwd
/usr/local/tomcat
# bin/shutdown.sh
# bin/startup.sh
web页面热部署/data/webapps/ROOT。右侧有start,stop,reload,undeploy,其中如果只是停止的话,缓存数据不会清除,但通常情况下也不要reload,如果是一个单体架构的站,那重新读取可能需要一定的时间,反部署通常也不使用。so 没事别点任何按钮。

访问测试:

Context配置
可用于版本更新,如myappv1,myappv2,针对不同版本设置虚拟访问路径映射。
Context作用:
- 路径映射
- 应用独立配置,例如单独配置应用日志、单独配置应用访问控制
<Context path="/test" docBase="/data/test" reloadable="" />
path指的是访问的路径
docBase,可以是绝对路径,也可以是相对路径(相对于Host的appBase)
reloadable,true表示如果WEB-INF/classes或META-INF/lib目录下.class文件有改动,就会将WEB应用重新加载。
生成环境中,会使用false来禁用。
将/projects/myapp/下面的项目文件复制到/data/下
# cp -r ~/projects/myapp /data/myappv1
# cd /data
# # ln -sv myappv1 test
可以修改一下index.jsp好区别一下。
Tomcat的配置文件server.xml中修改如下
<Host name="www.martinhe.com" appBase="/data/webapps"
unpackWARs="true" autoDeploy="false">
<Context path="/test" docBase="/data/test" reloadable="" />
</Host>
使用http://www.martinhe.com:8080/test/
注意:这里特别使用了软链接,原因就是以后版本升级,需要将软链接指向myappv2,重启Tomcat。如果新版上线后,出现问题,重新修改软链接到上一个版本的目录,并重启,就可以实现回滚。
常见部署方式:
- stanalone 单机tomcat部署
- 单机(nginx|httpd)–HTTP|AJP–》反向代理+tomcat部署
- 反向代理多机tomcat部署
- 反向代理多机多级tomcat部署
针对上述部署方式的一些说明:
动静分离,动态资源静态化(脚本动态资源生成静态页面文件),伪静态(看似静态页面,但其实是链接到动态资源去了),现在很多网站设计时,使用去session化。
1. standalone模式,Tomcat单独运行,直接接受用户的请求,不推荐。
1. 反向代理,单机运行,提供了一个Nginx作为反向代理,可以做到静态有nginx提供响应,动态jsp代理给Tomcat
1. LNMT:Linux + Nginx + MySQL + Tomcat
2. LAMT:Linux + Apache(Httpd)+ MySQL + Tomcat
1. 前置一台Nginx,给多台Tomcat实例做反向代理和负载均衡调度,Tomcat上部署的纯动态页面更适合
1. LNMT:Linux + Nginx + MySQL + Tomcat
1. 多级代理
1. LNNMT:Linux + Nginx + Nginx + MySQL + Tomcat
Nginx和Tomcat操作演练
nginx安装:
从epel源安装nginx
# wget -O /etc/yum.repos.d/epel.repo http://mirrors.aliyun.com/repo/epel-7.repo
# yum install nginx -y
# cd /etc/nginx
# vim nginx.conf
# nginx -t
全部反向代理测试
需要两台服务器,一台安装Nginx,另一台安装tomcat。
Nginx配置文件:
server_name t0.martinhe.com;
location / {
proxy_pass http://www.martinhe.com:8080;
}
tomcat配置文件:
<Engine name="Catalina" defaultHost="localhost">
<Host name="www.martinhe.com" appBase="/data/webapps"
unpackWARs="true" autoDeploy="false">
</Host>
</Engine>
window本地hosts文件,在最后添加一行地址解析;
172.16.36.132 t0.martinhe.com
测试访问:
http://172.16.36.132/
http://t0.martinhe.com/

动静分离代理:
Nginx配置文件:
server_name t0.martinhe.com;
location / {
root /data/htdocs; #nginx本机提供静态页面服务
index index.html;
}
location ~* \.(jsp|do)$ {
proxy_pass http://www.martinhe.com:8080; #动态页面发送给tomcat处理
}
/data/htdocs目录下增加一个index.html。
[root@haproxy1 ~]# cat /data/htdocs/index.html
<h1>/data/htdocs/<font color=#FF0000>index.html</font></h1>
测试访问:


但是实际上Tomcat不太适合做动静分离,它的管理程序的图片不好做动静分离部署,若动静分离后,显示效果只剩下文字。
应用管理
全部反向代理
location / {
proxy_pass http://172.16.36.102:8080; # 不管什么请求,都会访问后面的tomcat虚拟主机
}

点击Tomcat首页的右上角的“Manager App”按钮,弹出登录对话框。
管理界面
Applications 应用程序管理,可以启动、停止、重加载、反部署、清理过期session
Deploy 可以热部署,也可以部署war文件。
Host Manager虚拟主机管理
配置如下
[Wed Jul 03 15:00
root@Centos7 ~]$ vim /usr/local/tomcat/conf/tomcat-users.xml
<tomcat-users xmlns="http://tomcat.apache.org/xml"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://tomcat.apache.org/xml tomcat-users.xsd"
version="1.0">
<role rolename="manager-gui"/>
<role rolename="admin-gui"/>
<user username="martinhe" password="martinhe" roles="manager-gui,admin-gui"/>
</tomcat-users>
重启Tomcat,点击“Host Manager”按钮,可以新增虚拟主机。
httpd和Tomcat实践
# yum install httpd -y
# httpd -M
# httpd -M | grep proxy
proxy_module (shared)
proxy_ajp_module (shared)
proxy_balancer_module (shared)
proxy_http_module (shared)
****httpd配置****
proxy_http_module模块代理配置
# vim /etc/httpd/conf.d/vhosts.conf
<VirtualHost *:80>
ServerName www.martinhe.com
ProxyRequests Off #Off关闭正向代理。
ProxyVia On #代理的请求响应时提供一个response的via首部
ProxyPreserveHost On #On开启。让代理保留原请求的Host首部
ProxyPass / http://172.16.36.102:8080/ #反向代理指令
ProxyPassReverse / http://172.16.36.102:8080/ #保留代理的response头不重写(个别除外)
</VirtualHost>
测试访问:
http://172.16.36.132/ #显示tomcat默认首页
http://www.martinhe.com/ #显示自定义index.html
– This is 172.16.36.102:/data/webapps/ROOT/index.html
http://www.martinhe.com/index.jsp #显示自定义页面index.jsp
– 后面的内容是服务器端动态生成字符串,最后拼接在一起 Hello! Welcom to myapp .jsp
访问到的页面显示不相同,说明ProxyPreserveHost On起了作用。
当配置ProxyPreserveHost off时,通过域名访问时,仅能访问到tomcat默认首页。可通过修改tomcat配置文件,<Engine name="Catalina" defaultHost="www.martinhe.com">,通过IP或者域名访问就可以转发到自己设置的webapps目录里的ROOT内网页。
proxy_ajp_module模块代理配置
<VirtualHost *:80>
ServerName www.martinhe.com
ProxyRequests Off
ProxyVia On
ProxyPreserveHost On
ProxyPass / ajp://172.16.36.102:8009/
</VirtualHost>
查看Server Status可以看到确实使用的是ajp连接了。

负载均衡
动态服务器的瓶颈往往并发能力太弱,往往需要多台动态服务器同时提供服务,将并发分摊到每台动态服务器,就需要调度,采用适当的调度策略和算法,This is called LB(Load balance)负载均衡。
当单机tomcat服务能力不够时,出现后续的多机多级部署,凸显出的问题就是session。由于淡出设计HTTP协议未能考虑到未来的发展。
HTTP的无状态,有连接和短连接
- 无连接:即同一个浏览器间隔三秒访问同一个网站,服务器提供端不会记录用户是否访问过。后来通过cookie和session机制来判断。
- 浏览器端第一次HTTP请求服务器端时,在服务器端使用session这种技术,就可以在服务器端产生一个随机值即SessionID发给浏览器端,浏览器端收到后会保持这个SessionID在Cookie当中,这个Cookie值不能持久存储,浏览器关闭就消失。浏览器在每一次提交HTTP请求的时候会把这个SessionID传给服务器端,服务器端就可以通过比对知道是谁了
- Session通常会保存在服务器端内存中,如果没有持久化,则易丢失
- Session会定时过期。过期后浏览器如果再访问,服务端发现没有此ID,将重新获得新的SessionID
- 更换浏览器也将重新获得新的SessionID
- 有连接:是因为它基于TCP协议,是面向连接的,需要3次握手、4次断开。即TCP三次握手四次挥手。
- 短连接:Http 1.1之前,都是一个请求一个连接,而Tcp的连接创建销毁成本高,对服务器有很大的影响。所以,自Http 1.1开始,支持keep-alive,默认也开启,一个连接打开后,会保持一段时间(可设置),浏览器再访问该服务器就使用这个Tcp连接,减轻了服务器压力,提高了效率。
服务器端如果故障,即使Session被持久化了,但是服务没有恢复前都不能使用这些SessionID。
如果使用HAProxy或者Nginx等做负载均衡器,调度到了不同的Tomcat上,那么也会出现找不到SessionID的情况。
会话保持方式
1. session sticky会话黏性
Session绑定
—–nginx:source ip
—–HAProxy:cookie
优点:简单易配置
缺点:如果目标服务器故障后,如果没有做sessoin持久化,就会丢失session
2. session复制集群
Tomcat自己的提供的多播集群,通过多播将任何一台的session同步到其它节点。
缺点:
Tomcat的同步节点不宜过多,互相及时通信同步session需要太多带宽
每一台都拥有全部session,内存损耗太多
3. session server
session 共享服务器,使用memcached、redis做共享的Session服务器。
规划设计:
| IP地址———– | 主机名—- | 服务——- | 安装包———– |
|---|---|---|---|
| 172.16.36.132 | haproxy1 | 调度器— | Nginx,HTTPD |
| 172.16.36.102 | Centos7– | tomcat1 | JDK8,TOMCAT8 |
| 172.16.36.112 | node2—- | tomcat2 | JDK8,TOMCAT8 |
每台主机的域名解析
172.16.36.132 t0.martinhe.com t0
172.16.36.102 t1.martinhe.com t1
172.16.36.112 t2.martinhe.com t2
环境变量配置
# vim /etc/profile.d/tomcat.sh
export CATALINA_HOME=/usr/local/tomcat
export PATH=$CATALINA_HOME/bin:$PATH
项目路径配置
# mkdir -pv /data/webapps/ROOT
编写测试jsp文件,内容在下面
# vim /data/webapps/ROOT/index.jsp
# scp -r server.xml 172.16.36.112:/usr/local/tomcat/conf
启动Tomcat服务
# startup.sh
测试用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>
t1虚拟主机配置
<Engine name="Catalina" defaultHost="t1.martinhe.com">
<Host name="t1.martinhe.com" appBase="/data/webapps" autoDeploy="true" />
</Engine>
t2虚拟主机配置
<Engine name="Catalina" defaultHost="t2.martinhe.com">
<Host name="t2.martinhe.com" appBase="/data/webapps" autoDeploy="true" />
</Engine>
Nginx调度
nginx配置如下
vim /etc/nginx/conf.d/upstream.sh
upstream tomcats {
#ip_hash; # 先禁用看看轮询,之后开启开黏性
server t1.martinhe.com:8080;
server t2.martinhe.com:8080;
}
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name t0.martinhe.com;
root /usr/share/nginx/html;
location ~* \.(jsp|do)$ {
proxy_pass http://tomcats;
}
}
测试http://t0.martinhe.com/index.jsp,可以看到轮询调度效果。

刷新后

在upstream中使用ip_hash指令,使用客户端IP地址Hash。这个hash值使用IPv4地址的前24位或全部的IPv6地址。
配置完reload nginx服务。测试一下看看效果。关闭Session对应的Tomcat服务,再重启启动它,看看Session的变化。

关闭112这台服务器tomcat服务后,可看到自动切换到102服务器,sessionID变更:

重新开启112这台服务器tomcat服务后,可看到又自动切换到112服务器,sessionID变更:

Httpd调度

关闭httpd默认主机
# cd /etc/httpd/conf
# vim httpd.conf
注释 #DocumentRoot "/var/www/html"
# cd ../conf.d
# vim vhosts.conf
# httpd -t
# systemctl start httpd
负载均衡配置说明
配置代理到balancer
ProxyPass [path] !|url [key=value [key=value ...]]
Balancer成员
BalancerMember [balancerurl] url [key=value [key=value ...]]
设置Balancer或参数
ProxySet url key=value [key=value ...]
ProxyPass和BalancerMember指令参数

Balancer参数

RroxySet指令也可以使用上面的参数。
在tomcat的配置中Engine使用jvmRoute属性,可便于观察调度。
t1、t2的tomcat配置中分别增加jvmRoute
<Engine name="Catalina" defaultHost="t1.magedu.com" jvmRoute="Tomcat1">
<Engine name="Catalina" defaultHost="t2.magedu.com" jvmRoute="Tomcat2">
conf.d/vhosts.conf内容如下
[root@haproxy1 ~]# vim /etc/httpd/conf.d/vhosts.conf
<VirtualHost *:80>
ServerName t0.martinhe.com
ProxyRequests Off
ProxyVia On
ProxyPreserveHost On
ProxyPass / balancer://lbtomcats/
ProxyPassReverse / balancer://lbtomcats/
</VirtualHost>
<Proxy balancer://lbtomcats>
BalancerMember http://t1.martinhe.com:8080 loadfactor=1
BalancerMember http://t2.martinhe.com:8080 loadfactor=2
</Proxy>
loadfactor设置为1:2,便于观察。观察调度的结果是轮询的,轮询的不是很规律。
使用session黏性
修改conf.d/vhosts.conf
Header add Set-Cookie "ROUTEID=.%{BALANCER_WORKER_ROUTE}e; path=/" env=BALANCER_ROUTE_CHANGED
<VirtualHost *:80>
ServerName t0.martinhe.com
ProxyRequests Off
ProxyVia On
ProxyPreserveHost On
ProxyPass / balancer://lbtomcats/
ProxyPassReverse / balancer://lbtomcats/
</VirtualHost>
<Proxy balancer://lbtomcats>
BalancerMember http://t1.martinhe.com:8080 loadfactor=1 route=Tomcat1
BalancerMember http://t2.martinhe.com:8080 loadfactor=2 route=Tomcat2
ProxySet stickysession=ROUTEID
</Proxy>
多次刷新浏览器,发现Session不变了,一直找的同一个Tomcat服务器。

ajp调度
修改conf.d/vhosts.conf,注意ajp修改对应端口
[root@haproxy1 ~]# vim /etc/httpd/conf.d/vhosts.conf
<VirtualHost *:80>
ServerName t0.martinhe.com
ProxyRequests Off
ProxyVia On
ProxyPreserveHost On
ProxyPass / balancer://lbtomcats/
ProxyPassReverse / balancer://lbtomcats/
</VirtualHost>
<Proxy balancer://lbtomcats>
BalancerMember ajp://t1.martinhe.com:8009 loadfactor=1 route=Tomcat1
BalancerMember ajp://t2.martinhe.com:8009 loadfactor=2 route=Tomcat2
ProxySet stickysession=ROUTEID
</Proxy>
ProxySet stickysession=ROUTEID 开启后,发现Session不变了,一直找的同一个Tomcat服务器。

虽然,上面的做法实现客户端在一段时间内找同一台Tomcat,从而避免切换后导致的Session丢失。但是如果Tomcat节点挂掉,那么Session依旧丢失。
假设有A、B两个节点,都将Session持久化。如果Tomcat A服务下线期间用户切换到了Tomcat B上,就获得了Tomcat B的Session,就算持久化Session的Tomcat A上线了,也没用了。
Tomcat Session集群
参考:https://tomcat.apache.org/tomcat-8.5-doc/cluster-howto.html
<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="230.100.100.8"
port="45564"
frequency="500"
dropTime="3000"/>
<Receiver className="org.apache.catalina.tribes.transport.nio.NioReceiver"
address="192.168.1.7" #此处写有路由出口的IP地址,佛则集群不成功
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>
配置说明
- Cluster 集群配置
- Manager 会话管理器配置
- Channel 信道配置
- Membership 成员判定。使用什么多播地址、端口多少、间隔时长ms、超时时长ms。同一个多播地址和端口认为同属一个组。使用时修改这个多播地址,以防冲突
- Receiver 接收器,多线程接收多个其他节点的心跳、会话信息。默认会从4000到4100依次尝试可用端口。
- address=”auto”,auto可能绑定到127.0.0.1上,所以一定要改为可以用的IP上去
- Sender 多线程发送器,内部使用了tcp连接池。
- Interceptor 拦截器
- Valve
- ReplicationValve 检测哪些请求需要检测Session,Session数据是否有了变化,需要启动复制过程
- ClusterListener
- ClusterSessionListener 集群session侦听器
使用
添加到
添加到
最后,在应用程序内部启用了才可以使用
前提:
- 时间同步,确保NTP或Chrony服务正常运行。# systemctl status chronyd
- 防火墙规则。# systemctl stop firewalld
规划设计同上述实验:
| IP地址———– | 主机名—- | 服务——- | 安装包———– |
|---|---|---|---|
| 172.16.36.132 | haproxy1 | 调度器— | Nginx,HTTPD |
| 172.16.36.102 | Centos7– | tomcat1 | JDK8,TOMCAT8 |
| 172.16.36.112 | node2—- | tomcat2 | JDK8,TOMCAT8 |
本次把多播复制的配置放到缺省虚拟主机里面, 即Host之下。
特别注意修改Receiver的address属性为一个本机可对外的IP地址。
t1的server.xml中,如下
<Host name="t1.martinhe.com" appBase="/data/webapps"
unpackWARs="true" autoDeploy="false">
<Receiver className="org.apache.catalina.tribes.transport.nio.NioReceiver"
address="192.168.1.7"
port="4000"
autoBind="100"
selectorTimeout="5000"
maxThreads="6"/>
</Host>
t2的server.xml中,如下
<Host name="t2.martinhe.com" appBase="/data/webapps"
unpackWARs="true" autoDeploy="false">
<Receiver className="org.apache.catalina.tribes.transport.nio.NioReceiver"
address="192.168.1.6"
port="4000"
autoBind="100"
selectorTimeout="5000"
maxThreads="6"/>
</Host>
Tomcat重启后,ss命令能看到tomcat监听在4000端口上

尝试使用刚才配置过得负载均衡(移除Session黏性),测试发现Session还是变来变去。
准备web.xml
在应用中增加WEB-INF,从全局复制一个web.xml过来
为web.xml的
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version="3.1"
metadata-complete="true">
<display-name>Welcome to Tomcat</display-name>
<description>
Welcome to Tomcat
</description>
<welcome-file-list>
<welcome-file>index.jsp</welcome-file>
<welcome-file>index.html</welcome-file>
<welcome-file>index.htm</welcome-file>
</welcome-file-list>
#添加如下:
<distributable/>
</web-app>
重启全部Tomcat,通过负载均衡调度到不同节点,返回的SessionID不变了。


NoSQL
NoSQL是对非SQL、非传统关系型数据库的统称。
https://db-engines.com/en/ranking
分类
- Key-value Store
- redis、memcached
- Document Store
- mongodb、CouchDB
- Column Store列存数据库,Column-Oriented DB
- HBase、Cassandra
- Graph DB
- Neo4j
- Time Series 时序数据库
- InfluxDB
Memcached
Memcached只支持能序列化的数据类型,不支持持久化,基于Key-Value的内存缓存系统。
内存分配机制
应用程序运行需要使用内存存储数据,但对于一个缓存系统来说,申请内存、释放内存将十分频繁,非常容易导致大量内存碎片,最后导致无连续可用内存可用。
Memcached采用了Slab Allocator机制来分配、管理内存。
- Page:分配给Slab的内存空间,默认为1MB,分配后就得到一个Slab。Slab分配之后内存按照固定字节大小等分成chunk。
- Chunk:用于缓存记录kv值的内存空间。Memcached会根据数据大小选择存到哪一个chunk中,假设chunk有128bytes、64bytes,数据只有100bytes存储在128bytes中,存在些浪费。可根据存储数据需要自行指定chunk大小。
- Chunk最大就是Page的大小,即一个Page中就一个Chunk,即一个page里只分一个chunk
- Slab Class:Slab按照大小分组,就组成不同的Slab Class,即相同数量的chunk,且chunk大小相同,这样的page联合组成Slab Class
Slab之间的差异可以使用Growth Factor控制,默认1.25。
懒过期Lazy Expiration
memcached不会监视数据是否过期,而是在取数据时才看是否过期,过期的把数据有效期限标识为0,并不清除该数据。以后可以覆盖该位置存储其它数据。
LRU算法机制
当内存不足时,memcached会使用LRU(Least Recently Used)机制来查找可用空间,分配给新纪录使用。
集群
Memcached集群,称为基于客户端的分布式集群。
Memcached集群内部并不互相通信,一切都需要客户端连接到Memcached服务器后自行组织这些节点,并决定数据存储的节点。
安装Memcached
[root@node2-centos7 ~]# yum install memcached
[root@node2-centos7 ~]# 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
[root@node2-centos7 ~]# cat /usr/lib/systemd/system/memcached.service
[Unit]
Description=Memcached
Before=httpd.service
After=network.target
[Service]
Type=simple
EnvironmentFile=-/etc/sysconfig/memcached
ExecStart=/usr/bin/memcached -u $USER -p $PORT -m $CACHESIZE -c $MAXCONN $OPTIONS
[Install]
WantedBy=multi-user.target
[root@node2-centos7 ~]# cat /etc/sysconfig/memcached
PORT="11211"
USER="memcached"
MAXCONN="1024"
CACHESIZE="64"
OPTIONS=""
前台显示看看效果
# memcached -u memcached -p 11211 -f 1.25 -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
。。。
# memcached -u memcached -p 11211 -f 1.5 -vv
slab class 1: chunk size 96 perslab 10922
slab class 2: chunk size 144 perslab 7281
slab class 3: chunk size 216 perslab 4854
slab class 4: chunk size 328 perslab 3196
slab class 5: chunk size 496 perslab 2114
slab class 6: chunk size 744 perslab 1409
slab class 7: chunk size 1120 perslab 936
slab class 8: chunk size 1680 perslab 624
。。。
服务脚本启动
# systemctl start memcached
修改memcached运行参数,可以使用下面的选项修改/etc/sysconfig/memcached文件
- -u username memcached运行的用户身份,必须普通用户
- -p 绑定的端口,默认11211
- -m num 最大内存,单位MB,默认64MB
- -c num 最大连接数,缺省1024
- -d 守护进程方式运行
- -f 增长因子Growth Factor,默认1.25
- -v 详细信息,-vv能看到详细信息
- -M 内存耗尽,不许LRU
- -U 设置UDP监听端口,0表示禁用UDP
[root@node2-centos7 ~]# yum list all | grep memcached
memcached.x86_64 1.4.15-10.el7_3.1 @development
libmemcached.i686 1.0.16-5.el7 development
libmemcached.x86_64 1.0.16-5.el7 development
libmemcached-devel.i686 1.0.16-5.el7 development
libmemcached-devel.x86_64 1.0.16-5.el7 development
memcached-devel.i686 1.4.15-10.el7_3.1 development
memcached-devel.x86_64 1.4.15-10.el7_3.1 development
opensips-memcached.x86_64 1.10.5-4.el7 epelaliyun
php-ZendFramework-Cache-Backend-Libmemcached.noarch
php-pecl-memcached.x86_64 2.2.0-1.el7 epelaliyun
python-memcached.noarch 1.48-4.el7 development
uwsgi-router-memcached.x86_64 2.0.17.1-2.el7 epelaliyun
与memcached通信的不同语言的连接器。
libmemcached提供了C库和命令行工具。
协议
查看/usr/share/doc/memcached-1.4.15/protocol.txt
# yum install telnet
# telnet 172.16.36.102 11211
stats
add mykey 1 60 4 #flag1 存活60s 4个字节
test
STORED #存储成功
get mykey #获取mykey内容
VALUE mykey 1 4
test
END
set mykey 1 60 5
test1
STORED
get mykey
VALUE mykey 1 5
test1
END
session共享服务器
msm
msm(memcached session manager)提供将Tomcat的session保持到memcached或redis的程序,可以实现高可用。
项目托管在Github,https://github.com/magro/memcached-session-manager
支持Tomcat的6.x、7.x、8.x、9.x。
- Tomcat的Session管理类,Tomcat版本不同
- memcached-session-manager-2.3.2.jar
- memcached-session-manager-tc8-2.3.2.jar
- Session数据的序列化、反序列化类:序列化即C—》S,打包按字节发送,反序列化,将字节还原值
- 官方推荐kyro
- 在webapp中WEB-INF/lib/下
- 驱动类
- memcached(spymemcached.jar)
- Redis(jedis.jar)
安装
https://github.com/magro/memcached-session-manager/wiki/SetupAndConfiguration
将spymemcached.jar、memcached-session-manage、kyro相关的jar文件都放到Tomcat的lib目录中去,这个目录是$CATALINA_HOME/lib/ ,对应本次安装就是/usr/local/tomcat/lib。
asm-5.2.jar
kryo-3.0.3.jar
kryo-serializers-0.45.jar
memcached-session-manager-2.3.2.jar
memcached-session-manager-tc8-2.3.2.jar
minlog-1.3.1.jar
msm-kryo-serializer-2.3.2.jar
objenesis-2.6.jar
reflectasm-1.11.9.jar
spymemcached-2.12.3.jar
jedis-3.0.0.jar #如果存储使用redis,则需要此项

sticky模式:企业使用,必须掌握
原理
当请求结束时Tomcat的session会送给memcached备份。即Tomcat session为主session,memcached session为备session,使用memcached相当于备份了一份Session。
查询Session时Tomcat会优先使用自己内存的Session,Tomcat通过jvmRoute发现不是自己的Session,便从memcached中找到该Session,更新本机Session,请求完成后更新memcached。
部署
t1和m1部署在一台主机上,t2和m2部署在同一台。

配置
放到 $CATALINA_HOME/conf/context.xml 中
特别注意,t1配置中为failoverNodes=”n1″, t2配置为failoverNodes=”n2″
以下是sticky的配置
<Context>
...
<Manager className="de.javakaffee.web.msm.MemcachedBackupSessionManager"
memcachedNodes="n1:t1.martinhe.com:11211,n2:t2.martinhe.com:11211"
failoverNodes="n1"
requestUriIgnorePattern=".*\.(ico|png|gif|jpg|css|js)$"
transcoderFactoryClass="de.javakaffee.web.msm.serializer.kryo.KryoTranscoderFactory"
/>
</Context>
memcachedNodes="n1:t1.martinhe.com:11211,n2:t2.martinhe.com:11211"
memcached的节点们;n1、n2只是别名,可以重新命名。
failoverNodes故障转移节点,n1是备用节点,n2是主存储节点。另一台Tomcat将n1改为n2,其主节点是n1,备用节点是n2。
实验
如果配置成功,可以在logs/catalina.out中看到下面的内容
[Thu Jul 04 12:43
root@Centos7 /usr/local/tomcat]$ tail logs/catalina.out
- operation timeout: 1000
- node ids: [n2]
- failover node ids: [n1]
- storage key prefix: null
- locking mode: null (expiration: 5s)
.....
配置成功后,网页访问以下,页面中看到了Session。然后运行下面的Python程序,就可以看到是否存储到了memcached中了。
import memcache # pip3 install python3-memcached
mc = memcache.Client([
'172.16.36.102:11211',
'172.16.36.112:11211'
],debug=True)
stats = mc.get_stats()[0]
print(stats)
for k,v in stats[1].items():
print(k,v)
print('-' * 30)
#查看全部key
print(mc.get_stats('items')) # stats items 返回items:5:number 1
print('-' * 30)
print(mc.get_stats('cachedump 5 0')) #stats cachedump 5 0 # 5 和 上的items返回的值有关;0表示全部
t1、t2、n1、n2依次启动成功,分别使用http://t1.magedu.com:8080/ 和http://t2.magedu.com:8080/ 观察。
看起负载均衡调度器,通过http://t0.magedu.com来访问看看效果
可以看到浏览器端被调度到不同Tomcat上,但是都获得了同样的SessionID。


non-sticky模式
原理
从msm 1.4.0之后开始支持non-sticky模式。
Tomcat session为中转Session,n1为主session,n2为备session。产生的新的Session会发送给主、备memcached,并清除本地Session。
n1下线,n2转正。n1再次上线,n2依然是主Session存储节点。
memcached配置
<Context>
...
<Manager className="de.javakaffee.web.msm.MemcachedBackupSessionManager"
memcachedNodes="n1:t1.martinhe.com:11211,n2:t2.martinhe.com:11211"
sticky="false"
sessionBackupAsync="false"
lockingMode="uriPattern:/path1|/path2"
requestUriIgnorePattern=".*\.(ico|png|gif|jpg|css|js)$"
transcoderFactoryClass="de.javakaffee.web.msm.serializer.kryo.KryoTranscoderFactory"
/>
</Context>
redis配置
下载jedis.jar,放到$CATALINA_HOME/lib/ ,对应本次安装就是/usr/local/tomcat/lib。
# yum install redis
# vim /etc/redis.conf
bind 0.0.0.0
# systemctl start redis
放到 $CATALINA_HOME/conf/context.xml 中
<Context>
...
<Manager className="de.javakaffee.web.msm.MemcachedBackupSessionManager"
memcachedNodes="redis://172.16.36.112:6379"
sticky="false"
sessionBackupAsync="false"
lockingMode="uriPattern:/path1|/path2"
requestUriIgnorePattern=".*\.(ico|png|gif|jpg|css|js)$"
transcoderFactoryClass="de.javakaffee.web.msm.serializer.kryo.KryoTranscoderFactory"
/>
</Context>
总结
通过多组实验,使用不同技术实现了session持久机制
- session绑定,基于IP或session cookie的。其部署简单,尤其基于session黏性的方式,粒度小,对负载均衡影响小。但一旦后端服务器有故障,其上的session丢失。
- session复制集群,基于tomcat实现多个服务器内共享同步所有session。此方法可以保证任意一台后端服务器故障,其余各服务器上还都存有全部session,对业务无影响。但是它基于多播实现心跳,TCP单播实现复制,当设备节点过多,这种复制机制不是很好的解决方案。且并发连接多的时候,单机上的所有session占据的内存空间非常巨大,甚至耗尽内存。
- session服务器,将所有的session存储到一个共享的内存空间中,使用多个冗余节点保存session,这样做到session存储服务器的高可用,且占据业务服务器内存较小。是一种比较好的解决session持久的解决方案。
以上的方法都有其适用性。生产环境中,应根据实际需要合理选择。
不过以上这些方法都是在内存中实现了session的保持,可以使用数据库或者文件系统,把session数据存储起来,持久化。这样服务器重启后,也可以重新恢复session数据。不过session数据是有时效性的,是否需要这样做,视情况而定。