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 规划

基于LNMT的Session持久机制的多种方案实现及深入分析_第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

    "centre" border="1">     
      
        
        
      
      
        
        
     
    
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

    "centre" border="1">     
      
        
        
      
      
        
        
     
    
Session ID <%= session.getId() %>
Created on <%= session.getCreationTime() %>
<%="URL: " + request.getRequestURL()%>    
<%="URI: " + request.getRequestURI()%>     
  
 
   

 

3.4.6 测试

在/usr/local/tomcat/conf/server.xml 配置文件中,

            unpackWARs="true" autoDeploy="true">

可以看到webapps目录是自动部署的,所以应该可以立即访问index.jsp页面,而且前端还nginx可以做LB调度。

基于LNMT的Session持久机制的多种方案实现及深入分析_第2张图片

上图中session是变化的,访问路径是/jsp

 

使用context组件标签重新映射路径

            unpackWARs="true" autoDeploy="true">

   

基于LNMT的Session持久机制的多种方案实现及深入分析_第3张图片

由上图可以看到,B服务器增加了一个叫做test的虚拟目录,也可以访问。

 

 

4. Session绑定

4.1 IP绑定

本实验使用Nginx的ip_hash实现。

修改Nginx的配置文件,在upstream中启用ip_hash。

ip_hash;    
#session_sticky;

重读配置文件 # nginx -s reload

基于LNMT的Session持久机制的多种方案实现及深入分析_第4张图片

上图中同一个浏览器session相同。

基于LNMT的Session持久机制的多种方案实现及深入分析_第5张图片

上图中同一主机中不同浏览器访问同一个地址session相同。

以上的测试很好的说明了IP绑定的效果。

 

4.2 Session sticky

使用Tengine(淘宝定制的Nginx),其中集成了ngx_http_upstream_session_sticky_module模块。这个模块是动态加载的。

#ip_hash;    
session_sticky;

重读配置文件 # nginx -s reload

基于LNMT的Session持久机制的多种方案实现及深入分析_第6张图片 

上图中。请求提交了上一次的sessionID,但是响应的一个新的sessionID。

基于LNMT的Session持久机制的多种方案实现及深入分析_第7张图片

再次请求,本次提交了新的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的监听地址问题,这个一直是我排错的时候,看见了却认为没有问题的地方,实际上下图已经有了蛛丝马迹了。

基于LNMT的Session持久机制的多种方案实现及深入分析_第8张图片

Receive的地址address=”auto”,会是同步地址绑定在127.0.0.1上,就会同步失败。所以真正使用的时候,还是手动指定监听的IP最好。

因此,WEB1的server.xml

                      address="192.168.23.90"    
                      port="4000"    
                      autoBind="100"    
                      selectorTimeout="5000"    
                      maxThreads="6"/>

 

WEB2的server.xml

                      address="192.168.23.91"    
                      port="4000"    
                      autoBind="100"    
                      selectorTimeout="5000"    
                      maxThreads="6"/>

以上给Engine增加一个属性jvmRoute,这样在测试的结果中可以看得清楚一点。

 

5.3 数据分析

1) 心跳

基于LNMT的Session持久机制的多种方案实现及深入分析_第9张图片

可以看出使用多播传送心跳信息。

 

2) Session数据同步

使用TCP协议在监听端口4000上,Session复制的主机之间同个这个端口同步,使用TCP保证数据的传输的完整性。

基于LNMT的Session持久机制的多种方案实现及深入分析_第10张图片

上图,绿色框内是数据复制的过程。

基于LNMT的Session持久机制的多种方案实现及深入分析_第11张图片

将其中一个数据报文保存,用文本编辑器打开,可以清楚的看到要复制的sessionID就在其中。

 

3) http响应报文

基于LNMT的Session持久机制的多种方案实现及深入分析_第12张图片

客户端发起请求,调度器把请求转发给WEB2,WEB2响应。

基于LNMT的Session持久机制的多种方案实现及深入分析_第13张图片

上图可以清楚地看出响应首部中包含着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 实验结果

基于LNMT的Session持久机制的多种方案实现及深入分析_第14张图片

可以看出session保存在了n2上。调度到2个tomcat节点上,其session不变。

 

6.3.2 进一步分析

1) 初次请求阶段

基于LNMT的Session持久机制的多种方案实现及深入分析_第15张图片

上图很好的说明了WEB2上整个session的处理过程。

新建一个sessionID后,去n2取key为ping-n2没有找到,这个应该类似于健康监测。将sessionID存储到n2并设置校验值。然后去n1取key为ping-n1无果,将bak:sessionID存储。

 

2) 再次请求阶段

基于LNMT的Session持久机制的多种方案实现及深入分析_第16张图片

上图说明了第二次调度到了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