MSM實現tomcat集羣的session共享

本文首發於我的個人網站: https://hewanyue.com/
本文作者: Hechao
本文鏈接: https://hewanyue.com/blog/6254cc16.html

會話保持起源

  tomcat作爲一個應用服務器,單機性能上都是無法滿足生產中需要的,而想要解決高併發場景,光靠提升單機性能,成本與效果肯定都是無法讓人接受的,而此時我們一般都採用tomcat集羣的方式,用多臺tomcat服務器來共同支撐我們的業務。
  但這時就出現了一個新的問題,那就是會話保持。因爲每臺tomcat服務器的session是獨立的,當客戶端被調度到一個新的tomcat服務器時,他無法識別之前一臺的tomcat服務器分配的sessionID,於是對於此次訪問,之前的會話信息就都沒有了,這表現在用戶的客戶端就相當於,點開一個新的鏈接,就發現需要重新登陸,或者之前的購物車裏的商品都不見了等等。這樣的客戶訪問體驗絕對不是我們想要的,所以我們需要實現會話保持功能!

會話保持實現

  一般tomcat的會話保持有三種方案實現:

  • nginx、httpd或者haproxy的調度實現session綁定,一般是源地址哈希方式實現
    優點:簡單易配置
    缺點:
    ①如果目標服務器故障後,沒有做持久化的話就會丟失session;
    ②即便做了持久化,當服務器故障後,nginx或者haproxy會不得不重新分配一個tomcat服務器,而這時因爲新的tomcat服務器上沒有原來的sessionID,所以無法找到相應會話信息,會重新分配一個sessionID給客戶端,就算原來的tomcat服務器重新上線,又被分配到原來的tomcat服務器中,可此時客戶端已經有了新的sessionID,也不會去讀取最開始的session信息,那些會話信息就相當於永遠丟失了。
  • session複製集羣,官方給出的tomcat會話共享解決方案
      tomcat自己提供的多播集羣,通過多播將任何一臺的session同步到其他節點。
    缺點:
    ①tomcat的同步節點不宜過多,互相即時通信session需要太多帶寬;
    ②每一臺tomcat服務器都擁有全部session信息,內存佔用太多。
  • session server
      session 共享服務器,一般使用memcached、redis做共享的session服務器。
    目前最理想的解決方案,不過會需要額外的機器來配置共享服務器。

反向代理的session綁定

  這種實現session保持的方案,一般是使用的不多,一般用於公司內部中的會話保持場景。只需在haproxy或者nginx中的調度算法中,加入基於源地址hash即可(調度算法參考之前博客)。於是,用戶每次訪問都會被調度到同一臺tomcat服務器上,上面已有他的session信息,便實現了會話保持。

配置實現

  我們用一臺nginx服務器做反向代理,兩臺tomcat服務器來演示實現過程。

nginx主機ip:192.168.32.207
tomcat主機1ip:192.168.32.231
tomcat主機2ip:192.168.32.232

  將3臺機子中的nginx或tomcat服務啓動之後,分別檢查80端口和8080端口是否都已監聽,確保服務啓動。

iptables -F
getenforce 0

  確保防火牆和SELinux設置不會干擾我們幾臺主機間的相互通信。
  nginx主機反向代理的配置

upstream tomcat {
    #ip_hash; #先關閉原地址iphash,觀察效果
    server 192.168.32.231:8080 weight=1 fail_timeout=5s max_fails=3;
    server 192.168.32.232:8080 weight=1 fail_timeout=5s max_fails=3;
}

server {
    listen 80;
    index index.jsp
#    server_name www.example.net;
#    location ~* \.(jsp|do)$ {
    location / {
        proxy_pass http://tomcat;
    }
}

  可啓用主機名,也可不啓用直接用端口訪問。用域名訪問還需要改DNS或者hosts設置,比較麻煩,我們就直接通過IP+端口訪問就可以了。
  tomcat服務器中可以新建一個host,也可使用原先的localhost默認主機。我們這次不用之前的localhost,而是自己新創建一個host標籤,指定appBase在/data/myapp目錄下。兩個tomcat服務器都要執行如下操作:

vim /apps/tomcat/conf/server.xml

  找到Engine標籤,將默認主機修改爲``myapp`

    <Engine name="Catalina" defaultHost="myapp">

  找到localhost</Host>標籤,在下面創建新的主機myapp,指定appBase爲/data/myapp

      <Host name="myapp"  appBase="/data/myapp"
            unpackWARs="true" autoDeploy="true">
      </Host>

  PS:Host name一般爲主機域名,當一個tomcat中有多個host服務時,就是通過http報文請求頭部的host信息來判斷去訪問哪個host服務的,當找不到對應的主機之後才訪問defaultHost,我們這裏因爲只有啓用了一個host,且爲defaultHost,所以就無所謂,可以任意命名了,只需對應上就好。
  之後我們要在/data/myapp目錄下創建一個ROOT目錄來作爲tomcat訪問的默認目錄,注意ROOT是大寫的。

mkdir -p /data/myapp/ROOT

  爲了方便我們看到效果,我們編寫index.jsp時,調用一些函數,方便我們看到我們訪問的tomcat主機的IP和端口、sessionID、訪問時間。

vim /data/myapp/ROOT/index.jsp
<%@ page import="java.util.*" %>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>lbjsptest</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>

  此時,我們訪問nginx的80端口,就可以被反向代理到後端的兩個tomcat服務器上去。而這時,是輪詢的調度方式,也就是一邊一次,可以看到訪問的主機和sessionID都是一直在變化的。
效果如下圖所示:
nginx輪詢tomcat

  每一次的sessionID都沒有重複過,這肯定不滿足我們的需要。所以我們將nginx配置中#ip_hash;的#註釋去掉,重啓nginx服務,再看效果,如下圖所示:
在這裏插入圖片描述
  可以看到,每次刷新,主機IP和sessionID都不再變化,說明綁定session成功。

session複製集羣

  這是tomcat官方提供的解決方案,所有tomcat上都有全量的session,不過同步session信息會消耗帶寬,而且所有服務器保存所有session信息也比較佔用資源,對於tomcat這種本身就處於效率瓶頸的服務來說,高併發場景下超過若五個tomcat服務器,就不再建議使用。
  配置詳細可以參見官網配置說明

        <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.MessageDispatchInterceptor"/>
          </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>

  將官網上的這段集羣配置,插入到兩個tomcat服務器中我們創建的host標籤中(也可插入引擎標籤中,就相當於所有主機都生效),即<Host name標籤與</Host>標籤的中間。
  配置說明:

Cluster 集羣配置
Manager 會話管理器配置
Channel 信道配置
        Membership 成員判定。使用什麼多播地址、端口多少、間隔時長ms、超時時長ms。同一個多播地址和端口認爲同屬一個組。使用時修改這個多播地址,以防衝突
        Receiver 接收器,多線程接收多個其他節點的心跳、會話信息。默認會從4000到4100依次嘗試可用端口。
        address="auto",auto可能綁定到127.0.0.1上,所以一定要改爲可以用的IP上去
        Sender 多線程發送器,內部使用了tcp連接池。
        Interceptor 攔截器
ReplicationValve 檢測哪些請求需要檢測Session,Session數據是否有了變化,需要啓動複製過程
ClusterSessionListener 集羣session偵聽器

  複製完官方文檔裏的配置,我們還需要修改接收器的ip爲本機ip,不能使用auto,否則會無法同步session信息。
  此外,還需要在應用的web.xml文件中最後一行</web-app>標籤的上面一行插入子標籤<distributable/>表示可分配。
  操作如下:

mkdir /data/myapp/ROOT/WEB-INF
cp /apps/tomcat/conf/web.xml /data/myapp/ROOT/WEB-INF/
sed -i '/<\/web-app>/i<distributable/>' /data/myapp/ROOT/WEB-INF/web.xml

  此時,如果我們將前端Nginx或者其他反向代理服務器中的源地址hash去掉,我們就可以看到,雖然訪問的tomcat服務器在變化(服務器ip變化),而sessionID卻是不變的,因爲每個tomcat服務器上都有全量的相同的session信息。
集羣tomcat

session 共享服務器

  這種實現方式其實才是本文標題中真正的session共享,畢竟共享經濟炒的很熱,我們都知道,所謂共享,共用才叫共享,前面提到的那種tomcat集羣裏會話信息人手一份可不能稱作是共享。於是乎,我們想到用將session放到外部存儲,所有的tomcat服務器都去外部存儲中去查找sessionID。
  儲存session信息肯定不能存儲在磁盤文件中,這樣的讀取寫入性能都會很慢,放在mysql數據庫中或者以硬盤文件的方式保存,高併發場景下的讀取寫入速度都將會大打折扣。所以我們要使用類似memcached或者redis這種Key/Value的非關係型數據庫裏,也被稱作NoSQL。
  memcached和redis這種鍵值對型數據庫的數據信息都是存儲在內存中的,讀寫效率都很高,而且由於沒有複雜的表關係,採用的是哈希算法,他們對於信息的查找都是O(1),而不是類似mysql等數據庫的O(n),意思就是說耗時/耗空間與總數據量大小無關,不會隨着數據量的增大導致查找時間幾何倍數的增長。
  但也因爲memcached和redis的數據都是儲存在內存中的,而且memcached還不支持持久化,所以我們一定要做好高可用,一旦發生故障,會話信息將立即丟失,幾乎沒有恢復的可能。
  要實現tomcat共享session服務器,首先,我們要讓tomcat將session儲存到memcached或者redis等外部存儲上,這就需要我們對tomcat進行配置,其次我們要將session信息序列化爲變爲字節流以便能儲存在session服務器中,還要能將session服務器中的數據反序列化爲可以識別的session信息,最後,當然我們還需要一個客戶端來跟後端的session服務器通信,才能將數據寫入和讀取。
  這想實現確實也比較複雜,不過在github已有開源解決方案(網址是https://github.com/magro/memcached-session-manager),memcached-session-manager,簡稱msm,後端採用memcached或者redis都可以(之前只支持memcached,因而得名msm,後來支持redis後,人們還是習慣叫它msm),且已經完成了對tomcat的session共享的配置支持(支持tomcat6.X、7.X、8.X、9.X),我們直接去下載對應版本的去使用就可以了。
  根據項目的介紹文檔,我們知道,想實現tomcat的session共享,我們至少需要配套的工具有:

  • tomcat的session管理工具 memcached-session-manager
  • 與session服務器通信的客戶端
    如果是memcached,則建議使用spymemcached.jar
    如果是redis,則建議使用jedis.jar
  • 將session信息序列化的工具,作者推薦使用kryo。

  這些工具官網上也都提供了下載鏈接,我們直接下載下來即可。
  kryo如下圖所示
kryo
  其他工具包如下圖所示
msm
  將這些jar包統統拷貝到tomcat服務器的lib目錄下(改變lib目錄下的jar包需重啓tomcat服務才能生效)
  使用msm搭建session共享服務器,如果後端爲memcached,則有兩種模式可以選,分別是sticky模式和non-sticky模式,後端爲redis,則使用類似non-sticky模式。

sticky模式

  以兩臺服務器爲例,將tomcat1和memcached1部署在一臺服務器上(簡稱爲t1、m1),tomcat2和memcached2部署在另一臺服務器上(簡稱爲t2、m2)爲例,結構圖如下圖所示。

<t1>   <t2>
  . \ / .
  .  X  .
  . / \ .
<m1>   <m2>

  實現原理:當請求結束時Tomcat的session會送給memcached備份。即Tomcat session爲主session,memcached session爲備session,使用memcached相當於備份了一份Session。查詢Session時Tomcat會優先使用自己內存的Session,Tomcat通過jvmRoute發現不是自己的Session,便從memcached中找到該Session,更新本機Session,請求完成後更新memcached。
  可能有的朋友看的一頭霧水,這到底是個什麼結構。其實很簡單,sticky模式就是t1的session信息還是儲存在t1上,不過以m2爲備用數據庫,t2的session信息也是放在t2中儲存,以m1服務器爲備用服務器。這就意味着,用戶在訪問t1時,是從t1獲取session信息,當t1掛掉或者整個節點1服務器掛掉之後,用戶會被調度到t2上,而t2本地中沒有session信息時,就會去m2中上找相關sessionID,而m2因爲是t1的備用存儲,所以有跟t1完全相同的session信息,於是用戶的sessionID就可以被t2識別;而當m2備用存儲服務掛掉之後,t1服務會通過檢測發現自己沒有備用存儲,就會自動將m1也指定爲自己的備用存儲,將備份信息也同步至m1中,於是用戶若再從t2訪問時,雖然因爲m2掛掉,其中的數據都無法訪問,但t2就可以從m1上讀取到對應的sessionID並同步到t2本身的存儲中,也可以保持之前的會話信息。
  修改的配置也很簡單,依照官網說明,將下面的代碼標籤插入/conf/context.xml文件中的context標籤結尾就可以了

<Manager className="de.javakaffee.web.msm.MemcachedBackupSessionManager"
memcachedNodes="n1:192.168.32.231:11211,n2:192.168.32.232:11211"
failoverNodes="n1"
requestUriIgnorePattern=".*\.(ico|png|gif|jpg|css|js)$"
transcoderFactoryClass="de.javakaffee.web.msm.serializer.kryo.KryoTranscoderFactory"
/>

  n1、n2只是memcached的節點別名,可以重新命名。
failoverNodes是指故障轉移節點,也就是發生故障之後的備用節點,所以在n1節點上,n1是備用節點,n2是主存儲節點。另一臺Tomcat中配置將failoverNodes改爲n2,意思是其主節點是n1,備用節點是n2。
若配置成功,在/apps/tomcat/log/catalina.out文件中看到如下信息。

tail -n 20 /apps/tomcat/logs/catalina.out
23-Nov-2019 13:35:54.187 INFO [myapp-startStop-1] de.javakaffee.web.msm.MemcachedSessionService.startInternal --------
-  finished initialization:
- sticky: true
- operation timeout: 1000
- node ids: [n1]
- failover node ids: [n2]
- storage key prefix: null
- locking mode: null (expiration: 5s)
--------

此時在訪問我們的前端代理(取消ipHASH綁定)就會看到下面界面,將節點2 關機,可以看到訪問ip固定爲192.168.32.231,而使用的memcached變成了n1節點。
tomcatsticky

non-sticky模式

  從msm 1.4.0之後開始支持non-sticky模式。
Tomcat session爲中轉Session,如果n1爲主session,n2爲備session,則產生的新的Session會發送給主、備memcached,並清除本地Session,也就是說tomcat本身不儲存session信息,只負責產生session。
  需要注意的是,如果n1下線,n2轉換爲主節點。n1再次上線,n2依然是主Session存儲節點。
  配置方法與sticky大致相同,不過在/conf/context.xml文件中的context標籤結尾插入代碼略有不同,具體代碼如下

<Manager className="de.javakaffee.web.msm.MemcachedBackupSessionManager"
memcachedNodes="n1:192.168.32.231:11211,n2:192.168.32.232:11211"
sticky="false"
sessionBackupAsync="false"
lockingMode="uriPattern:/path1|/path2"
requestUriIgnorePattern=".*\.(ico|png|gif|jpg|css|js)$"
transcoderFactoryClass="de.javakaffee.web.msm.serializer.kryo.KryoTranscoderFactory"
/>

  重啓tomcat服務後生效。此時在/apps/tomcat/log/catalina.out文件中看到如下信息。

tail -n 20 /apps/tomcat/logs/catalina.out
23-Nov-2019 13:43:05.863 INFO [myapp-startStop-1] de.javakaffee.web.msm.MemcachedSessionService.startInternal --------
-  finished initialization:
- sticky: false
- operation timeout: 1000
- node ids: [n1, n2]
- failover node ids: []
- storage key prefix: null
- locking mode: uriPattern:/path1|/path2 (expiration: 5s)
--------

  再次嘗試訪問負載代理服務器,發現同樣實現了訪問tomcatIP變化,sessionID不變,說明配置成功。
  而後端使用redis作爲session共享服務器時,僅支持non-stricky模式。建議用另外的服務器安裝redis服務,並修改監聽IP後啓動,tomcat服務器中將jedis.jarjar包拷貝至tomcat安裝路徑下lib目錄,同樣在/conf/context.xml文件中的context標籤結尾插入下面的代碼即可(例如redis服務器IP端口爲192.168.32.233:6379,可配置redis集羣,可參考我之前博客redis高可用配置)。

<Manager className="de.javakaffee.web.msm.MemcachedBackupSessionManager"
memcachedNodes="redis://192.168.32.233:6379"
sticky="false"
sessionBackupAsync="false"
lockingMode="uriPattern:/path1|/path2"
requestUriIgnorePattern=".*\.(ico|png|gif|jpg|css|js)$"
transcoderFactoryClass="de.javakaffee.web.msm.serializer.kryo.KryoTranscoderFactory"
/>
發佈了43 篇原創文章 · 獲贊 52 · 訪問量 9174
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章