Tomcat - Klaster. Środowisko rozproszone oraz HA (wysoka dostępność)

Autor: Marcin Kasiński
02.11.2013 19:23:49 +0200

W niniejszym artykule postaram się opisać środowisko pozwalające na rozłożenie obciążenia aplikacji internetowych pomiędzy wiele instancji serwera WWW oraz Tomcat. Jako środowisko pokazowe wykorzystane zostaną w sumie 4 serwery z zainstalowanymi komponentami HAProxy, keepalived, httpd oraz tomcat.

Dla środowiska postawione zostały 2 główne cele. Pierwszym jest ciągłość pracy środowiska w przypadku awarii serwera poprzez zdublowanie serwerów. Każdy z komponentów środowiska występuje w dwóch instancjach i w normalnych warunkach oba serwery obsługują przychodzący ruch. Celem zdublowania jest to aby awaria jednego ze zdublowanych komponentów nie wpłynęła na ciągłość pracy. W przypadku awarii cały ruch będzie obsłużony przez drugi działający serwer do momentu, aż awaria nie zostanie rozwiązana. Drugim celem jest replikacja sesji aplikacji pomiędzy nodami. W przypadku prostego zdublowania serwera tomcat każdy z nodów ma własną sesję. W przypadku awarii jednego z nodów i ruch zostanie przekierowany na drugi powstanie wtedy nowa sesja. Oznacza to, że zostaną utracone wszelkie dane przechowywane w sesji danego użytkownika. Aby temu zapobiec należy sesje aplikacji przechowywać równocześnie na wszystkich nodach. W takiej sytuacji w przypadku awarii po przekierowaniu ruchu na drugi nod nie powstanie nowa sesja , tylko zostanie odnaleziona konkretna sesja związana z danym użytkownikiem.

Architektura rozwiązania

Celem opisu środowiska klastrowego Tomcat wykorzystam 4 maszyny z zainstalowanym systemem operacyjnym Fedora Core 19.

  • 192.168.146.133 Maszyna HA1 (HAProxy Load Balancer pierwszy nod)
  • 192.168.146.134 Maszyna HA2 (HAProxy Load Balancer drugi nod)
  • 192.168.146.100 Wirtulny adres HA0 (Wirtulne IP dla maszyn HA1 i HA2)
  • 192.168.146.130 Maszyna WWW1 (Serwer Apache oraz Tomcat pierwszy nod)
  • 192.168.146.132 Maszyna WWW2 (Serwer Apache oraz Tomcat drugi nod)

W naszej konfiguracji na zewnątrz wystawione jest IP wirtualne HA0. Wszelki ruch powinien odbywać się poprzez to wirtualne IP, które jest skonfigurowane na maszynach HA1 oraz HA2 za pomocą modułu keepalived. Oznacza to, że wywołując to wirtualne IP ruch będzie przekierowany na fizyczną maszynę HA1 lub HA2. Awaria jednej z tych maszyn spowoduje , że kolejne wywołania będą przekazywane do pozostałego działającego serwera.

Kiedy ruch trafi do serwera HA1 lub HA2 zostanie on obsłużony przez moduł HAProxy Load Balancer. Moduł ten ma za zadanie przekazanie ruchu do jednego z działających serwerów WWW1 lub WWW2.

Dalej na serwerze WWW1 lub WWW2 ruch trafi do serwera Apache httpd ze skonfigurowanym modułem proxy. Statyczna treść aplikacji webowej jest serwowany bezpośrednio przez serwer httpd. Treść dynamiczna za pomocą modułu proxy przekierowywana jest do jednego z serwerów tomcat. Serwery tomcat są skonfigurowane w klastrze, którego celem będzie replikacja sesji. W takiej konfiguracji przy awarii jednego serwera Tomcat kolejne zapytanie użytkowników będą kierowane do drugiego działającego serwera Tomcat, który to serwer będzie miał aktualny stan sesji. Oznacza to, że dla użytkownika końcowego awaria jednego Tomcata będzie niezauważona i nie wpłynie na działanie aplikacji.

Konfiguracja wszystkich serwerów

Poniżej znajdują się kroki jakie należy wykonać podczas instalacji serwera. Poniższe kroki należy wykonać na wszystkich maszynach składających się na nasze środowisko.
Podczas instalacji wybieramy standardowe opcje. Jako konfiguracje instalatora podczas instalacji wybieramy "serwer WWW". Po zakończeniu instalacji i restarcie serwera logujemy się na konto root. W pierwszym kroku aktualizujemy wszystkie pakiety na serwerze.

yum -y update

Następnie, jako że mamy do czynienia ze środowiskiem developerskim nie zajmujemy się tu zupełnie bezpieczeństwem, pozwalamy sobie na wyłączenie serwisu firewalld . Od tej pory środowisko mamy niezabezpieczone. Zwracam na to uwagę jeśli ktoś chciałby operację to wykonać na serwerach, na których zdecydowanie nie należy powyższej operacji wykonywać ze względów bezpieczeństwa.

systemctl stop firewalld.service
systemctl disable firewalld.service

Konfiguracja HAProxy Load Balancer

Na obu maszynach HA1 oraz HA2 powinno się zainstalować pakiety keepalived oraz haproxy. Keepalived odpowiada za wystawienie dla obu maszyn jednego wirtualnego IP dla komunikacji. Haproxy jest modułem Load Balancer pozwalającym na rozłożenie obciążenia pomiędzy istniejące serwery WWW1 i WWW2. Poniższe komendy instalują pakiety oraz włączają modułu co spowoduje, że będą one uruchamiane przy starcie serwera

yum install -y keepalived haproxy

systemctl enable haproxy.service 
chkconfig --level 2345 keepalived on 

Konfiguracja modułu haproxy.service

Plik konfiguracyjny haproxy znajduje się w lokalizacji /etc/haproxy/haproxy.cfg W pliku tym najważniejsze parametry to :

  • frontend : Parametry usługi HAProxy na jakiej następuje nasłuchiwanie na połączenia
  • backend : lista serwerów do których będzie następowało przekierowanie wraz z parametrami

W naszym przypadku na maszynach HA1 oraz HA2 na porcie 80 będzie wystawiona usługa HAProxy, która będzie przekierowywała komunikację do 2 serwerów 192.168.146.131 oraz 192.168.146.132, gdzie znajdują się uruchomione usługi httpd Apache nasłuchujące również na standardowym porcie 80.

Aby zrealizować komunikacje, o której jest mowa powyżej, na obu maszynach na koniec pliku konfiguracyjnego HAProxy należy dodać poniższy wpis.



# HAProxy's stats
listen stats  0.0.0.0:9090
  stats enable
  stats hide-version
  stats uri     /
  stats realm   HAProxy\ Statistics
  stats auth    admin:admin

frontend httpFrontEnd 0.0.0.0:80
    maxconn 100000
    option forwardfor header x-forwarded-for
    default_backend httpBackEnd

backend httpBackEnd
    server web1 192.168.146.131:80 cookie A check
    server web2 192.168.146.132:80 cookie B check

Dalej możemy uruchomić na obu maszynach serwis HA Proxy

systemctl enable haproxy.service 

Single Point Of Failure - Konfiguracja Keepalived

W wielu instrukcjach dotyczących keepalived możemy przeczytać, że w pierwszym kroku konfiguracji keepalived należy na obu maszynach HA1 oraz HA2 włączyć możliwość współdzielenia jednego IP oraz przeładować konfiguracje.

echo 1 > /proc/sys/net/ipv4/ip_forward 
echo 1 > /proc/sys/net/ipv4/ip_nonlocal_bind

sysctl -a | grep net.ipv4.ip_forward
sysctl -a | grep net.ipv4.ip_nonlocal_bind
sysctl -p

Celem ustawienia tych wartości na stałe aby były aktywowane przy starcie serwera należy w pliku /etc/sysctl.conf należy dodać poniższe linie (lub wyedytować ustawiając wartość 1):

net.ipv4.ip_forward = 1
net.ipv4.ip_nonlocal_bind = 1

Po tym można wykonać poniższą komendę celem przeładowania konfiguracji lub zrestartować serwer.

sysctl -p /etc/sysctl.conf

Z mojego doświadczenia wynika, że w przypadku dystrybucji Fedora Core 19 nie ma takiej potrzeby, wiec powyższe umieściłem tylko informacyjnie. Plik konfiguracyjny keepalived znajduje się w lokalizacji /etc/keepalived/keepalived.conf

W pliku tym najważniejsze parametry to :

  • virtual_ipaddress : Witrualny adres IP widoczny na zewnątrz
  • interface : nazwa interfesu sieciowego na jakim następuje komunikacja pomiędzy nodami keepalived

W naszym przypadku ustanowimy dla maszyn H1 oraz H2 wirtualne IP 192.168.16.100.

Na maszynie HA1 plik ten powinien mieć postać:

ovrrp_script chk_haproxy {
   script "killall -0 haproxy"   # verify the pid existance
   interval 2                    # check every 2 seconds
   weight 2                      # add 2 points of prio if OK
}

vrrp_instance VI_1 {
   interface eno16777736                # interface to monitor
   state MASTER
   virtual_router_id 51          # Assign one ID for this route
   priority 101                  # 101 on master, 100 on backup
   authentication {
auth_type PASS
auth_pass 1111 # put in your own numeric password here (In this example it's 1111)
}
   virtual_ipaddress {
       192.168.146.100            # the virtual IP
   }
   track_script {
       chk_haproxy
   }
} 

Na maszynie HA2 plik ten powinien mieć postać:


ovrrp_script chk_haproxy {
   script "killall -0 haproxy"   # verify the pid existance
   interval 2                    # check every 2 seconds
   weight 2                      # add 2 points of prio if OK
}

vrrp_instance VI_1 {
   interface eno16777736                # interface to monitor
   state BACKUP
   virtual_router_id 51          # Assign one ID for this route
   priority 101                  # 101 on master, 100 on backup
   authentication {
auth_type PASS
auth_pass 1111 # put in your own numeric password here (In this example it's 1111)
}
   virtual_ipaddress {
       192.168.146.100            # the virtual IP
   }
   track_script {
       chk_haproxy
   }
}
  

Dalej możemy uruchomić na obu maszynach serwis keepalived

systemctl enable haproxy.service 

Po uruchomieniu możemy zweryfikować interfejsy sieciowe. Przypominam o wprowadzeniu odpowiedniej nazwy interfejsu sieciowego.

ip a | grep -e inet.*eno16777736

Na maszynie H1 pełniącej rolę MASTER powinniśmy dostać wynik

inet 192.168.146.133/24 brd 192.168.146.255 scope global eno16777736
inet 192.168.146.100/32 scope global eno16777736

Na maszynie H2 pełniącej rolę BACKUP powinniśmy dostać wynik

inet 192.168.146.134/24 brd 192.168.146.255 scope global eno16777736

Oznacza to, że obecnie na maszynie H1 mamy wystawione IP wirtualne 192.168.146.100

Po powyższych operacjach możemy wylistować porty na jakich maszyny H1 oraz H2 nasłuchują

netstat -tulpn

W wyniku powyższej komendy na maszynach H1 oarz H2 powinniśmy otrzymać wynik jak poniżej. Z wyniku usunąłem nieistotne w naszym przypadku rekordy.

Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name
tcp        0      0 0.0.0.0:9090            0.0.0.0:*               LISTEN      448/haproxy
tcp        0      0 0.0.0.0:5000            0.0.0.0:*               LISTEN      448/haproxy
tcp        0      0 0.0.0.0:80              0.0.0.0:*               LISTEN      448/haproxy
...

Konfiguracja HTTP

Na obu maszynach WWW1 oraz WWW2 musimy zainstalować pakiet httpd. Jest to serwer Apache odpowiedzialny w naszym przypadku za serwowanie treści statycznej.

yum -y install httpd

Plik konfiguracyjny modułu proxy http znajduje się w lokalizacji /etc/httpd/conf.modules.d/00-proxy.conf

W pliku tym najważniejsze parametry to :

  • Proxy : Sekcja opisująca usługę proxy
  • BalancerMember : Parametry serwerów na które będzie przekierowywany ruch.
  • ProxyPass : Określa jakie URLe przychodzące do serwera WWW1 lub WWW2 będą przekierowywane do serwera Tomcat.

Na obu maszynach WWW1 oraz WWW2 plik ten powinien mieć postać:


# This file configures all the proxy modules:
LoadModule proxy_module modules/mod_proxy.so
LoadModule lbmethod_bybusyness_module modules/mod_lbmethod_bybusyness.so
LoadModule lbmethod_byrequests_module modules/mod_lbmethod_byrequests.so
LoadModule lbmethod_bytraffic_module modules/mod_lbmethod_bytraffic.so
LoadModule lbmethod_heartbeat_module modules/mod_lbmethod_heartbeat.so
LoadModule proxy_ajp_module modules/mod_proxy_ajp.so
LoadModule proxy_balancer_module modules/mod_proxy_balancer.so
LoadModule proxy_connect_module modules/mod_proxy_connect.so
LoadModule proxy_express_module modules/mod_proxy_express.so
LoadModule proxy_fcgi_module modules/mod_proxy_fcgi.so
LoadModule proxy_fdpass_module modules/mod_proxy_fdpass.so
LoadModule proxy_ftp_module modules/mod_proxy_ftp.so
LoadModule proxy_http_module modules/mod_proxy_http.so
LoadModule proxy_scgi_module modules/mod_proxy_scgi.so

<Proxy balancer://testcluster stickysession=JSESSIONID>
#<Proxy balancer://testcluster>
BalancerMember ajp://192.168.146.131:8009 min=10 max=100 route=node1 loadfactor=1
BalancerMember ajp://192.168.146.132:8009 min=20 max=200 route=node2 loadfactor=1
</Proxy>


ProxyPass /app balancer://testcluster/  

#bez poniższego każdy request to nowa sesja
ProxyPassReverseCookiePath / /app 

<Location /balancer-manager>
SetHandler balancer-manager
</Location> 
 

Po aktualizacji konfiguracji możemy włączyć (aby uruchamiał się automatycznie przy starcie systemu) i uruchomić serwis httpd

systemctl enable httpd.service 
systemctl start httpd.service 

Konfiguracja Tomcat

W naszym laboratoryjnym przypadku serwis httpd oraz tomcat znajdują się na jednej maszynie. W środowiskach produkcyjnych wskazane byłoby aby je rozdzielić na różne maszyny.

Na obu maszynach WWW1 oraz WWW2 należy zainstalować pakiet tomcat. Jest to serwer Tomcat odpowiedzialny w naszym przypadku za serwowanie treści dynamicznej i co ważniejsze za współdzielenie sesji pomiędzy nodami.

yum -y install tomcat tomcat-admin-webapps tomcat-webapps

Plik konfiguracyjny serwera tomcat znajduje się w lokalizacji /etc/tomcat/server.xml Na obu serwerach WWW1 oraz WWW2 modyfikujemy sekcje Engine ustawiając nazwę silnika.

Na serwerze WWW1 powinna ona mieć postać:

<Engine name="Catalina" defaultHost="localhost" jvmRoute="node1">

Na serwerze WWW2 powinna ona mieć postać:

<Engine name="Catalina" defaultHost="localhost" jvmRoute="node2">

Celem rozłożenia sesji pomiędzy nody w konfiguracji powinna zostać dodana sekcja Cluster definiująca klaster tomcat odpowiedzialny za przechowywanie sesji na wielu nodach. Rozróżniamy dwa rodzaje klastrów Tomcat. Klaster dynamiczny jest najprostszym rozwiązaniem , gdzie następuje automatyczna komunikacji pomiędzy nodami klastra za pomocą komunikacji multicastowej. Jeśli z jakiś przyczyn taka komunikacja z różnych przyczyn nie może być włączona należy zastosować klaster statyczny, gdzie w konfiguracji każdego noda musimy z góry zdefiniować adresy IP pozostałych członków klastra.

Tomcat - Klaster dynamiczny

Jeśli chcemy zdefiniować klaster dynamiczny do konfiguracji tomcat na obu nodach dodajemy poniższy wpis:


<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.JvmRouteSessionIDBinderListener"/>
          <ClusterListener className="org.apache.catalina.ha.session.ClusterSessionListener"/>
        </Cluster> 


Tomcat - Klaster statyczny

Jeśli chcemy zdefiniować klaster statyczny do konfiguracji tomcat na poszczególnych nodach dodajemy odpowiednią sekcje Cluster . W sekcji najważniejsze parametry to :

  • Receiver -> address : adres IP danego noda
  • Member : sekcja opisująca parametry członka klastra. Występuje tyle razy ile jest nodów w klastrze.

Na serwerze WWW1 wpis ten powinien mieć postać:


  <Cluster className="org.apache.catalina.ha.tcp.SimpleTcpCluster" channelSendOptions="8" channelStartOptions="3">
        <Manager className="org.apache.catalina.ha.session.DeltaManager"
                 expireSessionsOnShutdown="false"
                 notifyListenersOnReplication="true"/>

        <Channel className="org.apache.catalina.tribes.group.GroupChannel">

          <Receiver className="org.apache.catalina.tribes.transport.nio.NioReceiver"
                    address="192.168.146.131"
                    port="4110"
                    autoBind="9"
                    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.TcpPingInterceptor"/>
          <Interceptor className="org.apache.catalina.tribes.group.interceptors.TcpFailureDetector"/>
          <Interceptor className="org.apache.catalina.tribes.group.interceptors.MessageDispatch15Interceptor"/>

          <Interceptor className="org.apache.catalina.tribes.group.interceptors.StaticMembershipInterceptor">
                <!--Member className="org.apache.catalina.tribes.membership.StaticMember"
                  port="4110"
                  host="192.168.146.131"
                  domain="delta-static"
                  uniqueId="{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}"
                /-->
                <Member className="org.apache.catalina.tribes.membership.StaticMember"
                  port="4210"
                  host="192.168.146.132"
                  domain="delta-static"
                  uniqueId="{1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,0}"
                />
          </Interceptor>

        </Channel>
        <Valve className="org.apache.catalina.ha.tcp.ReplicationValve" filter=""/>
        <Valve className="org.apache.catalina.ha.session.JvmRouteBinderValve"/>

        <ClusterListener className="org.apache.catalina.ha.session.JvmRouteSessionIDBinderListener"/>
        <ClusterListener className="org.apache.catalina.ha.session.ClusterSessionListener"/>
      </Cluster>


Na serwerze WWW2 wpis ten powinien mieć postać:

  <Cluster className="org.apache.catalina.ha.tcp.SimpleTcpCluster" channelSendOptions="8" channelStartOptions="3">
        <Manager className="org.apache.catalina.ha.session.DeltaManager"
                 expireSessionsOnShutdown="false"
                 notifyListenersOnReplication="true"/>

        <Channel className="org.apache.catalina.tribes.group.GroupChannel">

          <Receiver className="org.apache.catalina.tribes.transport.nio.NioReceiver"
                    address="192.168.146.132"
                    port="4210"
                    autoBind="9"
                    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.TcpPingInterceptor"/>
          <Interceptor className="org.apache.catalina.tribes.group.interceptors.TcpFailureDetector"/>
          <Interceptor className="org.apache.catalina.tribes.group.interceptors.MessageDispatch15Interceptor"/>

          <Interceptor className="org.apache.catalina.tribes.group.interceptors.StaticMembershipInterceptor">
                <Member className="org.apache.catalina.tribes.membership.StaticMember"
                  port="4110"
                  host="192.168.146.131"
                  domain="delta-static"
                  uniqueId="{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}"
/>
<!--
                <Member className="org.apache.catalina.tribes.membership.StaticMember"
                  port="4210"
                  host="192.168.146.132"
                  domain="delta-static"
                  uniqueId="{1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,0}"
                />
-->
          </Interceptor>

        </Channel>
        <Valve className="org.apache.catalina.ha.tcp.ReplicationValve" filter=""/>
        <Valve className="org.apache.catalina.ha.session.JvmRouteBinderValve"/>

        <ClusterListener className="org.apache.catalina.ha.session.JvmRouteSessionIDBinderListener"/>
        <ClusterListener className="org.apache.catalina.ha.session.ClusterSessionListener"/>
      </Cluster>
  

Aplikacja testowa

Każda aplikacja webowa, która ma wykorzystywać współdzielenie sesji w ramach klastra w pliku web.xml wewnątrz tagu web-app musi posiadać wpis:

<distributable />

Celem przetestowania rozwiązania przygotowałem prostą aplikację zawierającą tylko jeden plik JSP. Zadaniem aplikacji jest wyświetlenie identyfikatora sesji , informacji czy sesja jest nowa oraz wartości atrybutu sesyjnego count . Wartość ta będzie przy każdym wywołaniu testowej stronie zwiększana co pozwoli zwerfikować poprawność działania rozwiązania.

<%@ page language="java" contentType="text/html; charset=ISO-8859-1" pageEncoding="ISO-8859-1"%>
<%
//   int name = request.getParameter( "username" );

String countString=  (String )session.getAttribute( "count" ) ;

int countInt=0;

if (countString!=null) countInt=Integer.parseInt(countString);

countInt++;

countString=String.valueOf(countInt);
   session.setAttribute( "count", countString );
%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
<title>Insert title here</title>
</head>
<body>

HostName=<%= request.getServerName() %><br>
Sessionid=<%= session.getId() %><br>
Sessionid is new=<%= session.isNew() %><br>
Count= <%= countString %>

</body>
</html>

Po zainstalowaniu aplikacji na obu nodach WWW1 oraz WWW2 zakładając, że nazwa aplikacji to WebSessionTester, możemy przetestować aplikację wywołując stronę:

http://192.168.146.100/app/WebSessionTester/

powrót
Zachęcam do przedstawienia swoich uwag i opinii w polu komentarzy.

Komentarze

Dodaj Komentarz