Schaalbare serverpush met Java

advertisement
20
JAVA ENTERPRISE 7
Efficiënte crossbrowser server-push
Met Java Enterprise 7
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? 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
JAVA magazine
20-23 schaalbare serverpush.indd 20-21
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:
Casus
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
Jurjen van
Geenen is software
architect bij Sogeti
Nederland BV. Hier
werkt hij in Java
Enterprise
projecten, bids
en onderhoud hij
betrekkingen
met diverse
onderwijsinstellingen.
@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.
Om het voorbeeld uit te kunnen werken
hebben we nodig:
< JDK 7 of hoger;
<E
en JEE 7-server, zoals JBoss WildFly 8
of GlassFish 4.0 (standaard configuraties
voldoen);
<E
en IDE (optioneel maar aanbevolen)
met Maven ondersteuning: de standaardconfiguratie van Netbeans 8.0 in de Java
EE versie voldoet;
<E
en browser (IE6 mag…. maar is niet
aanbevolen).
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.
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
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
Figuur 1: Class diagram
JAVA magazine | 05 2014
14-10-14 17:16
22
aan de request/response context. Een Thread
kost gemakkelijk één tot enkele megabytes
aan geheugen. Bij duizenden verbonden clients,
wat vrij gewoon is voor een webserver, kan dit
daarom gigabytes aan geheugenwinst opleve
ren, dit ten opzichte van synchrone afhandeling
waarbij een Thread eenvoudig geblokkeerd
wordt totdat een push-bericht beschikbaar is.
Registratie
Een willekeurigeThread uit de Http-ThreadPool van de server handelt een GET-request
naar “ webresources/aex/listen” af. De @
Path annotaties op AexResource en AexResource.register laat de
container dit request JAX-RS conform laatstgenoemde methode afhandelen. Vanwege
de @Suspended annotatie op de parameter
aexResource injecteert de container deze in de
methode aanroep (1) welke vervolgens plaats
vindt (2). Binnen AexResource.register
wordt vervolgens een nieuwe (2.1)aexResourceListener geregistreerd op zowel de
asyncResponse(2.2.1) als de aex (2.2.3):
< Registratie op aex is nodig voor het
luisteren naar nieuwe StockInfos;
< Registratie op asyncResponse is, zoals
eerder opgemerkt, nodig voor een nette huid
houding. Het wegwerp-object
aexResourceListener moet om
geheugenlekken te voorkomen, na een push
netjes worden verwijderd uit de collectieAex.
listeners. JAX-RS biedt voor dit soort
lifecycle-events zogenaamde requestprocessing callback interfaces.
CompletionCallback’s enige
onComplete-methode wordt na request
processing aangeroepen,mits de
Figuur 2: Sequence diagram
JAVA MAGAZINE
20-23 schaalbare serverpush.indd 22-23
implementatie tevoren via register
aangemeld is.
Push
Wanneer de EJB-timer aex.randomUpdate
aanroept (2), genereert deze elke drie
seconden een nieuweStockUpdate en stelt
onze aexResourceListener hiervan op de
hoogte (3.1). De aanroep gebeurt binnen de
Thread-context van de EJB timer-pool.
Client
De browser-client is eenvoudig, maar
doeltreffend. De belangrijkste functionaliteit
zit in functies init en listen van
js/ticker.js. Gebruikmakende van JQuery
haalt de functie init via een ajax-request
eerst alle StockInfo’s als JSON binnen van
‘ webresources/aex/stockInfos’. Deze
worden vervolgens aan de tabel van de
omringende HTML pagina toegevoegd.
Vervolgens abonneert de functie listen
zich via een ajax-request op de asynchrone
JAX-RS resource ‘webresources/aex/listen’. Via een oneindige repetitie worden zo
steeds nieuwe StockInfo’s opgehaald en
de details daarvan in de tabel getoond. Een
klein, maar belangrijk detail is de parameter
cache: false in beide ajax-requests. Deze
zorgt ervoor dat de browser niet de gevraagde
resources uit haar cache mag serveren.
Vergeet je deze mee te geven, dan ontstaat
in Internet Explorer 11 mogelijk een raceconditie, omdat de oude resources direct uit
de cache beschikbaar zijn.
Schaalbaarheid
Is asynchrone Long Polling met JAX-RS 2.0
schaalbaar? Zoals zo vaak in het leven geldt ook
Figuur 3: Geheugen gebruik
hier: meten is weten. Tijd voor een eenvoudig
experiment met JMeter. De gebruikte configura
tie 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.
Figuur 3 laat het geheugengebruik van de WildFly 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 overeen
komt met 1 van de Core i5’s Threads die bijna
maximaal benut wordt. Deze piek treedt steeds
op bij eenAex.update. Verwonderlijk is dit
niet, alle verbonden clients krijgen immers
hierin een nieuweStockUpdate. 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 geno
men 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 tiental-
len minuten testdraaien, draait alles foutloos.
Uit dit alles mogen we concluderen dat onze
Long Polling oplossing inderdaad behoorlijk
schaalbaar is.
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.
IS ASYNCHRONE
LONG POLLING
MET JAX-RS 2.0
SCHAALBAAR?
ZOALS ZO VAAK
IN HET LEVEN
GELDT OOK
HIER: METEN IS
WETEN!
BIBLIOGRAFIE
Oracle Corporation. (2013, Mei 22). JAX-RS: Java™ API for RESTful
Web Services Version 2.0. Redwood Shores, California, USA.
Sun Microsystems. (2009, December 10). Java™ Servlet
Specification Version 3.0. Santa Clara, California, USA.
JAVA MAGAZINE | 05 2014
14-10-14 17:16
Download