1. Session机制
Session就是会话,就是指的是对话双方或者交互双方建立的信息通道,从建立连接到断开连接的整个过程,称为一个会话。它是一种网络持久化的机制。
HTTP协议是一种无状态协议,客户端和服务器建立连接传输完数据后即断开连接,客户端再次发起连接后,服务器端无法知道这个连接是否和上一个连接有什么关系,它只能认为是不同的连接。
为了解决这个问题,一般采用的方法有两种:
-
客户端保持,即cookie机制
-
服务器端保持,即session机制。为了保持这个状态信息,它要让客户端至少保留一个session标识信息,即SessionID,也就说服务器端的session机制需要用到客户端cookie机制。
2. session问题
动态网站中,经常会使用服务器端变量session来保存当前会话信息,也可以在session中保存用户数据,例如购物车加入的商品。
如果使用多台服务器提供集群服务,通过负载均衡器调度后,从A服务器到了B服务器,那么B服务器并没有A服务器上的会话信息和数据,那么用户的感觉就是刚才的挑选的商品都不见了。等到重新调度到A服务器的时候,这会是一个新的会话,刚才的session失效了。
2.1 解决办法
2.1.1 Session绑定
1) IP绑定
可以使用某种算法,建立客户端IP和后端服务器的映射关系,使得同一客户端总是访问同一个后端服务器。这样用户的session就总是有效。
nginx的upstream的ip_hash,LVS的sh调度算法,HAProxy的Source算法都实现了这一功能。
但这种调度一定程度上影响了负载均衡的效果,如果被调度到的后端服务器故障,session就丢失了。
2) session黏性
同一个session的请求将被调度到同一台服务器,粒度小,以session为调度单位。基于cookie实现。
以上两种方式,都一定程度解决了会话状态保持,但是,一旦某一台后端服务器故障,就会造成一定session数据的丢失。但是由于session sticky实现的粒度小,其负载均衡效果要好于IP绑定的方法。
2.1.2 Session 复制集群
可以使用Session Cluster,采用复制机制,这样每一台服务器都拥有全部的用户会话,无论前端均衡器如何调度,每一台后端服务器都可以和前客户端保持会话。缺点是显而易见的,对于访问大的站点,所有Session对内存的消耗非常大。
2.1.3 Session 服务器
可以使用session服务器,将session信息复制到共享的区域,所有服务器都可以借助这个共享区域拥有全部的session。
任何一台后端服务器故障,都不会影响客户端,因为其它服务器从共享区域中可以匹配到客户端提交的SessionID。
使用Tomcat + memcached-session-manager + Memcached实现。
msm中的概念
sticky模式:配置双节点memcached,其中一个作为failoverNodes的节点,那么另一个节点就是该tomcat的优先存储节点,tomcat对它具有黏性。
non-sticky模式:随机挑选多个memcached节点中的一个节点作为主节点,逻辑上的下一个节点为备用节点。Session数据将存储在主和从节点上。如果session没有变化,则ping一下memcached节点中的session,以防过期。
注:我认为网上某些关于msm的黏性的说法不对。
今天的实验也就从这个三个方面来实践Session保持的方法。基于LNMT实现,Linux + Nginx + Memcached + Tomcat。
3. 实验环境搭建
3.1 规划
3.2 memcached安装
2台memcached
# yum -y install memcached
# rpm -ql memcached
/etc/rc.d/init.d/memcached
/etc/sysconfig/memcached
/usr/bin/memcached
/usr/bin/memcached-tool
/usr/share/doc/memcached-1.4.4
/usr/share/doc/memcached-1.4.4/AUTHORS
/usr/share/doc/memcached-1.4.4/CONTRIBUTORS
/usr/share/doc/memcached-1.4.4/COPYING
/usr/share/doc/memcached-1.4.4/ChangeLog
/usr/share/doc/memcached-1.4.4/NEWS
/usr/share/doc/memcached-1.4.4/README
/usr/share/doc/memcached-1.4.4/protocol.txt
/usr/share/doc/memcached-1.4.4/readme.txt
/usr/share/doc/memcached-1.4.4/threads.txt
/usr/share/man/man1/memcached.1.gz
/var/run/memcached
# ss -tunlp | grep mem
udp UNCONN 0 0 *:11211 *:* users:(("memcached",14359,28))
udp UNCONN 0 0 :::11211 :::* users:(("memcached",14359,29))
tcp LISTEN 0 128 :::11211 :::* users:(("memcached",14359,27))
tcp LISTEN 0 128 *:11211 *:* users:(("memcached",14359,26))
3.3 代理服务器
使用淘宝的Tengine来创建,官网去下载http://tengine.taobao.org/。
3.3.1 Tengine安装
# yum -y install gcc openssl-devel pcre-devel
# groupadd -r nginx
# useradd -r -g nginx -s /bin/nologin nginx
# tar xf tengine-2.0.3.tar.gz
# cd tengine-2.0.3
# ./configure --prefix=/usr/local/nginx \
--error-log-path=/var/log/nginx/error.log \
--http-log-path=/var/log/nginx/access.log \
--pid-path=/var/run/nginx/nginx.pid \
--lock-path=/var/lock/nginx.lock \
--user=nginx--group=nginx \
--with-http_ssl_module \
--with-http_flv_module \
--with-http_stub_status_module \
--with-http_gzip_static_module \
--http-client-body-temp-path=/usr/local/nginx/client \
--http-proxy-temp-path=/usr/local/nginx/proxy \
--http-fastcgi-temp-path=/usr/local/nginx/fcgi \
--http-uwsgi-temp-path=/usr/local/nginx/uwsgi \
--http-scgi-temp-path=/usr/local/nginx/scgi \
--with-http_upstream_session_sticky_module=shared \
--with-pcre
# make && make install
3.3.2 导出环境变量并查看模块
ngx_http_upstream_session_sticky_module.so模块后面Session黏性实验使用
# vim /etc/profile.d/nginx.sh
export PATH=/usr/local/nginx/sbin:$PATH
# source /etc/profile.d/nginx.sh
# nginx -v
Tengine version: Tengine/2.0.3 (nginx/1.4.7)
# ls /usr/local/nginx/modules/
ngx_http_upstream_session_sticky_module.so
3.3.3 配置文件
user nginx;
worker_processes auto;
error_log logs/error.log;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
# load modules compiled as Dynamic Shared Object (DSO)
dso {
load ngx_http_upstream_session_sticky_module.so;
}
http {
include mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log logs/access.log main;
sendfile on;
#tcp_nopush on;
#keepalive_timeout 0;
keepalive_timeout 5;
#gzip on;
upstream dynamic {
server 192.168.23.90:8080;
server 192.168.23.91:8080;
#ip_hash;
#session_sticky;
}
server {
listen 80;
server_name localhost;
add_header X-Proxy Proxy-$server_addr:$server_port;
#charset koi8-r;
#access_log logs/host.access.log main;
location / {
proxy_pass http://dynamic/;
}
# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
}
3.4 WEB1和WEB2
3.4.1 安装jdk
# rpm -ivh jdk-7u9-linux-x64.rpm
Preparing... ########################################### [100%]
1:jdk ########################################### [100%]
Unpacking JAR files...
rt.jar...
Error: Could not open input file: /usr/java/jdk1.7.0_09/jre/lib/rt.pack
jsse.jar...
Error: Could not open input file: /usr/java/jdk1.7.0_09/jre/lib/jsse.pack
charsets.jar...
Error: Could not open input file: /usr/java/jdk1.7.0_09/jre/lib/charsets.pack
tools.jar...
Error: Could not open input file: /usr/java/jdk1.7.0_09/lib/tools.pack
localedata.jar...
Error: Could not open input file: /usr/java/jdk1.7.0_09/jre/lib/ext/localedata.pack
# tar xf apache-tomcat-8.0.12.tar.gz -C /usr/local/
# cd /usr/local
# ln -sv apache-tomcat-8.0.12/ tomcat
`tomcat' -> `apache-tomcat-8.0.12/'
# vim /etc/profile.d/jdk.sh
3.4.2 设置重要的java环境变量
# vim /etc/profile.d/jdk.sh
JAVA_HOME=/usr/java/latest
PATH=$JAVA_HOME/bin:$PATH
export JAVA_HOME PATH
# vim /etc/profile.d/tomcat.sh
export CATALINA_HOME=/usr/local/tomcat
export PATH=$CATALINA_HOME/bin:$PATH
# source /etc/profile.d/jdk.sh
# source /etc/profile.d/tomcat.sh
3.4.3 尝试运行
# /usr/local/tomcat/bin/catalina.sh start
Using CATALINA_BASE: /usr/local/tomcat
Using CATALINA_HOME: /usr/local/tomcat
Using CATALINA_TMPDIR: /usr/local/tomcat/temp
Using JRE_HOME: /usr/java/latest
Using CLASSPATH: /usr/local/tomcat/bin/bootstrap.jar:/usr/local/tomcat/bin/tomcat-juli.jar
默认可以运行。看到了可爱的tom猫
3.4.5 部署测试页
提供一个jsp页面并部署在tomcat的站点目录/usr/local/tomcat/webapps下
# cd /usr/local/tomcat/webapps/
# mkdir jsp
# cd jsp/
# vim index.jsp
<%@ page language="java" %>
<% session.setAttribute("Blog","MeXP"); %>
TomcatA
"#FF0000">TomcatA.magedu.com
Session ID | <%= session.getId() %> |
Created on | <%= session.getCreationTime() %> |
<%="URL: " + request.getRequestURL()%>
<%="URI: " + request.getRequestURI()%>
上面这个页面提供给WEB1,下面提供给WEB2。
<%@ page language="java" %>
<% session.setAttribute("Blog","MeXP"); %>
TomcatB
"#0000FF">TomcatB.magedu.com
Session ID | <%= session.getId() %> |
Created on | <%= session.getCreationTime() %> |
<%="URL: " + request.getRequestURL()%>
<%="URI: " + request.getRequestURI()%>
3.4.6 测试
在/usr/local/tomcat/conf/server.xml 配置文件中,
可以看到webapps目录是自动部署的,所以应该可以立即访问index.jsp页面,而且前端还nginx可以做LB调度。
上图中session是变化的,访问路径是/jsp
使用context组件标签重新映射路径
由上图可以看到,B服务器增加了一个叫做test的虚拟目录,也可以访问。
4. Session绑定
4.1 IP绑定
本实验使用Nginx的ip_hash实现。
修改Nginx的配置文件,在upstream中启用ip_hash。
ip_hash;
#session_sticky;
重读配置文件 # nginx -s reload
上图中同一个浏览器session相同。
上图中同一主机中不同浏览器访问同一个地址session相同。
以上的测试很好的说明了IP绑定的效果。
4.2 Session sticky
使用Tengine(淘宝定制的Nginx),其中集成了ngx_http_upstream_session_sticky_module模块。这个模块是动态加载的。
#ip_hash;
session_sticky;
重读配置文件 # nginx -s reload
上图中。请求提交了上一次的sessionID,但是响应的一个新的sessionID。
再次请求,本次提交了新的sessionID,被调度到同一session的服务器响应,也就是上一次处理这个用户请求的服务器。
5. session复制集群实现
5.1 session集群配置
将上面Tengine的配置文件ip_hash和session_sticky注释掉。
将下面的部分加入到tomcat的/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="228.0.0.4"
port="45564"
frequency="500"
dropTime="3000"/>
<Receiver className="org.apache.catalina.tribes.transport.nio.NioReceiver"
address="auto"
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.MessageDispatch15Interceptor"/>
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>
WEB-INF目录中增加
# cp /usr/local/tomcat/conf/web.xml /usr/local/tomcat/webapps/jsp/WEB-INF/
web-app标记后添加
配置文件的几点说明:
228.0.0.4为多播地址,端口为45564。可以修改。
Tomcat7、8中Cluster标签的配置不同,但主要配置是相同的,请参照官网
官方文档中
建议使用时间同步
Session黏性,这一点我认为是没有必要的,可以不予考虑
5.2 排错
Session一直没有复制,以至于访问的时候,Session不停的变化。
检查了安装、配置、日志,都无明细的问题。最后还是在马哥的指点下,发现了问题,就是Receiver的监听地址问题,这个一直是我排错的时候,看见了却认为没有问题的地方,实际上下图已经有了蛛丝马迹了。
Receive的地址address=”auto”,会是同步地址绑定在127.0.0.1上,就会同步失败。所以真正使用的时候,还是手动指定监听的IP最好。
因此,WEB1的server.xml
port="4000"
autoBind="100"
selectorTimeout="5000"
maxThreads="6"/>
WEB2的server.xml
port="4000"
autoBind="100"
selectorTimeout="5000"
maxThreads="6"/>
以上给Engine增加一个属性jvmRoute,这样在测试的结果中可以看得清楚一点。
5.3 数据分析
1) 心跳
可以看出使用多播传送心跳信息。
2) Session数据同步
使用TCP协议在监听端口4000上,Session复制的主机之间同个这个端口同步,使用TCP保证数据的传输的完整性。
上图,绿色框内是数据复制的过程。
将其中一个数据报文保存,用文本编辑器打开,可以清楚的看到要复制的sessionID就在其中。
3) http响应报文
客户端发起请求,调度器把请求转发给WEB2,WEB2响应。
上图可以清楚地看出响应首部中包含着sessionID的cookie信息。
6. Session服务器
msm(memcached-session-manager) + memcached + tomcat实现
将上例server.xml和web.xml中的session复制集群的配置去掉。
6.1 配置文件
在$CATALINA_HOME/conf/context.xml中添加如下内容。使用非黏性模式。
<Context>
<WatchedResource>WEB-INF/web.xmlWatchedResource>
<WatchedResource>${catalina.base}/conf/web.xmlWatchedResource>
<Manager className="de.javakaffee.web.msm.MemcachedBackupSessionManager"
memcachedNodes="n1:192.168.23.180:11211,n2:192.168.23.181:11211"
sticky="false"
sessionBackupAsync="false"
lockingMode="uriPattern:/path1|/path2"
requestUriIgnorePattern=".*\.(ico|png|gif|jpg|css|js)$"
transcoderFactoryClass="de.javakaffee.web.msm.serializer.kryo.KryoTranscoderFactory"
/>
Context>
6.2 需要使用的jar包
memcached-session-manager-1.8.2.jar
memcached-session-manager-tc8-1.8.2.jar
spymemcached-2.11.1.jar
asm-3.2.jar
kryo-1.04.jar
kryo-serializers-0.11.jar
minlog-1.2.jar
msm-kryo-serializer-1.8.2.jar
reflectasm-1.01.jar
缺一个tomcat都不能正常使用。部分jar包在墙外,其自行下载。
这些jar包放在$CATALINA_HOME/lib下。
6.3 实验及分析
6.3.1 实验结果
可以看出session保存在了n2上。调度到2个tomcat节点上,其session不变。
6.3.2 进一步分析
1) 初次请求阶段
上图很好的说明了WEB2上整个session的处理过程。
新建一个sessionID后,去n2取key为ping-n2没有找到,这个应该类似于健康监测。将sessionID存储到n2并设置校验值。然后去n1取key为ping-n1无果,将bak:sessionID存储。
2) 再次请求阶段
上图说明了第二次调度到了WEB1的整个session处理过程
从客户端获取了sessionID,去n2取key为ping-n2没有找到,提取到sessionID和校验值,重新存储校验值,sessionID存储失败,因为已经存在。然后去n1取key为ping-n1无果,将bak:sessionID存储失败,因为已经存在。设置新值key为bak:validity:sessionID。
7、总结
本文通过多组实验,使用不同技术实现了session持久机制。
(1) session绑定,基于IP或session cookie的。其部署简单,尤其基于session黏性的方式,粒度小,对负载均衡影响小。但一旦后端服务器有故障,其上的session将全部丢失。
(2) session复制集群,基于tomcat实现多个服务器内共享所有session。此方法可以保证任意一台后端服务器故障,其余各服务器上还都存有全部session,对业务无影响。但是它基于多播实现心跳,TCP单播实现复制,当设备节点过多,这种复制机制不是很好的解决方案。且并发连接多的时候,单机上的所有session占据的内存空间非常巨大,甚至耗尽内存。
(3) session服务器,将所有的session存储到一个共享的内存空间中,使用多个冗余节点保存session,这样做到session存储服务器的高可用,且占据业务服务器内存较小。是一种比较好的解决session持久的解决方案。
以上的方法都有其适用性。生产环境中,应根据实际需要合理选择。
不过以上这些方法都是在内存中实现了session的保持,可以使用数据库或者文件系统,把session数据存储起来,这样服务器重启后,也可以重新恢复session数据。不过session数据是有时效性的,是否需要这样做,视情况而定。
[ 参考资料 ]
http://tomcat.apache.org/tomcat-7.0-doc/cluster-howto.html#Cluster_Architecture
http://code.google.com/p/memcached-session-manager/wiki/SetupAndConfiguration
http://code.google.com/p/memcached-session-manager/wiki/FAQ