• Keine Ergebnisse gefunden

7.3 Implementierung der Middleware-Komponenten

7.3.1 Implementierung der Taskverarbeitung

Die Verwendung des Pipelining-Protokolls zur effizienten Netzwerkkommunikation impliziert die Anordnung von Berechnungen als einzelne Tasks. Zur Unterst¨utzung der Applikations-entwicklung wird eine Programmbibliothek vorgestellt, die das Pipelining implizit durchf¨uhrt

he von Open Source-Implementierungen wie beispielsweiseCouchDB,Apache Cassandra undMongoDB.

(siehe [SF12])

ferb

Abbildung 7.3: UML-Diagramm der verf¨ugbaren Softwarepakete und deren wichtigste Klassen.

und die n¨otigen Funktionen zur Task-Programmierung bereitstellt. Die eigentlichen Daten-transfers werden von der Programmbibliothek durchgef¨uhrt und ebenso wie das Marshalling und Unmarschalling von Task-Objekten vor der Applikation verborgen. Wie bereits in Kapi-tel 4 beschrieben, besteht ein Task bei der ¨Ubertragung via Netzwerk aus folgenden Teilen:

struct Task {

Beim Transfer via Netzwerk vom Client zur VM werden task_id,task_function und in-put_dataubertragen. In die Gegenrichtung werden nur¨ task_idund output_data ¨ ubertra-gen. Anhand dertask_idk¨onnen auf dem Client ausgehende und eingehende Daten einander zugeordnet werden.

Im Allgemeinen ist davon auszugehen, dass die Ausf¨uhrungsdauer der Tasks im Vergleich zur Daten¨ubertragungszeit hoch ist, da sonst eine entfernte Ausf¨uhrung nicht sinnvoll w¨are.

Somit ist es notwendig, Puffer zu verwenden, die Tasks zwischenspeichern k¨onnen, damit w¨ahrend der Ausf¨uhrung weitere Tasks ¨ubertragen werden k¨onnen. Die Gr¨oße der Puffer wird begrenzt durch die ¨Uberlegung, dass auf einem Client ¨ublicherweise nur begrenzt viel Arbeitsspeicher zur Verf¨ugung steht. Somit sollten die Puffer auf dem Client nicht gr¨oßer ausfallen als n¨otig. Außerdem kann eine kabellose Netzwerkverbindung auch unvermittelt getrennt werden. Da jedoch mobile Daten¨ubertragungen oft bezahlt werden m¨ussen, sollten stets nicht mehr Tasks zur VM ubertragen werden, also dort auch momentan verarbeitet¨ werden k¨onnen. Im Falle eines Verbindungsabbruchs m¨ussten sonst eventuell Tasks erneut ubertragen werden und deren ¨¨ Ubertragungsdatengr¨oße w¨urde erneut abgerechnet.

Task und Task-Client-API

Die Java-API, die die Pipelining-Funktionalit¨at und die dazu notwendige Taskverarbeitung auf einem Client nutzbar macht, steht im Softwarepaket ferb.pipe zur Verf¨ugung. Die ab-strakte Klasse Task implementiert die grundlegenden Taskfunktionen. Von Task abgeleitete

7.3 Implementierung der Middleware-Komponenten

+ connect(String session_key, String server, int port):boolean + configureSSL(String keystore, String password):void

Abbildung 7.4: UML-Diagramm der Klassen aus dem Softwarepaket ferb.pipe zur Imple-mentierung und Verarbeitung eines beispielhaften EchoTask.

Klassen implementieren die eigentlichen Berechnungstasks. Zudem gibt es f¨ur die VMs die KlasseTaskServerPipelineals Implementierung der Taskverarbeitung auf Serverseite. F¨ur die Clients gibt es die KlasseTaskClientPipeline. Die UML Repr¨asentation der Klassen ist in Abbildung 7.4 zu sehen.

Die abstrakte KlasseTaskist vonjava.lang.Threadabgeleitet und kann somit nebenl¨ au-fig ausgef¨uhrt werden. Dies erm¨oglicht die parallele Verarbeitung von Tasks. Die KlasseTask wird auf Client- und auf Serverseite zur Repr¨asentation der Berechnungstasks benutzt. Sie im-plementiert die Methoden zur ¨Ubergabe der EingabedatensetInput(byte[] data)und zum Abholen der AusgabengetOutput(). Die eigentliche Taskfunktion byte[] process(byte[]

input) ist leer und muss von einer abgeleiteten Taskklasse implementiert werden. Eine von Task abgeleitete Klasse, die die Methode byte[] process(byte[] input) implementiert, kann dann als eigenst¨andiger Thread ausgef¨uhrt werden. ¨Uber die von java.lang.Thread geerbte Methodestart()wird der Task gestartet.

Der Task kann ebenso synchron vom aktuellen Thread abgearbeitet werden. Dazu wird statt der Methode start() die Methode processmanually() aufgerufen, die solange blo-ckiert, bis die Methodeprocess(...) des Tasks ausgef¨uhrt wurde. Diese Methode wird be-nutzt, wenn der Task beispielsweise wegen einer fehlenden Kommunikationsverbindung nicht entfernt ausgef¨uhrt werden kann, sondern lokal ausgef¨uhrt werden muss.

Um einen Task auf einer VM auszuf¨uhren, muss dessen Taskklasse bekannt sein. Vor der Ubertragung eines Tasks muss also zun¨¨ achst der Quellcode der Taskklasse an dieVMgesendet werden. Dazu wird die MethodegetSourceCode()in der abgeleiteten Taskklasse ¨ uberschrie-ben. Sie liefert den Quellcode der Taskklasse zur¨uck. Exemplarisch ist nachfolgend unten eine KlasseEchoTask dargestellt, die einen Task implementiert, der eingehende Daten unver¨ an-dert zur¨ucksendet. Dazu wirdEchoTaskvon ferb.pipe.Taskabgeleitet. Zus¨atzlich wird die Methode process(...) ¨uberschrieben. Sie implementiert die Funktionalit¨at des EchoTask und gibt die ¨ubergebenen Daten direkt zur¨uck. Der eigene Quellcode der Klasse ist in der Konstantecodeabgelegt und wird durchgetSourceCode()zur¨uckgeliefert. Das nachfolgende

Codebeispiel zur Benutzung der Taskklasse in der erstellten Middleware stellt dabei schon die vollst¨andige Klasse dar, die so durch die Taskverarbeitung der Middleware ausgef¨uhrt werden kann, was im nachfolgenden Beispiel dargestellt wird. F¨ur komplexere Funktionalit¨at muss die Methode process(...) angepasst werden und der Quellcode erneut in der Konstante code abgelegt werden.

// J a v a Code B e i s p i e l i m p o r t f e r b . p i p e . T a s k ;

p u b l i c c l a s s E c h o T a s k e x t e n d s T a s k {

p r i v a t e s t a t i c f i n a l S t r i n g c o d e = " i m p o r t f e r b . p i p e . T a s k ; "

+ " p u b l i c c l a s s E c h o T a s k e x t e n d s T a s k { "

+ " @ O v e r r i d e p u b l i c b y t e [] p r o c e s s ( b y t e [] i n p u t ) { "

+ " r e t u r n i n p u t ; }}} ";

@ O v e r r i d e

p u b l i c S t r i n g g e t S o u r c e C o d e() { r e t u r n c o d e;

}

@ O v e r r i d e

p u b l i c b y t e [] p r o c e s s( b y t e [] i n p u t) { r e t u r n i n p u t;

} }

Die Klasse TaskClientPipeline wird bereitgestellt, um die entfernte Ausf¨uhrung von Tasks zu erm¨oglichen. Sie implementiert die Methoden connect(String session_key, String server, int port),configureSSL(String truststore, String password)und close(), die der Verwaltung der Verbindung vom Client zur VM dienen. Weitere Metho-den dienen der eigentlichen Taskverarbeitung. Nachdem unter Angabe des Sessionkey uber¨ connect(...)eine Verbindung aufgebaut wurde, k¨onnen die Taskverarbeitungsmethoden bis zum Aufruf vonclose()verwendet werden. Durch Aufruf der Methodeclose()oder durch sonstiges Abbrechen der TCP-Verbindung wird die Abrechnung auf Serverseite beendet und der Sessionkey invalidiert. Der clientseitige Quellcode zur Verwendung der TaskClientPi-peline zur Ausf¨uhrung eines EchoTaskist nachfolgend unten dargestellt:

// J a v a Code B e i s p i e l

f e r b . p i p e . T a s k C l i e n t P i p e l i n e tcp = new f e r b . p i p e . T a s k C l i e n t P i p e l i n e ();

// SSL k o n f i g u r i e r e n

tcp.c o n f i g u r e S S L(" ./ r e s o u r c e s / S S L C l i e n t T r u s t ", " p a s s w o r d ");

// Zur vm v e r b i n d e n , w e i t e r n u r wenn "OK" , s o n s t A bb ru c h

if ( tcp.c o n n e c t(s e s s i o n _ k e y, s e r v e r _ e n d p o i n t, 8 0 8 0 ) .e q u a l s(" OK ")) // Task e r s t e l l e n

E c h o T a s k ct = new E c h o T a s k ();

// Task m i t N u t z d a t e n f ü l l e n ct.s e t I n p u t(c u s t o m _ d a t a);

// Task a b s e n d e n tcp.s t a r t T a s k(ct);

// Auf d a s E r g e b n i s d e r V e r a r b e i t u n g w a r t e n try {

b y t e [] r e s u l t = tcp.g e t N e x t R e s u l t();

} c a t c h (U n k n o w n T a s k F u n c E x c e p t i o n e) {

// F a l l s Task a u f S e r v e r s e i t e u n b e k a n n t w i r d E x c e p t i o n a u s g e l ö s t }

tcp.c l o s e();

Die interne Verarbeitung der Tasks durch die Klasse TaskClientPipeline ist in

Abbil-7.3 Implementierung der Middleware-Komponenten

Abbildung 7.5: Visualisierung der Taskverarbeitung in der KlasseTaskClientPipeline auf dem Client.

dung 7.5 dargestellt. Die Klasse implementiert die Methode String startTask(Task t), die einen Task in die Warteschlange f¨ur ausgehende Tasks einf¨ugt und blockiert, falls die-se Warteschlange bereits voll ist. Die Warteschlange f¨ur ausgehende Tasks hat zwei Pl¨atze.

Die Warteschlange sollte zwar so klein wie m¨oglich sein, jedoch hat sich herausgestellt, dass die Netzwerkschicht bei nur einem Platz manchmal nicht schnell genug mit Daten versorgt werden kann. Nach dem erfolgreichen Einf¨ugen liefert die Methode eine eindeutige Task-ID zur¨uck, die sp¨ater zum Abfragen des Ergebnisses der Taskausf¨uhrung verwendet werden kann.

Verarbeitete Tasks werden von derVM zur¨uck zum Client ¨ubertragen und dort in eine un-beschr¨ankt große Warteschlange eingef¨ugt. Da Ergebnisse eventuell nicht direkt entnommen werden, w¨urde eine beschr¨ankte Gr¨oße hier eventuell verhindern, dass alle fertigen Ergebnisse zwischengespeichert werden k¨onnen. Es ist also Aufgabe der Applikation, die fertigen Ergeb-nisse auch zeitnah zu entnehmen. ErgebErgeb-nisse von Tasksausf¨uhrungen k¨onnen am Client ¨uber die Methodebyte[] getNextResult()abgefragt werden. Diese Methode liefert die Ausgabe des n¨achsten Tasks in der Warteschlange zur¨uck und blockiert nur, falls keine Tasks in der Warteschlange vorhanden sind. Wartet man auf das Ergebnis eines bestimmten Tasks, so kann man die Methodebyte[] getResult(String taskID)aufrufen. Sie kehrt erst zur¨uck, wenn das Ergebnis des Tasks mit der angegebenen ID in der Warteschlange vorliegt. Diese Methode sollte jedoch nur benutzt werden, wenn sichergestellt ist, dass der Task, auf den gewartet wird, nicht vorher schon aus der Warteschlange abgeholt wurde. Sollte dies nicht sichergestellt sein, so kann die Methode dauerhaft blockieren, wenn der Task, auf den ge-wartet wird, schon entnommen wurde. Um diese Situation zu vermeiden, kann man durch Aufruf der MethodeString getLastResultID()die ID des zuletzt entnommenen Tasks ab-fragen. Durch das asynchrone Ausf¨uhren von Tasks ist es nicht direkt m¨oglich zu erkennen, ob ein Task, derstartTask(...)ubergeben wurde, auch ordnungsgem¨¨ aß ausgef¨uhrt werden konnte. Im Fehlerfalle werfen erst die MethodengetNextResult() und getResult(String

k

Sessionkey

Taskreader Taskwriter

SessionReader

Task Classloader

SessionWriter Eingehender

Puffer

Ausgehender Puffer TaskTask

Task Dispatcher

Install Task Install Task Verfügbare

Tasks

CPU1 CPU2

Netzwerk

Kommunikation mit Broker Kommunikation mit Mobile Client

zum Mobile Client zum Broker

Compute Server (VM)

Abbildung 7.6: Visualisierung der Taskverarbeitung auf der VM durch die Klasse TaskServerPipeline.

taskID) eine Exception vom TypUnknownTaskFuncException. Anhand der ID kann jedoch festgestellt werden, welcher Task den Fehler verursacht hat.

Task-Server-API

Der Task-Server auf der VM wird von der Klasse TaskServerPipeline implementiert. Sie akzeptiert eingehende Verbindungen ¨uber die externe API von jeweils nur einem Client gleich-zeitig. Dazu wird nur ein einziger TCP-Port ben¨otigt, der dann in der Firewall des IaaS-Anbieters f¨ur eingehende Verbindungen aus dem Internet ge¨offnet werden muss. Zus¨atzlich akzeptiert die Klasse TaskServerPipeline auf derVM ¨uber die interne API Verbindungen vomBroker. DerBrokerteilt derVMw¨ahrend des Allokationsvorgangs eines Clients den zu erwartendenSessionkey mit. Baut der Client dann eine Verbindung zurVM auf, so wird der ¨ubertragene Sessionkey des Client mit dem zuvor vom Broker erhaltenen Sessionkey verglichen. Abbildung 7.6 visualisiert die Interna der Klasse TaskServerPipeline.

Die Klasse verwaltet 2 Warteschlangen (java.util.concurrent.BlockingQueue<Task>), eine f¨ur eingehende und eine f¨ur ausgehende Tasks. Deren Gr¨oße entspricht der Anzahl ver-f¨ugbarer Prozessoren auf derVM. Dies entspricht der Forderung, dass stets nicht mehr Tasks transferiert werden, als auch verarbeitet werden k¨onnen. Sind die Tasklaufzeiten lang genug, dann ist stets ein Platz f¨ur das Ergebnis und f¨ur einen neuen Task zur Verarbeitung in den Warteschlangen bereit. Tasks werden, da sie von der Klasse java.lang.Thread abgeleitet sind, intern von einem Threadpool abgearbeitet, dessen Gr¨oße der Anzahl verf¨ugbarer logi-scher Prozessoren des Systems entspricht. Die Verwendung eines Threadpools ist sinnvoll bei der Verarbeitung vieler Tasks kurzer Laufzeit, da zur Ausf¨uhrung in einem Threadpool be-stehende Threads wiederverwendet werden und die Threaderzeugung nicht f¨ur jeden Task als Overhead anf¨allt. Java bietet dazu das Interface java.util.concurrent.ExecutorService

7.3 Implementierung der Middleware-Komponenten

an, welches von verschiedenen Threadpool-Varianten implementiert wird. Zun¨achst wird f¨ur eingehende Tasks eine Taskklasse des ben¨otigten Typs aus einem Archiv verf¨ugbarer Taskklas-sen instanziiert. Es k¨onnen sowohl von der Middleware bereitgestellte Taskklassen als auch zur Laufzeit installierte Taskklassen geladen werden. Sollte das Laden fehlschlagen, weil keine passende Class-Datei gefunden werden konnte, so wird einErrorTaskin die ausgehende War-teschlange eingef¨ugt, der den Fehler dem Client r¨uckmeldet. War die Instanziierung hingegen erfolgreich, so wird der Task zur Bearbeitung an den Threadpool ¨ubergeben. Das Ergebnis der Ausf¨uhrung wird danach direkt in die Warteschlange f¨ur ausgehende Tasks eingef¨ugt.

Nach Beendigung einer Verbindung sendet die KlasseTaskServerPipelineselbstst¨andig ei-ne Notiz an denBroker, dass die Session beendet ist und dieVMf¨ur neue Client-Anfragen bereitsteht. Zudem werden die gesammelten Logdaten versendet und das Verzeichnis f¨ur be-nutzerdefinierte Class-Dateien geleert. Somit kann der n¨achste Benutzer nicht versehentlich Tasks des Vorg¨angers instanziieren.

Die KlasseTaskServerPipelineenth¨alt zus¨atzlich Methoden, um den Taskserver zu star-ten und zu konfigurieren. Ein Minimalbeispiel zur Verwendung ist nachfolgend dargestellt.

// J a v a Code B e i s p i e l

f e r b . p i p e . T a s k S e r v e r P i p e l i n e tsp = new f e r b . p i p e . T a s k S e r v e r P i p e l i n e ( 8 0 8 0 ) ; tsp.c o n f i g u r e S S L(" ./ r e s o u r c e s / S S L S e r v e r S t o r e "," p a s s w o r d ");

tsp.s t a r t();

Der Konstruktor verlangt als Parameter den zu verwendenden externen TCP-Port. Die Me-thode configuteSSL(...) konfiguriert die Zertifikatdatei f¨ur die SSL-Verschl¨usselung und das zum ¨Offnen notwendige Passwort. Gestartet wird der Server durch Aufruf der blockie-renden Methode start(). Die Methode stop() f¨ahrt den Server herunter und f¨uhrt dazu, dassstart() zur¨uckkehrt.