7*24小时不间断运行系统是企业应用必须保证的,保持如此高的可用性,通常的做法是将系统部署到多台机器上,每台机器都对外提供同样的功能。
整体的思路可能像下图:
本文包含的内容有:
- DNS轮询
- Nginx做负载均衡
- Tomcat做复制session集群
DNS轮询是最常采用的负载均衡方法之一,就是在DNS服务器添加同一域名指向不同的服务器IP,通过一些算法分配解析IP,这样就完成了简单的负载均衡。可以通过几种方法简单实现DNS轮询:
- 大多域名注册商都支持多条A记录的解析,这就是DNS轮询
- 自己配置DNS服务器,例如BIND和Windows DNS服务
- 使用免费的智能DNS解析服务,国内比较好的有:
nginx不单可以作为强大的web服务器,也可以作为一个反向代理服务器,而且nginx还可以按照调度规则实现动态、静态页面的分离,可以按照轮询、ip哈希、URL哈希、权重等多种方式对后端服务器做负载均衡,同时还支持后端服务器的健康检查。
Nginx的的分配方式:
- 轮询(默认) :每个请求按时间顺序逐一分配到不同的后端服务器,如果后端服务器down掉,能自动剔除。
- weight: 指定轮询几率,weight和访问比率成正比,用于后端服务器性能不均的情况。
- ip_hash:每个请求按访问ip的hash结果分配,这样每个访客固定访问一个后端服务器,可以解决session的问题。
- fair(第三方) :后端服务器的响应时间来分配请求,响应时间短的优先分配。
- url_hash(第三方):确保了相同的url请求映射到一台固定后端服务器,使后端服务器内存资源得到充分利用,提高后端缓存服务器命中率。
例如配置:
upstream tomcat_server { server 192.168.1.10 weight=5; server 192.168.1.10:8080; server 192.168.1.11 down; server 192.168.1.12 backup; server 192.168.1.13 max_fails=3 fail_timeout=30s; #ip_hash;} server { location / { #使用代理 proxy_pass http://tomcat_server; }}upstream每个设备的状态:
- down 表示单前的server暂时不参与负载
- backup: 其它所有的非backup机器down或者忙的时候,请求backup机器
- weight 默认为1.weight越大,负载的权重就越大
- max_fails :允许请求失败的次数默认为1.当超过最大次数时,返回proxy_next_upstream 模块定义的错误
- fail_timeout:max_fails 次失败后,暂停的时间,默认10s
在需要使用负载的Server节点下添加
proxy_pass http://tomcat_server;Nginx负载均衡、分发用户的请求到后端不同的服务器,遇到一个问题就用户的会话数据(如果有)如何同步问题,常用的做法是:
- ip_hash:Nginx自带的指令,将某个ip的请求定向到同一台后端,这样一来这个ip下的某个客户端和某个后端就能建立起稳固的session
- 应用服务器自行实现共享:配置Tomcat(或其他应用服务器)自行实现session共享,下面会介绍到
以下内容是基于Tomcat Version 7.0.23进行。
WEB集群的难点是如何能在集群中的多个节点之间保持数据的一致性,会话(Session)信息是这些数据中最重要的一块。大体上有两种方式:
- 把所有Session数据放到一台服务器上或者数据库中,集群中的所有节点通过访问这台Session服务器来获取数据;简单、易于实现,但是存在着Session服务器发生故障会导致全系统不能正常工作的风险
- 在集群中的所有节点间进行Session数据的同步拷贝,任何一个节点均保存了所有的Session数据;可靠性更高,任一节点的故障不会对整个系统对客户访问的响应产生影响,是最常用的方式。下文内容就是取用这个方式。
抛开集群配置,应用程序要先符合Tomcat集群的条件:
- 所有的session属性必须实现接口java.io.Serializable
- 在web.xml 增加 <distributable/>
编辑Tomcat配置文件server.xml,在<Engine>或者<Host>元素里增加:
<ClusterclassName="org.apache.catalina.ha.tcp.SimpleTcpCluster"/>并在<Engine />增加jvmRoute 属性,用来标记节点名字:
<Enginename="Catalina"jvmRoute="node01">这样,一个Tomcat集群节点就配置完,所有的配置项都为Tomcat默认配置,完整的默认隐藏配置如下:
<ClusterclassName="org.apache.catalina.ha.tcp.SimpleTcpCluster"channelSendOptions="8"> <ManagerclassName="org.apache.catalina.ha.session.DeltaManager"expireSessionsOnShutdown="false"notifyListenersOnReplication="true"/> <ChannelclassName="org.apache.catalina.tribes.group.GroupChannel"><MembershipclassName="org.apache.catalina.tribes.membership.McastService"address="228.0.0.4"port="45564"frequency="500"dropTime="3000"/><ReceiverclassName="org.apache.catalina.tribes.transport.nio.NioReceiver"address="auto"port="4000"autoBind="100"selectorTimeout="5000"maxThreads="6"/> <SenderclassName="org.apache.catalina.tribes.transport.ReplicationTransmitter"><TransportclassName="org.apache.catalina.tribes.transport.nio.PooledParallelSender"/></Sender><InterceptorclassName="org.apache.catalina.tribes.group.interceptors.TcpFailureDetector"/><InterceptorclassName="org.apache.catalina.tribes.group.interceptors.MessageDispatch15Interceptor"/></Channel> <ValveclassName="org.apache.catalina.ha.tcp.ReplicationValve"filter=""/><ValveclassName="org.apache.catalina.ha.session.JvmRouteBinderValve"/> <DeployerclassName="org.apache.catalina.ha.deploy.FarmWarDeployer"tempDir="/tmp/war-temp/"deployDir="/tmp/war-deploy/"watchDir="/tmp/war-listen/"watchEnabled="false"/> <ClusterListenerclassName="org.apache.catalina.ha.session.JvmRouteSessionIDBinderListener"/><ClusterListenerclassName="org.apache.catalina.ha.session.ClusterSessionListener"/></Cluster>(三)深入理解配置选项把Tomcat集群默认配置拆分来说明各个配置项。
<ClusterclassName="org.apache.catalina.ha.tcp.SimpleTcpCluster"channelSendOptions="8">Tomcat集群各节点通过建立tcp链接来完成Session的拷贝,拷贝有同步和异步两种模式。在同步模式下,对客户端的响应必须在Session拷贝到其他节点完成后进行;异步模式无需等待Session拷贝完成就可响应。异步模式更高效,但是同步模式可靠性更高。同步异步模式由channelSendOptions参数控制,默认值是8,为异步模式,4是同步模式。值的详细内容请看:The Cluster object
<ManagerclassName="org.apache.catalina.ha.session.BackupManager"expireSessionsOnShutdown="false"notifyListenersOnReplication="true"mapSendOptions="6"/><!--<Manager className="org.apache.catalina.ha.session.DeltaManager" expireSessionsOnShutdown="false" notifyListenersOnReplication="true"/>-->Manager用来在节点间拷贝Session,默认使用DeltaManager,DeltaManager采用的一种all-to-all的工作方式,即集群中的节点会把Session数据向所有其他节点拷贝,而不管其他节点是否部署了当前应用。当集群中的节点数量很多并且部署着不同应用时,可以使用BackupManager,BackManager仅向部署了当前应用的节点拷贝Session。
<ChannelclassName="org.apache.catalina.tribes.group.GroupChannel"><MembershipclassName="org.apache.catalina.tribes.membership.McastService"address="228.0.0.4"port="45564"frequency="500"dropTime="3000"/>Channel负责对tomcat集群的IO层进行配置。Membership用于发现集群中的其他节点,这里的address用的是组播地址(Multicast address),使用同一个组播地址和端口的多个节点同属一个子集群,因此通过自定义组播地址和端口就可将一个大的tomcat集群分成多个子集群。
<ReceiverclassName="org.apache.catalina.tribes.transport.nio.NioReceiver"address="auto"port="5000"selectorTimeout="100"maxThreads="6"/>Receiver用于各个节点接收其他节点发送的数据,在默认配置下tomcat会从4000-4100间依次选取一个可用的端口进行接收,自定义配置时,如果多个tomcat节点在一台物理服务器上注意要使用不同的端口。
<SenderclassName="org.apache.catalina.tribes.transport.ReplicationTransmitter"><TransportclassName="org.apache.catalina.tribes.transport.nio.PooledParallelSender"/></Sender>Sender用于向其他节点发送数据,具体实现通过Transport配置,PooledParallelSender是从tcp连接池中获取连接,可以实现并行发送,即集群中的多个节点可以同时向其他所有节点发送数据而互不影响。
<InterceptorclassName="org.apache.catalina.tribes.group.interceptors.TcpFailureDetector"/><InterceptorclassName="org.apache.catalina.tribes.group.interceptors.MessageDispatch15Interceptor"/><InterceptorclassName="org.apache.catalina.tribes.group.interceptors.ThroughputInterceptor"/></Channel>Interceptor就是拦截器,起到一个阀门的作用,在数据到达目的节点前进行检测或其他操作,如TcpFailureDetector用于检测在数据的传输过程中是否发生了tcp错误。
<ValveclassName="org.apache.catalina.ha.tcp.ReplicationValve"filter=".*\.gif|.*\.js|.*\.jpeg|.*\.jpg|.*\.png|.*\.htm|.*\.html|.*\.css|.*\.txt"/>Valve用于在节点向客户端响应前进行检测或进行某些操作,ReplicationValve就是用于用于检测当前的响应是否涉及Session数据的更新,如果是则启动Session拷贝操作,filter用于过滤请求,如客户端对图片,css,js的请求就不会涉及Session,因此不需检测,默认状态下不进行过滤,监测所有的响应。
<DeployerclassName="org.apache.catalina.ha.deploy.FarmWarDeployer"tempDir="/tmp/war-temp/"deployDir="/tmp/war-deploy/"watchDir="/tmp/war-listen/"watchEnabled="false"/>Deployer用于集群的farm功能,监控应用中文件的更新,以保证集群中所有节点应用的一致性,如某个用户上传文件到集群中某个节点的应用程序目录下,Deployer会监测到这一操作并把这一文件拷贝到集群中其他节点相同应用的对应目录下以保持所有应用的一致。
<ClusterListenerclassName="org.apache.catalina.ha.session.ClusterSessionListener"/></Cluster>Listener用于跟踪集群中节点发出和收到的数据,也有点类似Valve的功能。
(四)Tomcat集群如何工作- TomcatA starts up:TomcatA按照标准的tomcat启动,当Host对象被创建时,一个Cluster对象(默认配置下是SimpleTcpCluster)也同时被关联到这个Host对象。当某个应用在web.xml中设置了distributable时,Tomcat将为此应用的上下文环境创建一个DeltaManager。SimpleTcpCluster启动membership服务和Replication服务(用于建立tcp连接)
- TomcatB starts up (Wait that TomcatA start is complete):TomcatB会执行和TomcatrA一样的操作,然后SimpleTcpCluster会建立一个由TomcatA 和TomcatB组成的membership。接着TomcatB向集群中已启动的服务器即TomcatrA请求Session数据,如果TomcatrA没有响应TomcatB的拷贝请求,t2会在60秒后time out。在Session数据拷贝完成之前TomcatB不会接收客户端的http或mod_jk/ajp请求。
- TomcatA receives a request, a session S1 is created:TomcatA 正常响应客户请求,但是在TomcatA 把结果发送回客户端时,ReplicationValve会拦截当前请求(如果filter中配置了不需拦截的请求类型,这一步就不会进行,默认配置下拦截所有请求),如果发现当前请求更新了Session,调用Replication服务建立tcp连接把Session拷贝到membership列表中的其他节点即TomcatB,返回结果给客户端(注意,如果采用同步拷贝,必须等拷贝完成后才会返回结果,异步拷贝在数据发送到tcp连接就返回结果,不等待拷贝完成)。在拷贝时,所有保存在当前Session中的可序列化的对象都会被拷贝,而不仅仅是发生更新的部分
- TomcatA crashes: 当TomcatA 崩溃时,TomcatB会被告知TomcatA 已从集群中退出,然后TomcatB就会把TomcatA 从自己的membership列表中删除,发生在TomcatB的Session更新不再往TomcatA 拷贝,同时负载均衡器会把后续的http请求全部转发给TomcatB。在此过程中所有的Session数据不会丢失
- TomcatB receives a request for session S1:TomcatB正常响应TomcatA的请求,因为TomcatB保存着TomcatA的所有数据
- TomcatA starts up:按步骤1、2一样的操作启动,加入集群,从TomcatB拷贝所有Session数据,拷贝完成后开放自己的http和mod_jk/ajp端口接收请求
- TomcatA receives a request, invalidate is called on the session (S1):TomcatA 继续接收请求,把s1设置为过期。这里的过期并非因为s1处于非活动状态超过设置的时间,而是执行类似注销的操作而引起的Session失效。这时TomcatA 并非发送s1的所有数据而是一个类似s1 expired的消息,TomcatB收到消息后也会把s1设为过期
- TomcatB receives a request, for a new session (S2):和步骤3一样
- TomcatA The session S2 expires due to inactivity:对于因超时引起的Session失效TomcatA无需通知TomcatB,因为TomcatB同样知道s2已经超时。因此对于tomcat集群有一点非常重要,所有节点的操作系统时间必须一致!不然会出现某个节点Session已过期而在另一节点此Session仍处于活动状态的现象