web服务之Tomcat反向代理,负载均衡,SessionID

结合反向代理实现tomcat部署

常见部署方式介绍

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

利用 nginx 反向代理实现全部转发置指定同一个虚拟主机

配置说明

利用nginx反向代理功能,实现上图的代理功能,将用户请求全部转发至指定的同一个tomcat主机
利用nginx指令proxy_pass 可以向后端服务器转发请求报文,并且在转发时会保留客户端的请求报文中的host首部

#从yum源安装nginx
[root@tomcat ~]# yum install nginx -y
[root@tomcat ~]# vim /etc/nginx/nginx.conf
#全部反向代理测试
location / {
# proxy_pass http://127.0.0.1:8080; # 不管什么请求,都会访问后面的localhost虚拟主机
proxy_pass http://node1.longxuan.vip:8080; # 此项将用户访问全部请求转发到node1的虚拟
主机上
#proxy_pass http://node2.longxuan.vip:8080; #此项将用户访问全部请求转发到node2的虚拟
主机上
#以上两项都需要修改nginx服务器的/etc/hosts,实现node1.magedu.com和node2.magedu.com
到IP的解析
}
[root@tomcat ~]# nginx -t
[root@tomcat ~]# systemctl restart nginx
#说明: proxy_pass http://FQDN/ 中的FQDN 决定转发至后端哪个虚拟主机,而与用户请求的URL无关
#如果转到后端的哪个服务器由用户请求决定,可以向后端服务转发请求的主机头实现,示例:
proxy_set_header Host $http_host;

案例

一台主机,实现nginx和tomcat
tomcat上有两个虚拟主机node1和node2

#先按3.4.5.4介绍方式在同一下主机上建立两个tomcat虚拟主机,node1.longxuan.vip和node2.longxuan.vip

#修改/etc/hosts文件,实现名称解析
[root@centos8 ~]# vim /etc/hosts
127.0.0.1 localhost localhost.localdomain localhost4 localhost4.localdomain4
centos8.localdomain
::1 localhost localhost.localdomain localhost6 localhost6.localdomain6
172.31.0.100 node1.longxuan.vip node2.longxuan.vip

#安装nginx
[root@centos8 ~]# yum -y install nginx

#修改nginx.conf配置文件
[root@centos8 ~]# vim /etc/nginx/nginx.conf
......
#修改location / 此行,添加以下内容
location / {
#proxy_pass http://127.0.0.1:8080;
proxy_pass http://node1.longxuan.vip:8080;
}
......

[root@centos8 ~]# systemctl enable --now nginx
#先别访问node1,node2和IP都可以看到一样的node1的虚拟主机页面
[root@centos8 ~]# curl http://node1.longxuan.vip/
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>jsp例子</title>
</head>
<body>
后面的内容是服务器端动态生成字符串,最后拼接在一起
node1.longxuan.vip
</body>
</html>
[root@centos8 ~]# curl http://node2.longxuan.vip/
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>jsp例子</title>
</head>
<body>
后面的内容是服务器端动态生成字符串,最后拼接在一起
node2.longxuan.vip
</body>
</html>
[root@centos8 ~]# curl http://127.0.0.1/
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>jsp例子</title>
</head>
<body>
后面的内容是服务器端动态生成字符串,最后拼接在一起
node1.longxuan.vip
</body>
</html>
[root@centos8 ~]# curl http://172.31.0.100/
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>jsp例子</title>
</head>
<body>
后面的内容是服务器端动态生成字符串,最后拼接在一起
node1.longxuan.vip
</body>
</html>
[root@centos8 ~]# systemctl restart nginx

#再次修改nginx.conf配置文件
[root@centos8 ~]# vim /etc/nginx/nginx.conf
......
#修改location / 行,添加以下内容
location / {
#proxy_pass http://127.0.0.1:8080;
proxy_pass http://node2.longxuan.vip:8080;
}
......

#先别访问node1,node2和IP都可以看到一样的node2的虚拟主机页面
[root@centos8 ~]# curl http://node1.longxuan.vip/
[root@centos8 ~]# curl http://node2.longxuan.vip/
[root@centos8 ~]# curl http://127.0.0.1/
[root@centos8 ~]# curl http://172.31.0.100/
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>jsp例子</title>
</head>
<body>
后面的内容是服务器端动态生成字符串,最后拼接在一起
node2.longxuan.vip
</body>
</html>

利用nginx实现动静分离代理

配置说明

可以利用nginx实现动静分离

[root@centos8 ~]# vim nginx.conf
root /usr/share/nginx/html;
#下面行可不加
#location / {
# root /data/webapps/ROOT;
# index index.html;
#}
# ~* 不区分大小写
location ~* \.jsp$ {
    proxy_pass http://node1.longxuan.vip:8080; #注意: 8080后不要加/,需要在nginx服务器修改 /etc/hosts
}

以上设置,可以将jsp的请求反向代理到tomcat,而其它文件仍由nginx处理,从而实现所谓动静分离。
但由于jsp文件中实际上是由静态资源和动态组成,所以无法彻底实现动静分离。实际上Tomcat不太适
合做动静分离,用它来管理程序的图片不好做动静分离部署

案例

#准备三个不同的资源文件
[root@centos8 ~]# echo /usr/local/tomcat/webapps/ROOT/test.html >
/usr/local/tomcat/webapps/ROOT/test.html
[root@centos8 ~]# echo /usr/local/tomcat/webapps/ROOT/test.jsp >
/usr/local/tomcat/webapps/ROOT/test.jsp
[root@centos8 ~]# echo /usr/share/nginx/html/test.html >
/usr/share/nginx/html/test.html

[root@centos8 ~]# vim /etc/nginx/nginx.conf
......
root /usr/share/nginx/html;
#location / {
#
#}
location ~* \.jsp$ {
    proxy_pass http://127.0.0.1:8080;
}
......
[root@centos8 ~]# systemctl restart nginx
#访问test.html,观察结果都一样访问的是nginx自身的资源
[root@centos8 ~]# curl http://node1.longxuan.vip/test.html
[root@centos8 ~]# curl http://node2.longxuan.vip/test.html
[root@centos8 ~]# curl http://127.0.0.1/test.html
[root@centos8 ~]# curl http://172.31.0.8/test.html
/usr/share/nginx/html/test.html

#访问test.jsp,观察结果都一样访问的是tomcat的默认主机资源
[root@centos8 ~]# curl http://node1.longxuan.vip/test.jsp
[root@centos8 ~]# curl http://node2.longxuan.vip/test.jsp
[root@centos8 ~]# curl http://127.0.0.1/test.jsp
[root@centos8 ~]# curl http://172.31.0.8/test.jsp
/usr/local/tomcat/webapps/ROOT/test.jsp

利用httpd实现基于http协议的反向代理至后端Tomcat服务器

配置说明

httpd也提供了反向代理功能,所以可以实现对tomcat的反向代理功能
范例:查看代理相关模块

[root@centos8 ~]# httpd -M|grep proxy
AH00558: httpd: Could not reliably determine the server's fully qualified domain
name, using centos8.localdomain. Set the 'ServerName' directive globally to
suppress this message
proxy_module (shared)
proxy_ajp_module (shared) #ajp
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) #http
proxy_hcheck_module (shared)
proxy_scgi_module (shared)
proxy_uwsgi_module (shared)
proxy_wstunnel_module (shared)
proxy_http2_module (shared)

proxy_http_module模块代理配置

[root@centos8 ~]# vim /etc/httpd/conf.d/http-tomcat.conf
<VirtualHost *:80>
ServerName node1.longxuan.vip
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.longxuan.vip/
http://node1.longxuan.vip/index.jsp
#以上3个URL看到了不同的页面,说明ProxyPreserveHost On起了作用
#设置ProxyPreserveHost Off再看效果,说明什么?

利用 httpd 实现基于AJP协议的反向代理至后端Tomcat服务器

AJP 协议说明

AJP(Apache JServ Protocol)是定向包协议,是一个二进制的TCP传输协议,相比HTTP这种纯文本的
协议来说,效率和性能更高,也做了很多优化。但是浏览器并不能直接支持AJP13协议,只支持HTTP协
议。所以实际情况是,通过Apache的proxy_ajp模块进行反向代理,暴露成http协议给客户端访问

启用和禁用 AJP

注意: Tomcat/8.5.51之后版本基于安全需求默认禁用AJP协议

范例: Tomcat/8.5.51之后版启用支持AJP协议

[root@centos8 tomcat]# vim conf/server.xml
#取消前面的注释,并修改下面行,修改address和secretRequired
<Connector protocol="AJP/1.3" address="0.0.0.0" port="8009"
redirectPort="8443" secretRequired="" />

[root@centos8 tomcat]# systemctl restart tomcat
[root@centos8 tomcat]# ss -ntl

注意: 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协议
[root@centos8 ~]# ss -ntl

#配置tomcat配置文件,删除下面一行
[root@centos8 ~]# vim /usr/local/tomcat/conf/server.xml
<Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />

[root@centos8 ~]# systemctl restart tomcat
[root@centos8 ~]# ss -ntl

httpd 实现 AJP 反向代理

配置说明

相对来讲,AJP协议基于二进制比使用HTTP协议的连接器效率高些。
proxy_ajp_module模块代理配置

<VirtualHost *:80>
    ServerName node1.longxuan.vip
    ProxyRequests Off
    ProxyVia On
    ProxyPreserveHost On
    ProxyPass / ajp://127.0.0.1:8009/
</VirtualHost>

查看Server Status可以看到确实使用的是ajp连接了

案例范例:启用httpd的AJP反向代理功能

[root@centos8 ~]# vim /etc/httpd/conf.d/tomcat.conf
[root@centos8 ~]# cat /etc/httpd/conf.d/tomcat.conf
<VirtualHost *:80>
ServerName node1.llongxuan.vip
ProxyRequests Off
ProxyVia On #此项对AJP无效
ProxyPreserveHost On #此项对AJP无效
ProxyPass / ajp://127.0.0.1:8009/
</VirtualHost>

[root@centos8 ~]# systemctl restart httpd
#再次用用下面不同URL访问,可以看以下结果
[root@centos8 ~]# curl http://node1.longxuan.vip/test.html
/data/node1/ROOT/test.html

[root@centos8 ~]# curl http://node2.longxuan.vip/test.html
/data/node2/ROOT/test.html

[root@centos8 ~]# curl http://172.31.0.8/test.html
/usr/local/tomcat/webapps/ROOT/test.html

[root@centos8 ~]# curl http://127.0.0.1/test.html
/usr/local/tomcat/webapps/ROOT/test.html

[root@centos8 ~]# vim /etc/httpd/conf.d/tomcat.conf
#只修改下面一行,关闭向后端转发请求的host首部
ProxyPreserveHost Off

#再次用用下面不同URL访问,可以看到和上面一样的结果,说明AJP协议和Http不同,自动转发所有首部信息
[root@centos8 ~]# curl http://node1.longxuan.vip/test.html
/data/node1/ROOT/test.html

[root@centos8 ~]# curl http://node2.longxuan.vip/test.html
/data/node2/ROOT/test.html

[root@centos8 ~]# curl http://172.31.0.8/test.html
/usr/local/tomcat/webapps/ROOT/test.html

[root@centos8 ~]# curl http://127.0.0.1/test.html
/usr/local/tomcat/webapps/ROOT/test.html

可以通过status页面看到AJP的信息

#用iptables禁用AJP的访问
[root@centos8 ~]# iptables -A INPUT -p tcp --dport 8009 -j REJECT
[root@centos8 ~]# curl http://node1.longxuan.vip/test.html
<!DOCTYPE HTML PUBLIC "-//IETF//DTD HTML 2.0//EN">
<html><head>
<title>503 Service Unavailable</title>
</head><body>
<h1>Service Unavailable</h1>
<p>The server is temporarily unable to service your
request due to maintenance downtime or capacity
problems. Please try again later.</p>
</body></html>

实现 tomcat 负载均衡

动态服务器的问题,往往就是并发能力太弱,往往需要多台动态服务器一起提供服务。如何把并发的压
力分摊,这就需要调度,采用一定的调度策略,将请求分发给不同的服务器,这就是Load Balance负载
均衡。
当单机Tomcat,演化出多机多级部署的时候,一个问题便凸显出来,这就是Session。而这个问题的由
来,都是由于HTTP协议在设计之初没有想到未来的发展。

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的连接创建销毁成本高,对服务器有很大的影响。所以,自Http1.1开始,支持keep-alive,默认也开启,一个连接打开后,会保持一段时间(可设置),浏览器再访问该服务器就使用这个Tcp连接,减轻了服务器压力,提高了效率。

服务器端如果故障,即使Session被持久化了,但是服务没有恢复前都不能使用这些SessionID。

如果使用HAProxy或者Nginx等做负载均衡器,调度到了不同的Tomcat上,那么也会出现找不到
SessionID的情况。

会话保持方式

session sticky会话黏性

Session绑定
    nginx:source ip, cookie
    HAProxy:source ip, cookie
优点:简单易配置
缺点:如果目标服务器故障后,如果没有做sessoin持久化,就会丢失session,此方式生产很少使用

Session 复制集群

Tomcat自己的提供的多播集群,通过多播将任何一台的session同步到其它节点。

缺点
Tomcat的同步节点不宜过多,互相即时通信同步session需要太多带宽
每一台都拥有全部session,内存损耗太多

Session Server

session 共享服务器,使用memcached、redis做共享的Session服务器,此为推荐方式

范例:

#只需在172.31.0.100的nginx主机上实现域名解析
[root@centos8 ~]# vim /etc/hosts
#添加以下三行
172.31.0.100 proxy.longxuan.vip proxy
172.31.0.101 t1.longxua.vip t1
172.31.0.102 t2.longxuan.vip t2

负载均衡tomcat主机准备

修改tomcat的虚拟机主机为自定义的主机名,并设为默认的虚拟主机
t1虚拟主机配置conf/server.xml

<Engine name="Catalina" defaultHost="t1.longxuan.vip">
    <Host name="t1.longxuan.vip" appBase="/data/webapps" autoDeploy="true" >
    </Host>
</Engine>

t2虚拟主机配置conf/server.xml

<Engine name="Catalina" defaultHost="t2.longxuan.vip">
    <Host name="t2.longxuan.vip" appBase="/data/webapps" autoDeploy="true" >
    </Host>
</Engine>

准备负载均衡规划测试用的jsp文件

在t1和 t2节点创建相同的文件/data/webapps/ROOT/index.jsp

#项目路径配置
[root@centos8 ~]# 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>

#设置权限
[root@centos8 ~]# chown -R tomcat.tomcat /data/webapps/

Nginx 实现后端 tomcat 的负载均衡调度

Nginx 实现后端 tomcat 的负载均衡

nginx 配置如下

[root@centos8 ~]# vim /etc/nginx/nginx.conf
#在http块中加以下内容
#注意名称不要用下划线
upstream tomcat-server {
    #ip_hash; # 先禁用看看轮询,之后开启开黏性
    #hash $cookie_JSESSIONID; # 先禁用看看轮询,之后开启开黏性
    server t1.longxuan.vip:8080;
    server t2.longxuan.vip:8080;
}
server {
    location ~* \.(jsp|do)$ {
        proxy_pass http://tomcat-server;
    }
}

测试 http://proxy.longxuan.vip/index.jsp,可以看到轮询调度效果,每次刷新后端主机和SessionID都会变化

[root@proxy ~]# curl http://proxy.longxuan.vip/index.jsp
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>tomcat test</title>
</head>
<body>
<h1> tomcat website </h1>
<div>On tomcat-server</div>
<div>172.31.0.101:8080</div>
<div>SessionID = <span
style="color:blue">2E4BFA5135497EA3628F1EBDA56493E</span></div>
Thu Jul 09 17:58:06 CST 2021
</body>
</html>
[root@proxy ~]#curl http://proxy.longxuan.vip/index.jsp
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>tomcat test</title>
</head>
<body>
<h1> tomcat website </h1>
<div>On tomcat-server</div>
<div>172.31.0.102:8080</div>
<div>SessionID = <span
style="color:blue">C5CC437BC05EE5A8620822C4578E71B7C</span></div>
Thu Jul 09 17:58:07 CST 2021
</body>
</html>

实现 session 黏性

在upstream中使用ip_hash指令,使用客户端IP地址Hash。

[root@proxy ~]# vim /etc/nginx/nginx.conf
#只添加ip_hash;这一行
upstream tomcat-server {
    ip_hash; #启动源地址hash
    #hash $cookie_JSESSIONID #启动基于cookie的hash
    server t1.longxuan.vip:8080;
    server t2.longxuan.vip:8080;
}

配置完reload nginx服务。curl 测试一下看看效果。

#用curl访问每次都调度到172.31.0.102主机上,但因为curl每次请求不会自动携带之前获取的cookie,所有SessionID每次都在变化
[root@proxy ~]# curl http://proxy.longxuan.vip/index.jsp
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>tomcat test</title>
</head>
<body>
<h1> tomcat website </h1>
<div>On tomcat-server</div>
<div>172.31.0.102:8080</div>
<div>SessionID = <span
style="color:blue">C471641C26865B08B2FDA970BE7C71A6</span></div>
Thu Jul 09 18:02:48 CST 2021
</body>
</html>
[root@proxy ~]#curl http://proxy.longxuan.vip/index.jsp
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>tomcat test</title>
</head>
<body>
<h1> tomcat website </h1>
<div>On tomcat-server</div>
<div>172.31.0.102:8080</div>
<div>SessionID = <span
style="color:blue">3F61232DFD791A94D60D0D2E9561309A</span></div>
Thu Jul 09 18:02:52 CST 2021
</body>
</html>

通过图形浏览器看到主机不变,sessionID不变

关闭Session对应的Tomcat服务,再重启启动它,看看Session的变化

[root@t2 ~]# systemctl restart tomcat

通过浏览器看到主机不变,但sessionID和上一次变化,但后续刷新不再变化

Httpd 实现后端tomcat的负载均衡调度

和nginx一样, httpd 也支持负载均衡调度功能

httpd 的负载均衡配置说明

使用httpd -M 可以看到proxy_balancer_module,用它来实现负载均衡。

官方帮助: http://httpd.apache.org/docs/2.4/mod/mod_proxy_balancer.html
方式 依赖模块
http负载均衡 mod_proxy,mod_proxy_http,mod_proxy_balancer
ajp负载均衡 ,mod_proxy,mod_proxy_ajp,mod_proxy_balancer

负载均衡配置说明

#配置代理到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 指令参数

参数 缺省值 说明
min 0 连接池最小容量
max 1 - n 连接池最大容量
retry 60 apache请求发送到后端服务器错误后等待的时间秒数。0表示立即重试
Balancer 参数
参数 缺省值 说明
loadfactor 定义负载均衡后端服务器权重,取值范围: 1 - 100
lbmethod byrequests 负载均衡调度方法。byrequests 基于权重的统计请求个数进行调度bytrafficz 执行基于权重的流量计数调度bybusyness 通过考量每个后端服务器当前负载进行调度
maxattempts 1 放弃接受请求前,实现故障转移的次数,默认为1,其最大值不应该大于总的节点数
nofailover Off On 表示不允许故障转移, 如果后端服务器没有Session副本可以设置为On,Off表示故障可以转移
stickysession 调度器的sticky session名字,根据web后台编程语言不同,可以设置为JSESSIONID或PHPSESSIONID
ProxySet指令也可以使用上面的参数。

启用 httpd 的负载均衡

在 tomcat 的配置中Engine使用jvmRoute属性,通过此项可得知SessionID是在哪个tomcat生成

#t1的conf/server.xml配置如下:
<Engine name="Catalina" defaultHost="t1.longxuan.vip" jvmRoute="Tomcat1">
<Host name="t1.longxuan.vip" appBase="/data/webapps" autoDeploy="true">
</Host>
#t2的conf/server.xml配置如下:
<Engine name="Catalina" defaultHost="t2.longxuan.vip" jvmRoute="Tomcat2">
<Host name="t2.longxuan.vip" appBase="/data/webapps" autoDeploy="true">
</Host>

这样设置后 SessionID 就变成了以下形式:

SessionID = 9C949FA4AFCBE9337F5F0669548B36DF.Tomcat1

httpd配置如下

[root@proxy ~]# vim /etc/httpd/conf.d/tomcat.conf
[root@proxy ~]# cat /etc/httpd/conf.d/tomcat.conf
<Proxy balancer://tomcat-server>
    BalancerMember http://t1.longxuan.vip:8080 loadfactor=1
    BalancerMember http://t2.longxuan.vip:8080 loadfactor=2
</Proxy>
<VirtualHost *:80>
    ServerName proxy.longxuan.vip
    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,便于观察。观察调度的结果是轮询的。

实现 session 黏性

官方文档:http://httpd.apache.org/docs/2.4/mod/mod_proxy_balancer.html

%{BALANCER_WORKER_ROUTE}e The route of the worker chosen.

范例:

[root@proxy ~]# 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.longxuan.vip:8080 loadfactor=1 route=T1 #修改行,指定后端
服务器对应的ROUTEID
    BalancerMember http://t2.longxuan.vip:8080 loadfactor=1 route=T2 #修改行
    ProxySet stickysession=ROUTEID #添加此行,指定用cookie中的ROUTEID值做为调度条件
</Proxy>
<VirtualHost *:80>
    ServerName proxy.longxuan.vip
    ProxyRequests Off
    ProxyVia On
    ProxyPreserveHost On
    ProxyPass / balancer://tomcat-server/
    ProxyPassReverse / balancer://tomcat-server/
</VirtualHost>
#开启httpd负载均衡的状态页
<Location /balancer-manager>
    SetHandler balancer-manager
    ProxyPass !
    Require all granted
</Location>

用浏览器访问发现Session不变了,一直找的同一个Tomcat服务器

#观察结果
[root@proxy ~]# curl http://proxy.longxuan.vip/index.jsp #轮询
[root@proxy ~]# curl -b "ROUTEID=.T1" http://proxy.longxuan.vip/index.jsp #固定调度到
t1
[root@proxy ~]# curl -b "ROUTEID=.T2" http://proxy.longxuan.vip/index.jsp
[root@centos7 ~]# curl http://proxy.longxuan.vip/index.jsp -I
HTTP/1.1 200
Date: Wed, 15 Jul 2021 00:55:05 GMT
Server: Apache/2.4.37 (centos)
Content-Type: text/html;charset=ISO-8859-1
Transfer-Encoding: chunked
Set-Cookie: JSESSIONID=214D58236E1BD3095814B86A65C46C8A.Tomcat1; Path=/;
HttpOnly
Via: 1.1 proxy.longxuan.vip
Set-Cookie: ROUTEID=.T1; path=/
[root@centos7 ~]# curl -b "JSESSIONID=214D58236E1BD3095814B86A65C46C8A.Tomcat1;
ROUTEID=.T1" http://proxy.longxuan.vip/index.jsp

重新启动 tomcat2 的tomcat服务,再查看保持不变

[root@t2 ~]# systemctl start tomcat

实现 AJP 协议的负载均衡

在上面基础上修改httpd的配置文件

#在t1和t2的tomcat-8.5.51以上版本的需启用AJP
[root@centos8 tomcat]# vim conf/server.xml
#取消前面的注释,并修改下面行
<Connector protocol="AJP/1.3" address="0.0.0.0" port="8009"
redirectPort="8443" secretRequired="" />

[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 ajp://t1.longxuan.vip:8009 loadfactor=1 route=T1 #修改此行
    BalancerMember ajp://t2.longxuan.vip:8009 loadfactor=1 route=T2 #修改此行
#ProxySet stickysession=ROUTEID #先注释此行
</Proxy>
<VirtualHost *:80>
    ServerName proxy.longxuan.vip
    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服务器

[root@proxy ~]# vim /etc/httpd/conf.d/tomcat.conf
<Proxy balancer://tomcat-server>
    BalancerMember ajp://t1.longxuan.vip:8009 loadfactor=1 route=T1
    BalancerMember ajp://t2.longxuan.vip:8009 loadfactor=2 route=T2
    ProxySet stickysession=ROUTEID #取消此行注释,只修改此行
</Proxy>

[root@proxy ~]# systemctl restart httpd

多次刷新页面,不再变化

虽然,上面的做法实现客户端在一段时间内找同一台Tomcat,从而避免切换后导致的Session丢失。但是如果Tomcat节点挂掉,那么Session依旧丢失。

[root@t1 ~]# systemctl stop tomcat

再恢复t1,观察到仍不会变化

[root@t1 ~]# systemctl start tomcat

结论:
假设有A、B两个节点,都将Session持久化。如果Tomcat A节点下线期间用户切换到了Tomcat B上,
就获得了Tomcat B的Session,原有Sesssion将丢失,就算将持久化Session的Tomcat A节点再次上
线了,也没用了。因此需要实现Session的高可用性来解决上述问题。

Tomcat 官方实现了 Session 的复制集群,将每个Tomcat的Session进行相互的复制同步,从而保证所有Tomcat都有相同的Session信息.

配置说明

官方文档:

https://tomcat.apache.org/tomcat-9.0-doc/cluster-howto.html
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"/> #故障阈值3s
    <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.MessageDispatchIntercep
tor"/>
</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侦听器
    
使用<Cluster className="org.apache.catalina.ha.tcp.SimpleTcpCluster"/>
添加到<Engine> 所有虚拟主机都可以启用Session复制
添加到<Host> ,该虚拟主机可以启用Session复制
最后,在应用程序内部启用了才可以使用

案例: 实现 Tomcat Session 集群

环境准备:

IP 主机名 服务 软件
172.31.0.100 proxy.longxuan.vip 调度器 Nginx、HTTPD
172.31.0.101 t1.longxuan.vip tomcat1 JDK8、Tomcat8
172.31.0.102 t2.longxuan.vip tomcat2 JDK8、Tomcat8
时间同步,确保NTP或Chrony服务正常运行
防火墙规则
在 proxy 主机设置 httpd (或nginx)实现后端tomcat主机轮询
[root@proxy ~]# cat /etc/httpd/conf.d/tomcat.conf
<Proxy balancer://tomcat-server>
BalancerMember http://t1.longxuan.vip:8080 loadfactor=1
BalancerMember http://t2.longxuan.vip:8080 loadfactor=1
</Proxy>
<VirtualHost *:80>
    ServerName proxy.longxuan.vip
    ProxyRequests Off
    ProxyVia On
    ProxyPreserveHost On
    ProxyPass / balancer://tomcat-server/
    ProxyPassReverse / balancer://tomcat-server/
</VirtualHost>

[root@proxy ~]# systemctl restart httpd

在所有后端tomcat主机上修改conf/server.xml

本次把多播复制的配置放到t1.longxuan.vip和t2.longxuan.vip虚拟主机里面, 即Host块中。
特别注意修改Receiver的address属性为一个本机可对外的IP地址。

修改 t1 主机的 conf/server.xml

#将5.1 内容复制到conf/server.xml的Host块内或Engine块(针对所有主机)
[root@t1 ~]# vim /usr/local/tomcat/conf/server.xml
[root@t1 ~]# cat /usr/local/tomcat/conf/server.xml
.....以上省略.....
<Host name="t1.longxuan.vip" appBase="/data/webapps" unpackWARs="true"
autoDeploy="true">
###################在<Host> </host>块中间加下面一段内容##############################
<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="172.31.0.101" #指定网卡的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.MessageDispatchIntercep
tor"/>
</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>
#######################################以上内容是新加的#################################
</Host>
</Engine>
</Service>
</Server>

[root@t1 ~]# systemctl restart tomcat
[root@t1 ~]# ss -ntl

简化说明

t1的conf/server.xml中,如下

<Host name="t1.longxuan.vip" appBase="/data/webapps" autoDeploy="true" >
#其他略去
    <Receiver className="org.apache.catalina.tribes.transport.nio.NioReceiver"
        address="172.31.0.101" #只改此行
        port="4000"
        autoBind="100"
        selectorTimeout="5000"
        maxThreads="6"/>

修改 t2 主机的 conf/server.xml

[root@t2 ~]# vim /usr/local/tomcat/conf/server.xml
[root@t2 ~]# cat /usr/local/tomcat/conf/server.xml
.....以上省略.....
<Host name="localhost" appBase="webapps"
unpackWARs="true" autoDeploy="true">
<!-- SingleSignOn valve, share authentication between web applications
Documentation at: /docs/config/valve.html -->
<!--
<Valve className="org.apache.catalina.authenticator.SingleSignOn" />
-->
<!-- Access log processes all example.
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 &quot;%r&quot; %s %b" />
</Host>
<Host name="t2.longxuan.vip" appBase="/data/webapps" autoDeploy="true" >
###################在<Host> </host>块中间加下面一段内容##############################
<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"
Receiver className="org.apache.catalina.tribes.transport.nio.NioReceiver"
address="172.31.0.102" #此行指定当前主机的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.MessageDispatchIntercep
tor"/>
</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>
#######################################以上内容是新加的#################################
</Host>
</Engine>
</Service>
</Server>

[root@t2 ~]# systemctl restart tomcat
[root@t2 ~]# ss -ntl

简化说明

t2主机的server.xml中,如下

<Host name="t2.longxuan.vip" appBase="/data/webapps" autoDeploy="true" >
其他略去
    <Receiver className="org.apache.catalina.tribes.transport.nio.NioReceiver"
        address="172.31.0.102" #只改此行
        port="4000"
        autoBind="100"
        selectorTimeout="5000"
        maxThreads="6"/>

尝试使用刚才配置过得负载均衡(移除Session黏性),测试发现Session还是变来变去。

修改应用的web.xml文件开启该应用程序的分布式

参考官方说明: https://tomcat.apache.org/tomcat-8.5-doc/cluster-howto.html

Make sure your web.xml has the <distributable/> element

为所有tomcat主机应用web.xml的 标签增加子标签 来开启该应用程序的分布式。

修改t1主机的应用的web.xml文件
[root@t1 ~]# ll /usr/local/tomcat/webapps/ROOT/WEB-INF/
total 4
-rw-r----- 1 tomcat tomcat 1227 Jul 1 05:53 web.xml
[root@t1 ~]# cp -a /usr/local/tomcat/webapps/ROOT/WEB-INF/ /data/webapps/ROOT/
[root@t1 ~]# tree /data/webapps/ROOT/
/data/webapps/ROOT/
├── index.jsp
└── WEB-INF
    └── web.xml
1 directory, 2 files

#在倒数第二行加一行
[root@t1 ~]# vim /data/webapps/ROOT/WEB-INF/web.xml
[root@t1 ~]# tail -n3 /data/webapps/ROOT/WEB-INF/web.xml
</description>
<distributable/> #添加此行
</web-app>

#注意权限
[root@t1 ~]# ll /data/webapps/ROOT/WEB-INF/
total 4
-rw-r----- 1 tomcat tomcat 1243 Jan 17 09:37 web.xml
[root@t1 ~]# systemctl restart tomcat

#同时观察日志
[root@t1 ~]# tail -f /usr/local/tomcat/logs/catalina.out
15-Jul-2021 11:29:10.998 INFO [Membership-MemberAdded.]
org.apache.catalina.ha.tcp.SimpleTcpCluster.memberAdded Replication member added:
[org.apache.catalina.tribes.membership.MemberImpl[tcp://{172, 31, 0, 102}:4000,
{172, 31, 0, 102},4000, alive=1022, securePort=-1, UDP Port=-1, id={89 -26 -30 -99
16 80 65 95 -65 14 -33 124 -55 -123 -30 82 }, payload={}, command={}, domain=
{}]]

修改t2主机的应用的web.xml文件

#与5.2.3.1上的t1相同的操作
[root@t2 ~]# cp -a /usr/local/tomcat/webapps/ROOT/WEB-INF/ /data/webapps/ROOT/
[root@t2 ~]# vim /data/webapps/ROOT/WEB-INF/web.xml
[root@t2 ~]# tail -n3 /data/webapps/ROOT/WEB-INF/web.xml
</description>
<distributable/> #添加此行
</web-app>

#注意权限
[root@t2 ~]# ll /data/webapps/ROOT/WEB-INF/
total 4
-rw-r----- 1 tomcat tomcat 1243 Jan 17 09:38 web.xml
[root@t2 ~]# systemctl restart tomcat
#同时观察日志
[root@t2 ~]# tail -f /usr/local/tomcat/logs/catalina.out
15-Jul-2021 11:29:12.088 INFO [t2.longxuan.vip-startStop-1]
org.apache.catalina.ha.session.DeltaManager.getAllClusterSessions Manager [],
requesting session state from
[org.apache.catalina.tribes.membership.MemberImpl[tcp://{172, 31, 0, 101}:4000,
{172, 31, 0, 101},4000, alive=208408, securePort=-1, UDP Port=-1, id={118 -108
-116 119 58 22 73 113 -123 -96 -94 111 -65 -90 -87 -107 }, payload={}, command=
{}, domain={}]]. This operation will timeout if no session state has been
received within [60] seconds.

测试访问

重启全部Tomcat,通过负载均衡调度到不同节点,返回的SessionID不变了。

用浏览器访问,并刷新多次,发现SessionID 不变,但后端主机在轮询
但此方式当后端tomcat主机较多时,会重复占用大量的内存,并不适合后端服务器众多的场景

#修改t1和t2的配置项,删除jvmRoute配置项
[root@t1 tomcat]# vim conf/server.xml
<Engine name="Catalina" defaultHost="t1.longxuan.vip" >

[root@t1 tomcat]# systemctl restart tomcat
#多次执行下面操作,可以看到SessionID不变
[root@centos7 ~]# curl -b 'JSESSIONID=1A3E7EED14F3E44FAF7469F8693E1CB6'
proxy.longxuan.vip/index.jsp
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>tomcat test</title>
</head>
<body>
<h1> tomcat website </h1>
<div>On tomcat-server</div>
<div>172.31.0.102:8080</div>
<div>SessionID = <span
style="color:blue">1A3E7EED14F3E44FAF7469F8693E1CB6</span></div>
Wed Jul 15 11:33:09 CST 2021
</body>
</html>
[root@centos7 ~]# curl -b 'JSESSIONID=1A3E7EED14F3E44FAF7469F8693E1CB6'
proxy.longxuan.vip/index.jsp
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>tomcat test</title>
</head>
<body>
<h1> tomcat website </h1>
<div>On tomcat-server</div>
<div>172.31.0.101:8080</div>
<div>SessionID = <span
style="color:blue">1A3E7EED14F3E44FAF7469F8693E1CB6</span></div>
Wed Jul 15 11:33:10 CST 2021
</body>
</html>
[root@centos7 ~]#

故障模拟

#模拟t2节点故障
[root@t2 ~]# systemctl stop tomcat
#多次访问SessionID不变
[root@centos7 ~]# curl -b 'JSESSIONID=1A3E7EED14F3E44FAF7469F8693E1CB6'
proxy.longxuan.vip/index.jsp
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>tomcat test</title>
</head>
<body>
<h1> tomcat website </h1>
<div>On tomcat-server</div>
<div>172.31.0.101:8080</div>
<div>SessionID = <span
style="color:blue">1A3E7EED14F3E44FAF7469F8693E1CB6</span></div>
Wed Jul 15 12:01:16 CST 2021
</body>
</html>

恢复实验环境

本小节结束,为学习后面的内容,删除此节相关配置,为后续内容准备

#恢复t1环境
[root@t1 ~]# vim /usr/local/tomcat/conf/server.xml
[root@t1 ~]# tail /usr/local/tomcat/conf/server.xml
    <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
        prefix="localhost_access_log" suffix=".txt"
        pattern="%h %l %u %t &quot;%r&quot; %s %b" />
      </Host>
      <Host name="t1.longxuan.vip" appBase="/data/webapps" unpackWARs="true" autoDeploy="true">
      </Host>
    </Engine>
  </Service>
</Server>

[root@t1 ~]# rm -f /data/webapps/ROOT/WEB-INF/web.xml
[root@t1 ~]# systemctl restart tomcat

#恢复t2环境
[root@t2 ~]# vim /usr/local/tomcat/conf/server.xml
[root@t2 ~]# tail /usr/local/tomcat/conf/server.xml
        prefix="localhost_access_log" suffix=".txt"
        pattern="%h %l %u %t &quot;%r&quot; %s %b" />
    </Host>
        <Host name="t2.longxuan.vip" appBase="/data/webapps" autoDeploy="true" >
        </Host>
   </Engine>
 </Service>
</Server>

[root@t2 ~]# rm -f /data/webapps/ROOT/WEB-INF/web.xml
[root@t2 ~]# systemctl restart tomcat
posted @ 2021-08-20 21:03  空白的旋律  阅读(1145)  评论(0编辑  收藏  举报