Apache+Tomcat实现负载均衡的两种实现方法

如果我们将工作在不同平台的apache能够实现彼此间的高效通信,因此它需要一种底层机制来实现--叫做apr

Apr的主要目的就是为了其能够让apache工作在不同的平台上,但在linux上安装apache的时候通常都是默认安装的

[root@node2 ~]#rpm -qi apr
Name                 :apr                                        Relocations: (not relocatable)
Version              :1.3.9                                      Vendor: CentOS
Release              :5.el6_2                                    Build Date: Thu 14 Jun 2012 06:53:03 PM CST
Install Date: Fri 16 May 2014 07:11:23 PM CST                          Build Host:c6b5.bsys.dev.centos.org
Group                :System Environment/Libraries                    Source RPM: apr-1.3.9-5.el6_2.src.rpm
Size                 :303205                                     License: ASL 2.0
Signature            : RSA/SHA1,Thu 14 Jun 2012 08:27:12 PM CST, Key ID 0946fca2c105b9de
Packager             :CentOS BuildSystem 
URL                   http://apr.apache.org/
Summary              :Apache Portable Runtime library
Description :
The mission of the Apache Portable Runtime (APR) is to provide a
free library of C data structures and routines, forming a system
portability layer to as many operating systems as possible,
including Unices, MS Win32, BeOS and OS/2.

 

Tomcat连接器架构:
基于Apache做为Tomcat前端的架构来讲,Apache通过mod_jk、mod_jk2或mod_proxy模块与后端的Tomcat进行数据交换。而对Tomcat来说,每个Web容器实例都有一个Java语言开发的连接器模块组件,在Tomcat6中,这个连接器是org.apache.catalina.Connector类。这个类的构造器可以构造两种类别的连接器:HTTP/1.1负责响应基于HTTP/HTTPS协议的请求,AJP/1.3负责响应基于AJP的请求。但可以简单地通过在server.xml配置文件中实现连接器的创建,但创建时所使用的类根据系统是支持APR(ApachePortable Runtime)而有所不同。

APR是附加在提供了通用和标准API的操作系统之上一个通讯层的本地库的集合,它能够为使用了APR的应用程序在与Apache通信时提供较好伸缩能力时带去平衡效用。

同时,需要说明的是,mod_jk2模块目前已经不再被支持了,mod_jk模块目前还apache被支持,但其项目活跃度已经大大降低。因此,目前更常用的方式是使用mod_proxy模块。
如果支持APR:

1、HTTP/1.1:org.apache.coyote.http11.Http11AprProtocol

2、AJP/1.3:org.apache.coyote.ajp.AjpAprProtocol

不支持APR:

HTTP/1.1:org.apache.coyote.http11.Http11Protocol
AJP/1.3: org.apache.jk.server.
JkCoyoteHandler

 

tomcat的运行方式

·standalone 独立运行的服务,使其本身就能接收web服务,而且能服务动态又能服务静态内容

   在tomcat处理静态内容完全不亚于apache,因为如果必须将动静分割开来的话,至少可以降低动态请求的压力,在架构上是必须的,但tomcat本身的确是具有web服务响应能力的

   而tomcat的连接器有多种类型,它的主要目的是服务于动态程序,所以我们是不应该让它直接面对用户的

·apache+tomcat 将节点分离,使apache接受请求而tomcat在后端进行动态请求处理

    如果tomcat不直接面向客户端的话,而是只接受apache的请求,有种协议非常高效,叫做ajp,这种方式可以完全禁用tomcat的http连接器,而只启用ajp

    这样用户是绝对不可能访问tomcat,由此所有的请求要想访问tomcat,必须先由apcahe代理,而apache是支持ajp协议的

这就是为什么很多场景都使用apache跟tomcat结合的原因

 

其好处是,tomcat不会直接面对用户,很多用户的连接进来的请求只能跟前端apache建立关系,如果支持长连接,但是这段时间又没请求新的内容,可以将这些连接当做非活动链接,非活动链接是不会被apache转向后端的,所以如果前端是长连接而后端使用的是ajp或http协议没有使用长连接方式,尤其是非活动请求都只到apache就停止活动,因此tomcat的资源就被释放出来

 

如果能够这样配置会更好:

静态内容都直接交给apache响应,而不会向后转发,动态内容则直接交给tomcat,这样就算使用nginx也可以

 

·tomcat+apache 都工作在独立主机上

    如果跨主机的方式转发,网络延迟是不容忽视的,网络必然要有延迟的

    如果在同台主机上,基于127.0.0.1作为转发地址的话,所有请求都会在内核中进行转发

    内核最大带宽大概为10G左右甚至更高,内核通信间也是需要占用带宽的

 

那么问题又来了:

如何进行会话保持呢?如果是电商站点,很显然是必须要使用会话保持的

好在tomcat已经考虑到会话机制了,因此在tomcat中有很多内置会话管理器

 

tomcat会话管理器

·标准会话管理器(StandardManager)  

    有持久功能,比如tomcat已关机,会话是不会丢失的会将会话提前保存在磁盘中(定期保存),此后上线还会读取,但是突然崩溃会话一定会崩溃的

    tomcat正常关闭,会话不会丢失,但是如果主机崩溃或者进程崩溃,会话一定会丢失

 

·持久会话管理器(PersistentManager):

    在进程关闭的的时候将会话保存在磁盘上,就算崩溃了也只是丢失一部分没有来得及同步的会话,其他已同步的会话依然可用

 

·集群会话管理器(DeltaManager)

     对tomcat而言完全将几个节点做成会话集群,每个节点定期通过多播地址向其他节点通告本机依然在在线的心跳信息,而多久进行通过是以微秒级别进行定义的,只要向外通告心跳,其他节点都认为其是正常的,一旦不通告自己的心跳,其他节点就将其移除出节点

 

集群会话管理器的最大好处是可以在内存级别共享会话的

    如果用户来访问被定向至第一个服务器,这时会在第一个节点产生会话的,会通过所谓的多播通道将自己的会话信息多播给其他节点,所以这个时候每个节点都会存放一份

    这时任何节点故障,通过重新定向至其他tomcat,会话依旧存在

 

但对于非常繁忙的服务器来讲其工作量会很大,所以当在大规模集群中是非常不适用的,因为非常占用资源

因此还需要靠其他机制来实现共享会话

 

如果节点非常的多,这时候就算适用source或ip hash 也未必会有太大问题,因为节点很多而平时第一次访问的时候通常都是被负载均衡的,而用户所有的请求能够被热点集中在一台主机或几台主机上几率非常小,虽然基于这类算法有可能会损害一部分负载均衡效果但是在全局范围内可以被忽略的

 

如果节点非常少结果就非常明显,所以在节点多的环境下直接绑定会话问题也不大

或者自己提供共享会话机制来存放会话,比如memcached或redis等机制

 

·BackManager 

    将一个节点的会话同步给一个或有限个数的节点,而不是所有,这样就可降低会话同步造成的代价

 

对于集群类型的DeltaManager 其额外需要依赖的组件非常多

因为如果启用DeltaManager,就必须启动tomcat内置集群功能,这个集群需要监听在某个多播地址上传递心跳信息

apache+tomcat集群部署

apache+tomcat实现集群,无非就是用apache两组模块来实现的,分别为mod_proxy 和 mod_jk 最为常见

mod_proxy 

    ajp

    http

mod_jk

    ajp 

    http

mod_jk2

在apache2.0或2.0之后的版本中只支持mod_jk的方式 ,而mode_proxy是2.2以后支持的功能

 

·方法一:基于mod_jk模块对tomcat实现负载均衡

配置Apache

检测httpd是否安装以下模块

[root@node1 ~]# httpd -M

proxy_module (shared)
proxy_balancer_module (shared)
proxy_ftp_module (shared)
proxy_http_module (shared)
proxy_ajp_module (shared)
proxy_connect_module (shared)

首先确保apr模块是否安装

[root@node1 ~]# rpm -qa | grepapr
apr-1.3.9-5.el6_2.x86_64
haproxy-1.4.24-2.el6.x86_64
apr-util-1.3.9-3.el6_0.1.x86_64
apr-devel-1.3.9-5.el6_2.x86_64
apr-util-ldap-1.3.9-3.el6_0.1.x86_64

确保apxs是否存在,由于是临时yum安装http-deve应该没有被安装,如果是编译安装的httpd会自带apxs

[root@node1 ~]# yum -y install httpd-devel

安装connectors模块

[root@node1 tmp]# tar xftomcat-connectors-1.2.37-src.tar.gz 

[root@node1 tmp]# cdtomcat-connectors-1.2.37-src/

[[email protected]]# cd native/

[root@node1 native]#./configure --with-apxs=/usr/sbin/apxs     

[root@node1 native]# make&& make install

安装过程中可以看到如下信息提示:

chmod 755/usr/lib64/httpd/modules/mod_jk.so
Please be sure to arrange /etc/httpd/conf/httpd.conf...

查看模块是否存在

[root@node1 ~]#  ls /etc/httpd/modules/  | grep jk

mod_jk.so

配置启用模块

[root@node1 native]# cd/etc/httpd/conf.d/

配置mod_jk.conf

创建新配置文件mod_jk.conf,并加入以下内容:

[root@node1 conf]# cat/etc/httpd/conf.d/mod_jk.conf
# Load the mod_jk

LoadModule  jk_module modules/mod_jk.so

JkWorkersFile  /etc/httpd/conf.d/workers.properties

JkLogFile       logs/mod_jk.log

JkLogLevel      debug

JkMount  /*    TomcatA              #标注,必须跟wokers.properties文件中一致

JkMount  /status/ stat1

 

接下来要配置workers.properties

/etc/httpd/conf.d/workers.properties 文件一般由两类指令组成:

·mod_jk可以连接的各worker名称列表;

·每一个worker的属性配置信息;

但是woker name不是随便命名的,它是需要根据tomcat中engin组件jvmroute参数的值

配置workers.properties

创建文件后加入以下内容:

[root@node1 conf.d]# catworkers.properties

worker.list=TomcatA,stat1                      #tomcatA监听端口,ajp协议默认就已监听

worker.TomcatA.host=10.0.10.61                 #TomcatA的主机地址,这里为本机的IP地址,因为本地也在运行tomcat

worker.TomcatA.type=ajp13                      #连接器类型

worker.TomcatA.lbfactor=1                      #负载均衡调度权重

worker.stat1.type = status                    

以上实例在mod_jk中已做调用

stat1 这个参数是瞎编的,因为他主要目的是在于输出状态信息的,因为mod_jk有状态接口,相当于nginx状态监测端口一样

通常都用stat1来表示

 

到此配置已完成,启动tomcat以及httpd

 [root@node1 conf]# catalina.sh start

[root@node1 conf.d]# httpd -t

Syntax OK

[root@node1 conf.d]#/etc/init.d/httpd start

Starting httpd:                                           [  OK  ]

[root@node1 conf.d]# netstat-lnt | grep 80

tcp        0     0 :::80                        :::*                       LISTEN     

tcp        0     0 :::8080                     :::*                        LISTEN     

tcp        0     0 ::ffff:127.0.0.1:8005       :::*                        LISTEN     

tcp        0      0:::8009                     :::*                        LISTEN     

访问测试:

基于Apache+Tomcat负载均衡的两种实现方法_第1张图片

打开status/

http://10.0.10.61/status/

最后的斜线必须得加,因为配置mod_jk的时候就已经加过了

基于Apache+Tomcat负载均衡的两种实现方法_第2张图片

如上所示,可以显示出后端信息但是我们发现并没有定义这些属性,之后慢慢会提到

 

补充:woker的类型

Worker        有多种不同的类型,而类型有三种

ajp3          此类型表示当前woker为一个运行tocat的实例

lb            专用于负载均衡场景,

status  

worker其它常见的属性说明

· host:Tomcat 7的worker实例所在的主机;
· port:Tomcat 7实例上AJP1.3连接器的端口;
· connection_pool_minsize:最少要保存在连接池中的连接的个数;默认为pool_size/2;
· connection_pool_timeout:连接池中连接的超时时长;
· mount:由当前worker提供的context路径,如果有多个则使用空格格开;此属性可以由JkMount指令替代;
· retries:错误发生时的重试次数;
· socket_timeout:mod_jk等待worker响应的时长,默认为0,即无限等待;
· socket_keepalive:是否启用keepalive的功能,1表示启用,0表示禁用;
· lbfactor:worker的权重,可以在负载均衡的应用场景中为worker定义此属性;

 

实现多实例转发

基于mod_jk模块是可以实现负载均衡的方式向后端进行转发

规划如下

服务器IP

服务器角色

10.0.10.61

Apache/Tomcat

10.0.10.62

Tomcat

双方配置好基础环境,步骤略,并且建立测试页面

TomcatA页面内容如下所示:

[root@node1 app1]# catindex.jsp

<%@ pagelanguage="java" %>

 TomcatA

 

   

TomcatA

   

     

        Session ID

    <%session.setAttribute("abc","abc"); %>

        <%= session.getId()%>

     

     

        Created on

        <%= session.getCreationTime()%>

    

   

 

TomcatB页面内容如下所示:

[root@node2 conf]# mkdir -p/tomcat/app1/WEB-INF/{lib,classes}

[root@node2 app1]# catindex.jsp

<%@ pagelanguage="java" %>

  TomcatB

 

   

TomcatB

   

     

        Session ID

    <%session.setAttribute("abc","abc"); %>

        <%= session.getId()%>

     

     

        Created on

        <%=session.getCreationTime() %>

    

   

 

 

配置负载均衡

首先配置缺省路由并隐藏对外IP,由于我们这里其中一个节点分别跑了apache和tomcat所以只能将另外一个节点隐藏其真实ip

eth0      Link encap:Ethernet   HWaddr 00:0C:29:EF:9C:E5 

          inet addr:10.0.10.62  Bcast:10.0.10.255  Mask:255.255.255.0

eth1      Link encap:Ethernet   HWaddr 00:0C:29:EF:9C:EF 

          inet addr:172.23.214.58  Bcast:172.23.215.255  Mask:255.255.254.0

将eth0网卡关闭

[root@node2 app1]# ifdown eth0

配置默认路由

[root@node2 ~]# route adddefault gw 172.23.215.25   

 

切换至apache节点

开启路由转发

[root@node1 app1]# vim/etc/sysctl.conf

修改参数

net.ipv4.ip_forward = 1

保存退出并让其生效

[root@node1 app1]#sysctl -p

编辑tomcat server.xml 找到以下参数

   

TomcatA修改为:

    jvmRoute="TomcatA">

TomcatB修改为:                                         

    jvmRoute="TomcatB" >

·为了避免用户直接访问后端Tomcat实例,影响负载均衡的效果,建议在Tomcat 7的各实例上禁用HTTP/1.1连接器。
·为每一个Tomcat 7实例的引擎添加jvmRoute参数,并通过其为当前引擎设置全局惟一标识符。如下所示。需要注意的是,每一个实例的jvmRoute的值均不能相同。

而后去配置apache,修改为如下内容:

# Load the mod_jk

LoadModule  jk_module modules/mod_jk.so

JkWorkersFile  /etc/httpd/conf.d/workers.properties

JkLogFile       logs/mod_jk.log

JkLogLevel      debug

JkMount  /*    lbcluster1

JkMount  /status/ stat1

编辑workers.properties,添加如下内容:

[root@node1 conf.d]# vimworkers.properties

worker.list = lbcluster1,stat1                     #lbcluster1必须与后端的一直

worker.TomcatA.type = ajp13                        #使用的类型为ajp协议

worker.TomcatA.host = 172.23.215.25                #tomcatA的ip地址

worker.TomcatA.port = 8009                         #ajp协议的监听端口

worker.TomcatA.lbfactor = 1                        #负载均衡的权重

 

worker.TomcatB.type = ajp13

worker.TomcatB.host = 172.23.214.58

worker.TomcatB.port = 8009

worker.TomcatB.lbfactor = 1

    

worker.lbcluster1.type = lb                        #定义lbcluster1为负载均衡,类型为lb

worker.lbcluster1.sticky_session= 0                #会话绑定为0,0意为不让其绑定

worker.lbcluster1.balance_workers= TomcatA, TomcatB         #lbcluster组内有多少个节点并对组内的节点事先负载均衡,这里为2个,分别为tomcata和tomcatb

worker.stat1.type = status

所以这里lbcluster1可以自动向tomcatA tomcatB上进行转发了,而lbcluster1这个专用的woker有一些专用的属性,如下:

·balance_workers

    用来定义后端有几个server,每个实例都需要定义其属性的

·method

    设定调度方法,此类有三种

    为R、T或B;默认为R,即根据请求的个数进行调度;

    T表示根据已经发送给worker的实际流量大小进行调度;

    B表示根据实际负载情况进行调度

    因此,定义负载均衡的时候可以写为worker.lbcluster1.method= R,T,B

·sticky_session

    绑定会话,如果使用此参数就不要使用调度算法,因为这个参数相当于ip_hash或source 来自同一客户端请求始终被定向至同一个woker

保存退出并检查语法

[root@node1 conf.d]#/etc/init.d/httpd configtest

Syntax OK

启动apache

[root@node1 conf.d]#/etc/init.d/httpd start

启动tomcat

[root@node2 ~]# catalina.shstart

访问测试,结果无误,查看status

 

 

·方法二:基于mod_proxy模块对tomcat实现负载均衡

实现简单单台server转发

[root@node1 conf.d]# mvmod_jk.conf mod_jk.conf.bak_$(date +%F)

确保以下模块存在

[root@node1 conf.d]# httpd -M| grep proxy

Syntax OK

 proxy_module (shared)

 proxy_balancer_module (shared)

 proxy_ftp_module (shared)

 proxy_http_module (shared)

 proxy_ajp_module (shared)

 proxy_connect_module (shared)

如要开启proxy模块,需要在httpd全局配置文件或某虚拟主机中加入以下参数:

ProxyVia On
ProxyRequests Off

如下所示:

以全局配置为例,首先创建配置文件mod_proxy.conf,并加入以下参数

[root@node1 conf.d]# vimmod_proxy.conf    

ProxyVia On

ProxyRequests Off

 

    Order allow,deny

    Allow from all

#这里是以http协议进行反向代理

ProxyPass /http://172.23.215.25:8080/

ProxyPa***everse /http://172.23.215.25:8080/

 

    Order allow,deny

    Allow from all

配置参数说明:

ProxyPreserveHost {On|Off}:如果启用此功能,代理会将用户请求报文中的Host:行发送给后端的服务器,而不再使用ProxyPass指定的服务器地址。如果想在反向代理中支持虚拟主机,则需要开启此项,否则就无需打开此功能。

ProxyVia {On|Off|Full|Block}:用于控制在http首部是否使用Via:主要用于在多级代理中控制代理请求的流向。默认为Off,即不启用此功能;On表示每个请求和响应报文均添加Via:;Full表示每个Via:行都会添加当前apache服务器的版本号信息;Block表示每个代理请求报文中的Via:都会被移除。
ProxyRequests {On|Off}:是否开启apache正向代理的功能;启用此项时为了代理http协议必须启用mod_proxy_http模块。同时,如果为apache设置了ProxyPass,则必须将ProxyRequests设置为Off。想要使用反向代理必须关闭正向代理

ProxyPass  [path] !|url  [key=value key=value ...]]:将后端服务器某URL与当前服务器的某虚拟路径关联起来作为提供服务的路径,path为当前服务器上的某虚拟路径,url为后端服务器上某URL路径。使用此指令时必须将ProxyRequests的值设置为Off。需要注意的是,如果path以“/”结尾,则对应的url也必须以“/”结尾,反之亦然。

·lbmethod:apache实现负载均衡的调度方法,默认是byrequests,即基于权重将统计请求个数进行调度,bytraffic则执行基于权重的流量计数调度,bybusyness通过考量每个后端服务器的当前负载进行调度。
· maxattempts:放弃请求之前实现故障转移的次数,默认为1,其最大值不应该大于总的节点数。
· nofailover:取值为On或Off,设置为On时表示后端服务器故障时,用户的session将损坏;因此,在后端服务器不支持session复制时可将其设置为On。
· stickysession:调度器的stickysession的名字,根据web程序语言的不同,其值为JSESSIONID或PHPSESSIONID。

保存退出,并访问测试测试

[root@node1 conf.d]# httpd -t

Syntax OK

[root@node1 conf.d]#catalina.sh start

访问http://10.0.10.61/

基于Apache+Tomcat负载均衡的两种实现方法_第3张图片

使用ajp协议进行反向代理

ProxyVia On

ProxyRequests Off

 

    Order allow,deny

    Allow from all

 

ProxyPass /ajp://172.23.215.25:8009/

ProxyPa***everse/ ajp://172.23.215.25:8009/

 

    Order allow,deny

    Allow from all

 

 

基于lb cluster实现负载均衡

如果想实现负载均衡不光是需要proxy模块,还需要借助于mod_blancer模块

[root@node1 conf.d]# httpd -M| grep bal

Syntax OK

 proxy_balancer_module (shared)

 

配置balancer模块

要使用负载均衡首先要定义以后balancer 而后在调度的时候使用ProxyPass,其并不是指定一个server 而是一个cluster组,如下所示:

将mod_proxy.conf配置文件修改为:

ProxyVia On

ProxyRequests Off

 

     

BalancerMember  ajp://172.23.215.25:8009 loadfactor=1            #后端server ,以ajp协议进行对接,权重为1

BalancerMember  ajp://172.23.214.58:8009 loadfactor=1

ProxySet  lbmethod=bytraffic                                     #使用的算法,bytraffic为轮询算法

 

    Order allow,deny

    Allow from all

    

ProxyPass /balancer://hotcluster/                               #必须与上面一致,将hotcluster组内的主机进行负载均衡

ProxyPa***everse/ balancer://hotcluster/

 

    Order allow,deny

    Allow from all

balancer必须定义在虚拟主机之外

[root@node1 conf.d]# httpd -t

Syntax OK

[root@node1 conf.d]#/etc/init.d/httpd restart

访问测试,确保无误

如果再加入新的节点无非是将其balancer://hotcluster中定义BalancerMember 即可

 

mod_proxy的状态信息检查

mod_jk有状态信息输出的功能,可以随时监控后端服务的实时健康状态,如果是非监控的会自动终止代理转发

mod_proyx同时也具备此功能呢,只不过是上线时间稍微慢一些

mod_proyx 也可以像mod_jk一样输出状态信息

 

编辑配置文件,并加入以下参数:

ProxyVia On

ProxyRequests Off

 

BalancerMember  ajp://172.23.215.25:8009 loadfactor=1

BalancerMember  ajp://172.23.214.58:8009 loadfactor=1

ProxySet  lbmethod=bytraffic

 

    Order allow,deny

    Allow from all

 

  SetHandler balancer-manager

  Proxypass !

  Order Allow,Deny

  Allow from all

#一定要放在ProxyPass的上面 不然会导致无法访问

 

ProxyPass /balancer://hotcluster/

ProxyPa***everse /balancer://hotcluster/

 

    Order allow,deny

    Allow from all

检查语法并重新加载

[root@node1 conf.d]# httpd -t

Syntax OK

[root@node1 conf.d]#/etc/init.d/httpd reload

 访问测试

http://10.0.10.61/balancer-manager

基于Apache+Tomcat负载均衡的两种实现方法_第4张图片

Sessionscity算法类似于ip_hash,但是后端服务器如果宕机了,那么会话会消失

如果数量不多的话可以使用tomcat的内部会话保持机制 detamanager算法

 

实现冗余

如果单台节点进行分发的话 那么如果其节点故障,那么整个环境将会瘫痪

于是我们可以将其进行冗余配置,一来使资源不再浪费,二来不用担心因某节点故障而影响业务

规划如下:

基于Apache+Tomcat负载均衡的两种实现方法_第5张图片

服务器IP

服务器角色

10.0.10.61

Node1:Apache/Tomcat

10.0.10.62

Node2:Apache/Tomcat

对Node2进行基本配置

开启转发

[root@node2 conf.d]# vim/etc/sysctl.conf

修改参数:

net.ipv4.ip_forward = 1

使内核参数生效

[[email protected]]# sysctl -p

由于是yum安装的httpd 所以模块已具备

[[email protected]]# httpd -M | grep proxy

SyntaxOK

 proxy_module (shared)

 proxy_balancer_module (shared)

 proxy_ftp_module (shared)

 proxy_http_module (shared)

 proxy_ajp_module (shared)

 proxy_connect_module (shared)

以上,我们已经配置了一台apache,现在我们只需要在另外一个节点开启apache并且配置mod_proxy即可,如下所示:

我们将之前配置好的mod_proxy拷贝至Node2一份

[root@node1 conf.d]# scpmod_proxy.conf node2:/etc/httpd/conf.d/

检查语法无误,并重启服务

[[email protected]]# httpd -t

SyntaxOK

访问测试,确保无误

基于Apache+Tomcat负载均衡的两种实现方法_第6张图片

最后我们将其下游服务器对apache进行负载均衡即可达到冗余效果

 

END,感谢各位