Efficiënte crossbrowser server-push met Java Enterprise 7

Jurjen van Geenen - Het HTTP-protocol is ooit bedacht voor overdracht van hypertext, bijvoorbeeld van een (web-)server naar een browser. De browser stuurt hiertoe een request en krijgt daarop een response van de web-server terug. Maar hoe stuur je vanuit een server informatie naar verbonden browsers, zonder dat deze hierom vragen?

Crossbrowser push Java Enterprise 7

Java Enterprise Edition 7 (JEE 7) standaardiseert WebSockets welke de gevraagde server-push mogelijk maken, maar niet alle browsers ondersteunen die. Internet Explorer in versies kleiner dan 10 zijn een bekend voorbeeld hiervan en hebben nog altijd een substantieel marktaandeel. De vraag die ik beantwoord in dit artikel is: hoe implementeer je efficiënte cross-browser server-push met standaard JEE technologie?

Best served async

JEE 6 (in het bijzonder Servlet 3.0) standaardiseerde het asynchroon afhandelen van requests door de server (Sun Microsystems, 2009). Het onderliggende concept is dat de server-Thread die een binnenkomende request accepteert, wordt ontkoppeld van het genereren van de response. Dit kan asynchroon, vanuit een andere Thread worden gedaan. We kunnen hiermee server-push simuleren door pas een response te genereren, wanneer vanuit de Thread-context een bericht beschikbaar komt. Ik spreek opzettelijk van simuleren: met het request poll je immers de beschikbaarheid van een nieuw push bericht. Deze techniek, waarin relatief lang een verbinding wordt opgehouden voordat een response wordt gegenereerd, heet long polling. Een belangrijk voordeel van asynchrone Servlets is efficiëntie. In de tussenliggende periode dat de response niet gekoppeld is aan een Thread, gebruikt deze relatief weinig resources. Het openhouden van een netwerkverbinding kost immers veel minder geheugen, dan een geblokkeerde Thread. Hierdoor is een push-oplossing op deze basis goed schaalbaar. De Servlet 3.0 async API is wel tamelijk low-level: je moet als ontwikkelaar zelf het nodige doen.

JEE7 to the RES(T)cue

Sinds JEE7 is asynchrone afhandeling door RESTful webservices middels JAX-RS 2.0 ook gestandaardiseerd (Oracle Corporation, 2013). Implementaties hiervan bouwen voort op asynchrone Servlets. JAX-RS heeft ten opzicht van standaard Servlets bekende voordelen: content negotiation en serialisatie worden je grotendeels uit handen genomen. Op beide onderdelen is de API ook pluggable. Een voorbeeld van een synchrone methode uit een RESTful webservice is het volgende:

@GET
@Path(“/syncPath”)
Public ReturnType getSomething(){

return returnValue;
}

Omgetoverd tot asynchrone REST-methode,
oogt dit als volgt:

@GET
@Path(“/asyncPath”)
Public void doSomething(@Suspended AsyncResponse response){

response.resume(returnValue);
}

De met @Suspended geannoteerde parameter maakt de container duidelijk dat returnValue asynchroon terug moet kunnen worden geschreven middels response.resume(returnValue). Request processing wordt tot dat moment geparkeerd (‘suspended’). Roepen we response.resume(returnValue)zoals hierboven binnen onze asynchrone REST-methode aan, dan hebben we geen profijt van onze nieuwe asynchrone API. Zowel het accepteren als het terugschrijven van de response zou immers in dezelfde Thread-context gebeuren. We hadden returnValue daarom net zo goed klassiek kunnen retourneren volgens het synchrone model. In het vervolg werk ik een voorbeeld uit van volledig asynchrone afhandeling.

Casus: streamen actuele informatie AEX

Stock tickers streamen actuele informatie over aandelen genoteerd aan de beurs naar browsers. In Nederland is dit de AEX, dus gaan we hiervan uit. We gaan de AEX simuleren met een component die autonoom de koersen van aandelen verandert. Browsers moeten via een asynchrone RESTful service van deze veranderingen op de hoogte worden gehouden. Het verdient aanbeveling om de sourcecode van het ‘StockTicker’ project op https://github.com/MousePilots/ StockTicker tijdens het lezen van dit artikel te bestuderen.

Ingrediënten casus AEX

Om het voorbeeld uit te kunnen werken hebben we nodig:

  • JDK 7 of hoger;
  • Een JEE 7-server, zoals JBoss WildFly 8 of GlassFish 4.0 (standaard configuraties voldoen);
  • Een IDE (optioneel maar aanbevolen) met Maven ondersteuning: de standaardconfiguratie van Netbeans 8.0 in de Java EE versie voldoet;
  • Een browser (IE6 mag…. maar is niet aanbevolen).

The big picture(s)

Voor we in detail treden, bespreken we eerst het ontwerp op hoofdlijnen aan de hand van structuur en gedrag:

  • Structuur:

Figuur 1 toont een overzicht van de belangrijkste klassen in de server. Onze enige echte domein-klasse StockInfo representeert informatie over een aandeel zoals naam, actuele prijs, en prijs-Trend (een eenvoudige enum). De AEX is vertegenwoordigd door de @Singleton EJB Aex. Deze genereert middels een @Schedule (een EJB-timer) op randomUpdate elke drie seconden een willekeurige prijsverandering van een aandeel in de property stockInfos. Via een AexListener kan naar zulke veranderingen worden geluisterd in vorm van veranderde StockInfo’s. De ontsluiting van Aex via REST wordt verzorgd door AexResource. Deze heeft twee methoden: getStockInfo om alle StockInfo’s ineens op te halen en listen om vanuit de browser te luisteren naar updates. Via een AexResourceListener abonneert deze methode zich op de Aex. Deze event-listener overerft zowel van AexListener als het JAX-RS interface CompletionCallback. Dit laatste interface dient vooral huishoudelijke doelen, ik kom hierop later terug.

Java Enterprise 7 - Figuur 1

  • Gedrag:

Figuur 2 toont het (asynchrone) gedrag van ons systeem ten aanzien vanaf het luisteren vanuit de browser naar prijsveranderingen op de beurs. De twee gekleurde vlakken geven hierin de twee verschillende Thread-contexten aan:

Het bovenste vlak omvat de registratie van de browser op de Aex. Het onderste vlak omvat het versturen van een nieuwe StockInfo als gevolg van een prijsverandering op de Aex na eerdere registratie: het versturen van een push bericht. Hiermee is meteen verduidelijkt dat tussen registratie en push geen Thread gekoppeld is hier: meten is weten. Tijd voor een eenvoudig experiment met JMeter. De gebruikte configuratie is als volgt:

  1. Een laptop met Core i5-3320M, 16GB RAM, Windows 7, gebruikt zowel voor JMeter als JBoss WildFly 8.1.0 standalone.xml (standaardconfiguratie) met 1-2GB heap;
  2. Java 8, X86-64;
  3. JMeter test plan met 2000 threads, ramp-up period 60 seconds, die allemaal ‘webresources/ aex/listen’ aanroepen.

Java Enterprise 7 - Figuur 2

Figuur 3 laat het geheugengebruik van de Wild- Fly 8.1 instantie zien tijdens het bombardement door 2000 threads van JMeter. De paarse grafiek toont de gebruikte heap en de rode de grootte van de heap. Hieruit blijkt dat ik WildFly een onnodig grote heap heb meegegeven: het daadwerkelijk gebruik piekt bij slechts 330MB. Windows’ TaskManager laat zien dat het CPU gebruik van de server hier schommelt van 14-22%: hetgeen op mijn machine overeenkomt met 1 van de Core i5’s Threads die bijna maximaal benut wordt. Deze piek treedt steeds op bij een Aex.update. Verwonderlijk is dit niet, alle verbonden clients krijgen immers hierin een nieuwe StockUpdate.

JMeter rapporteert na enkele minuten een throughput van ongeveer 38000 requests/minuut en een gemiddelde responsetijd per Long Pol van 1808 milliseconden. Aangezien Aex elke 3 seconden een update pusht, zouden we gemiddeld genomen een responsetijd van 1500 milliseconden verwachten. Het iets hogere daadwerkelijke gemiddelde is te verklaren door de manier waarop de response wordt weggeschreven. Alle naar verwachting 2000 responses worden namelijk door hetzelfde Thread afgehandeld, wat enige rekentijd mag kosten. Ook na tientallen minuten testdraaien, draait alles foutloos. Uit dit alles mogen we concluderen dat onze Long Polling oplossing inderdaad behoorlijk schaalbaar is.

Java Enterprise 7 - Figuur 3

Conclusie

Long Polling had als belangrijk nadeel dat het tot een groot geheugengebruik leidde op servers, omdat Threads geblokkeerd werden totdat berichten beschikbaar kwamen voor de hiermee geassocieerde responses. Door gebruik te maken van asynchronous request-processing uit JAX-RS 2.0 kunnen Threads losgekoppeld worden van hun request/response-context tijdens het wachten op push-berichten. Hiermee blijft het geheugengebruik van de server ook bij duizenden verbonden browsers minimaal. In tegenstelling tot WebSockets of Server Sent Events werkt long polling bovendien met alle browsers.

Opzoek naar (een baan als) Java specialist?

Ben je opzoek naar een Java-specialist of wil je als Java-specialist aan de slag bij Sogeti? Stuur dan een e-mail naar Robert Jan ten Dijke of bel hem via: