Rozklad zátěže pomocí JMS fronty

Před nějakou dobou jsme v jedné velké energetické společnosti nasazovali řešení, v jehož středu stojí náš Identity Management CzechIdM. Komunikuje s ostatními systémy a prostřednictvím portálového rozhraní umožňuje statisícům zákazníků spravovat své heslo, vyžádat si heslo zapomenuté nebo provést samoregistraci do systému (kdysi jsem tu o tom psal). V jednom okamžiku tak můžou CzechIdM bombardovat stovky výpočetně náročných požadavků ze všech stran. Pojďte si přečíst, jak jsme se s extrémními nároky vypořádali.

Fronta

Úvod

Každý informační systém má své limity, sebelepší hardware je schopen v jednom okamžiku zpracovávat pouze jistý počet instrukcí. Složité prostředí moderního informačního systému s sebou přináší omezení daleko víc. Kromě paměti, místa na disku a procesorového času od operačního systému je třeba počítat s omezeným síťovým spojením, omezenou možností komunikovat s databázemi a schopností portálu reagovat na určitý počet vstupů v jednom okamžiku.

V našem případě je každý požadavek z portálu důležitý, protože pro uživatele znamená změnu hesla, tedy bezpečnostní operaci. Žádný požadavek tedy nesmí zůstat neobsloužen. Co byste udělali, kdybyste byli majitelé supermarketu, sto zákazníků by chtělo platit a vy měli jen deset otevřených pokladen? Nechali byste těch devadesát zákazníků odejít bez placení? Předpokládám, že ne, jednoduše byste je seřadili do front a nechali chvíli čekat. Moderní informační systémy fungují úplně stejně a CzechIdM není výjimkou.

Technologie JMS

Java ve svých standardních knihovnách nabízí technologii zasílání zpráv (Java Messaging Services, JMS). Různé subjekty v programu si mohou pomocí zpráv předávat data. Zprávy jsou objekty držené ve frontě v paměti nebo v databázi a jsou technologií uchovávány, dokud je adresát nepřevezme.

V našem případě tedy uživatelské požadavky obsluhují jednoduché třídy, které z každého příchozího požadavku sestaví zprávu, co nejrychleji ji strčí na konec fronty a usnou, dokud nedostanou odpověď.

O fronty se stará samotný aplikační server, my používáme JBoss 5.1. Konfigurovat novou frontu znamená upravit soubor components.xml a přidat do něj nového odesilatele:

<jms:managed-queue-sender name="myQueueSender" auto-create="true" queue-jndi-name="queue/myQueue"/>

, kromě toho vytvořit frontě vlastní definici jako nové službě:

<?xml version="1.0" encoding="UTF-8"?>
<server>
<mbean
code="org.jboss.jms.server.destination.QueueService"
name="jboss.messaging.destination:service=Queue,name=myQueue"
xmbean-dd="xmdesc/Queue-xmbean.xml">
<depends optional-attribute-name="ServerPeer">jboss.messaging:service=ServerPeer</depends>
<depends>jboss.messaging:service=PostOffice</depends>
</mbean>
</server>

a nakonec napsat třídu, která bude do fronty přidávat zprávy:

@Name(TaskMultithreadController.COMPONENT_NAME)
@Stateless
public class TaskMultithreadControllerBean implements TaskMultithreadController {

@In("myQueueSender")
protected QueueSender queueSender;

@In(create = true)
protected QueueSession queueSession;

public Object executeTask(ApplicationTask task, int priority, boolean waitForResponse) {
try {

//pripravim zpravu k odeslani
ObjectMessage message = this.queueSession.createObjectMessage();
TemporaryQueue tempQueue = null;
MessageConsumer consumer = null;

//pokud ocekavam odpoved, pripravim si prichozi kanal
if (waitForResponse) {
tempQueue = this.queueSession.createTemporaryQueue();
consumer = this.queueSession.createConsumer(tempQueue);
message.setJMSReplyTo(tempQueue);
}

message.setObject(task);
this.queueSender.send(message, DeliveryMode.PERSISTENT, priority, timeout);

...

//pokud mam cekat na odpoved, vytvorim si docasnou zpetnou frontu a posloucham na ni
if (waitForResponse) {
ObjectMessage response = (ObjectMessage) consumer.receive(waitForResponseTimeout);

Object result = null;

//pokud mi prisla nejaka odpoved, vratim ji.
if (response != null) {
result = response.getObject();
}
return result;
} else {
return null;
}
...
}

Message Driven Bean

Na druhém konci fronty poslouchají takzvané Message Driven Beany, tedy třídy reagující na příchozí zprávy. Jejich počet je ovšem omezený, nikdy jich nebude víc, než dovolíme v konfiguraci. Díky tomu máme jistotu, že nedojde paměť, nepřekročíme povolený počet spojení do databází nebo že aplikační server kvůli vytížení nezačne příchozí požadavky zahazovat.

Message Driven Bean tedy postupně odebírají požadavky z fronty a zajišťují jejich vykonávání. V Javě EE stačí třídu označit několika anotacemi a implementovat příslušný interface:

@Name("myMDBean")
@Depends( {"jboss.web.deployment:war=/idm"} )
@MessageDriven(
        name = "myMDBean",
        activationConfig = {
                @ActivationConfigProperty(propertyName = "destinationType", propertyValue = "javax.jms.Queue"),
                @ActivationConfigProperty(propertyName = "destination", propertyValue = "queue/myQueue"),
                @ActivationConfigProperty(propertyName = "maxSession", propertyValue = "10")}
)
public class TaskMultithreadExecutorBean implements MessageListener {

    @In(create = true)
    protected QueueSession queueSession;

   public void onMessage(Message message) {
        try {
            //zjistim, jestli je uvedeno, kam se ma odpovidat
            Destination replyTo = message.getJMSReplyTo();
            Integer timeout = null;
            //pokud se odpovidat ma, nachystam si producer, kterym odpoved odeslu a prectu si timeout
            if (replyTo != null) {
                replyProducer = queueSession.createProducer(null);

                timeout = IdMConfiguration.getInstance().getAsInteger("queueResponseTimeout");
            }
            ApplicationTask task = message.getObject();
            ...
             //pokud je kam odpovidat, odpovim (s nejvyssi prioritou)
             if (replyTo != null) {
                        ObjectMessage response = queueSession.createObjectMessage();                
                        response.setObject(result);

                        replyProducer.send(replyTo, response, DeliveryMode.PERSISTENT, 9, timeout);
           }
           ... 
        } catch (Throwable e) {
           ...
        }
    }
}

Hlavní metodou Message Driven Beany je onMessage(). Na vstupu dostane celou zprávu a může ji zpracovat. Oříškem může být, pokud chcete ve zprávě posílat několik netriviálních objektů. Standardně totiž zprávou můžete odeslat objekt jeden, anebo pouze primitivní typy a řetězec. S omezením jsme se vypořádali pomocí standardní Java serializace a kódování base64 – sebesložitější objekt převedeme pro zprávu na řetězec a po rozbalení zprávy ho zase sestavíme.

Jak řídít počet obslužných vláken

Message Driven Beany jako ta, kterou jsme si před chvílí představili, mohou tvořit pool omezené velikosti. Aplikační server pro něj vyčlení jen jistý počet vláken, například 10. Máme-li tedy jistotu, že 10 paralelních požadavků naše prostředí ustojí, můžeme omezit velikost poolu pomocí parametru jako v kódu výše v anotaci:

@ActivationConfigProperty(propertyName = "maxSession", propertyValue = "10")

To je vše. Technologie JMS se postará o všechno ostatní – o řízení fronty a její synchronizaci, o přidělování volných vláken i o případnou peristenci zpráv v databázi.

Závěr

V článku jsme se podívali na technologii JMS, kterou dnes CzechIdM používá pro rozklad zátěže z mnoha paralelních uživatelských požadavků. Kdybyste chtěli vědět víc, nebojte se mi napsat na vojtech.matocha@bcvsolutions.eu. Brzy opět na přečtenou!