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