一、Tomcat 基础功能
1.1 JDK
1.1.1 JDK和JRE
Java SE API: Java 基础类库开发接口
JRE:Java Runtime Environment缩写,指Java运行时环境, 包含 JVM + Java核心类库
JDK:Java Development Kit,即 Java 语言的软件开发工具包,JDK协议基于 JRL(JavaResearchLicense)协议
1.1.2 Oracle JDK版本
收费
从2019年1月份开始,Oracle JDK 开始对 Java SE 8 之后的版本开始进行商用收费,确切的说是8u201/202 之后的版本。如果你用 Java 开发的功能如果是用作商业用途的,如果还不想花钱购买的话,能免费使用的最新版本是 8u201/202。当然如果是个人客户端或者个人开发者可以免费试用Oracle JDK 所有的版本。
发版方式
在 JDK 9 发布之前,Oracle 的发版策略是以特性驱动的,只有重大的特性改变才会发布大版本,比如JDK 7 到 JDK 8,中间会发多个更新版本。而从 JDK 9 开始变为以时间驱动的方式。发布周期为6月一个大版本,比如 JDK 9 到 JDK 10,3个月一次补丁版,3年一个 LTS(长期支持版本)。
1.1.3 OpenJDK
OpenJDK是Sun公司采用GPL v2协议发布的JDK开源版本,于2009年正式发布。
官方网站:https://openjdk.java.net/projects/jdk6/
OpenJDK 7是基于JDK7的beta版开发,但为了也将Java SE 6开源,从OpenJDK7的b20构建反向分支开发,从中剥离了不符合Java SE 6规范的代码,发布OpenJDK 6。所以OpenJDK6和JDK6没什么关系,只是API兼容而已
OpenJDK使用GPL v2可以用于商业用途。目前由红帽维护。OpenJDK也有在其基础上的众多发行版,比如阿里的Dragonwell。
相对来说,Oracle jDK具有更好的响应能力和JVM性能,更加稳定。
1.1.4 二进制安装JDK
#脚本
JDK_FILE='jdk-8u281-linux-x64.tar.gz'
install_dir='/usr/local/'
install_jdk(){
[ -f $JDK_FILE ] || { echo "jdk压缩包不存在";exit; }
[ -h $install_dir/jdk ] && { echo "jdk已经安装";exit; }
tar xvf $JDK_FILE -C $install_dir
jdk_dir=`ls $install_dir | grep 'jdk'`
cd $install_dir
ln -s $jdk_dir jdk
echo "export JAVA_HOME=${install_dir}/jdk" >/etc/profile.d/jdk.sh
echo 'export PATH=$PATH:$JAVA_HOME/bin' >>/etc/profile.d/jdk.sh
echo 'export JRE_HOME=$JAVA_HOME/jre' >>/etc/profile.d/jdk.sh
echo 'export CLASSPATH=$JAVA_HOME/lib/:$JRE_HOME/lib/' >>/etc/profile.d/jdk.sh
}
1.2 安装 Tomcat
1.2.1 基于包安装 Tomcat
1.2.1.1 CentOS 包安装
#CentOS 8 包仓库中目前还没有提供tomcat相关包
[root@centos8 ~]#yum list tomcat
#CentOS 7 yum仓库源中自带的Tomcat 7.0版本安装,此方式安装tomcat版本较低,不推荐
[root@centos7 ~]#yum list tomcat*
#Ubuntu安装 tomcat
[root@ubuntu1804 ~]#apt -y install tomcat8 tomcat8-admin tomcat8-docs
1.2.2 二进制安装 Tomcat
CentOS 7 的yum源的tomcat版本老旧,而CentOS8 yum源里无tomcat
目前比较主流的Tomcat是8.5.X版本,推荐从Apache官网下载二进制tomcat包进行安装,此为生产常用方式
1.2.2.1 下载并安装
注意: 安装tomcat 前必须先部署JDK
官方和镜像站点下载:
https://tomcat.apache.org/download-80.cgi
https://mirrors.tuna.tsinghua.edu.cn/apache/tomcat/
范例:二进制部署脚本
#安装JDK
install_jdk(){
#变量可自定义
JDK_FILE='jdk-8u281-linux-x64.tar.gz'
install_dir='/usr/local/'
[ -d $install_dir ] || mkdir ${install_dir} -pv
[ -f $JDK_FILE ] || { echo "jdk压缩包不存在";exit; }
java -version &>/dev/null && { echo "jdk已经安装";exit; }
tar xf $JDK_FILE -C $install_dir
jdk_dir=`ls $install_dir | grep 'jdk'`
cd $install_dir
ln -s $jdk_dir jdk
echo "export JAVA_HOME=${install_dir}/jdk" >/etc/profile.d/jdk.sh
echo 'export PATH=$PATH:$JAVA_HOME/bin' >>/etc/profile.d/jdk.sh
echo 'export JRE_HOME=$JAVA_HOME/jre' >>/etc/profile.d/jdk.sh
echo 'export CLASSPATH=$JAVA_HOME/lib/:$JRE_HOME/lib/' >>/etc/profile.d/jdk.sh
}
install_tomcat(){
#变量可自定义
tomcat_file='apache-tomcat-8.5.64.tar.gz'
install_dir_to='/usr/local/'
dir=`pwd`
[ -f $tomcat_file ] || { echo "tomcat二进制压缩包不存在";exit; }
[ -d $install_dir_to ] || mkdir -p $install_dir_to
[ -h $install_dir_to/tomcat ] && { echo "tomcat已经安装";exit; }
java -version &>/dev/null || { install_jdk;. /etc/profile.d/jdk.sh; }
cd $dir
tar xf $tomcat_file -C $install_dir_to
tomcat_dir=`ls $install_dir_to | grep tomcat`
cd $install_dir_to
ln -s $tomcat_dir tomcat
echo "PATH=$install_dir_to/tomcat/bin:\$PATH" >/etc/profile.d/tomcat.sh
id tomcat &>/dev/null || useradd -r -s /sbin/nologin tomcat
jdk_dir=`which java | sed -rn 's/(.*)(\/bin\/java)$/\1/p'`
cat >>$install_dir_to/tomcat/conf/tomcat.conf<<EOF
JAVA_HOME=$jdk_dir
JER_HOME=$jdk_dir/jre
EOF
chown -R tomcat.tomcat $install_dir_to/tomcat/
cat >/lib/systemd/system/tomcat.service<<EOF
[Unit]
Description=Tomcat
#After=syslog.target network.target remote-fs.target nss-lookup.target
After=syslog.target network.target
[Service]
Type=forking
EnvironmentFile=$install_dir_to/tomcat/conf/tomcat.conf
ExecStart=$install_dir_to/tomcat/bin/startup.sh
ExecStop=$install_dir_to/tomcat/bin/shutdown.sh
PrivateTmp=true
User=tomcat
Group=tomcat
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
}
install_tomcat
tomcat启动
#启动tomcat
[10:40:45 root@tomcat ~]#startup.sh
#查看端口
[10:41:33 root@tomcat ~]#ss -ntl
State Recv-Q Send-Q Local Address:Port Peer Address:Port
LISTEN 0 128 0.0.0.0:111 0.0.0.0:*
LISTEN 0 128 0.0.0.0:22 0.0.0.0:*
LISTEN 0 1 [::ffff:127.0.0.1]:8005 *:*
LISTEN 0 128 [::]:111 [::]:*
LISTEN 0 100 *:8080 *:*
LISTEN 0 128 [::]:22 [::]:*
#查看进程是以root启动的
[root@centos8 ~]#ps aux|grep tomcat
#关闭tomcat
[10:41:35 root@tomcat ~]#shutdown.sh
#或者以下也可以,指定10s后停止,默认5s
[root@centos8 ~]#catalina.sh stop 10
#再次用不同方式启动tomcat
[root@centos8 ~]#catalina.sh start
#再次用不同方式关闭tomcat
[root@centos8 ~]#catalina.sh stop
打开浏览器访问:http://tomcat:8080/,正常可以看到以下界面
扩展知识:tomcat 和 catalina 关系
Tomcat的servlet容器在4.X版本中被Craig McClanahan(Apache Struts项目的创始人,也是Tomcat的Catalina 的架构师)重新设计为Catalina.即Catalina就是servlet容器。
Tomcat的核心分为3个部分:
(1)Web容器:处理静态页面;
(2)JSP容器:把jsp页面翻译成一般的 servlet
(3)catalina: 是一个servlet容器,用于处理servlet
Catalina是美国西海岸靠近洛杉矶22英里的一个小岛,因为其风景秀丽而著名,曾被评为全美最漂亮的小岛。Servlet运行模块的最早开发者Craig McClanahan因为喜欢Catalina岛,故以Catalina命名他所开发这个模块,另外在开发的早期阶段,Tomcat是被搭建在一个叫Avalon的服务器框架上,而Avalon则是Catalina岛上的一个小镇的名字,于是想一个与小镇名字相关联的单词也是自然而然。设计者估计是想把tomcat设计成最美的轻量级容器吧。下图为该小岛。
1.3 tomcat的文件结构和组成
1.3.1 目录结构
目录 | 说明 |
---|---|
bin | 服务启动、停止等相关程序和文件 |
conf | 配置文件 |
lib | 库目录 |
logs | 日志目录 |
webapps | 应用程序,应用部署目录 |
work | jsp编译后的结果文件,建议提前预热访问 |
1.3.2 配置文件
1.3.2.1 配置文件说明
官方帮助文档:http://tomcat.apache.org/tomcat-8.5-doc/index.html
在tomcat安装目录下的 conf 子目录中,有以下的 tomcat 的配置文件
注意:配置文件大小写敏感
范例:查看配置文件
[10:48:05 root@tomcat conf]#wc -l server.xml web.xml context.xml tomcat-users.xml catalina.policy catalina.properties logging.properties
171 server.xml
4731 web.xml
30 context.xml
44 tomcat-users.xml
260 catalina.policy
214 catalina.properties
75 logging.properties
5525 total
1.3.2.2 日志文件
参考文档: https://cwiki.apache.org/confluence/display/TOMCAT/Logging
日志格式: https://tomcat.apache.org/tomcat-9.0-doc/config/valve.html#Access_Logging
[root@centos8 ~]#ls /usr/local/tomcat/logs/ -l
catalina.2020-07-14.log #tomcat服务日志
catalina.out #tomcat服务日志
host-manager.2020-07-14.log #host manager管理日志
localhost.2020-07-14.log #默认主机日志
localhost_access_log.2020-07-14.txt ##默认主机访问日志
manager.2020-07-14.log #manager 管理日志
范例: tomcat的访问日志格式
[10:51:54 root@tomcat conf]#tail /usr/local/tomcat/conf/server.xml
Documentation at: /docs/config/valve.html
Note: The pattern used is equivalent to using pattern="common" -->
<Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
prefix="localhost_access_log" suffix=".txt"
pattern="%h %l %u %t "%r" %s %b" /> #说明: "在html中表示双引号"符号
</Host>
</Engine>
</Service>
</Server>
#查看访问日志
[10:59:03 root@tomcat conf]#tail -f /usr/local/tomcat/logs/localhost_access_log.2021-03-16.txt
192.168.10.1 - - [16/Mar/2021:10:58:50 +0800] "GET / HTTP/1.1" 200 11156
192.168.10.1 - - [16/Mar/2021:10:58:50 +0800] "GET /favicon.ico HTTP/1.1" 200 21630
192.168.10.1 - - [16/Mar/2021:10:58:50 +0800] "GET / HTTP/1.1" 200 11156
1.3.3 组件
1.3.3.1 组件分层和分类
顶级组件
Server,代表整个Tomcat容器,一台主机可以启动多tomcat实例,需要确保端口不要产生冲突
服务类组件
Service,实现组织Engine和Connector,建立两者之间关联关系, service 里面只能包含一个Engine
连接器组件
Connector,有HTTP(默认端口8080/tcp)、HTTPS(默认端口8443/tcp)、AJP(默认端口8009/tcp)协议的连接器,AJP(Apache Jserv protocol)是一种基于TCP的二进制通讯协议。
容器类
Engine、Host(虚拟主机)、Context(上下文件,解决路径映射)都是容器类组件,可以嵌入其它组件,内部配置如何运行应用程序。
内嵌类
可以内嵌到其他组件内,valve、logger、realm、loader、manager等。以logger举例,在不同容器组件内分别定义。
集群类组件
listener、cluster
1.3.3.2 Tomcat 内部组成
由上述组件就构成了Tomcat,如下图
每一个组件都由一个Java“类”实现,这些组件大体可分为以下几个类型:
顶级组件:Server
服务类组件:Service
连接器组件:http, https, ajp(apache jserv protocol)
容器类:Engine, Host, Context
被嵌套类:valve, logger, realm, loader, manager, ...
集群类组件:listener, cluster, ...
1.3.3.3 核心组件
-
Tomcat启动一个Server进程。可以启动多个Server,即tomcat的多实例, 但一般只启动一个
-
创建一个Service提供服务。可以创建多个Service,但一般也只创建一个
- 每个Service中,是Engine和其连接器Connector的关联配置
- 可以为这个Service提供多个连接器Connector,这些Connector使用了不同的协议,绑定了不同的端口。其作用就是处理来自客户端的不同的连接请求或响应
-
Service 内部还定义了Engine,引擎才是真正的处理请求的入口,其内部定义多个虚拟主机Host
- Engine对请求头做了分析,将请求发送给相应的虚拟主机
- 如果没有匹配,数据就发往Engine上的defaultHost缺省虚拟主机
- Engine上的缺省虚拟主机可以修改
-
Host 定义虚拟主机,虚拟主机有name名称,通过名称匹配
-
Context 定义应用程序单独的路径映射和配置
范例:多个组件关系 conf/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">
<Context >
<Context />
</Host>
</Engine>
</Service>
</Server>
1.3.3.4 tomcat 处理请求过程
假设来自客户的请求为:http://localhost:8080/test/index.jsp
- 浏览器端的请求被发送到服务端端口8080,Tomcat进程监听在此端口上。通过侦听的HTTP/1.1Connector获得此请求。
- Connector把该请求交给它所在的Service的Engine来处理,并等待Engine的响应
- Engine获得请求localhost:8080/test/index.jsp,遍历它所有虚拟主机Host
- Engine匹配到名为localhost的Host。如果匹配不到,就把请求交给该Engine中的defaultHost处理
- 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对象返回给浏览器端
1.4 应用部署
1.4.1 tomcat的根目录结构
Tomcat中默认网站根目录是$CATALINA_BASE/webapps/
在Tomcat中部署主站应用程序和其他应用程序,和之前WEB服务程序不同
nginx
假设在nginx中部署2个网站应用eshop、forum,假设网站根目录是/data/nginx/html,那么部署可以是这样的。
eshop解压缩所有文件放到 /data/nginx/html/ 目录下,forum 的文件放在 /data/nginx/html/forum/ 下。
最终网站链接有以下对应关系
http://localhost/ 对应于eshop的应用,即 /data/nginx/html/
http://localhost/forum/ 对应于forum的应用,即/data/nginx/html/forum/
Tomcat
Tomcat中默认网站根目录是$CATALINA_BASE/webapps/
在Tomcat的webapps目录中,有个非常特殊的目录ROOT,它就是网站默认根目录。
将eshop解压后的文件放到这个$CATALINA_BASE/webapps/ROOT中。
bbs解压后文件都放在$CATALINA_BASE/webapps/forum目录下。
$CATALINA_BASE/webapps下面的每个目录都对应一个Web应用,即WebApp
最终网站链接有以下对应关系
http://localhost/ 对应于eshop的应用WebApp,即$CATALINA_BASE/webapps/ROOT/目录,
http://localhost/forum/ 对应于forum的应用WebApp,即$CATALINA_BASE/webapps/forum/
如果同时存在$CATALINA_BASE /webapps/ROOT/forum ,仍以 $CATALINA_BASE/webapps/forum/优先生效
每一个虚拟主机都可以使用appBase指令配置自己的站点目录,使用appBase目录下的ROOT目录作为主站目录。
范例: 主页目录和编码
[11:16:05 root@tomcat ROOT]#cat /usr/local/tomcat/webapps/ROOT/index.html
<h1>偷得浮生</h1>
[11:16:37 root@tomcat ROOT]#curl 192.168.10.81:8080/index.html -I
HTTP/1.1 200
Accept-Ranges: bytes
ETag: W/"22-1615864565000"
Last-Modified: Tue, 16 Mar 2021 03:16:05 GMT
Content-Type: text/html #tomcat无指定编码,浏览器自动识别为GBK,可能会导致乱码
Content-Length: 22
Date: Tue, 16 Mar 2021 03:16:49 GMT
#httpd服务器默认指定编码为UTF-8,因为服务器本身不会出现乱码
#nginx服务器默认在响应头部没有批定编码,也会出现乱码
#浏览器的设置默认不是UTF-8,可能会导致乱码
#修改网页指定编码
[11:19:46 root@tomcat ROOT]#cat /usr/local/tomcat/webapps/ROOT/index.html
<html>
<head>
<meta http-equiv=Content-Type content="text/html;charset=utf-8">
<title>tomcat</title>
</head>
<h1>偷得浮生</h1>
1.4.2 JSP WebApp目录结构
$CATALINA_BASE/webapps下面的每个目录对应的WebApp,可能有以下子目录,但下面子目录是非必须的
- 主页配置:默认按以下顺序查找主页文件 index.html,index.htm、index.jsp
- WEB-INF/:当前目录WebApp的私有资源路径,通常存储当前应用使用的web.xml和context.xml配置文件
- META-INF/:类似于WEB-INF,也是私有资源的配置信息,和WEB-INF/目录一样浏览器无法访问
- classes/:类文件,当前webapp需要的类
- lib/:当前应用依赖的jar包
1.4.3 主页设置
1.4.3.1 全局配置实现修改默认主页文件
默认情况下 tomcat 会在$CATALINA_BASE/webapps/ROOT/目录下按以下次序查找文件,找到第一个则进行显示
- index.html
- index.htm
- index.jsp
可以通过修改 $CATALINA_BASE/conf/web.xml 中的下面
范例:修改默认主页文件
[11:23:38 root@tomcat tomcat]#tail conf/web.xml
<!-- here, so be sure to include any of the default values that you wish -->
<!-- to use within your application. -->
<welcome-file-list>
<welcome-file>index.html</welcome-file>
<welcome-file>index.htm</welcome-file>
<welcome-file>index.jsp</welcome-file>
</welcome-file-list>
</web-app>
1.4.3.2 WebApp的专用配置文件
将上面主配置文件conf/web.xml中的
范例: 针对主站点根目录设置专用配置文件
[11:23:44 root@tomcat tomcat]#cp conf/web.xml webapps/ROOT/WEB-INF/
[11:25:38 root@tomcat tomcat]#vim webapps/ROOT/WEB-INF/web.xml
<welcome-file>index.jsp</welcome-file> #修改三个文件的顺序
<welcome-file>index.htm</welcome-file>
<welcome-file>index.html</welcome-file>
#配置修改后,无需重启tomcat服务,即可观察首页变化
[11:27:08 root@tomcat tomcat]#curl 192.168.10.81:8080
范例: 针对特定APP目录设置专用配置文件
[11:27:56 root@tomcat webapps]#mkdir zhangzhuo
[11:28:17 root@tomcat webapps]#cp -a ROOT/WEB-INF/ zhangzhuo/
[11:28:35 root@tomcat webapps]#echo /usr/local/tomcat/webapps/zhangzhuo/test.html > zhangzhuo/test.html
[11:30:06 root@tomcat zhangzhuo]#echo /usr/local/tomcat/webapps/zhangzhuo/index.html > index.html
[11:30:27 root@tomcat zhangzhuo]#echo /usr/local/tomcat/webapps/zhangzhuo/index.jps > index.jsp
[11:30:40 root@tomcat zhangzhuo]#echo /usr/local/tomcat/webapps/zhangzhuo/index.htm > index.htm
[11:31:04 root@tomcat zhangzhuo]#tree
.
├── index.htm
├── index.html
├── index.jsp
├── test.html
└── WEB-INF
└── web.xml
[11:33:41 root@tomcat ~]#tail /usr/local/tomcat/webapps/zhangzhuo/WEB-INF/web.xml
<!-- here, so be sure to include any of the default values that you wish -->
<!-- to use within your application. -->
<welcome-file-list>
<welcome-file>index.html</welcome-file>
<welcome-file>index.htm</welcome-file>
<welcome-file>index.jsp</welcome-file>
</welcome-file-list>
</web-app>
[11:34:10 root@tomcat ~]#curl 192.168.10.81:8080/zhangzhuo/
/usr/local/tomcat/webapps/zhangzhuo/index.html
配置规则:
- webApp的专有配置优先于系统的全局配置
- 修改系统的全局配置文件,需要重新启动服务生效
- 修改 webApp的专有配置,无需重启即可生效
1.4.4 应用部署实现
1.4.4.1 WebApp应用的归档格式
- .war:WebApp打包,类zip格式文件,通常包括一个应用的所有资源,比如jsp,html,配置文件等
- .jar:EJB类文件的打包压缩类zip格式文件,,包括很多的class文件, 网景公司发明
- .rar:资源适配器类打包文件,目前已不常用
- .ear:企业级WebApp打包,目前已不常用
传统应用开发测试后,通常打包为war格式,这种文件部署到Tomcat的webapps目录下,并默认会自动解包展开和部署上线。
#conf/server.xml中文件配置
<Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="true">
1.4.4.2 部署方式
- 部署Deploy:将webapp的源文件放置到目标目录,通过web.xml和context.xml文件中配置的路径就可以访问该webapp,通过类加载器加载其特有的类和依赖的类到JVM上,即:最终用户可以通过浏览器访问该应用
- 自动部署:Tomcat一旦发现多了一个web应用APP.war包,默认会自动把它解压缩,加载并启动起来
- 手动部署
- 冷部署:将webapp放到指定目录,才去启动Tomcat服务
- 热部署:Tomcat服务不停止,需要依赖manager、ant脚本、tcd(tomcat clientdeployer)等工具
- 反部署undeploy:停止webapp运行,并从JVM上清除已经加载的类,从Tomcat应用目录中移除部署的文件
- 启动start:是webapp能够访问
- 停止stop:webapp不能访问,不能提供服务,但是JVM并不清除它
1.4.4.3 部署WebApp的目录结构
#目录结构一般由开发用工具自动生成,以下模拟生成相关目录
mkdir projects/myapp/{WEB-INF,META-INF,classes,lib} -pv
mkdir: 已创建目录 "projects"
mkdir: 已创建目录 "projects/myapp"
mkdir: 已创建目录 "projects/myapp/WEB-INF"
mkdir: 已创建目录 "projects/myapp/META-INF"
mkdir: 已创建目录 "projects/myapp/classes"
mkdir: 已创建目录 "projects/myapp/lib"
#常见应用首页,内容就用前面的test.jsp内部
vi projects/myapp/index.jsp
#手动复制项目目录到webapps目录下去
cp -r projects/myapp/ /usr/local/tomcat/webapps/
#注意权限和属性
chown -R tomcat.tomcat /usr/local/tomcat/webapps/myapp
#访问http://YourIP:8080/myapp/
1.4.4.7 基于WEB的管理Server status和Manager APP实现应用部署
tomcat 提供了基于WEB的管理页面,默认由 tomcat-admin-webapps.noarch包提供相关文件
默认的管理页面被禁用,启用方法如下
修改conf/conf/tomcat-users.xml
[14:16:34 root@tomcat tomcat]#tail conf/tomcat-users.xml
#加下面两行,指定用户和密码
<role rolename="manager-gui"/>
<user username="admin" password="" roles="manager-gui"/>
#修改全局配置文件需要重启服务生效
修改webapps/manager/META-INF/context.xml
[14:19:06 root@tomcat tomcat]#tail webapps/manager/META-INF/context.xml
See the License for the specific language governing permissions and
limitations under the License.
-->
<Context antiResourceLocking="false" privileged="true" >
<CookieProcessor className="org.apache.tomcat.util.http.Rfc6265CookieProcessor"
sameSiteCookies="strict" />
<Valve className="org.apache.catalina.valves.RemoteAddrValve"
allow="127\.\d+\.\d+\.\d+|::1|0:0:0:0:0:0:0:1|\d+\.\d+\.\d+\.\d+" />
<Manager sessionAttributeValueClassNameFilter="java\.lang\.(?:Boolean|Integer|Long|Number|String)|org\.apache\.catalina\.filters\.CsrfPreventionFilter\$LruCache(?:\$1)?|java\.util\.(?:Linked)?HashMap"/>
</Context>
#查看正则表达式就知道是本地访问了,由于当前访问地址是192.168.x.x,可以修改正则表达式为\d+\.\d+\.\d+\.\d+
再次通过浏览器访问两个按钮Server Status和Manager App,可以看到以下管理界面,输入前面的用户和密码进行登录
1.4.5 常见配置详解
1.4.5.1 端口8005/tcp 安全配置管理
在conf/server.xml 有以下内容
<Server port="8005" shutdown="SHUTDOWN">
8005是Tomcat的管理端口,默认监听在127.0.0.1上。无需验证就可发送SHUTDOWN (大小写敏感)这个字符串,tomcat接收到后就会关闭此Server。
此管理功能建议禁用,可将SHUTDOWN改为一串猜不出的字符串实现
或者port修改成 0, 会使用随机端口,如:36913
port设为-1等无效端口,将关闭此功能
此行不能被注释,否则无法启动tomcat服务
1.4.5.2 显示指定的http服务器版本信息
默认不显示tomcat的http的Server头信息, 可以指定tomcat的http的Server头信息为相应的值
#conf/server.xml
<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000"
redirectPort="8443" Server="zhangzhuo"/>
[14:27:35 root@tomcat tomcat]#curl 192.168.10.81:8080 -I
HTTP/1.1 200
Content-Type: text/html;charset=UTF-8
Transfer-Encoding: chunked
Date: Tue, 16 Mar 2021 06:27:38 GMT
Server: zhangzhuo
1.4.5.3 其它配置
conf/server.xml中可以配置service,connector, Engine,Host等
- service配置
一般情况下,一个Server实例配置一个Service,name属性相当于该Service的ID
<Service name="Catalina">
- 连接器配置
<Connector port="8080" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443" />
redirectPort,如果访问HTTPS协议,自动转向这个连接器。但大多数时候,Tomcat并不会开启HTTPS,因为Tomcat往往部署在内部,HTTPS性能较差
- 引擎配置
<Engine name="Catalina" defaultHost="localhost">
- defaultHost 配置
<Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="true">
1.4.5.4 多虚拟主机配置
- name 必须是主机名,用主机名来匹配
- appBase 当前主机的网页根目录,是相对于 $CATALINA_HOME ,也可以使用绝对路径
- unpackWARs 是否自动解压war格式
- autoDeploy 热部署,自动加载并运行应用
虚拟主机配置过程
- 再添加和配置一个新的虚拟主机,并将myapp部署到/data/webapps目录下
[14:32:31 root@tomcat tomcat]#vim conf/server.xml
#在文件最后面增加下面内容
<Host name="www.zhangzhuo.org" appBase="/data/webapps/"
unpackWARs="true" autoDeploy="false">
<Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
prefix="localhost_access_log" suffix=".txt"
pattern="%h %l %u %t "%r" %s %b" />
</Host>
#以下行是自带的不需要修改
</Engine>
</Service>
</Server>
#或者如果不加日志也可以用下面简化写法
<Host name="www.zhangzhuo.org" appBase="/data/webapps/" unpackWARs="True"
autoDeploy="false"/>
- 准备虚拟主机的数据目录
[14:36:56 root@tomcat tomcat]#mkdir /data/webapps/ROOT -pv
[14:38:01 root@tomcat tomcat]#chown -R tomcat: /data/webapps
[14:38:32 root@tomcat tomcat]#echo www.zhangzhuo.org >/data/webapps/ROOT/index.html
- 测试
刚才在虚拟主机中主机名定义www.zhangzhuo.org,所以需要主机在本机手动配置一个域名解析。如果是windows,修改在C:\Windows\System32\drivers\etc下的hosts文件,需要管理员权限。
使用http://www.zhangzhuo.org:8080/访问查看
[14:49:30 root@tomcat tomcat]#curl www.zhangzhuo.org:8080
www.zhangzhuo.org
实战案例:tomcat实现多虚拟主机
[14:54:37 root@tomcat tomcat]#pwd
/usr/local/tomcat
[14:54:38 root@tomcat tomcat]#vim conf/server.xml
<Host name="node1.zhangzhuo.org" appBase="webapps" unpackWARs="true" autoDeploy="true"/>
<Host name="node2.zhangzhuo.org" appBase="webapps" unpackWARs="true" autoDeploy="true"/>
#对每个虚拟主机,准备数据
[15:05:32 root@tomcat tomcat]#mkdir /data/webapps{1,2}/ROOT -pv
[15:06:12 root@tomcat tomcat]#echo /data/webapps1/ROOT/index.html >/data/webapps1/ROOT/index.html
[15:06:58 root@tomcat tomcat]#echo /data/webapps2/ROOT/index.html >/data/webapps2/ROOT/index.html
#设置权限
[15:07:05 root@tomcat tomcat]#chown -R tomcat: /data/*
#准备虚拟主机的名称解析
[15:07:31 root@tomcat tomcat]#echo 192.168.10.81 node1.zhangzhuo.org >>/etc/hosts
[15:08:03 root@tomcat tomcat]#echo 192.168.10.81 node2.zhangzhuo.org >>/etc/hosts
[15:09:06 root@tomcat tomcat]#systemctl restart tomcat.service
#测试
[15:09:14 root@tomcat tomcat]#curl node1.zhangzhuo.org:8080
/data/webapps1/ROOT/index.html
[15:09:33 root@tomcat tomcat]#curl node2.zhangzhuo.org:8080
/data/webapps2/ROOT/index.html
1.4.5.5 基于web方式的Host Manager虚拟主机管理
可以通过tomcat的管理页面点下面Host Manager按钮进入管理虚拟主机的页面
默认Host Manager 管理页被禁用,解决方法类似于3.4.4.6
<role rolename="admin-gui"/>
<user username="admin" password="" roles="admin-gui"/>
1.4.5.6 Context 配置
Context作用:
- 路径映射:将url映射至指定路径,而非使用appBase下的物理目录,实现虚拟目录功能
- 应用独立配置,例如单独配置应用日志、单独配置应用访问控制
#路径映射
<Context path="/test" docBase="/data/test" reloadable="true" />
#还可以添加日志等独立的配置
<Context path="/test" docBase="/data/test" reloadable="true" >
<Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
prefix="localhost_test_log" suffix=".txt"
pattern="%h %l %u %t "%r" %s %b" />
</Context>
说明:
- path:指的是访问的URL路径,如果path与appBase下面的子目录同名,context的docBase路径优先更高
- docBase:可以是磁盘文件的绝对路径,也可以是相对路径(相对于Host的appBase)
- reloadable:true表示如果WEB-INF/classes或META-INF/lib目录下.class文件有改动,就会将WEB应用重新加载。生产环境中,建议使用false来禁用。
Centext实现过程
- 将/data/webapps1/下面的项目文件复制到/opt/下,可以修改一下index.html 区别一下
注意:这里特别使用了软链接,原因方便后期版升级或回滚,如是是版本升级,需要将软链接指向myappv2,重新启动。如果新版上线后,出现问题,重新修改软链接到上一个版本的目录,并重启,就可以实现回滚
- 修改conf/server.xml设置context
</Host>
<Host name="node1.zhangzhuo.org" appBase="/data/webapps1/" unpackWARs="true" autoDeploy="true">
<Context path="/opt" docBase="/opt/" reloadable="true"/>
</Host>
- 测试
[15:37:52 root@tomcat opt]#curl node1.zhangzhuo.org:8080/opt/
/opt/index.html
Valve组件
valve(阀门)组件可以定义日志
<Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
prefix="localhost_access_log" suffix=".txt"
pattern="%h %l %u %t "%r" %s %b" />
valve存在多种类型:
定义访问日志:org.apache.catalina.valves.AccessLogValve
定义访问控制:org.apache.catalina.valves.RemoteAddrValve
二、结合反向代理实现tomcat部署
2.1 常见部署方式介绍
-
standalone模式,Tomcat单独运行,直接接受用户的请求,不推荐。
-
反向代理,单机运行,提供了一个Nginx作为反向代理,可以做到静态由nginx提供响应,动态jsp代理给Tomcat
- LNMT:Linux + Nginx + MySQL + Tomcat
- LAMT:Linux + Apache(Httpd)+ MySQL + Tomcat
-
前置一台Nginx,给多台Tomcat实例做反向代理和负载均衡调度,Tomcat上部署的纯动态页面更适合
- LNMT:Linux + Nginx + MySQL + Tomcat
-
多级代理
- LNNMT:Linux + Nginx + Nginx + MySQL + Tomcat
2.2 利用 nginx 反向代理实现全部转发置指定同一个虚拟主机
2.2.1 配置说明
利用nginx反向代理功能,实现 4.1 图(2)的代理功能,将用户请求全部转发至指定的同一个tomcat主机
利用nginx指令proxy_pass 可以向后端服务器转发请求报文,并且在转发时会保留客户端的请求报文中的host首部
#从yum源安装nginx
yum install nginx -y
vim /etc/nginx/nginx.conf
#全部反向代理测试
location / {
proxy_pass http://127.0.0.1:8080; # 不管什么请求,都会访问后面的localhost虚拟主机
proxy_pass http://node1.zhangzhuo.org:8080; #此项将用户访问全部请求转发到node1的虚拟主机上
proxy_pass http://node2.zhangzhuo.org:8080; #此项将用户访问全部请求转发到node2的虚拟主机上
#以上两项都需要修改nginx服务器的/etc/hosts,实现node1.magedu.com和node2.magedu.com到IP的解析
}
[15:59:41 root@nginx ~]#nginx -t
[15:59:41 root@nginx ~]#systemctl enable --now nginx.service
#说明: proxy_pass http://FQDN/ 中的FQDN 决定转发至后端哪个虚拟主机,而与用户请求的URL无关
#如果转到后端的哪个服务器由用户请求决定,可以向后端服务转发请求的主机头实现,示例:
proxy_set_header Host $http_host;
2.3 利用nginx实现动静分离代理
2.3.1 配置说明
可以利用nginx实现动静分离
[16:11:26 root@nginx ~]#vim /etc/nginx/nginx.conf
#下面行可不加
# location / {
#proxy_pass http://192.168.10.81:8080;
#proxy_set_header Host $http_host:8080;
proxy_pass http://node1.zhangzhuo.org:8080;
#proxy_pass http://node2.zhangzhuo.org:8080;
# }
# ~* 不区分大小写
location ~* \.jsp$ {
proxy_pass http://node1.zhangzhuo.org:8080; #注意: 8080后不要加/,需要nginx修改
}
以上设置,可以将jsp的请求反向代理到tomcat,而其它文件仍由nginx处理,从而实现所谓动静分离。但由于jsp文件中实际上是由静态资源和动态组成,所以无法彻底实现动静分离。实际上Tomcat不太适合做动静分离,用它来管理程序的图片不好做动静分离部署
2.4 利用httpd实现基于http协议的反向代理至后端Tomcat服务器
httpd也提供了反向代理功能,所以可以实现对tomcat的反向代理功能
范例:查看代理相关模块
[09:49:32 root@tomcat ~]#httpd -M | grep proxy
AH00558: httpd: Could not reliably determine the server's fully qualified domain name, using tomcat.zhangzhuo.org. Set the 'ServerName' directive globally to suppress this message
proxy_module (shared)
proxy_ajp_module (shared)
proxy_balancer_module (shared)
proxy_connect_module (shared)
proxy_express_module (shared)
proxy_fcgi_module (shared)
proxy_fdpass_module (shared)
proxy_ftp_module (shared)
proxy_http_module (shared)
proxy_hcheck_module (shared)
proxy_scgi_module (shared)
proxy_uwsgi_module (shared)
proxy_wstunnel_module (shared)
proxy_http2_module (shared)
proxy_http_module模块代理配置
<VirtualHost *:80>
ServerName node1.zhangzhuo.org
ProxyRequests Off
ProxyPass / http://127.0.0.1:8080/
ProxyPassReverse / http://127.0.0.1:8080/
ProxyPreserveHost On
ProxyVia On
</VirtualHost>
- ProxyRequests:Off 关闭正向代理功能,即启动反向代理
- ProxyPass:反向代理指令,指向后端服务器
- ProxyPassReverse:当反向代理时,返回给客户端的报文需将之重写个别后端主机的response头,如:Location,Content-Location,URI
- ProxyPreserveHost:On时让反向代理保留原请求的Host首部转发给后端服务器,off 时则删除host首部后再转发至后面端服务器, 这将导致只能转发到后端的默认虚拟主机
- ProxyVia:On开启。反向代理的响应报文中提供一个response的via首部,默认值off
说明: 关于ProxyPreserveHost
#分别访问下面不同链接
http://httpd服务IP/
http://node1.magedu.org/
http://node1.magedu.org/index.jsp
#以上3个URL看到了不同的页面,说明ProxyPreserveHost On起了作用
#设置ProxyPreserveHost Off再看效果,说明什么?
2.4.2 实战案例
#对不同的虚拟主机生成页面文件
#修改httpd配置
[09:58:32 root@tomcat ~]#vim /etc/httpd/conf.d/http-tomcat.conf
<VirtualHost *:80>
ServerName node1.zhangzhuo.org
ProxyRequests Off
ProxyPass / http://127.0.0.1:8080/
ProxyPassReverse / http://127.0.0.1:8080/
ProxyPreserveHost On
ProxyVia On
</VirtualHost>
[09:59:55 root@tomcat ~]#systemctl enable --now httpd.service
#用下面不同URL访问,可以看不同结果
[10:01:51 root@tomcat ~]#curl node1.zhangzhuo.org
/data/tomcat1/ROOT/index.html
[10:01:55 root@tomcat ~]#curl node2.zhangzhuo.org
/data/tomcat2/ROOT/index.html
[10:01:58 root@tomcat ~]#curl 192.168.10.83
/usr/local/tomcat/webapps/ROOT/index.html
#修改配置
[10:02:05 root@tomcat ~]#vim /etc/httpd/conf.d/http-tomcat.conf
#只修改下面一行
ProxyPreserveHost Off
[10:02:54 root@tomcat ~]#systemctl reload httpd.service
#再次用用下面不同URL访问,可以看相同结果
[10:03:18 root@tomcat ~]#curl 192.168.10.83
/usr/local/tomcat/webapps/ROOT/index.html
[10:03:34 root@tomcat ~]#curl node1.zhangzhuo.org
/usr/local/tomcat/webapps/ROOT/index.html
[10:03:39 root@tomcat ~]#curl node2.zhangzhuo.org
/usr/local/tomcat/webapps/ROOT/index.html
2.5 利用 httpd 实现基于AJP协议的反向代理至后端Tomcat服务器
2.5.1 AJP 协议说明
AJP(Apache JServ Protocol)是定向包协议,是一个二进制的TCP传输协议,相比HTTP这种纯文本的协议来说,效率和性能更高,也做了很多优化。但是浏览器并不能直接支持AJP13协议,只支持HTTP协议。所以实际情况是,通过Apache的proxy_ajp模块进行反向代理,暴露成http协议给客户端访问
2.5.2 启用和禁用 AJP
注意: Tomcat/8.5.51之后版本基于安全需求默认禁用AJP协议
范例: Tomcat/8.5.51之后版启用支持AJP协议
[10:03:43 root@tomcat ~]#vim /usr/local/tomcat/conf/server.xml
#取消前面的注释,并修改下面行,修改address和secretRequired
<Connector protocol="AJP/1.3" address="0.0.0.0" port="8009" redirectPort="8443" secretRequired="" />
[10:07:02 root@tomcat ~]#systemctl restart tomcat.service
[10:13:19 root@tomcat ~]#ss -ntl | grep 8009
LISTEN 0 100 *:8009 *:*
注意: secretRequired="" 必须加上,否则出现以下错误提示
[root@centos8 tomcat]#cat logs/catalina.log
Caused by: java.lang.IllegalArgumentException: The AJP Connector is configured
with secretRequired="true" but the secret attribute is either null or "". This
combination is not valid.
除httpd外,其它支持AJP代理的服务器非常少,比如Nginx就不支持AJP,所以目前一般都禁用AJP协议端
范例:禁用AJP协议
#Tomcat/8.5.50版本之前默认支持AJP协议
#配置tomcat配置文件,删除下面一行
<Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />
2.5.3 httpd 实现 AJP 反向代理
2.5.3.1 配置说明
相对来讲,AJP协议基于二进制比使用HTTP协议的连接器效率高些。
proxy_ajp_module模块代理配置
<VirtualHost *:80>
ServerName node1.zhangzhuo.org
ProxyRequests Off
ProxyPreserveHost On
ProxyVia On
ProxyPass / ajp://127.0.0.1:8009/
</VirtualHost>
查看Server Status可以看到确实使用的是ajp连接了。
2.6 实现tomcat负载均衡
动态服务器的问题,往往就是并发能力太弱,往往需要多台动态服务器一起提供服务。如何把并发的压力分摊,这就需要调度,采用一定的调度策略,将请求分发给不同的服务器,这就是Load Balance负载均衡。
当单机Tomcat,演化出多机多级部署的时候,一个问题便凸显出来,这就是Session。而这个问题的由来,都是由于HTTP协议在设计之初没有想到未来的发展。
2.6.1 HTTP的无状态,有连接和短连接
-
无状态:指的是服务器端无法知道2次请求之间的联系,即使是前后2次请求来自同一个浏览器,也没有任何数据能够判断出是同一个浏览器的请求。后来可以通过cookie、session机制来判断。
- 浏览器端第一次HTTP请求服务器端时,在服务器端使用session这种技术,就可以在服务器端产生一个随机值即SessionID发给浏览器端,浏览器端收到后会保持这个SessionID在Cookie当中,这个Cookie值一般不能持久存储,浏览器关闭就消失。浏览器在每一次提交HTTP请求的时候会把这个SessionID传给服务器端,服务器端就可以通过比对知道是谁了
- Session通常会保存在服务器端内存中,如果没有持久化,则易丢失
- Session会定时过期。过期后浏览器如果再访问,服务端发现没有此ID,将给浏览器端重新发新的SessionID
- 更换浏览器也将重新获得新的SessionID
-
有连接:是因为它基于TCP协议,是面向连接的,需要3次握手、4次断开。
-
短连接:Http 1.1之前,都是一个请求一个连接,而Tcp的连接创建销毁成本高,对服务器有很大的影响。所以,自Http 1.1开始,支持keep-alive,默认也开启,一个连接打开后,会保持一段时间(可设置),浏览器再访问该服务器就使用这个Tcp连接,减轻了服务器压力,提高了效率。
服务器端如果故障,即使Session被持久化了,但是服务没有恢复前都不能使用这些SessionID。
如果使用HAProxy或者Nginx等做负载均衡器,调度到了不同的Tomcat上,那么也会出现找不到SessionID的情况。
2.6.2 会话保持方式
2.6.2.1 session sticky会话黏性
Session绑定
- nginx:source ip, cookie
- HAProxy:source ip, cookie
优点:简单易配置
缺点:如果目标服务器故障后,如果没有做sessoin持久化,就会丢失session,此方式生产很少使用
2.6.2.2 session 复制集群
Tomcat自己的提供的多播集群,通过多播将任何一台的session同步到其它节点。
缺点
- Tomcat的同步节点不宜过多,互相即时通信同步session需要太多带宽
- 每一台都拥有全部session,内存损耗太多
2.6.2.3 session server
session 共享服务器,使用memcached、redis做共享的Session服务器,此为推荐方式
2.6.3 负载均衡规划和准备
2.6.3.1 负载均衡主机和网络地址规划
#只需在192.168.10.81的nginx主机上实现域名解析
vim /etc/hosts
#添加以下三行
192.168.10.81 proxy.zhangzhuo.org proxy
192.168.10.82 t1.zhangzhuo.org
192.168.10.83 t2.zhangzhuo.org
2.6.3.2 负载均衡tomcat主机准备
修改tomcat的虚拟机主机为自定义的主机名,并设为默认的虚拟主机
t1虚拟主机配置conf/server.xml
<Engine name="Catalina" defaultHost="t1.zhangzhuo.org">
<Host name="t1.zhangzhuo.org" appBase="/data/webapps" unpackWARs="true" autoDeploy="true">
t2虚拟主机配置conf/server.xml
<Engine name="Catalina" defaultHost="t2.zhangzhuo.org">
<Host name="t2.zhangzhuo.org" appBase="/data/webapps" unpackWARs="true" autoDeploy="true">
2.6.3.3 准备负载均衡规划测试用的jsp文件
在t1和 t2节点创建相同的文件/data/webapps/ROOT/index.jsp
#项目路径配置
mkdir -pv /data/webapps/ROOT
#编写测试jsp文件,内容在下面
vim /data/webapps/ROOT/index.jsp
<%@ page import="java.util.*" %>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>tomcat test</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>
#设置权限
chown -R tomcat: /data/*
2.6.4 Nginx 实现后端 tomcat 的负载均衡调度
2.6.4.1 Nginx 实现后端 tomcat 的负载均衡
nginx 配置如下
yum install -y nginx
vim /etc/nginx/nginx.conf
#在http块中加以下内容
#注意名称不要用下划线
upstream tomcat-server {
#ip_hash
#hash $cookie_JSESSIONID;
server t1.zhangzhuo.org:8080;
server t2.zhangzhuo.org:8080;
}
#修该server中的location
server {
location ~* \.(jsp|do)$ {
proxy_pass http://tomcat-server;
}
}
测试http://proxy.magedu.com/index.jsp,可以看到轮询调度效果,每次刷新后端主机和SessionID都会变化
[11:02:35 root@proxy ~]#curl proxy.zhangzhuo.org/index.jsp
<!DOCTYPE html>
<html lang="en">
<head>
? <meta charset="UTF-8">
? <title>tomcat test</title>
</head>
<body>
<div>On tomcat-server</div>
<div>192.168.10.83:8080</div>
<div>SessionID = <span style="color:blue">94BF0E8A4DC91C895E49A96DC45B487E</span></div>
Thu Mar 18 11:02:36 CST 2021
</body>
</html>
[11:02:36 root@proxy ~]#curl proxy.zhangzhuo.org/index.jsp
<!DOCTYPE html>
<html lang="en">
<head>
? <meta charset="UTF-8">
? <title>tomcat test</title>
</head>
<body>
<div>On tomcat-server</div>
<div>192.168.10.82:8080</div>
<div>SessionID = <span style="color:blue">1DBAED2F3AF861F315881B3165E01035</span></div>
Thu Mar 18 11:02:39 CST 2021
</body>
</html>
2.6.4.2 实现 session 黏性
在upstream中使用ip_hash指令,使用客户端IP地址Hash
[11:05:41 root@proxy ~]#vim /etc/nginx/nginx.conf
#只添加ip_hash;这一行
upstream tomcat-server {
ip_hash;
#hash $cookie_JSESSIONID;
server t1.zhangzhuo.org:8080;
server t2.zhangzhuo.org:8080;
}
配置完reload nginx服务。curl 测试一下看看效果。
#用curl访问每次都调度到10.0.0.102主机上,但因为curl每次请求不会自动携带之前获取的cookie,所有SessionID每次都在变化
[11:09:25 root@proxy ~]#curl proxy.zhangzhuo.org
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>tomcat test</title>
</head>
<body>
<div>On tomcat-server</div>
<div>192.168.10.83:8080</div>
<div>SessionID = <span style="color:blue">0F95C31449951998BB9A83332022F40D</span></div>
Thu Mar 18 11:10:19 CST 2021
</body>
</html>
[11:10:19 root@proxy ~]#curl proxy.zhangzhuo.org
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>tomcat test</title>
</head>
<body>
<div>On tomcat-server</div>
<div>192.168.10.83:8080</div>
<div>SessionID = <span style="color:blue">B0FD485A9EA362C1FAA9A6904BFD3390</span></div>
Thu Mar 18 11:10:21 CST 2021
</body>
</html>
通过图形浏览器看到主机不变,sessionID不变
关闭Session对应的Tomcat服务,再重启启动它,看看Session的变化。
通过浏览器看到主机不变,但sessionID和上一次变化,但后续刷新不再变化
2.6.5 Httpd 实现后端tomcat的负载均衡调度
和nginx一样, httpd 也支持负载均衡调度功能
2.6.5.1 httpd 的负载均衡配置说明
使用 httpd -M 可以看到 proxy_balancer_module,用它来实现负载均衡。
官方帮助: http://httpd.apache.org/docs/2.4/mod/mod_proxy_balancer.html
负载均衡配置说明
#配置代理到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 参数
ProxySet指令也可以使用上面的参数。
2.6.5.2 启用 httpd 的负载均衡
在 tomcat 的配置中Engine使用jvmRoute属性,通过此项可得知SessionID是在哪个tomcat生成
#t1的conf/server.xml配置如下:
<Engine name="Catalina" defaultHost="t1.zhangzhuo.org" jvmRoute="tomcat1">
#t2的conf/server.xml配置如下:
<Engine name="Catalina" defaultHost="t2.zhangzhuo.org" jvmRoute="tomcat2">
这样设置后 SessionID 就变成了以下形式:
SessionID = 9C949FA4AFCBE9337F5F0669548BD4DF.Tomcat1
httpd配置如下
yum install httpd
vim /etc/httpd/conf.d/tomcat.conf
<Proxy balancer://tomcat-server>
BalancerMember http://t1.zhangzhuo.org:8080 loadfactor=1
BalancerMember http://t2.zhangzhuo.org:8080 loadfactor=2
</Proxy>
<VirtualHost *:80>
ServerName proxy.zhangzhuo.org
ProxyRequests Off
ProxyVia On
ProxyPreserveHost On #off时不向后端转发原请求host首部,而转发采用BalancerMember指向名称为首部
ProxyPass / balancer://tomcat-server/
ProxyPassReverse / balancer://tomcat-server/
</VirtualHost>
#开启httpd负载均衡的状态页
<Location /balancer-manager>
SetHandler balancer-manager
ProxyPass !
Require all granted
</Location>
loadfactor设置为1:2,便于观察。观察调度的结果是轮询的。
查看状态页
2.6.5.3 实现 session 黏性
官方文档:http://httpd.apache.org/docs/2.4/mod/mod_proxy_balancer.html
%{BALANCER_WORKER_ROUTE}e The route of the worker chosen.
范例:
vim /etc/httpd/conf.d/tomcat.conf
#添加此行,在cookie 添加 ROUTEID的定义
Header add Set-Cookie "ROUTEID=.%{BALANCER_WORKER_ROUTE}e; path=/" env=BALANCER_ROUTE_CHANGED
<Proxy balancer://tomcat-server>
BalancerMember http://t1.zhangzhuo.org:8080 loadfactor=1 route=T1 #修改行,指定后端服务器对应的ROUTEID
BalancerMember http://t2.zhangzhuo.org:8080 loadfactor=2 route=T2 #修改行
ProxySet stickysession=ROUTEID #添加此行,指定用cookie中的ROUTEID值做为调度条件
</Proxy>
用浏览器访问发现Session不变了,一直找的同一个Tomcat服务器
2.6.5.4 实现 AJP 协议的负载均衡
在上面基础上修改httpd的配置文件
#在t1和t2的tomcat-8.5.51以上版本的需启用AJP
<Connector protocol="AJP/1.3" address="0.0.0.0" port="8009" redirectPort="8443" secretRequired="" />
vim /etc/httpd/conf.d/tomcat.conf
#注释此行
#Header add Set-Cookie "ROUTEID=.%{BALANCER_WORKER_ROUTE}e; path=/" env=BALANCER_ROUTE_CHANGED
<Proxy balancer://tomcat-server>
BalancerMember ajp://t1.zhangzhuo.org:8009 loadfactor=1 route=T1 #修改此行
BalancerMember ajp://t2.zhangzhuo.org:8009 loadfactor=1 route=T2 #修改此行
#ProxySet stickysession=ROUTEID #先注释此行
</Proxy>
<VirtualHost *:80>
ServerName proxy.zhangzhuo.org
ProxyRequests Off
ProxyVia On
ProxyPreserveHost On
ProxyPass / balancer://tomcat-server/
ProxyPassReverse / balancer://tomcat-server/
</VirtualHost>
<Location /balancer-manager>
SetHandler balancer-manager
ProxyPass !
Require all granted
</Location>
#ProxySet stickysession=ROUTEID先禁用,可以看到不断轮询的切换效果
开启ProxySet后,发现Session不变了,一直找的同一个Tomcat服务器。
<Proxy balancer://tomcat-server>
BalancerMember ajp://t1.zhangzhuo.org:8009 loadfactor=1 route=T1
BalancerMember ajp://t2.zhangzhuo.org:8009 loadfactor=1 route=T2
ProxySet stickysession=ROUTEID #取消此行注释,只修改此行
</Proxy>
systemctl restart httpd
多次刷新页面,不再变化
结论:
假设有A、B两个节点,都将Session持久化。如果Tomcat A节点下线期间用户切换到了Tomcat B上,就获得了Tomcat B的Session,原有Sesssion将丢失,就算将持久化Session的Tomcat A节点再次上线了,也没用了。因此需要实现Session的高可用性来解决上述问题。
三、Tomcat Session Replication Cluster
Tomcat 官方实现了 Session 的复制集群,将每个Tomcat的Session进行相互的复制同步,从而保证所有Tomcat都有相同的Session信息.
3.1 配置说明
官方文档: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="228.0.0.4" #指定的多播地址
port="45564" #45564/UDP
frequency="500" #间隔500ms发送
dropTime="3000"/>
<Receiver className="org.apache.catalina.tribes.transport.nio.NioReceiver"
address="auto" #监听地址,此项建议修改为当前主机的IP
port="4000" #监听端口
autoBind="100" #如果端口冲突,自动绑定其它端口,范围是4000-4100
selectorTimeout="5000" #自动绑定超时时长5s
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>
#注意:tomcat7的官方文档此处有错误
http://tomcat.apache.org/tomcat-7.0-doc/cluster-howto.html
......
<ClusterListener
className="org.apache.catalina.ha.session.JvmRouteSessionIDBinderListener">
<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侦听器
使用
添加到
添加到
最后,在应用程序内部启用了才可以使用
3.2 实战案例: 实现 Tomcat Session 集群
环境准备:
- 时间同步,确保NTP或Chrony服务正常运行
- 防火墙规则
3.2.1 在 proxy 主机设置 httpd (或nginx)实现后端tomcat主机轮询
[14:23:20 root@proxy ~]#cat /etc/httpd/conf.d/tomcat.conf
#Header add Set-Cookie "ROUTEID=.%{BALANCER_WORKER_ROUTE}e; path=/" env=BALANCER_ROUTE_CHANGED
<Proxy balancer://tomcat-server>
BalancerMember http://t1.zhangzhuo.org:8080 loadfactor=1 route=T1
BalancerMember http://t2.zhangzhuo.org:8080 loadfactor=1 route=T2
#ProxySet stickysession=ROUTEID
</Proxy>
<VirtualHost *:80>
ServerName proxy.zhangzhuo.org
ProxyRequests Off
ProxyVia On
ProxyPreserveHost On
ProxyPass / balancer://tomcat-server/
ProxyPassReverse / balancer://tomcat-server/
</VirtualHost>
<Location /balancer-manager>
SetHandler balancer-manager
ProxyPass !
Require all granted
</Location>
[14:23:54 root@proxy ~]#systemctl restart httpd
3.2.2 在所有后端tomcat主机上修改conf/server.xml
本次把多播复制的配置放到t1.magedu.org和t2.magedu.org虚拟主机里面, 即Host块中。
特别注意修改Receiver的address属性为一个本机可对外的IP地址
3.2.2.1 修改 t1 主机的 conf/server.xml
#将5.1 内容复制到conf/server.xml的Host块内或Engine块(针对所有主机)
[14:28:29 root@t1 ~]#vim /usr/local/tomcat/conf/server.xml
<Host name="t1.zhangzhuo.org" appBase="/data/webapps"
unpackWARs="true" autoDeploy="true">
<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.100"
port="45564"
frequency="500"
dropTime="3000"/>
<Receiver className="org.apache.catalina.tribes.transport.nio.NioReceiver"
address="192.168.10.82"
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>
[14:29:07 root@t1 ~]#ss -ntl
State Recv-Q Send-Q Local Address:Port Peer Address:Port
LISTEN 0 128 0.0.0.0:111 0.0.0.0:*
LISTEN 0 128 0.0.0.0:22 0.0.0.0:*
LISTEN 0 128 [::]:111 [::]:*
LISTEN 0 100 *:8080 *:*
LISTEN 0 128 [::]:22 [::]:*
LISTEN 0 50 [::ffff:192.168.10.82]:4000 *:*
LISTEN 0 100 *:8009 *:*
简化说明
t1的conf/server.xml中,如下
<Host name="t1.magedu.com" appBase="/data/webapps" autoDeploy="true" >
#其他略去
<Receiver className="org.apache.catalina.tribes.transport.nio.NioReceiver"
address="192.168.10.82" #只改此行
port="4000"
autoBind="100"
selectorTimeout="5000"
maxThreads="6"/>
3.2.2.2 修改 t2 主机的 conf/server.xml
[11:41:50 root@t2 ~]#vim /usr/local/tomcat/conf/server.xml
<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.100"
port="45564"
frequency="500"
dropTime="3000"/>
<Receiver className="org.apache.catalina.tribes.transport.nio.NioReceiver"
address="192.168.10.83" #此行指定当前主机的IP,其它和T1节点配置相同
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>
[14:32:37 root@t2 ~]#ss -ntl
State Recv-Q Send-Q Local Address:Port Peer Address:Port
LISTEN 0 128 0.0.0.0:111 0.0.0.0:*
LISTEN 0 128 0.0.0.0:22 0.0.0.0:*
LISTEN 0 100 *:8009 *:*
LISTEN 0 128 [::]:111 [::]:*
LISTEN 0 100 *:8080 *:*
LISTEN 0 128 [::]:22 [::]:*
LISTEN 0 50 [::ffff:192.168.10.83]:4000 *:*
简化说明
t2主机的server.xml中,如下
<Host name="t2.magedu.com" appBase="/data/webapps" autoDeploy="true" >
其他略去
<Receiver className="org.apache.catalina.tribes.transport.nio.NioReceiver"
address="192.168.10.83" #只改此行
port="4000"
autoBind="100"
selectorTimeout="5000"
maxThreads="6"/>
尝试使用刚才配置过得负载均衡(移除Session黏性),测试发现Session还是变来变去。
3.2.3 修改应用的web.xml文件开启该应用程序的分布式
参考官方说明: https://tomcat.apache.org/tomcat-8.5-doc/cluster-howto.html
Make sure your web.xml has the element
为所有tomcat主机应用web.xml的
3.2.3.1 修改t1主机的应用的web.xml文件
[14:29:07 root@t1 ~]#ll /usr/local/tomcat/webapps/ROOT/WEB-INF/
total 4
-rw-r----- 1 tomcat tomcat 1227 Mar 5 07:17 web.xml
[14:37:39 root@t1 ~]#cp -a /usr/local/tomcat/webapps/ROOT/WEB-INF /data/webapps/ROOT/
[14:38:01 root@t1 ~]#tree /data/webapps/ROOT/
/data/webapps/ROOT/
├── index.jsp
└── WEB-INF
└── web.xml
#在倒数第二行加一行
[14:38:16 root@t1 ~]#vim /data/webapps/ROOT/WEB-INF/web.xml
</description>
<distributable/> #添加此行
</web-app>
#注意权限
[14:39:26 root@t1 ~]#ll /data/webapps/ROOT/WEB-INF/
total 4
-rw-r----- 1 tomcat tomcat 1244 Mar 18 14:39 web.xml
[14:41:28 root@t1 ~]#systemctl restart tomcat.service
#同时观察日志
[14:43:59 root@t1 ~]#systemctl restart tomcat.service
18-Mar-2021 14:41:54.616 INFO [Membership-MemberAdded.] org.apache.catalina.ha.tcp.SimpleTcpCluster.memberAdded Replication member added:[org.apache.catalina.tribes.membership.MemberImpl[tcp://{192, 168, 10, 83}:4000,{192, 168, 10, 83},4000, alive=559883, securePort=-1, UDP Port=-1, id={87 114 -110 73 126 -34 64 126 -77 51 -24 81 -58 55 116 47 }, payload={}, command={}, domain={}]]
3.2.3.2 修改t2主机的应用的web.xml文件
#与5.2.3.1上的t1相同的操作
[14:32:37 root@t2 ~]#cp -a /usr/local/tomcat/webapps/ROOT/WEB-INF /data/webapps/ROOT/
[14:43:07 root@t2 ~]#vim /data/webapps/ROOT/WEB-INF/web.xml
</description>
<distributable/> #添加此行
</web-app>
#注意权限
[14:43:28 root@t2 ~]#ll /data/webapps/ROOT/WEB-INF/web.xml
-rw-r----- 1 tomcat tomcat 1244 Mar 18 14:43 /data/webapps/ROOT/WEB-INF/web.xml
#同时观察日志
[14:43:59 root@t2 ~]#systemctl restart tomcat.service
18-Mar-2021 14:44:49.868 INFO [Membership-MemberAdded.] org.apache.catalina.ha.tcp.SimpleTcpCluster.memberAdded Replication member added:[org.apache.catalina.tribes.membership.MemberImpl[tcp://{192, 168, 10, 82}:4000,{192, 168, 10, 82},4000, alive=175284, securePort=-1, UDP Port=-1, id={-49 -105 84 52 28 -34 64 -38 -115 -100 57 -77 83 32 14 -124 }, payload={}, command={}, domain={}]]
3.2.4 测试访问
重启全部Tomcat,通过负载均衡调度到不同节点,返回的SessionID不变了。
用浏览器访问,并刷新多次,发现SessionID 不变,但后端主机在轮询
但此方式当后端tomcat主机较多时,会重复占用大量的内存,并不适合后端服务器众多的场景
3.2.5 故障模拟
#模拟t2节点故障
[14:44:47 root@t2 ~]#systemctl stop tomcat.service
#多次访问SessionID不变
四、Memcached
4.1 NoSQL介绍
NoSQL是对 Not Only SQL、非传统关系型数据库的统称。
NoSQL一词诞生于1998年,2009年这个词汇被再次提出指非关系型、分布式、不提供ACID的数据库设计模式。
随着互联网时代的到来,数据爆发式增长,数据库技术发展日新月异,要适应新的业务需求。
而随着移动互联网、物联网的到来,大数据的技术中NoSQL也同样重要。
数据库排名:https://db-engines.com/en/ranking
NoSQL 分类
- Key-value Store k/v数据库
- 性能好 O(1) , 如: redis、memcached
- Document Store 文档数据库
- mongodb、CouchDB
- Column Store 列存数据库,Column-Oriented DB
- HBase、Cassandra,大数据领域应用广泛
- Graph DB 图数据库
- Neo4j
- Time Series 时序数据库
- InfluxDB、Prometheus
4.2 Memcached
Memcached 只支持能序列化的数据类型,不支持持久化,基于Key-Value的内存缓存系统
memcached 虽然没有像redis所具备的数据持久化功能,比如RDB和AOF都没有,但是可以通过做集群同步的方式,让各memcached服务器的数据进行同步,从而实现数据的一致性,即保证各memcached的数据是一样的,即使有任何一台 memcached 发生故障,只要集群中有一台memcached 可用就不会出现数据丢失,当其他memcached 重新加入到集群的时候,可以自动从有数据的memcached 当中自动获取数据并提供服务。
Memcached 借助了操作系统的 libevent 工具做高效的读写。libevent是个程序库,它将Linux的epoll、BSD类操作系统的kqueue等事件处理功能封装成统一的接口。即使对服务器的连接数增加,也能发挥高性能。memcached使用这个libevent库,因此能在Linux、BSD、Solaris等操作系统上发挥其高性能
Memcached 支持最大的内存存储对象为1M,超过1M的数据可以使用客户端压缩或拆分报包放到多个key中,比较大的数据在进行读取的时候需要消耗的时间比较长,memcached 最适合保存用户的session实现session共享
Memcached存储数据时, Memcached会去申请1MB的内存, 把该块内存称为一个slab, 也称为一个page
Memcached 支持多种开发语言,包括:JAVA,C,Python,PHP,C#,Ruby,Perl等
Memcached 官网:http://memcached.org/
4.3 Memcached 和 Redis 比较
4.4 Memcached 工作机制
4.4.1 内存分配机制
应用程序运行需要使用内存存储数据,但对于一个缓存系统来说,申请内存、释放内存将十分频繁,非常容易导致大量内存碎片,最后导致无连续可用内存可用。
Memcached采用了Slab Allocator机制来分配、管理内存。
-
Page:分配给Slab的内存空间,默认为1MB,分配后就得到一个Slab。Slab分配之后内存按照固定字节大小等分成chunk。
-
Chunk:用于缓存记录k/v值的内存空间。Memcached会根据数据大小选择存到哪一个chunk中,假设chunk有128bytes、64bytes等多种,数据只有100bytes存储在128bytes中,存在少许浪费。
- Chunk最大就是Page的大小,即一个Page中就一个Chunk
-
Slab Class:Slab按照Chunk的大小分组,就组成不同的Slab Class, 第一个Chunk大小为 96B的Slab为Class1,Chunk 120B为Class 2,如果有100bytes要存,那么Memcached会选择下图中SlabClass 2 存储,因为它是120bytes的Chunk。Slab之间的差异可以使用Growth Factor 控制,默认1.25。
范例:查看Slab Class
[15:02:54 root@memcached ~]#memcached -u memcached -f 2 -vv
slab class 1: chunk size 96 perslab 10922
slab class 2: chunk size 192 perslab 5461
slab class 3: chunk size 384 perslab 2730
slab class 4: chunk size 768 perslab 1365
slab class 5: chunk size 1536 perslab 682
slab class 6: chunk size 3072 perslab 341
slab class 7: chunk size 6144 perslab 170
slab class 8: chunk size 12288 perslab 85
slab class 9: chunk size 24576 perslab 42
slab class 10: chunk size 49152 perslab 21
slab class 11: chunk size 98304 perslab 10
slab class 12: chunk size 196608 perslab 5
slab class 13: chunk size 524288 perslab 2
<27 server listening (auto-negotiate)
<28 server listening (auto-negotiate)
4.4.2 懒过期 Lazy Expiration
memcached不会监视数据是否过期,而是在取数据时才看是否过期,如果过期,把数据有效期限标识为0,并不清除该数据。以后可以覆盖该位置存储其它数据。
4.4.3 LRU
当内存不足时,memcached会使用LRU(Least Recently Used)机制来查找可用空间,分配给新记录使用。
4.4.4 集群
Memcached集群,称为基于客户端的分布式集群,即由客户端实现集群功能,即Memcached本身不支持集群
Memcached集群内部并不互相通信,一切都需要客户端连接到Memcached服务器后自行组织这些节点,并决定数据存储的节点。
4.5 安装和启动
官方安装说明
https://github.com/memcached/memcached/wiki/Install
4.5.1 yum安装
范例: CentOS 8 安装 memcached
[15:03:18 root@memcached ~]#yum info memcached
[15:05:04 root@memcached ~]#yum install -y memcached
[15:05:26 root@memcached ~]#rpm -ql memcached
[15:05:55 root@memcached ~]#cat /etc/sysconfig/memcached
PORT="11211" #监听端口
USER="memcached" #启动用户
MAXCONN="1024" #最大连接数
CACHESIZE="64" #最大使用内存
OPTIONS="-l 127.0.0.1,::1" #其他选项
[15:07:21 root@memcached ~]#grep -Ev "^#|^$" /usr/lib/systemd/system/memcached.service
[Unit]
Description=memcached daemon
Before=httpd.service
After=network.target
[Service]
EnvironmentFile=/etc/sysconfig/memcached
ExecStart=/usr/bin/memcached -p ${PORT} -u ${USER} -m ${CACHESIZE} -c ${MAXCONN} $OPTIONS
PrivateTmp=true
ProtectSystem=full
NoNewPrivileges=true
PrivateDevices=true
CapabilityBoundingSet=CAP_SETGID CAP_SETUID CAP_SYS_RESOURCE
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
[Install]
WantedBy=multi-user.target
[15:08:19 root@memcached ~]#getent passwd memcached
memcached:x:992:990:Memcached daemon:/run/memcached:/sbin/nologin
[15:08:35 root@memcached ~]#systemctl start memcached.service
[15:10:07 root@memcached ~]#pstree -p | grep memcached
|-memcached(1814)-+-{memcached}(1815)
| |-{memcached}(1816)
| |-{memcached}(1817)
| |-{memcached}(1818)
| |-{memcached}(1819)
| |-{memcached}(1820)
| |-{memcached}(1821)
| |-{memcached}(1822)
| `-{memcached}(1823)
[15:10:32 root@memcached ~]#ss -ntlup | grep memcached
tcp LISTEN 0 128 127.0.0.1:11211 0.0.0.0:* users:(("memcached",pid=1814,fd=27))
tcp LISTEN 0 128 [::1]:11211 [::]:* users:(("memcached",pid=1814,fd=28))
#修改端口绑定的IP为当前主机的所有IP
PORT="11211"
USER="memcached"
MAXCONN="1024"
CACHESIZE="64"
#OPTIONS="-l 127.0.0.1,::1"
OPTIONS=""
[15:11:37 root@memcached ~]#systemctl restart memcached.service
[15:11:55 root@memcached ~]#ss -ntl
State Recv-Q Send-Q Local Address:Port Peer Address:Port
LISTEN 0 128 0.0.0.0:11211 0.0.0.0:*
LISTEN 0 128 0.0.0.0:111 0.0.0.0:*
LISTEN 0 128 0.0.0.0:22 0.0.0.0:*
LISTEN 0 128 [::]:11211 [::]:*
LISTEN 0 128 [::]:111 [::]:*
LISTEN 0 128 [::]:22 [::]:*
4.5.2 编译安装
#安装脚本
install_dir='/usr/local/'
yum install -y gcc libevent-devel
rpm -q wget &>/dev/null || yum install -y wget
wget http://memcached.org/files/memcached-1.6.9.tar.gz
tar xvf memcached-1.6.9.tar.gz
cd memcached-1.6.9
[ -d $install_dir ] || mkdir -p $install_mkdir
./configure --prefix=${install_dir}/memcached
make && make install
echo "PATH=${install_mkdir}/memcached/bin:\$PATH" >/etc/profile.d/memcached.sh
id memcached &>/dev/null || useradd -r -s /sbin/nologin memcached
cat >/etc/sysconfig/memcached<<EOF
PORT="11211"
USER="memcached"
MAXCONN="1024"
CACHESIZE="64"
#OPTIONS="-l 127.0.0.1,::1"
OPTIONS=""
EOF
cat >/lib/systemd/system/memcached.service<<EOF
[Unit]
Description=memcached daemon
Before=httpd.service
After=network.target
[Service]
EnvironmentFile=/etc/sysconfig/memcached
ExecStart=${install_dir}/memcached/bin/memcached -p \${PORT} -u \${USER} -m \${CACHESIZE} -c \${MAXCONN} \$OPTIONS
PrivateTmp=true
ProtectSystem=full
NoNewPrivileges=true
PrivateDevices=true
CapabilityBoundingSet=CAP_SETGID CAP_SETUID CAP_SYS_RESOURCE
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
[Install]
WantedBy=multi-user.target
EOF
systemctl daemon-reload
echo "安装完成"
4.5.3 memcached 启动程序说明
修改memcached 运行参数,可以使用下面的选项修改/etc/sysconfig/memcached文件
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
4.6 使用 memcached
4.6.1 memcached 开发库和工具
与memcached通信的不同语言的连接器。libmemcached提供了C库和命令行工具。
范例: 查看memcached相关包
[15:36:27 root@centos8 ~]#yum list "*memcached*"
协议
查看/usr/share/doc/memcached-1.4.15/protocol.txt
[15:36:47 root@centos8 ~]#dnf info libmemcached
[15:38:14 root@centos8 ~]#yum install libmemcached
[15:38:01 root@centos8 ~]#memstat --servers=192.168.10.84
4.6.2 memcached 操作命令
帮助文档:
[root@centos8 ~]#cat /usr/share/doc/memcached/protocol.txt
五种基本 memcached 命令执行最简单的操作。这些命令和操作包括:
- set
- add
- replace
- get
- delete
#前三个命令是用于操作存储在 memcached 中的键值对的标准修改命令,都使用如下所示的语法:
command
#参数说明如下:
command set/add/replace
key key 用于查找缓存值
flags 可以包括键值对的整型参数,客户机使用它存储关于键值对的额外信息
expiration time 在缓存中保存键值对的时间长度(以秒为单位,0 表示永远)
bytes 在缓存中存储的字节点
value 存储的值(始终位于第二行)
#增加key,过期时间为秒,bytes为存储数据的字节数
add key flags exptime bytes
范例:
[15:40:01 root@centos8 ~]#telnet localhost 11211
Trying ::1...
Connected to localhost.
Escape character is '^]'.
stats
add mykey 1 60 4
test
STORED
get mykey
VALUE mykey 1 4
test
END
set mykey 1 60 5
test1
STORED
get mykey
VALUE mykey 1 5
test1
END
delete mykey
DELETED
get mykey
END
flush_all
OK
get mykey
END
quit
4.6.3 python 语言连接 memcached
4.6.3.1 范例: python3 测试代码
[15:48:04 root@centos8 ~]#cat m2.py
#!/usr/bin/env python
import memcache
m = memcache.Client(['127.0.0.1:11211'], debug=True)
for i in range(10):
m.set("key%d" % i,"v%d" % i)
ret = m.get('key%d' % i)
print("%s" % ret)
[15:48:09 root@centos8 ~]#python3 m2.py
v0
v1
v2
v3
v4
v5
v6
v7
v8
v9
4.6.3.2 范例: python2 测试代码
[root@centos7 ~]#yum -y install python-memcached
[root@centos7 ~]#cat m2.py
#!/usr/bin/env python
#coding:utf-8
import memcache
m = memcache.Client(['127.0.0.1:11211'], debug=True)
for i in range(10):
m.set("key%d" % i,"v%d" % i)
ret = m.get('key%d' % i)
print ret
[root@centos7 ~]#python m2.py
v0
v1
v2
v3
v4
v5
v6
v7
v8
v9
4.7 memcached 集群部署架构
4.7.1 基于 magent 的部署架构
Magent是google开发的项目,应用端通过负载均衡服务器连接到magent,然后再由magent代理用户应用请求到memcached处理,底层的memcached为双主结构会自动同步数据,本部署方式存在magent单点问题,因此需要两个magent做高可用。
项目站点:https://code.google.com/archive/p/memagent/
此项目过于陈旧,且不稳定,当前已经废弃
4.7.2 Repcached 实现原理
项目站点:http://repcached.sourceforge.net/
在 master上可以通过 -X 选项指定 replication port(默认为11212/tcp),在 slave上通过 -x 指定复制的master并连接,事实上,如果同时指定了 -x/-X, repcached先会尝试连接对端的master,但如果连接失败,它就会用 -X参数来自己 listen(成为master);如果 master坏掉, slave侦测到连接断了,它会自动 listen而成为 master;而如果 slave坏掉,master也会侦测到连接断开,它就会重新 listen等待新的 slave加入。
从这方案的技术实现来看,其实它是一个单 master单 slave的方案,但它的 master/slave都是可读写的,而且可以相互同步,所以从功能上看,也可以认为它是双机 master-master方案
4.7.3 简化后的部署架构
magent已经有很长时间没有更新,因此可以不再使用magent,直接通过负载均衡连接到memcached,仍然有两台memcached做高可用,repcached版本的memcached之间会自动同步数据,以保持数据一致性,即使其中的一台memcached故障也不影响业务正常运行,故障的memcached修复上线后再自动从另外一台同步数据即可保持数据一致性。
4.7.4 部署repcached
[15:55:52 root@centos7 ~]#yum -y install gcc libevent libevent-devel
[16:02:35 root@centos7 ~]#wget http://sourceforge.net/projects/repcached/files/repcached/2.2.1-1.2.8/memcached-1.2.8-repcached-2.2.1.tar.gz/download
[16:02:50 root@centos7 ~]#tar xf memcached-1.2.8-repcached-2.2.1.tar.gz
[16:02:56 root@centos7 ~]#cd memcached-1.2.8-repcached-2.2.1/
[16:02:59 root@centos7 memcached-1.2.8-repcached-2.2.1]#./configure --prefix=/usr/local/repcached --enable-replication
[16:04:07 root@centos7 memcached-1.2.8-repcached-2.2.1]#make #报错如下
解决办法:
[16:04:10 root@centos7 memcached-1.2.8-repcached-2.2.1]#vim memcache
56 #ifndef IOV_MAX
57 #if defined(__FreeBSD__) || defined(__APPLE__)
58 # define IOV_MAX 1024
59 #endif
60 #endif
#改为如下内容,即删除原有的原第57,59行
56 #ifndef IOV_MAX
57 # define IOV_MAX 1024
58 #endif
再次编译安装:
[16:07:22 root@centos7 memcached-1.2.8-repcached-2.2.1]#make && make install
[16:07:05 root@centos7 ~]#tree /usr/local/repcached/
/usr/local/repcached/
├── bin
│ ├── memcached
│ └── memcached-debug
└── share
└── man
└── man1
└── memcached.1
4 directories, 3 files
4.7.5 验证是否可执行
[16:07:43 root@centos7 ~]#echo 'PATH=/usr/local/repcached/bin:$PATH' >/etc/profile.d/repcached.sh
[16:08:52 root@centos7 ~]#memcached -h
memcached 1.2.8
repcached 2.2.1
-p TCP port number to listen on (default: 11211)
-U UDP port number to listen on (default: 11211, 0 is off)
-s unix socket path to listen on (disables network support)
-a access mask for unix socket, in octal (default 0700)
.....
4.7.6 启动 memcached
4.7.6.1 server1 相关操作
#创建用户
[16:09:46 root@centos7 ~]#useradd -r -s /sbin/nologin memcached
#后台启动
#-x 192.168.10.72为对端memcached的地址
[16:18:34 root@centos7 ~]#memcached -d -m 128 -p 11211 -u memcached -c 128 -x 192.168.10.72
[16:19:51 root@centos7 ~]#ss -ntl
State Recv-Q Send-Q Local Address:Port Peer Address:Port
LISTEN 0 128 *:111 *:*
LISTEN 0 128 *:22 *:*
LISTEN 0 100 127.0.0.1:25 *:*
LISTEN 0 128 *:11211 *:*
LISTEN 0 128 [::]:111 [::]:*
LISTEN 0 128 [::]:22 [::]:*
LISTEN 0 100 [::1]:25 [::]:*
LISTEN 0 128 [::]:11211 [::]:*
4.7.6.2 server2 相关操作
[16:18:31 root@centos7 ~]#memcached -d -m 128 -p 11211 -u memcached -c 128 -x 192.168.10.71
[16:19:42 root@centos7 ~]#ss -ntl
State Recv-Q Send-Q Local Address:Port Peer Address:Port
LISTEN 0 128 *:22 *:*
LISTEN 0 100 127.0.0.1:25 *:*
LISTEN 0 128 *:11211 *:*
LISTEN 0 128 *:111 *:*
LISTEN 0 128 [::]:22 [::]:*
LISTEN 0 100 [::1]:25 [::]:*
LISTEN 0 128 [::]:11211 [::]:*
LISTEN 0 128 [::]:111 [::]:*
4.7.7 连接到 memcached 验证数据
4.7.7.1 shell 命令测试
#在第一台memcached上创建数据
[16:22:05 root@centos7 ~]#telnet 127.0.0.1 11211
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
add name 0 0 5
zhang
STORED
get name
VALUE name 0 5
zhang
#到另外一台memcached服务器验证是否有同步过来的数据
[16:22:39 root@centos7 ~]#telnet 127.0.0.1 11211
Trying 127.0.0.1...
Connected to 127.0.0.1.
Escape character is '^]'.
get name
VALUE name 0 5
zhang
END
set age 0 0 2
20
STORED
#在第一台memcached上验证实现双向复制
get age
VALUE age 0 2
20
五、session 共享服务器
5.1 msm 介绍
msm(memcached session manager)提供将Tomcat的session保持到memcached或redis的程序,可以实现高可用。
项目早期托管在google code,目前在Github
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数据的序列化、反序列化类
- 官方推荐kyro
- 在webapp中WEB-INF/lib/下
- 驱动类
- memcached(spymemcached.jar)
- Redis(jedis.jar)
5.2 安装
参考链接: 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。
kryo-3.0.3.jar
asm-5.2.jar
objenesis-2.6.jar
reflectasm-1.11.9.jar
minlog-1.3.1.jar
kryo-serializers-0.45.jar
msm-kryo-serializer-2.3.2.jar
memcached-session-manager-tc8-2.3.2.jar
spymemcached-2.12.3.jar
memcached-session-manager-2.3.2.jar
5.3 sticky 模式
5.3.1 sticky 模式工作原理
sticky 模式即前端tomcat和后端memcached有关联(粘性)关系
参考文档:https://github.com/magro/memcached-session-manager/wiki/SetupAndConfiguration
Tomcat-1(t1)主要将其会话存储在另一台计算机上运行的memcached-2(m2)中(m2是t1的常规节
点)。 仅当m2不可用时,t1才会将其会话存储在memcached-1中(m1,m1是t1的failoverNode)。 使
用此配置,当计算机1(服务于t1和m1)崩溃时,会话不会丢失。 以下非常好的ASCII艺术显示了此设置。
<t1> <t2>
. \ / .
. X .
. / \ .
<m1> <m2>
t1和m1部署可以在一台主机上,t2和m2部署也可以在同一台。
当新用户发请求到Tomcat1时, Tomcat1生成session返回给用户的同时,也会同时发给memcached2备份。即Tomcat1 session为主session,memcached2 session为备用session,使用memcached相当于备份了一份Session
如果Tomcat1发现memcached2 失败,无法备份Session到memcached2,则将Sessoin备份存放在memcached1中
5.3.2 配置过程
5.3.2.1 下载相关jar包
下载相关jar包,参考下面官方说明的下载链接
https://github.com/magro/memcached-session-manager/wiki/SetupAndConfiguration
tomcat和memcached相关包
序列化相关下载
5.3.2.2 修改tomcat配置
修改 $CATALINA_HOME/conf/context.xml
特别注意,t1配置中为failoverNodes="n1", t2配置为failoverNodes="n2
#以下是sticky的配置
<Context>
...
<Manager className="de.javakaffee.web.msm.MemcachedBackupSessionManager"
memcachedNodes="n1:192.168.10.82:11211,n2:192.168.10.83:11211"
failoverNodes="n1"
requestUriIgnorePattern=".*\.(ico|png|gif|jpg|css|js)$"
transcoderFactoryClass="de.javakaffee.web.msm.serializer.kryo.KryoTranscoderFac
tory"
/>
</Context>
配置说明
memcachedNodes="n1:host1.yourdomain.com:11211,n2:host2.yourdomain.com:11211"
memcached的节点: n1、n2只是别名,可以重新命名。
failoverNodes 为故障转移节点,n1是备用节点,n2是主存储节点。另一台Tomcat将n1改为n2,其主节
点是n1,备用节点是n2。
如果配置成功,可以在logs/catalina.out中看到下面的内容
12-APR-2020 16:24:08.975 INFO [t1.magedu.com-startStop-1]
de.javakaffee.web.msm.MemcachedSessionService.startInternal --------
- finished initialization:
- sticky: true
- operation timeout: 1000
- node ids: [n2]
- failover node ids: [n1]
- storage key prefix: null
- locking mode: null (expiration: 5s)
5.3.2.3 准备测试msm的python脚本
t1、t2、n1、n2依次启动成功,分别使用http://t1.magedu.org:8080/ 和http://t2.magedu.org:8080/观察。
开启负载均衡调度器,通过http://proxy.magedu.com来访问看看效果
可以看到浏览器端被调度到不同Tomcat上,但是都获得了同样的SessionID。
停止t2、n2看看效果,恢复看看效果。
范例:访问tomcat,查看memcached中SessionID信息
#!/bin/python3
import memcache
mc = memcache.Client(['10.0.0.101: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表示全部
5.3.3 实战案例 1 : tomcat和memcached集成在一台主机
环境准备:
- 时间同步,确保NTP或Chrony服务正常运行。
- 防火墙规则
- 禁用SELinux
- 三台主机
5.3.3.1 配置nginx充当proxy
upstream tomcat-server {
#ip_hash;
#hash $cookie_JSESSIONID;
server t1.zhangzhuo.org:8080;
server t2.zhangzhuo.org:8080;
}
location / {
proxy_pass http://tomcat-server;
}
[17:31:51 root@proxy ~]#cat /etc/hosts
192.168.10.81 proxy.zhangzhuo.org proxy
192.168.10.82 t1.zhangzhuo.org
192.168.10.83 t2.zhangzhuo.org
5.3.3.2 配置memcached
[17:22:21 root@t1 tomcat]#cat /etc/sysconfig/memcached
PORT="11211"
USER="memcached"
MAXCONN="1024"
CACHESIZE="64"
#注释下面行
#OPTIONS="-l 127.0.0.1,::1"
[17:32:33 root@t1 tomcat]#systemctl enable --now memcached.service
5.3.3.3 配置 tomcat
配置 tomcat1
[17:34:39 root@t1 tomcat]#vim conf/server.xml
<Host name="t1.zhangzhuo.org" appBase="/data/webapps"
unpackWARs="true" autoDeploy="true">
[17:34:39 root@t1 tomcat]#vim conf/context.xml
###倒数第一行前,即</Context>行的前面,加下面内容
<Manager className="de.javakaffee.web.msm.MemcachedBackupSessionManager"
memcachedNodes="n1:192.168.10.82:11211,n2:192.168.10.83:11211"
failoverNodes="n1"
requestUriIgnorePattern=".*\.(ico|png|gif|jpg|css|js)$"
transcoderFactoryClass="de.javakaffee.web.msm.serializer.kryo.KryoTranscoderFactory" />
#将相关包传到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
[17:58:10 root@t1 ~]#ll -t /usr/local/tomcat/lib/ | tail -11
-rw-r--r-- 1 tomcat tomcat 53259 Aug 22 2020 asm-5.2.jar
-rw-r--r-- 1 tomcat tomcat 126366 Aug 22 2020 kryo-serializers-0.45.jar
-rw-r--r-- 1 tomcat tomcat 38372 Aug 22 2020 msm-kryo-serializer-2.3.2.jar
-rw-r--r-- 1 tomcat tomcat 285211 Aug 22 2020 kryo-3.0.3.jar
-rw-r--r-- 1 tomcat tomcat 5923 Aug 22 2020 minlog-1.3.1.jar
-rw-r--r-- 1 tomcat tomcat 55684 Aug 22 2020 objenesis-2.6.jar
-rw-r--r-- 1 tomcat tomcat 72265 Aug 22 2020 reflectasm-1.11.9.jar
-rw-r--r-- 1 tomcat tomcat 586620 Aug 22 2020 jedis-3.0.0.jar
-rw-r--r-- 1 tomcat tomcat 167294 Aug 22 2020 memcached-session-manager-2.3.2.jar
-rw-r--r-- 1 tomcat tomcat 10826 Aug 22 2020 memcached-session-manager-tc8-2.3.2.jar
-rw-r--r-- 1 tomcat tomcat 473774 Aug 22 2020 spymemcached-2.12.3.jar
[17:37:03 root@t1 tomcat]#systemctl restart tomcat.service
配置 tomcat2
#t2参考上面t1做配置
<Manager className="de.javakaffee.web.msm.MemcachedBackupSessionManager"
memcachedNodes="n1:192.168.10.82:11211,n2:192.168.10.83:11211"
failoverNodes="n2"
requestUriIgnorePattern=".*\.(ico|png|gif|jpg|css|js)$"
transcoderFactoryClass="de.javakaffee.web.msm.serializer.kryo.KryoTranscoderFactory"/>
#将相关包传到lib/目录下
查看tomcat日志
[18:01:44 root@t2 tomcat]#cat logs/catalina.out
- finished initialization:
- sticky: true
- operation timeout: 1000
- node ids: [n1]
- failover node ids: [n2]
- storage key prefix: null
- locking mode: null (expiration: 5s)
[18:02:05 root@t1 ~]#cat /usr/local/tomcat/logs/catalina.out
- finished initialization:
- sticky: true
- operation timeout: 1000
- node ids: [n2]
- failover node ids: [n1]
- storage key prefix: null
- locking mode: null (expiration: 5s)
5.3.3.4 python测试脚本
在t1 上安装部署python3环境,访问memcached
[18:05:57 root@t1 ~]#yum install python3 python3-memcached
#准备python测试脚本
#!/usr/bin/python3
import memcache # pip install python-memcached
mc = memcache.Client(['192.168.10.82:11211','192.168.10.83:11211'], debug=True)
print('-' * 30) #查看全部 key
#for x in mc.get_stats('items'): # stats items 返回 items:5:number 1
# print(x)
#print('-' * 30)
for x in mc.get_stats('cachedump 5 0'):
print(x)
5.4 non-sticky 模式
5.4.1 non-sticky 模式工作原理
non-sticky 模式即前端tomcat和后端memcached无关联(无粘性)关系
从msm 1.4.0之后版本开始支持non-sticky模式。
Tomcat session为中转Session,对每一个SessionID随机选中后端的memcached节点n1(或者n2)为主session,而另一个memcached节点n2(或者是n1)为备session。产生的新的Session会发送给主、备memcached,并清除本地Session。
后端两个memcached服务器对一个session来说是一个是主,一个是备,但对所有session信息来说每个memcached即是主同时也是备
如果n1下线,n2则转正。n1再次上线,n2依然是主Session存储节点
5.4.2 memcached配置
放到 $CATALINA_HOME/conf/context.xml 中
<Manager className="de.javakaffee.web.msm.MemcachedBackupSessionManager"
memcachedNodes="n1:192.168.10.88:11211,n2:192.168.10.89:11211"
sticky="false"
sessionBackupAsync="false"
lockingMode="uriPattern:/path1|/path2"
requestUriIgnorePattern=".*\.(ico|png|gif|jpg|css|js)$"
transcoderFactoryClass="de.javakaffee.web.msm.serializer.kryo.KryoTranscoderFactory"
/>
5.4.3 redis 配置
支持将session存放在Redis中,但当前对Redis的支持不允许连接到多个Redis节点,可以通过Redis的集群服务实现防止redis的单点失败
参考文档 :
https://github.com/ran-jit/tomcat-cluster-redis-session-manager/wiki
下载 jedis.jar,放到 $CATALINA_HOME/lib/ ,对应本次安装就是/usr/local/tomcat/lib。
[15:07:49 root@tomcat1 ~]#yum install -y redis
[15:08:30 root@tomcat1 ~]#vim /etc/redis.conf
bind 0.0.0.0
[15:10:17 root@tomcat1 ~]#systemctl enable --now redis
放到 $CATALINA_HOME/conf/context.xml 中
<Manager className="de.javakaffee.web.msm.MemcachedBackupSessionManager"
memcachedNodes="redis://redis.example.com"
sticky="false"
sessionBackupAsync="false"
lockingMode="uriPattern:/path1|/path2"
requestUriIgnorePattern=".*\.(ico|png|gif|jpg|css|js)$"
transcoderFactoryClass="de.javakaffee.web.msm.serializer.kryo.KryoTranscoderFactory"
/>
浏览器访问,使用redis相关工具可以观察到redis中的信息
5.4.4 实战案例: memcached 实现non-sticky模式
5.4.4.1 修改tomcat配置
#在前面实验基础上修改,memcached配置不变,只需要修改tomcat配置
[15:15:17 root@tomcat1 tomcat]#vim conf/context.xml
<Manager className="de.javakaffee.web.msm.MemcachedBackupSessionManager"
memcachedNodes="n1:192.168.10.88:11211,n2:192.168.10.89:11211"
sticky="false"
sessionBackupAsync="false"
lockingMode="uriPattern:/path1|/path2"
requestUriIgnorePattern=".*\.(ico|png|gif|jpg|css|js)$"
transcoderFactoryClass="de.javakaffee.web.msm.serializer.kryo.KryoTranscoderFactory"/>
[15:14:57 root@tomcat1 tomcat]#systemctl restart tomcat.service
#查看日志
- 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)
#t2和t1相同操作
#运行脚本查看key
[15:18:13 root@tomcat1 ~]#python3 py_memcached.py
------------------------------
('192.168.10.88:11211 (1)', {'426C05715FA18C99CA2C7F4EC476D12A-n1.tomcat2': '[97 b; 1616141731 s]'})
('192.168.10.89:11211 (1)', {'bak:426C05715FA18C99CA2C7F4EC476D12A-n1.tomcat2': '[97 b; 1616141731 s]'})
#再次运行脚本后可以看到,t1为memcached的主节点,t2为备份节点
5.4.5 实战案例: redis 实现 non-sticky 模式的msm
5.4.5.1 上传redis库到tomcat服务器
[15:16:43 root@tomcat2 tomcat]#ll lib/jedis-3.0.0.jar
-rw-r--r-- 1 tomcat tomcat 586620 Aug 22 2020 lib/jedis-3.0.0.jar
5.4.5.2 安装并配置 Redis 服务
[15:07:49 root@tomcat1 ~]#yum install -y redis
[15:08:30 root@tomcat1 ~]#vim /etc/redis.conf
bind 0.0.0.0
[15:10:17 root@tomcat1 ~]#systemctl enable --now redis
5.4.5.3 修改tomcat 配置指定redis服务器地址
[15:32:47 root@tomcat1 tomcat]#vim conf/context.xml
<Manager className="de.javakaffee.web.msm.MemcachedBackupSessionManager"
memcachedNodes="redis://192.168.10.88:6379" #和non-sticky的memcached相比,只修改此行
sticky="false"
sessionBackupAsync="false"
lockingMode="uriPattern:/path1|/path2"
requestUriIgnorePattern=".*\.(ico|png|gif|jpg|css|js)$"
transcoderFactoryClass="de.javakaffee.web.msm.serializer.kryo.KryoTranscoderFactory"/>
#查看日志
[15:36:06 root@tomcat1 tomcat]#cat logs/catalina.out
finished initialization:
- sticky: false
- operation timeout: 1000
- node ids: []
- failover node ids: []
- storage key prefix: null
- locking mode: uriPattern:/path1|/path2 (expiration: 5s)
#t2和t1配置相同
5.4.5.4 测试访问
浏览器刷新访问多次,主机轮询,但SessionID不变
使用redis工具连接redis 查看SessionID
[15:39:01 root@tomcat1 tomcat]#redis-cli
127.0.0.1:6379> KEYS *
1) "A531E5656BB9D23FA10F9433A7D0F517.tomcat2"
2) "validity:A531E5656BB9D23FA10F9433A7D0F517.tomcat2"
127.0.0.1:6379> KEYS *
3) "A531E5656BB9D23FA10F9433A7D0F517.tomcat2"
4) "validity:C8EF4EA10928F7D77B58FEF5F597FD1C.tomcat1"
5) "C8EF4EA10928F7D77B58FEF5F597FD1C.tomcat1"
6) "validity:A531E5656BB9D23FA10F9433A7D0F517.tomcat2"
5.5 Session 问题方案总结
通过多组实验,使用不同技术实现了session持久机制
- session绑定,基于IP或session cookie的。其部署简单,尤其基于session黏性的方式,粒度小,对负载均衡影响小。但一旦后端服务器有故障,其上的session丢失。
- session复制集群,基于tomcat实现多个服务器内共享同步所有session。此方法可以保证任意一台后端服务器故障,其余各服务器上还都存有全部session,对业务无影响。但是它基于多播实现心跳,TCP单播实现复制,当设备节点过多,这种复制机制不是很好的解决方案。且并发连接多的时候,单机上的所有session占据的内存空间非常巨大,甚至耗尽内存。
- session服务器,将所有的session存储到一个共享的内存空间中,使用多个冗余节点保存session,这样做到session存储服务器的高可用,且占据业务服务器内存较小。是一种比较好的解决session持久的解决方案。
以上的方法都有其适用性。生产环境中,应根据实际需要合理选择。
不过以上这些方法都是在内存中实现了session的保持,可以使用数据库或者文件系统,把session数据存储起来,持久化。这样服务器重启后,也可以重新恢复session数据。不过session数据是有时效性的,是否需要这样做,视情况而定。
六、Tomcat 性能优化
在目前流行的互联网架构中,Tomcat在目前的网络编程中是举足轻重的,由于Tomcat的运行依赖于JVM,从虚拟机的角度把Tomcat的调整分为外部环境调优 JVM 和 Tomcat 自身调优两部分
6.1 JVM组成
[15:53:34 root@tomcat1 tomcat]#java -version
java version "1.8.0_281"
Java(TM) SE Runtime Environment (build 1.8.0_281-b09)
Java HotSpot(TM) 64-Bit Server VM (build 25.281-b09, mixed mode)
6.1.1 JVM组成
JVM 组成部分
- 类加载子系统: 使用Java语言编写.java Source Code文件,通过javac编译成.class Byte Code文件。class loader类加载器将所需所有类加载到内存,必要时将类实例化成实例
- 运行时数据区: 最消耗内存的空间,需要优化
- 执行引擎: 包括JIT (JustInTimeCompiler)即时编译器, GC垃圾回收器
- 本地方法接口: 将本地方法栈通过JNI(Java Native Interface)调用Native Method Libraries, 比如:C,C++库等,扩展Java功能,融合不同的编程语言为Java所用
JVM运行时数据区域由下面部分构成:
- Method Area 方法区(线程共享): 所有线程共享的内存空间,存放已加载的类信息(构造方法,接口定义),常量(final),静态变量(static), 运行时常量池等。但实例变量存放在堆内存中. 从JDK8开始此空间由永久代改名为元空间
- heap 堆(线程共享): 虚拟机启动时创建,存放创建的所有对象信息。如果对象无法申请到可用内存将抛出OOM异常.堆是靠GC垃圾回收器管理的,通过-Xmx -Xms 指定最大堆和最小堆空间大小
- Java stack Java栈(线程私有): 每个线程会分配一个栈,存放java中8大基本数据类型,对象引用,实例的本地变量,方法参数和返回值等,基于FILO()(First In Last Out),每个方法为一个栈帧
- Program Counter Register PC寄存器(线程私有): 就是一个指针,指向方法区中的方法字节码,每一个线程用于记录当前线程正在执行的字节码指令地址。由执行引擎读取下一条指令.因为线程需要切换,当一个线程被切换回来需要执行的时候,知道执行到哪里了
- Native Method stack 本地方法栈(线程私有): 为本地方法执行构建的内存空间,存放本地方法执行时的局部变量、操作数等。所谓本地方法,使用native 关健字修饰的方法,比如:Thread.sleep方法. 简单的说是非Java实现的方法,例如操作系统的C编写的库提供的本地方法,Java调用这些本地方法接口执行。但是要注意,本地方法应该避免直接编程使用,因为Java可能跨平台使用,如果用了Windows API,换到了Linux平台部署就有了问题
6.1.2 虚拟机
目前Oracle官方使用的是HotSpot, 它最早由一家名为"Longview Technologies"公司设计,使用了很多优秀的设计理念和出色的性能,1997年该公司被SUN公司收购。后来随着JDK一起发布了HotSpotVM。目前HotSpot是最主要的VM。
安卓程序需要运行在JVM上,而安卓平台使用了Google自研的Java虚拟机——Dalvid,适合于内存、处理器能力有限系统。
6.2 GC (Garbage Collection) 垃圾收集器
在堆内存中如果创建的对象不再使用,仍占用着内存,此时即为垃圾.需要即使进行垃圾回收,从而释放内存空间给其它对象使用
其实不同的开发语言都有垃圾回收问题,C,C++需要程序员人为回收,造成开发难度大,容易出错等问题,但执行效率高,而JAVA和Python中不需要程序员进行人为的回收垃圾,而由JVM或相关程序自动回收垃圾,减轻程序员的开发难度,但可能会造成执行效率低下
堆内存里面经常创建、销毁对象,内存也是被使用、被释放。如果不妥善处理,一个使用频繁的进程,可能会出现虽然有足够的内存容量,但是无法分配出可用内存空间,因为没有连续成片的内存了,内存全是碎片化的空间。
所以需要有合适的垃圾回收机制,确保正常释放不再使用的内存空间,还需要保证内存空间尽可能的保持
一定的连续
对于垃圾回收,需要解决三个问题
- 哪些是垃圾要回收
- 怎么回收垃圾
- 什么时候回收垃圾
6.2.1 Garbage 垃圾确定方法
- 引用计数: 每一个堆内对象上都与一个私有引用计数器,记录着被引用的次数,引用计数清零,该对象所占用堆内存就可以被回收。循环引用的对象都无法将引用计数归零,就无法清除。Python中即使用此种方式
- 根搜索(可达)算法 Root Searching
6.2.2 垃圾回收基本算法
6.2.2.1 标记-清除 Mark-Sweep
分垃圾标记阶段和内存释放阶段。标记阶段,找到所有可访问对象打个标记。清理阶段,遍历整个堆,对未标记对象(即不再使用的对象)清理。
标记-清除最大的问题会造成内存碎片,但是效率很高,不浪费空间
6.2.2.2 标记-压缩 (压实)Mark-Compact
分垃圾标记阶段和内存整理阶段。标记阶段,找到所有可访问对象打个标记。内存清理阶段时,整理时将对象向内存一端移动,整理后存活对象连续的集中在内存一端
标记-压缩算法好处是整理后内存空间连续分配,有大段的连续内存可分配,没有内存碎片。
缺点是内存整理过程有消耗,效率相对低下
6.2.2.3 复制 Copying
先将可用内存分为大小相同两块区域A和B,每次只用其中一块,比如A。当A用完后,则将A中存活的对象复制到B。复制到B的时候连续的使用内存,最后将A一次性清除干净。
缺点是比较浪费内存,只能使用原来一半内存,因为内存对半划分了,复制过程毕竟也是有代价。
好处是没有碎片,复制过程中保证对象使用连续空间
6.2.2.4 多种算法总结
没有最好的算法,在不同场景选择最合适的算法
- 效率: 复制算法>标记清除算法> 标记压缩算法
- 内存整齐度: 复制算法=标记压缩算法> 标记清除算法
- 内存利用率: 标记压缩算法=标记清除算法>复制算法
STW
对于大多数垃圾回收算法而言,GC线程工作时,停止所有工作的线程,称为Stop The World。GC 完成时,恢复其他工作线程运行。这也是JVM运行中最头疼的问题。
6.2.3 分代堆内存GC策略
上述垃圾回收算法都有优缺点,能不能对不同数据进行区分管理,不同分区对数据实施不同回收策略,分而治之。
6.2.3.1 堆内存分代
将heap内存空间分为三个不同类别: 年轻代、老年代、持久代
Heap堆内存分为
- 年轻代Young:Young Generation
- 伊甸园区eden: 只有一个,刚刚创建的对象
- 幸存(存活)区Servivor Space:有2个幸存区,一个是from区,一个是to区。大小相等、地位相同、可互换。
- from 指的是本次复制数据的源区
- to 指的是本次复制数据的目标区
- 老年代Tenured:Old Generation, 长时间存活的对象
默认空间大小比例:
永久代: JDK1.7之前使用, 即Method Area方法区,保存JVM自身的类和方法,存储JAVA运行时的环境信息,JDK1.8后 改名为 MetaSpace,此空间不存在垃圾回收,关闭JVM会释放此区域内存,此空间物理上不属于heap内存,但逻辑上存在于heap内存
- 永久代必须指定大小限制,字符串常量JDK1.7存放在永久代,1.8后存放在heap中
- MetaSpace 可以设置,也可不设置,无上限
规律: 一般情况99%的对象都是临时对象
范例: 在tomcat 状态页可以看到以下的内存分代
范例: 查看JVM内存分配情况
[19:46:16 root@tomcat1 tomcat]#cat Heap.java
public class Heap {
public static void main(String[] args){
//返回虚拟机试图使用的最大内存,字节单位
long max = Runtime.getRuntime().maxMemory();
//返回JVM初始化总内存
long total = Runtime.getRuntime().totalMemory();
System.out.println("max="+max+"字节\t"+(max/(double)1024/1024)+"MB");
System.out.println("total="+total+"字节\t"+(total/(double)1024/1024)+"MB");
}
}
[19:46:49 root@tomcat1 tomcat]#javac Heap.java
[19:47:02 root@tomcat1 tomcat]#java -classpath . Heap
max=243269632字节 232.0MB
total=16252928字节 15.5MB
[19:48:07 root@tomcat1 tomcat]#java -XX:+PrintGCDetails -classpath . Heap
max=243269632字节 232.0MB
total=16252928字节 15.5MB
Heap
def new generation total 4928K, used 530K [0x00000000f1000000, 0x00000000f1550000, 0x00000000f6000000)
eden space 4416K, 12% used [0x00000000f1000000, 0x00000000f1084b00, 0x00000000f1450000)
from space 512K, 0% used [0x00000000f1450000, 0x00000000f1450000, 0x00000000f14d0000)
to space 512K, 0% used [0x00000000f14d0000, 0x00000000f14d0000, 0x00000000f1550000)
tenured generation total 10944K, used 0K [0x00000000f6000000, 0x00000000f6ab0000, 0x0000000100000000)
the space 10944K, 0% used [0x00000000f6000000, 0x00000000f6000000, 0x00000000f6000200, 0x00000000f6ab0000)
Metaspace used 2525K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 269K, capacity 386K, committed 512K, reserved 1048576K
[19:48:15 root@tomcat1 tomcat]#echo "scale=2;(4928+10944)/1024" |bc
15.50
#说明年轻代+老年代占用了所有heap空间, Metaspace实际不占heap空间,逻辑上存在于Heap
#默认JVM试图分配最大内存的总内存的1/4,初始化默认总内存为总内存的1/64
6.2.3.2 年轻代回收 Minor GC
- 起始时,所有新建对象(特大对象直接进入老年代)都出生在eden,当eden满了,启动GC。这个称为Young GC 或者 Minor GC。
- 先标记eden存活对象,然后将存活对象复制到s0(假设本次是s0,也可以是s1,它们可以调换),eden剩余所有空间都清空。GC完成。
- 继续新建对象,当eden满了,启动GC。
- 先标记eden和s0中存活对象,然后将存活对象复制到s1。将eden和s0清空,此次GC完成
- 继续新建对象,当eden满了,启动GC。
- 先标记eden和s1中存活对象,然后将存活对象复制到s0。将eden和s1清空,此次GC完成
以后就重复上面的步骤。
通常场景下,大多数对象都不会存活很久,而且创建活动非常多,新生代就需要频繁垃圾回收。
但是,如果一个对象一直存活,它最后就在from、to来回复制,如果from区中对象复制次数达到阈值(默认15次,CMS为6次,可通过java的选项 -XX:MaxTenuringThreshold=N 指定),就直接复制到老年代。
6.2.3.3 老年代回收 Major GC
进入老年代的数据较少,所以老年代区被占满的速度较慢,所以垃圾回收也不频繁。
如果老年代也满了,会触发老年代GC,称为Old GC或者 Major GC。
由于老年代对象一般来说存活次数较长,所以较常采用标记-压缩算法。
当老年代满时,会触发 Full GC,即对所有"代"的内存进行垃圾回收
Minor GC比较频繁,Major GC较少。但一般Major GC时,由于老年代对象也可以引用新生代对象,所以先进行一次Minor GC,然后在Major GC会提高效率。可以认为回收老年代的时候完成了一次FullGC。
所以可以认为 MajorGC = FullGC
6.2.3.4 GC 触发条件
Minor GC 触发条件:当eden区满了触发
Full GC 触发条件:
- 老年代满了
- System.gc()手动调用。不推荐
年轻代:
- 存活时长低
- 适合复制算法
老年代:
- 区域大,存活时长高
- 适合标记清除和标记压缩算法
6.2.4 java 内存调整相关参数
6.2.4.1 JVM 内存常用相关参数
Java 命令行参考文档: https://docs.oracle.com/javase/8/docs/technotes/tools/unix/java.html
帮助:man java
选项分类
- -选项名称 此为标准选项,所有HotSpot都支持
- -X选项名称 此为稳定的非标准选项
- -XX:选项名称 非标准的不稳定选项,下一个版本可能会取消
范例: 查看java的选项帮助
#查看java命令标准选项
[19:54:24 root@tomcat1 ~]#java
#查看java的非标准选项
[19:54:24 root@tomcat1 ~]#java -X
#查看所有不稳定选项的当前生效值
[19:55:31 root@tomcat1 ~]#java -XX:+PrintFlagsFinal
#查看所有不稳定选项的默认值
[19:56:02 root@tomcat1 ~]#java -XX:+PrintFlagsInitial
#查看当前命令行的使用的选项设置
[19:56:40 root@tomcat1 ~]#java -XX:+PrintCommandLineFlags
-XX:InitialHeapSize=15598016 -XX:MaxHeapSize=249568256 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops
#上面的-XX:+UseParallelGC 说明当前使用Parallel Scavenge + Parallel Old
范例: 查看OOM
[19:58:38 root@tomcat1 ~]#cat HeapOom2.java
import java. util. Random;
public class HeapOom2 {
public static void main(String[] args) {
String str = "I am lao wang";
while (true){
str += str + new Random().nextInt(88888888);
}
}
}
[19:58:47 root@tomcat1 ~]#javac HeapOom2.java
[20:03:02 root@tomcat1 ~]#java -Xms100m -Xmx100m -XX:+PrintGCDetails -classpath . HeapOom2
[GC (Allocation Failure) [DefNew: 27099K->2929K(30720K), 0.0128489 secs] 27099K->5601K(99008K), 0.0128768 secs] [Times: user=0.00 sys=0.02, real=0.01 secs]
[GC (Allocation Failure) [DefNew: 30190K->0K(30720K), 0.0503294 secs] 32862K->13617K(99008K), 0.0503605 secs] [Times: user=0.00 sys=0.02, real=0.05 secs]
[GC (Allocation Failure) [DefNew: 10688K->0K(30720K), 0.0436912 secs] 24305K->24305K(99008K), 0.0437188 secs] [Times: user=0.00 sys=0.03, real=0.05 secs]
[GC (Allocation Failure) [DefNew: 21376K->0K(30720K), 0.0688476 secs] 45681K->45681K(99008K), 0.0688720 secs] [Times: user=0.00 sys=0.06, real=0.07 secs]
[GC (Allocation Failure) [DefNew: 21844K->0K(30720K), 0.0844690 secs] 67525K->67057K(99008K), 0.0844960 secs] [Times: user=0.00 sys=0.05, real=0.08 secs]
[GC (Allocation Failure) [DefNew: 21376K->21376K(30720K), 0.0000175 secs][Tenured: 67057K->34992K(68288K), 0.0102542 secs] 88433K->34992K(99008K), [Metaspace: 2479K->2479K(1056768K)], 0.0104149 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
[Full GC (Allocation Failure) [Tenured: 34992K->32307K(68288K), 0.0067376 secs] 34992K->32307K(99008K), [Metaspace: 2479K->2479K(1056768K)], 0.0067600 secs] [Times: user=0.00 sys=0.01, real=0.00 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3332)
at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:124)
at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:674)
at java.lang.StringBuilder.append(StringBuilder.java:208)
at HeapOom2.main(HeapOom2.java:6)
Heap
def new generation total 30720K, used 1038K [0x00000000f9c00000, 0x00000000fbd50000, 0x00000000fbd50000)
eden space 27328K, 3% used [0x00000000f9c00000, 0x00000000f9d03928, 0x00000000fb6b0000)
from space 3392K, 0% used [0x00000000fba00000, 0x00000000fba00000, 0x00000000fbd50000)
to space 3392K, 0% used [0x00000000fb6b0000, 0x00000000fb6b0000, 0x00000000fba00000)
tenured generation total 68288K, used 32307K [0x00000000fbd50000, 0x0000000100000000, 0x0000000100000000)
the space 68288K, 47% used [0x00000000fbd50000, 0x00000000fdcdcc80, 0x00000000fdcdce00, 0x0000000100000000)
Metaspace used 2513K, capacity 4486K, committed 4864K, reserved 1056768K
class space used 268K, capacity 386K, committed 512K, reserved 1048576K
6.2.4.2 JDK 工具监控使用情况
Jprofiler定位OOM的问题原因
JProfiler官网:http://www.ej-technologies.com/products/jprofiler/overview.html
JProfiler是一款功能强大的Java开发分析工具,它可以快速的帮助用户分析出存在的错误,软件还可对需要的显示类进行标记,包括了内存的分配情况和信息的视图等
范例: 安装jprofiler工具定位OOM原因和源码问题的位置
#安装OpenJDK
[root@centos8 ~]#dnf -y install java-1.8.0-openjdk-devel
[20:10:38 root@tomcat1 ~]#java -Xms100m -Xmx100m -XX:+HeapDumpOnOutOfMemoryError -classpath . HeapOom2
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid7118.hprof ...
Heap dump file created [33830224 bytes in 0.135 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at java.util.Arrays.copyOf(Arrays.java:3332)
at java.lang.AbstractStringBuilder.ensureCapacityInternal(AbstractStringBuilder.java:124)
at java.lang.AbstractStringBuilder.append(AbstractStringBuilder.java:674)
at java.lang.StringBuilder.append(StringBuilder.java:208)
at HeapOom2.main(HeapOom2.java:6)
[20:10:41 root@tomcat1 ~]#ls
HeapOom2.class HeapOom2.java java_pid7118.hprof
下载并安装Jprofiler
6.2.4.3 Tomcat的JVM参数设置
默认不指定,-Xmx大约使用了1/4的内存,当前本机内存指定约为1G。
在bin/catalina.sh中增加一行
......
# OS specific support. $var _must_ be set to either true or false.
#添加下面一行
JAVA_OPTS="-server -Xms128m -Xmx512m -XX:NewSize=48m -XX:MaxNewSize=200m"
cygwin=false
darwin=false
........
-server:VM运行在server模式,为在服务器端最大化程序运行速度而优化
-client:VM运行在Client模式,为客户端环境减少启动时间而优化
重启 Tomcat 观察
[14:27:11 root@tomcat1 tomcat]#ps aux | grep tomcat
tomcat 1636 23.8 22.7 2520556 221596 ? Sl 14:25 0:19 /usr/local/jdk/bin/java -Djava.util.logging.config.file=/usr/local/tomcat/conf/logging.properties -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager -server -Xms128m -Xmx512m -XX:NewSize=48m -XX:MaxNewSize=200m -Djdk.tls.ephemeralDHKeySize=2048 -Djava.protocol.handler.pkgs=org.apache.catalina.webresources -Dorg.apache.catalina.security.SecurityListener.UMASK=0027 -Dignore.endorsed.dirs= -classpath /usr/local/tomcat/bin/bootstrap.jar:/usr/local/tomcat/bin/tomcat-juli.jar -Dcatalina.base=/usr/local/tomcat -Dcatalina.home=/usr/local/tomcat -Djava.io.tmpdir=/usr/local/tomcat/temp org.apache.catalina.startup.Bootstrap start
root 1686 0.0 0.1 12112 1080 pts/0 R+ 14:27 0:00 grep --color=auto tomcat
浏览器访问server status页面,可以看到以下页面
6.2.5 垃圾收集方式
按工作模式不同:指的是GC线程和工作线程是否一起运行
- 独占垃圾回收器:只有GC在工作,STW 一直进行到回收完毕,工作线程才能继续执行
- 并发垃圾回收器:让GC线程垃圾回收某些阶段可以和工作线程一起进行,如:标记阶段并行,回收阶段仍然串行
按回收线程数:指的是GC线程是否串行或并行执行
- 串行垃圾回收器:一个GC线程完成回收工作
- 并行垃圾回收器:多个GC线程同时一起完成回收工作,充分利用CPU资源
6.2.6 调整策略
对JVM调整策略应用极广
- 在WEB领域中Tomcat等
- 在大数据领域Hadoop生态各组件
- 在消息中间件领域的Kafka等
- 在搜索引擎领域的ElasticSearch、Solr等
注意: 在不同领域和场景对JVM需要不同的调整策略
- 减少 STW 时长,串行变并行
- 减少 GC 次数,要分配合适的内存大小
一般情况下,我们大概可以使用以下原则:
- 客户端或较小程序,内存使用量不大,可以使用串行回收
- 对于服务端大型计算,可以使用并行回收
- 大型WEB应用,用户端不愿意等待,尽量少的STW,可以使用并发回收
6.2.7 垃圾回收器
6.2.7.1 常用垃圾回收器
按分代设置不同垃圾回收器
新生代
-
新生代串行收集器Serial:单线程、独占式串行,采用复制算法,简单高效但会造成STW
-
新生代并行回收收集器PS(Parallel Scavenge):多线程并行、独占式,会产生STW, 使用复制算法
- 关注调整吞吐量,此收集器关注点是达到一个可控制的吞吐量
- 吞吐量 = 运行用户代码时间/(运行用户代码时间+垃圾收集时间),比如虚拟机总共运行100分钟,其中垃圾回收花掉1分钟,那吞吐量就是99%。
- 高吞吐量可以高效率利用CPU时间,尽快完成运算任务,主要适合在后台运算而不需要太多交互的任务。
- 除此之外,Parallel Scavenge 收集器具有自适应调节策略,它可以将内存管理的调优任务交给虚拟机去完成。自适应调节策略也是Parallel Scavenge与 ParNew 收集器的一个重要区别。
- 和ParNew不同,PS不可以和老年代的CMS组合
-
新生代并行收集器ParNew:就是Serial 收集器的多线程版,将单线程的串行收集器变成了多线程并行、独占式,使用复制算法,相当于PS的改进版
- 经常和CMS配合使用,关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,适合需要与用户交互的程序,良好的响应速度能提升用户体验
老年代:
- 经常和CMS配合使用,关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,适合需要与用户交互的程序,良好的响应速度能提升用户体验
-
老年代串行收集器Serial Old:Serial Old是Serial收集器的老年代版本,单线程、独占式串行,回收算法使用标记压缩
-
老年代并行回收收集器Parallel Old:多线程、独占式并行,回收算法使用标记压缩,关注调整吞吐量
- Parallel Old收集器是Parallel Scavenge 收集器的老年代版本,这个收集器是JDK1.6之后才开始提供,从HotSpot虚拟机的垃圾收集器的图中也可以看出,Parallel Scavenge 收集器无法与CMS收集器配合工作,因为一个是为了吞吐量,一个是为了客户体验(也就是暂停时间的缩短)
-
CMS (Concurrent Mark Sweep并发标记清除算法) 收集器
- 在某些阶段尽量使用和工作线程一起运行,减少STW时长(200ms以内), 提升响应速度,是互联网服务端BS系统上较佳的回收算法
- 分为4个阶段:初始标记、并发标记、重新标记、并发清除,在初始标记、重新标记时需要STW。
- 初始标记:此过程需要STW(Stop The Word),只标记一下GC Roots能直接关联到的对象,速度很快。
- 并发标记:就是GC Roots进行扫描可达链的过程,为了找出哪些对象需要收集。这个过程远远慢于初始标记,但它是和用户线程一起运行的,不会出现STW,所有用户并不会感受到。
- 重新标记:为了修正在并发标记期间,用户线程产生的垃圾,这个过程会比初始标记时间稍微长一点,但是也很快,和初始标记一样会产生STW。
- 并发清理:在重新标记之后,对现有的垃圾进行清理,和并发标记一样也是和用户线程一起运行的,耗时较长(和初始标记比的话),不会出现STW。
- 由于整个过程中,耗时最长的并发标记和并发清理都是与用户线程一起执行的,所以总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。
以下收集器不再按明确的分代单独设置
- G1(Garbage First)收集器
- 是最新垃圾回收器,从JDK1.6实验性提供,JDK1.7发布,其设计目标是在多处理器、大内存服务器端提供优于CMS收集器的吞吐量和停顿控制的回收器。JDK9将G1设为默认的收集器,建议 JDK9版本以后使用。
- 基于标记压缩算法,不会产生大量的空间碎片,有利于程序的长期执行。
- 分为4个阶段:初始标记、并发标记、最终标记、筛选回收。并发标记并发执行,其它阶段STW只有GC线程并行执行。
- G1收集器是面向服务端的收集器,它的思想就是首先回收尽可能多的垃圾(这也是GarbageFirst名字的由来)
- G1能充分的利用多CPU,多核环境下的硬件优势,使用多个CPU来缩短STW停顿的时间(10ms以内)。
- 可预测的停顿:这是G1相对于CMS的另一大优势,G1和CMS一样都是关注于降低停顿时间,但是G1能够让使用者明确的指定在一个M毫秒的时间片段内,消耗在垃圾收集的时间不得超过N毫秒。
- 通过此选项指定: +UseG1GC
- ZGC收集器: 减少SWT时长(1ms以内), 可以PK C++的效率,目前实验阶段
- Shenandoah收集器: 和ZGC竞争关系,目前实验阶段
- Epsilon收集器: 调试 JDK 使用,内部使用,不用于生产环境
JVM 1.8 默认的垃圾回收器:PS + ParallelOld,所以大多数都是针对此进行调优
6.2.7.2 垃圾收集器设置
优化调整Java 相关参数的目标: 尽量减少FullGC和STW
通过以下选项可以单独指定新生代、老年代的垃圾收集器
- -server 指定为Server模式,也是默认值,一般使用此工作模式
- -XX:+UseSerialGC
- 运行在Client模式下,新生代是Serial, 老年代使用SerialOld
- -XX:+UseParNewGC
- 新生代使用ParNew,老年代使用SerialOld
- -XX:+UseParallelGC
- 运行于server模式下,新生代使用Serial Scavenge, 老年代使用SerialOld
- -XX:+UseParallelOldGC
- 新生代使用Paralell Scavenge, 老年代使用Paralell Old
- -XX:ParallelGCThreads=N,在关注吞吐量的场景使用此选项增加并行线程数
- -XX:+UseConcMarkSweepGC
- 新生代使用ParNew, 老年代优先使用CMS,备选方式为Serial Old
- 响应时间要短,停顿短使用这个垃圾收集器
- -XX:CMSInitiatingOccupancyFraction=N,N为0-100整数表示达到老年代的大小的百分比多少触发回收
- 默认68
- -XX:+UseCMSCompactAtFullCollection 开启此值,在CMS收集后,进行内存碎片整理
- -XX:CMSFullGCsBeforeCompaction=N 设定多少次CMS后,进行一次内存碎片整理
- -XX:+CMSParallelRemarkEnabled 降低标记停顿
范例:
-XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=80 -
XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=5
范例: 查看默认模式
[14:27:13 root@tomcat1 tomcat]#java |& grep '-server'
-server to select the "server" VM
The default VM is server.
[14:47:13 root@tomcat1 tomcat]#tail -n 2 /usr/local/jdk/jre/lib/amd64/jvm.cfg
-server KNOWN
-client IGNORE
范例: 指定垃圾回收设置
#将参数加入到bin/catalina.sh中,重启观察Tomcat status。老年代已经使用CMS
[14:54:17 root@tomcat1 tomcat]#vim bin/catalina.sh
JAVA_OPTS="-server -Xms128m -Xmx512m -XX:NewSize=48m -XX:MaxNewSize=200m -XX:+UseConcMarkSweepGC -XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=5"
[14:54:33 root@tomcat1 tomcat]#systemctl restart tomcat.service
[14:54:53 root@tomcat1 tomcat]#ps aux | grep tomcat
tomcat 1839 97.6 19.4 2551188 189360 ? Sl 14:54 0:07 /usr/local/jdk/bin/java -Djava.util.logging.config.file=/usr/local/tomcat/conf/logging.properties -Djava.util.logging.manager=org.apache.juli.ClassLoaderLogManager -server -Xms128m -Xmx512m -XX:NewSize=48m -XX:MaxNewSize=200m -XX:+UseConcMarkSweepGC -XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=5 -Djdk.tls.ephemeralDHKeySize=2048 -Djava.protocol.handler.pkgs=org.apache.catalina.webresources -Dorg.apache.catalina.security.SecurityListener.UMASK=0027 -Dignore.endorsed.dirs= -classpath /usr/local/tomcat/bin/bootstrap.jar:/usr/local/tomcat/bin/tomcat-juli.jar -Dcatalina.base=/usr/local/tomcat -Dcatalina.home=/usr/local/tomcat -Djava.io.tmpdir=/usr/local/tomcat/temp org.apache.catalina.startup.Bootstrap start
开启垃圾回收输出统计信息,适用于调试环境的相关选项
- -XX:+PrintGC 输出GC信息
- -XX:+PrintGCDetails 输出GC详细信息
- -XX:+PrintGCTimeStamps 与前两个组合使用,在信息上加上一个时间戳
- -XX:+PrintHeapAtGC 生成GC前后椎栈的详细信息,日志会更大
注意: 以上适用调试环境,生产环境请移除这些参数,否则有非常多的日志输出
6.3 JVM相关工具
6.3.1 JVM 工具概述
$JAVA_HOME/bin下
6.3.2 jps
JVM 进程状态工具
格式
jps:Java virutal machine Process Status tool,
jps [-q] [-mlvV] [< hostid>]
-q:静默模式;
-v:显示传递给jvm的命令行参数;
-m:输出传入main方法的参数;
-l:输出main类或jar完全限定名称;
-v:显示通过flag文件传递给jvm的参数;
[< hostid>]:主机id,默认为localhost;
6.3.3 jinfo
输出给定的java进程的所有配置信息
格式:
jinfo [option] < pid>
-flags:打印 VM flags
-sysprops:to print Java system properties
-flag < name>:to print the value of the named VM flag
6.3.4 jstat
输出指定的java进程的统计信息
格式:
jstat -help|-options
jstat -< option> [-t] [-h< lines>] < vmid> [< interval> [< count>]]
[< interval> [< count>]]
interval:时间间隔,单位是毫秒;
count:显示的次数;
#返回可用统计项列表
#jstat -options
-class:class loader
-compiler:JIT
-gc:gc
-gccapacity:统计堆中各代的容量
-gccause:
-gcmetacapacity
-gcnew:新生代
-gcnewcapacity
-gcold:老年代
范例:
[15:19:58 root@tomcat1 ~]#jps
2708 Jps
2606 HelloWorld
[15:20:04 root@tomcat1 ~]#jstat -gc 2606
S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT GCT
512.0 512.0 0.0 0.0 4416.0 442.4 10944.0 0.0 4480.0 875.3 384.0 76.6 0 0.000 0 0.000 0.000
S0C:S0区容量
YGC:新生代的垃圾回收次数
YGCT:新生代垃圾回收消耗的时长;
FGC:Full GC的次数
FGCT:Full GC消耗的时长
GCT:GC消耗的总时长
#3次,一秒一次
[15:21:21 root@tomcat1 ~]#jstat -gcnew 2606 1000 3
6.3.5 jstack
程序员常用堆栈情况查看工具
jstack:查看指定的java进程的线程栈的相关信息
格式:
jstack [-l] < pid>
jstack -F [-m] [-l] < pid>
-l:long listings,会显示额外的锁信息,因此,发生死锁时常用此选项
-m:混合模式,既输出java堆栈信息,也输出C/C++堆栈信息
-F:当使用"jstack -l PID"无响应,可以使用-F强制输出信息
6.3.6 jmap
Memory Map, 用于查看堆内存的使用状态
#查看进程堆内存情况
[root@tomcat ~]#jmap -heap 21407
6.3.7 jhat
Java Heap Analysis Tool 堆分析工具
格式
jmap [option]
#查看堆空间的详细信息:
jmap -heap
6.4 Tomcat 性能优化常用配置
6.4.1 内存空间优化
JAVA_OPTS="-server -Xms4g -Xmx4g -XX:NewSize= -XX:MaxNewSize= "
-server:服务器模式
-Xms:堆内存初始化大小
-Xmx:堆内存空间上限
-XX:NewSize=:新生代空间初始化大小
-XX:MaxNewSize=:新生代空间最大值
生产案例:
[root@centos8 ~]#vim /usr/local/tomcat/bin/catalina.sh
JAVA_OPTS="-server -Xms4g -Xmx4g -Xss512k -Xmn1g -
XX:CMSInitiatingOccupancyFraction=65 -XX:+AggressiveOpts -XX:+UseBiasedLocking -XX:+DisableExplicitGC
-XX:MaxTenuringThreshold=10 -XX:NewRatio=2
-XX:PermSize=128m -XX:MaxPermSize=512m -XX:CMSFullGCsBeforeCompaction=5
-XX:+ExplicitGCInvokesConcurrent -XX:+UseConcMarkSweepGC -XX:+UseParNewGC
-XX:+CMSParallelRemarkEnabled -XX:+UseCMSCompactAtFullCollection
-XX:LargePageSizeInBytes=128m -XX:+UseFastAccessorMethods"
#一台tomcat服务器并发连接数不高,生产建议分配物理内存通常4G到8G较多,如果需要更多连接,一般会利用
虚拟化技术实现多台tomcat
6.4.2 线程池调整
[root@centos8 ~]#vim /usr/local/tomcat/conf/server.xml
< Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000"
redirectPort="8443" />
常用属性:
- connectionTimeout :连接超时时长,单位ms
- maxThreads:最大线程数,默认200
- minSpareThreads:最小空闲线程数
- maxSpareThreads:最大空闲线程数
- acceptCount:当启动线程满了之后,等待队列的最大长度,默认100
- URIEncoding:URI 地址编码格式,建议使用 UTF-8
- enableLookups:是否启用客户端主机名的DNS反向解析,缺省禁用,建议禁用,就使用客户端IP就行
- compression:是否启用传输压缩机制,建议 "on",CPU和流量的平衡
- compressionMinSize:启用压缩传输的数据流最小值,单位是字节
- compressableMimeType:定义启用压缩功能的MIME类型text/html, text/xml, text/css,text/javascript
6.4.3 Java压力测试工具
PerfMa 致力于打造一站式IT系统稳定性保障解决方案,专注于性能评测与调优、故障根因定位与解决,为企业提供一系列技术产品与专家服务,提升系统研发与运行质量。
#社区产品
https://opts.console.perfma.com/