Abstrakte Verhaltensbeschreibung von CCM
Transcription
Abstrakte Verhaltensbeschreibung von CCM
Abstrakte Verhaltensbeschreibung von CCM Softwarekomponenten Diplomarbeit von Peter Süttner 3144238 geboren am 11. März 1984 in Bamberg 31. März 2011 Betreuer: Dipl. - Inf. Sebastian Cech, Dipl. - Inf. Sebastian Götz Verantwortlicher Hochschullehrer: Prof. Dr. rer. nat. habil. Uwe Aßmann Institut / Lehrstuhl: Software und Multimediatechnik / Softwaretechnologie Inhaltsverzeichnis 1 Einleitung 7 2 Das CoolSoftware Projekt 2.1 Motivation . . . . . . . . . . . . . . . 2.2 Projektskizze . . . . . . . . . . . . . 2.2.1 Komponentenmodell CCM . . 2.2.2 Vertragssprache ECL . . . . . 2.2.3 Laufzeitumgebung THEATRE 2.3 Aufgabenstellung und Einordnung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3 Verwandte Arbeiten und Literatur 3.1 Verhaltensbeschreibungen . . . . . . . . . . . . 3.1.1 Theoretische Hintergründe . . . . . . . . 3.1.2 Verhalten: Beschreibungsformen und ihre 3.1.3 Diskussion im Kontext von CoolSoftware 3.2 Software und Energieverbrauch . . . . . . . . . 3.2.1 Einführung . . . . . . . . . . . . . . . . 3.2.2 State of the Art . . . . . . . . . . . . . . 3.2.3 Zusammenfassung . . . . . . . . . . . . . 4 Java und die Ressourcennutzung 4.1 Beschreibung der Systemumgebung . . . 4.1.1 Übersicht . . . . . . . . . . . . . 4.1.2 Die Hardware . . . . . . . . . . . 4.1.3 Das Betriebssystem . . . . . . . . 4.1.4 Die Java Virtual Machine (JVM) 4.2 Möglichkeiten zur Datenerhebung . . . . 4.2.1 Instrumentierung . . . . . . . . . 4.2.2 Sampling . . . . . . . . . . . . . 4.2.3 Profiling . . . . . . . . . . . . . . 4.2.4 Benchmarking . . . . . . . . . . . 3 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Gewinnung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13 13 14 15 18 21 24 . . . . . . . . 27 27 28 31 34 37 37 39 43 . . . . . . . . . . 45 46 46 48 52 58 63 64 66 67 70 5 JRUPI als konzeptuelles Framework 5.1 Ergebnisdefinition und Konsequenzen . . . . . . 5.1.1 Die Komponente als Blackbox . . . . . . 5.1.2 Erwartetes Ergebnis und Abhängigkeiten 5.2 Kontext und Entwurfskriterien . . . . . . . . . . 5.2.1 Kontext der Betrachtungen . . . . . . . 5.2.2 Formulierung der Entwurfskriterien . . . 5.3 Das JRUPI Architekturkonzept . . . . . . . . . 5.4 Der JRUPI Workflow . . . . . . . . . . . . . . . 5.5 Zusammenfassung umzusetzender Komponenten . . . . . . . . . 6 Proof-of-Concept Implementierung 6.1 Instanziierung der Komponenten . . . . . . . . . 6.1.1 Profiler: SystemTap . . . . . . . . . . . . . 6.1.2 Instrumentierung: BTrace . . . . . . . . . 6.1.3 Datenaustauschformat: JSON . . . . . . . 6.1.4 Intelligente Datenanalyse: Eureqa . . . . . 6.2 Technische Umsetzung . . . . . . . . . . . . . . . 6.2.1 Brücke zwischen Codeartefakt und Profiler 6.2.2 Umsetzung der Profiler . . . . . . . . . . . 6.2.3 Umsetzung der JRUPI-Workbench . . . . 7 Evaluation 7.1 Fallbeispiel: Sortieralgorithmen . . . . . 7.1.1 Kurzvorstellung der Algorithmen 7.1.2 Setup der Testumgebung . . . . . 7.1.3 Auswertung der Ergebnisse . . . . 7.2 Flexibilität der Datenerhebung . . . . . 7.3 Kritische Bewertung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73 74 74 76 79 79 82 83 85 88 . . . . . . . . . . . . . . . . . . 91 92 92 97 100 102 104 105 108 112 . . . . . . 119 . 120 . 121 . 122 . 125 . 133 . 137 . . . . . . . . . 8 Zusammenfassung und Ausblick 139 8.1 Zusammenfassung . . . . . . . . . . . . . . . . . . . . . . . . . 139 8.2 Ausblick . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 142 Literaturverzeichnis 146 Abbildungsverzeichnis 154 A CoolSoftware A.1 CCM-Metamodell . . . . . . . . . . . . . . . . . . . . . . . . A.2 Beispiel . . . . . . . . . . . . . . . . . . . . . . . . . . . . . A.3 ECL für Beispiel . . . . . . . . . . . . . . . . . . . . . . . . 157 . 157 . 159 . 160 B Quellcode 163 B.1 Zentrale Bestandteile der Profiler . . . . . . . . . . . . . . . . 163 B.2 BTrace-Skript für Sortieralgorithmen . . . . . . . . . . . . . . 165 Kapitel 1 Einleitung „We cannot rely on technological breakthroughs to be the silver bullet. We need to be smarter in how we use what we already have available.“ [Gar07] Energieeffizienz ist eines der Schlagworte, das in der Debatte um GreenIT immer wieder auftaucht. Kritische Stimmen behaupten, dass dadurch bei gleichem Energieverbrauch lediglich mehr Rechenleistung oder mehr Speicherplatz erzielt wird. Effizienzsteigerung kann aber auch, im Sinne des einleitenden Zitats, als optimierte Verwendung der vorhandenen Ressourcen verstanden werden. In diesem Fall wird auch tatsächlich Energie eingespart. Die Einsparung von Energie ist ein Thema, das zunehmend alle Bereiche der Informations- und Kommunikationstechnik (IKT) betrifft. Das hat zum Einen mit dem immensen Energiehunger bereits bestehender Infrastrukturen zu tun, zum Anderen aber auch, und das ist wohl noch bedenklicher, mit dem enormen Wachstum der Branche auf globaler Ebene [HJMM09]. So unterschiedlich die Motivationen auch sein mögen, ob ökologisch, ökonomisch, durch politischen Druck, zur Imageverbesserung oder schlichtweg um eine Verlängerung der Batterielaufzeit bei mobilen Geräten zu erreichen, Eines ist klar: Die Steigerung der Energieeffizienz ist ein erstrebenswertes Ziel. Wie durch das einleitende Zitat verdeutlicht wird, wäre es naiv, sich ausschließlich auf Verbesserungen von Seite der Hardwarekomponenten zu verlassen. Die vorrangig eingesetzte Halbleitertechnik unterliegt natürlichen Grenzen bezüglich des Einsparungspotenzials, das durch fundamentale Verbesserungen der Technologie erreicht werden kann [Gar07]. Um ein Maximum an Energieeffizienz zu ermöglichen ist eine gesamtheitliche Betrachtung notwendig, die sowohl Hardware als auch Software einschließt. Nur durch die Analyse des Zusammenspiels von Hard- und Software lässt sich eine effektive Energieverwaltung zur Erreichung maximaler Energieeffizienz umsetzen. 7 8 KAPITEL 1. EINLEITUNG Eine Frage der Auslastung Die Verwendung von Virtualisierungstechniken in Rechenzentren, um eine balancierte Lastverteilung zu erreichen, ist eine Möglichkeit. Durch Virtualisierung können mehrere Server auf einen Einzelnen konsolidiert werden, der durchschnittlich eine höhere Auslastung besitzt. Damit wird das Problem angegangen, dass viele Server, auch wenn sie quasi keine Arbeit verrichten, eine beachtliche Menge Energie verbrauchen. Daraus folgt direkt eine weitere Möglichkeit: anstatt die vorhandene Hardware möglichst gleichmäßig und hoch auszulasten, was nur innerhalb der Grenzen des Rechenzentrums möglich ist, erscheint die Forderung nach mehr Proportionalität zwischen Ressourcenauslastung und Energieverbrauch sinnvoll. Ist keine Proportionalität gegeben, so ergibt sich die höchste Energieeffizienz bei einer Auslastung von über 80%. Ein Wert, der weit über der typischen Betriebsregion (ca. 10-50% bei Servern ohne Virtualisierung) liegt [BH07]. Das Ziel der Proportionalität erfordert eine gesamtheitliche Herangehensweise, die sowohl von Hardware, als auch von Software getragen wird. Eine Vorreiterrolle spielen in diesem Zusammenhang die Kernkomponenten eines jeden Rechnersystems: die Prozessoren. Getreu dem Sprichwort „Not macht erfinderisch“, stammen die Innovationen meist aus dem Bereich der mobilen Endgeräte, mit ihrer chronischen Energieknappheit aufgrund begrenzter Batteriekapazität, finden aber seid geraumer Zeit auch Einzug in Systeme mit konstanter Stromversorgung. Moderne Prozessoren beherrschen Techniken zur dynamischen Anpassung von Taktrate und Versorgungsspannung (Dynamic Voltage and Frequency Scaling (DVFS) [TWM+ 08]), um Energie einzusparen. Zur effektiven Ausnutzung dieser Mechanismen ist eine softwareseitige Verwaltung nötig, die eine Anpassung entsprechend der Auslastung realisiert (z.B. [PS06] unter Linux). Dem Vorbild der Prozessoren folgend, können derartige Anpassungen auch für andere Hardwarekomponenten umgesetzt werden [HPIM+ 10]. Die Energieverwaltung, die sich um die dynamischen Anpassungen der Hardwarekomponenten kümmert, ist typischerweise Aufgabe des Betriebssystems, da es die Kontrolle über alle Geräte besitzt [Tan07]. Eine effektive Verwaltung der heterogenen Ressourcen, die zudem über immer mehr Betriebsmodi verfügen, könnte durch die Aufteilung in mehrere, eventuell hierarchische organisierte, Ressourcenmanager erfolgen [Sax10]. Dies ist um so sinnvoller, je größer die Systeme werden, da mit der Anzahl der zu verwaltenden Geräte die möglichen Energie- und Performancezustände um ein Vielfaches steigen. Um den Ressourcenmanagern eine proaktive Energieverwaltung zu ermöglichen, benötigen sie Hinweise, welche Hardwareressourcen in welchen Umfang von Software genutzt werden. 9 Ressourcennutzung als Indikator Im Rahmen praktischer Untersuchungen, wie beispielsweise in [Fli01], konnte nachgewiesen werden, dass der Umfang der Ressourcennutzung direkten Einfluss auf den Energieverbrauch hat und die Verwendung von Hardwarefeatures wie DVFS den Energiebedarf beachtlich senkt. Eine effektive Energieverwaltung ermöglicht jedoch eine weit höhere Einsparung, vor allem in Anbetracht immer besserer Unterstützung seitens der Hardware. Dazu ist es essentiell, den Ressourcenmanagern möglichst viele Informationen über das Verhalten der Anwendungssoftware bezüglich der Ressourcennutzung zukommen zu lassen, um eine proaktive Energieverwaltung zu erleichtern. Da die Ressourcennutzung offensichtlich sowohl direkten als auch indirekten Einfluss ausübt, zieht sich folgende These wie ein Leitmotiv durch die gesamte Arbeit: Der Energieverbrauch eines Systems ist, in nicht vernachlässigbaren Maße davon abhängig, ob und in welchem Umfang Hardwarekomponenten von Software benutzt werden. Um den Ressourcenmanagern die benötigten Hinweise zur Ressourcennutzung von Software bereitstellen zu können, muss die Ressourcennutzung zunächst ermittelbar sein und anschließend in Form einer passenden Beschreibung zur Verfügung gestellt werden. Dabei sollte die Beschreibung so abstrakt und kompakt wie möglich sein. Das Ziel der Arbeit besteht darin, dem Entwickler geeignete Methoden und Verfahren an die Hand zu geben, um den Ressourcenverbrauch einzelner Codeartefakten ermitteln und ausdrücken zu können. Im Speziellen wird eine geeignete Methode zur Erhebung der Ressourcennutzungsdaten von Java-Codeartefakten, die von der JVM (Java Virtual Maching) ausgeführt werden, entwickelt. Mit der JVM muss eine weitere Abstraktionsebene überwunden werden, da sie essentielle Bestandteile wie die Allokation und die Freigabe von Ressourcen fast gänzlich übernimmt. Dadurch wird zwar die Softwareentwicklung vereinfacht, in der Konsequenz gestaltet sich aber eine detaillierte Datenerhebung über die Ressourcennutzung schwierig und vorhandene Werkzeuge unterliegen etlichen Beschränkungen. So wie das Betriebssystem von der Komplexität der Hardwarekomponenten abstrahiert, so abstrahiert die JVM weitgehend von der Komplexität des Betriebssystems. Die Überbrückung dieser Abstraktionsebenen ist eine der großen Herausforderung dieser Arbeit. Ein weiteres Problem besteht darin, dass Ressourcennutzungsdaten gewöhnlich plattformspezifisch sind. Daher reicht es im Allgemeinen nicht aus, die Daten einmal am Entwicklersystem zu erheben und daraus eine Beschreibung abzuleiten. Aus diesem Grund ist es ein weiteres Ziel, dass die Lösung auch am Zielsystem, und damit eventuell an einem Produktivsystem, anwendbar ist. 10 KAPITEL 1. EINLEITUNG Kurzbeschreibung der Lösung Zum Profiling (dem verwendeten Verfahren zur Datenerhebung) der Ressourcennutzung von Java-Anwendungen definiert diese Arbeit zunächst ein konzeptuelles Framework, das Jrupi (Java Resource Usage Profiling Infrastructure) getauft wurde. Es umfasst zum Einen ein Architekturkonzept, das zunächst plattformunabhängig beschreibt, wie das Profiling der Ressourcennutzung einer Java-Anwendung umgesetzt werden kann. Zum Anderen gibt es dem Modellentwickler einen Workflow vor, wie eine Umsetzung von Jrupi zu benutzen ist. Schließlich wird der Ansatz in Form einer Proof-of-Concept Implementierung validiert. Eine zentrale Rolle für die entwickelte Lösung spielen Verfahren zur dynamischen Ablaufverfolgung, die auch als dynamisches Tracing [Sun08] bezeichnet werden. Mit ihrer Hilfe lässt sich Tracing-Code an nahezu beliebigen Stellen, sogenannten Prüfpunkten, im Betriebssystem oder in Anwendungen einbringen (Instrumentierung). Dabei geht ihre Funktionalität weit über das Einbringen einzelner Tracing-Statements hinaus. Sie sind in der Lage, Kontextinformationen während der Laufzeit an den instrumentierten Stellen aufzulösen und diese zu analysieren, zu filtern, zu aggregieren oder zusammenzufassen. Für die Programmierung der Prüfpunkte stehen mächtige Skriptsprachen bereit, um die Daten in der genannten Art und Weise zu verarbeiten und Ausgaben zu erzeugen. Alles dynamisch während der Laufzeit - ohne Rekompilieren oder Neustarten. Verfahren zur Dynamischen Ablaufverfolgung sind von Natur aus sehr betriebssystemspezifisch, wobei der bekannteste Verteter DTrace [Sun08] für die Solaris Plattform (mit Ports nach BSD,OSX) ist. Für Linux wurde, aus Lizenzgründen, eine eigene (von DTrace inspirerte) Lösung namens Systemtap [BPBS10] entwickelt, das in den Repositories fast aller größeren Distributionen Einzug erhalten hat. Aufgrund der weiteren Verbreitung von Linux wurde für die Proof-of-Concept Implementierung Systemtap gewählt. Nun lassen sich mit SystemTap Profiler zur Datenerhebung auf Betriebssystemebene implementieren. Was noch fehlt ist eine genaue Zuordnung der Ressourcennutzung zu den Java-Codeartefakten, die gerade von der JVM ausgeführt werden. Diese Zuordnung wird durch eine Instrumentierung der entsprechenden Stellen in der Java-Anwendung erreicht. Die Proof-ofConcept Implementierung greift zu diesem Zweck auf Verfahren zur dynamischen Bytecode-Instrumentierung zurück, die den benötigten Code direkt in die laufende Anwendung an zuvor festgelegte Stellen injizieren. Auch sie lassen sich zur Laufzeit an die ausführende JVM binden. Die aufgezeichneten Daten werden nach der Ausführung im Rahmen einer Offline-Analyse weiter verarbeitet (veredelt) und analysiert. Anschlie- 11 ßend können die auf diese Weise aufbereiteten Daten als Eingabe für weitere Werkzeuge dienen. So können beispielsweise Verfahren der intelligenten Datenanalyse [Pae06] verwendet werden, um in den Daten nach mathematischen Zusammenhängen - eine abstrakte Beschreibung - zu suchen. In Abbildung 1.1 ist der gesamte Vorgang schematisch dargestellt. 0: iconst_2 1: istore_1 2: iload_1 3: sipush 1000 6: if_icmpge 44 9: iconst_2 10: istore_2 11: iload_2 12: iload_1 13: if _icmpge 31 16: iload_1 17: iload_2 18: irem 19: ifne 25 22: goto 38 25: iinc 2, 1 28: goto 11 dyn. dyn. BytecodeBytecodeInstrumentation Instrumentation Bytecode JVM Betriebssystem Analyse Analyse (Offline) (Offline) Profiler Profiler Abbildung 1.1: Schematische Darstellung der Lösung Während der Ausführung des Bytecodes (Zwischenrepräsentation) durch die JVM instrumentieren die Profiler das Betriebssystem, um die Daten über Ressourcennutzung zu sammeln, während gleichzeitig die aktuelle Stelle (in der Ausführung) durch dynamische Bytecode-Manipulation „markiert“ wird. Anschließend werden die Daten im Rahmen einer Offline-Analyse weiterverarbeitet. Aufbau der Arbeit In Kapitel 2 wird zunächst das CoolSoftware Projekt vorgestellt, in dessen Rahmen die vorliegende Arbeit entstanden ist. Das erklärte Ziel von CoolSoftware ist die Optimierung der Energieeffizienz von IT-Infrastrukturen. Zu diesem Zweck definiert es eine energiebewusste, komponentenbasierte Softwarearchitektur sowie eine Laufzeitumgebung zur dynamischen Optimierung. Dazu wird für die konkreten Implementierungen von Softwarekomponenten eine abstrakte Verhaltensbeschreibung bezüglich der Ressourcennutzung benötigt, wie sie bereits beschrieben wurde. Daraufhin stellt Kapitel 3 eine Auswahl an Literatur und verwandter Arbeiten vor, die in Bezug zur gegebenen Aufgabenstellung stehen. Einerseits geht das Kapitel allgemein auf Möglichkeiten zur Verhaltensbeschreibung von Software ein und beschreibt Verfahren zur Gewinnung von Verhaltensmodel- 12 KAPITEL 1. EINLEITUNG len. Andererseits widmet es sich dem Zusammenhang zwischen Software und Energieverbrauch. Da die Datenerhebung über die Ressourcennutzung einzelner Codeartefakte in Java eine relativ komplizierte Angelegenheit ist, werden in Kapitel 4 zunächst einige Grundlagen erläutert. Das umfasst zum Einen die betrachteten Teile der Systemumgebung (wie JVM und Betriebssystem), deren wichtigsten Konzepte erklärt werden, soweit sie zum Verständnis nötig sind. Zum Anderen werden grundlegende Methoden zur Datenerhebung vorgestellt, die bei der späteren Implementierung verwendet werden. Nachdem die notwendigen Grundlagen erläutert wurden, wird in Kapitel 5 mit Jrupi ein konzeptuelles Framework zur Lösung der Aufgabe vorgestellt. Erstens gibt es den Aufbau einer Infrastruktur vor, um detaillierte Daten über die Ressourcennutzung von Java-Codeartefakten zu erheben und anschließend weiter zu verarbeiten. Zweitens enthält es eine Vorgehensbeschreibung (Workflow) für den Modellentwickler, die ihm Handlungsanweisungen zur Benutzung der Infrastruktur vorgibt, um am Ende zu einer abstrakten Beschreibung der Ressourcennutzung von Codeartefakten zu kommen. Anschließend wird das konzeptuelle Frameweork Jrupi in Kapitel 6 mit einer konkreten Umsetzung in Form einer Proof-of-Concept Implementierung instanziiert. Sie zeigt, wie sich unterschiedliche Verfahren und Werkzeugen kombinieren lassen, um eine effiziente und flexible Datenerhebung zu ermöglichen, und wie die Daten anschließend analysiert werden können, um ein abstraktes Modell daraus abzuleiten. In Kapitel 7 wird Proof-of-Concept Implementierung, und damit Jrupi, anhand von Beispielen evaluiert. Hier treten diverse Sortieralgorithmen in Bezug auf CPU-Nutzungsverhalten gegeneinander an und es wird gezeigt, wie die Proof-of-Concept Implementierung spezielle Methode im laufenden Eclipse bezüglich Netzwerknutzung profiliert. Den Abschluss der Arbeit bildet Kapitel 8. Es fasst die wichtigsten Aspekte der Lösung zusammen und gibt einen Ausblick, welche zukünftigen Arbeiten als sinnvoll erachtet werden. Kapitel 2 Das CoolSoftware Projekt Die vorliegende Arbeit ist innerhalb des CoolSoftware Projektes1 angesiedelt, welches Teil des CoolSilicon Spitzencluster2 ist, und durch das Bundesministerium für Bildung und Forschung (BMBF) finanziert wird. Daher skizziert dieses Kapitel die wichtigsten Aspekte des Forschungsprojektes aus Sicht dieser Arbeit. Einerseits muss sich der entwickelte Ansatz in den vorgegebenen Projektrahmen einfügen, andererseits aber auch von anderen Projektteilen abgrenzen. Darüber hinaus sind Zielstellung sowie die bisherige Konzeption und Umsetzung von CoolSoftware von entscheidender Bedeutung für die eigenen Entwicklungsentscheidungen. Alle Betrachtungen in den Folgekapiteln sind vor dem Hintergrund der im CoolSoftware-Projekt definierten Zielen zu sehen. Das Kapitel gliedert sich in folgende Teile: Abschnitt 2.1 resümiert allgemein über die Motivation für das Projekt. Daraufhin wird im Abschnitt 2.2 skizzenhaft das CoolSoftware-Projekt selbst vorgestellt. Die Unterabschnitte geben einen kurzen Abriss der Projektteile wieder, die von besonderer Relevanz für die späteren Erläuterungen sind. Den Abschluss bildet Abschnitt 2.3 mit der Einordnung der Aufgabenstellung. 2.1 Motivation GreenIT - dieses Schlagwort kursiert seit mehreren Jahren innerhalb der Branche. Aus „The Big Blue“ wird „The Big Green“, weil 3900 Server auf 30 Linux-Großrechner konsolidiert wurden3 . In der Tat wird dadurch ein erhebliches Einsparpotenzial beim Energieverbrauch von Rechenzentren verdeut1 www.cool-software.org www.cool-silicon.de 3 www.ibm.com 2 13 14 KAPITEL 2. DAS COOLSOFTWARE PROJEKT licht. Aber Steigerung der Energieeffizienz betrifft nicht nur dieses Gebiet. In [Tan07] wird das Beispiel von Desktop-PCs aufgeführt. Unter der Annahme, dass jedes Gerät mit einem 200-Watt-Netzteil (wobei der durchschnittliche Wirkungsgrad 85% beträgt - 15% der zugeführten Energie werden also in Wärme umgewandelt) ausgestattet ist, würde der zeitgleiche Betrieb von 100 Millionen derartiger PCs einen Verbrauch 20.000 Megawatt entsprechen. Diese Zahl entspricht etwa dem Gesamtausstoß von 20 mittelgroßen Kernkraftwerken. Durch eine Halbierung des Energieverbrauchs bei den Geräten könnten dementsprechend 10 Kraftwerke eingespart werden. Laut [HJMM09] betrug 2007 der weltweite Ausstoß an Treibhausgas, gemessen in CO2-äquivalentem Gas, etwa 45.000 Millionen Tonnen. Mit 820.000 Tonnen entfielen davon gerade einmal 2% auf die IT-Branche. So klein dieser Wert auch erscheint, entsprach er dennoch der Menge an Gas, der durch den weltweiten Flugverkehr verursacht wurde. Außerdem wird auf das Problem hingewiesen, dass sich dieser Wert, trotz steigender Energieeffizienz, bis 2020 auf 3% vergrößern wird. Die Ursache liegt in der wachsenden Verbreitung von IT-Technologien in den Schwellenländern, die selbst durch eine beständig steigende Leistungsfähigkeit nicht kompensiert werden könne. Daher ist eine möglichst große Energieeinsparung aus ökologischer Sicht sehr erstrebenswert. Ein weiterer Gesichtspunkt sind ökonomische Aspekte. In [HJMM09] wird alleine innerhalb der EU ein Verbrauch von 40 Terrawattstunden, und dadurch Kosten in Höhe von 6 Milliarden Euro, für den Betrieb von Servern und Infrastruktur angeführt. Eine Steigerung der Energieeffizienz führt also zu einem wirtschaftlichen Nutzen im Sinne einer Kostenreduktion, die sich positiv auf die Gewinn- und Verlustrechnung auswirkt. Einen Imagegewinn bekommen Unternehmen quasi gratis dazu. Das CoolSoftware-Projekt verfolgt den Ansatz, IT-Infrastrukturen in ihrer Gesamtheit hinsichtlich Energie zu betrachten. Dafür wird im Speziellen untersucht, welchen Einfluss Software auf den Energieverbrauch hat. Schließlich ist es Software, die einerseits durch Inanspruchnahme Hardware auslastet, und andererseits die einzelnen Komponenten steuert. Grund genug, das Energieverhalten als nicht-funktionale Eigenschaft in den Softwareentwicklungsprozess einzubeziehen. 2.2 Projektskizze Das übergeordneten Ziel des CoolSoftware-Projektes besteht in der Optimierung der Energieeffizienz von IT-Infrastrukturen. Während sich die Bemühungen in der Vergangenheit meist auf einzelne Hardwarekomponenten 2.2. PROJEKTSKIZZE 15 konzentrierten, rückt nunmehr die Optimierung von und mit Software in den Vordergrund. Dabei müssen sowohl Hard- als auch Softwarekomponenten in die Betrachtungen einbezogen werden. Das Bestreben richtet sich auf die Schaffung energieadaptiver, selbst-rekonfiguriernder Anwendungssysteme. Dafür wurde der Ansatz des Energy Auto Tuning (EAT) [GWS+ 10a] entwickelt. Ein EAT-Softwaresystem ist durch zwei wesentliche Eigenschaften gekennzeichnet: • Eine energiebewusste, komponentenbasierte Softwarearchitektur, welche durch das Cool Component Model (CCM) in Kombination mit der Energy Contract Language (ECL) definiert ist. • Eine Laufzeitumgebung, genannt THree Layer Auto Tuning Runtime Environment (THEATRE). CCM, ECL und THEATRE sind die drei Kernbestandteile des Projektes und beschreiben zusammen die CoolSoftware-Architektur [GCW+ 10]. Daher werden sie in den folgenden Unterabschnitten vorgestellt. 2.2.1 Komponentenmodell CCM Um eine automatische Rekonfiguration in einen energieoptimalen Zustand zu ermöglichen, wird für ein EAT-System eine spezielle Architekturbeschreibung benötigt, die zusätzliche Eigenschaften hinsichtlich der spezifischen Anforderungen der Dimension Energie erfüllt. So reicht es nicht aus, lediglich den strukturellen Aufbau der Software zu erfassen. Vielmehr ist es notwendig, die komplette Infrastruktur mit einzubeziehen. Das bedeutet, dass eine explizite Verbindung zwischen den Bausteinen der Softwarearchitektur und der Hardwareressourcen modellierbar ist. Zu diesem Zweck wurde das CCM als ein zentraler Bestandteil der CoolSoftware-Architektur entwickelt, welches im Detail in [CSG+ 10] spezifiziert ist. Innerhalb des CCM stellt der Begriff der Komponente [Szy02] das Schlüsselkonzept dar. Aufbauend auf den zusätzlichen Voraussetzungen für EAT wurden die Anforderungen an das Komponentenmodell wie folgt formuliert: • Modellierung von Hard- und Software: Das CCM muss in der Lage sein, sowohl Hard- als auch Software zu modellieren, um eine Betrachtung des Gesamtsystems zu erlauben. Dazu verwendet es in beiden Fällen zur Beschreibung die zentralen Abstraktion der Komponente. • Hierarchische Modellierung: Um die komplexen Strukturen von Softwarekomponenten und der darunter liegenden Hardwareressourcen 16 KAPITEL 2. DAS COOLSOFTWARE PROJEKT Varianten Struktur CCM Verhalten Abbildung 2.1: Zentrale Pakete im CCM adäquat erfassen zu können, wird eine hierarchische Modellierung unterstützt. Einerseits wird dadurch die Wiederverwendbarkeit von Ressourcen und Softwarekomponenten gefördert. Andererseits führt ein hierarchischer Aufbau zu einer reduzierten Komplexität, was der späteren Analyse zur Laufzeit zu Gute kommt. • Quality-of-Service(QoS)-Eigenschaften: Als Basis für eine Optimierung zur Laufzeit müssen QoS-Parameter als nicht-funktionale Eigenschaft für Komponenten explizit definierbar sein. • Variantenmodellierung: Jede Komponente innerhalb des CCM kann in verschiedenen Implementierungsvarianten vorliegen. Der Unterschied liegt beispielsweise in den QoS-Eigenschaften der angebotenen Dienste. Daher weisen verschiedene Varianten einer Komponente für gewöhnlich auch eine unterschiedliche Energiecharakteristik auf. • Verhaltensmodellierung: Alle Komponenten müssen bezüglich ihres Verhaltens beschreibbar sein. So besitzen Hardwarekomponenten typischerweise verschiedene Zustände (vergleiche Unterabschnitt 4.1.2), in denen sie unterschiedlich viel Energie verbrauchen. Bei Softwarekomponenten hingegen dient das Verhaltensmodell der Beschreibung ob, und in welchem Umfang, spezifische Ressourcen in Anspruch genommen werden. Das CCM selbst ist durch ein geschichtetes Metamodell definiert. Darin enthalten sind drei Pakete, die die Grundpfeiler des Modells bilden: Struktur(structure package), Verhaltens- (behavior package) und Variantenpaket (variant package), wie in Abbildung 2.1 dargestellt. Im Folgenden werden die Kernkonzepte der drei Pakete vorgestellt. Alle Worte, die in Schreibmaschinenschrift gedruckt sind, beziehen sich dabei 2.2. PROJEKTSKIZZE 17 auf Elementnamen des Metamodells. Die Metamodelle sind in reduzierter Form in Anhang A.1 dargestellt. Das Strukturpaket Im Strukturpaket sind alle Elemente definiert, die für die strukturelle Modellierung hierarchischer IT-Infrastrukturen benötigt werden. Ein wichtiger Aspekt ist die Vorgabe eines Typsystems. Die zentrale Abstraktion ist der Komponententyp (ComponentType), für den drei weitere Spezialisierungen definiert sind: Benutzertyp (UserType), Softwarekomponententyp (SWComponentType) und Ressourcentyp (ResourceType). Dabei sind Softwarekomponententypen und Ressourcentypen in der Lage, Hierarchien zu bilden. Zur Modellierung von Interaktionen zwischen einzelnen Komponenten dienen Porttyp (PortType) und Portverbindungstyp (PortConnectorType). Die Menge der Ports einer Komponente definiert ihre Schnittstelle nach außen, während ein Verbindungstyp jeweils zwei passende Porttypen miteinander verbindet. Zu unterscheiden sind hier angebotene (OUT), benötigte (IN) sowie hybride (INOUT) Porttypen. Auf diese Weise lassen sich mögliche Verbindungen zwischen Komponenten definieren. Der Vertrag (Contract) einer Komponente legt fest, welche Dienste von ihr angeboten beziehungsweise benötigt werden. Mit Hilfe des Strukturpaketes lassen sich also Komponenten und ihre Schnittstellen hierarchisch beschreiben, Verbindungen dazwischen definieren, und Verträge schließen, die erfüllt werden müssen. Das Verhaltenspaket Um eine Entkopplung des Verhaltens einer Komponente von ihrer Struktur zu erreichen, sind die Elemente zur Verhaltensbeschreibung in einem separaten Paket definiert. Ein wichtiges Konzept stellt in diesem Zusammenhang die Verhaltensschablone (BehaviorTemplate) dar. Eine Verhaltensschablone beschreibt das Verhalten (einer Komponente) und besitzt eine Menge von Kostenparametern (CostParameter). So werden beispielsweise konkrete Hardwareressourcen typischerweise mit Zustandsautomaten beschrieben, deren Zustände und Zustandsübergänge mit Kostenparametern (wie Zeitoder Energieverbrauch) annotiert werden können. Eine konkrete Implementierungsvariante ist später dafür verantwortlich, die Kostenparameter an konkrete Werte zu binden. Verschiedene Workloads (die in einer eigenen DSL beschreiben werden) lösen schließlich, zu einem späteren Zeitpunkt, das beschriebene Verhalten aus. Daher sind diese Konzepte von besonderer Bedeutung für die Simulation der Infrastruktur. 18 KAPITEL 2. DAS COOLSOFTWARE PROJEKT Analog zu Ports bei Komponententypen besitzen Verhaltensschablonen Pins, die ihre externe Schnittstelle definieren. Diese sind über das NavigatingElement mit dem zugehörigen Zustandsautomaten verbunden. Die Pins der Schablone lassen sich mit den Ports einer Komponente verbinden, wodurch ein Zusammenhang zwischen dem Aufruf der Schnittstelle der einen Komponente mit dem Verhalten einer anderen (z.B. einer Hardwareressource) ausdrückbar ist. Das Variantenpaket Wie in Abbildung 2.1 bereits angedeutet ist, stellt das Variantenpaket das Verbindungsglied zwischen Struktur- und Verhaltenspaket dar. Hier werden Elemente zur Definition von konkreten Varianten einer Komponente definiert. Zwei zentrale Konzepte sind Komponente (Component) und Verhalten (Behavior). Eine Komponente hat einen Komponententyp, und ein Verhalten hat eine Verhaltensschablone (aus Verhaltenspaket). Die Spezialisierung einer Komponente entspricht den Möglichkeiten, die aus dem Typsystem des Strukturpaketes bekannt sind. Des Weiteren verfügt eine Komponente analog über eine Menge von Ports und PortConnector mit entsprechenden Typ. Eine konkrete Variante einer Komponente entsteht durch die Verbindung eines Komponententyps mit einer Verhaltensschablone und Bindung konkreter Werte an die Kostenparameter. Zur Herstellung dieser Verbindung dient das Link-Element, welches die Ports der Variante mit den Pins der Schablone verbindet. Der beschriebene Aufbau ermöglicht es, Varianten kompletter (Sub-)Systeme zu definieren. Solche komplexen Variationen können später von der Laufzeitumgebung (THEATRE) verwendet werden, um alle Systemkonfiguration zu bewerten und eine geeignete auszuwählen, die den Bedürfnissen des Benutzers bei gleichzeitiger Erreichung optimaler Energieeffizienz entspricht. 2.2.2 Vertragssprache ECL Die ECL ist eine Kompositionssprache für CCM Komponenten mit speziellen Fokus auf Quality of Service-(QoS-)Parameter, welche nicht-funktionale Eigenschaften als zusätzliches Kompositionskriterium definierbar machen. Wie der Name bereits andeutet, ist es mit der Sprache möglich, Verträge (Contract) als Verbindungsglied zwischen angebotenen (provided) und benötigten (required) Diensten (Services) von Komponenten deklarieren. Dadurch lassen sich die Abhängigkeitsbeziehungen zwischen konkreten Softwarekomponenten untereinander sowie Hard- und Softwarekomponenten (Varianten) explizit festhalten. Für Details zur Sprache sei auf die ECL-Spezifikation 2.2. PROJEKTSKIZZE 19 [WCG+ 10] verwiesen. ECL basiert im Wesentlichen auf folgenden Konzepten: • Definition von Qualitätscharakteristiken: Eine Charakteristik dient als Maßeinheit, um eine spezifische Qualität ausdrücken zu können. So kann beispielsweise die Framerate eines Videostroms als Qualitätscharakteristik definiert werden. Dabei besitzt jede Charakteristik eine gewissen Domäne (domain), in der sie definiert ist. Im Beispiel der Framerate wäre die Domäne Anzahl der Frames pro Zeiteinheit (z.B. Sekunde), repräsentiert durch eine positive Ganzzahl. • Beschreibung von Ressourcen: ECL bietet außerdem Möglichkeiten um Hardwareressourcen beschreiben zu könnnen. Zur Definition einer Ressource können wiederrum Qualitätscharakteristiken verwendet werden. Eine mögliche Ressource wäre zum Beispiel ein Netzwerkgerät, welches als Qualitätscharakteristik die verfügbare Bandbreite aus der Domäne der rationalen Zahlen mit der Einheit Menge an übertragenen Bits (z.B. KBit) pro Zeiteinheit (z.B. Sekunde) definiert. • Definition von Profilen für CCM Komponenten: Profile sind das wichtigste Werkzeug, um verschiedene Komponenten und ihre Abhängigkeiten untereinander beschreiben zu können. Auf oberster Ebene werden die einzelnen Zustände (state), in denen sich eine Komponente zu einem Zeitpunkt befinden kann, sowie Zustandsübergänge (transition) dazwischen, beschrieben. Dabei können Profile weiter nach ihrer Art in Hard- und Softwarekomponentenprofile differenziert werden. Im Falle einer Hardwarekomponente Netzwerk könnte ein Profil die Zustände Verbunden, Getrennt, und jeweils eine Transition vom einen Zustand in den anderen, definieren. Dabei werden einzelne Zustände durch einen spezifischen Energieeffekt charakterisiert, der beispielsweise den jeweiligen Energieverbrauch angibt. Die Beschreibung von Transitionen hingegen erfolgt mit Hilfe von Event-Condition-Action(ECA)-Regeln, die angeben, wann (und unter welchen Bedingungen) der Übergang erfolgt. Darüber hinaus besitzen auch Zustandsübergänge einen Energieeffekt. Mit ECL können also Hardwarekomponenten durch erweiterte Zustandsmaschinen repräsentiert werden. Das Profil einer Softwarekomponente stellt das zentrale Verbindungsglied zwischen allen bisherigen Beschreibungsformen dar. Um die Konzepte zu erläutern und ein Gefühl für die Sprache zu schaffen, zeigt Listing 2.1 ein Beispiel. 20 KAPITEL 2. DAS COOLSOFTWARE PROJEKT 1 profile P l a y e r P r o f i l e for VideoPlayer { 2 state h i g h Q u a l i t y { 3 uses [ S e r v e r P r o f i l e . h i g h Q u a l i t y ] ; 4 provides [ c h a r a c t e r i s t i c frameRate = 3 0 ] 5 and [ c h a r a c t e r i s t i c image_width = 3 5 2 ] 6 and [ c h a r a c t e r i s t i c image_height = 2 8 8 ] ; 7 resources 8 [ c h a r a c t e r i s t i c bandwidth = 4 5 0 ] , 9 [ c h a r a c t e r i s t i c cpu_usage = 6 6 . 7 ] , 10 [ c h a r a c t e r i s t i c ram = 2 0 0 . 0 ] ; 11 } 12 state l o w Q u a l i t y { 13 uses [ S e r v e r P r o f i l e . l o w Q u a l i t y ] ; 14 provides [ c h a r a c t e r i s t i c frameRate = 1 0 ] 15 and [ c h a r a c t e r i s t i c image_width = 1 7 6 ] 16 and [ c h a r a c t e r i s t i c image_height = 1 4 4 ] ; 17 resources 18 [ c h a r a c t e r i s t i c bandwidth = 1 5 0 ] , 19 [ c h a r a c t e r i s t i c cpu_usage = 2 5 . 0 ] , 20 [ c h a r a c t e r i s t i c ram = 5 0 . 0 ] ; 21 } 22 t r a n s i t i o n any−state −> any−state {} 23 precedence h i g h Q u a l i t y , l o w Q u a l i t y ; 24 } Listing 2.1: ECL Beispiel VideoPlayer aus [GCW+ 10] Das Profil mit Namen PlayerProfile wird für eine Softwarekomponente namens VideoPlayer definiert. In diesem Fall besitzt der Player zwei Zustände (lowQuality und highQuality) sowie eine bidirektionale Transition dazwischen. Innerhalb der Zustände kann angegeben werden, welche Teile eines anderen Profils benötigt (uses), welche Qualitätscharakteristiken angeboten (provides), und welche Hardwarekomponenten (resources) dabei in Anspruch genommen werden. Die Zustandsbeschreibungen implizieren weiterhin, dass die Komponente in zwei Varianten, eine für geringe, und eine für hohe Qualität, vorliegen muss. Insgesamt beinhaltet das Profil alle nötigen Informationen, um festzustellen, wie Komponenten verteilt (deployed) und miteinander kombiniert (composed) werden können, bei gleichzeitiger Einhaltung gewisser QoS-Parameter. Daher können Profile auch als Einschränkungen Constraints verstanden werden, die üblicherweise vom Komponentenentwickler festgelegt werden. Mit ihnen lassen sich sowohl Abhängig- 2.2. PROJEKTSKIZZE 21 Abbildung 2.2: Autonome Kontrollschleife eines Auto Tuning Systems, aus [DDF+ 06] (neu gezeichnet) keiten unter Softwarekomponenten, als auch Abhängigkeiten zwischen Soft- und Hardwarekomponenten ausdrücken. 2.2.3 Laufzeitumgebung THEATRE Mit den bisher vorgestellten Teilen der CoolSoftware-Architektur können Komponenten (Hard- und Software) und ihre Abhängigkeiten untereinander zur Entwicklungszeit beschrieben werden. Die Architekturbeschreibung enthält darüber hinaus zusätzliche Informationen, die für eine Abschätzung des Energieverhaltens von Bedeutung sind. Um nun eine Optimierung des Energieverbrauchs zu erlauben, müssen diese Informationen zur Laufzeit interpretiert und mit weiteren Informationen, die nicht statisch vorhanden sind, kombiniert werden, um schließlich angemessen reagieren zu können. Das setzt eine übergeordnete Autorität voraus, die in CoolSoftware als THree Layer Auto Tuning Runtime Environment (THEATRE) bezeichnet wird. Der Name leitet sich zum Einen durch den Einbezug von drei Schichten, zum Anderen durch die Verfolgung eines Auto Tuning Ansatzes [DDF+ 06] ab. Ein Auto Tuning System zeichnet sich durch eine autonome Kontrollschleife aus, wie sie in Abbildung 2.2 dargestellt ist. Ein Schleifendurchlauf ist durch vier Stufen charakterisiert: • Sammeln (Collect) von Daten über das betrachtete System. • Analysieren (Analyse) der gesammelten Daten. • Entscheiden (Decide), welche Maßnahmen für ein Tuning nötig sind (auf Grundlage der ausgewerteten Daten). • Handeln (Act) - entspricht der aktiven Umsetzung der getroffenen Entscheidungen. 22 KAPITEL 2. DAS COOLSOFTWARE PROJEKT Users Contextor Energy Efficiency Software Components Local Energy Manager 1.1 Local Energy Manager 1.2 Local Energy Manager 1 Global Energy Manager Local Energy Manager 2 Resources (Hardware, OS, VM, …) Local Resource Manager 1 Local Resource Manager 2.1 Global Resource Manager Local Resource Manager 2 Abbildung 2.3: Architektur des THEATRE, aus [GWS+ 10b] Ein System, das diese Schritte umsetzt, kann sich dynamsich (zur Laufzeit) und automatisch an immer neue Situationen anpassen. Hinsichtlich Energieoptimierung muss ein derartiges System in der Lage sein, relevante Daten zu sammeln, das System bezüglich alternativer Konfigurationen zu analysieren, Entscheidungen abzuleiten, und schließlich das System automatisch in einen energieoptimalen Zustand zu rekonfigurieren. Zur Umsetzung eines Energy Auto Tuning (EAT) Systems wird eine spezielle Laufzeitumgebung benötig. Einerseits braucht sie Zugang zu relevanten Information und muss diese sinnvoll auswerten können. Andererseits muss sie in der Lage sein, entsprechende Rekonfigurationen praktisch durchzuführen. Um diesen Aufgaben nachkommen zu können, erstreckt sich die THEATRE über drei Schichten. Die grob-granulare Architektur ist in Abbildung 2.3 dargestellt und wird im Folgenden, gegliedert nach den einzelnen Schichten, erläutert. • Nutzerschicht: Das CoolSoftware-Projekt geht von der Annahme aus, dass dem Benutzer bei der Steigerung der Energieeffizienz eine besondere Bedeutung zukommt. Schließlich ist es der Benutzer, der weiß, welche QoSParameter er möchte beziehungsweise benötigt. Mit der Wahl der Parameter übt er also Einfluss auf den Energieverbrauch aus. Aufbauend 23 2.2. PROJEKTSKIZZE auf diesen Gedanken wird Energieeffizienz als Kosten-Nutzen-Funktion in [GWS+ 10b] wie folgt definiert: wi ∗ N utzermetriki N utzen = Energieef f izienz = Energieverbrauch Energieverbrauch z.B. 0.7 ∗ Antwortzeit + 0.3 ∗ Auf lösung = Energieverbrauch (2.1) P Durch Inanspruchnahme von Diensten induzieren Benutzer einen Workload auf Komponenten. Dabei ist der Nutzen für den Benutzer durch seine Bedürfnisse (wahrnehmbare Qualitäten = Nutzermetrik), also den QoS-Parametern, die unterschiedlich gewichtet sein können (w), bestimmt. Die Kosten hingegen äußern sich in Form des dafür benötigten Energieverbrauchs. Um im Allgemeinen die Bedürfnisse von Benutzern zu erfassen, sammeln Kontextoren (Contextor) (vgl. Abb. 2.3) Daten über die Wünsche der Benutzer. Effizienz stellt demnach ein Optimierungsproblem der in Gleichung 2.1. angegebenen Funktion dar. • Ressourcenschicht: Für die Verwaltung der Hardwarekomponenten sind Ressourcenmanager (Resourcemanager) [GWS+ 10b] zuständig. Jede Ressource besitzt einen eigenen, lokalen Manager (LRM), der Informationen zum aktuellen Zustand der Ressource liefert und deren Steuerung übernimmt. Bei den gelieferten Informationen kann es sich beispielsweise um vorhandene Kapazitäten, Verfügbarkeiten oder Auslastungen handeln. Dabei kennen LRM jeweils andere, adjazente LRM anderer Komponenten. Schließlich werden die Informationen aller LRM im globalen Resourcemanager (GRM) aggregiert. Dieser verfügt damit über Wissen, das die gesamte Infrastruktur abdeckt (z.B. welche Ressourcen gerade verfügbar sind und wie man sie erreichen kann). Darüber hinaus kann der GRM von allen Komponenten ihren aktuellen Energiezustand erfragen. • Softwarekomponentenschicht Neben den Ressourcenmanagern existieren Energiemanager (Energymanager) [GWS+ 10b], deren grundsätzlicher Aufbau ähnlich ist. Auch hier besitzt jede Softwarekomponente einen eigenen, lokalen Energiemanager (LEM), der wiederum andere adjazente LEM kennt. Jeder LEM besitzt Wissen über die Komponente, für die er zuständig ist. Dies umfasst sowohl bereitgestellte und benötigte Dienste, als auch In- 24 KAPITEL 2. DAS COOLSOFTWARE PROJEKT formationen zum Einsetzen (Deployment), Umsiedeln (Redeployment) und der Rekonfiguration einer Komponente. Die oberste Instanz stellt der globale Energiemanager (GEM) dar, dem alle validen Konfigurationen von Komponenten bekannt sind. In Zusammenarbeit mit dem GRM und dem Wissen seiner LEM ist er in der Lage, eine optimale Konfiguration des Gesamtsystems bezüglich des Energieverbrauchs abzuleiten. Die Rekonfiguration (vgl. Act aus Abbildung 2.2) geschieht schließlich durch die Kooperation von Ressourcenmanagern und Energiemanagern. Nachdem durch den GEM analysiert und über die optimale Konfiguration entschieden wurde, ist der GEM für die Verteilung der Komponenten zuständig, während der GRM dafür sorgt, dass geeignete Hardwarekomponenten in entsprechenden Energiezuständen bereitstehen. 2.3 Aufgabenstellung und Einordnung Das CoolSoftware Projekt hat als Hauptziel die Optimierung der Energieeffizienz von IT-Infrastrukturen. Um den speziellen Bedürfnissen energieadaptiver, selbst-rekonfigurierender Anwendungssysteme im Rahmen des EATAnsatzes gerecht zu werden, müssen zwei Gesichtspunkte beachtet werden: 1. Zur Entwicklungszeit: Die Softwarearchitektur muss für eine Energieoptimierung ausgelegt sein, wofür das CCM und die ECL verantwortlich sind. 2. Zur Laufzeit: Die Ausführungsumgebung muss die zusätzlichen Informationen der Softwarearchitektur mit Laufzeitinformationen kombinieren, um das System in einen energieoptimalen Zustand zu rekonfigurieren. Wie in Abschnitt 2.2.1 erläutert wurde, wird das Verhalten von Hardwarekomponenten innerhalb des CCM mit Zustandsdiagrammen, sogenannten Energy State Charts modelliert. Diese Modelle können die Ressourcenmanager, und damit auch der globale Energiemanager, des THEATRE verwenden, um ihre Entscheidungen, beispielsweise über die Wahl einer passenden Hardwareressource, zu treffen. Über das Verhalten von Hardwareressourcen hinaus wird auch eine Beschreibung des Verhaltens von Softwarekomponenten benötigt. Während sich Hardwareressourcen über die zur Verfügung stehenden Energiezustände sowie den möglichen Zustandsübergängen dazwischen beschreiben lassen, gestaltet 2.3. AUFGABENSTELLUNG UND EINORDNUNG 25 sich dieser Prozess bei Software deutlich schwieriger. Eine Verhaltensbeschreibung von Software im Kontext des CoolSoftware Projekt dient in erster Linie dazu, Rückschlüsse auf den Energieverbrauch der darunter liegenden Hardware zu erlauben. Allerdings ist der Energieverbrauch der Hardware stark davon abhängig, ob und auf welche Art und Weise Ressourcen von der Software verwendet werden. Die vorliegenden Arbeit hat zur Aufgabe, die Lücke zwischen Codeartefakten und Ressourcennutzung zu schließen, und aus den gefundenen Zusammenhängen eine geeignete Verhaltensbeschreibung für CCM Komponenten in Hinsicht auf ihren Energieverbrauch abzuleiten. Eine solche Beschreibung kann von der Laufzeitumgebung verwendet werden, um beispielsweise eine geeignete Implementierungsvariante einer Komponente auszuwählen oder über das optimale Deployment einer Komponente zu entscheiden. Abhängig von der Art der Verhaltensbeschreibung gibt es zwei mögliche Integrationsstellen. Ein Verhaltensmodell im klassischen Sinne (z.B. spezielles Zustandsdiagramm) könnte durch die Erweiterung des CCM Metamodells ausgedrückt werden. Dazu müssten zusätzliche Elemente, die zur Beschreibung eines BehaviorTemplate für Softwarekomponenten nötig sind, im Verhaltenspaket ergänzt werden. Für alternative Modelle bietet sich aber auch die ECL an. So könnte die ECL ohne Probleme dazu verwendet werden, Heuristiken über die Ressourcennutzung auszudrücken. Im Zusammenhang mit der Ermittlung der Ressourcennutzung von CCM Komponenten ergeben sich einige besondere Herausforderungen: 1. Eine Bestimmung der Ressourcennutzung kann nur zur Laufzeit erfolgen. 2. Für die Implementierung von CCM Komponenten ist vorrangig die Programmiersprache Java vorgesehen, wodurch eine zusätzliche Abstraktionsebene in Form der Java Virtual Machine eingeführt wird. 3. Das Entwicklersystem ist selten auch das System, auf dem eine Komponente eingesetzt wird. Der Ressourcenverbrauch muss also auch für das letztendliche Zielsystem ermittelbar sein. 26 KAPITEL 2. DAS COOLSOFTWARE PROJEKT Kapitel 3 Verwandte Arbeiten und Literatur Sowohl der Einfluss von Software auf den Energieverbrauch als auch die Verhaltensbeschreibung von Software selbst waren in der Vergangenheit wichtiger Gegenstand in Forschung und Entwicklung. Dieses Kapitel fasst eine Auswahl aus Literatur und aktuellen Entwicklungen in der Forschung (Stateof-the-Art) zusammen, die im Rahmen der vorliegenden Arbeit als bedeutsam eingestuft werden. Abschnitt 3.1 widmet sich den Möglichkeiten zur Verhaltensbeschreibung von Software sowie deren Gewinnung. Nachdem eine Auswahl theoretischer Grundlagen erläutert wurde, werden anschließend diverse Arbeiten zur Extraktion von Verhaltensbeschreibungen aus bestehender Software vorgestellt. Abschließend erfolgt eine Diskussion der Verhaltensbeschreibung von Softwarekomponenten im Kontext von CoolSoftware. Daraufhin befasst sich Abschnitt 3.2 zunächst allgemein mit dem Zusammenhang zwischen Software und Energieverbrauch und klärt anschließend, in einer State of the Art Analyse, wie der Einfluss von Software auf den Energieverbrauch aussehen kann. 3.1 Verhaltensbeschreibungen Die Beschreibung eines Verhaltens findet typischerweise auf einer abstrakten Ebene statt. Intuitiv denken Softwareingenieure dabei an Modelle. Allerdings wird der Begriff Verhaltensmodell in der Softwaretechnik üblicherweise mit konkreten Repräsentationsformen von Verhaltensmodellen assoziiert. Dieser Abschnitt erläutert daher zunächst einige theoretische Grundlagen, um eine allgemeinere Sichtweise auf Modelle zu eröffnen und das Vokabular für eine angemessene Diskussion zur Verfügung zu stellen. Anschließend erfolgt eine 27 28 KAPITEL 3. VERWANDTE ARBEITEN UND LITERATUR State-of-the-Art Analyse zu Verhaltensmodellen und ihrer Gewinnung. Zum Abschluss werden die verschiedenen Modellarten hinsichtlich ihrer Eignung für CoolSoftware diskutiert 3.1.1 Theoretische Hintergründe Modelle sind allgegenwärtig. Vor allem im Ingenieurwesen und in der Forschung sind sie ein explizites Themengebiet. Im Ingenieurwesen helfen sie oft, Artefakte zu erzeugen, indem sie Informationen über die Konsequenzen der Erzeugung dieser Artefakte zur Verfügung stellen, bevor diese tatsächlich produziert werden. Die Wissenschaft liefert hingegen häufig Theorien, die eine spezielle Form eines Modells darstellen [Lud03]. Der Modellbegriff Eine allgemeingültige Definition des Begriffs Modell gestaltet sich schwierig, wenn nicht gar unmöglich. Allerdings liefert die Allgemeine Modelltheorie (AMT) eine theoretische Basis, die eine angemessene Diskussion über geeignete Modelle erlaubt. In der AMT nach Stachowiak [Sta73] werden folgende Kriterien genannt, die Modelle von anderen Artefakten unterscheiden: • Abbildungskriterium: Es existiert ein Original (Objekt oder Phänomen), das auf das Modell abgebildet wird. Dabei ist es nicht erforderlich, dass das abzubildende Objekt tatsächlich in der Realität vorhanden ist. • Reduktionskriterium: Nicht alle Eigenschaften des Originals werden auf das Modell abgebildet. Durch Abstraktion, die auf einen gewissen Zweck hingerichtet ist, wird die Informationsmenge auf eine handhabbare Größe reduziert. Daher kann das Modell auf Situationen reagieren, mit denen das Original eventuell nicht umgehen kann [Lud03]. • Pragmatisches Kriterium: Das Modell ist nützlich, d.h. es kann das Original zu bestimmten Zwecken ersetzen. Daher kommen Modelle häufig zum Einsatz, wenn die Verwendung des Originals zu aufwändig wäre oder oder schlichtweg nicht möglich ist. Die Modellbildung Der Prozess der Erstellung eines Modells wird in der AMT als Modellbildung bezeichnet, die dem in der Softwaretechnik üblichen Begriff der Modellierung entspricht. Ein Modell wird zu einen bestimmten Zweck von einem Subjekt 29 3.1. VERHALTENSBESCHREIBUNGEN (dem Ersteller) für ein Original konstruiert, wie in Abbildung 3.1 dargestellt. Dabei ist es möglich, dass das Original selbst ein Modell ist. Sieht man beispielsweise den Quellcode als Modell für die auszuführenden Schritte auf Maschinenebene, so ist jedes Modell der Software ein Modell für ein Modell (Quellcode) [Lud03]. Original Original Modell Modell Subjekt Subjekt Zweck Zweck Abbildung 3.1: Prozess der Modellbildung nach der AMT Ein wichtiges Unterscheidungskriterium der AMT ist die Aufteilung in deskriptive und präskriptive Modellierung. Erstere bezeichnet die Modellierung eines bereits existierenden (oder zukünftigen, aber nicht gestaltbaren) Originals, während bei Letzterer ein zu schaffendes, gestaltbares Original modelliert wird. In diesen beiden Kriterien lässt sich eine Korrelation zu den Begriffen Forward Engineering und Reverse Engineering erkennen [CI90]. • Forward Engineering ist der traditionelle Prozess, von hohen Abstraktionsgraden und logischen, implementierungsunabhängigen Entwürfen, zu physischen Implementierungen zu kommen. • Reverse Engineering ist der Prozess der Analyse eines Systems, um Systemkomponenten und ihre Beziehungen zu identifizieren, sowie Repräsentationen des Systems in einer anderen Form oder auf einer anderen Abstraktionsstufe zu erstellen. Nach diesen Definition hat die Modellierung im Rahmen eines Forward Engineering Ansatzes also einen präskriptiven Charakter, während eine Modellextraktion beim Reverse Engineering einer deskriptiven Modellbildung entspricht. 30 KAPITEL 3. VERWANDTE ARBEITEN UND LITERATUR Systemtheorie Die Systemtheorie geht davon aus, dass grundlegende Konzepte in allen Systemen, so unterschiedlich sie auch anmuten mögen, zu finden sind. Als interdisziplinäre Herangehensweise bildet sie auch innerhalb der Informatik die Grundlage zahlreicher Theorien und Modelle, wie beispielsweise für die Theorie der Modellierung und Simulation [ZPK00]. Hier spielt vor allem die mathematische Systemtheorie eine entscheidende Rolle. Besonders im Zusammenhang mit Verhaltensbeschreibungen lohnt ein Blick auf diese weit allgemeinere Theorie, da sie explizit zwischen der Struktur und dem Verhalten eines Systems unterscheidet: • Struktur: Die interne Struktur (innerer Aufbau) eines Systems beinhaltet seinen Zustand, einen Zustandsübergangsmechanismus (Vorschrift, wie aktueller Zustand durch Input in zukünftigen überführt wird) und ein Mapping vom Zustand auf die Ausgabe. • Verhalten: Das externe Verhalten (äußere Manifestation) eines Systems ist die Beziehung, die es zeitlich zwischen seinen Eingaben und Ausgaben einführt. Das Ein- und Ausgabeverhalten besteht aus Paaren von Datensätzen (input-output), die direkt am System oder an einem repräsentativen Modell erhoben werden. Abbildung 3.2 zeigt die grundlegenden Konzepte eines Systems aus einer funktionalen Sichtweise (strukturelle und hierarchische Sichtweisen werden hier nicht behandelt). Aus dieser erscheint das System als Blackbox, die nichts über die interne Struktur beziehungsweise die einzelnen Elemente preis gibt. Das System interagiert über Ein- und Ausgaben mit der Systemumgebung und kann mehrere Zustände besitzen. Eingabe (input) Zustand (state) Ausgabe (output) Abbildung 3.2: Grundlegende Konzepte eines Systems Die Systemanalyse versucht, das Verhalten eines (möglicherweise noch nicht existierenden) Systems basierend auf dessen Struktur zu verstehen. Unter der Annahme, dass über die interne Struktur des Systems nichts bekannt ist (Blackbox), wie in Abbildung 3.2, so bezeichnet Systeminferenz den 3.1. VERHALTENSBESCHREIBUNGEN 31 Versuch, die Struktur aus Beobachtungen am realen System (Quellsystem) zu „raten“ [ZPK00]. Dabei werden typischerweise Ein- und Ausgaben erfasst, um anschließend ein generatives System oder gar ein Struktursystem aus den beobachteten Daten zu rekonstruieren. Ein generatives System versucht, die Beziehung zwischen Ein- und Ausgangsdaten möglichst genau nach zu bilden. Der Prozess wird auch als Modellkonstruktion bezeichnet. Typische Ausdrucksformen derartiger Modelle umfassen Anweisungen, Regeln, Gleichungen oder Constraints, die das Verhalten beschreiben. 3.1.2 Verhalten: Beschreibungsformen und ihre Gewinnung Die Konzeption eines Verhaltensmodells, das in der Lage ist, vorhandene Softwarekomponenten im Detail zu beschreiben, ist nicht das Ziel dieser Arbeit, da der Fokus auf ihrer Ressourcennutzung liegt. Nichtsdestotrotz weist der Prozess der Modellextraktion, also die Gewinnung eines Verhaltensmodells aus vorhandenen Softwareartefakten, eine gewisse Parallelität zur vorliegenden Problemstellung auf. Alle Ansätze greifen dazu, wenngleich dies nicht immer explizit erwähnt wird, auf Verfahren der Programmanalyse zurück. Verfahren zur Programmanalyse lassen sich in zwei Hauptbereiche unterscheiden: • statische Programmanalyse beschäftigt sich mit der Untersuchung des Quellcodes, um auf Eigenschaften eines Programms zu schließen, ohne es auszuführen. Typische Vertreter sind Analysen bezüglich des Kontrollflusses oder des Datenflusses. • dynamische Programmanalyse bezeichnet alle Verfahren, die den Quellcode zur Ausführung bringen, um anschließend auf gewisse Eigenschaften des Programms schließen zu können. Da die relevanten Informationen zur Ressourcennutzung lediglich zur Laufzeit verfügbar sind, sind ausschließlich dynamische Verfahren von Interesse. Modellextraktion Endliche Zustandsautomaten (Finite State Automata (FSA)) stellen die mit Abstand verbreitetste Form dar, um das Verhalten von Software zu repräsentieren. Modellextraktion bedeutet in diesem Fall, einen FSA auf der Grundlage zur Laufzeit gesammelter Daten zu „lernen“. Erst kürzlich erschien zu 32 KAPITEL 3. VERWANDTE ARBEITEN UND LITERATUR diesem Thema eine ausgezeichnete1 Doktorarbeit von Dallmeier [Dal10]. Im Folgenden werden diese und weitere artverwandte Arbeiten auf informale Weise zusammengefasst (für formale Definitionen sei auf Kapitel 3 in [Dal10] verwiesen). Object Behavior Models Dallmeier führt verallgemeinernd den Begriff des Softwareausführungsmodells (software execution model) ein, womit Modelle beschrieben werden, die das Verhalten eines Programms in ein Modell einfangen, welches automatisiert verarbeitet werden kann. Der Zweck derartiger Modelle wird in ihrer Nützlichkeit für das Debugging sowie dem Verständnis existierender Programme gesehen. Aufbauend auf einer guten Literaturanalyse wird eine neue Modellart, die sogenannten Objektverhaltensmodelle (object behavior models), entwickelt. Das zentrale Mittel zur Datenerhebung stellt eine Ablaufverfolgung der Ausführung dar, die Ereignisse von Interesse aufzeichnet. Die Aufzeichnungen werden in sogenannten execution traces festgehalten, die aus einer Sequenz einzelner Trace-Einträge bestehen. Ein Trace-Eintrag verbindet erhobene Daten mit einem Zeitstempel. Das Problem der Modellextraktion wird schließlich als das Lernen eines FSA aus den gesammelten execution traces formuliert (analog zum Erlernen eines FSA in der Sprachtheorie). Die Werte lokaler Felder sowie als Inspektoren (Inspector, Methoden mit Rückgabewert und ohne Parameter) bezeichnete Methoden des Objekts werden verwendet, um die Zustände des FSA zu charakterisieren. Jeweils zu Beginn und zum Ende eines Methodenaufrufs werden die Werte der Felder sowie die Inspektoren festgehalten. Auf diese Weise können execution traces relativ unkompliziert in Zustände und Transitionen, also einen FSA, umgewandelt werden. Abbildung 3.3 zeigt beispielhaft ein Objektverhaltensmodell der Klasse Vector. Der initiale Zustand wird mit start beschriftet und repräsentiert das Objekt direkt nach seiner Erzeugung. Zustandsänderungen ergeben sich bei diesen Modellen immer durch Methodenaufrufe. Die Entscheidung, Zustände der Klasse Vector lediglich durch den einen Inspektor isEmpty() zu repräsentieren, setzt manuelles Eingreifen voraus. So besitzt die Klasse beispielsweise noch ein lokales Feld size. Ein FSA, der diese Variable für die Zustandsbeschreibung verwendet, müsste für jeden Wert der Variable einen eigenen Zustand definieren. Angenommen das Objekt hat 1000 Einträge gespeichert, so hätte der FSA dementsprechend über 1000 Zustände, wodurch er nahezu unbrauchbar würde. Dieser Sachverhalt wird gewöhnlich als state explosion problem bezeichnet. Um dieses Problem zu adressieren, wurden sogenannte 1 Ernst-Denert-Preis 33 3.1. VERHALTENSBESCHREIBUNGEN !remove() add(),remove() add() start isEmpty():true isEmpty():false remove(), clear(), removeAll() Abbildung 3.3: Ein Objektverhaltensmodell der Klasse Vector, neu gezeichnet aus [Dal10] Abstraktionsfunktionen eingeführt, die die Werte eines Zustandes auf einen anderen (abstrakteren) Zustand mit eingeschränkten Wertebereich abbilden. Weitere Arbeiten Im Folgenden werden weitere Arbeiten, die ähnliche Ergebnisse erzielen, sich aber im Vorgehen unterscheiden, kurz zusammengefasst. • Extended Finite State Machines (EFSM) Der in [LMP08] von Lorenzoli et al. vorgestellte Ansatz geht ähnlich vor wie die Arbeit von Dallmeier. Es kommen wieder Tracing Methoden zum Einsatz, die Methodenaufrufe, Parameter und Variablen zur Laufzeit protokollieren (interaction traces).Ein EFSM ist ein nicht-deterministischer Automat mit anonymen Zuständen, der Aufrufsequenzen von Methoden, die mit zusätzlichen Daten annotiert sind, repräsentiert. Transitionen stehen für einen Methodenaufruf, und können mit Parameter- und Variablenwerten sowie Prädikaten beschriftet sein. Zur vollständig automatischen Ableitung eines EFSM aus den Traces wurde der gk-tailInferenzalgorithmus als Erweiterung des k-tail-Algorithmus entwickelt. Dazu leitet gk-tail Kriterien für das Zusammenfassen von Zuständen ab. Um abstrakte Prädikate (Relationen aus einer Menge VariablenWert Paare) aus den Parameterwerten abzuleiten, kommt das Werkzeug Daikon [EPG+ 07], ein dynamischer Detektor für Invarianten, zum Einsatz. • Verhaltensmodell aus Kontextinformation Duarte [Dua07] stellt einen Ansatz vor, der statische (Kontrollfluss) und dynamische Informationen (Trace) in Form von Kontextinformation zusammenführt. 34 KAPITEL 3. VERWANDTE ARBEITEN UND LITERATUR Das Konzept des Kontext kombiniert einen Ausführungspunkt im Kontrollflussgraph und den Zustand, repräsentiert durch Werte der Variablen, des Systems. Kontexte sind verschachtelte Strukturen, die die Extraktion von kausalen Zusammenhängen zwischen Aktionen im System unterstützen können. Die Modelle werden in Form eines Labelled Transition Systems extrahiert. • Interactive Grammar Inference Auch in [WBHS07] werden Traces zur Laufzeit generiert. Der Unterschied liegt in der interaktiven Ableitung des Zustandsautomaten, unter Verwendung einer grammer inference Technik. Interaktiv bedeutet, dass während der Konstruktion Fragen für den Nutzer generiert werden, die als Systemtests interpretiert werden können. • Markov Modelle Diese Art von Modellen geht davon aus, dass der Zustandsautomat die Markov-Eigenschaft erfüllt. Kurz gesagt bedeutet das, dass jeder Folgezustand nur von Informationen des aktuellen Zustandes abhängig ist. Dadurch kann der Automat mit wahrscheinlichkeitstheoretischen Informationen kombiniert werden. Ein Beispiel hierfür ist [BRH04]. Hier werden die Transitionen mit Wahrscheinlichkeiten annotiert, die angeben, dass beispielsweise Zustand B mit der Wahrscheinlichkeit x zum Zeitpunkt t+1 erreicht wird, wenn zum Zeitpunkt t der vorherige Zustand A war. • Blackbox Ansatz Der Artikel [SMSS10] beschreibt eine andere Herangehensweise. Grundlegend verschieden ist die Annahme, dass von einer Komponente nur das Interface bekannt ist, sie also als Blackbox vorliegt. Weiter wird davon ausgegangen, dass eine Komponente in verschiedenen Zuständen immer nur eine Untermenge ihrer Services unterstützt (Menge der aktiven Methoden). Diese Untermenge charakterisiert den aktuellen Zustand der Komponente, da durch die Blackbox-Annahme keine Variablen verfügbar sind. Durch bestimmte Methodenaufrufe verändert sich die Menge der aktiven Methoden. Diese Aufrufe stellen die Beschriftung der Transitionen dar. 3.1.3 Diskussion im Kontext von CoolSoftware Ein Ziel dieser Arbeit ist der Entwurf beziehungsweise die Auswahl eines Verhaltensmodells, um die Ressourcennutzung von Software zu charakterisieren. Durch die Ausführung des Quellcodes wird eine bestimmte Nutzung unterschiedlicher Hardwareressourcen verursacht. Das Nutzungsmodell ist dem- 35 3.1. VERHALTENSBESCHREIBUNGEN nach indirekt vom konkreten Quellcode und von spezifischen Hardwarekomponenten abhängig. Abbildung 3.4 verdeutlicht diesen Zusammenhang. Ausführung CCMCCMModell Modell RessourcenRessourcennutzungsmodell nutzungsmodell Quellcode Quellcode Hardware Hardware RessourcenRessourcenauslastung auslastung Abbildung 3.4: Problematik des Ressourcennutzungsmodells Da der eigentliche Gegenstand der Modellierung die Ressourcennutzung während der Ausführung ist, kann das Ressourcennutzungsmodell nicht (oder nur mit unvertretbaren Aufwand) vor der Implementierung dem CCM-Modell hinzugefügt werden. Das CCM-Modell muss demnach zunächst in Quellcode umgesetzt und alle Funktionen implementiert werden, bevor das Ressourcennutzungsmodell erstellt werden kann, da die Beobachtungen während der Ausführung erfolgen. Aus den Beobachtungen lässt sich schließlich ein Modell ableiten und dem bestehenden CCM-Modell im Nachhinein hinzufügen. Konkretisierung des Modells und der Modellbildung nach AMT Eine besondere Bedeutung kommt dem Reduktionskriterium zu. Das Modell sollte wirksam von der Komplexität des Quellcodes auf der einen Seite, und der Komplexität der Hardwarekomponenten auf der anderen Seite, abstrahieren. Zur Beschreibung des gewünschten Ergebnisses werden die aus der AMT bekannten Begriffen verwendet: • Original: In diesem Fall ist das Original ein Phänomen, nämlich die physische Ressourcenauslastung der Hardware, hervorgerufen durch die Ausführung des Quellcodes einer CCM Komponente. • Zweck: Das Modell soll eine Prognose über das Verhalten bezüglich der Ressourcennutzung einer CCM Komponente während der Ausfüh- 36 KAPITEL 3. VERWANDTE ARBEITEN UND LITERATUR rung erlauben. Das ermöglicht einen Verzicht auf Details (von Hardware und Software), die kaum oder gar keinen Einfluss auf die Nutzung haben. • Subjekt: Der Ersteller des Modells ist nicht zwangsweise der Entwickler der CCM Komponente. Diese Aufgabe kann auch von einem Softwareingenieur übernommen werden, der für das Deployment oder das Zusammenfügen der Komponenten verantwortlich ist. Ein weiteres wichtiges Kriterium ist die Richtung der Modellbildung. CCM Komponenten werden üblicherweise im Rahmen eines Forward Engineering Ansatzes entwickelt. Demnach führt der übliche Weg, im Sinne eines Wasserfallmodells [Roy70], von hohen Abstraktionsstufen zur Implementierung. Dieses Vorgehen ist für die Bildung Ressourcennutzungsmodells nur bedingt geeignet: • Forward Engineering: Eine präskriptive Modellbildung lässt sich, aufgrund der Abhängigkeit zu konkreter Hardware, nur schwer realisieren. Eine angemessene Vorhersage erfordert einen Simulationsansatz [ZPK00], der wiederum detaillierte Modelle der Hardware voraussetzt. Darüber hinaus müsste die komplette Systemumgebung (z.B. Betriebssystem) mit einbezogen werden. Dieser Ansatz erscheint für einzelne wissenschaftliche Untersuchungen geeignet. In der Praxis ist er aber eindeutig zu aufwändig. • Reverse Engineering: Die deskriptive Modellbildung erlaubt eine eher pragmatische Vorgehensweise. Den Ausgangspunkt bildet die Annahme, dass die Komponente bereits implementiert (Quell- oder Bytecode) vorliegt, und auf der Zielplattform eingesetzt ist. Durch Beobachtungen innerhalb ihrer Umgebung zur Ausführungszeit wird versucht, ein Modell abzuleiten. Diese Vorgehen entspricht einer Systeminferenz. Zusammenfassung Die vorgestellten Arbeiten zur Modellextraktion leiten, aus zur Laufzeit gesammelten Informationen, ein Verhaltensmodell der Software ab. Während das Vorgehen verwandt ist, liegt ein Unterschied in der Zielstellung vor: Softwareausführungsmodelle sollen das Verhalten möglichst genau beschreiben, damit sie zum weiteren Erkenntnisgewinn über die Software verwendet werden können. Diese Arbeit hingegen sucht nach einem möglichst einfachen (abstrakten) Zusammenhang zwischen Quellcode, Hardware und Ressourcennutzung. Zweifellos hat der interne Aufbau (z.B. Kontrollfluss) der Software 3.2. SOFTWARE UND ENERGIEVERBRAUCH 37 Einfluss auf die Ressourcennutzung. Des Weiteren könnten wahrscheinlichkeitstheoretische Informationen helfen, die Modelle akkurater zu gestalten. Dennoch: je mehr Informationen einfließen, umso komplexer wird das Modell, was der Zielstellung widerspricht. Abschnitt 5.1 erläutert genauer, was in dieser Arbeit unter abstrakter Verhaltensbeschreibung verstanden wird. . 3.2 Software und Energieverbrauch „Smart power management is all about doing more with the resources we have.“[Gar07] Software hat entscheidenden Einfluss auf den Energieverbrauch eines Systems. Das Ziel dieses Abschnitts besteht darin, im Sinne einer State-of-theArt Analyse zu klären, wie dieser Einfluss aussehen kann. Das Thema ist gleichermaßen komplex wie relevant. Schließlich gilt es zu zeigen, dass der Energieverbrauch maßgeblich von der Art und Weise, wie Software mit den direkten Energieverbrauchern, den verfügbaren Hardwarekomponenten, interagiert, abhängig ist. Einleitend ein (naiver) Denkansatz: Ein moderner Prozessor besitzt derzeit mehrere 100 Millionen Transistoren. Selbstverständlich muss Energie zugeführt werden, um den Zustand dieser Transistoren zu verändern und damit den Prozessor zum „rechnen“ zu bewegen. Je mehr Rechenoperationen von einer Anwendung ausgehen, desto höher ist die Anzahl erforderlicher Schaltungen, und dadurch auch die benötigte Energiezufuhr. Wird eine berechnungsintensive Applikation (über einen längeren Zeitraum) ausgeführt, ist dieser Effekt oft direkt in Form einer erhöhten Lüfterdrehzahl wahrnehmbar. Kombiniert man diese Beobachtung mit dem Wissen, dass ein Prozessor 100% Verlustleistung besitzt, d.h. die komplette aufgenommene Energie in Wärme umgewandelt wird, so wird ein (erster) Zusammenhang zwischen Software und Energieverbrauch deutlich. Das Beispiel ist natürlich stark vereinfacht, trifft aber den Kern der Problematik: Es spielt eine Rolle, wie Hardwarekomponenten von Software verwendet werden. Um den Zusammenhang umfassender zu untersuchen, werden im Folgenden repräsentative Arbeiten vorgestellt, die sich dem Thema Software und Energieverbrauch widmen. 3.2.1 Einführung Die Art und Weise, wie Software Einfluss auf den Energieverbrauch nehmen kann, ist mannigfaltig. Es sollte zunächst klar sein, dass Software nicht nur 38 KAPITEL 3. VERWANDTE ARBEITEN UND LITERATUR Anwendungsprogramme, sondern auch das Betriebssystem umfasst. Nach Tanenbaum [Tan07] gibt es zwei grundsätzliche Ansätze, den Energieverbrauch einzuschränken: • Energieverwaltung durch das Betriebssystem: Da das Betriebssystem alle Geräte kontrolliert, muss es auch entscheiden, wann einzelne Ressourcen den Wechsel in einen niedrigeren Energiezustand vollziehen. Energiezustände von Hardware werden später in Abschnitt 4.1.2 genauer erläutert. • Eingeschränkter Betrieb: Diese Maßnahmen betreffen Anwendungsprogramme. Die Energieeinsparung ergibt sich aus einer Verringerung der Geschwindigkeit, die mit Einschränkungen des Komforts für den Benutzer einhergeht (vergleiche CoolSoftware Abschnitt 2.2.3). Als Beispiel wird von Tanenbaum die Arbeit von Flinn [FS99] angeführt. Um Maßnahmen zur Steigerung der Energieeffizienz zu kategorisieren, ist es hilfreich, Software in die bekannten Rollen Konsumenten (z.B. Anwendungen) und Erzeuger (Ressourcenmanager) einzuteilen [Sax10]. Aus dem Studium der Literatur kann eine grobe Einteilung in Stufen, die aber keineswegs unabhängig von einander sind, erfolgen. 1. Die Software nimmt direkten Einfluss, so wie es das einleitende Beispiel verdeutlichte. Ein effizienterer Algorithmus, der mit weniger Rechenoperationen auskommt, wird sich immer positiv auf den Energieverbrauch auswirken. 2. Die Software nimmt indirekt Einfluss, indem sie so entwickelt wird, dass sie eine effiziente Ausnutzung der Energiezustände moderner Hardwarekomponenten erlaubt. Ein anschauliches Beispiel bietet das „race to idle“ Prinzip [Gar07]. Demnach ist es energieeffizienter, den Prozessor eine Menge Arbeit bei voller Geschwindigkeit verrichten zu lassen, um anschließend möglichst lange im Zustand idle zu verweilen, als ihn bei gedrosselter Geschwindigkeit ständig kleine Portionen Arbeit verrichten zu lassen. 3. Die Software fungiert als Ressourcenmanager, was (momentan) typischerweise die Rolle des Betriebssystem ist. Je komplexer die Infrastruktur ist, desto größer sind die Optimierungsmöglichkeiten hinsichtlich energieeffizienter Ressourcenverwaltung [Sax10]. 3.2. SOFTWARE UND ENERGIEVERBRAUCH 3.2.2 39 State of the Art Um den Energieverbrauch von Software zu bestimmen, muss es zunächst möglich sein, selbigen in geeigneter Form zu ermitteln. Die Herausforderung besteht unter Anderem darin, eine möglichst genaue Zuordnung zwischen der Ausführung von Softwareartefakten und deren Verbrauch herzustellen. Im Wesentlichen finden sich dazu zwei Verfahren: • Messung: Messungen des Energieverbrauchs können beispielsweise mit einem Digitalmultimeter (DMM) realisiert werden. Diesen Ansatz verfolgte auch Flinn [Fli01] bei der Entwicklung von PowerScope, das ein statistisches Samplingverfahren verwendet. Dabei kommt jeweils ein Rechner für das Profiling der Anwendung und einer für die Datensammlung zum Einsatz. Der Profilingrechner zeichnet periodisch Daten über die aktuelle Anwendung auf (system monitor), während der Datenrechner die Samplingdaten aus dem DMM speichert (energy monitor). Das DMM ist zwischen die beiden Rechner geschaltet. und dessen Zeitgeber gibt die Samplingraten vor. Durch eine Verbindung des DMM (Triggereingang bzw Triggerausgang) mit dem Proflingrechner erfolgt die Synchronisation, indem es den Wert eines Inputpins kippt, wodurch ein Interrupt ausgelöst wird, der den Systemmonitor zur Datenaufzeichnung anstößt. • Simulation: In [GSI+ 02] wurde ein auf Simulation basierender Ansatz, genannt SoftWatt, entwickelt. Dabei werden alle Hardwarekomponenten sowie das Betriebssystem simuliert, um den Energieverbracuh zu ermitteln. Das größte Problem bei derartigen Ansätzen ist das Aufsetzen der Infrastruktur. Da alle Hardwarekomponenten simulierbar sein müssen, werden detaillierte und validierte Modelle benötigt. Derartige Ansätze werden auch häufig als complete system simulation bezeichnet. Die Simulationskosten steigen daher mit der Komplexität des zu simulierenden Systems. Die Vorteile liegen in der Genauigkeit, und dass der Simulator Daten zu Ereignissen liefern kann, die bei realer Hardware nicht zugänglich sind. Energieverbrauch Anwendungen In [Fli01] untersuchte Flinn, wie die Entwurfsentscheidungen von Software den Energieverbrauch von Systemen beeinflussen. Es wurde gezeigt, wie sich Anpassungen (Energy-aware Adpation) zur Laufzeit auf den Energieverbrauch auswirken. Für die Messungen kam das beschriebene PowerScope zum Einsatz. Damit Anpassungen sinnvoll sind, müssen zwei Voraussetzung erfüllt sein: 40 KAPITEL 3. VERWANDTE ARBEITEN UND LITERATUR • Eine Änderung des Verhaltens der Anwendung muss zu Energieeinsparungen führen. • Die Energieeinsparungen sollten vorhersagbar sein. Im Zentrum der Untersuchungen stand der Zusammenhang zwischen qualitativ unterschiedlichen Daten und dem Energieverbrauch. Dazu wurden vier Applikationen getestet. Eine davon war ein Videoplayer, der vier Videos unterschiedlicher Qualitäten (Größe und Höhe der verlustbehafteten Kompression bei der Framekodierung) über einen Netzwerkstream abspielt. Dabei wurde nachgewiesen, dass sowohl die unterschiedlichen Qualitäten den Energieverbrauch signifikant beeinflussen, als auch dass eine Kombination mit Energiesparstrategien der Hardware einen weiteren Gewinn bringt. Des Weiteren wurde damit gezeigt, dass eine höhere Qualität eine höhere Ressourcenauslastung zur Folge hat, wodurch wiederum der Energieverbrauch steigt. In weiteren Arbeiten [FPS02] wurde noch der Zusammenhang zwischen Lokalität (remote oder lokale Ausführung von Softwareartefakten) und Energieverbrauch untersucht. Schließlich werden zur Vorhersage des Energieverhaltens lineare mathematische Modelle verwendet, die auf Qualität und Größe der Daten basieren. Während bei Flinn Datenqualitäten im Vordergrund standen, untersuchten Fei et al. [FZJ08] gezielt die Auswirkungen von Veränderungen der Berechnungsqualitäten. Dazu wurde ebenfalls das Beispiel eines Videoplayers verwendet, und verschiedene Parameter (Ditheringmethode, Framerate, Frameanzeigegröße) als QoS-Dimensionen definiert. Die Kombination unterschiedlicher QoS-Werte ergibt einen spezifischen QoS-Punkt, der mit Energiekosten assoziiert ist. Auch hier konnte nachgewiesen werden, dass eine Verminderung der Qualitäten zu signifikanten Energieeinsparungen führen kann. Zur Modellierung des Energieverbrauchs kommen Makromodelle zum Einsatz. Das Ergebnis stellt ein Framework zur Steigerung der Energieeffizienz dar, das die Nutzerwünsche und QoS-Parameter einbezieht. Eine spezifischere Untersuchung stammt von Poess und Nambiar [PN08]. Sie entwickelten auf Grundlage vorhandener Ergebnisse des TPC-C Benchmark (ein Benchmark für OLTP Systeme) ein Energieverbrauchsmodell für transaktionsorientierte Systeme. Das Modell ist relativ einfach zusammengestellt: für jede einzelne Komponente (CPU, Speicher, Festplatten, Serverchassis und Festplattengehäuse) wird der Energieverbrauch aus Spezifikationen (Peak) angenommen und für Gehäuse und Chassis ein prozentualer Anteil, abhängig von den beherbergten Komponenten, sowie ein Overhead angenommen. Um das Modell zu Testen wurden wieder Messungen am realen System (für das die Ergebnisse des Benchmark vorlagen) durchgeführt. Bei der Auswertung wurde festgestellt, dass der Workload des TPC-C den 3.2. SOFTWARE UND ENERGIEVERBRAUCH 41 größten Einfluss auf den Energieverbrauch hat, da er hauptsächlich für die Belastung der Komponenten verantwortlich ist. Von Seo et al. [SMM08] wurde ein Framework zur Abschätzung des Energieverbrauchs, vorrangig verteilter und eingebetteter, auf Java-basierter Anwendungen entwickelt. Das Hauptziel besteht in der Ermöglichung fundierter Entscheidungen, wann die Systemarchitektur angepasst werden sollte, um den Energieverbrauch batteriebetriebener Geräte zu vermindern und damit die Kerndienste einer Komponente länger anbieten zu können. Der Ansatz bezieht das (verteilte) System als Ganzes mit ein, schätzt den Energieverbrauch aber auf Komponentenebene ab. Durch eine genaue Abschätzung des Verbrauchs können passende Aktionen ausgeführt werden, um das System als Ganzes länger funktionstüchtig zu halten. Solche Aktionen umfassen beispielsweise nicht benötigte oder entbehrliche Komponenten abzuschalten (unload), die Umsiedlung energiehungriger Komponenten auf einen größeren Host oder Komponenten,die häufig miteinander kommunizieren, auf denselben Host zu platzieren. Die Energiekosten einer Komponente setzen sich aus Berechnungs- und Kommunikationskosten zusammen. Erstere umfassen zum Beispiel CPU-Nutzung, Speicherzugriffe und IO, während Kommunikationskosten hauptsächlich durch Netzwerkkomponenten verursacht werden. Die Berechnungskosten werden auf Schnittstellenebene bestimmt, was im allgemeinen Fall Methodenaufrufen entspricht. Modelliert werden diese Kosten durch die Summierung der Energiekosten für Bytecodes, native Methodenaufrufe und Monitore (diese Begriffe werden in Abschnitt 4.1.4 erläutert). Für Kommunikationskosten werden nur Verbindungen über das UDP (User Datagram Protocol) in Form der übertragenen Datenmenge betrachtet. Um an die erforderlichen Daten zu kommen wurde eine spezifische JVM (Kaffe 1.1.5) instrumentiert, die für Lern- und Forschungsfragen (clean room) konzipiert wurde. Allgemein ist für den Ansatz immer erforderlich, dass dieselbe JVM, dasselbe Betriebssystem und dieselbe Hardware verwendet wird. In einer folgenden Arbeit wurde von Seo et al.[SEP+ 09] auch noch der Einfluss unterschiedlicher Architekturstile auf den Energieverbrauch untersucht. Energieverbrauch Betriebssysteme Arbeiten zur Ermittlung des Energieverbrauchs von Betriebssystemen stammen ebenfalls meist aus dem Embedded Bereich, wobei die von Tan et al. [TKTJ02] und Li et al. [LJ03] entwickelten Methoden als die relevantesten eingeschätzt werden. Die Vorgehensweise der beiden fast zeitgleich entstandenen Arbeiten ist sehr ähnlich: der Energieverbrauch grundlegender Betriebssystemroutinen wird unter Verwendung von Testprogrammen bemessen und 42 KAPITEL 3. VERWANDTE ARBEITEN UND LITERATUR anschließend daraus ein mathematisches Modell abgeleitet. Der Hauptunterschied liegt im angedachten Verwendungszeitpunkt der gewonnenen Modelle. Im Folgenden werden beide Ansätze kurz vorgestellt und anschließend bewertet. Bei Li et al. [TKTJ02] liegt der primäre Fokus auf der Entwicklung eines Energiemodells, das zur Laufzeit zur Energieverwaltung verwendet werden kann. Moderne Betriebssysteme werden als komplexe Software gesehen, die in der Lage sind, eine heterogene Ressourcenlandschaft zu verwalten. Die Komplexität dieser Software wird dabei hinter einer relativ simplen Schnittstelle versteckt, den Kernel-Diensten (operating system kernel services). Daher werden, zur Modellierung des Energieverhaltens des komplexen Betriebssystems, die auf Ebene dieser Schnittstelle zur Verfügung gestellten Dienste betrachtet. Das Energieverhalten des Gesamtsystems ist schließlich, vereinfacht ausgedrückt, durch die Summe aller Dienste charakterisiert. Für die Untersuchungen kam ein Simulator zum Einsatz, der in der Lage ist, die komplette Systemumgebung zu simulieren. Dabei standen für die Hardwarekomponenten validierte Energiemodelle zur Verfügung und es wurde ein spezifisches Betriebssystem simuliert. Auf dieser Testplattform wurden anschließend verschiedene Testprogramme mit unterschiedlichen Charakteristiken gefahren. Dazu erfolgte eine Einteilung der Benchmarks in zwei Gruppen. Die eine zur Datenerhebung für die Modelle (Profiling), die andere um die Exaktheit der Modelle zu validieren. Für jede Betriebssystemroutine wurde die Energie sowie die Standardabweichung mit verschiedenen Benchmarks gemessen. Die Untersuchungen am Simulator zeigten, dass unterschiedliche Routinen auch unterschiedlich viel Energie verbrauchen. Daher ist es nicht ausreichend, den Energieverbrauch aller zu messen und einen Durchschnitt zu errechnen. Vielmehr sollte jede einzeln bemessen werden.Des Weiteren zeigte sich bei den Testprogrammen, dass unterschiedliche Benchmarks verschiedene Routinen unterschiedlich häufig aufrufen. Dadurch kommen auf Anwendungsebene verschiedene Energieverteilungsmuster zustande. Ein weiteres wichtiges Ergebnis ist die Korrelation zwischen Performance und Energieverbrauch. Als Maß für die Performance wurde die Menge an Instruktionen, die pro Taktzyklus (instructions per cycle (IPC)) vom Prozessor ausgeführt werden können, herangezogen. Die Korrelation zwischen Energieverbrauch und IPC erweist sich als linear und kann damit als Parameter (Korrelationsnummer) in die Gleichungen des Energiemodells einfließen. Laut Evaluation waren die Modelle in der Lage, tatsächlichen Energieverbrauch mit einer durchschnittlichen Abweichung von 6% berechnen können. Die von Tan et al. [LJ03] entwickelten Makromodelle sollen dem Entwick- 3.2. SOFTWARE UND ENERGIEVERBRAUCH 43 ler bei Entwurfsentscheidungen unterstützen. Durch frühes Feedback über den Energieverbrauch von Betriebssystemdiensten soll es ermöglicht werden, dass der Entwickler die Auswirkungen der Architekturwahl auf das spätere Energieverhalten bereits beim Entwurf beurteilen kann. Das Betriebssystem wird als ein sehr großer Prozess gesehen, der mit den Prozessen der einzelnen Anwendungen interagiert. Anwendungen können über Systemaufrufe (syscalls mit dem großen Prozess in Verbindung treten. Nach einem solchen Funktionsaufruf springt die Kontrolle zum großen Prozess über. Sobald die Funktion zurückkehrt, geht die Kontrolle wieder an die Anwendung über. Daher wird das Betriebssystem auch als multi-entry multi-exit pair (MEME-P) bezeichnet. Das Betriebssystem wird als große Box aufgefasst, die von verschiedenen Pfaden, die sich im Inneren teilweise überlappen, durchzogen ist. Im Gegensatz zu Li et al. wird zusätzlich zwischen impliziten und expliziten Systemaufrufen unterschieden. Erstere umfassen im Wesentlichen Interrupts, Signalbehandlung sowie Scheduling, die bei der Ausführung des Betriebssystems auftreten. Letztere sind die explizit ausgeführten Systemaufrufe. Tan et al. gehen davon aus, dass der Quellcode des Betriebssystems zur Verfügung steht. Daraus soll der Entwickler zunächst eine Auswahl an expliziten Systemaufrufen treffen. Die Auswahl erfolgt nach der erwarteten Aufrufhäufigkeit beziehungsweise nach der Relevanz für einen bestimmten Architekturstil. Daraufhin sollte für jeden ausgewählten Pfad ein Testprogramm erstellt werden. Die Testprogramme der einzelnen Funktionen werden dann, wie zuvor bei Li et al., im Simulator ausgeführt. Dadurch lässt sich der Energieverbrauch pro Funktionsaufruf bestimmen. Da Systemaufrufe überladen sein können, wird anschließend der durchschnittliche Energieverbrauch für jeden Pfad ermittelt. Ebenso wird der Energieverbrauch der impliziten Aufrufe durch Simulation spezifischer Testprogramme bestimmt. Das entsprechende Makromodell zum Energieverhalten des Betriebssystems wird anschließend durch Verschmelzung von impliziten und expliziten Charakteristiken abgeleitet. Derselbe Simulator, der zur Bestimmung des Energieverbrauchs der einzelnen Pfade verwendet wurde, wird verwendet um den Energieverbrauch von Anwendungen zu bestimmen. Die Ergebnisse wurden abschließend mit den durch das Makromodell vorhergesagten Verglichen. Dabei wurde eine Genauigkeit von etwa 5% festgestellt. 3.2.3 Zusammenfassung In diesen Abschnitt wurden unterschiedliche Arbeiten vorgestellt, die sich mit dem Zusammenhang zwischen Software und Energieverbrauch beschäftigten. Die Verfahren zur Ermittlung des Energieverbrauchs lassen sich dabei in Si- 44 KAPITEL 3. VERWANDTE ARBEITEN UND LITERATUR mulation und Messung unterscheiden. In den Untersuchungen konnte nachgewiesen werden, dass die Ressourcennutzung von Software einen großen Einfluss auf den Gesamtverbrauch an Energie nehmen kann. Weitere Arbeiten, wie beispielsweise [Gar07, Sax10], setzen den Fokus darauf, welches Verhalten bei Software zur Verschwendung von Energie des Systems führen kann. In beiden Arbeiten wird beispielsweise Polling (das periodische Abfragen von Zuständen) als negatives Kriterium genannt, da es die Hardwarekomponenten davon abhält, in einen niedrigeren Energiezustand zu wechseln. Die vorgestellten Arbeiten zur Ermittlung der Energiecharakteristik von Betriebssystem basierten beide auf einen Simulationsansatz. Zusammenfassen lieferten sie folgende Erkenntnisse. • Es ist prinzipiell möglich, den Energieverbrauch auf Ebene einzelner Systemdienste hinreichend genau zu bestimmen. • Ein Makromodell kann dazu verwendet werden, das Energieverhalten eines komplexen Systems mit ausreichender Genauigkeit zu beschreiben. • Um den genauen Energieverbrauch zu bestimmen, sind spezialisierte Simulatoren für spezifische Hardware und Betriebssysteme nötig. • Es besteht eine nachgewiesene Korrelation zwischen Performance und Energieverbrauch. Diese kann ausgenutzt werden, um die Modelle akkurater zu gestalten. • Verschiedene Anwendungen verwenden oft nur eine Untermenge aller zur Verfügung stehenden Systemaufrufe, und zudem einige davon recht häufig. Dadurch entsteht eine individuelle Energiecharakteristik. Kapitel 4 Java und die Ressourcennutzung An dieser Stelle erscheint eine Wiederholung des Leitmotivs der Arbeit angebracht: Der Energieverbrauch eines Systems ist, in nicht vernachlässigbaren Maße davon abhängig, ob und in welchem Umfang Hardwarekomponenten von Software benutzt werden. Der Nachweis der Richtigkeit dieser These erfolgte in Abschnitt 3.2 unter Berufung auf Untersuchungen, die im Rahmen anderer Arbeiten durchgeführt wurden. Im Sinne der Überschrift dieses Kapitels können wir den Zusammenhang auch kürzer formulieren: Das Verhalten von Softwareartefakten bezüglich ihrer Ressourcennutzung ist von entscheidender Bedeutung für den Energieverbrauch. Das letztendliche Ziel ist ein generatives System, das auf Grundlage von Beobachtungen am realen System, die Beziehung zwischen Ein- und Ausgangsdaten nachbildet. In unserem speziellen Fall sind die Ausgangsdaten die Daten über die Ressourcennutzung, für die ein Zusammenhang mit Codeartefakten hergestellt werden muss. Um zu sinnvollen Daten über das reale System zu kommen, muss dessen Funktionsweise zunächst verstanden werden. Die Grundlagen dafür schafft dieses Kapitel. Das Kapitel besteht aus zwei Teilen: Beschreibung der Systemumgebung in Abschnitt 4.1 und Möglichkeiten der Datenerhebung in Abschnitt 4.2. Im ersten Teil werden wichtige Eigenschaften eine Computersystems, von Hardware bis zum Java-Quellcode, behandelt, die bei der Datenerhebung von Bedeutung sind. Um sich ein Bild von der tatsächlichen Ressourcennutzung zu machen, muss der Entwickler Kenntnisse dieser Eigenschaften besitzen. Diese Arbeit stellt die Methodiken und Werkzeuge zur Verfügung, um an die benötigten Daten zu kommen und sie zu verarbeiten, kann aber nicht das Denken für den Entwickler übernehmen. Zieht er beispielsweise ein zeitgebundene Metrik zur Bestimmung der CPU-Nutzung heran, sollte auch der aktuel45 46 KAPITEL 4. JAVA UND DIE RESSOURCENNUTZUNG le Performancezustand des Prozessors beachtet werden. Der zweite Teil beschäftigt sich zunächst mit grundlegenden Möglichkeiten der Datenerhebung. Sie bilden die Grundlage für die JRUPI und geben ebenfalls dem Entwickler wichtige Hinweise. Insgesamt betrachtet ist das Kapitel ist eine Kombination aus Hintergrundinformationen, Literaturanalyse, Definitionen und Best Practices, die aus zahlreichen Quellen zusammengetragen wurden. Es formt einen zentralen Teil, da alle Konzept- und Entwurfsentscheidungen der vorliegenden Arbeiten diesem Kapitel aufbauen. 4.1 Beschreibung der Systemumgebung Die Analyse der Abhängigkeiten zwischen Java-Codeartefakten und Ressourcennutzung ist ein wichtiger Bestandteil dieser Arbeit. Dabei wird unter dem Begriff Codeartefakt folgendes verstanden: • Was? Komponenten, Klassen bzw Objekte und Methoden • Wie? vorliegend in Quell- oder Bytecode • Wann? statisch oder dynamisch (zur Laufzeit) Zwischen den beschriebenen Codeartefakten in Java, und einer physischen Ressourcennutzung liegen mehrere Ebenen, die auf verschiedene Weise Einfluss nehmen. Allgemein wird im Folgenden von der Systemumgebung gesprochen. Neben den Einflüssen, die sie ausübt, wird sie auch zur Erhebung der erforderlichen Daten benötigt. Daher ist eine gewisse Kenntnis über die Funktionsweise der einzelnen Ebenen notwendig. Die Beschreibung wichtiger Eigenschaften und Bestandteile ist die Aufgabe dieses Abschnitts. 4.1.1 Übersicht Eine Besonderheit bei der Analyse von Java-Codeartefakten stellt zweifelsohne die Tatsache dar, dass der Quellcode nicht direkt in ein plattformspezifisches Format kompiliert wird. Stattdessen wird der Quelltext vom Compiler in die kompakte und plattformunabhängige Bytecodedarstellung übersetzt, die anschließend von einer geeigneten Laufzeitumgebung ausgeführt werden kann. Im Gegensatz zum Bytecode ist die Laufzeitumgebung, Java Virtual Machine (JVM) genannt, plattformspezifisch [MH10]. Dank der Offenlegung der JVM-Spezifikation [LY99] existieren auch verschiedene Implementierungen der JVM von Drittanbietern. Da die Ausführung des Bytecodes 47 4.1. BESCHREIBUNG DER SYSTEMUMGEBUNG komplett in der Verantwortung der JVM liegt, stellt sie eine weitere Abstraktionsebene dar. In Abbildung 4.1 sind die Ebenen der Systemumgebung, wie sie hier betrachtet werden, schematisch dargestellt. Java Anwendung Java Virtual Machine Betriebssystem Hardware Ziel Aufbau Abbildung 4.1: Schematische Darstellung des betrachteten Gesamtsystems Aus der Grafik sind folgende Schichten zu erkennen: Hardware, Betriebssystem, Java Virtual Machine (JVM) und schließlich die eigentliche Java-Anwendnung; die Betrachtung weiterer Abstraktionsebenen, wie Midddleware-Systeme und Virtualisierung, stehen nicht im Fokus dieser Arbeit. In der gegebenen Reihenfolge werden die einzelnen Schichten auch in den folgenden Unterabschnitten behandelt (rechts: Aufbau). Die folgenden Ausführungen stellen weder eine Einführung noch eine detaillierte Behandlung der jeweiligen Themen dar. Vielmehr nehmen sie sich einige wichtige Aspekte heraus und erläutern diese beispielhaft. Es sei darauf hingewiesen, dass jede dieser Ebenen sehr variabel ist. Die Herausforderung wird daher sein, ein möglichst ausgewogenes Verhältnis zwischen plattformspezifisch und allgemeingültig zu erreichen. Dabei sind beispielsweise folgende Fragen von Bedeutung: Sollten spezielle Funktionen spezifischer Hardwarekomponenten zur Datenerhebung verwendet werden? Können Daten unabhängig vom Betriebssystem gesammelt werden? Kann die JVM einfach durch eine andere Implementierung ausgetauscht werden? 48 4.1.2 KAPITEL 4. JAVA UND DIE RESSOURCENNUTZUNG Die Hardware Das Ziel dieses Unterabschnitts ist es, einige wichtige Eigenschaften von Hardwarekomponenten bezüglich ihres Energieverhaltens aufzuzeigen.Für die Ziele der vorliegenden Arbeit sind die Details der Hardwarekomponenten weitgehend irrelevant. Es werden auch keine spezifischen Hardwareeigenschaften, wie Hardware Performance Counter (HPC) [UGV08], für die Datenerhebung genutzt, da sie keine einheitliche Schnittstelle besitzen und von Komponente zu Komponente unterschiedlich sind. Würden sie durch den Nachteil des Fehlens einer einheitlichen Schnittstelle für einen allgemeingültigen Ansatz nicht disqualifiziert, so wären sie eine effiziente Datenquelle. Viele Hardwarekomponente bieten inzwischen diverse Betriebsmodi, auch als Energie- oder Performancezustände bezeichnet, in denen sie unterschiedlich viel Energie benötigen, indem sie beispielsweise ihre Performance einschränken. Sie sind ein erster Schritt in Richtung einer allgemeineren Forderung: mehr Proportionalität zwischen Ressourcennutzung und Energieverbrauch. Die Auswirkungen einer verbesserten Proportionalität sind in Abbildung 4.2 dargestellt. Abbildung 4.2: Energieeffizienz bei schlechter Proportionalität (links) und besserer Proportionalität (rechts), aus [BH07] Die Grafik stellt gegenüber, wie sich die Energieeffizienz bei schlechter Proportionalität und besserer Proportionalität in Bezug auf die Ressourcennutzung verändert. Es ist deutlich erkennbar, dass sich eine Verbesserung der Proportionalität direkt in einer Steigerung der Energieeffizienz niederschlägt. Ist keine Proportionalität gegeben, so ergibt sich die größte Energieeffizienz bei einer Auslastung von über 80%. Das liegt aber weit über den typischen Betriebsregionen (10-50% bei Servern) der meisten Rechnersysteme [BH07]. 4.1. BESCHREIBUNG DER SYSTEMUMGEBUNG 49 CPU (Central Processing Unit) Die CPUs oder Prozessoren stellen die Schlüsselressourcen eines jeden Computersystems dar [MMG06]. Dabei reicht das Spektrum von einzelnen Einkernprozessoren (wie beispielsweise in Netbooks oder Smartphones) über einzelne Mehrkernprozessoren (wie beispielsweise in Notebook- und Desktopsystemen) bis zu großen Serversystemen mit einer hohen Anzahl von Mehrkernprozessoren. Zudem können Prozessoren in Verteilten Systemen auch gemeinsam benutzt werden. Allerdings benötigt dennoch jeder Teilnehmer eine eigene, lokale CPU. Ein Beispiel hierfür wären Thin-Clients, die laut [HJMM09] eine Renaissance im Zusammenhang mit Green-IT erleben könnten. Das ACPI (Advanced Configuration and Power Interface) stellt bezüglich Energiemanagement den wichtigsten Industriestandard dar. Nach dessen Spezifikation in [HPIM+ 10] liegt die Hoheit über die Energieverwaltung beim Betriebssystem, während es Aufgabe der Hardwarehersteller ist, die benötigiten Funktionen und Schnittstellen für ihre Komponenten zur Verfügung zu stellen. ACPI selbst kann als zusätzliche Abstraktionsschicht zwischen Betriebssytem und darunter liegender Firm- und Hardware gesehen werden. Allgemein wird als Grundlage für das Energiemanagement ein Zustandsmodell verwendet. Die höchste Abstraktionebene stellen die globalen Energiezustände des Systems (Global System Power States) dar. Der jeweils aktuelle Zustand wird unter Anderem durch die aktuellen Zustände der einzelnen Komponenten bestimmt, die jeweils durch separate Zustandsautomaten beschrieben werden. Dabei kommt der Zustandsbeschreibung der CPU eine besondere Rolle (als Schlüsselressource) zu. In Abbildung 4.3 ist das Zustandsmodell eines Prozessors nach ACPI Definition dargestellt. Der umgebende Zustand G0 gibt an, dass das System am Arbeiten (Working) ist. Innerhalb dieses globalen Zustands kann sich die CPU in verschiedenen Energiezuständen (C0,C1,C2,C3,...,Cn)1 befinden, wobei C0 einem aktiven Zustand, in dem der Prozessor Instruktionen ausführt, entspricht. Die verbleibenden Energiezustände (C1,...,Cn) entsprechen verschiedenen Sleep-Modi (es werden keine Instruktionen zu dieser Zeit ausgeführt), in denen die CPU weniger Enerige verbraucht und geringere Abwärme erzeugt. Der aktive Zustand C0 weist darüber hinaus einige interessante Eigenschaften auf. So sind innerhalb dieses Energiezustands zusätzlich unterschiedliche Performancezustände Px definiert. Der Prozessor führt zwar Instruktionen aus, aber (unter Umständen) mit gedrosselter (Throttling=Drosselung) Performance. Dadurch ergibt sich wiederum ein Einsparpotenzial sowohl bei Energieverbrauch als auch bei erzeugter Abwärme, während die CPU den1 C0 bis C3 sind fest vorgegeben, können aber beliebig bis Cn erweitert werden 50 KAPITEL 4. JAVA UND DIE RESSOURCENNUTZUNG G0 G0 = = Working Working Performance Performance State State Px Px C1 C1 C0 C0 C2 C2 Throttling Throttling C3 C3 Abbildung 4.3: Processor Power States, vereinfacht aus [HPIM+ 10] noch Arbeit verrichtet. Die unterschiedlichen Performancezustände werden durch eine dynamischen Anpassung von Taktrate und Versorgungsspannung (Dynamic Voltage and Frequency Scaling (DVFS) [TWM+ 08] erreicht und müssen softwareseitig verwaltet werden (wie beispielsweise mit [?]). Für das Betriebssystem Linux existiert für Intel-Prozessoren das Werkzeug PowerTOP [Gar07], um Anwendungen zu identifizieren, die den Prozessor davon abhalten, in einen kleineren Energie- beziehungsweise Performancezustand zu wechseln. Von Interesse ist an dieser Stelle die Anzeige der Energie- und Performancezustände selbst. Abbildung 4.4 zeigt die Ausgabe des von PowerTOP für eines der in dieser Arbeit verwendeten Testsysteme. Im vorliegenden Fall wird auf der linken Seite angezeigt, wie lange die CPU innerhalb der vergangenen fünf Minuten anteilig in den verschiedenen Energiezusänden (C0,C1,C2, und C6) verweilte. Auf der rechten Seite werden die Performancezustände, geordnet nach Taktfrequenz, mit prozentualen Anteilen dargestellt. Zum Zeitpunkt der Aufnahme befand sich das System im Batteriebetrieb, wodurch auch eine aggressivere Drosselungsstrategie (Throttling) zur Anwendung kam. Dieser Umstand ermöglicht PowerTOP außerdem eine Abschätzung (basierend auf den gesammelten Daten der vergangenen fünf Minuten) der restlichen Batteriebetriebszeit. 4.1. BESCHREIBUNG DER SYSTEMUMGEBUNG 51 Abbildung 4.4: Ausgabe von Powertop für Intel(R) Core(TM)2 Duo CPU (P7350), Rechner im Batteriebetrieb Weitere Geräte Neben der CPU führt die Verwendung weiterer Hardwarekomponenten zu einem höheren Energieverbrauch des Systems. In der ACPI Spezifikation [HPIM+ 10] werden diese Komponenten einheitlich als Geräte (Devices) behandelt. Für Geräte werden, ähnlich wie zuvor bei den Prozessoren, ebenfalls Energie- (D0,D1,...,Dn) und Performancezustände definiert. Energiezustände können auch bei Geräten durch Zustandsautomaten dargestellt werden (vergleiche Verhaltensschablone und Zustandsautomaten für Hardwareressourcen im CCM, Unterabschnitt 2.2.1). Für Performancezustände werden folgende Beispiele genannt, wobei die Abstufungen bei den einzelnen Geräten jeweils mit der Höhe des Energieverbrauch korrelieren: • Festplatten stellen Stufen mit maximalen Durchsatz bereit. • Flüssigkristallbildschirme (LCD (Liquid Chrystal Display)) können mehrere Helligkeitsstufen zur Verfügung stellen. • Grafikkarten können Performance zwischen 2D und 3D Darstellungsmodus skalieren. Zudem besitzen Computersysteme häufig einen separaten Mikroprozessor, die GPU (graphics processing unit), die auf Berechnungen der grafischen Ausgabe spezialisiert ist. Viele Modelle bieten Möglichkeiten zur Anpassung der Taktfrequenz, ähnlich dem Hauptprozessor. • Audiosysteme mit mehreren Lautstärkestufen. • Arbeitsspeicher-Controller, die mehrere Stufen an Speicherdurchsatz durch Drosselung (Throttle) der maximalen Bandbreite bereitstellt. 52 KAPITEL 4. JAVA UND DIE RESSOURCENNUTZUNG All diese Maßnahmen zielen darauf ab, mehr Proportionalität zwischen der Ressourcennutzung und dem Energieverbrauch zu erreichen. Allerdings sind sie (noch) nicht bei allen Geräten optimal umgesetzt. Aufgrund der breiten Akzeptanz der ACPI-Spezifikation und der zunehmenden Notwendigkeit des Energiesparens kann aber zukünftig von weiteren Verbesserungen ausgegangen werden. Sind auf Hardwareseite die Voraussetzungen durch verschiedene Energiezustände erfüllt, und bietet die Software Möglichkeiten zur effektiven Steuerung, so steigt die Energieeffizienz. In diesem Zusammenhang nimmt der Hauptprozessor bisher eine Vorreiterrolle ein. Da er typischerweise unter den Komponenten den höchsten Energieverbrauch aufwies, war er auch Hauptgegenstand zahlreicher Optimierungen. Dank Techniken wie DVFS und breiter Unterstützung durch Betriebssysteme nimmt er heute nicht mehr zwangsweise diese Rolle ein [BH07]. 4.1.3 Das Betriebssystem Das Betriebssystem ist eine Softwareschicht, die vorhandene Geräte verwaltet und Benutzerprogrammen eine einfache Schnittstelle zur Hardware bietet. Nach [Tan07] kann das Betriebssystem aus zwei Blickwinkeln betrachtet werden: • Top-down-Sicht Das Betriebssystem stellt den Benutzer eine geeignete Schnittstelle zur Hardware zur Verfügung. In dieser Sichtweise ist es eine erweiterte beziehungsweise virtuelle Maschine, die leichter zu programmieren ist als die darunterliegende Hardware. Das Betriebssystem versteckt also die Komplexität der Hardware vor dem Programmierer. • Bottom-up-Sicht Bei dieser Sichtweise ist es die Aufgabe des Betriebssystems, eine geordnete und kontrollierte Zuteilung (Allokation) der Hardwareressourcen an die darum konkurrierenden Prozesse zu gewährleisten. Daher ist das Betriebssystem ein Ressourcenmanager. Bereits während der ersten Recherchen zu dieser Arbeit wurde klar, dass zur Analyse der Ressourcennutzung einer Anwendung kein Weg am Betriebssystem vorbei führt. Das liegt in der einzigartigen Aufgabe, die es erfüllt, begründet. In gewisser Weise kann es als Vermittler zwischen Hardware und restlicher Software gesehen werden. Im Allgemeinen besitzen Benutzerprogramme keine Informationen über spezifische Hardwarekomponenten, sondern verwenden die zentralen Abstraktionen, die ihnen das Betriebssystem zur Verfügung stellt. Um konkrete Informationen zur Ressourcennutzung zu erhalten ist es daher notwendig, sich an das Betriebssystem zu wenden. Abbildung 4.5 visualisiert den Zusammenhang. 53 4.1. BESCHREIBUNG DER SYSTEMUMGEBUNG BenutzerBenutzerprogramme programme BetriebsBetriebssystem system HardwareHardwarekomponenten komponenten Abbildung 4.5: Das Betriebssystem zwischen Hardware und Software Bei der Analyse der Ressourcennutzung einer Anwendung sind gerade die Daten zu den konkreten Hardwarekomponenten interessant, vor deren Komplexität das Betriebssystem abstrahiert. Die Wahl des Betriebssystems Da die benötigten Informationen typischerweise nur vom Betriebssystem bezogen werden können, ist für die spätere Proof-of-Concept Implementierung eine Festlegung auf ein spezifisches System unumgänglich. Für diese Arbeit wurde, aus den folgenden Gründen, eine strategische Entscheidung für Linux getroffen: • Linux besitzt mit SystemTap [BPBS10] eine Umsetzung des dynamischen Tracing, das für die Implementierung von besonderer Bedeutung ist. Es ist ein Pandon zu Konkurrenz und Vorreiter DTrace [MMG06], welches für die Systeme Solaris, BSD und OSX verfügbar ist. Eine vergleichbare Alternative für Windows ist nicht bekannt. • Unter den verbleibenden Systemen (dynamisches Tracing ist ein Ausschlusskriterium) genießt Linux zweifelsfrei die größte Verbreitung und Vielseitigkeit. So ist es in verschiedensten Varianten auf Geräten von Smartphones, Laptops, Servern bis hin zu Supercomputern zu Hause. Daher könnte es auch, im größeren Zusammenhang mit CoolSoftware, als einheitliche Grundlage für die Optimierung ganzer Infrastrukturen dienen. • Für Linux steht der komplette Quellcode des Systems zur Verfügung. Das sorgt für eine Transparenz der inneren Funktionsweise, die vor allem in Kombination mit dynamischen Tracing-Verfahren sehr mächtig ist. Ist das System hingegen eine „proprietäre Blackbox“ (Formulierung von Bryan Cantrill), schränkt das die verfügbaren Möglichkeiten erheblich ein. SystemTap wird kontinuierlich weiterentwickelt und hat mittlerweile den Sprung in die Repositories aller größeren Distributionen geschafft, so dass es 54 KAPITEL 4. JAVA UND DIE RESSOURCENNUTZUNG meist relativ unkompliziert über die Paketverwaltung nachgerüstet werden kann. Als Entwicklungssystem kam in der vorliegenden Arbeit überwiegend Fedora 13 (mit Kernel 2.6.34.7-66.fc13.i686) zum Einsatz. Der Rest dieses Unterabschnitts widmet sich einigen wichtigen Eigenschaften von Betriebssystemen und ist dabei weitgehend auf Linux ausgerichtet. Die erläuterten Informationen stammen vorrangig aus [Tan07], [Lov10], [BC00] und [ET10], sowie allgemeinen Beobachtungen in der Praxis bei der Implementierung. Grundlegende Konzepte Es liegt in der Natur von Betriebssystemen, dass sie sehr komplex sind. Daher wäre es ein sinnloses Unterfangen, die genaue Funktionsweise in einem kurzen Abschnitt erläutern zu wollen. Stattdessen werden grundlegende Konzepte anhand einiger Schlagworte erläutert. Sie wurden gezielt ausgewählt, weil sie im Zusammenhang mit der Datenerhebung beziehungsweise der Funktionsweise von SystemTap von Bedeutung sind. Kern- und Benutzermodus: Die Unterscheidung zwischen Kern- und Benutzermodus ist hardwareseitig durch den Prozessor umgesetzt und dient dem Schutz des Betriebssystems. Alle Benutzerprogramme werden gewöhnlich im Benutzermodus ausgeführt und können nur über eine wohldefinierte Schnittstelle (Systemaufrufe) Kerneldienste in Anspruch nehmen. Wird solch ein Dienst aufgerufen, geht die Kontrolle an das Betriebssystem über, das den Dienst im Auftrag des Benutzerprogramms im Kernmodus ausführt, und das Ergebnis anschließend zurück liefert. Nachdem die Anfrage abgearbeitet wurde, schaltet der Kernel die Hardware wieder in den Benutzermodus zurück. Monolithischer Kernel: Unix-ähnliche Betriebssysteme wie Linux verfügen typischerweise über einen monolithischen Kernel. Das bedeutet, dass der komplette Kern als ein einziger, großer Prozess implementiert ist, der beim Systemstart in den Arbeitsspeicher geladen wird. Als Konsequenz verwendet der komplette Kern einen einzelnen Adressraum, wodurch die Kommunikation innerhalb des Kerns trivial ist (alles kann alles aufrufen). Zudem können alle Kernelfunktionen im Kernmodus ausgeführt werden, was häufige Kontextwechsel erspart. Den Gegenentwurf stellt der sogenannte Microkernel (z.B. Windows XP, Vista, 7) dar, bei dem die Funktionalitäten des Kerns in mehrere getrennte Prozesse aufgespalten wird, was wiederum den Kommunikationsaufwand (message passing) erhöht. Jeder Prozess besitzt dementsprechend einen eigenen Adressraum und kann unter Umständen im nicht-privilegierten Benut- 4.1. BESCHREIBUNG DER SYSTEMUMGEBUNG 55 zermodus ausgeführt werden. Durch die Modularisierung vereinfacht sich die Struktur und der Kernel selbst kann relativ klein gehalten werden. Kernelmodule: Monolithische Kernel sind nicht unumstritten, da dieses Design wenig Struktur besitzt. Linux schafft durch sogenannte Kernelmodule Abhilfe, die den Vorteil von Microkerneln auch im monolithischen Linuxkernel erlauben, ohne dass die Performance darunter leitet. Ein Kernelmodul ist einen Objektdatei, die eine Menge Funktionen beinhaltet, und dynamisch zur Laufzeit in den Kernel geladen werden kann. Kernelmodule arbeiten gewöhnlich auf den oberen Schichten des Kerns und werden beispielsweise für die Implementierung von Gerätetreibern oder Dateisystemen genutzt. Im Gegensatz zu Microkerneln werden Module nicht als spezieller Prozess ausgeführt, sondern laufen vollständig im privilegierten Kernmodus und können daher (ohne zusätzlichen Kommunikationsaufwand) wie statisch-verlinkte Funktionen ausgeführt werden. Dieser Ansatz versucht also das Beste aus beiden Welten zu vereinen. Das Konzept der dynamisch ladbaren Kernelmodule ist vor allem im Zusammenhang mit der Funktionsweise dynamischer Tracingverfahren von Bedeutung. Prozesse: Prozesse sind fundamentale Abstraktion innerhalb eines jeden Betriebssystems, da sie von einem laufenden Programm abstrahieren. Häufig wird auch von der Instanz eines Programms in der Ausführung oder dem Ausführungskontext eines Programms gesprochen. Eine effektive Prozessverwaltung ist eine der wichtigsten Aufgaben, die ein Betriebssystem erfüllt. Aus Sicht eines Prozesses hat er die alleinige Herrschaft über alle Ressourcen, das heißt er kann alle Ressourcen verwenden, mit denen der Linuxkernel umgehen kann. Zur Verwaltung aller Prozesse verwendet Linux eine zentrale Datenstruktur (task_struct), die auch als Prozessdeskriptor bezeichnet wird. Jeder Prozess wird durch diese Struktur repräsentiert, die alle wichtigen Informationen über die Prozessstruktur beinhaltet: Prozess-ID (pid), Adressraum, aktueller Prozesszustand, geöffnete Dateien sowie Informationen zu den Threads, um nur einige Beispiele zu nennen. Die möglichen Zustände, in denen sich ein Prozess befinden kann, sind in Abbildung 4.6 dargestellt. Jeder individuelle Prozess besitzt einen eigenen Zustand, der sich während seiner Ausführung ändert. Möglich Zustände sind [ET10]: • Task_Running Hier handelt es sich genau genommen um zwei Zustände, deren Übergänge zwei weitere wichtige Eigenschaften verdeutlichen. Der Prozess ist entweder bereit (links) zur Ausführung (liegt auf der run queue) oder wird gerade von einem Prozessor ausgeführt (laufend; rechts). 56 KAPITEL 4. JAVA UND DIE RESSOURCENNUTZUNG existierende existierendeTask Task ruft ruftfork() fork()auf auf und erstellt und erstellt neuen neuenProzess Prozess Task Taskist istbeendet beendet Task spaltet sich Task von Scheduler ausgewählt TASK_RUNNING TASK_RUNNING TASK_RUNNING TASK_RUNNING (bereit) (bereit) (laufend) (laufend) Task mit höherer Priorität Ereignis taucht auf und Task wird aufgeweckt und zurück auf die Warteschlange gesetzt TASK_INTERRUPTIBLE TASK_INTERRUPTIBLE oder oder TASK_UNINTERRUPTIBLE TASK_UNINTERRUPTIBLE (wartend) (wartend) Task schläft und wartet auf gewisses Ereignis Abbildung 4.6: Prozesszustände in Linux , nach [Lov10] • Task_Interruptible In diesem Zustand ist der Prozess suspendiert und wartet auf ein bestimmtes Ereignis. Ein typisches Beispiel hierfür ist das Warten auf einen Interrupt durch die Tastatur (Empfang eines Signals). • Task_Uninterruptible Dieser Zustand ist ähnlich dem Task_Interruptible Zustand, jedoch reagiert der Prozess nicht auf Signale. Ein typisches Beispiel hierfür ist das Warten auf Festplatten E/A. Die Übergänge zwischen den Zuständen bereit und laufend führen uns zu einer weiteren Kernkomponente des Betriebssystems: dem (CPU) Scheduler. Dessen Aufgabe besteht in der Koordination zwischen den rechnenden Ressourcen (Prozessoren) und den zu berechnenden Tasks (Prozesse und Threads). Es liegt also in seiner Verantwortung, wie die auszuführenden Prozesse auf die Prozessoren verteilt werden. Der aktuelle Algorithmus zur Zuteilung eines Prozesses, den Linux verwendet, besitzt eine Komplexität von O(1). Das heißt, er verhält sich konstant, egal wie viele Prozesse verwaltet 4.1. BESCHREIBUNG DER SYSTEMUMGEBUNG 57 werden müssen [ET10]. Hat der Scheduler den nächsten Prozess für eine CPU bestimmt, so wechselt dessen Zustand von bereit zu laufend. In umgekehrter Richtung ist es aber auch möglich, z.B. wenn ein Prozess höherer Priorität wartet, einen laufenden Prozess vom Prozessor zurück auf die Warteschlange zu legen. Dieses Vorgehen wird als Präemption bezeichnet und ist selbst dann möglich, wenn Prozesse im privilegierten Kernmodus arbeiten (häufig auch als präemptiver Kernel bezeichnet). Präemptive Betriebssysteme verwenden für das Scheduling oft einen Zeitscheibenansatz. Dabei bekommt der laufende Prozess eine Zeitscheibe zugewiesen und das Scheduling zum nächsten wartenden Prozess wird nach Ablauf der Zeitspanne erzwungen. Traditionell wurde dafür eine feste Dauer (z.B. 10ms) angenommen, was zur Folge hatte, dass hundert mal pro Sekunde ein Interrupt zur Durchsetzung des Scheduling feuerte (fixed tick). Ab Version 2.6.21 des Linux Kernels arbeitet der Scheduler nicht mehr statisch, sondern überwacht eine Liste von Tasks die darauf warten, in den Zustand laufend zu wechseln. Befindet sich das System im Zustand idle, so hält es Ausschau nach dem nächsten erwarteten Ablauf eines Timers (einer Anwendung) und programmiert den Timer-Interrupt, zu diesem Zeitpunkt zu feuern (tickless kernel) [Gar07]. Das hat vor allem Auswirkungen in Bezug auf die Energieverwaltung: Da die CPU nicht periodisch Interrupts bearbeiten muss, kann das System (vor allem die CPU selbst) länger in niedrigen Engergiezuständen verweilen, wenn es tatsächlich nichts zu tun gibt. Threads: Ein Thread (Ausführungsfaden) ist eine Ausführungseinheit innerhalb eines Prozesses und läuft parallel zu anderen Threads, die zum selben Prozess gehören. Threads unterscheiden sich hauptsächlich in dem Punkt von Prozessen, dass sie untereinander dieselben Ressourcen teilen und auf demselben Adressraum arbeiten. In Linux sind Threads auf eine spezielle Weise implementiert: aufgrund der weitgehenden Ähnlichkeit zu Prozessen werden sie auch wie jeder Standardprozess umgesetzt. Sie besitzen, im Gegensatz zu anderen Betriebssystemen wie Solaris oder Windows, weder eine eigene Scheduling-Semantik noch spezielle Datenstrukturen. Oft werden Threads auch als Light Weight Process (LWP) bezeichnet und haben, in Sachen Performance, den Vorteil bei der Erstellung, dass Ressourcen nicht kopiert werden müssen. Durch Threads wird ein quasi-paralleler Ablauf bei einem Prozessor beziehungsweise echte Parallelität bei mehreren Prozessoren ermöglicht. Alle Ausführungen zum Scheduling von Prozessen besitzen (bei Linux) daher auch Geltung für Threads. 58 KAPITEL 4. JAVA UND DIE RESSOURCENNUTZUNG 4.1.4 Die Java Virtual Machine (JVM) „Java is a lovely multi-purpose hammer, but not every problem is a nail!“ [MH10] Laut einer aktuellen Umfrage2 ist Java die verbreitetste Programmiersprache bei IT-Dienstleistern. Einer der Hauptgründe für den Erfolg ist in der Plattformunabhängigkeit von Java-Anwendungen zu sehen, die durch zwei Konzepte ermöglicht wird: • Der Quellcode wird zunächst in eine andere Repräsentation übersetzt: Den kompakten und plattformunabhängigen Bytecode. • Der Bytecode wird anschließend von einer plattformspezifischen JVM augeführt. Damit wird das Portabilitätsproblem in die JVM ausgelagert. Die Anwendung selbst kann daher auf jeder Plattform, für die eine geeignete JVM existiert, ausgeführt werden. Da die JVM die komplette Ausführung überwacht, steuert und für den finalen Übergang zum nativen Code zuständig ist, kann die Bytecoderepräsentation eines Programms ohne erneute Kompilierung auf verschiedensten Architekturen ausgeführt werden. In der vorliegenden Arbeit wird die JVM als zweite wichtige Abstraktionsebene betrachtet und soll daher im Folgenden erläutert werden. Für detailliertere Erklärungen sei das Buch [MH10], das alle wichtigen Konzepte abdeckt, und auch als Grundlage für diesen Unterabschnitt dient, empfohlen. Neben Details zur JRockit Implementierung, einer der wichtigsten JVMs im Serverbereich, deckt es auch allgemeine Eigenschaften von JVMs sehr gut ab. Eine weitere wichtige Informationsquelle ist selbstverständlich die JVM Spezifikation [LY99]. Bytecodeformat Bytecode klingt zunächst nach low-level, ist aber lediglich eine andere Repräsentationsform, die man sich als serialisierten Quellcode vorstellen kann [MH10]. Er ist eine kompakte und portable Form einer strukturierten Programmbeschreibung. In Listing 4.1 und Listing 4.2 sind der Quell- und der Bytecode einer Methode gegenübergestellt. Die Bytecode-Repräsentation einer Methode bildet der sogenannte Frame, der aus zwei Teilen besteht: Zum Einen beinhaltet er eine Menge indizierter Speicherplätze (Slots) für lokale Varibalen, zum Anderen den Operandenstack. Letzterer besteht aus Werten, 2 Quelle: www.heise.de, Titel: Umfrage: Java ist verbreitetste Programmiersprache bei IT-Dienstleistern, 25.02.2011 4.1. BESCHREIBUNG DER SYSTEMUMGEBUNG 59 die als Operanden für diverse Bytecode-Instruktionen dienen und nach dem LIFO (last in first out) Prinzip abgerufen werden. Die JVM arbeitet also wie ein Kellerautomat. Bytecode-Instruktionen dienen entweder dem Transfer zwischen lokalen Variablen und Operandenstack (ILOAD,ISTORE) oder arbeiten auf dem Operandenstack (IADD, IRETURN). 74 75 76 77 78 79 1 /* * * anderer Code * */ 80 81 82 83 84 2 3 4 5 6 7 public int add ( int x , int y ) { int z = x + y ; return z ; } Listing 4.1: Quellcode 8 9 10 11 public add ( II ) I L0 LINENUMBER 82 L0 ILOAD 1 ILOAD 2 IADD ISTORE 3 L1 LINENUMBER 83 L1 ILOAD 3 IRETURN Listing 4.2: Bytecode Die beiden Parameter der Methode add werden zunächst in die Slot 1 und Slot 2 als lokale Variablen gespeichert. In Zeile 4 und Zeile 5 werden die beiden lokalen Variablen x und y auf den Operandenstack geladen ILOAD, anschließend addiert (IADD - Operation kombiniert die Operanden und legt das Ergebnis zurück auf den Stack) und das Ergebnis in die lokale Variable z (Slot 3) geschrieben (ISTORE). Dieser Vorgang entspricht dem Statement in Zeile 82 des Quellcodes. Zur Umsetzung der Rückgabeanweisung in Zeile 83 wird zunächst die Variable z auf den Stack geladen (Zeile 10) und anschließend zurückgegeben (Zeile 11). Die Instruktion IRETURN nimmt dabei die Variable z vom Stack und terminiert die Ausführung der Methode. Jede Bytecode-Instruktion besteht aus einem Opcode, einer festen Anzahl Argumenten zur Festlegung des Instruktionsverhaltens, und einer optionalen Menge von Operanden variabler Länge. Ein Opcode wird durch eine mnemonisches Symbol (wie IADD) identifiziert und besitzt den Wert eines Bytes (daher existieren maximal 256 Opcodes). Das verdeutlicht die Kompaktheit des Bytecodeformats, da pro Instruktion typischerweise lediglich zwischen ein und drei Bytes benötigt werden [MH10]. Der Bytecode zu einer Java-Klasse kann mit dem Konsolenwerkzeug javap, das Bestandteil des JDK (Java Development Kit) ist, in Kombination mit dem Parameter -c ausgegeben werden. Einen komfortableren Vergleich ermöglicht beispielsweise die Bytecode View für Eclipse, die auch für das Beispiel in Listing 4.2 verwendet wurde. 60 KAPITEL 4. JAVA UND DIE RESSOURCENNUTZUNG Arbeitsweisen von JVMs Die beschriebene Bytecode-Repräsentation ist plattformunabhängig und kann daher von der physischen CPU nicht ausgeführt werden. Es liegt in der Verantwortung der JVM den Bytecode in nativen Code umzuwandeln, der spezifisch für eine bestimmte Hardwarearchitektur (z.B. x86 Assembler oder Maschinencode) ist. Zur Umsetzung bieten sich prinzipiell zwei Möglichkeiten an [MH10]: • Interpretation In der Spezifikation [LY99] ist die JVM komplett als Zustandsautomat beschrieben, der den gesamten Ausführungsstatus eines Java-Programms emuliert. Der Bytecode wird also vollständig interpretiert, wodurch eine Übersetzung in nativen Code überflüssig wird. • Just-In-Time Compilation (JIT) Mit JIT wird der Vorgang bezeichnet, Codeartefakte (typischerweise Methoden) in nativen Code zu übersetzen, kurz bevor sie ausgeführt werden. Die JVM kompiliert den auszuführenden Bytecode in nativen Code für die jeweilige Plattform und ruft ihn anschließend auf. Frühe Implementierungen der JVM waren meist reine Bytecode-Interpreter. Das machte die Portierung der JVM auf eine andere Plattform relativ simpel, da der Code-Generator der JVM lediglich neu kompiliert werden musste. Der große Nachteil bestand in der bescheidenen Performance, die der Interpretation geschuldet ist, und noch heute der Programmiersprache Java nachgesagt wird. Dabei hat sich die Situation entscheidend geändert, da immer ausgefeiltere Codegenerierungsstrategien entwickelt wurden: • statische Kompilierung Der komplette Bytecode wird direkt in nativen Code übersetzt. Diese Vorgehensweise bringt eine große Performancesteigerung, führt aber zum Verlust fast aller anderen Vorteile, die Java bietet: Plattformunabhängigkeit, automatisches Speichermanagement und die dynamischen Eigenschaften (wie Codeersetzung zur Laufzeit) sind nicht mehr gegeben. • vollständige JIT-Kompilierung Diesen Weg hat die JRockit Implementierung der JVM eingeschlagen: jede angetroffene Methode wird direkt mit dem JIT-Verfahren kompiliert. Damit wird, im Gegensatz zur statischen Kompilierung, die Kompilierzeit zum Laufzeitfaktor. Der größte Nachteil dieser Methode liegt in der langsamen Codegenerierung, die daher der Hauptgegenstand von Optimierungen ist. Dazu werden die Methoden bei der Ausführung bewertet und in die Kategorien hot, 4.1. BESCHREIBUNG DER SYSTEMUMGEBUNG 61 für häufig verwendete Methoden mit hohem Einfluss auf die Performance, und cold, für selten verwendete Methoden mit geringem Einfluss auf die Performance, eingeteilt. Zu Beginn der Ausführung kommt für alle Methoden eine schnelle, aber dafür wenig optimierte, Kompilierung (quick and dirty) zur Anwendung, um herauszufinden, welche Methoden hot beziehungsweise cold sind. Die Bewertungen der Methoden werden im Laufe der Ausführung immer weiter verfeinert. Ab einem gewissen Schwellenwert wird die Qualität der Kompilierung (die Implementierung kennt des JIT-Compiler kennt verschiedene Qualitätsstufen) gesteigert und dadurch native Code mit höherer Optimierung erzeugt. Da eine erneute Kompilierung zunächst zu einer Erhöhung der Gesamtzeit führt, ist es wichtig, nur bei Methoden zu optimieren, die den Aufwand durch häufige Aufrufe mit kürzerer Ausführungszeit rechtfertigen. • Mixed mode interpretation In dieser Mischform zwischen Interpretation und JIT-Verfahren werden zu Beginn alle Methoden interpretiert. Sobald eine Methode als hot bewertet wird, erfolgt eine JITKompilierung in effizienteren nativen Code. Methoden die als cold eingestuft wurden, werden weiterhin interpretiert. Auch bei diesem Verfahren ist der Bewertungsalgorithmus von entscheidender Bedeutung. Mit einer mixed mode interpretation arbeitet beispielsweise der HotSpot-Compiler, der in der Sun JVM zum Einsatz kommt. In der Praxis sind heute nur die letzten beiden Verfahren von Bedeutung. Ihr Vorgehen wird auch als adaptive Codegenerierung bezeichnet, da eine schrittweise Optimierung zur Laufzeit stattfindet. Aufgrund der dynamischen Natur von Java sind diese Ansätze mit einem erhöhten Verwaltungsaufwand für den generierten Code (bookkeeping) verbunden, da nativer Code durch Änderungen in der Umgebeung (adaptive Runtime) eventuell nicht mehr valide ist [MH10]. Die vorgestellten Verfahren sind für die Datenerhebung und für das Aufsetzen von Benchmarks relevant und werden in Abschnitt 4.2 wieder aufgegriffen. Weitere wichtige Eingenschaften JVMs, die mit den beschriebenen Verfahren arbeiten, sind überaus komplexe Softwaresysteme, deren Konzepte nicht in einen einzelnen Unterabschnitt zusammengefasst werden können. An dieser Stelle werden daher einige Aspekte angesprochen, die für die vorliegende Arbeit relevant sind. 62 KAPITEL 4. JAVA UND DIE RESSOURCENNUTZUNG Threads Java-Code wird in Threads ausgeführt. Auch hier besteht das Ziel in einer quasi-parallelen Ausführung beziehungsweise echten Parallelität mehrerer Aktivitäten. Intern besitzen Java-Threads folgenden Aufbau: jeder Thread besitzt einen Ausführungsstack, der aus Frames besteht, die einen Methodenaufruf repräsentieren. Bei jedem Aufruf einer Methode wird der neue Frame auf den Stack gelegt (push) und bei Rückkehr wieder herunter genommen (pop), wodurch die Ausführung bei der aufrufenden Methode fortgesetzt werden kann. Auch an dieser Stelle greift die JVM das Konzept des Kellerautomaten auf. Zur Synchronisation zwischen Threads werden sogenannte Monitore eingesetzt. Ein weiterer wichtiger Punkt ist die Implementierung der Threads. Früher wurden häufig eigene Thread-Implementierungen (Green Threads) in der JVM verwendet, die innerhalb eines Threads im Betriebssystem ausgeführt wurden. Da dieses Vorgehen aber diverse Nachteile hat (Verwaltungsaufwand, Komplexität, eigener Scheduler, Deadlocks bei nativen Code), und die Semantik sehr ähnlich ist, werden heute üblicherweise BetriebssystemThreads zur Implementierung von java.lang.Thread verwendet. Zumindest alle Server-Implementierungen von JVMs (wie JRockit und HotSpot) verwenden ein 1:1-Mapping [MH10]. Dieser Zusammenhang ist von fundamentaler Bedeutung: bei der Datenerhebung zur Ressourcennutzung auf Betriebssystemebene können Betriebssystem-Threads anstelle von Java-Threads beobachtet werden. Damit ist auch der Scheduler des Betriebssystems dafür verantwortlich, die Ausführung der Java-Threads zu steuern. Deamon-Threads Die JVM benötigt während der Ausführung zur Erfüllung ihrer Aufgaben mehrere Threads, die im Hintergrund laufen (housekeeping). Diese werden, wie es von Unix-ähnlichen Betriebssystemen bekannt ist, als Deamon bezeichnet. So existieren zum Beispiel separate Threads, die sich um Signalverarbeitung (signal handling), Speicherverwaltung (garbage collection (GC)) oder JIT-Kompilierung kümmern. Wird die JVM auf Prozessebene betrachtet, so führen diese Threads zu einer Erhöhung der Ressourcennutzung, die nicht direkt abhängig vom aktuellen Verhalten des gerade ausgeführten Code ist. Dieser Umstand sollte bei Messungen beachtet werden. Garbage Collection (GC) Die JVM besitzt ein automatisches Speichermanagement. Das bedeutet im Wesentlichen, dass sich die Technik der GC automatisch um die Entsorgung nicht mehr benötigter Referenzen kümmert, und damit der entsprechende Speicherplatz wieder freigegeben wird. Der Ent- 4.2. MÖGLICHKEITEN ZUR DATENERHEBUNG 63 wickler muss ich also nicht explizit um die Speicherverwaltung kümmern. In Java ist es die Aufgabe des Garbage Collector, der als Thread im Hintergrund (Deamon) arbeitet, den Speicher freizugeben. Zu welchem Zeitpunkt die GC aktiv wird, ist zum Einen vom Verhalten der Anwendung zur Laufzeit abhängig, zum Anderen aber auch von spezifischen Einstellungen der JVM [MH10]. Für die GC ist es teilweise erforderlich, die Ausführung der Anwendung zu unterbrechen, wodurch gewisse Messergebnisse verfälscht werden können. Zudem bringt die GC zusätzlichen Overhead, der eine Erhöhung der Ressourcennutzung des JVM-Prozesses zur Folge hat. Um das erste Problem zu umgehen bietet es sich an, mehrere Messungen durchzuführen und beispielsweise eine Standardabweichung zu berechnen. Im Zusammenhang mit der höheren Ressourcennutzung des JVM-Prozesses infolge der Ausführung von Hintergrund-Threads bietet es sich an, die Ressourcennutzung auf ThreadEbene zu untersuchen. Eignung von Java Wie das einleitende Zitat verdeutlichte, ist Java für viele Domänen einsetzbar, aber nicht für alle geeignet. Bei Problemen, die direkten Zugriff auf Hardwarekomponenten erfordern, sollte besser zu Alternativen wie C oder C++ gegriffen werden [MH10], da das Konzept der Verwendung einer JVM offensichtlich diesem Anwendungsgebiet widerspricht. Konkret bedeutet das für die vorliegende Arbeit, dass Java kein geeignetes Werkzeug zur Erhebung von Daten über die physische Ressourcennutzung ist. Im größeren Zusammenhang mit CoolSoftware ist Java daher auch nicht zur Implementierung der Ressourcenmanager geeignet. 4.2 Möglichkeiten zur Datenerhebung Der erste Abschnitt dieses Kapitels widmete sich vorrangig der Beschreibung der Systemumgebung, mit besonderen Fokus auf die beiden Abstraktionsebenen Betriebssystem (Linux) und JVM. In der Top-Down-Sicht nach Tanenbaum wird das Betriebssystem als eine virtuelle Maschine gesehen, die von der Komplexität der Hardware abstrahiert. Analog dazu ist die JVM, und hier steckt der Begriff direkt im Namen, eine virtuelle Maschine, die vom Betriebssystem abstrahiert. Während sich die Abstraktionsschichten für den Softwareentwickler im Allgemeinen positiv in Form einer verminderten Komplexität auswirken, erschwert diese doppelte Schichtung die Datenerhebung: die Kluft zwischen der physischen Ressourcennutzung, die auf niedriger Ebene stattfindet, und 64 KAPITEL 4. JAVA UND DIE RESSOURCENNUTZUNG Codeartefakten in Java, die auf höchster Ebene in Quellcode vorliegen, ist sehr breit - und ihre Überbrückung eine der großen Herausforderungen. Dieser Abschnitt führt in einige grundlegende Verfahren ein, die zur Datenerhebung benötigt werden. Des Weiteren existieren viele Werkzeuge, sowohl für Java als auch für Betriebssysteme, die interessante Daten zu Anwendungen liefern. Aus Platzgründen wird hier auf eine konkrete Vorstellung verzichtet und die Werkzeuge lediglich genannt. Für Werkzeuge im Umfeld von Solaris und Linux bieten [MMG06, Sun08, ET10, BPBS10] eine gute Übersicht und Einführung. 4.2.1 Instrumentierung Die Instrumentierung ist ein grundlegendes Verfahren, um Daten über eine Anwendung während der Laufzeit zu erheben, und wird typischerweise bei der Programmanalyse eingesetzt. In vielen Arbeiten wird der Begriff so selbstverständlich in einem gewissen Kontext verwendet, dass eine allgemeine Festlegung meist ausbleibt. Daher wird in dieser Arbeit folgende Definition eingeführt: • Intrumentierung: Das Einbringen zusätzlicher Anweisungen in eine Anwendung, die nicht ihrem eigentlichen Zweck dienen, sondern zusätzliche Informationen über die Anwendung zur Laufzeit liefern. Dabei werden zur Umsetzung der Instrumentierung nicht zwangsweise fortgeschrittene Techniken benötigt. Das simple Einfügen einer println Anweisung, um den Wert einer Variablen zur Laufzeit auszugeben, ist manchmal vollkommen ausreichend [MH10]. In diesem Fall dient die Instrumentierung dem Debugging. Weitere Anwendungsgebiete sind beispielsweise Profiling, Logging, Monitoring oder Tracing 3 . Listing 4.3 zeigt ein Beispiel, wie durch Instrumentierung die Ausführungszeit einer Java-Methode bestimmt wird. public int ggT ( int x , int y ) { long start = System . nanoTime () ; int t ; while ( y > 0) { t = x % y; x = y; y = t; } long end = System . nanoTime () ; Profiler . logMethod ( " ggT " , end - start ) ; return x ; } 1 2 3 4 5 6 7 8 9 10 11 12 Listing 4.3: Instrumentierung zur Ermittlung der Ausführungszeit (Java) 3 siehe auch Javadoc zur Schnittstelle java.lang.instrument 4.2. MÖGLICHKEITEN ZUR DATENERHEBUNG 65 Direkt nach Eintritt in die Methode ggT wird die aktuelle Systemzeit (in Nanosekunden) in die Variable start gespeichert (Zeile 2). Nachdem alle Statements der Methode abgearbeitet (außer return) sind, wird erneut die aktuelle Zeit in die Variable end gespeichert (Zeile 9). Die Ausführungszeit lässt sich nun einfach durch Subtraktion (end-start) ermitteln und kann beispielsweise an eine weitere Komponente weitergegeben werden (Zeile 10). Da die zusätzlichen Anweisungen in den Zeilen 2, 9 und 10 nichts zur eigentlichen Funktionalität (die Berechnung des ggT) beitragen, werden sie als Instrumentationscode bezeichnet. Prinzipiell gibt es verschiedene Möglichkeiten, den Instrumentationscode in die Anwendung einzubringen. Im Fall von Java können die Folgenden Techniken unterschieden werden: • Instrumentierung des Quellcodes: Die simpelste Form der Instrumentierung ist die direkte Modifikation des Quellcodes, entweder durch manuelles oder (semi-automatisches) Einfügen der Anweisungen. Ein Beispiel zeigte bereits Listing 4.3. • Instrumentierung bei Übersetzung: Durch die Verwendung eines speziellen Compilers (oder spezifischer Compileroptionen) können zusätzliche Anweisungen bei der Übersetzung in den resultierenden Bytecode eingepflegt werden. So kann beispielsweise AspectJ, eine Umsetzung des Aspect-Oriented Programming (AOP) [KLM+ 97] Paradigmas für Java, verwendet werden, um zusätzlichen Code an Methodeneingängen und Methodenausgängen zu positionieren. Genau genommen verwendet AspectJ einen sogenannten Weaver, der die beschriebenen Aspekte in den Quellcode „webt“, und die neue Version anschließend vom Compiler übersetzen lässt. • Instrumentierung des Bytecodes: Wie in Abschnitt 4.1.4 gezeigt wurde, ist der Bytecode lediglich eine andere Repräsentationsform von Quellcode. Die nachträgliche Manipulation dieser Repräsentation wird als Bytecode Engineering bezeichnet, worauf sich mehrere Frameworks wie extscJavaAssist[Chi04] BCEL [The06] und ASM[Bru07] spezialisiert haben. • Instrumentierung zur Laufzeit: Die Instrumentierung zur Laufzeit erfordert logischerweise die Manipulation des Bytecodes, da zu diesem Zeitpunkt kein Quellcode mehr verfügbar ist. In Java erfolgt eine derartige Instrumentierung typischerweise, wenn eine Klasse vom Class Loader geladen wird. Inzwischen existiert zu diesem Zweck eine eigene Schnittstelle (java.lang.instrument) bei Java. Die Implementierung, 66 KAPITEL 4. JAVA UND DIE RESSOURCENNUTZUNG in Form eines Agent, liegt in der Verantwortung des Entwicklers, so dass zum Beispiel die erwähnten Frameworks zur Bytecodemanipulation verwendet werden können. 4.2.2 Sampling Das Sampling ist eine weitere Form der Datenerhebung. Im Gegensatz zur Instrumentation wird die Anwendung selbst dabei nicht verändert, sondern Daten periodisch an einer anderen Stelle erhoben. In Unterabschnitt 3.2.2 wurde mit PowerScope [Fli01] bereits ein derartiges Verfahren vorgestellt. Dabei wurde ein Digitalmultimeter (DMM) verwendet, das während der Programmausführung in einem gewissen Takt Energiedaten aufzeichnete. Der Takt wird auch als Samplingrate bezeichnet. Aufgrund der periodischen Aufzeichnung der Daten ist Sampling ein statistisches Verfahren, das eine Annäherung an die Realität darstellt. Da auch beim Sampling in der Literatur keine einheitliche Definition existiert, wird für diese Arbeit eine allgemeine Definition gegeben: • Sampling: Eine periodische Datenerhebung, vorgegeben durch eine Samplingrate, die außerhalb der unveränderten Anwendung stattfindet und statistisch akkurate Daten liefert. Bei der Instrumentation kann es vorkommen, abhängig von Menge und Art der zusätzlich eingebrachten Anweisungen, dass sich das Verhalten der Anwendung verändert (Heisenberg-Effekt), was beim Sampling nicht passieren kann. Dafür gelten Samplingverfahren aufgrund ihrer statistischen Natur als ungenauer. Samplingverfahren können in zwei Kategorien eingeteilt werden [JF04]: • Time Based Sampling (TBS): Dabei wird ein Taktgeber verwendet, der in einem festen Intervall (z.B. 100Hz) beispielsweise den Befehlszähler der CPU ausliest • Event Based Sampling (EBS) Bei dieser Variante wird das Auftreten eines gewissen Ereignisses (z.B. Interrupt) als Auslöser der Datenaufzeichnung herangezogen. Manche Verfahren greifen auf Betriebssystemebene (unter Linux) auf den Timer-Interrupt des Scheduler (system tick) als Taktgeber zurück. In früheren Versionen des Scheduler (fixed tick) entsprach das Vorgehen eher einem TBS, während es sich mit Einführung des (tickless kernel) eher dem EBS zuordnen lässt. 4.2. MÖGLICHKEITEN ZUR DATENERHEBUNG 67 Eine sehr beliebte Datenquelle für das Sampling sind Hardware Performance Counter (HPC) [UGV08], die meist in einer bestimmten Frequenz ausgelesen werden. Das größte Problem bei der Verwendung von HPC ist, dass keine einheitliche Schnittstelle existiert und sie von jeder Hardwarekomponente unterschiedlich (in Anzahl und Art) umgesetzt werden. Daher ist ihre Verwendung nur für spezielle Konfigurationen geeignet. Zwar existieren Bibliotheken, die das Auslesen der Werte für spezielle Hardwarekomponenten erlauben [TW04], sie sind aber nicht immer verfügbar und müssten für jede Hardwarekonfiguration neu zusammengestellt werden. Würden sie durch diese Nachteile nicht disqualifiziert, so wären sie eine optimale Datenquelle zur Ermittlung der Ressourcennutzung. 4.2.3 Profiling Auch für Profiling ist es schwierig, eine allgemeingültige Definition zu finden. Je nach Anwendungsgebiet und Ziel werden die Aufgaben dieser Tätigkeit verschieden beschrieben. Daher wird erneut der Versuch unternommen, eine generische Definition für diese Arbeit zu geben: • Profiling Eine Tätigkeit, bei der Werkzeuge (Profiler) zum Einsatz kommen, die zur Datenerhebung auf die Verfahren Instrumentierung, Sampling oder eine Kombination aus beiden zurückgreifen, um zur Laufzeit gewisse Aspekte einer Anwendung zu untersuchen. Aufbauend auf den vorgestellten Möglichkeiten zur Datenerhebung unterscheidet man bei Profilern im Allgemeinen zwischen zwei Profilingtechniken: • invasiv Ein Profiler gilt als invasiv, wenn er zur Datenerhebung auf Verfahren der Instrumentation zurückgreift. • nicht-invasiv Ein Profiler gilt als invasiv, wenn er Samplingverfahren zur Datenerhebung verwendet. Am häufigsten ist der Begriff Profiling im Zusammenhang mit Performance anzutreffen. So kann ein Profiler als Werkzeug zur Analyse von Performanceund Stabilitätsproblemen gesehen werden [MN07]. In der Tat ist das Gebiet der Performanceanalyse eng verwandt mit der Ressourcennutzung. Daher wird für diese Arbeit eine allgemeinere Defintion angenommen, die zwar auch die Performance umfasst, sich aber nicht auf sie beschränkt. Das Ziel besteht darin, durch Analyse ein Profil der Ressoucennutzung einer Anwendung zu erstellen. Daher ist Profiling das Mittel der Wahl, und das Werkzeug wird als Profiler bezeichnet. 68 KAPITEL 4. JAVA UND DIE RESSOURCENNUTZUNG Java-Profiler Die Performance von Anwendungen ist eine der wichtigsten nicht-funktionalen Anforderungen. Daher existieren für Java zahlreiche Werkzeuge (Profiler) zur Analyse von Performance- und Stabilitätsproblemen. Sie untersuchen das Laufzeitverhalten, Nebenläufigkeit und Speicherverhalten der GC [MN07]. Bekannte Repräsentanten sind der Netbeans Profiler4 , Eclipse TPTP5 , JProfiler6 , JXInsight7 oder das JRockit Mission Control Center [MH10]. • Das Problem: Soweit bekannt ist, arbeiten alle Profiler mit den Möglichkeiten, die die JVM zur Beobachtung einer Anwendung bietet. So wird häufig die JVMTI Schnittstelle verwendet, um eine Bytecodeinstrumentierung zur Laufzeit durchzuführen und beispielsweise Ausführungszeiten zu bestimmen. Die Profiler erheben die Daten also innerhalb der JVM, was zum Zwecke der Performanceanalyse auch ausreichend ist. Das hat aber zur Konsequenz, dass diese Werkzeuge gar nicht die Möglichkeit besitzen, eine physische Ressourcennutzung zu bestimmen. Daher sind Speicher und CPU typischerweise das einzige, was an Ressourcennutzung erinnert. Doch auch hier ist Vorsicht geboten: Speicher bezieht sich auf die interne Speicherverwaltung der JVM und nicht auf den physischen Arbeitsspeicher. Auch CPU Informationen müssen kritisch betrachtet werden. So verwendet das Werkzeug hprof (eine Proof-of-Concept Implementierung von Sun) zur Bestimmung des CPU Nutzungsprofils ein zeitliches Profil, das auf Zeitmessungen der Methoden beruht. Die Methode wird aber innerhalb eines Java-Threads ausgeführt, der wiederum 1:1 auf einen Betriebssystem-Thread gemapped ist. Mit einem präemptiven Scheduler ist die Methode daher nicht immer ihre komplette Ausführungszeit auf der CPU, so dass diese Methodik nur innerhalb der JVM Sinn ergibt, aber wenig über die tatsächliche physische Auslastung der CPU aussagt. Aus diesen Gründen ist es nötig, den Ressourcenverbrauch außerhalb der JVM zu bestimmen. Genauer gesagt, sollte die Ressourcennutzung des JVM Prozesses und seiner Threads analysiert werden. Verhältnis zu Diagnose und Monitoring In der Praxis werden neben Profilern häufig auch Diagnose- und Monitoring-Tools unterschieden [MN07]. 4 http://netbeans.org/features/java/profiler.html http://www.eclipse.org/tptp/ 6 http://www.ej-technologies.com/products/jprofiler/overview.html 7 http://www.jinspired.com/products/jxinsight/ 5 4.2. MÖGLICHKEITEN ZUR DATENERHEBUNG 69 • Zur Diagnose kommen Performance- und Lasttest-Tools zum Einsatz, um festzustellen, ob eine Anwendung Anforderungen an Antwortzeitverhalten, Durchsatz, parallele Nutzer und Stabilität gerecht wird. Die Anwendung wird von außen, dass heißt im Rahmen eine BlackboxTests, untersucht. Der Hauptunterschied besteht darin, dass weniger Details und dafür mehr Systemmetriken mit Einfließen . • Monitoring hingegen entspricht einer Überwachung des Produktivsystems zur Laufzeit. Java Monitoring Tools überwachen dazu spezifische Java Komponenten und integrieren sich häufig in vorhandene Monitoring Infrastrukturen. Im Allgemeinen wird Monitoring über einen längeren Zeitraum (oder ständig) zur Überwachung des Betriebs und Fehleridentifikation eingesetzt. Bezüglich der Überwachung anderer Systemressourcen entsprechen Monitoring und Diagnose eher dem Ziel dieser Arbeit. Allerdings passen ihre Aufgaben nicht mit der geforderten Verbindung zum Quellcode zusammen. Daher, und weil die Konzepte ähnlich sind, wird im Folgenden immer von Profiling gesprochen. Weitere Werkzeuge Für Betriebssysteme existiert eine Vielzahl unterschiedlicher Monitoring-Tools, um die Performance des Systems zu überwachen. Eine gute Übersicht bieten [MMG06] für Solaris und [ET10] für Linux. In dieser Arbeit sind lediglich Letztere von Interesse. Da beide Systeme ihren Ursprung in Unix haben, sind die Werkzeuge meist unter gleichen Namen, aber in verschiedenen Implementierungen vorhanden. Bei Linux stützen sich derartige Werkzeuge hauptsächlich auf das virtuelle proc Dateisystem, dessen Aufgabe nicht die Datenspeicherung ist. Stattdessen bietet es eine zentrale Schnittstelle zum Linux-Kernel, um ihn beobachten und verändern zu können. Der Name Dateisystem stammt daher, dass aktuelle Zustände für den Administrator beziehungsweise für Werkzeuge in Form von Dateien erscheinen, die ganz normal ausgelesen werden können. Werkzeuge wie vmstat oder top [ET10] lesen spezifische Dateien, die von Interesse für die Aufgabe des Werkzeugs sind, ein. Anschließend werden die Informationen gefiltert oder neu berechnet und für eine übersichtliche Ausgabe aufbereitet. Das Einlesen und Weiterverarbeiten der Informationen ist jedoch zeitaufwändig und dementsprechend teuer. Daher eigenen sich diese Werkzeuge besonders, um sich einen schnellen Überblick zu verschaffen oder Langzeitinformationen zu sammeln. [Lev03] 70 KAPITEL 4. JAVA UND DIE RESSOURCENNUTZUNG 4.2.4 Benchmarking Benchmarking ist eine fundamentale Methode zur Messung der Performance einer Anwendung. Da Performance eine überaus wichtige Eigenschaft ist, wird in [MH10] vorgeschlagen, dass Benchmarking während der Entwicklung, parallel zu Regressionstests, stattfinden sollte. Also immer dann, wenn größere Änderungen im Code vorgenommen wurden, soll die Erfüllung der Performanceanforderungen durch Performance Regression Testing validiert werden. Der Aufbau einer Infrastruktur für das Benchmarking dient also der Qualitätssicherung. Um die Performance von Codeartefakten überhaupt messen zu können, müssen sie zunächst zur Ausführung gebracht werden. Im Kontext von Benchmarking wird die Komponente, die den betreffenden Code aufruft, als Treiber bezeichnet. Das kann im einfachsten Fall eine main Methode sein, die die zu untersuchenden Methoden mit Eingabedaten aufruft. Es ist die Aufgabe des Treibers, einen Workload in den Benchmark zu injizieren. Optimal sind zu diesem Zweck Treiber, die extern vorliegen, und somit leicht auch auf anderen Rechnern verwendet werde können. Bei der Erstellung eines Treibers ist darauf zu achten, dass die Erstellung der Testdaten außerhalb der Messungen geschieht. Benchmarking wird an dieser Stelle behandelt, weil auch zur Ermittlung der Ressourcennutzung die Anwendung zur Ausführung gebracht werden muss. Viele der Regeln des Benchmarking besitzen auch beim Profiling der Ressourcennutzung Gültigkeit. Leider ist Erstellung von aussagekräftigen Benchmarks eine komplexe Angelegenheit, bei der viel beachtet werden muss, um nicht fehlerhafte Rückschlüsse zu ziehen. Vor allem beim vermessen von Java-Anwendungen ergeben sich, aufgrund der komplizierten JITKompilierungs- und Optimierungsverfahren, etliche Schwierigkeiten [KA05]. Wie der Benchmark umgesetzt wird, der später für das Profiling der Ressourcennutzung eingesetzt wird, liegt vollständig in der Verantwortung des Entwicklers. Um einzelne Codeartefakte zu untersuchen, bieten sich sogenannte Micro-Benchmarks an. Sie bestehen nur aus einer geringen Menge Code und sind einfach zu schreiben, da sie nur einen gewissen Teil der Funktionalität (z.B. eine Auswahl an Methoden) messen. Micro-Benchmarks bilden das Rückgrat des Performance Regression Testing und dienen häufig dem Vergleich verschiedener Algorithmen [KA05, MH10]. Im Folgenden werden einige Best Practices zum (Micro-)Benchmarking von Java-Anwendungen angegeben. Für weiterführende Literatur sei auf [Cor10] [MH10] und [KA05] verwiesen. • Datenerzeugung: Die Testdaten, die während der Ausführung vom Treiber verwendet werden, sollten nach Möglichkeit außerhalb des Sys- 4.2. MÖGLICHKEITEN ZUR DATENERHEBUNG 71 tems erzeugt werden. Vor allem sollte die Datenerzeugung nicht mit in die Messungen einfließen, außer die Datenerzeugung selbst ist der Gegenstand der Untersuchungen. • Inlining: Der JIT-Compiler nimmt bei monomorphen Methoden häufig eine Inline-Optimierung, also die Ersetzung von Methodenaufrufen mit den Statements des Methodenrumpfs, vor. Java hat allerdings polymorphen Charakter, das heißt dass dynamisch zur Laufzeit aufgrund vorhandener Typinformationen entscheiden wird, welche Implementierung einer Methode zu verwenden ist. Wie erläutert wurde, sammelt JVM ständig Informationen über Methoden. Wird eine polymorphe Methode immer nur monomorph verwendet, kann in diesem Fall auch Inlining eingesetzt werden. Zu Beginn gilt für alle Methoden die Annahme, dass sie monomorph sind, da noch keine weiteren Informationen vorliegen. Wird später doch festgestellt, dass die betroffene Methode polymorph verwendet wird, so muss das Inlining wieder rückgängig gemacht werden, was Zeit beansprucht und die Ausführungszeit der Methode unter Umständen verlangsamt. Dieses Problem kann innerhalb durch eine Aufwärmphase umgangen werden. Bei Erstellung des Treibers sollte zudem vermieden werden, die zu messenden Methode über ein Interface aufzurufen. • Aufwärmphase: Wie zuvor erläutert wurde, bringt die JIT-Kompilierung einige komplexe Optimierungen beim Übergang in den nativen Code ins Spiel. Je häufiger eine Methode (hot) aufgerufen wird, desto mehr Optimierungen werden bei der Kompilierung durchgeführt. Daher sollte der JVM vor den Messungen die Möglichkeit gegeben werden, die Methoden angemessen zu optimieren. Das kann beispielsweise durch wiederholtes Aufrufen innerhalb einer Aufwärmphase geschehen. Wird eine Methode nur einmal vom Treiber aufgerufen und bemessen, ist das Ergebnis für gewöhnlich nicht repräsentativ, da sie nicht optimiert wurde oder bei HotSpot lediglich interpretiert wird. • Dead-Code-Elimination: Es sollte darauf geachtet werden, dass der Code, der zum Messen und zur Ausführung der Methoden verwendet wird, zumindest scheinbar etwas sinnvolles macht. Das hat den Hintergrund, dass, sollte von der JVM festgestellt werden, dass Codeartefakte kein Ergebnis produzieren, oder dass das Ergebnis nicht weiter verwendet wird, sie die betreffenden Stellen komplett eliminiert (DeadCode-Elimination). Daher ist darauf zu achten, dass einzelne Testfälle nicht komplett von der JVM wegoptimiert werden, weil beispielsweise das Ergebnis der Methodenausführung nicht weiter verwendet wird. 72 KAPITEL 4. JAVA UND DIE RESSOURCENNUTZUNG • Mehrfache Messungen: Gerade bei Java-Anwendungen, bei denen die JVM komplexe Optimierungsstrategien einsetzt, sollte immer mehrfach gemessen werden, um eine Aussage über die Qualität der Daten, beispielsweise durch Ermittlung einer Standardabweichung oder Varianz der Messwerte, zu ermöglichen. Kapitel 5 JRUPI als konzeptueller Rahmen zur Bestimmung der Ressourcennutzung Im vorangegangenen Kapitel wurde die Systemumgebung und einige ihrer Besonderheiten, sowie die Möglichkeiten zur Datenerhebung innerhalb dieser Umgebung beschrieben. Vor allem wurden dabei bereits Schwierigkeiten im Zusammenhang mit der JVM herausgearbeitet, Daten über die Ressourcennutzung von Java-Anwendungen zu erheben. Einerseits existieren viele verschiedene Profiler, um die Performance von Java-Anwendungen zu untersuchen. Sie eignen sich allerdings nicht zur Ermittlung der physischen Ressourcennutzung, da sie nur innerhalb der JVM arbeiten. Andererseits existieren viele Werkzeuge, um Ressourcennutzung auf Prozess- oder Threadebene zu beobachten. Mit ihnen kann das Verhalten des JVM-Prozesses auf Systemebene analysiert werden. Sie haben aber wiederum keine Verbindung zu den aktuell ausgeführten Codeartefakten. Es fehlen also geeignete Werkzeuge, um Daten über die Ressourcennutzung spezifischer Java-Codeartefakte zu erheben. Die Schließung dieser Lücke ist die Aufgabe von Jrupi (Java Resource Usage Profiling Infrastructure). Dabei handelt es sich zunächst um ein konzeptuelles Framework, das zwei Aufgaben erfüllt. Erstens gibt es den Aufbau einer Infrastruktur vor, um detaillierte Daten über die Ressourcennutzung von Java-Codeartefakten zu erheben. Anschließend können diese Daten weiterverarbeitet werden, um eine abstraktere Beschreibung daraus abzuleiten. Zweitens enthält es eine Vorgehensbeschreibung für den Modellentwickler, die ihm Handlungsanweisungen zur Benutzung der Infrastruktur vorgibt, um am Ende zu einer abstrakten Beschreibung der Ressourcennutzung von Codeartefakten zu kommen. Zur Datenerhebung nutzt Jrupi ein einheitliches, modulares Verfahren, das sich 73 74 KAPITEL 5. JRUPI ALS KONZEPTUELLES FRAMEWORK sowohl während der Entwicklung als auch am Produktivsystem einsetzen lässt, ohne dass Komponenten verändert werden müssen. Es ist die Aufgabe dieses Kapitels, die Konzepte hinter Jrupi zu erläutern. In Abschnitt 5.1 wird zunächst das erwartete Ergebnis näher spezifiziert und auf resultierende Folgen für den Modellerstellungsprozess eingegangen. Daraufhin grenzt Abschnitt 5.2 die betrachteten Teile der Systemumgebung genauer ein und formuliert darauf aufbauend Entwurfskriterien für Jrupi. In Abschnitt 5.3 wird das Architekturkonzept vorgestellt und Abschnitt 5.4 beschreibt den Ablauf (Workflow) für den Modellersteller. Abschließend werden in Abschnitt 5.5 die Komponenten zusammengefasst, die bei einer Implementierung von Jrupi durch geeignete Werkzeuge oder Implementierungen instanziiert werden müssen. 5.1 Ergebnisdefinition und Konsequenzen Das Ziel dieses Abschnitts besteht darin, genauer zu klären, was in dieser Arbeit unter dem Begriff abstrakte Verhaltensbeschreibung verstanden wird. In Unterabschnitt 3.1.3 erfolgte bereits eine Eingrenzung des Modells und der Modellbildung, die sich an der Allgemeinen Modelltheorie (AMT) orientierte. Dabei wurde festgelegt, dass das zu modellierende Original ein Phänomen, nämlich die physische Ressourcennutzung, hervorgerufen durch die Ausführung von Codeartefakten, ist. Weil das Phänomen lediglich zur Laufzeit beobachtbar, und zudem von spezifischen Hardwarekomponenten abhängig ist, kommt aus pragmatischen Gründen nur ein Reverse Engineering Ansatz für die Modellbildung in Frage. 5.1.1 Die Komponente als Blackbox Das Ziel ist die Gewinnung einer Verhaltensbeschreibung einer CCM Komponente in Bezug auf ihre Ressourcennutzung. In diesem Zusammenhang stellt die Wahl einer geeigneten Abstraktionsstufe für die Beschreibung eine wichtige Entscheidung dar. Dabei ist der Zweck des Modells zu beachten. Es sollte eine möglichst einfache (im Sinne von Rechenaufwand) Prognose über die Ressourcennutzung einer CCM-Komponente erlauben, die von Ressourcenoder Energiemanagern verwendet werden kann, um ihre Konfigurationsentscheidungen zur Laufzeit zu treffen. Ist die Abstraktionsstufe zu hoch, so ist die Prognose unter Umständen zu ungenau. Wird sie zu klein gewählt, so ist sie zur Laufzeit eventuell nicht verwendbar, da ihre Auswertung beziehungsweise Berechnung zu viel Zeit und Ressourcen in Anspruch nimmt. Es ist eine der grundlegenden Eigenschaften von Komponenten, dass sie 5.1. ERGEBNISDEFINITION UND KONSEQUENZEN 75 mit ihrer Umgebung nur über eine wohldefinierte Schnittstelle kommunizieren. Daher können sie relativ simpel mit anderen Komponenten mit derselben Funktionalität, aber einer anderen Implementierung, ausgetauscht werden. In CoolSoftware wird dieser Umstand genutzt, um beispielsweise Varianten mit unterschiedlichen Servicequalitäten anbieten zu können. Daher erscheint es sinnvoll, das Verhalten auf Ebene der öffentlichen Schnittstelle einer CCMKomponente pro Variante zu bestimmen. Die Variante einer Komponente wird deshalb als Blackbox aufgefasst. Um diesen Sachverhalt mit Begriffen der Systemtheorie zu beschreiben, ist die Variante das zu beschreibende System aus einer funktionalen Sichtweise. Ihre Eingabe besteht aus den Parametern der angebotenen Services und die Ausgabe äußert sich in Form einer physischen Ressourcennutzung. Das Modell wird in Form eines generativen Systems beschrieben, das versucht, den Zusammenhang zwischen den genannten Ein- und Ausgaben (externe Verhalten) möglichst exakt nachzubilden. Da angenommen wird, dass über die interne Struktur nichts bekannt ist (Blackbox), entspricht die Modellbildung einer Systeminferenz. In Abbildung 5.1 werden diese Zusammenhänge durch ein Beispiel verdeutlicht. Ressourcennutzung { 'methodname': sort, 'CPU' : [50, 0], 'Disk':{ 'dev':sda1, 'R':800, 'W':920 } } Ressourcennutzung { 'methodname': sort, 'CPU' : [70, 0], 'Disk':{ 'dev':sda1, 'R':2132, 'W':3234 } } Parameter Beobachtungen während der Ausführung { 'type' : Array, 'size' : 2000, 'generic' : { 'type' :String, 'length' : 4} } <<Port>> <<component>> Quicksort Parameter { 'type' : Array, 'size' : 5000, 'generic' : { 'type' :String, 'length' : 8} sort() } interner Zustand konkret abstrakt abstrakte Beschreibung Nutzungsverhalten z.B, mathematisches Modell Abbildung 5.1: Verhaltensbeschreibung: abstrakt aus konkret Das Beispiel zeigt die Variante Quicksort (rechts) einer Sortierkompo- 76 KAPITEL 5. JRUPI ALS KONZEPTUELLES FRAMEWORK nente. Sie besitzt einen Port namens sort(), der als Parameter ein zu sortierendes Array entgegen nimmt. Aufgrund der zahlreichen Gemeinsamkeiten zwischen Objektorientierung und Komponentenparadigma, wird ein Port immer in Form einer Methode implementiert sein. Daher werden die beiden Begriffe in dieser Arbeit austauschbar verwendet. Das zu sortierende Array ist demnach die Eingabe des betrachteten Systems (Variante) und die resultierende Ressourcennutzung zur Durchführung der Aufgabe ist die Ausgabe, wobei alle Beobachtungen zur Laufzeit durchgeführt werden. Jeder konkrete Eingabedatensatz ist daher genau einem konkreten Ausgabedatensatz zuzuordnen, so dass sich jeweils Paare bestehend aus beliebig vielen Werten bilden lassen. Aus den konkreten Wertepaaren muss schließlich eine abstrakte Verhaltensbeschreibung abgeleitet werden. Zu diesem Zweck bietet sich ein mathematisches Modell an, da es eine kompakte und formale Methode der Modellierung bietet, um das generative System zu beschreiben. 5.1.2 Erwartetes Ergebnis und Abhängigkeiten „The only conceivable way of unveiling a black box, is to play with it“ [Tho83] Die Betrachtung der Komponente als Blackbox bringt einige Konsequenzen mit sich, die sich auf das Ergebnis auswirken und die Abhängigkeiten des Ergebnisses festlegen. Zunächst abstrahiert diese Betrachtungsweise vom internen Zustand sowie vom internen Aufbau der Komponente. Als Eingabe in das System kommen daher nur die Parameter der zu bemessenden Methode in Frage. Die physische Ressourcennutzung stellt die Ausgabe des betrachteten Systems dar und resultiert aus der Ausführung der Anweisungen im Rumpf der Methode. Sie ist durch konkrete Werte, wie beispielsweise geschriebener Bytes auf die Festplatte, die zur Ausführungszeit beobachtet werden, gegeben. Das gesuchte Modell soll den beschriebenen Zusammenhang durch mathematische Gleichungen beschreiben und ist daher ein mathematisches Modell. Die Gleichungen haben dabei die folgende Form: id Heinheit = f (P) (5.1) Dabei steht H für eine Art von Hardwarekomponente (wie CPU, Festplatte oder Netzwerkschnittstelle) und P für die Parameter der Methode, die für die Nutzung der Hardwarekomponente relevant sind. Über den optionalen Index id kann die Nutzung einer spezifischen Hardwarekomponente über die Gerätekennung (z.B. sda1 oder sdb1 für Festplatten) angegeben werden, was beispielsweise zur Unterscheidung der verwendeten Netzwerkschnittstelle 5.1. ERGEBNISDEFINITION UND KONSEQUENZEN 77 sinnvoll ist. Der Index einheit ist ebenfalls optional und gibt eine zusätzliche Einheit (z.B. Bytes) an, sollte sie aus dem Kontext der Hardwarekomponente nicht klar sein. Das Verhalten eines Ports (respektive einer Methode) einer Komponente erfolgt durch eine Menge Gleichungen, die die Nutzung aller von ihr verwendeten Hardwareressourcen beschreiben. Durch eine derartige Beschreibung für alle Ports einer Komponente ist, aufgrund der Annahme einer Blackbox, das abstrakte Verhalten der Komponente spezifiziert. Abhängigkeit zu Parametern Die Menge P der relevanten Parameter stellt bei den Betrachtungen eine gewisse Problematik dar. Um die Ableitung eines mathematischen Modells zu ermöglichen, sollten die relevanten Parameter in Form numerischer Daten beschrieben sein. Im Beispiel von Abbildung 5.1 ist der Parameter der sort() Methode ein generisches Array, der in dieser Form keine mathematische Beschreibung erlaubt. Vielmehr kommt es auf den Inhalt des Arrays an, wie etwa die Anzahl der Elemente oder die durchschnittliche Länge der Strings, die als Sortierkriterium dienen. Diese Informationen stecken implizit im Array, sind aber für gewöhnlich auf Schnittstellenebene nicht sichtbar. Es werden also zusätzliche Daten über die die Eingabedaten, sogenannte Metadaten, benötigt. Welche Metadaten für die Ressourcennutzung relevant sind lässt sich nicht verallgemeinern. Das liegt in der objektorientierten Natur von Java begründet, da als Parameter einer Methode beispielsweise Referenzen auf beliebige Objekte, die wiederum Referenzen auf andere Objekte halten, verwendet werden können. Daher ist es auf Schnittstellenebene nicht entscheidbar, welche Metadaten von Bedeutung sind. Allerdings kann durch eine kontrollierte Ausführung mit bekannten Testdaten anhand der Beobachtungen der Ergebnisse auf die Relevanz einzelner Metadaten geschlossen werden. So ist bei der Sortierung beispielsweise eine Korrelation zwischen CPU-Nutzung und Größe des Arrays erkennbar. Während bei der Sortierung die Abhängigkeit zu der Größe des Arrays relativ klar ist, müssen die relevanten Parameter bei unbekannten Komponenten anhand der Datenaufzeichnungen zur Ressourcennutzung und einer Parameteranalyse ermittelt werden. Durch eine kontrollierte Ausführung mit bekannten Testdaten sollte sich ein Zusammenhang, sofern dieser überhaupt besteht, zwischen Aufbau der Testdaten und Ressourcennutzung herstellen lassen. Somit können relevante Metadaten ermittelt werden. Sind die Metadaten, wie im gegebenen Beispiel bekannt, so kann das Verhalten bezüglich der Ressourcennutzung einer Methode in Abhängigkeit der 78 KAPITEL 5. JRUPI ALS KONZEPTUELLES FRAMEWORK Parameter beschrieben werden. Das Verhalten der sort() Methode könnte beispielsweise folgendermaßen spezifiziert werden: cputicks = f (size) (5.2) sda1 diskbyte = f (size, length) (5.3) Die relevanten Parameter können sich je nach Hardwaretyp unterscheiden. So ist bei Sortierkomponenten beispielsweise die Länge der Strings für die CPU-Nutzung (vergleiche Evaluation, Kapitel 7) meist vernachlässigbar. Wird das sortierte Array jedoch auf die Festplatte gespeichert, so macht die Länge der Strings sehr wohl einen großen Unterschied aus. Die mathematischen Gleichungen, die das Ergebnis darstellen, sind eine heuristische Annäherung der zu erwartenden Ressourcennutzung. Um sie effektiv einsetzen zu können, sollten sie mit Erfahrungswerten über die Parameter kombiniert werden. Auch die Kontextoren, die bei CoolSoftware Daten über die Nutzerbedürfnisse sammeln, könne hilfreiche Informationen dazu liefern. Um eine Funktion in Abhängigkeit der Parameter (beziehungsweise ihre Metadaten) ableiten zu können, müssen diese für die Analyse zur Verfügung stehen. Die einfachste, jedoch aufwändigste Variante, ist die Einbringung von Hand durch den Modellersteller bei der Auswertung. Um Metadaten automatisch während der Datensammlung verfügbar zu machen, bietet sich eine Expansion der Parameterliste der Methode um die zusätzlichen numerischen Daten an. So können auch diese automatisch mitprotokolliert werden. In Listing 5.1 ist ein Beispiel zur Parameterexpansion für die sort() Methode angegeben. 1 2 // normale Me t h o d e n s i g n a t u r public T [] sort ( T [] in ) ; 3 4 5 // um Metadaten expandierte Parame terliste public T [] sort ( T [] in , int arraySize , int meanS ortLengt h ) ; Listing 5.1: Expansion der Parameterliste von sort()) Da die Änderung der Schnittstelle einer Komponente im Nachhinein nicht immer möglich ist, kann die Expansion der Parameterliste auch extern, z.B. im Treiber, realisiert werden. Statt der eigentlichen Methode wird ein Wrapper oder Decorator [GHJV95] für die Messungen verwendet, dessen Parameterliste um die benötigten Metadaten expandiert ist, und der die eigentliche Methode ganz normal aufruft. Der verwendete Blackbox Ansatz zur Verhaltensbeschreibung stellt einen Kompromiss dar. Einerseits vermindert er die Komplexität erheblich, da (im 5.2. KONTEXT UND ENTWURFSKRITERIEN 79 Gegensatz zu den vorgestellten Softwareausführungsmodellen) kein state explosion Problem auftreten kann und keine komplizierte Beschreibung des internen Kontrollflusses benötigt wird. Andererseits ist es gerade der interne Kontrollfluss, der Einfluss auf die Ressourcennutzung nehmen kann. Eine einfache if-Anweisung kann dafür sorgen, dass Anweisungen, die physische Ressourcennutzung verursachen würden, gar nicht ausgeführt werden. Demgegenüber kann ein Blackbox Ansatz aber auch für beliebige Komponenten verwendet werden, für die kein Quellcode vorhanden ist. So können mit diesem Verfahren auch Komponenten von Drittanbietern innerhalb der eigenen Infrastruktur vermessen werden. Da insgesamt die Argumente dafür überwiegen, wird generell in Jrupi ein Blackbox Ansatz verwendet. Zugunsten von Flexibilität und Laufzeitkomplexität wird eventuell ein Stück weit auf Genauigkeit verzichtet. 5.2 Kontext und Entwurfskriterien In diesem Abschnitt wird der Kontext der Betrachtungen genauer umrissen. Er legt fest, welche Teile der Systemumgebung (vergleiche Abschnitt 4.1), für Jrupi interessant sind. Den zweiten Teil bildet die Formulierung der Entwurfskriterien für Jrupi. Sie ergeben sich zum großen Teil aus den speziellen Eigenschaften, die die betrachtete Systemumgebung aufweist. 5.2.1 Kontext der Betrachtungen Der Kontext definiert die Arbeitsumgebung für Jrupi, in der die Daten erhoben werden. Abbildung 5.3 zeigt eine schematische Darstellung der betrachteten Abstraktionsebenen und ihrer wichtigsten Komponenten, die den Kontext für Jrupi vorgeben. Auf unterster Ebene (Hardware) sind die einzelnen Hardwarekomponenten, die zusammen das physische Rechnersystem bilden, zu finden. Wie im vorherigen Kapitel bereits erläutert wurde, werden für die Messungen keine spezifischen Hardwarefeatures, wie HPCs verwendet. Der Mangel an Standardisierung in diesem Bereich überwiegt leider den Vorteilen der effizienten Datensammlung. Bei den Hardwarekomponenten ist es, für den größeren Zusammenhang, wichtig, die diversen Energie- und Performancezustände im Zusammenhang mit Ressourcennutzung und Proportionalität, wie sie in Unterabschnitt 4.1.2 erläutert wurden, im Hinterkopf zu behalten. Die zentrale Abstraktionsebene ist durch das Betriebssystem gegeben. Es befindet sich zwischen der Hardware auf der einen Seite, und den Benutzerprogrammen, repräsentiert durch einzelne Prozesse, auf der anderen Seite. Um spezifische Hardwarekomponenten anzusprechen, verwendet das 80 KAPITEL 5. JRUPI ALS KONZEPTUELLES FRAMEWORK Javaanwendung Prozessebene Komponente X Komponente Y Java Virtual Machine Anwendung X JNI Komponente usw Anwendung Y Anwendung usw API (syscalls) | Ausnahmebehandlung | Interrupts Betriebssystem Prozessmanagement Speichermanagement Block-I/Omanagement Netzwerkmanagement usw Netzwerkgeräte usw Gerätetreiber Hardware Prozessoren (CPUs) Arbeitsspeicher Speichergeräte Abbildung 5.2: Kontext der Betrachtungen Betriebssystem Gerätetreiber1 . Die Treiber arbeiten dabei im Kernmodus und sind für Linux üblicherweise in Form von Kernelmodulen implementiert. Da Treiber hardwarespezifisch sind, werden sie in dieser Arbeit nicht für die Datenerhebung genutzt. Innerhalb eines Betriebssystems existieren für gewöhnlich weitere zentrale Abstraktionen, die manchmal auch als Subsysteme bezeichnet werden. So bietet Linux beispielsweise ein virtuelles Dateisystem, das eine zusätzliche Abstraktionsschicht zwischen Prozessen und unterschiedlichen Dateisystemarten einführt, indem es ein einheitliches Objektmodell zur Verfügung stellt. Weiterhin besitzt ein Betriebssystem typischerweise Subsysteme, die sich um Speichermanagement, Block-I/O-Management und Netzwerkmanagement kümmern. Als zentraler Bestandteil des Prozessmanagements wurde im vorigen Kapitel bereits die Funktionsweise des Scheduler erläutert, der die Zuteilung zwischen Prozessen und CPUs verwaltet. Die einzelnen Subsysteme sind meist selbst in mehrere Ebenen unterteilt und arbeiten bei Linux durch relativ komplexe Aufrufstrukturen zusammen (zur Erinnerung: im Linuxkernel kann jede Funktion von jeder Stelle aufgerufen werden, vergleiche Abschnitt 4.1). Daher wird an dieser Stelle nicht auf nähere Details eingegangen. Es sei lediglich angemerkt, dass sie aufgrund ihrer 1 genau genommen sind Gerätetreiber Software, die meist mit einem Controller, der eine vereinfachte Stelle zum Gerät bietet, kommuniziert [Tan07] 5.2. KONTEXT UND ENTWURFSKRITERIEN 81 zentralen Lage innerhalb der Aufrufstrukturen des Kerns, im Zusammenspiel mit dem aktuellen Kontext eine wertvolle Datenquelle darstellen. Innerhalb der Systemumgebung wurde die JVM als weitere Abstraktionsebene eingeführt, die zwischen Betriebssystem und Codeartefakten steht. Aus Sicht des Betriebssystems ist die JVM lediglich ein weiterer Prozess neben den anderen Anwendungen, die im System laufen. Daher wurde in Abbildung 5.3 die JVM der Prozessebene zugeordnet. Aus der Sicht von Prozessen gibt es nur drei Wege, um mit dem Linuxkernel zu kommunizieren: Systemaufrufe (API, syscalls), Ausnahmebehandlung und Interrupts. In der JVM steht zu diesem Zweck das JNI (Java Native Interface) zur Verfügung. Es erlaubt den Aufruf von nativen Code aus der JVM heraus. Eine Überwachung der Systemaufrufe bietet sich ebenfalls an, um Informationen über die Ressourcennutzung zu bekommen. Da sie die zentrale Schnittstelle zum Kern darstellen, müssen sie von Anwendungen verwendet werden, wenn diese Zugriff auf Hardwarekomponenten benötigen. Das bringt allerdings mehrere Probleme mit sich. Zum einen existieren mehrere hundert verschiedene dieser Systemaufrufe, die je nach Parameter einen anderen Weg durch die Aufrufstrukturen im Kern einschlagen. Daher ist ihr Verhalten nicht-deterministisch und kann nicht vereinheitlicht werden. Zum Anderen stehen Systemaufrufe auf oberster Abstraktionsstufe. Es steht daher nicht immer ausreichend Kontextinformationen zur Verfügung, um auf die tatsächliche Nutzung physischer Geräte Rückschlüsse ziehen zu können. Die letzte, und damit höchste, Abstraktionsebene der Betrachtungen stellen die zu analysierenden Java-Anwendungen dar, deren Ausführung von der JVM als Laufzeitumgebung gesteuert und überwacht wird. JVMs sind typischerweise komplexe Softwaresysteme, die zahlreiche Optimierungen, vor allem beim Übergang zu nativen Code, während der Ausführung durchführen. Damit soll der Geschwindigkeitsnachteil, den Java aufgrund seiner dynmaischen Natur besitzt, ausgeglichen werden. Es ist überaus wichtig, diese Optimierungen im Zuge des Benchmarking beziehungsweise Profilings zu beachten, um zu aussagekräftigen Ergebnissen zu kommen (vergleiche Unterabschnitt 4.2). Im Kontext von CoolSoftware liegen Java-Anwendungen in Form von CCM-Komponenten, die über ihre definierten Schnittstellen (Ports) miteinander kommunizieren, vor. Da zur Implementierung der Ports Java zum Einsatz kommt, kann die Jrupi das Profiling von CCM-Komponenten ebenfalls auf Methodenebene erfolgen, da sie verwendet werden, um Ports zu implementieren. 82 KAPITEL 5. JRUPI ALS KONZEPTUELLES FRAMEWORK 5.2.2 Formulierung der Entwurfskriterien Aufbauend auf den bisherigen Betrachtungen wurden für Jrupi die folgenden Entwurfskriterien definiert: • Portabilität Aufgrund der direkten Abhängigkeit zu den verwendeten Hardwarekomponenten, sollte es möglich sein, dasselbe Profiling auf verschiedenen Rechnern durchführen zu können. Als Voraussetzung wird lediglich angenommen, dass Linux als Betriebssystem verwendet wird. • Einsatz am Produktivsystem Eine Datenerhebung sollte auch am Produktivsystem durchführbar sein, ohne größere Änderungen durchführen zu müssen. Dazu gehört auch, dass die Funktionalität dynamisch, ohne Neustart und Unterbrechungen des Systems oder der Anwendung, zur Verfügung gestellt werden kann. • Austauschformat Die erhobenen Daten sollten in einem flexiblen Austauschformat gespeichert werden, das leicht weiterverarbeitet werden kann. • Visualisierung der Daten Zur Betrachtung und weiteren Auswertung der Daten sollten diese in geeigneter Form dargestellt werden. • Analyse der Daten Eine Voranalyse der Daten sollte statistische Verfahren wie die Ermittlung einer Standardabweichung, um die Qualität der erhobenen Daten zu überprüfen, ermöglichen. • Flexibilität Die einzelnen Profiler sollten ohne größere Umstände durch andere Implementierungen ersetzt werden können. • Erweiterbarkeit Neue Profiler, Auswertungs- und Visualisierungskomponenten müssen sich leicht hinzufügen lassen. • Modularität Die Forderung nach eine modularen Aufbau folgt direkt aus der benötigten Flexibilität. Zudem garantiert Modularität einfache Erweiterbarkeit. • Automatisierung Zeitaufwändige Aufgaben, wie die Erstellung von Instrumentationen, sollte weitestgehend automatisiert werden. Dazu bietet sich eine Codegenerierung an. 5.3. DAS JRUPI ARCHITEKTURKONZEPT 5.3 83 Das JRUPI Architekturkonzept Jrupi ist kein Framework, das in Form einer Bibliothek oder Ähnlichem vorliegt, und einfach verwendet werden kann. Vielmehr gibt es einen Rahmen vor, der festlegt, wie durch die Kombination unterschiedlicher Techniken, Methodiken und Werkzeuge eine sinnvolle Aussage über das Ressourcennutzungsverhalten von Java-Codeartefakten getroffen werden kann. Eine konkrete Implementierung von Jrupi muss die einzelnen, lose gekoppelten Bestandteile des Frameworks durch geeignete Werkzeuge und Implementierungen instanziieren. Die Aufteilung der Systemumgebung in mehrere Abstraktionsebenen spiegelt sich auch in der Architektur der Jrupi wieder. Das Betriebssystem ist die niedrigste Abstraktionsebene und dient der eigentlichen Datengewinnung über die Ressourcennutzung. Der JVM-Prozess, der als Laufzeitumgebung der Java-Anwendungen dient, ist der eigentliche Auslöser der Ressourcennutzung. Zur genaueren Zuordnung der Daten zu spezifische Codeartefakten erfolgt eine Instrumentierung der CCM-Komponente. In Abbildung 5.3 ist die konzeptuelle Architektur von Jrupi dargestellt. Der Gegenstand der Untersuchungen ist eine konkrete Implementierung (Variante) einer CCM-Komponente. Da eine Datenerhebung nur zur Laufzeit möglich ist, wird eine Ausführungsumgebung für die Komponente benötigt. Ihre Aufgabe ist es, die zu bemessenden Methoden der Komponente, unter Verwendung sinnvoller Daten als Parameter, zur Ausführung zu bringen. Ähnlich dem Benchmarking, kann diese Funktionalität in Form eines externen Treibers implementiert werden. Die genaue Umsetzung liegt vollständig in der Verantwortung des jeweiligen Modellerstellers, sollte aber unter Beachtung der in Unterabschnitt 4.2.4 erläuterten Best Pracitces erfolgen. Ein weiterer, zentraler Bestandteil von Jrupi ist die Umsetzung einer geeigneten Instrumentierung. Sie dient der Markierung der jeweiligen Codeartefakte im aktuell ausgeführten Bytecode. Es ist ihre Aufgabe, eine Verbindung zwischen Codeartefakt und der Datenerhebung auf Betriebssystemebene herzustellen. Um eine Zuordnung von Ressourcennutzungsdaten zu spezifischen Codeartefakten zu ermöglichen, sind zwei Vorgehensweisen denkbar: • Protokollierung von Start- und Endzeit der Ausführung eines Codeartefakts und nachträgliche Zuordnung zu den Ressourcennutzungsdaten des JVM-Prozesses. • Kommunikation mit den Profilern, um ihnen mitzuteilen, welches Codeartefakt gerade ausgeführt wird. 84 KAPITEL 5. JRUPI ALS KONZEPTUELLES FRAMEWORK CCM Komponente Ausführungsumgebung Instrumentation Java Virtual Machine Betriebssystem CPU Profiler Netzwerk Profiler Dateisystem Profiler ... Analyse-Framework Datendarstellung (visuell) Datenauswertung (statistisch) Datenexport (Werkzeugformate) Intelligente Datenanalyse cpu=f x , y Abbildung 5.3: Übersicht Konzept Zur technischen Umsetzung der Instrumentierung können die in Unterabschnitt 4.2.1 beschriebenen Verfahren verwendet werden. Profiler sammeln Daten über die Ressourcennutzung des JVM-Prozesses, seiner Threads, sowie der restlichen Systemumgebung, während die JavaAnwendung durch die JVM ausgeführt wird. Für jede zu überwachende Hardwarekomponente existiert ein Profiler, der eine oder mehrere relevante Stellen (Subsysteme) im Betriebssystem überwacht und dabei Daten über die Nutzung der jeweiligen Hardwarekomponenten sammelt. Der modulare Aufbau trägt zur Skalierbarkeit bei, da der Overhead durch das separate Zu- und Abschalten einzelner Profiler minimiert werden kann. Des Weiteren ermög- 5.4. DER JRUPI WORKFLOW 85 licht die Modularität, dass neue oder geänderte Implementierungen einfach hinzugefügt werden können. Die Profiler stellen die gesammelten Ressourcennutzungsdaten in Form eines flexiblen Austauschformats bereit. Zur weiteren Auswertung können die Daten vom Analyse-Framework eingelesen werden. Es bietet Funktionalität zur Datendarstellung, Datenauswertung und zum Datenexport. Die Weiterverarbeitung der Daten durch das Analyse-Framework erfolgt nach der Ausführung, was häufig auch als Offline-Analyse (oder post mortem) bezeichnet wird. Auch diese Maßnahme dient der Reduktion des Overheads während der Datenerhebung. Die Hauptaufgabe des Analyse-Framework ist eine statistische Vorauswertung der Daten durchzuführen. Zum Einen um ihre Qualität zu bestimmen, zum Anderen um die Menge an Daten zu reduzieren. Anschließend können die Daten durch ein anderes, externes Werkzeug weiterverarbeitet werden. Einerseits erfüllt das Analyse-Framework die Funktion eines Filters, andererseits ist es eine Abstraktionsebene für spezifischere Werkzeuge. Diese Werkzeuge dienen der (halb-)automatischen Weiterverarbeitung der Daten, um einen mathematischen Zusammenhang abzuleiten. Die Wahl eine geeigneten Werkzeugs liegt in der Verantwortung des Modellerstellers. In Abbildung 5.3 wird verallgemeinernd der Begriff Intelligente Datenanalyse verwendet, da derartige Werkzeuge häufig auf Verfahren der Künstlichen Intelligenz zurückgreifen. 5.4 Der JRUPI Workflow Es wurde bereits mehrfach angedeutet, dass der Modellersteller nicht zwangsläufig der Entwickler der CCM-Komponente ist. Der Grund dafür ist, dass das Entwicklersystem gewöhnlich nicht das System ist, auf dem die Komponente später tatsächlich eingesetzt wird. Das Profiling von Codeartefakten liefert aber Ressourcennutzungsdaten, die charakteristisch für die verbauten Hardwarekomponenten sind. Es ist daher wahrscheinlich, dass Komponenten, die auf einem anderen System, mit einer anderen Hardwarekonfiguration eingesetzt werden, auch eine andere Ressourcennutzung aufweisen. Es ist zudem denkbar, dass Komponenten von Drittanbietern ohne Quellcode erworben werden. Der Blackbox Ansatz ermöglicht zwar das Vermessen der Komponente, doch ist es oft nicht erstrebenswert (Kosten und Geheimhaltung), Dritte innerhalb der eigenen Infrastruktur arbeiten zu lassen. Die Notwendigkeit, das Ressourcennutzungsmodell im Rahmen eines Reverse Engineering Ansatzes zu konstruieren, verlangt eine gewisse Anpassung des Vorgehensmodells. CCM-Komponenten werden zunächst modelliert und 86 KAPITEL 5. JRUPI ALS KONZEPTUELLES FRAMEWORK anschließend implementiert. Daraufhin kommt Jrupi zum Einsatz, um Daten über die Ressourcennutzung zu erheben und ein mathematisches Modell zu konstruieren, das die Ressourcennutzung der Komponente auf Interfaceebene spezifiziert. In Abbildung 5.4 ist dieser Vorgang detaillierter in Form eine Flussdiagramms dargestellt, um den Modellentwickler eine konkrete Folge von Handlungsanweisungen zu geben. START ja CCM Komponente vorhanden? START 2. nein 2.1.Methoden 2.1.Methoden auswählen auswählen 1.CCM 1.CCMKomponente Komponente entwickeln entwickeln Profiling am Produktivsystem? 2.Ausführungs2.Ausführungskontext kontextaufsetzen aufsetzen ja nein 3.System 3.System ausführen ausführen 4.Instrumentation 4.Instrumentation laden laden 2.2.Microbenchmarks 2.2.Microbenchmarks entwickeln entwickeln 5.Profiler 5.Profiler starten starten 2.3.Instrumentation 2.3.Instrumentation entwickeln entwickeln ENDE 2. Ressourcennutzungsdaten 6.Daten 6.Daten analysieren analysieren 7.Modell 7.Modell ableiten ableiten ENDE Abbildung 5.4: Ablauf für Modellersteller als Flussdiagramm Im Folgenden werden die einzelnen Aktivitäten, die vom Modellersteller auszuführen sind, um zu einer abstrakten (mathematischen) Verhaltensbeschreibung einer Komponente zu gelangen, schrittweise erläutert: 1. CCM Komponente entwickeln: Es liegt in der Natur des Reverse Engineering, dass die Komponente zunächst vorhanden sein muss. 5.4. DER JRUPI WORKFLOW 87 Idealerweise wird sie selbst entwickelt, könnte aber auch von anderen Anbietern geliefert werden. 2. Ausführungskontext aufsetzen: Das Aufsetzen eines geeigneten Ausführungskontext ist die aufwändigste und zugleich schwierigste Aufgabe, die der Modellersteller selbst durchführen muss. Zunächst muss eine Auswahl an Methoden, für die die Erstellung eines Modells auch sinnvoll ist, getroffen werden. Ist die Komponente in Quellcode vorhanden, kann diese Auswahl auch interne Methoden umfassen. Ansonsten wird sie gleich der öffentlichen Schnittstelle der Komponente sein. Eventuell können gewöhnliche Java-Profiler dem Modellentwickler Hinweise geben, für welche Methoden sich ein Profiling lohnt. Je nachdem ob das Profiling am Produktivsystem erfolgt oder nicht, müssen gegebenenfalls Microbenchmarks, wie sie in Abschnitt 4.2 beschrieben wurden, erstellt werden. Da diese Microbenchmarks sowieso benötigt werden, ist es besser, sie bereits während der Entwicklung der Komponente parallel zu erstellen. Dabei können sie beispielsweise für Performance Regression Testing verwendet werden, wie es auch in [MH10] gefordert wird. Um die Verbindung zwischen Codeartefakt und Profiler zu ermöglichen, müssen auf jeden Fall Instrumentationen für alle zu beobachtenden Methoden entwickelt werden. Da die Instrumentierungen sich für verschiedene Methoden meist wenig unterscheiden, können sie automatisch generiert werden, um den Modellentwickler zu entlasten. 3. System ausführen: Sind alle Vorbereitungen getroffen, so kann das System ausgeführt werden. Wird mit Microbenchmarks gearbeitet, so empfiehlt es sich, nach dem Aufwärmen der JVM und der Konstruktion der Testdaten, die Ausführung zu unterbrechen, um die Profiler und Instrumentation zu starten. 4. Instrumentierung laden: Genau genommen kommt es auf die Form der gewählten Instrumentierung an, obs ie an dieser Stelle geladen werden muss. Wurde sie beispielsweise direkt im Quellcode vorgenommen, ist dieser Schritt nicht nötig. Allerdings wird für Jrupi die Verwendung eines Verfahrens empfohlen, das eine dynamische Bytecodeinstrumentierung erlaubt, also zur Laufzeit der Anwendung die Instrumentierungen in den Bytecode einbringt. 5. Profiler starten: Die Profiler benötigen als Eingabe die PID des JVMProzesses, den sie beobachten sollen. Daher können sie erst gestartet werden, wenn die Java-Anwendung innerhalb ihres Ausführungskontext zum Laufen gebracht wurde. 88 KAPITEL 5. JRUPI ALS KONZEPTUELLES FRAMEWORK 6. Daten analysieren: An dieser Stelle erfolgt die Vorauswertung und Visualisierung der Daten mit Hilfe des Analyse-Framework. Es unterstützt den Modellentwickler dabei, die Qualität der Daten zu bestimmen und die Menge an Daten zu reduzieren. 7. Modell ableiten: Die finale Aufgabe besteht für den Modellentwickler darin, ein mathematisches Modell aus den Daten abzuleiten. Bei simplen Zusammenhängen kann dies direkt und ohne Werkzeugunterstützung erfolgen. Für Jrupi wird die Verwendung von Verfahren zur intelligenten Datenanalyse empfohlen, die in Datenmengen (semi)automatisch nach mathematischen Zusammenhängen suchen. 5.5 Zusammenfassung umzusetzender Komponenten Dieses Kapitel hat die grundlegenden Konzepte von Jrupi erläutert. Jrupi ist kein starres Gebilde, sondern gibt einen konzeptuellen Rahmen vor, wie ein Zusammenhang zwischen Java-Codeartefakten und physischer Ressourcennutzung hergestellt werden kann. Es besteht im Wesentlichen aus den folgenden, weitgehend unabhängigen, Komponenten, die bei einer Umsetzung von Jrupi mit konkreten Implementierungen oder Werkzeugen instanziiert werden müssen. • Profiler: Ihre Aufgabe ist die Sammlung von Daten über die physische Ressourcennutzung. Es sollte für jeden Hardwarekomponententyp ein separater Profiler auf Betriebssystemebene (z.B. Subsystem, Treiber) implementiert werden. • Datenaustauschformat: Die Profiler sollten die gesammelten Daten in Form eines flexiblen Austauschformat zur Verfügung stellen, um eine spätere Offline-Analyse zu ermöglichen. • Instrumentierung: Die Instrumentierung der Java-Codeartefakte dient der Verbindung zwischen Codeartefakt und Profiler. • Analyse-Framework: Für jeden Profiler (und damit jede Hardwarekomponente) existiert im Analyse-Framework eine Komponente, die die gesammelten Daten einliest, darstellt und aufbereitet. • Intelligente Datenanalyse: Die veredelten Daten müssen schließlich auf einen mathematischen Zusammenhang zwischen Ein- und Ausgabe 5.5. ZUSAMMENFASSUNG UMZUSETZENDER KOMPONENTEN 89 untersucht werden. Dazu können eigene Implementierungen oder externe Werkzeuge zum Einsatz kommen. Die Aufteilung in voneinander unabhängige Komponenten ist der erläuterten Variabilität der einzelnen Abstraktionsebenen der Systemumgebung (vergleiche Abschnitt 4.1) geschuldet. Aufgrund der Heterogenität der Implementierungen der Abstraktionsebenen kann Jrupi lediglich einen allgemeinen Rahmen vorgeben. Für eine konkrete Umsetzung sollten, soweit dies möglich ist, existierende Werkzeuge und Frameworks zur Anwendung kommen. Da die einzelnen Komponenten weitgehend unabhängig voneinander sind, besteht die Herausforderung in der Bildung einer gut funktionierenden Werkzeugkette. Das folgende Kapitel 6 zeigt, wie Jrupi effizient implementiert werden kann. Die Profiler sind in diesem Fall für das Betriebssystem Linux implementiert. 90 KAPITEL 5. JRUPI ALS KONZEPTUELLES FRAMEWORK Kapitel 6 Die Proof-of-Concept Implementierung der JRUPI Die Jrupi (Java Resource Usage Profiling Infrastructure) ist, wie im vorangegangenen Kapitel erläutert wurde, ein konzeptuelles Rahmenwerk zur Ableitung eines mathematischen Zusammenhangs zwischen Java-Codeartefakten und physischer Ressourcennutzung. Einerseits gibt Jrupi dem Modellentwickler eine Folge von Handlungsanweisungen vor, die zur Gewinnung eines mathematischen Modells nötig sind. Andererseits beschreibt Jrupi die einzelnen Bestandteile, die für eine konkrete Umsetzung durch geeignete Implementierungen oder Werkzeuge instanziiert werden müssen. Die zunächst abstrakte Beschreibung der Bestandteile liegt in der Heterogenität der Abstraktionsebenen der Systemumgebung begründet, wobei hauptsächlich die Ebenen Betriebssystem und JVM betroffen sind. Es wurde gezeigt, dass es sinnvoll ist, Daten über die physische Ressourcennutzung auf Ebene des Betriebssystems zu erheben. Dieser Umstand macht die verwendeten Profiler von Natur aus plattformspezifisch (Betriebssystem). Die verwendete JVM kann auf zwei Arten Einfluss innerhalb von Jrupi nehmen. Zum Einen sollte der Modellentwickler bei der Erstellung von Benchmarks die jeweiligen Optimierungen der verwendeten JVM beachten. Zum Anderen kann die Implementierung der JVM Einfluss auf die Umsetzung der Instrumentierung haben. Werden die zusätzlichen Anweisungen statisch in Quell- oder Bytecode eingefügt, so ist die JVM nicht von Bedeutung. Bei einer Instrumentierung zur Laufzeit muss sie jedoch geeignete Mechanismen bereitstellen, um den Instrumentationscode vor dem Laden einer Klasse durch den Class Loader einzufügen. Dieses Kapitel beschreibt eine Proof-of-Concept Implementierung von Jrupi, die das Profiling für das Betriebssystem Linux umsetzt. Die Instrumentierung erfolgt dynamisch durch Bytecode-Manipulation zur Laufzeit. 91 92 KAPITEL 6. PROOF-OF-CONCEPT IMPLEMENTIERUNG Die restlichen Komponenten sind plattformunabhängig. Bei Implementierung von Jrupi kommen zur Umsetzung der einzelnen Komponenten unterschiedliche Werkzeuge und Konzepte zum Einsatz. Da die Aufgaben, die sie übernehmen, relativ komplex sind, wurde soweit möglich auf existierende Werkzeuge zurückgegriffen. Abschnitt 6.1 instanziiert zunächst die Mehrzahl der durch Jrupi vorgegebenen Komponenten mit konkreten Werkzeugen. Lediglich das Analyse-Framework ist eine Eigenimplementierung und wird daher erst in Abschnitt 6.2, der die konkrete Umsetzung in Form einer Werkzeugkette beschreibt, besprochen. 6.1 Instanziierung der Komponenten Jeder der folgenden Unterabschnitte ist einer spezifischen Komponente gewidmet und erfüllt zweierlei Aufgaben: Erstens liefert er eine Begründung für die Auswahl der jeweiligen Umsetzung. Zweitens gibt er eine kurze Einführung in die Funktionalitäten der jeweiligen Werkzeuge. Die Beschreibung, wie die Werkzeuge für die spezifische Implementierung der Komponenten eingesetzt und kombiniert werden, gibt später Abschnitt 6.2. 6.1.1 Profiler: SystemTap „(SystemTap) ...makes it very easy to observe anything about a live system...the problem is to figure out what you want to observe“ Die Profiler sind das Herzstück einer jeden Implementierung der Jrupi. Es ist ihre Aufgabe, die Daten über die physische Ressourcennutzung auf Betriebssystemebene zu erheben. Damit sind sie die messenden Komponenten und ihre Implementierung entscheidet wesentlich über die Qualität der erhobenen Daten. In dieser Arbeit werden ausdrücklich Verfahren empfohlen, die im Allgemeinen unter den Begriffen Dynamisches Tracing oder Dynamische Ablaufverfolgung bekannt sind. Sie erlauben eine Instrumentierung nahezu beliebiger Stellen im Betriebssystemkern. Für das Betriebssystem Linux steht mit SystemTap1 eine Umsetzung der dynamischen Ablaufverfolgung zur Verfügung, die mittlerweile als ausgereift betrachtet werden kann. Lediglich die Anwendung im user space, die mit SystemTap prinzipiell auch möglich ist, sollte mit Vorsicht eingesetzt werden, da sie noch nicht vollständig unterstützt ist. So hat SystemTap den Sprung in die Repositories aller größeren Distributionen geschafft und kann auch bei Enterprise Varianten 1 http://sourceware.org/systemtap/ 6.1. INSTANZIIERUNG DER KOMPONENTEN 93 meist problemlos über die Paketverwaltung nachgerüstet werden. Im Folgenden werden die Konzepte und die Funktionsweise von SystemTap vorgestellt. Für weiterführende Informationen sei auf die Dokumentation (unter der angegebenen Webseite) oder [BPBS10] verwiesen. Grundlegende Konzepte SystemTap erlaubt die dynamische Instrumentierung nahezu beliebiger Stellen innerhalb des Linux-Kernels, wobei die instrumentierten Stellen als Prüfpunkte (Probe) bezeichnet werden und programmierbar sind. Dabei setzt sich jeder Prüfpunkt aus den fundamentalen Bestandteilen Event und Handler zusammen. • Event: Der Eintritt eines Ereignisses, das von besonderen Interesse ist. Ein Event kann beispielsweise durch Funktionseintritt, Funktionsaustritt oder den Ablauf eines Timers ausgelöst werden. • Handler: Sobald ein spezifiziertes Event eingetreten ist, werden die durch einen zugehörigen Handler festgelegten Aktionen ausgeführt. Die Prüfpunkte können daher mit programmierbaren Messfühlern innerhalb der Kernels verglichen werden. Mit diesen Konzepten ist es möglich, Aktivitäten innerhalb des Linux-Kernels während der Ausführung zu beobachten, zu inspizieren und zu analysieren. Zur Beschreibung der Prüfpunkte wurde für SystemTap eine eigene Skriptsprache entwickelt, deren Syntax an C und awk angelehnt ist. Einerseits erlaubt sie die Benennung der Events, die von Interesse sind. Andererseits wird in dieser Sprache auch der Handlercode, also die Statements, die bei Eintritt eines spezifizierten Ereignisses ausgeführt werden sollen, festgelegt. Ein Prüfpunkt hat allgemein den in Listing 6.1 dargestellten Aufbau. 1 probe event { statements } Listing 6.1: Allgemeiner Definition eines Prüfpunktes Ein Prüfpunkt in SystemTap wird durch das Schlüsselwort probe eingeleitet, gefolgt von einen oder mehreren Events (event). Bei der Festlegung der Events können auch fortgeschrittene Konzepte wie Wildcards (*) zum Einsatz kommen. Der Handlercode wird durch eine Reihe von Statements (statements) beschrieben, die alle Sprachkonzepte der Skriptsprache verwenden können. Dabei ist die Skriptsprache relativ mächtig und definiert beispielsweise Konstrukte wie Schleifen, assoziative Arrays, statistische Aggregate, eingebaute Hilfsfunktionen und viele mehr. Listing 6.2 zeigt ein Beispiel für die Definition eines Prüfpunktes. 94 1 2 3 KAPITEL 6. PROOF-OF-CONCEPT IMPLEMENTIERUNG probe kernel . function ( " * @net / socket . c " ) { printf ( " % s -> % s \ n " , t h r e a d _ i n d e n t (1) , p r o b e f u n c () ) } Listing 6.2: Beispiel der Definition eines Prüfpunktes Dieser Prüfpunkt definiert als Events den Eintritt in eine beliebige Funktion, die im Linux-Kernel innerhalb der Datei /net/socket.c definiert ist. An dieser Stelle wird deutlich, dass zum Einsatz eine gewisse Kenntnis über den Linux-Kernel nötig ist. Gute Informationsquellen sind [Lov10, BC00] oder der Quellcode von Linux, der frei verfügbar ist. Im Handler-Body ist ein für das Tracing typisches Ausgabestatement (printf()) definiert. Das Beispiel führt zu zwei weiteren wichtigen Konzepten: • Tapsets sind Bibliotheken, die nützliche Hilfsfunktionen (im Beispiel probefunc() und thread_indent()) und vordefinierte Prüfpunkte umfassen. Einerseits dienen sie der Abstraktion, wenn die Sammlung von Daten für eine spezifische Kernelfunktion kompliziert ist. Andererseits fördern sie die Wiederverwendung, indem sie Aliase für Prüfpunkte definieren. Ein Alias kann dabei für mehrere Prüfpunkte verwendet werden (1:n Mapping) und ist daher mit einem Pointcut aus der AOP [KLM+ 97] vergleichbar. • Kontext: Die meisten Informationen, die in Handlern gesammelt oder ausgegeben werden, stammen aus dem Kontext. Dieser umfasst beispielsweise Parameter der Kernelfunktion, lokale Variablen oder über Hilfsfunktionen aufgelöste Informationen wie die aktuelle Prozess-ID (PID). Beispiel In Abbildung 6.1 ist der Zusammenhang zwischen SystemTap-Skript, Tapsets, sowie der eigentlichen Kernelfunktion dargestellt. Dieses Beispiel verdeutlicht alle bisher vorgestellten Konzepte. Im oberen Bereich ist ein Auszug aus einem Skript namens disktop.stp abgebildet, das Prozesse mit der höchsten Festplattenaktivität ermittelt und ausgibt. Zu diesem Zweck definiert es einen Prüfpunkt für das Event vfs.write.return, welches sich auf eine spezielle Kernelfunktion im virtuellen Dateisystem von Linux bezieht. Das verwendete Event ist ein Alias für einen anderen Prüfpunkt, der als Event die Kernelfunktion (vfs_write()) bestimmt, und in einem Tapset mit Namen vfs.stp definiert ist (mittlerer Block). Eine Suche im LinuxKernel zeigt, dass sich diese Funktion in der Datei /fs/read_write.c (unterer Block) befindet. An diesem Beispiel lässt sich sehr schön die verfügbare 95 6.1. INSTANZIIERUNG DER KOMPONENTEN probe probe vfs.write.return vfs.write.return {{ if if ($return>0) ($return>0) {{ if if (devname!="N/A") (devname!="N/A") {{ /*skip /*skip update update cache*/ cache*/ io_stat[pid(),execname(),uid(),ppid(),"W"] io_stat[pid(),execname(),uid(),ppid(),"W"] += += $return $return device[pid(),execname(),uid(),ppid(),"W"] device[pid(),execname(),uid(),ppid(),"W"] == devname devname write_bytes write_bytes += += $return $return }} }} }} Skript disktop.stp probe probe vfs.write.return vfs.write.return == kernel.function("vfs_write").return kernel.function("vfs_write").return {{ name = "vfs.write" name = "vfs.write" retstr retstr == sprintf("%d", sprintf("%d", $return) $return) file file == $file $file pos pos == $pos $pos buf buf == $buf $buf bytes_to_write bytes_to_write == $count $count [...] [...] Tapset vfs.stp ssize_t ssize_t vfs_write(struct vfs_write(struct file file *file, *file, const const char char __user __user *buf, *buf, size_t size_t count, count, loff_t loff_t *pos) *pos) {{ ssize_t ssize_t ret; ret; [...] [...] Kernel /fs/read_write.c Abbildung 6.1: Verhältnis zwischen Skript, Tapset und Kernelfunktion Kontextinformation erkennen. So werden alle Parameter und einige lokale Variablen im Tapset aufgelöst und können im Skript einfach verwendet werden. Das Skript selbst verwendet mehrere Hilfsfunktionen, die zusätzliche Kontextinformationen zum Prozess liefern und in assoziative Arrays (io_stat und device) gespeichert werden. Da der Prüfpunkt am Funktionsausgang definiert ist, kann zusätzlich der Rückgabewert der Funktion ($return) verwendet werden, der der Menge geschriebener Bytes entspricht. Funktionsweise Bisher wurde gezeigt, wie Skripte erstellt werden, die zur Laufzeit Daten über den Linux-Kernel sammeln oder ausgeben. Doch wie ist das überhaupt möglich? SystemTap liefert als Frontend das Konsolenwerkzeug stap. Es nimmt zunächst eine Skriptdatei entgegen und erstellt daraus in mehreren Schritten ein geladenes Kernelmodul. In Abbildung 6.2 ist der gesamte Prozess dargestellt. Im Folgenden werden die einzelnen Stufen stichpunktartig erläutert. Für detaillierte Informationen sei auf [BPBS10] verwiesen. • Übersetzen: Zunächst wird das Skript (profiler.stp) vom Parser eingelesen und anschließend auf Fehler untersucht. Daraufhin wird es 96 KAPITEL 6. PROOF-OF-CONCEPT IMPLEMENTIERUNG Skript Skript (profiler.stp) (profiler.stp) Tapsets Tapsets Übersetzen Übersetzen Debug-Info Debug-Info Profiler.c Profiler.c Build Build Profiler.ko Profiler.ko Laden Laden und und Ausführen Ausführen Ausgabe Ausgabe Aktivitäten Aktivitäten (stap) (stap) Artefakte Artefakte Entladen Entladen Legende Abbildung 6.2: Funktionsweise von SystemTap nach mit Informationen aus Tapsets und Debug-Info2 vervollständigt und schließlich nach C übersetzt (Profiler.c). • Build: Aus dem C-Code wird ein Kernelmodul (Profiler.ko) erstellt, das dynamisch in den Kernel geladen werden kann. • Laden und Ausführen: SystemTap übernimmt das Laden des erzeugten Kernelmoduls. Sobald das Modul geladen ist beginnt die Ausführung, bei der der Handler-Code an die korrekten Positionen der Beschriebenen Prüfpunkte eingesetzt wird. Das Verfahren ist vergleichbar mit dem Aufruf einer schnellen Subroutine. Begründung SystemTap ist ein mächtiges Framework, das es beispielsweise ermöglicht, Prüfpunkte an strategischen Stellen in den Subsystemen des Betriebssystems zu platzieren. Meist stellen die Subsysteme, wie das gegebene Beispiel für das virtuelle Dateisystem zeigte, zentrale Abstraktionen bereit, deren 2 wird für die Adressierung verwendeter Funktionen und Variablen benötigt und bei der Kompilierung des Kernels erzeugt 6.1. INSTANZIIERUNG DER KOMPONENTEN 97 Funktionen zentrale Anlaufstelle für alle Operationen, die spezifische Hardwarekomponenten betreffen, sind. Prüfpunkte an diesen Funktionen können aus den bereitgestellten Kontextinformationen detailliert die Nutzung spezifischer Hardwarekomponenten einzelnen Prozessen oder Threads zuordnen. Daher eignet sich SystemTap hervorragend zur Implementierung der Profiler. Neben der allgemeinen Eignung bietet SystemTap folgende Vorteile: • Es ist dynamisch, da die Instrumentationen in den laufenden Kernel, ohne Neustart oder Rekompilieren, injiziert werden. • Es ist sicher, da die Einhaltung von Sicherheitsbeschränkungen durch stap überwacht wird. • Es ist effizient, da der Handler-Code im privilegierten Kernmodus, vergleichbar mit dem Aufruf einer schnellen Subroutine, an den vorgegebenen Stellen ausgeführt wird. Darüber hinaus sind immer nur die Prüfpunkte aktiv, die tatsächlich auch angegeben wurden. • Durch die bisher genannten Vorteile eignet es sich zum Einsatz am Produktivsystem. • Es ist flexibel, da es durch eine mächtige Skriptsprache erlaubt, Daten zu analysieren, zu filtern, zu aggregieren, zu transformieren und zusammenzufassen. Damit wird auch eine kontrollierte Ausgabe ermöglicht. • SystemTap liefert bei der Installation bereits eine große Menge Tapsets der Entwickler mit. Dadurch wird eine korrekte Verwendung erleichtert. • Es ist für fast alle Distributionen verfügbar und kann relativ einfach nachgerüstet werden. Zudem steht es unter aktiver Entwicklung und Fragen an die Mailingliste wurden schnell und kompetent beantwortet. Demgegenüber steht eine steile Lernkurve, die unter Anderem von vorhandenen Kenntnissen des Kernels abhängig ist. Auch die Entwicklung effektiver Skripte ist bisweilen schwierig, da Ausführungszeit ein wichtiger Faktor ist und Sicherheitsbeschränkungen eingehalten werden müssen. 6.1.2 Instrumentierung: BTrace Die Instrumentierung der Java-Anwendung wird benötigt, um eine Verbindung zwischen aktuellen Codeartefakt und den Profilern herzustellen. Jrupi ist zunächst unabhängig davon, welche der in Abschnitt 4.2 vorgestellten Techniken verwendet wird. Im Folgenden werden die Vor- und Nachteile der unterschiedlichen Verfahren zusammengefasst. 98 KAPITEL 6. PROOF-OF-CONCEPT IMPLEMENTIERUNG Instrumentierung Quellcode Bytecode Vorteile + einfach + unabhängig von JVM + kein Quellcode nötig + dynamisch möglich Nachteile - Quellcode nötig - unflexibel - komplexer Für Jrupi wird eine Instrumentierung des Bytecodes empfohlen, da dafür kein Quellcode benötigt wird. So können auch Komponenten von Drittanbietern oder externe Bibliotheken vermessen werden. Der Preis dafür ist die höhere Komplexität, weil mit der abstrakteren Bytecode-Repräsentation umgegangen werden muss. Da spezielle Stellen (beispielsweise Methodeneinund ausgang) instrumentiert werden sollen, bieten sich auch Werkzeuge der AOP [KLM+ 97] zur Umsetzung an, um Aspekte zur Übersetzungszeit einbringen zu lassen. Da die zusätzlichen Anweisungen allerdings nur während des Profiling benötigt werden, müssten zwei Versionen der übersetzten Komponente verwaltet werden: eine mit und eine ohne Instrumentierungen. Daher wurde für die Umsetzung der Instrumentierungen dynamische BytecodeManipulation gewählt. Bei diesem Verfahren erfolgt die Manipulation des Bytecodes typischerweise zur Laufzeit durch einen Java-Agenten, der die Dienste der Schnittstelle java.lang.instrument nutzt. Zur Implementierung des Agenten können Frameworks wie JavaAssist[Chi04], BCEL [The06] oder ASM[Bru07] verwendet werden. Der Agent wird entweder mit der JVM gestartet oder dynamisch in die JVM geladen. Weil sich die Implementierung einer flexiblen Lösung zur dynamischen Bytecode-Manipulation relativ komplex darstellt, wurde auf das Framework BTrace 3 zurückgegriffen. Grundlagen BTrace BTrace wurde bei der Entwicklung maßgeblich von Verfahren zur dynamischen Ablaufverfolgung beeinflusst. Daher besitzen die meisten Konzepte, die für SystemTap vorgestellt wurden, auch für BTrace Gültigkeit. Der Gegenstand der Instrumentierung ist jedoch eine Java-Anwendung anstelle des Kernels. BTrace leistet auf der Abstraktionsebene JVM ähnliches wie SystemTap auf Betriebssystemebene. Technisch gesehen verwendet BTrace das ASM Framework zur BytecodeManipulation sowie einen Mechanismus, um den Agenten dynamisch in eine laufende JVM zu laden und wieder aus der JVM zu entfernen. Der Instumentationscode wird beim ersten Laden einer Klasse durch den Class Loader eingebracht. 3 http://kenai.com/projects/btrace 6.1. INSTANZIIERUNG DER KOMPONENTEN 99 Zur Beschreibung der Prüfpunkte verwendet BTrace keine eigene Skriptsprache (wie SystemTap), sondern Java selbst. Ein Prüfpunkt setzt sich aus der Definition eines Events und darauf folgender Aktionen (ähnlich Handler) zusammen. Zur Definition der Events werden Annotationen verwendet und der Handlercode wird innerhalb eines Methodenrumpfes ausgedrückt. Daher erfolgt die Beschreibung der Prüfpunkte innerhalb einer gewöhnlichen Java-Klasse, die aus Methoden der folgenden Form besteht: 1 2 3 4 @Eventbeschreibung public static void einName () { // Aktionen } Die Methoden sind mit einer Beschreibung des Events annotiert, die beispielsweise eine spezielle Stelle im Code der Anwendung beschreiben. Im Rumpf werden die Anweisungen definiert, die bei Eintritt des Events ausgeführt werden sollen. 1 2 @BTrace public class T h r e a d S t a r t T r a c e r { 3 @OnMethod ( clazz = " java . lang . Thread " , method = " start " ) public static void threadStart () { BTraceUtil . println ( " Ein Thread wurde g e s t a r t e t " ) } 4 5 6 7 8 9 } Bei BTrace wird besonderer Wert auf Sicherheit gelegt, da es für eine Verwendung zur Laufzeit ausgelegt ist. Daher dürfen alle Aktionen nur lesend stattfinden und können eine gewisse Ausführungszeit nicht überschreiten. Um sicherzustellen, dass die eingebrachten Anweisungen den Programmzustand nicht verändern, sind lediglich statische Aufrufe an die BTrace-Runtime erlaubt. Begründung BTrace bietet eine ausgereifte Infrastruktur, um zur Laufzeit zusätzliche Anweisungen an diverse Stellen in den Bytecode einzufügen. Damit verfügt es über alle Vorteile, die für die Instrumentierung des Bytecodes genannt wurden. Für Jrupi sind vor allem folgende Eigenschaften von BTrace von Vorteil: • Es arbeitet dynamisch, so dass kein Neustart der JVM oder der JavaAnwendung nötig ist. • Da die Instrumentierung zur Laufzeit aktiviert beziehungsweise deaktiviert werden kann und beim Laden der Klasse durchgeführt wird, kann 100 KAPITEL 6. PROOF-OF-CONCEPT IMPLEMENTIERUNG die unveränderte Anwendung vorübergehend Gegenstand der Instrumentierung sein. Es muss nur eine Version der Anwendung verwaltet werden. • Dieser Umstand garantiert auch Portabilität. Die Instrumentierungsbeschreibung muss nur einmal, zum Beispiel am Entwicklersystem, umgesetzt werden. Anschließend kann sie auf allen anderen System zur Laufzeit in die JVM geladen werden. • Aufgrund der Sicherheitsbeschränkungen eignet sich BTrace zum Einsatz am Produktivsystem. • BTrace ist einfach in der Anwendung. Es ist nicht nötig, sich in die, teilweise komplizierte, Bytecode-Manipulation einzuarbeiten. 6.1.3 Datenaustauschformat: JSON Die mit SystemTap implementierten Profiler erlauben eine kontrollierte und formatierte Ausgabe der gesammelten Daten. Um die Daten für eine spätere Offline-Analyse wieder einlesen zu können, sollte die Ausgabe in Form eines flexiblen Austauschformats erfolgen. Zu diesem Zweck wurde für die Proof-ofConcept Implementierung die JSON4 (JavaScript Object Notation) gewählt. Dabei handelt es sich um eine einfache Sprache im Textformat, die sowohl von Menschen als auch von Maschinen leicht gelesen und geschrieben werden kann. Häufig kommt sie im Bereich von Web-Anwendungen (beispielsweise zum Datenaustausch im Zusammenspiel mit Ajax) zum Einsatz, weil sie gewöhnlich eine kompaktere Repräsentation der Daten als andere Austauschformate (z.B. XML) ermöglicht. In Abbildung 6.3 sind die Kernelemente der Sprache als Syntaxdiagramme angegeben. Die beiden Hauptkonstrukte bilden die universellen Datenstrukturen Objekt (object) und Array (array). • Ein Objekt ist eine ungeordnete Menge bestehend aus Schlüssel- und Wertpaaren. Der Schlüssel ist ein String (string) und der Wert (value) kann ein beliebiges Element sein, das in der JSON definiert ist. Unter anderem kann der Wert wieder ein Objekt sein, so dass die Bildung rekursiver Strukturen möglich ist. • Ein Array ist eine einfache Sammlung von Werten, die durch ein Komma voneinander getrennt werden. Auch hier sind rekursive Strukturen möglich, beispielsweise durch Verschachtelung von Arrays. 4 http://www.json.org/ 101 6.1. INSTANZIIERUNG DER KOMPONENTEN object object {{ string string :: value value }} ,, array array [[ value value ]] ,, value value string string number number object object array array true true false false null null Abbildung 6.3: Kernelemente der JSON als Syntaxdiagramm Durch die Verwendung dieser universellen Konzepte ist die JSON unabhängig von einer konkreten Programmiersprache und kann durch ihre rekursive Natur auch komplexe Strukturen einfach ausdrücken. Da JSON auf einer Untermenge von JavaScript basiert, ist jede Definition, die sich an die gegebenen Syntaxregeln hält auch valides JavaScript und kann mit der eval() Funktion direkt eingelesen werden. In Listing 6.3 ist ein Beispiel für die Verwendung der JSON im Kontext des CPU-Profiling auf Methodenebene angegeben. Die rekursive Natur wird im Beispiel zur Beschreibung der Daten beliebig vieler CPUs („cpu“) verwendet, indem pro CPU ein eigenes Objekt mit dem Schlüssel der CPU-Nummer angelegt wird. 1 { " type " : " m e t h o d _ c p u " , // Typ : CPU - Nutzung einer Methode " s i g n a t u r e " : " a l g o r i t h m s : H e a p S o r t : sort : 1 0 0 0 0 0 ; 5 " , // Signatur " start " :1297082500412911469 , // Startzeit : Nanosekunden seid Epoch " delta " :154187389 , // Au sf üh r un gs ze i t der Methode " cpu " :{ " 0 " :{ " method " :152 , " idle " :0 , " target " :152 , " rest " :2} , // CPU : 0 " 1 " :{ " method " :0 , " idle " :133 , " target " :0 , " rest " :21} // CPU : 1 } 2 3 4 5 6 7 8 9 10 } Listing 6.3: Beispiel: Ein Datensatz in JSON, erzeugt durch den CPU-Profiler für eine Methodenausführung 102 KAPITEL 6. PROOF-OF-CONCEPT IMPLEMENTIERUNG Begründung Die JSON ist ein flexibles Austauschformat, das mit Hilfe weniger und einfacher Strukturen in der Lage ist, beliebig komplexe Daten auf elegante Weise darzustellen. • JSON ist kompakt in der Beschreibung, wodurch der Overhead reduziert wird. Da die Ausgaben der Profiler zur Laufzeit erfolgen, sollte die Menge der aufgezeichneten Daten möglichst gering sein. • Es ist leicht, die Ausgabe eines Profilers um zusätzliche Schlüssel- und Wertpaare zu erweitern. Das Ergebnis ist wieder valides Format, solange die Syntaxregeln beachtet werden. • Es ist für Menschen einfach zu lesen und kann daher direkt zur Einsicht und Analyse der Daten verwendet werden. Für Maschinen ist die JSON einfach zu parsen und zu generieren, so dass die Aufzeichnungen leicht erstellt und weiterverarbeitet werden können. 6.1.4 Intelligente Datenanalyse: Eureqa Nachdem die Daten vom Analyse-Framework aufbereitet und eventuell verdichtet wurden, folgt der letzte Schritt. In der verbleibenden Datenmenge muss nach Zusammenhängen gesucht werden, die das Verhältnis zwischen Eingabe (Methodenparameter und zugehörige Metadaten) und Ausgabe (Ressourcennutzung) nachbilden. Oft sind diese Zusammenhänge nicht direkt ersichtlich und in den Daten „versteckt“. Um in diesem Fall dennoch zu einer sinnvollen Aussage zu kommen, wird die Verwendung von Verfahren aus dem Bereich der intelligenten Datenanalyse (IDA) empfohlen. Diese Datenanalysen kommen (fast) ohne Modellannahmen aus und extrahieren Wissen direkt aus den vorhandenen Datenmengen. Dabei wird das verwendete Verfahren (beziehungsweise dessen Parameter) an die jeweiligen Daten adaptiert. Da derartige Verfahren eine hohe Komplexität aufweisen, arbeiten sie meist heuristisch und finden daher suboptimale Lösungen [Pae06]. Im Zusammenhang mit der IDA sind Soft Computing und Data Mining verwandte Begriffen. Wie solche Verfahren im Detail arbeiten, ist für diese Arbeit nicht von Bedeutung. Von Interesse ist lediglich, dass entsprechende Werkzeuge existieren und sie die formulierte Aufgabe fast automatisch lösen können. Zur Ableitung eines mathematischen Modells wird das Werkzeug Eureqa [SL09] empfohlen, das bei seinem Erscheinen für einiges Aufsehen in der Wissenschaft sorgte. Aus den aufgezeichneten Bewegungsdaten eines Doppelpendels konnte es 6.1. INSTANZIIERUNG DER KOMPONENTEN 103 innerhalb weniger Stunden das zweite newtonsche Gesetz für mechanische Bewegungsgleichungen sowie das Gesetz der Impulserhaltung ableiten. Eureqa nimmt als Eingabe eine beliebige Datenmenge entgegen. Die Daten können zunächst vorbereitet und ergänzt werden. Dabei ist es auch möglich, Beziehungen zwischen einzelnen Datenreihen explizit anzugeben und verschiedene Verfahren aus der statistischen Datenanalyse darauf anzuwenden. Anschließend muss noch angegeben werden, auf welchen Zusammenhang die Daten untersucht werden sollen und welche Bausteine (z.B. Konstanten, Summe, Produkt, Logarithmus, Wurzel, Polynome,...) in der gesuchten Lösung verwendet werden dürfen. Eureqa versucht zunächst, Verbindungen zwischen einzelnen Zahlen in der Datenmenge herzustellen und das Verhältnis mit simplen Gleichungen anzunähern. Die meisten der verwendeten Gleichungen werden zunächst wieder verworfen, wenn der festgestellte Fehler zu groß ist. Die Gleichungen, die den Zusammenhang am besten beschreiben, werden zwischengespeichert, optimiert, neu kombiniert und wieder mit den Daten verglichen. In einem iterativen Prozess kommen so immer bessere Annäherungen an die realen Daten zu Stande, die beispielsweise in einem Diagramm Fehler gegen Komplexität (Gleichung) zusammengefasst werden. In Abbildung 6.4 ist beispielhaft die Ergebnisansicht bei der Auswertung der CPU-Daten einer HeapSort Komponente dargestellt. Abbildung 6.4: Eureqa bei der Auswertung der CPU-Daten einer HeapSortKomponente 104 KAPITEL 6. PROOF-OF-CONCEPT IMPLEMENTIERUNG Begründung Eureqa ermöglicht es, Gleichungen und (versteckte) mathematische Beziehungen in Datenmengen zu ermitteln. Im Einzelnen ergeben sich folgende Vorteile bei der Verwendung: • Eureqa arbeitet bei der Ableitung vollständig automatisch und kommt ohne Modellannahmen aus. Im Gegensatz zu wissensbasierten Verfahren muss kein zusätzliches Expertenwissen eingebracht werden, so dass der Modellentwickler weiter entlastet wird. • Eureqa leitet eine mathematische Beschreibung ab, die ein kompaktes und formales Modell zur Beschreibung des Verhältnisses zwischen Einund Ausgabe darstellt. • Die gefundenen Zusammenhänge werden direkt nach Fehler und Komplexität bewertet. Der Modellentwickler muss lediglich den besten Kompromiss auswählen. • Eureqa bietet viele Einstellmöglichkeiten bezüglich der Art gewünschten Lösung. • Daten können vor der Ableitung bequem editiert und mit Hilfe statistischer Verfahren bearbeitet werden. Der Nachteil bei der Verwendung von Eureqa besteht darin, dass es ein externes Werkzeug ist und nicht ohne weiteres integriert werden kann. Daten müssen daher in ein für Eureqa lesbares Format (wie CSV) exportiert werden. Zudem ist Eureqa nativ nur für Windows vorhanden, kann aber unter Verwendung von Wine5 auch unter Linux eingesetzt werden. 6.2 Technische Umsetzung und Bildung einer Werkzeugkette Im vorangegangenen Abschnitt wurden die durch Jrupi vorgegebenen Komponenten mit konkreten Werkzeugen instanziiert. Bisher sind die einzelnen Werkzeuge und ihre Funktionen als Individuen bekannt. Um dem Modellentwickler eine stringente Arbeitsumgebung zu bieten, müssen die einzelnen Werkzeuge in geeigneter Form zusammenarbeiten. Aufgrund der Unabhängigkeit der Werkzeuge untereinander bietet sich ein lockerer Verbund 5 http://www.winehq.org/ 6.2. TECHNISCHE UMSETZUNG 105 in Gestalt einer Werkzeugkette an. Der kritischste Teil besteht in der Verbindung zwischen Codeartefakten und korrespondierender Ressourcennutzung durch Instrumentierung, worauf Unterabschnitt 6.2.1 näher eingeht. Die Profiler sind durch das JSON-Austauschformat zur Datenbeschreibung lose mit dem Analyse-Framework gekoppelt. Dabei ist das Analyse-Framework Teil der Jrupi-Workbench, die zwei Aufgaben erfüllt. Einerseits unterstützt sie den Modellentwickler bei der Erstellung der Instrumentationen und dem Aufsetzen einer Testumgebung, indem sie Möglichkeiten zur automatischen Codegenerierung bietet. Andererseits setzt sie die Funktionen des AnalyseFrameworks (Datendarstellung, Datenauswertung, Datenexport) um. Im Folgenden werden relevante Details der Umsetzung näher erläutert. 6.2.1 Brücke zwischen Codeartefakt und Profiler Die Verbindung zwischen aktuell ausgeführten Codeartefakt und Ressourcennutzung ist von essentieller Bedeutung für jede Implementierung von Jrupi. Sie unterscheidet diesen Ansatz von anderen Methoden, wie sie von Monitoring- oder Diagnosewerkzeugen verwendet werden. Der Kern des Problems besteht in der Tatsache, dass die Ausführung auf der Abstraktionsebene der JVM stattfindet, die Daten aber an anderer Stelle, nämlich auf Betriebssystemebene erhoben werden. Diese Herangehensweise minimiert den Overhead der Datenerhebung (beziehungsweise verlagert ihn), weil die Profiler ihre Daten an einer komplett anderen Stelle erheben. Sie arbeiten zwar auch mit Instrumentierung (des Kernels), bieten aber die Vorteile des Samplings, da sie die Ausführung der Java-Anwendung nicht (oder kaum) beeinflussen. Um die Nutzung einzelner Hardwareressourcen spezifischen Methoden zuordnen zu können, wird eine Brücke zwischen aktueller Stelle im Bytecode und Profiler auf Systemebene benötigt. Zu diesem Zweck wird BTrace eingesetzt, um zusätzliche Anweisungen dynamisch in den Bytecode zu injizieren, die ein durch die Profiler feststellbares Ereignis auf Betriebssystemebene auslösen. In Abbildung 6.5 ist die konkrete Implementierung der Brücke im Detail dargestellt. Die zentrale Idee besteht darin, aus der Java-Anwendung heraus, an strategischen Stellen, einen Systemaufruf (syscall) auszulösen, der auf Betriebssystemebene durch SystemTap abgefangen und analysiert werden kann. Für die Umsetzung bietet sich die Verwendung des write() Systemaufrufs an, da er über seinen String-Parameter (Payload) zur Übermittlung von Informationen benutzt werden kann, und sich relativ einfach aus Java heraus aufrufen lässt. Er wird von vielen nativen Methoden, die bei Java für die Ausgabe (System.out) zuständig sind, verwendet. So führt auch der Aufruf von System.out.println() in der Konsequenz zu einem write() 106 KAPITEL 6. PROOF-OF-CONCEPT IMPLEMENTIERUNG Java Virtual Machine Betriebssystem 'Package.Class.Method.Params.method_entr' public public void void someMethod(){ someMethod(){ System.out.println(); System.out.println(); //do //do work work }} probe probe syscall.write syscall.write {{ [System] [System] write() write() JNI [C-library] [C-library] write() write() [syscall] write() sysarg sysarg == user_string($buf) user_string($buf) //parse //parse string... string... System.out.println(); System.out.println(); 'Package.Class.Method.Params.method_exit' [BTrace] [BTrace] Bytecode Bytecode Instrumentierung Instrumentierung [Systemtap] [Systemtap] Kernel Kernel Instrumentierung Instrumentierung Abbildung 6.5: Verbindung zwischen JVM und Profiler durch Bytecode Instrumentierung Systemaufruf, der als Parameter genau den übergebenen String besitzt. Daher wird für relevante Methoden, jeweils an Methodeneingang und Methodenausgang, durch BTrace ein System.out.println() Statement eingefügt. Der auszugebende String gehorcht dabei einen festen Format: • Package:Class:Method:Params:method_entr für Methodeneingang. • Package:Class:Method:Params:method_exit für Methodenausgang. Jeder String ist daher ein geordnetes 5-Tupel (Package, Class, Method, Params, Stelle), dessen einzelne Komponenten durch konkrete Werte zu substituieren sind: 1. Package: Name des Packages, das die Klasse enthält 2. Class: Name der Klasse, die die Methode definiert 3. Method: Name der Methode 4. Params: Werte numerischer Parameter der Methode, jeweils durch ein Semikolon (;) getrennt 5. Stelle: Markierung, ob Methodeneingang (method_entr) oder Methodenausgang (method_exit) Die ersten vier Komponenten des 5-Tupels dienen der eindeutigen Identifikation einer Methode, wobei die Parameter darüber hinaus für eine spätere Analyse verwendet werden können. SystemTap definiert einen Prüfpunkt, der als Event auf den Systemaufruf write() reagiert (probe syscall.write). Stellt SystemTap fest, dass 6.2. TECHNISCHE UMSETZUNG 107 ein write() ausgelöst wurde, so wird zunächst geprüft, ob der Auslöser der zu überwachende JVM Prozess ist. Ist dies der Fall, so wird die Zeichenkette (aus user_string($buf)) auf den Inhalt method_entr beziehungsweise method_exit untersucht. Ergibt auch dieser Test ein positives Resultat, so ist davon auszugehen, dass es sich um einen Methodeneingang oder Methodenausgang handelt. Die Zeichenkette wird daraufhin in einzelne Token zerlegt und je nach Art weiter verwendet. Die ersten vier Token der übermittelten Zeichenkette dienen, in Kombination mit der Thread-Id (aus den verfügbaren Kontextinformationen) des aufrufenden Threads, der eindeutigen Identifikation der aktuellen Methode. Zur Verwaltung der aktiven Methoden verwenden die Profiler ein assoziatives Array als Datenstruktur, das folgenden Aufbau besitzt: active_methods[Package.Class.Method.Params,TID] = count Durch die doppelte Zuordnung mit qualifizierter Methodenbezeichnung und Thread-Id wird sichergestellt, dass die Methodeneingang und Methodenausgang auch tatsächlich richtig verwaltet werden (Thread-Safety), da jeder Java-Thread durch die JVM 1:1 auf einen Betriebssystem-Thread abgebildet wird. Die Variable count ermöglicht zusätzlich die Erkennung von rekursiven Methodenaufrufen: • Im Falle eines Methodeneingangs wird sie um Eins inkrementiert. • Im Falle eines Methodenausgangs wird sie um Eins dekrementiert. Ist sie bei Null angelangt, so wurden alle rekursiven Aufrufe erfasst und die Nutzungsdaten der korrekten Methode zugeordnet. Die beschriebenen Statements (System.out.println()) mit passender Zeichenkette als Parameter werden von BTrace zur Laufzeit an die strategischen Stellen injiziert. Dafür muss im Vorfeld eine geeignete Beschreibung in Form einer speziell annotierten BTrace-Klasse erfolgen. Diese Beschreibung besteht aus Paaren von Prüfpunkten, von denen der Eine den Methodeneingang, und der Andere den Methodenausgang einer relevanten Methode beschreibt. In Listing 6.4 ist eine Beispielbeschreibung für die expandierte sort() Methode angegeben. 1 2 3 // um Metadaten expandierte Parame terliste // public T [] sort ( T [] in , int arraySize , int meanSor tLength ) ; 4 5 6 7 8 9 @ O n M e t h o d ( clazz = " a l g o r i t h m s . Q u i c k S o r t " , method = " sort " ) public static void s o r t Q u i c k S o r t E n t r y ( AnyType [] args ) { System . out . println ( " a l g o r i t h m s : Q u i c k S o r t : sort : " + args [1] + " ; " + args [2] + " : m e t h o d _ e n t r : " ) ; } 108 KAPITEL 6. PROOF-OF-CONCEPT IMPLEMENTIERUNG 10 11 12 13 14 15 @ O n M e t h o d ( clazz = " a l g o r i t h m s . Q u i c k S o r t " , method = " sort " , location = @ L o c a t i o n ( value = Kind . RETURN ) ) public static void s o r t Q u i c k S o r t E x i t ( AnyType [] args ) { System . out . println ( " a l g o r i t h m s : Q u i c k S o r t : sort : " + args [1] + " ; " + args [2] + " : m e t h o d _ e x i t : " ) ; } Listing 6.4: BTrace Instrumentierung für expandierte sort() Methode Die Definition der Stelle, die instrumentiert werden soll, unterscheidet sich lediglich im zusätzlichen Annotationsparameter location, wobei der Wert @Location(value = Kind.RETURN) für einen Methodenausgang steht. Im Methodenrumpf werden die Ausgaben beschrieben, so dass sie der zuvor erläuterten Form gerecht werden. Dabei ist zu erkennen, dass BTrace auch in der Lage ist, beliebige Parameter abzufangen. Dazu wird das Array AnyType[] args verwendet. Die Indizes des Arrays korrespondieren mit der Position der Parameter in der Methodensignatur. Für das Beispiel der expandierten sort() Methode sind die erweiterten Metadaten an den Stellen args[1] und args[2] zu finden. Bei der Erstellung der Skripte ist unbedingt darauf zu achten, dass die Parameter sowohl bei Methodeneingang als auch bei Methodenausgang in der gleichen Art und Weise ausgegeben werden, da sie von den Profilern zur eindeutigen Identifikation der Methode verwendet werden. Weil die Beschreibungen für BTrace immer ähnlich aussehen werden, kann die Erstellung durch einen Code-Generator automatisiert werden. Es ändern sich lediglich die Namen und die Parameter, konzeptuell gesehen bleiben die Paare aber gleich. Daher wurde für die Workbench ein solcher Generator implementiert. 6.2.2 Umsetzung der Profiler Die Profiler sind das Herzstück der Implementierung von Jrupi. Ihre Hauptaufgabe besteht darin, geeignete Stellen im Betriebssystem zu instrumentieren, um Daten über die Ressourcennutzung zu sammeln. Als geeignete Stellen bieten sich für gewöhnlich zentrale Kernelfunktionen in den Subsystemen an, die von einer heterogenen Menge Hardwareressourcen des gleichen Gerätetyps abstrahieren. Im Wesentlichen besteht jeder Profilers aus folgenden Bestandteilen: • Einen oder mehrere Prüfpunkte, die die relevanten Subsysteme instrumentieren. • Ein Prüfpunkt zur Instrumentierung der write() Systemaufrufe. Dabei werden die im vorangegangenen Unterabschnitt beschriebenen Auf- 6.2. TECHNISCHE UMSETZUNG 109 gaben wahrgenommen. Die Implementierung sieht immer ähnlich aus, kann aber aus zwei Gründen nicht in ein Tapset ausgelagert werden: Erstens werden diverse Arrays, die Daten für den konkreten Profiler aufzeichnen, an dieser Stelle gelöscht. Zweitens ist eine Kommunikation von Tapset zu Profiler nicht möglich. Die Profiler müssten daher ständig über eine Hilfsfunktion erfragen, ob sich die Liste aktiver Methoden verändert hat (Polling). • Einen asynchronen Prüfpunkt, der, von einen Timer gesteuert, periodisch grob-granulare Informationen zur Ressourcennutzung des Systemumgebung ausgibt. • Eine Funktion, die die Ausgabe auf Methodenebene realisiert, sobald deren Ausführung (letzte Rekursion) beendet ist. Die beiden letztgenannten Bestandteile sind für die Ausgabe im JSON Austauschformat zuständig. Auch diese Ausgaben sind spezifisch für den jeweiligen Profiler. Jedoch wurden Hilfsfunktionen zur einfacheren Verwendung von JSON in ein Tapset ausgelagert. Im Rahmen der Proof-of-Concept Implementierung von Jrupi wurden drei Profiler für zentrale Arten von Hardwarekomponenten eines Rechnersystems umgesetzt: • CPUProfiler: CPUs sind zentraler Bestandteil eines jeden Rechnersystems und für gewöhnlich einer der größten Energieverbraucher. Zum Profiling der CPU-Nutzung bieten sich zwei verschiedene Strategien an. Erstens können die Kernelfunktionen des Scheduler instrumentiert und für jeden Thread die Zeit gemessen werden, die er tatsächlich auf einer CPU verbringt. Dazu bietet sich die Verwendung der Prüfpunkte scheduler.cpu_on und probe scheduler.cpu_off an, die bereits durch ein mitgeliefertes Tapset verfügbar sind. Zweitens kann der Prüfpunkt timer.profile verwendet werden, der bei jedem Timer-Interrupt (system tick) ausgelöst wird. Aus den Kontextinformationen kann daraufhin ermittelt werden, welcher Thread oder Prozess gerade auf welcher CPU abgearbeitet wird. Dieses Vorgehen hat den Charakter von Sampling, da in einer hohen Frequenz an einer anderen Stelle als dem Scheduler die Daten erhoben werden, wodurch weniger Overhead entsteht. Daher wurde sich bei der Implementierung für die zweite Variante entschieden. Die Ausführliche Definition des Prüfpunktes ist in B.2 dargestellt. • Festplatten-Profiler: Festplatten stellen weitere zentrale Komponenten eines Rechnersystems dar und sind, vor allem wenn mehrere zum 110 KAPITEL 6. PROOF-OF-CONCEPT IMPLEMENTIERUNG Einsatz kommen, häufig für einen großen Teil des Energieverbrauchs verantwortlich. Die Prüfpunkte für Festplatten werden im Anschluss an diese Zusammenstellung detaillierter erläutert. • Netzwerk-Profiler: Auch Netzwerkgeräte verbrauchen, abhängig von Art und Anzahl, eine gewisse Menge Energie. Mit dem implementierten Profiler lässt sich die genaue Menge an Traffic ermitteln die, durch Ausführung einer Java-Methode, auf einer spezifischen Netzwerkressource verursacht wurde. Konzeptuell arbeiten die Prüfpunkte analog zu den Prüfpunkten für Festplatten und sind in B.1 dargestellt. Im Folgenden wird exemplarisch ein Prüfpunkt des Festplatten-Profilers genauer erläutert. In Listing 6.5 ist der Prüfpunkt für schreibende Zugriffe (vfs.write.return), der bereits aus Abschnitt 6.1.1 bekannt ist (vergleiche Abbildung 6.1), mit dem Handler-Code des Profilers dargestellt. 1 probe vfs . write . return { 2 if ( $return >0) { // wurde tatsächlich etwas geschrieben ? 3 4 if ( devname != " N / A " ) { /* skip update cache */ 5 6 tid = tid () 7 // Thread - Id aus Kontext 8 if ( pid () == target () ) { // Aufruf i . A . der JVM ? 9 10 // iteriere über alle aktiven Methoden foreach ([ s , t ] in activ e_method s ) { 11 12 13 // eine Methode eines Threads kann nur aktiv sein if ( t == tid ) { 14 15 16 // speichere Gerätename , TID und Menge Bytes method_w [ tid , devname ] += $return break 17 18 19 20 } } // kam von JVM , aber von anderer Stelle target_w [ devname ] += $return 21 22 23 24 25 } else { // zeichne U m g e b u n g s a k t i v i t ä t auf all_w [ devname ] += $return 26 27 28 29 } // aktualisiere Geräteliste if (!([ devname ] in dev_list ) ) dev_list [ devname ] = 1 30 31 32 } 33 } 34 35 } Listing 6.5: Ein Prüfpunkt des Festplatten-Profilers 6.2. TECHNISCHE UMSETZUNG 111 Darüber hinaus definiert der Festplatten-Profiler einen weiteren Prüfpunkt für lesende Zugriffe (vfs.read.return), der nach demselben Muster aufgebaut ist. Zwar geben die eingefügten Kommentare eine kurze Erklärung zu den jeweiligen Stellen im Quellcode die wichtigsten Eigenschaften sollen aber dennoch kurz erläutert werden. • Zeile 12: Zu diesem Zeitpunkt steht fest, dass etwas geschrieben wurde und der anfordernde Prozess der JVM entsprach. Daher wird nun über das Array der aktiven Methoden iteriert, um festzustellen, ob die auslösende Methode eine der überwachten Methoden ist. Das Array der in Ausführung befindlichen Methoden ist gewöhnlich relativ klein, so dass sich der Overhead der Iteration in Grenzen hält. • Zeile 15: An dieser Stelle wird überprüft, ob die Thread-ID des auslösenden Threads des JVM-Prozesses in der Liste aktiver Methoden geführt wird, da der richtige Methodenname am Prüfpunkt nicht verfügbar ist. Allerdings reicht, aufgrund der erläuterten Arbeitsweise der JVM bei der Methodenausführung (vergleiche Unterabschnitt 4.1.4) in Kombination mit dem 1:1 Mapping zwischen Java-Thread und Betriebssystem-Thread, der Abgleich der Thread-ID aus, um eine korrekte Zuordnung zu einer speziellen Methode zu ermöglichen. • Zeile 18: Die Anzahl geschriebener Bytes wird nun in ein assoziatives Array mit Thread-ID und Gerätebezeichnung abgelegt. Wird später festgestellt, dass eine Methode mit ihrer kompletten Ausführung fertig ist, so kann wiederum über die Thread-ID der korrekte Wert geschriebener Bytes und die genaue Gerätebezeichnung der richtigen Methode zugeordnet werden. • Zeile 24, 28: Diese beiden Statements dienen dem Profiling der Systemumgebung. Das Erste zeichnet Festplattenaktivitäten der JVM, das Zweite der restlichen Prozesse auf. Um die Profiler möglichst effizient zu programmieren, wurde auf eine Zuordnung zu einzelnen Threads oder Prozessen verzichtet. Assoziative Arrays bieten eine einfache Möglichkeit, eine Menge Datensätze miteinander in Verbindung zu setzen und aufzuzeichnen. Ihr Einsatz sollte jedoch mit Bedacht erfolgen. So darf ihre maximale Größe gewisse Werte nicht überschreiten. Dazu ist es nötig, die Arrays effektiv zu verwalten und ihre Daten zu geeigneten Zeitpunkten zu löschen. Darüber hinaus kann eine exzessive Verwendung den Profiler ineffizient machen, da assoziative Arrays 112 KAPITEL 6. PROOF-OF-CONCEPT IMPLEMENTIERUNG als globale Variabel über einen Locking-Mechanismus geschützt werden müssen. Das kann sich unter Anderem im Überspringen einzelner Prüfpunkte äußern, da ihre maximale Ausführungszeit gewissen Grenzen unterliegt. Daher sollten bei der Programmierung von Profilern einige Grundregeln beachtet werden: • Nur die Daten speichern, die tatsächlich benötigt werden. • Arrays regelmäßig und so früh wie möglich, ganz oder teilweise (einzelne Elemente), löschen (Clean-Up). Hierfür bietet sich meist die Ausgabe an. • Möglichst wenige Berechnungen innerhalb der Profiler anstellen, wenn sie auch außerhalb erfolgen können. Mit SystemTap kann ein Profiling fast beliebig genau umgesetzt werden. Jedoch sollten dabei immer auch der eingebrachte Overhead sowie die Sicherheitsbeschränkungen, denen SystemTap unterliegt, Beachtung finden. Die größte Herausforderung besteht zumeist in einer Optimierung zwischen der Effizienz des Profilers und dem Detailgrad der erhobenen Daten. 6.2.3 Umsetzung der JRUPI-Workbench Die Jrupi-Workbench erfüllt im Wesentlichen zwei Aufgaben: Erstens automatisiert sie Teile des Jrupi Workflows, indem sie Codegeneratoren zur Verfügung stellt. Zweitens beherbergt sie die Implementierung das AnalyseFramework. Die Workbench setzt dabei auf das Eclipse-Framework auf und erweitert die Eclipse-IDE um zusätzliche Plugins [GB04]. Damit steht für eventuelle Erweiterungen des Analyse-Frameworks, beispielsweise um zusätzliche Komponenten für die Datenanalyse neuer Profiler zu implementieren, der flexible Erweiterungsmechanismus von Eclipse zur Verfügung. Darüber hinaus bietet sich Eclipse zur Entwicklung von JavaAnwendungen an und beheimatet ebenfalls die CoolSoftware-Workbench. So steht dem Anwender eine konsistente Entwicklungsumgebung für den gesamten Softwareentwicklungsprozess zur Verfügung. Automatisierung durch Codegenerierung Der Hauptgegenstand der Codegenerierung ist die aufwändige und fehleranfällige Erstellung der BTrace-Instrumentierungsbeschreibungen. Für jede Methode, die profiliert werden soll, wird ein Paar Prüfpunkte, jeweils bestehend aus Methodeneingang und Methodenausgang, benötigt. Wie Listing 6.4 113 6.2. TECHNISCHE UMSETZUNG bereits zeigte, unterscheidet sich die Definition dieser beiden Prüfpunkte lediglich im zusätzlichen Annotationsparameter (location) am Methodenausgang sowie in der letzten Komponente des auszugebenden Identifikationsstrings (method_entr oder method_exit). Ist der qualifizierte Klassenname sowie die Signatur der Methode verfügbar, so kann die Erstellung vollständig automatisiert werden. Neben der Codegenerierung für BTrace-Skripte wurde ein Generator implementiert, der das Skelett einer einfachen Testumgebung aufsetzt. Sie kann zunächst als Treiber für einzelne Microbenchmarks verwendet werden und orientiert sich im Aufbau an den besonderen Anforderung an das Benchmarking. In Abbildung 6.6 ist die generierte Klassenstruktur dargestellt. Main Main runSUT() AbstractSUTRunner AbstractSUTRunner setUp() warmUp() run() runSUT() //Testdaten aufsetzen setUp(); //JVM aufwärmen warmUp(); /*Unterbrechung zum Starten der Profiler & BTrace */ //eigentlicher Testlauf run(); SUTRunner SUTRunner setUp() warmUp() run() Abbildung 6.6: Simples SUT-Skellet, wie es vom Codegenerator erstellt wird Als Einstiegspunkt (main() Methode) wird die Klasse Main erstellt. Sie hält eine Referenz auf den abstrakten AbstractSUTRunner, der die Ausführung eines Testlaufs koordiniert. Die abstrakten Methoden sind vom Ersteller des Benchmarks in der Unterklasse SUTRunner zu implementieren und werden von der Methode runSUT() der abstrakten Klasse in folgender Reihenfolge aufgerufen: 1. setUp() dient dem isolierten Aufsetzen von Testdaten, die später als Parameter der zu bemessenden Methoden dienen. Diese Maßnahme soll verhindern, dass die Testdatenerstellung Einfluss auf die Messungen ausübt. 2. warmUp() dient dem Aufwärmen der JVM, damit Methoden durch JITbeziehungsweise Hotspot-Kompilierung hinreichend optimiert werden 114 KAPITEL 6. PROOF-OF-CONCEPT IMPLEMENTIERUNG können. Dafür werden sie typischerweise mehrfach mit den generierten Testdaten aufgerufen. 3. run() dient der eigentlichen Ausführung der Microbenchmarks. An dieser Stelle muss der Entwickler entscheiden, welche Methoden wie oft mit was für Parametern aufgerufen werden. Beim Aufruf der einzelnen Methoden werden zusätzliche Ausgaben auf der Konsole erzeugt, um die aktuelle Stelle in der Ausführung zu zeigen. Nachdem der zweite Schritt (Aufwärmphase) beendet ist, wird die Ausführung unterbrochen, damit der Nutzer die Möglichkeit hat, die Instrumentierungen mit BTrace in die JVM zu laden und die Profiler zu starten. Dieses Vorgehen ist (bei eigenständigen Tests) nötig, da sowohl BTrace als auch SystemTap die PID der JVM übergeben werden muss. Sobald Instrumentierungen und Profiler gestartet sind, wird die Ausführung, nach Drücken einer beliebigen Taste, durch Aufruf der Methode run() fortgesetzt. Die Integration in die grafische Oberfläche der Eclipse-IDE wurde in Form eines Wizards implementiert. Er kann über einen Button gestartet werden, wenn im Package Explorer ein Package selektiert ist. Abbildung 6.7 zeigt die Oberfläche des Wizards, wobei beim Start ein Package mit verschiedenen Sortieralgorithmen in unterschiedlichen Klassen ausgewählt war. In der obersten Sektion ist der Zielort für die Codegenerierung einstellbar. Angegebene Packages und Pfade werden automatisch angelegt. In der mittleren Sektion können Konfigurationen für die Codegenerierung vorgenommen werden. Dabei lässt sich festlegen, was generiert werden soll und ob der Generator BTrace in den Classpath des Projekts hinzugefügt. Letzteres ist nicht zwingend erforderlich, vermeidet aber die Anzeige von Fehlern in Eclipse. Die untere Sektion dient schließlich derjenigen Methoden, für die im BTrace-Skript Prüfpunkte zu definieren sind. Zur Implementierung der Codegeneratoren wurde weitgehend auf die Java Development Tools (JDT)6 für Eclipse aufgebaut, die eine interne Repräsentation aller Java-Sprachelemente besitzen. Daraus lassen sich die benötigten Daten auslesen und als Eingabe für die Codegeneratoren verwenden. Analyse-Framework in Eclipse Die Aufgabe des Analyse-Framework ist die Darstellung und Auswertung der Daten, die durch die Profiler gesammelt wurden. Darüber hinaus kümmert es sich (optional) um den Export der ausgewerteten Daten in ein werkzeugspezifisches Format, das von Werkzeugen zur intelligenten Datenanalyse 6 http://www.eclipse.org/jdt/ 6.2. TECHNISCHE UMSETZUNG 115 Abbildung 6.7: Wizard zur Code-Generierung wieder eingelesen werden kann. Bei dieser Betrachtung wirkt das AnalyseFramework daher wie ein Filter innerhalb der Werkzeugkette. Zunächst muss das Analyse-Framework in der Lage sein, die Daten der Profiler, die im JSON-Format vorliegen, einzulesen. Daher wurde, für die beschriebene JSON-Syntax, mit Hilfe von EMFText7 eine Textsprache entwickelt. Zur Definition der textuellen Syntax benötigt EMFText ein EcoreMetamodell, das mit dem Eclipse Modeling Framework (EMF) [SBPM08] erstellt werden kann, und die einzelnen Sprachelemente sowie ihre Beziehungen beschreibt. Die Verwendung von EMFText zur Sprachdefinition bietet folgende Vorteile: • Es wird automatisch ein Editor mit Syntaxhervorhebung und Syntax7 EMFText dient üblicherweise der Definition domänenspezifischer Sprachen (DSL), siehe http://www.emftext.org/ 116 KAPITEL 6. PROOF-OF-CONCEPT IMPLEMENTIERUNG überprüfung (sowie weiteren aus Eclipse bekannten Funktionen) generiert, der als eine rudimentäre Form der Datendarstellung betrachtet werden kann. • Da EMFText im Hintergrund mit einer Instanz des definierten EcoreMetamodells arbeitet, können die EMF-Datenstrukturen (wie Resource, ResourceSet) zum programmatischen Zugriff auf die Daten verwendet werden. Die Inhalte dieser Datenstrukturen entsprechen den instanziierten Metamodellelementen und sind einfach zu traversieren (z.B. mit einem TreeIterator). Die von den Profilern erhobenen Daten liegen in Form einer Datei vor, die in Eclipse mit dem generierten Editor geöffnet werden kann. Der Zugriff auf die Rohdaten sollte allerdings nur lesend erfolgen, um sie für verschiedene Analysen wiederverwenden zu können. Zur Darstellung und Analyse empfiehlt sich die Verwendung eigener, interner Datenstrukturen, da aus den gesammelten Daten beispielsweise zusätzliche Felder berechnet werden können. Außerdem können die Daten im Rahmen einer statistischen Auswertung hinsichtlich ihrer Qualität bewertet werden, indem zum Beispiel aus mehreren Datensätzen (mit gleichen Parametern) der Mittelwert sowie die Standardabweichung bestimmt wird. In diesem Fall werden viele Datensätze durch einen einzigen ersetzt, der als zusätzliches Feld beispielsweise die Standardabweichung aufweist. Daher sollten derartige Operationen nicht auf den Originaldaten erfolgen. Zur Datendarstellung bietet sich die Erweiterung der Eclipse-Workbench um zusätzliche Views [GB04] an. Derzeit existieren für die diversen Profilertypen tabellarische Darstellungsformen, die die Daten möglichst übersichtlich anzeigen. Abbildung 6.8 zeigt beispielhaft die CPU-View. Abbildung 6.8: Die CPU-View des Analyse-Framework, mit ausklappbaren Detaildaten 6.2. TECHNISCHE UMSETZUNG 117 Links in der Tabelle ist zu erkennen, dass es sich bei den Aufzeichnungen um die Methode sort() der Klasse Heapsort handelt, die mit verschiedenen Parametern in mehrfachen Wiederholungen aufgerufen wurde. Neben Startzeit (start) und Ausführungsdauer (duration) ist die Anzahl der system ticks, in der sich die Methode während der Datenerhebung auf der CPU befand, dargestellt. Da zu diesem Zeitpunkt noch keine statistische Datenauswertung stattfand, entspricht der Mittelwert (mean) noch der Gesamtzahl (total), und die Standardabweichung (deviation) ist Null. Die Tabellen sind, um eine bessere Übersicht zu gewährleisten, ausklappbar (>>). Im nicht sichtbaren Teil befinden sich Daten zur CPU Nutzung der Systemumgebung sowie detaillierte Daten, die auf die einzelnen CPUs im System aufgeteilt sind. Erweiterung um zusätzliche Profiler Werden weitere Profiler mit SystemTap implementiert, die beispielsweise andere Hardwarekomponenten überwachen oder verschiedene Kontextinformationen zur Verfügung stellen, so muss das Analyse-Framework um Komponenten, die die gesammelten Daten einlesen, darstellen und verarbeiten können, erweiterbar sein. Die Komponenten, die für jeweils einen Profiler zuständig sind, werden im Kontext des Analyse-Frameworks als Analyser bezeichnet. Um die einfache Erweiterbarkeit zu garantieren, definiert das Analyse-Framework einen Erweiterungspunkt (extension-point) mit zugehörigen Schema: • Erweiterungspunkt: org.coolsoftware.profiling.workbench.analysis.analyser • Das Schema gibt die Struktur vor, die von der Erweiterung eingehalten werden muss. Abbildung 6.9 zeigt den Aufbau des Erweiterungspunktes. Jeder Nutzer des Erweiterungspunktes ProfilerAnalyser muss mindestens eine AnalyserController Erweiterung implementieren. Von besonderer Bedeutung bei der Definition der Erweiterung ist das verpflichtende Attribut type. Über dieses Attribut wird die Erweiterung eindeutig als die Komponente identifiziert, die für die Verarbeitung des, in der Ausgabe des Profilers angegebenen Typs, geeignet ist. Das Analyse-Framework verwaltet alle Erweiterungen, die den definierten Erweiterungspunkt nutzen und wählt, auf Grundlage der eingelesenen JSON-Datei, automatisch die passende aus. Des Weiteren muss die Schnittstelle IAnalyserController von der Erweiterung implementiert werden. Sie definiert die Methode analyseJSON(), 118 KAPITEL 6. PROOF-OF-CONCEPT IMPLEMENTIERUNG <<extension-point>> ProfilerAnalyser 1..* <<element>> <<element>> AnalyserController AnalyserController id: id: string string type: type: string string class: class: implements implements <<interface>> <<interface>> IAnalyserController IAnalyserController Ausgabe des neuen Profiler: { "profiler_type":"cpu_start_flightbox", "time":1297082491078821571, "interval":2000 } [...] analyseJSON(JsonResource jres) Abbildung 6.9: Schema des Erweiterungspunktes für Analyser der als Parameter eine JsonResource (spezielle EMF-Ressource) übergeben wird. Sobald die korrekte Erweiterung zur Verarbeitung der eingelesenen JSON-Daten vom Analyse-Framework gefunden wurde, wird über diese Methode die Ressource übergeben. Ab diesem Zeitpunkt liegt die weitere Vorgehensweise bei der Erweiterung. Auch die bestehenden Implementierungen CPU-, Netzwerk- und Festplattendaten verwenden den definierten Erweiterungspunkt. Sie sind nach dem Model-View-Controller Muster implementiert und öffnen automatisch passende Eclipse-Views, sobald ihre analyseJSON() Methode aufgerufen wird. Des Weiteren wurden die Apache Commons Mathematics Library 8 für statistische Auswertungen sowie das Nebula Project 9 zur Erstellung der Tabellen als externe Bibliotheken in das Analyse-Framework integriert, so dass auch sie von potenziellen Erweiterungen verwendet werden können. 8 9 http://commons.apache.org/math/ http://www.eclipse.org/nebula/ Kapitel 7 Evaluation Tip 15 [HT99] „Use Tracer Bullets to Find the Target “ Zur Ermöglichung eines aktiven Energiemanagements, wie es beispielsweise die CoolSoftware-Architektur anstrebt, ist eine abstrakte Beschreibung des Ressourcennutzungsverhaltens von Software hilfreich. Stehen den verwaltenden Autoritäten derartige Beschreibungen in kompakter und formaler Form zur Verfügung, so können sie proaktiv handeln, anstatt nur auf die Gegebenheiten zu reagieren, um einen energieoptimalen Zustand des Gesamtsystems herbeizuführen. Es war das Ziel dieser Arbeit, die Ressourcennutzung von Java-Codeartefakten ermittelbar zu machen und eine abstrakte Verhaltensbeschreibung daraus abzuleiten, die auch für CCM Komponenten verwendet werden kann. Da in der Praxis keine zufriedenstellende Lösung bekannt ist, um die physische Ressourcennutzung einzelnen Java-Codeartefakten zuzuordnen, musste ein eigener Ansatz entwickelt werden. Zu diesem Zweck wurde das Jrupi Framework eingeführt, dessen Aufgabe es ist, einen konzeptuellen Rahmen vorzugeben, wie das formulierte Ziel erreicht werden kann. Das Vorgehen bei der Proof-of-Concept Implementierung wurde nach einem Verfahren entwickelt, das in [HT99] als Tracer Bullets (Leuchtspurmunition) bezeichnet wird. Da sehr viele Variablen und Unbekannten in der Umgebung existieren, wurden zunächst viele alternative Werkzeuge und Implementierungen getestet, um zu sehen, mit welcher Kombination das Gesamtziel erreichbar ist. Eine Entwicklung nach diesem Prinzip produziert keinen Code, der wieder verworfen und durch den Eigentlichen ersetzt wird (wie beim Prototyp), sondern zeigt den Weg zur Lösung auf, die anschließend um die verbliebenen Funktionen erweitert werden muss. Aufgrund der begrenzten Bearbeitungszeit einerseits, und der Spannbreite des Themas andererseits, 119 120 KAPITEL 7. EVALUATION erschien dieses Verfahren als angemessen. Die prinzipielle Umsetzbarkeit von Jrupi ist durch die Proof-of-Concept Implementierung bereits gezeigt worden. Nun gilt es noch zu zeigen, dass, um in der Metapher zu bleiben, die Tracer Bullets auch das Ziel getroffen hat. 7.1 Fallbeispiel: Sortieralgorithmen Zur Evaluation der Anwendbarkeit wurde mit Sortieralgorithmen ein anschauliches Beispiel ausgewählt, das auch in der Praxis in dieser Form vorkommen könnte. Das Sortieren einer Liste von Elementen nach einen bestimmten Sortierkriterium ist ein grundlegendes algorithmisches Problem und unterschiedliche Sortierverfahren sind daher gut erforscht. So ist typischerweise die Laufzeitkomplexität der einzelnen Algorithmen für den besten (Best-Case), den durchschnittlichen (Average-Case)und den schlechtesten Fall (Worst-Case) bekannt. Die Komplexität von Algorithmen wird häufig in der Landau-Notation O() angegeben, wobei O eine obere Komplexitätsgrenze (z.B. für Rechenzeit) beschreibt. Ist beispielsweise die Laufzeitkomplexität eines Sortieralgorithmus mit O(n2 ) angegeben, so bedeutet das ausgeschrieben O(g) mit g : n 7→ n2 bzw g(n) = n2 (7.1) Wird nun die Anzahl der zu sortierenden Elemente verdoppelt, steigt die Laufzeit annäherungsweise um ein Vierfaches [HT99]. Als Funktion ausgedrückt ergibt sich g(2n) = (2n)2 = 22 n2 = 4n2 (7.2) Die O-Notation ist jedoch mit Vorsicht zu genießen, da verschiedene Algorithmen üblicherweise in Klassen (z.B. O(1), O(n), O(n·log(n)), O(n2 ),...) eingeteilt werden. Alle Terme niedrigerer Ordnung beziehungsweise Konstanten werden ignoriert, wie das folgende Beispiel zeigt [HT99]. O( n2 n2 + 3n) ⇔ O( ) ⇔ O(n2 ) 2 2 (7.3) Das bedeutet in der Konsequenz, dass bei zwei Algorithmen, die derselben Klasse angehören, einer von beiden um ein Vielfaches schneller sein kann als der andere. Daher erscheint die Verwendung von Sortieralgorithmen für die Evaluation besonders geeignet, da sich die Ergebnisse, soweit es die CPU betrifft, mit ihrer jeweiligen Komplexitätsklasse gegenprüfen lassen. Die gegebenen Komplexitäten stellen zwar lediglich grobe Abschätzungen dar, sollten sich aber dennoch im Ergebnis widerspiegeln. Zudem können an diesen Beispiel folgende Eigenschaften gezeigt werden: 121 7.1. FALLBEISPIEL: SORTIERALGORITHMEN • Sortieralgorithmen zeigen die Problematik auf, dass die Ressourcennutzung von den Eingabedaten (Methodenparametern) abhängig ist. Damit ist indirekt auch der resultierende Energieverbrauch von den Parametern abhängig. • Da die Eingabedaten für Sortierverfahren aus einer Liste von Elementen besteht, wird die Notwendigkeit von Metadaten (wie Größe der Liste oder durchschnittliche Länge der Zeichenkette) gezeigt, um eine mathematische Beschreibung der Ressourcennutzung zu ermöglichen. • Die Ausführung der Algorithmen belastet die CPU relativ stark und die unterschiedlichen Laufzeitkomplexitäten lassen vermuten, dass sich die automatische Ableitung einer mathematischen Formel mit Eureqa als vorteilhaft erweist. 7.1.1 Kurzvorstellung der Algorithmen Zur Demonstration des Profiling der CPU-Nutzung und der Datenauswertung wurden die drei bekannten Soriteralgorithmen Bubblesort, Quicksort und Heapsort ausgewählt. Die folgende Tabelle zeigt eine Gegenüberstellung der Komplexitäten der einzelnen Algorithmen [Lan03]. Algorithmus Bubblesort Quicksort Heapsort Worst-Case O(n2 ) O(n2 ) O(n · log(n)) Average-Case O(n2 ) O(n · log(n)) O(n · log(n)) Best-Case O(n) O(n · log(n)) O(n · log(n)) Bubblesort ist der einfachste der angegebenen Algorithmen und wird häufig als naives Negativbeispiel angeführt. Er durchläuft immer wieder das Array und vertauscht direkt benachbarte Elemente (auf Grundlage eines Vergleichs), wenn nötig. Der Algorithmus ist abgearbeitet, wenn in einem Durchlauf kein Vertauschen mehr notwendig ist. Daher kommt für den besten Fall, der genau dann eintritt, wenn die Liste bereits sortiert ist, eine Komplexität von O(n) zustande. Für alle anderen Fälle gilt O(n2 ). Quicksort gilt als eines der schnellsten Sortierverfahren und arbeitet nach dem Divide-and-Conquer-Prinzip. Die zu sortierende Folge wird dabei in zwei Teilstücke zerlegt (divide), die durch ein Vergleichselement X getrennt werden. Alle Werte kleiner X kommen in das erste, alle Elemente größer X in das zweite Teilstück. Daraufhin wird dasselbe Verfahren rekursiv auf die beiden Teilstücke angewandt. Werden die Teilstücke wieder zusammengesetzt, ergibt sich die sortierte Folge. Im schlechtesten Fall besitzt Quicksort die 122 KAPITEL 7. EVALUATION Komplexität O(n2 ). Dieser Fall ist aber sehr unwahrscheinlich und tritt nur dann ein, wenn ein Teilstück stets nur ein Element besitzt [Lan03]. Der letzte Testkandidat ist Heapsort, der als Datenstruktur einen binären Baum (den Heap) verwendet, dessen Knoten die zu sortierenden Elemente repräsentieren. Die Sortierung erfolgt durch eine Umordnung der Knotenmarken. Das genaue Verfahren kann beispielsweise in [Lan03] nachgelesen werden. Da die Verwaltung des Heaps relativ aufwändig ist, gilt Heapsort im durchschnittlichen Fall als langsamer im Vergleich zu Quicksort. Der Algorithmus garantiert in jedem Fall O(n · log(n)). Bubblesort tritt aufgrund seiner quadratischen Laufzeit außer Konkurrenz an. Interessanter ist ein direkter Vergleich zwischen Quick- und Heapsort bezüglich ihrer CPU-Nutzung. In Bezug auf die durchschnittliche Laufzeit gilt Quicksort als einer der schnellsten Algorithmen (auch im Vergleich zu Heapsort). 7.1.2 Setup der Testumgebung Ein vorstellbarer Anwendungsfall im Zusammenhang mit CoolSoftware wäre die Definition einer Sortierkomponente. Als Varianten könnten dann beispielsweise Implementierungen von Bubblesort, Quicksort und Heapsort zur Verfügung stehen. Die Komponente bietet den Port sort() an, der ein Array von Elementen, die untereinander vergleichbar1 sind, entgegen nimmt, und das sortierte Array zurück gibt. Aufgrund der vielen Gemeinsamkeiten zwischen Komponenten und Objektorientierung, kann der Port für das Profiling durch eine normale Methode implementiert werden. Jede Implementierungsvariante wird durch eine eigenen Klasse repräsentiert, die das Interface Sort implementiert. In Abbildung 7.1 ist der Aufbau der Testumgebung als Klassendiagramm dargestellt. Das Skelett der Testumgebung wurde zunächst mit den Codegeneratoren der Jrupi-Workbench (vergleiche Unterabschnitt 6.2.3) erstellt. Dabei wurden die Klassen AbstractSUTRunner, SUTRunner und Main generiert. Das Interface Sort sowie die implementierenden Klasse BubbleSort, QuickSort und HeapSort stehen stellvertretend für die Komponente und ihre Varianten. Sie können daher als gegeben angesehen werden und sind der Gegenstand der Untersuchung. Der Einfachheit halber wurde in diesem Fall die Parameterliste der sort() Methode direkt um die, mutmaßlich nützlichen, Metadaten (arraySize und meanLength) erweitert. Die Klasse Person stellt den Sortiergegenstand dar, mit dem Attribut name als Sortierkriterium. Die in Abschnitt 6.2 beschriebenen BTrace-Instrumentierungen wurden 1 in Java durch Implementierung des Interface Comparable 123 7.1. FALLBEISPIEL: SORTIERALGORITHMEN AbstractSUTRunner AbstractSUTRunner runSUT() Main Main setUp() warmUp() run() runSUT() * Person Person name:String SUTRunner SUTRunner setUp() warmUp() run() <<interface>> <<interface>> Sort Sort sort(in:T[], arraySize:int , meanLength:int):T[] BubbleSort BubbleSort QuickSort QuickSort HeapSort HeapSort Abbildung 7.1: Profiling-Setup für Sortieralgorithmen ebenfalls durch die Codegenerierung der Jrupi-Workbench vollständig automatisch erstellt (vergleiche Anhang B.2). Sie wurden für die sort() Methode jeder einzelnen Klasse der Sortieralgorithmen (allerdings im selben BTraceSkript) generiert. Zur Instrumentierung sollten immer die konkreten Klassen und nicht die Schnittstelle verwendet werden, da nur so eine eindeutige Identifikation und Zuordnung (im Nachhinein, die Datensammlung ist nicht betroffen) möglich ist. Es existiert allerdings noch ein weiterer Grund, der in Unterabschnitt 4.2.4 beim Thema Benchmarking bereits erläutert wurde. Zur Datenerhebung, vor allem in Bezug auf CPU-Daten, sollten die Methoden nicht über eine Schnittstelle aufgerufen werden, um Probleme mit dem Inlining der JVM zu vermeiden. Daher ruft (vergleiche Abbildung 7.1) der SUTRunner die Methoden direkt in den jeweiligen Klassen auf, anstatt die Schnittstelle zu verwenden. Allgemein sollten die erläuterten Hinweise zur Erstellung von Benchmarks und die Arbeitsweise der JVM beachtet werden (vergleiche Unterabschnitte 4.1.4 und 4.2.4). Das ist vor allem beim CPUProfiling wichtig, da die Optimierungen bei der JIT-Kompilierung größtenteils darauf abzielen. 124 KAPITEL 7. EVALUATION Verwendete Testdaten Für Sortieralgorithmen ist bekannt, dass die Art und der Umfang der verwendeten Testdaten Einfluss auf die Laufzeit hat. Der Einfluss der Größe der zu sortierenden Datenliste ist direkt in der Komplexitätsbeschreibung ablesbar. Interessant ist beim CPU-Profiling, ob eine direkte Korrelation zur CPU-Nutzung feststellbar ist. Weiterhin ist bekannt, dass, abhängig von der Arbeitsweise des Sortierverfahrens, die Vorsortierung der Daten Einfluss auf die Komplexität des Soriteralgorithmus (z.B. Bubblesort Best-Case) haben kann. Dieser Umstand wird nicht betrachtet, da er keine allgemeine Aussage über die Qualität eines Verfahrens erlaubt. Die Testdaten werden nach folgenden Verfahren erzeugt: • Arrays unterschiedlicher Größe, die Objekte der Klasse Person halten. • Für jede Person wird ein zufälliger String generiert, der das Attribut name als Sortierkriterium vorgibt, und aus zufälligen Zeichen des Alphabets besteht. • Der zufällig generierte String hat innerhalb eines Arrays immer dieselbe Länge. So kann der Parameter der durchschnittlichen Länge direkt auf seinen Einfluss untersucht werden. Mit den generierten Testdaten sollten folgende Fragen bei der Evaluation beantwortet werden können: • Welche Parameter haben wie viel Einfluss auf die CPU-Nutzung? • Wie unterscheiden sich die Verfahren bei der Verwendung der CPU? • Existiert eine Korrelation zwischen CPU-Nutzung und Ausführungszeit der Methode? • Spiegeln sich die Komplexitäten in der CPU-Nutzung wieder? Das Hauptziel besteht darin, die Anwendbarkeit der Proof-of-Concept Implementierung von Jrupi zu demonstrieren. Das umfasst einerseits die Erhebung der notwendigen Daten und andererseits die Ableitung eine mathematischen Formel, um die CPU-Nutzung der Methoden (respektive Ports) zu charakterisieren. 125 7.1. FALLBEISPIEL: SORTIERALGORITHMEN 7.1.3 Auswertung der Ergebnisse Da die Laufzeitkomplexitäten der Algorithmen durchaus Unterschiede aufweisen, wurde zunächst ein Testlauf mit relativ kleinen Arrays gefahren, um die einzelnen Verfahren einzuschätzen. Vor allem die quadratische Komplexität von Bubblesort lässt auf ein sehr schnelles Ansteigen der Laufzeit schließen. Als Testdaten kamen Arrays der Größe 10.000 bis 40.000 mit der Schrittweite 10.000 zum Einsatz. Die Länge der Strings betrug fünf zufällige Zeichen und die Ausführung wurde fünf mal pro Arraygröße wiederholt. Im folgenden sind die Ergebnisse in tabellarischer Form dargestellt: Anzahl (n) Bubblesort 10.000 2.138 20.000 12.433 30.000 44.828 40.000 70.757 Quicksort 5 14 19 25 Heapsort 7 24 27 61 CPU-Nutzung der Algorithmen (system ticks auf CPU) Die in der Tabelle dargestellten Ergebnisse mögen auf den ersten Blick überraschen, sind aber korrekt und durch mehrfache Messungen überprüft. Damit werden gleich mehrere Probleme deutlich: • Das quadratische Wachstum der Ausführungszeit von Bubblesort ist ein echtes Problem, das dazu führt, dass er nicht ohne Weiteres mit denselben Testdaten wie die anderen beiden Algorithmen arbeiten kann. So benötigte er für 40.000 Elemente im schlimmsten Fall über 80 Sekunden. Zum Vergleich: Heap- beziehungsweise Quicksort schafften eine Million Elemente durchschnittlich in knapp über 3 beziehungsweise knapp unter 2 Sekunden. • Die Werte für Heap- und Quicksort sind sehr klein, so dass mit ihnen noch keine vernünftige Aussage möglich ist. Dieses Problem ist noch offensichtlicher, wenn die Ausführungszeiten noch kleinerer Arrays betrachtet werden: Anzahl (n) Quicksort Heapsort 500 230.000 250.000 1.000 450.000 520.000 Ausführungsdauer in Nanosekunden 126 KAPITEL 7. EVALUATION Die Zeit, die Quicksort zum sortieren von 500 Einträgen benötigt, kann wie folgt in Millisekunden und Sekunden umgerechnet werden. 1ns = 10−6 ms ⇒ 230000ns = 0, 23ms = 0, 00023s (7.4) Die Problematik besteht bei solch kurzen Laufzeiten darin, dass der TimerInterrupt, der für das Profiling verwendet wird, nicht in einem so hohen Takt aufgerufen wird (typischerweise wird er zwischen 500 und 2.000 mal pro Sekunde aufgerufen, abhängig von der CPU-Architektur). Daher sind derartig geringe Laufzeiten nicht für ein Sampling geeignet. Es kann in diesem Fall davon ausgegangen werden, dass die Methode ihre komplette Ausführungszeit auch tatsächlich auf der CPU gewesen ist. Daher sollte besser die Ausführungszeit der Methode als Metrik verwendet werden. Einfluss der durchschnittlichen Länge Der Einfluss der durchschnittlichen Länge der Strings stellte sich als vernachlässigbar heraus, wie es Abbildung 7.2 zeigt. Heapsort Quicksort Abbildung 7.2: Einfluss der Länge der zu sortierenden Strings auf die CPU Nutzung bei Heap- und Quicksort 7.1. FALLBEISPIEL: SORTIERALGORITHMEN 127 Zum Test wurden Arrays der Größe 500.000 mit einer durchschnittlichen Länge von 5 bis 30 Zeichen pro String (Schrittweite 5) bei zehnfacher Wiederholung getestet. Zunächst ist ersichtlich, dass Quicksort sehr viel geringere CPU-Belastung verursacht als Heapsort. Das deckt sich mit den Beobachtungen, dass Quicksort im Durchschnitt auch schneller sortiert als Heapsort. Generell lässt sich mit steigender Länge der Zeichenkette leichter Anstieg feststellen, der aber bei Betrachtung der gesamten CPU-Nutzung vernachlässigbar ist. Offensichtlich sind die lexikographischen Vergleiche keine teuren Operationen und dieser Parameter (eigentlich Metadatum) kann eliminiert werden. CPU-Nutzung: Quicksort vs Heapsort Da der Einfluss der Länge der Zeichenkette als vernachlässigbar eingestuft werden konnte, ist nur noch der Parameter Größe des Arrays übrig. Zur Ermittlung der CPU-Nutzung musste jeder der beiden Algorithmen folgende Aufgabe lösen: • Sortierung von 10 Arraygrößen in den Größenordnung von 100.000 bis 1.000.000 (Schrittweite = 100.000). • Pro Schritt 10 Wiederholungen, mit 10 unterschiedlichen und zufällig erzeugten Arrays der gleichen Größe. • Die Länge der Strings (Sortierkriterium) betrug einheitlich 5, da ihr Einfluss vernachlässigbar ist. Die Ergebnisse zur CPU-Nutzung sind in Abbildung 7.3 dargestellt. Das Diagramm zeigt die CPU-Nutzung der Methode in der Einheit system ticks auf der y-Achse und die Anzahl der zu sortierenden Elemente auf der x-Achse. Zunächst lässt sich erkennen, dass der Parameter Arraygröße, wie erwartet, maßgeblichen Einfluss auf die CPU-Nutzung hat. Auch in diesem Test wird Heapsort von Quicksort bezüglich der Beanspruchung der CPU geschlagen. Da zufällige Arrays mit mehrfacher Wiederholung der Sortierung verwendet und daraus ein Durchschnitt ermittelt wurde, kann davon ausgegangen werden, dass es sich um das durchschnittliche Ergebnis der Algorithmen handelt. Damit wird auch die Ungenauigkeit der LandauNotation verdeutlicht, die für beide Algorithmen im durchschnittlichen Fall O(n · log(n)) angibt. Das lässt sich durch das Fehlen von Konstanten und Termen niederer Ordnung erklären. Der erhöhte Aufwand zur Verwaltung des Baumes bei Heapsort wird damit beispielsweise nicht erfasst. Als Ergebnis kann festgehalten werden, dass Quicksort im Durchschnitt bezüglich CPU-Nutzung der „grünere“ der beiden Algorithmen ist. 128 KAPITEL 7. EVALUATION 40 0 0 350 0 Heapsort Methode CPU (system ticks) 300 0 25 0 0 20 0 0 Quicksort 150 0 100 0 5 00 0 0 100 00 0 2 00 00 0 300 00 0 40 00 00 50 00 00 60 00 00 700 0 00 8 00 0 00 9 00 00 0 100 00 00 Anzahl zu sortierender Elemente Abbildung 7.3: CPU-Nutzung von Heap- und Quicksort in Abhängigkeit der Arraygröße Ein weiteres Ergebnis ist eine direkte Korrelation zwischen CPU-Nutzung und Ausführungszeit. Auf dem Testsystem unterschieden sich die Werte zwischen Ausführungszeit in Millisekunden und CPU-Nutzung in system ticks (auf CPU) nur geringfügig. Das lässt sich zum Einen durch das Verfahren der Datenerhebung des CPU-Profilers erklären, das bei jeden Timer-Interrupt überprüft, welcher Prozess oder Thread gerade auf der CPU ist. Offensichtlich wird dieser Interrupt auf dem Testsystem etwa jede Millisekunde ausgelöst. Zum Anderen ist ein Grund in der kontrollierten Ausführung zu sehen. Da kaum andere Aktivität im System herrschte, musste der Scheduler nicht eingreifen und die Methode konnte einen CPU-Kern für ihre komplette Ausführung nutzen. In diesem Fall ist die Ausführungszeit also durchaus ein brauchbarer Indikator für die CPU-Nutzung. Müssen Daten allerdings an einem Produktivsystem, auf dem typischerweise viele Prozesse um die CPU konkurrieren, erhoben werden, so spiegelt die Ausführungszeit nicht die tatsächliche Ressourcennutzung wieder, da die Methode mehr oder minder häufig in der Ausführung unterbrochen wird. Ebenfalls ermöglicht der CPUProfiler eine genaue Zuordnung der Belastung einzelner CPUs oder Kerne sowie die Ermittlung von context switches. All diese Aussagen lassen sich mit 7.1. FALLBEISPIEL: SORTIERALGORITHMEN 129 der Ausführungszeit nicht treffen. Ergebnisse Eureqa Die erhobenen Daten wurden zunächst mit dem Analyse-Framework gesichtet und statistisch ausgewertet. Das Ergebnis stellte Abbildung 7.3 dar. Anschließend muss eine geeignete Funktion gefunden werden, die das Verhältnis zwischen Ressourcennutzung und Parameter annähert. Zu diesem Zweck wurden die Daten exportiert und es kam Eureqa zur automatischen Ableitung des Zusammenhangs zum Einsatz. Gesucht war eine Funkion f , die das Verhältnis folgendermaßen beschreibt cputicks = f (n) (7.5) Dabei bezeichnet n die Größe des Arrays. Zur Konstruktion der Formel durfte Eureqa die Komponente Addition, Division, Multiplikation, Exponent, Logarithmus und Quadratwurzel verwenden. Da der Zusammenhang relativ einfach ist, wurden die Formeln innerhalb kurzer Zeit ermittelt. Von ihnen wurden diejeneigen ausgewählt, die den besten Kompromiss zwischen Genauigkeit und Komplexität bieten. Das Ergebnis für Quicksort lautete cputicks = f (n) = 14n + 40n · log(4n) (7.6) Für Heapsort ergab sich die folgende Abhängigkeit cputicks = f (n) = 119n · log(2n) (7.7) Dabei fällt direkt auf, dass sich die durchschnittlichen Komplexitäten (O(n·log(n)) der Algorithmen in den beiden Funktionen wiederfinden. Allerdings sind in diesem Fall Konstanten vorhanden, die bei der Landau-Notation weggelassen würden. Wie der beachtliche Unterschied in der CPU-Nutzung der beiden Algorithmen jedoch verdeutlichte, dürften diese Konstanten auch Einfluss auf den Energieverbrauch haben. Es reicht daher nicht aus lediglich die Komplexitätsklasse eines Algorithmus als Formel heranzuziehen. Festplatten-Nutzung bei Speicherung des Ergebnis Zur Demonstration des Profilings der Festplattenaktivität wird ebenfalls das Beispiel der Sortieralgorithmen verwendet, indem das Ergebnis (sortiertes Arrays) im Anschluss an die Sortierung in eine Datei gespeichert wird. Damit kann gezeigt werden, dass die durchschnittliche Länge der Zeichenketten, im Gegensatz zur CPU-Nutzung, in diesem Fall einen erheblichen Einfluss auf 130 KAPITEL 7. EVALUATION die Ressourcennutzung hat. Das verwendete Sortierverfahren hingegen ist zunächst irrelevant. Zum Profiling kamen wieder Arrays der Größe 100.000 bis 1.000.0000 (Schrittweite 100.000) und Zeichenketten der Länge 5 bis 20 (Schrittweite 5) zum Einsatz. In Abbildung 7.4 ist das Ergebnis des Profilings dargestellt. 25 00 00 00 Bytes geschrieben 20 00 00 00 15 00 00 00 Län ge Län ge Län ge Län ge 10 00 00 00 5 10 15 20 5 00 00 00 0 10 000 0 2 00 0 00 30 00 00 40 00 00 50 0 00 0 6 00 00 0 7 00 00 0 800 0 00 90 00 00 100 00 00 Anzahl Elemente im Array Abbildung 7.4: Vergleich der Festplattennutzung bei Speicherung verschiedener Arrays Das Beispiel der Speicherung von sortierten Arrays in eine Datei ist relativ simpel. Wie zu erwarten verhält sich die Anzahl geschriebener Bytes linear zu Arraygröße und durchschnittlicher Länge der Zeichenketten. Allerdings besitzt die Funktion zur Beschreibung des Zusammenhangs zwei abhängige Parameter. Gesucht ist damit folgende Funktion: id diskbytes = f (n, l) (7.8) Dabei gibt n die Größe des Arrays und l die durchschnittliche Zeichenkettenlänge an. Als Einheit dienen Bytes und aus den aufgezeichneten Profilerdaten ist bekannt, dass auf Laufwerk sda geschrieben wurde. Auch in diesem Fall können die Daten in Eureqa eingelesen und die gesuchte Funktion angegeben werden. Die Verwendung mehrerer Funktionsparameter stellt dabei kein Problem dar. Eureqa lieferte das folgende Ergebnis: sda diskbytes = f (n, l) = 1 + n · l + n (7.9) Dieser Zusammenhang ist relativ einfach und setzt nicht zwangsweise den Einsatz von Eureqa voraus, um die angegebene Funktion zu ermitteln. Da 7.1. FALLBEISPIEL: SORTIERALGORITHMEN 131 die einzelnen Zeichenketten in eine Textdatei geschrieben wurden, kann davon ausgegangen werden, dass pro Zeichen ein Byte benötigt wird. Dadurch kommt der Term n · l in die Gleichung. Hinzu kommt ein Zeilenumbruch oder Trennzeichen pro Element (Addition von n). Bei komplexeren Interaktionen sind diese Zusammenhänge allerdings nicht ohne Weiteres erkennbar und die Komplexität der Funktionen steigt. In diesem Fall muss bei der Formelsuche mit Eureqa eine Abwägung zwischen Fehler der Annäherung und Komplexität der Lösung erfolgen. Dieses Beispiel sollte lediglich anschaulich verdeutlichen, dass die physische Ressourcennutzung der zu Erwartenden entspricht. Der zeitliche Aspekt der Ressourcennutzung Wurde beim Profiling festgestellt, dass eine Methode hohe Festplattennutzung verursacht, so ist unter Umständen der zeitliche Faktor der tatsächlichen Nutzung von Bedeutung. Dieser Umstand wird in den Standardprofilern nicht berücksichtigt, da sie kumulativ arbeiten, um eine möglichst geringe Menge an Daten zu erzeugen. Sie summieren die gesamte Nutzung der Hardwarekomponenten für eine Methode auf und speichern lediglich das Ergebnis zur Ausgabe. Dieses Vorgehen ist für gewöhnlich ausreichend, um eine gute Vorhersage zu ermöglichen. Allerdings ist es, gerade bei langlaufenden Methoden, denkbar, die physische Ressourcennutzung zeitlich innerhalb der Ausführungsdauer zu ermitteln. Zu diesem Zweck wurden zwei weitere SystemTap-Skripte für Netzwerk- und Festplattennutzung von Methoden unter Berücksichtigung der zeitlichen Einordnung der physischen Zugriffe entwickelt. Die Problematik besteht darin, dass sehr viele Daten aufgezeichnet werden müssen, da die Kernelfunktionen in den Subsystemen, die zur eigentlichen Ressourcennutzung führen, meist sehr häufig mit sehr geringen Ausführungszeiten aufgerufen werden. Beispielsweise wird beim schreibenden Zugriff auf die Festplatte jeweils für das Schreiben einer Blockgröße (8192 Bytes) ein Aufruf im virtuellen Dateisystem (VFS) durchgeführt. Beim Speichern eines Arrays mit 1.000.000 Elementen (a 20 Zeichen) erfolgten 2566 Aufrufe der write() Methode des VFS mit einer durchschnittlichen Ausführungsdauer von etwa 15.000 Nanosekunden. In Abbildung 7.5 ist die Methodenausführung in ihren zeitlichen Ablauf und der Anteil, der auf Festplattenaktivität entfiel, dargestellt. In diesem Beispiel lassen sich Festplattenaktivität und restliche Methodenausführung klar voneinander trennen, da zunächst sortiert, und anschließend geschrieben wird. Es fällt auf, dass das Schreiben der Daten auf die Festplatte einen relativ kleinen Teil der Gesamtzeitausführungszeit der Methode ausmacht. Der überwiegende Teil wird für die Sortierung des Arrays 132 KAPITEL 7. EVALUATION Ende der Methodenausführung 9 00 0 Byt es pro VFS-Aufruf 8 00 0 2566 Aufrufe ca. 200ms keine Festplattenaktivität (Sortierung) 7 00 0 6 00 0 5 00 0 4 00 0 30 00 2 00 0 10 00 0 0 100 0 2 00 0 3000 40 00 5 00 0 6 00 0 Zeit (ms) Abbildung 7.5: Zeitlicher Verlauf der Methodenausführung (sortieren und speichern) (zeitlich gesehen spielt daher das verwendete Sortierverfahren eine Rolle) benötigt. Diese Betrachtungen sind sehr detailliert und es sollte geprüft werden, ob sich daraus brauchbare Aussagen ableiten lassen. Soll die zeitliche Komponente in die Beschreibung aufgenommen werden, so ist es um so wichtiger, die Vorgaben des Benchmarking zu beachten (vergleiche Unterabschnitt 4.2.4), da sonst die Messergebnisse verfälscht werden. Eine derartige Beschreibung kann, im Gegensatz zur kumulativen Angabe, nicht über eine simple Funktion erfolgen. Als mathematische Beschreibung bieten sich beispielsweise Differentialgleichungen an, die sich auch mit Eureqa bestimmen lassen. Allerdings sollte sehr genau abgewogen werden, ob sich dieser Aufwand lohnt. • Die zu sammelnde Datenmenge wird um ein Vielfaches erhöht, da pro Methode alle Zeiten aller Hardwarezugriffe (eventuell mit Werten) protokolliert werden müssen. In der Regel sind das sehr viele und sehr kurze Aufrufe in den Subsystemen. • Je mehr Daten gesammelt werden, desto schwieriger gestaltet sich ihre Auswertung. • Je komplexer die gesammelten Daten sind, umso schwieriger lassen sich die benötigten Zusammenhänge herstellen. • Der Zeitfaktor benötigt für eine formale Beschreibung eine Differentialgleichungen, deren Ermittlung, falls überhaupt möglich, aufwändig ist. 7.2. FLEXIBILITÄT DER DATENERHEBUNG 133 • Die zeitliche Verteilung ist im Allgemeinen sporadischer als im gegebenen Beispiel. In diese Arbeit konnte die Gewinnung einer Beschreibung, die die zeitliche Komponente auf Methodenebene berücksichtigt, nicht mehr aufgenommen werden. Zukünftige Arbeiten könnten zunächst prüfen, ob dieser Aspekt sinnvoll und mit vertretbaren Aufwand in eine Beschreibung einfließen kann. Die Möglichkeiten zur Datenerhebung bestehen bereits. 7.2 Flexibilität der Datenerhebung Der vorangegangene Abschnitt hat gezeigt, wie die Jrupi Implementierung anwendbar ist, um Daten für verschiedene Implementierungsvarianten zu sammeln und daraus eine abstrakte Beschreibung der Ressourcennutzung in Form einer mathematischen Formel abzuleiten. Der Ausgangspunkt war das Aufsetzen einiger Microbenchmarks, die Generierung der BTrace-Instrumentierungen durch die Workbench sowie eine kontrollierte Ausführung mit anschließender Datensammlung und Auswertung. Doch die Implementierung kann sehr viel mehr leisten. Es wurde bereits mehrfach erwähnt, dass alle Komponenten für den Einsatz am Produktivsystem ausgelegt sind. Darüber hinaus ist es eine wesentliche Stärke der Proofof-Concept Implementierung von Jrupi, dass sie auf dynamische Manipulation des Bytecodes mit BTrace setzt. Dieser Umstand erlaubt es, Daten am laufenden Produktivsystem, bei gleichzeitig laufender Anwendung zu erheben. Doch mit BTrace ist noch mehr möglich. Während die Anwendung läuft kann Tracing-Code eingebracht werden, um beispielsweise für das Profiling interessante Methoden in der Anwendung aufzuspüren. Wurden die Methoden von Interesse identifiziert, so können die Parametertypen der Methode erfragt werden. Das alles, ohne dass jemals der Quell- oder Bytecode der Anwendung bekannt sein muss und ohne jemals die laufende Anwendung am Produktivsystem unterbrochen zu haben. Sind die Methoden identifiziert und ihre relevanten Parameter bekannt, so können die bereits bekannten BTraceInstrumentierungen geschrieben werden, die die Verbindung zu den low-level Profilern herstellen. Der Instrumentationcode wird daraufhin in die (immer noch laufende) Anwendung injiziert und die Profiler wie gewohnt gestartet. Dieser Aspekt der Implementierung soll nun anhand eines praktischen Beispiels erläutert werden. Da bisher die Netzwerk-Profiler noch nicht zum Einsatz kamen, besteht das Ziel im Auffinden einer speziellen Methode und der Erhebung ihrer Netzwerknutzungsdaten, ohne die Anwendung dazu stoppen zu müssen. Als laufende Anwendung am Produktivsystem bietet sich die 134 KAPITEL 7. EVALUATION Eclipse-IDE an, die größtenteils in Java implementiert ist. Um den Suchbereich ein wenig einzugrenzen, soll eine Methode des Versionskontrollsystems SVN auf von ihr verursachten Netzwerktraffic untersucht werden. Profiling in Eclipse Bei dieser Demonstration handelt es sich tatsächlich um ein Produktivsystem, da die Zielanwendung die laufende Eclipse-IDE ist, die auch zur Entwicklung und Verwaltung der Implementierungen in dieser Arbeit verwendet wird. Das Ziel besteht darin, zunächst die Methode zu finden, die für das commit Kommando bei SVN zuständig ist, und anschließend die genau Menge an Netzwerktraffic einzelner Aufrufe zu ermitteln. Der verwendete SVN-Connector lässt sich in der Liste installierter Plugins nachschauen. Bei der vorliegenden Eclipse-IDE ist es SVNKit von Polarion2 . Die relevanten Packages können entweder in der jar-Datei ermittelt oder aus der Plugin-Id „geraten“ werden. Da BTrace zur Definition der Prüfpunkte reguläre Ausdrücke zulässt, muss die genaue Bezeichnung nicht bekannt sein. Es ist auch nicht erforderlich, den Klassen oder Methodennamen zu kennen. Allerdings sollte bei großen Anwendungen darauf geachtet werden, dass der Overhead durch Instrumentierung stark zunimmt, wenn ohne Bedacht sehr viele Prüfpunkte aktiviert werden. Daher sollte die Auswahl, vor allem bei laufzeitkritischen Produktivsystemen, so weit wie möglich eingeschränkt werden. Im konkreten Fall des SVN-Connector Plugins ist die Aktivierung vieler Prüfpunkte allerdings verschmerzbar. In Listing 7.1 ist die Definition des Prüfpunktes zu sehen. 1 // Instumentiere alle Klassen und alle Methoden im Package @ O n M e t h o d ( clazz = " / org \\. p o l a r i o n \\. team \\. svn \\. c o n n e c t o r \\..+/ " , method = " /.*/ " ) public static void allSVN ( @ P r o b eM e t h o d N a m e String pmn , @ Pr ob eC l as sN am e String pcn ) { // gib Klassennamen und Methodennamen aus println ( Strings . strcat ( Strings . strcat ( pcn , " . " ) , pmn ) ) ; } 2 3 4 5 6 7 8 9 Listing 7.1: BTrace Instumentierung für alle Methoden aller Klassen des SVN Connectors Der Prüfpunkt gibt an, dass alle Klassen und alle Methoden im Package org.polarion.team.svn.connector instrumentiert, und Ausgaben zum Tracing der Klassen- (pcn) und Methodennamen (pmn) erfolgen sollen. Da sich die Eclipse-IDE in Ausführung befindet, kann der Aufruf der gesuchten Methode provoziert werden. Dazu wurde die Integration von SVN 2 http://www.polarion.com/products/svn/ 7.2. FLEXIBILITÄT DER DATENERHEBUNG 135 in die Eclipse-GUI genutzt, um die geänderte Version eines Projektes auf den SVN-Server von CoolSoftware zu laden (Commit). Das erzeugt zunächst eine große Menge Ausgaben durch BTrace, da alle Methoden aller Klassen im Package instrumentiert werden. Unter diesen Ausgaben befindet sich auch die gesuchte commit() Methode, wie in Listing 7.2 dargestellt. 1 2 3 4 5 6 7 [...] org . polarion . team . svn . connector . svnkit . S V N K i t C o n n e c t o r F a c t o r y . getId org . polarion . team . svn . connector . svnkit . S V N K i t C o n n e c t o r F a c t o r y . getSupportedFeatures org . polarion . team . svn . connector . svnkit . S VN Ki tC o nn ec to r . commit org . polarion . team . svn . connector . svnkit . S V N K i t C o n n e c t o r $ P r o g r e s s M o n i t o r W r a p p e r . < init > org . polarion . team . svn . connector . svnkit . S V N K i t C o n n e c t o r $ P r o g r e s s M o n i t o r W r a p p e r . start [...] Listing 7.2: Ausgabe auf Konsolen Damit ist die gesuchte Methode gefunden und alle Informationen vorhanden, um den Prüfpunkt für das Profiling zu definieren. Die Definition der Instrumentierung erfolgt nach dem in 6.2.1 vorgestellten Schema und ist in Listing 7.3 dargestellt. 1 2 3 4 5 @OnMethod ( clazz = " org . p o l a r i o n . team . svn . c o n n e c t o r . svnkit . S V N K i t C o n n e c t o r " , method = " commit " ) public static void te s tC om mi t En tr y ( AnyType [] args ) { 6 System . out . println ( " org . p o l a r i o n . team . svn . c o n n e c t o r . svnkit : S V N K i t C o n n e c t o r : commit :0: m e t h o d _ e n t r : " ) ; 7 8 } 9 10 11 12 13 14 @OnMethod ( clazz = " org . p o l a r i o n . team . svn . c o n n e c t o r . svnkit . S V N K i t C o n n e c t o r " , method = " commit " , location = @ L o c a t i o n ( value = Kind . RETURN ) ) public static void t estCommi tExit ( AnyType [] args , @ P r o b e Me t h o d N a m e String pmn , @ Pr ob e Cl as sN am e String pcn ) { 15 System . out . println ( " org . p o l a r i o n . team . svn . c o n n e c t o r . svnkit : S V N K i t C o n n e c t o r : commit :0: m e t h o d _ e x i t : " ) ; 16 17 } Listing 7.3: Prüfpunkte für commit() des SVNConnector Die Instrumentierungen können nun durch BTrace in die (immer noch laufende) Eclipse-IDE geladen werden. Gleichzeitig werden die relevanten Profiler (in diesem Fall der Netzwerk) mit der Prozess-ID des Eclipse Prozesses als Parameter gestartet. Wird die Ausführung der Methode erneut angestoßen, so zeichnet der Netzwerk-Profiler die exakte Menge (Bytes) an Netzwerktraffic, die durch die Ausführung der Methode verursacht wurden, 136 KAPITEL 7. EVALUATION auf. Die Ermittlung der Daten in dieser Form am Produktivsystem hat den Nachteil, dass keine kontrollierte Ausführung (wie im CPU Beispiel) stattfindet. Daher lässt sich nur bedingt Einfluss auf die Eingabedaten ausüben. Allerdings kann BTrace dazu verwendet werden, eine zusätzliche Analyse der Parameter durchzuführen. Über das bereits bekannte Array AnyType[] kann der Zugriff auf alle Parameter erfolgen. Sind die Parameter nicht numerischer Natur, so kann beispielsweise durch zusätzliche Ausgaben in einen separaten Skript der Typ der Parameter ermittelt werden. Ist der Typ ein Objekt, so kann in der Instrumentationsbeschreibung durch einen Typecast bei der Ausführung eine Referenz auf das Objekt erlangt werden, um weitere Daten daraus zu erhalten. Jedoch wurde dieses Beispiel gewählt, um die Grenzen des verwendeten Verfahrens aufzuzeigen. Zwar lassen sich mit Hilfe des Netzwerk-Profilers genaue Daten zur Ressourcennutzung erheben und mit BTrace die Parameter der Methode (und auch Metadaten darüber) ermitteln. Letztere stehen aber in diesem Fall in keinen ersichtlichen Zusammenhang zur tatsächlichen Ressourcennutzung. Der commit() Methode wird ein Array von Strings übergeben, die jeweils ein zu aktualisierendes Projekt repräsentieren. Das SVNSystem speichert seine Steuerinformationen allerdings in Form von Metadaten im Projekt selbst. Daher werden durch die übergebenen Strings lediglich die zu prüfenden Projekte charakterisiert, deren SVN-Metadaten auf Änderungen untersucht werden sollen. Es ist deshalb nicht möglich, nur durch Betrachtung der Parameter eine Aussage darüber zu treffen, wie viele Daten übertragen werden müssen. In Abbildung 7.6 ist der zeitliche Verlauf der physischen Ressourcennutzung dargestellt. Traffic [Byte] 20 0 0 180 0 kein Traffic 160 0 140 0 120 0 100 0 8 00 6 00 Netzwerkgerätezugriffe 4 00 2 00 0 0 10 0 2 00 30 0 40 0 50 0 60 0 70 0 80 0 90 0 100 0 Zeit [ms] Abbildung 7.6: Netzwerknutzung im zeitlichen Ablauf zweier commit() Ausführungen 7.3. KRITISCHE BEWERTUNG 137 Bei einer Ermittlung der zeitlichen Abfolge der physischen Netzwerkzugriffe lässt sich erkennen, dass die Verwendung der Netzwerkschnittstelle relativ gleichmäßig verteilt über die Ausführungszeit der Methode stattfindet. Die Menge der übertragenen Daten ist für die einzelnen Nutzungen meist gleich, was durch die Bildung von Paketen zur Datenübertragung erklärbar ist. Insgesamt verhält sich die Methode aber immer noch unvorhersehbar. Dieses Beispiel stellt ein Extrem dar, da eine willkürliche Methode gewählt wurde, ohne Kenntnis der Implementierung des Gesamtsystems zu besitzen. Es zeigt, wie flexibel die Datenerhebung stattfinden kann. Allerdings ist nicht immer eine sinnvolle Beschreibung lediglich unter Betrachtung der Methodenparameter und ihrer Metadaten möglich. Eine weitere Arbeit könnte sich mit der Aufnahme von Kontrollflussinformationen beziehungsweise interner Zustände in die Beschreibung befassen. Im optimalen Fall liegen einer Komponente daher bei der Auslieferung Benchmarks bei, die eine gezielte Vermessung im eigenen System ermöglichen. Ist dies nicht der Fall, so kann die beschriebene Methode angewendet werden. Der Preis dafür ist jedoch ein erhöhter manueller Aufwand, sowohl bei der Datenerhebung, als auch bei der Auswertung. Um ein sinnvolles mathematisches Modell daraus ableiten zu können, müssen die Daten eventuell im Nachhinein nach Parametern sortiert werden. 7.3 Kritische Bewertung Der große Nachteil der beschriebenen Verfahren ist die Abhängigkeit von den Parametern beziehungsweise der Metadaten über die Parameter. Zum Einen sind sie nicht immer verfügbar, zum Anderen, und das wiegt noch schwerer, ist im allgemeinen Fall nicht bekannt, welche Parameter von Bedeutung sind. So ist beispielsweise bei den Sortieralgorithmen der Parameter der Methode sort() ein generisches Array, realisiert durch ein Template (T()). Darin enthalten ist, aufgrund der Natur von Templates, eine im Voraus unbekannte Menge beliebiger Objekte. Im Fall der Sortieralgorithmen ist klar, dass die Objekte das Interface Comparable implementieren müssen. Nach welchem Kriterium aber genau sortiert wird, kann nicht ermittelt werden. Da Methodenparameter in Java aus einer beliebigen Menge Objekte bestehen können, und diese Objekte wiederum Referenzen auf andere Objekte halten können, die von ihnen verwendet werden, ist eine vollständige Analyse zur Laufzeit nahezu unmöglich. Daher sollten die Ressourcennutzungsdaten zunächst in einer kontrollierten Ausführung durch Benchmarks ermittelt werden. In diesem Fall sind die Testdaten bekannt und es kann untersucht werden, welche Parameter bei Veränderung tatsächlich Einfluss ausüben. 138 KAPITEL 7. EVALUATION Ein weiterer wichtiger Punkt sind Methoden, deren Ressourcennutzung in keinen Verhältnis zu den übergebenen Parametern stehen. Wird beispielsweise ein Webservice aus einer spezifischen Methode heraus aufgerufen, so kann die empfangene Menge an Bytes der Antwort im Normalfall nicht aus den Parametern geschlossen werden. Ähnliches gilt für das Versenden einer Suchanfrage, da der String mit der Anfrage in keinen Verhältnis zu den durch die Suchmaschine gelieferten Ergebnis steht. Dieselbe Problematik besteht ebenfalls bei Datenbankanfragen. Ebenso kann es sein, dass die Ressourcennutzung sehr stark vom internen Kontrollfluss oder vom internen Zustand einer Komponente abhängig ist. Auch in diesem Fall ist es schwierig oder unmöglich, eine akkurate mathematische Beschreibung zu gewinnen. Allgemein lassen sich Methoden in drei Kategorien einteilen (zu einen ähnlichen Ergebnis kam auch [SMM08]). 1. Methoden, die, unabhängig von Parametern, immer die gleiche Ressourcennutzung verursachen. 2. Methoden, deren Ressourcennutzung in Abhängigkeit der Parameter heuristisch vorhersagbar ist. 3. Methoden, bei denen kein Verhältnis zwischen Ressourcennutzung Parametern erkennbar ist. Die Implementierung von Jrupi deckt die ersten beiden Fälle vollständig ab. Beim dritten Fall ist es möglich, Daten über die Ressourcennutzung zu sammeln. Eventuell können durch längere Beobachtungen gewisse Muster bei der Ressourcennutzung erkannt werden. Eine genaue Vorhersage in Form einer mathematischen Beschreibung ist allerdings nicht möglich. Der Umgang mit derartigen Methoden sollte in einer Folgearbeit genauer untersucht werden. Kapitel 8 Zusammenfassung und Ausblick Eine abstrakte Verhaltensbeschreibung von CCM-Komponenten, und damit von Java-Anwendungen im Allgemeinen, bezüglich ihrer Ressourcennutzung zu gewinnen, stellte sich als schwierige Aufgabe heraus. Dabei liegt die Schwierigkeit nicht etwa in der Komplexität der Beschreibung an sich begründet, sondern vielmehr in der Problematik, dass sie nur auf Grundlage von Daten zur Nutzung physischer Hardwarekomponenten durch die implementierte Komponente während der Ausführung adäquat bei gleichzeitig vertretbaren Aufwand ermittelt werden kann. In Abschnitt 8.1 werden die geleisteten Beiträge der Arbeit zusammengefasst. Die Problematik stellte sich als ein weitreichendes Gebiet heraus. So setzt die Umsetzung einer Profiling-Infrastruktur eine gewisse Kenntnis von Hardware, Betriebssystem und der JVM auf Seiten der Systemumgebung voraus. Andererseits umfasst es aber auch das Wissen um Verfahren wie Sampling, Instrumentierung, Profiling, Benchmarking sowie die Anwendung unterschiedlichster Werkzeuge. Die Implementierungen reichten von der Entwicklung der low-level Profiler bis zur Erweiterung der Eclipse-Workbench. Aufgrund der begrenzten Arbeitszeit ist es schlicht nicht möglich, alle Aspekte vollständig umzusetzen. In Kapitel 7 wurde gezeigt, dass das Ziel getroffen wurde (Tracer Bullet). Abschnitt 8.2 zeigt auf, was noch getan werden muss. 8.1 Zusammenfassung In dieser Arbeit wurde die Gewinnung einer abstrakten Verhaltensbeschreibung bezüglich der Ressourcennutzung von Java-Codeartefakten im Rahmen eines Reverse Engineering umgesetzt. Die Motivation für eine derartige Beschreibung kam aus dem CoolSoftware Projekt, das zur Ermöglichung eines aktiven Energiemanagements für IT-Infrastrukturen ein abstraktes Verhal139 140 KAPITEL 8. ZUSAMMENFASSUNG UND AUSBLICK tensmodell der Ressourcennutzung der CCM Komponenten benötigt. Zur abstrakten Beschreibung wurde eine mathematische Modellierung der Ressourcennutzung auf Schnittstellenebene der Komponente ausgewählt. Die Komponente wird als Blackbox angesehen, deren Verhältnis zwischen Einund Ausgabedaten durch ein generatives System in Form einer Menge mathematischer Formeln für die relevanten Hardwarekomponenten durch Systeminferenz nachgebildet wird. Die Eingabedaten entsprechen den Parameter (beziehungsweise Metadaten über die Parameter) der angebotenen Schnittstelle und die Ausgabedaten der resultierenden Ressourcennutzung bei der Ausführung. Eine Problematik stellen die Parameter dar, weil sie in einer Sprache wie Java eine nicht vorhersagbare Struktur aufweisen können. Zudem werden zur Ableitung einer mathematischen Formel numerische Daten benötigt, die oft nur als Metadaten, zum Beispiel für Arrays oder Objekte, verfügbar gemacht werden können. Ein weiteres Problem tritt auf, wenn die Parameter in keinem Verhältnis zur Ressourcennutzung stehen, wie es beispielsweise bei Suchanfragen an einen Webserver der Fall ist. Jenseits dieser Probleme bei der Modellerstellung leistet die entworfene und implementierte Lösung eine detaillierte Datenerhebung zur Ressourcennutzung von Java-Codeartefakten. Sie hebt sich dadurch hervor, dass sie eine direkte Verbindung zwischen dem in der Ausführung befindlichen Codeartefakt und dem Profiling der physischen Ressourcennutzung auf Betriebssystemebene etabliert. Durch die Zuordnung der ausgeführten Methode zu einem durch das Betriebssystem verwalteten Thread kann die Ressourcennutzung, die als Konsequenz des Methodenaufrufs erfolgte, exakt einer Methode zugeordnet werden. Ermöglicht wird dies durch den Einsatz von Werkzeugen zur dynamischen Ablaufverfolgung (SystemTap, BTrace) und der Instrumentierung der JavaAnwendung auf Bytecodeebene. Die Instumentierung des Bytecodes erfolgt dynamisch durch das Werkzeug BTrace, das die benötigten Ausgabestatements, nachdem es seinen Agenten zur Bytecode-Manipulation in die laufende JVM geladen hat, beim Class Loading der entsprechenden Klassen in die laufende Anwendung einbringt. Über eine geschickte Verbindung, die den Systemaufruf write() nutzt, um die aktuelle Stelle in der Java-Anwendung den low level Profilern mitzuteilen, können die Ressourcennutzungsdaten exakt einzelnen Codeartefakten zugeordnet werden. Damit findet die eigentliche Datenerhebung außerhalb des JVM-Prozesses auf Betriebssystemebene statt und beeinflusst Java-Anwendung nur minimal durch die zusätzlich eingebrachten Ausgabestatements. Obwohl auf beiden Seiten (JVM und Betriebssystem) mit Instumentierung gearbeitet wird, können auf diese Weise dennoch die Vorteile von Sampling-Verfahren erreicht werden, da die Datenerhebung entkoppelt von der Anwendung stattfindet. Damit wird der ein- 8.1. ZUSAMMENFASSUNG 141 gebrachte Overhead minimiert, so dass einem Heisenberg-Effekt vorgebeugt wird. Die Flexibilität die durch die Kombination von BTrace und SystemTap erreicht wird, wurde in der Evaluation in Abschnitt 7.2 gezeigt. Mit BTrace lassen sich zunächst Methoden von Interesse in einer der laufenden Anwendung aufspüren und ihre Parameter erfragen. Sind diese Informationen bekannt, so können die Instrumentierungen zur Verbindung zu den Profilern wie gewohnt geschrieben werden. Sie werden anschließend in die, immer noch laufende Anwendung geladen und die Profiler auf Systemebene gestartet. So lassen sich unbekannte Codeartefakte, für die weder Quell- noch Bytecode verfügbar ist, vermessen. Dazu muss die Anwendung, die gegebenenfalls auf einem Produktivsystem läuft und nicht gestoppt werden kann, niemals angehalten werden. Im Allgemeinen ist es aber leichter, die Daten im Rahmen einer kontrollierten Ausführung durch einen Benchmark zu erheben. Zum Einen sind damit die Eingabedaten bekannt, so dass die Parameter nicht über ein umständliches Verfahren ermittelt werden müssen. Zum Anderen ermöglicht dies eine vorgegebene Reihenfolge der Ausführung, wodurch die erhobenen Daten im Nachhinein nicht geordnet werden müssen. Die Erstellung dieser Benchmarks sollte bereits während der Entwicklung erfolgen und kann gleichzeitig für ein Performance Regression Testing [MH10] zur Sicherstellung der Erreichung von Performancezielen dienen. Daher ergeben sich zusätzliche Synergieeffekte, da ihre Erstellung zur Sicherstellung der beiden nicht-funktionalen Eigenschaften Energie und Performance dient, und somit der zusätzliche Aufwand gerechtfertigt ist. Nachdem die Daten über die physische Ressourcennutzung erhoben sind, müssen sie ausgewertet werden. Zunächst erfolgt eine Vorauswertung im Rahmen einer statistischen Datenanalyse. Zu diesem Zweck wurde das AnalyseFramework implementiert. Es liest die Daten, die durch SystemTap im JSONFormat in eine Datei gespeichert wurden (Flightrecorder), ein und zeigt sie zunächst in tabellarischer Form an. Daraufhin können sie statistisch ausgewertet werden, indem beispielsweise Mittelwert und Standardabweichungen berechnet werden. Nach dieser Auswertung muss nach einem mathematischen Zusammenhang in den verbleibenden Daten gesucht werden. Dieser muss das Verhältnis zwischen den Parametern, beziehungsweise ihren relevanten Metadaten, zu den aufgezeichneten Ressourcennutzungsdaten herstellen. Ist dieser Zusammenhang einfach erkennbar, so kann er direkt in eine simple mathematische Formel gefasst werden. Andernfalls wird die Verwendung von Werkzeugen zur intelligenten Datenanalyse empfohlen. Im Speziellen wurde für die Proof-ofConcept Implementierung Eureqa verwendet, das mathematische Zusammen- 142 KAPITEL 8. ZUSAMMENFASSUNG UND AUSBLICK hänge aus Datenmengen nahezu automatisch lernt. Lediglich die erlaubten Operationen und die gesuchte Funktion muss angegeben werden. Sind Funktionen für alle Hardwarekomponenten gefunden, die ein Port (respektive Methode) verwendet, so ist dieser vollständig beschrieben. Durch Beschreibung aller Ports einer Variante ist auch die Variante vollständig beschrieben. Das Ziel einer abstrakten Verhaltensbeschreibung für CCM Softwarekomponenten bezüglich ihrer Ressourcennutzung ist erreicht. 8.2 Ausblick Die Proof-of-Concept Implementierung hat gezeigt, dass das konzeptuelle Framework Jrupi in der Praxis umsetzbar ist. Durch die geschickte Auswahl geeigneter Werkzeuge können detaillierte Daten zur Ressourcennutzung einzelner Java-Codeartefakte gesammelt und eine abstrakte Beschreibung in Form eines mathematischen Modells daraus abgeleitet werden. So lässt sich die Ressourcennutzung von Java-Methoden im Allgemeinen und von Ports einer CCM Komponente im Speziellen ermitteln und beschreiben. Dieser Abschnitt erfüllt zwei Aufgaben. Zum Einen gibt er einen Überblick über weitere Funktionen, die implementiert werden sollten, da sie aufgrund der großen Spannweite des Themas nicht mehr den Weg in diese Arbeit gefunden haben. Zum Anderen erläutert er mögliche Verwendungen in CoolSoftware. Offene Funktionen und weitere Aufgaben Für die Implementierung von Jrupi wären folgende weitere Funktionen wünschenswert: • Implementierung weiterer Profiler: Bisher sind Profiler für CPUs, Netzwerkschnittstellen und Festplatten implementiert worden. Eine Implementierung für weitere Hardwarekomponenten (z.B. Arbeitsspeicher, Grafikkarte,...) wäre wünschenswert. • Erweiterung um Metriken: Die Profiler könnten um zusätzliche Metriken erweitert werden. Bisher sammeln sie quantitative Daten und zeitliche Informationen. Es wäre möglich, weitere Metriken (z.B. Durchsatz (Throughput), Warteschlangenlänge, Sättigung (Saturation),...) direkt innerhalb der Profiler mit einzubeziehen. • Erweiterung des Analyse-Framework: Werden neue Profiler hinzugefügt, so muss auch das Analyse-Framework entsprechend erweitert 8.2. AUSBLICK 143 werden. Darüber hinaus wäre es sinnvoll, weitere Analyseverfahren einzubeziehen und eine übersichtlichere Datendarstellung (z.B Diagramme) zu ermöglichen. Der Grundstein wurde dazu wurde durch das Einbinden von JFreeChart1 bereits gelegt. Aufgaben für zukünftige Arbeiten könnten beispielsweise folgende Themen umfassen: • Kombination mit Kontrollfluss Der entwickelte Blackbox Ansatz bietet den Vorteil, dass er zu relativ simplem Modellen führt. Dafür wird unter Umständen auf Genauigkeit verzichtet, da der interne Kontrollfluss sowie der innere Zustand einer Komponente nicht betrachtet werden. Das führt in der Konsequenz dazu, dass die Ressourcennutzung nicht immer exakt vorhersagbar ist. Eine Kombination von Jrupi mit den Interna einer Komponente könnte Abhilfe schaffen. Dabei könnten die in Unterabschnitt 3.1.2 Softwareausführungsmodellen und der Prozess der Modellextraktion hilfreich sein. • Lösung des Parameterproblems Ein weiteres wichtiges Problem ist die Abhängigkeit von Parametern beziehungsweise von Metadaten über die Parameter. Bei komplexen Strukturen müssten sie in Form einer gezielten Parameteranalyse in Bezug auf ihre Auswirkungen auf die Ressourcennutzung ermittelt werden. Eine weitere Arbeit könnte sich daher damit beschäftigen, wie diese Metadaten gezielt und im besten Fall automatisch aufgefunden werden können. • Weitere Beschreibungsformen Die vorgestellten mathematischen Modelle bieten den Vorteil, dass sie kompakt und formal sind. Darüber hinaus beschreiben sie den Zusammenhang in der universalen Sprache der Mathematik und sind daher vielseitig anwendbar. Dennoch wäre es denkbar, gerade in Kombination mit den beiden zuvor genannten Punkten, weitere Beschreibungsformen mit einzubeziehen. Denkbar wären auch komplexere mathematische Modelle, mit einer übergeordneten Struktur oder unter Einbezug zeitlicher Eigenschaften, beispielsweise in Form von Differentialgleichungen. Auch Fuzzy-Logik [Pae06] könnte hilfreich sein, um eine sinnvolle Beschreibung in Anbetracht unscharfer Parameter zu erreichen. • Mehr Automatisierung Der bisherige Ansatz benötigt immer noch einige manuelle Arbeit. Zwar sind externe Werkzeuge wie Eureqa in der Lage, mathematische Zusammenhänge automatisch abzuleiten, aber 1 http://www.jfree.org/jfreechart/ 144 KAPITEL 8. ZUSAMMENFASSUNG UND AUSBLICK nur lose durch Datenimport gekoppelt. Es wäre wünschenswert, diesen Prozess direkt in die Entwicklungsumgebung zu integrieren. Eventuell könnten Verfahren zur intelligenten Datenanalyse oder auch neuronale Netze [Pae06] zum Einsatz kommen, um die Beschreibung automatisch aus einer Menge Daten innerhalb der Entwicklungsumgebung abzuleiten. Verwendung in CoolSoftware Das CoolSoftware Projekt befindet zum aktuellen Zeitpunkt noch in einer relativ frühen Phase. Für zentrale Bestandteile wie die Laufzeitumgebung THEATRE sind noch keine Implementierungen verfügbar. Andere Bestandteile wie CCM und ECL befinden sich unter aktiver Entwicklung und wurden im Verlauf der Arbeit größeren Änderungen unterzogen. Deshalb, und wegen der begrenzten Bearbeitungszeit, war es bisher nicht möglich, einen endgültigen Platz für die mathematischen Beschreibungen des Ressourcennutzungsverhalten zu ermitteln. Aufgrund der universalen Natur einer mathematischen Beschreibung ist die Integration jedoch in jedem Fall nur mit geringem Aufwand verbunden. Folgende Punkte sollten dabei berücksichtigt werden: • CCM Intuitiv sollte die Beschreibung des Ports einer Variante die geeignete Stelle zur Vorhaltung der mathematischen Beschreibung sein. • Expressions Um diverse Ausdrücke und Operationen in ECL verwenden zu können, wurde bereits eine DSL (Domain Specific Language) für Expressions erstellt. Sie müsste gegebenenfalls um zusätzliche mathematische Funktionen erweitert werden, sollten die abgeleiteten Beschreibungen dies erfordern. Prinzipiell kann sie aber direkt zum Ausdruck der mathematischen Funktionen verwendet werden. Diese Beschreibung müsste anschließend lediglich von der Variante referenziert werden. • Simulation In CoolSoftware wird die Infrastruktur simuliert. Durch die Kombination der mathematischen Beschreibungen mit typischen Benutzeranfragen aus den Kontextoren und Erfahrungswerten (oder Abschätzungen) über die typischen Parameter können nun auch Softwarekomponenten in die Simulation einbezogen werden. • Energie- und Ressourcenmanager Stehen die Energie- und Ressourcenmanager zur Verfügung, so können die Modelle verwendet werden, um eine Abschätzung über die zukünftigen Ressourcennutzung 8.2. AUSBLICK 145 (in Kombination mit Erfahrungswerten und Daten aus der Vergangenheit) zu treffen und gegebenenfalls eine Rekonfiguration zu veranlassen. Insbesondere sollte es möglich sein, die Energiezustände der Hardwarekomponenten vorausschauend zu verwalten. So könnte beispielsweise anhand der Modelle entschieden werden, dass die zur Verfügung stehende Menge einzelner Hardwareressourcen ausreicht, um die Dienste der Komponente zu erfüllen, so dass andere Hardwarekomponenten in niedrigen Energiezuständen verweilen oder abgeschaltet bleiben können. Schlussbemerkung Die Beschreibung des Ressourcennutzungsverhaltens von Softwareartefakten ist lediglich ein Baustein auf dem Weg zum großen Ziel der Steigerung der Energieeffizienz von IT-Infrastrukturen. Erst durch Einsatz im Rahmen eines gesamtheitlichen Ansatzes, wie ihn CoolSoftware anstrebt, kann entsprechend großer Nutzen daraus gezogen werden. Die Einsparung von Energie ist ein Thema, das auch die Softwareseite erreicht hat. So sind in Zukunft eine Menge neuer Methoden und Ansätze zu erwarten. Eventuell können die Ergebnisse dieser Arbeit einen Beitrag zum Gesamtziel leisten. In dieser Arbeit wurde eine Möglichkeit entwickelt, um Ressourcennutzungsdaten zu erheben und einzelnen Java-Methoden zuzuordnen, um anschließend eine mathematische Beschreibung daraus abzuleiten. Die implementierten Profiler sind jedoch unabhängig von Java, da sie auf Betriebssystemebene arbeiten und lediglich einen spezifizierten Prozess (und dessen Threads) überwachen. Sie können daher für beliebige Anwendungen verwendet werden und einzelne Codeartefakte auf Ressourcennutzung untersuchen - unter der Voraussetzung, dass die Codeartefakte durch die benötigten Systemaufrufe markiert werden. Eine Möglichkeit, um die Instrumentationen einzubringen, bietet sich durch die Verwendung von SystemTap im user space. Werden Anwendungen mit zusätzlichen Debuginformationen kompiliert, so ist SystemTap in der Lage, innerhalb der Anwendungen Prüfpunkte zu definieren und Kontextinformationen aufzulösen. Ist dieses Feature ausgereift, so kann ein konsistentes Profiling nahezu beliebiger Anwendungen mit SystemTap erfolgen. Die implementierten Profiler sollten nicht als starres Gebilde gesehen werden. Dank der Verwendung von SystemTap sind sie programmierbar und können daher leicht angepasst und erweitert werden. Ein Abgleich zwischen Ressourcennutzung und gemessenen Energieverbrauch wird zeigen müssen, welche Metriken sinnvolle Aussagen liefern. Dann ist die Zeit gekommen, eine Anpassung der Profiler und der Verhaltensbeschreibung durchzuführen. 146 KAPITEL 8. ZUSAMMENFASSUNG UND AUSBLICK Literaturverzeichnis [BC00] Daniel Pierre Bovet and Marco Cassetti, Understanding the Linux Kernel, O’Reilly & Associates, Inc., Sebastopol, CA, USA, 2000. [BH07] Luiz André Barroso and Urs Hölzle, The Case for EnergyProportional Computing, Computer 40 (2007), 33–37. [BPBS10] Jacob Bart, Larson Paul, Leitao Breno, Henrique, and Augusto Saulo, SystemTap: Instrumenting the Linux Kernel for Analyzing Performance and Functional Problems, IBM Redbooks, 2010. [BRH04] James F. Bowring, James M. Rehg, and Mary Jean Harrold, Active learning for automatic classification of software behavior, SIGSOFT Softw. Eng. Notes 29 (2004), no. 4, 195–205. [Bru07] Eric Bruneton, ASM 3.0 A Java bytecode engineering library, 2007. [Chi04] Shigeru Chiba, Javassist: Java Bytecode Engineering Made Simple, 2004. [CI90] Elliot J. Chikofsky and James H. Cross II, Reverse Engineering and Design Recovery: A Taxonomy, IEEE Software 7 (1990), 13–17. [Cor10] Standard Performance Evaluation Corporation, SPEC Power and Performance, 2010. [CSG+ 10] Sebastian Cech, Matthias Schmidt, Sebastian Götz, Claas Wilke, Johannes Waltsgott, and Ronny Fritzsche, Cool Component Model Specification, Tech. report, Dresden, Germany, 2010. [Dal10] Valentin Dallmeier, Mining and Checking Object Behavior, Ph.D. thesis, Universität des Saarlandes, 2010. 147 148 LITERATURVERZEICHNIS [DDF+ 06] Simon Dobson, Spyros Denazis, Antonio Fernández, Dominique Gaïti, Erol Gelenbe, Fabio Massacci, Paddy Nixon, Fabrice Saffre, Nikita Schmidt, and Franco Zambonelli, A survey of autonomic communications, ACM Trans. Auton. Adapt. Syst. 1 (2006), no. 2, 223–259. [Dua07] Lucio Mauro Duarte, Behaviour Model Extraction using Context Information, Ph.D. thesis, University of London, 2007. [EPG+ 07] Michael D. Ernst, Jeff H. Perkins, Philip J. Guo, Stephen McCamant, Carlos Pacheco, Matthew S. Tschantz, and Chen Xiao, The Daikon system for dynamic detection of likely invariants, Science of Computer Programming 69 (2007), no. 1–3, 35–45. [ET10] Ciliendo Eduardo and Kunimasa Takechika, Linux Performance and Tuning Guidelines, IBM Redbooks, 2010. [Fli01] Jason Flinn, Extending Mobile Computer Battery Life through Energy-Aware Adaptation, Ph.D. thesis, School of Computer Science, Computer Science Department, Carnegie Mellon University, Pittsburgh, PA, December 2001. [FPS02] Jason Flinn, SoYoung Park, and M. Satyanarayanan, Balancing Performance, Energy, and Quality in Pervasive Computing, ICDCS ’02: Proceedings of the 22 nd International Conference on Distributed Computing Systems (ICDCS’02) (Washington, DC, USA), IEEE Computer Society, 2002, p. 217. [FS99] Jason Flinn and M. Satyanarayanan, PowerScope: A Tool for Profiling the Energy Usage of Mobile Applications, Proceedings of the Second IEEE Workshop on Mobile Computer Systems and Applications (Washington, DC, USA), WMCSA ’99, IEEE Computer Society, 1999, pp. 2–. [FZJ08] Yunsi Fei, Lin Zhong, and Niraj K. Jha, An energy-aware framework for dynamic software management in mobile computing systems, ACM Trans. Embed. Comput. Syst. 7 (2008), no. 3, 1–31. [Gar07] Matthew Garrett, Powering Down, ACM Queue 5 (2007), no. 7, 16–21. [GB04] Erich Gamma and Kent Beck, Contributing to Eclipse: Principles, Patterns, and Plug-Ins, Addison Wesley, 2004. LITERATURVERZEICHNIS 149 [GCW+ 10] Sebastian Götz, Sebastian Cech, Claas Wilke, Matthias Schmidt, Johannes Waltsgott, and Ronny Fritzsche, CoolSoftware Architecture Specification, Tech. report, Dresden. Germany, 2010. [GHJV95] Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides, Design patterns: elements of reusable object-oriented software, Addison-Wesley Professional, 1995. [GSI+ 02] S. Gurumurthi, A. Sivasubramaniam, M. J. Irwin, N. Vijaykrishnan, and M. Kandemir, Using complete machine simulation for software power estimation: the SoftWatt approach, feb. 2002, pp. 141–150. [GWS+ 10a] Sebastian Götz, Claas Wilke, Matthias Schmidt, Sebastian Cech, and Uwe Assmann, Towards Energy Auto Tuning, 2010. [GWS+ 10b] Sebastian Götz, Claas Wilke, Matthias Schmidt, Sebastian Cech, Johannes Waltsgott, and Ronny Fritzsche, THEATRE Resource Manager Interface Specification, Tech. report, To be published in 2010. [HJMM09] Buhl Hans, Ulrich, Laartz Jürgen, Löffler Markus, and Röglinger Maximilian, Green IT reicht nicht aus!, WIRTSCHAFTSINFORMATIK & MANAGEMENT, Januar 2009. [HPIM+ 10] Hewlett-Packard, Intel, Microsoft, Phoenix Technologies, and Toshiba, Advanced Configuration and Power Interface Specification, Revision 4.0a, http://www.acpi.info/spec.htm, April 2010. [HT99] Andrew Hunt and David Thomas, The pragmatic programmer: from journeyman to master, Addison-Wesley Longman Publishing Co., Inc., Boston, MA, USA, 1999. [JF04] Weidendorfer Josef and Zenith Federico, The KCachegrind Handbook, 2004. [KA05] Kreft Klaus and Langer Angelika, Java Performance: MicroBenchmarking, JavaSPEKTRUM, September 2005. [KLM+ 97] Gregor Kiczales, John Lamping, Anurag Mendhekar, Chris Maeda, Cristina Lopes, Jean-Marc Loingtier, and John Irwin, Aspect-oriented programming, ECOOP’97 - Object-Oriented 150 LITERATURVERZEICHNIS Programming (Mehmet Akşit and Satoshi Matsuoka, eds.), Lecture Notes in Computer Science, vol. 1241, Springer-Verlag, Berlin/Heidelberg, 1997, pp. 220–242. [Lan03] Hans-Werner Lang, Algorithmen in java, Oldenbourg, 2003. [Lev03] John Levon, Oprofile internals, 2003. [LJ03] Tao Li and Lizy Kurian John, Run-time modeling and estimation of operating system power consumption, SIGMETRICS Perform. Eval. Rev. 31 (2003), no. 1, 160–171. [LMP08] Davide Lorenzoli, Leonardo Mariani, and Mauro Pezzè, Automatic generation of software behavioral models, Proceedings of the 30th international conference on Software engineering (New York, NY, USA), ICSE ’08, ACM, 2008, pp. 501–510. [Lov10] Robert Love, Linux Kernel Development, third ed., AddisonWesley Professional, 2010. [Lud03] Jochen Ludewig, Models in software engineering: an introduction, Software and Systems Modeling 2 (2003), no. 1, 5–14, 10.1007/s10270-003-0020-3. [LY99] Tim Lindholm and Frank Yellin, Java virtual machine specification, 2nd ed., Addison-Wesley Longman Publishing Co., Inc., Boston, MA, USA, 1999. [MD07] Mesfin Mulugeta Dinku, Qos contract negotiation in distributed component-based software, Ph.D. thesis, Technische Universität Dresden, Dresden, Germany, July 2007. [MH10] Marcus Lagergren Marcus Hirt, Oracle JRockit The Definitive Guide, Packt Publishing, 2010. [MMG06] Richard McDougall, Jim Mauro, and Brendan Gregg, Solaris(TM) Performance and Tools: DTrace and MDB Techniques for Solaris 10 and OpenSolaris (Solaris Series), Prentice Hall PTR, Upper Saddle River, NJ, USA, 2006. [MN07] Marc van den Bogaard Mirco Novakovic, Profiling, Diagnose und Monitoring, Javamagazin, Januar 2007. LITERATURVERZEICHNIS 151 [Pae06] Jürgen Paetz, Soft Computing in der Bioinformatik: Eine grundlegende Einführung und Übersicht (eXamen.press), SpringerVerlag New York, Inc., Secaucus, NJ, USA, 2006. [PN08] Meikel Poess and Raghuanath Othayoh Nambiar, Energy Cost, The Key Challenge of Today’s Data Centers: A Power Consumption Analysis of TPC-C Results, Proceedings of the PVLD08 (New York, NY, USA), ACM Press, 2008, pp. 1229– 1240. [PS06] Venkatesh Pallipadi and Alexey Starikovskiy, The ondemand governor: past, present and future, Proceedings of Linux Symposium, vol. 2, pp. 223-238, 2006. [Roy70] Walker W. Royce, Managing the development of large software systems: concepts and techniques, Proc. IEEE WESTCON, Los Angeles (1970), 1–9, Reprinted in Proceedings of the Ninth International Conference on Software Engineering, March 1987, pp. 328–338. [Sax10] Eric Saxe, Power-Efficient Software, ACM Queue 8 (2010), no. 1, 10–17. [SBPM08] Dave Steinberg, Frank Budinsky, Marcelo Paternostro, and Ed Merks, EMF: Eclipse Modeling Framework, Addison-Wesley Professional, 2008. [SEP+ 09] Chiyoung Seo, George Edwards, Daniel Popescu, Sam Malek, and Nenad Medvidovic, A framework for estimating the energy consumption induced by a distributed system’s architectural style, Proceedings of the 8th international workshop on Specification and verification of component-based systems (New York, NY, USA), SAVCBS ’09, ACM, 2009, pp. 27–34. [SL09] Michael Schmidt and Hod Lipson, Distilling Free-Form Natural Laws from Experimental Data, Science 324 (2009), no. 5923, 81–85. [SMM08] Chiyoung Seo, Sam Malek, and Nenad Medvidovic, Estimating the Energy Consumption in Pervasive Java-Based Systems, Pervasive Computing and Communications, IEEE International Conference on 0 (2008), 243–247. 152 LITERATURVERZEICHNIS [SMSS10] Rajiv Ranjan Suman, Rajib Mall, Srihari Sukumaran, and Manoranjan Satpathy, Extracting State Models for Black-Box Software Components, Journal of Object Technology 9 (2010), no. 3, 79–103. [Sta73] Herbert Stachowiak, Allgemeine Modelltheorie, Springer, Wien [u.a.], 1973. [Sun08] Sun, Handbuch zur dynamischen Ablaufverfolgung in Solaris. [Szy02] Clemens Szyperski, Component Software: Beyond ObjectOriented Programming, 2nd ed., Addison-Wesley Longman Publishing Co., Inc., Boston, MA, USA, 2002. [Tan07] Andrew S. Tanenbaum, Modern Operating Systems, 3rd ed., Prentice Hall Press, Upper Saddle River, NJ, USA, 2007. [The06] The Apache Software Foundation, Bcel, 2006. [Tho83] R. Thom, Mathematical models of morphogenesis, Ellis Horwood series in mathematics and its applications, Ellis Horwood, 1983. [TKTJ02] A. Raghunathan T. K. Tan and N. K. Jha, Embedded Operating System Energy Analysis and Macro-Modeling, ICCD ’02: Proceedings of the 2002 IEEE International Conference on Computer Design: VLSI in Computers and Processors (ICCD’02) (Washington, DC, USA), IEEE Computer Society, 2002, p. 515. [TW04] Jie Tao and Josef Weidendorfer, Cache Simulation Based on Runtime Instrumentation for OpenMP Applications, Proceedings of the 37th annual symposium on Simulation (Washington, DC, USA), ANSS ’04, IEEE Computer Society, 2004, pp. 97–. [TWM+ 08] Niraj Tolia, Zhikui Wang, Manish Marwah, Cullen Bash, Parthasarathy Ranganathan, and Xiaoyun Zhu, Delivering Energy Proportionality with Non Energy-Proportional Systems: Optimizing the Ensemble, HotPower ’08: Workshop on Power Aware Computing and Systems, ACM, December 2008. [UGV08] Leif Uhsadel, Andy Georges, and Ingrid Verbauwhede, Exploiting Hardware Performance Counters, Proceedings of the 2008 5th Workshop on Fault Diagnosis and Tolerance in Cryptography (Washington, DC, USA), IEEE Computer Society, 2008, pp. 59–67. LITERATURVERZEICHNIS 153 [WBHS07] Neil Walkinshaw, Kirill Bogdanov, Mike Holcombe, and Sarah Salahuddin, Reverse Engineering State Machines by Interactive Grammar Inference, WCRE ’07: Proceedings of the 14th Working Conference on Reverse Engineering (Washington, DC, USA), IEEE Computer Society, 2007, pp. 209–218. [WCG+ 10] Claas Wilke, Sebastian Cech, Sebastian Götz, Matthias Schmidt, Johannes Waltsgott, and Ronny Fritzsche, Energy Contract Language (ECL) Specification v0.1, Tech. report, Dresden. Germany, 2010. [ZPK00] Bernard P. Zeigler, Herbert Praehofer, and Tag G. Kim, Theory of Modeling and Simulation, Second Edition, 2 ed., Academic Press, January 2000. 154 LITERATURVERZEICHNIS Abbildungsverzeichnis 1.1 Schematische Darstellung der Lösung . . . . . . . . . . . . . . 11 2.1 2.2 Zentrale Pakete im CCM . . . . . . . . . . . . Autonome Kontrollschleife eines Auto Tuning [DDF+ 06] (neu gezeichnet) . . . . . . . . . . . Architektur des THEATRE, aus [GWS+ 10b] . 2.3 3.1 3.2 3.3 3.4 4.1 4.2 . . . . . . . . . 16 Systems, aus . . . . . . . . . 21 . . . . . . . . . 22 Prozess der Modellbildung nach der AMT . . . . . . . . . . Grundlegende Konzepte eines Systems . . . . . . . . . . . . Ein Objektverhaltensmodell der Klasse Vector, neu gezeichnet aus [Dal10] . . . . . . . . . . . . . . . . . . . . . . . . . Problematik des Ressourcennutzungsmodells . . . . . . . . . . 29 . 30 . 33 . 35 4.5 4.6 Schematische Darstellung des betrachteten Gesamtsystems . . Energieeffizienz bei schlechter Proportionalität (links) und besserer Proportionalität (rechts), aus [BH07] . . . . . . . . . . . Processor Power States, vereinfacht aus [HPIM+ 10] . . . . . . Ausgabe von Powertop für Intel(R) Core(TM)2 Duo CPU (P7350), Rechner im Batteriebetrieb . . . . . . . . . . . . . . Das Betriebssystem zwischen Hardware und Software . . . . . Prozesszustände in Linux , nach [Lov10] . . . . . . . . . . . . 5.1 5.2 5.3 5.4 Verhaltensbeschreibung: abstrakt aus konkret Kontext der Betrachtungen . . . . . . . . . . . Übersicht Konzept . . . . . . . . . . . . . . . Ablauf für Modellersteller als Flussdiagramm . 6.1 6.2 6.3 6.4 Verhältnis zwischen Skript, Tapset und Kernelfunktion . . . Funktionsweise von SystemTap nach . . . . . . . . . . . . . Kernelemente der JSON als Syntaxdiagramm . . . . . . . . Eureqa bei der Auswertung der CPU-Daten einer HeapSortKomponente . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.3 4.4 155 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47 48 50 51 53 56 75 80 84 86 . 95 . 96 . 101 . 103 156 6.5 6.6 6.7 6.8 6.9 7.1 7.2 7.3 7.4 7.5 7.6 A.1 A.2 A.3 A.4 ABBILDUNGSVERZEICHNIS Verbindung zwischen JVM und Profiler durch Bytecode Instrumentierung . . . . . . . . . . . . . . . . . . . . . . . . . Simples SUT-Skellet, wie es vom Codegenerator erstellt wird Wizard zur Code-Generierung . . . . . . . . . . . . . . . . . Die CPU-View des Analyse-Framework, mit ausklappbaren Detaildaten . . . . . . . . . . . . . . . . . . . . . . . . . . . Schema des Erweiterungspunktes für Analyser . . . . . . . . . 106 . 113 . 115 . 116 . 118 Profiling-Setup für Sortieralgorithmen . . . . . . . . . . . . . . Einfluss der Länge der zu sortierenden Strings auf die CPU Nutzung bei Heap- und Quicksort . . . . . . . . . . . . . . . . CPU-Nutzung von Heap- und Quicksort in Abhängigkeit der Arraygröße . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Vergleich der Festplattennutzung bei Speicherung verschiedener Arrays . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Zeitlicher Verlauf der Methodenausführung (sortieren und speichern) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Netzwerknutzung im zeitlichen Ablauf zweier commit() Ausführungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Das Strukturpaket des CCM, aus [GCW+ 10] . . . . . . Das Verhaltenspaket des CCM, aus [GCW+ 10] . . . . . Das Variantenpaket des CCM, aus [GCW+ 10] . . . . . Beispiel: Komponenten eines Videoservers, aus [MD07]. . . . . . . . . . . . . . . . . 123 126 128 130 132 136 157 158 158 159 Anhang A CoolSoftware A.1 CCM-Metamodell Direction 2 PortType IN OUT INOUT d : Direction PortConnectorType UserType * ComponentType ResourceType * 1 Contract SWComponent Type * Abbildung A.1: Das Strukturpaket des CCM, aus [GCW+ 10] 157 158 ANHANG A. COOLSOFTWARE Amount Time 1 1 WorkloadItem Pin d : Direction * Navigating Element * * BehaviorTemplate Workload StateMachine CostParameter * Abbildung A.2: Das Verhaltenspaket des CCM, aus [GCW+ 10] PortConnectorType type PortType 1 type 1 2 PortConnector 1 1 Link * * Component 1 Port Pin * * 1 Behavior BehaviorTemplate type type 1 ComponentType * * CostParameter Substitution 1 CostParameter Abbildung A.3: Das Variantenpaket des CCM, aus [GCW+ 10] 159 A.2. BEISPIEL A.2 Beispiel <<container>> <<container>> Client Server <<component>> <<component>> VideoServer VideoPlayer <<uses>> <<uses>> <<uses>> <<uses>> <<uses>> <<resource>> <<resource>> <<resource>> <<resource>> CPU Memory Connection FileServer Abbildung A.4: Beispiel: Komponenten eines Videoservers, aus [MD07]. 160 ANHANG A. COOLSOFTWARE A.3 1 2 3 4 5 1 2 3 4 5 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 ECL für Beispiel q u a l i t y _ c h a r a c t e r i s t i c frameRate ( stream : VideoStream ) { domain : numeric integer frames / second ; values : stream . getFrameRate () } r e s o u r c e Network { q u a l i t y _ c h a r a c t e r i s t i c bandwidth { domain : numeric real KBit / Second ; } } profile NetworkStates for Network { state connected { energy - effect 0.8 Watt ; } state disconnected { energy - effect 0.4 Watt ; } t r a n s i t i o n connected -> disconnected { w h e n e v e r event - occurs disconnect () ; delay 1.0 seconds ; energy - effect 5.0 Watt ; } t r a n s i t i o n disconnected -> connected { w h e n e v e r event - occurs send () ; delay 10.0 seconds ; energy - effect 30.0 Watt ; } initial - state disconnected ; p r e c e d e n c e connected , disconnected ; } profile ServerProfile for VideoServer { state highQuality { p r o v i d e s [ c h a r a c t e r i s t i c frameRate = 30] and [ c h a r a c t e r i s t i c imageWidth = 352] and [ c h a r a c t e r i s t i c imageHeight = 288]; resources [ c h a r a c t e r i s t i c bandwidth = 450]; } state lowQuality { p r o v i d e s [ c h a r a c t e r i s t i c frameRate = 10] and [ c h a r a c t e r i s t i c imageWidth = 176] and [ c h a r a c t e r i s t i c imageHeight = 144]; resources [ c h a r a c t e r i s t i c bandwidth = 150]; } t r a n s i t i o n any - state -> any - state {} p r e c e d e n c e highQuality , lowQuality ; } A.3. ECL FÜR BEISPIEL 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 profile PlayerProfile for VideoPlayer { state highQuality { uses [ ServerProfile . highQuality ]; p r o v i d e s [ c h a r a c t e r i s t i c frameRate = 30] and [ c h a r a c t e r i s t i c image_width = 352] and [ c h a r a c t e r i s t i c image_height = 288]; resources [ c h a r a c t e r i s t i c bandwidth = 450] , [ c h a r a c t e r i s t i c cpu_usage = 66.7] , [ c h a r a c t e r i s t i c ram = 200.0]; } state lowQuality { uses [ ServerProfile . lowQuality ]; p r o v i d e s [ c h a r a c t e r i s t i c frameRate = 10] and [ c h a r a c t e r i s t i c image_width = 176] and [ c h a r a c t e r i s t i c image_height = 144]; resources [ c h a r a c t e r i s t i c bandwidth = 150] , [ c h a r a c t e r i s t i c cpu_usage = 25.0] , [ c h a r a c t e r i s t i c ram = 50.0]; } t r a n s i t i o n any - state -> any - state {} p r e c e d e n c e highQuality , lowQuality ; } 161 162 ANHANG A. COOLSOFTWARE Anhang B Quellcode B.1 1 2 3 4 Zentrale Bestandteile der Profiler /* BEGIN NETWORK */ probe netdev . transmit { tid = tid () 5 if ( pid () == target () ) { 6 7 foreach ([ s , t ] in activ e_method s ) { 8 9 if ( t == tid ) { method_trans [ tid , dev_name ] += truesize break } 10 11 12 13 14 } 15 16 target_trans [ dev_name ] += truesize 17 18 } else { rest_trans [ dev_name ] += truesize } if (!([ dev_name ] in dev_list ) ) dev_list [ dev_name ] = 1 19 20 21 22 23 } 24 25 26 27 28 probe netdev . receive { tid = tid () 29 30 if ( pid () == target () ) { 31 32 foreach ([ s , t ] in activ e_method s ) { 33 if ( t == tid ) { method_rec [ tid , dev_name ] += truesize break } 34 35 36 37 38 } 163 164 ANHANG B. QUELLCODE target_rec [ dev_name ] += truesize } else { rest_rec [ dev_name ] += truesize } if (!([ dev_name ] in dev_list ) ) dev_list [ dev_name ] = 1 39 40 41 42 43 44 45 } /* END NETWORK */ Listing B.1: Hauptprüfpunkte des Netzwerk-Profiler 1 2 3 4 5 6 7 8 9 /* [...] from SystemTap RedBook : * There is also a very useful asynchronous probe called * timer . profile . Handler routines attached to this probe * are executed on each CPU at every timer interrupt . This makes * the timer . profile probe useful for writing performance * profiling tools in SystemTap , because the handler can look * at the process ’s state on every CPU at a high frequency . */ probe timer . profile { 10 tid = tid () exe = execname () cpu = cpu () 11 12 13 14 if ( pid () == target () ) { 15 16 foreach ([ s , t ] in activ e_method s ) { 17 18 if ( t == tid ) { method_ticks [ tid , cpu ] += 1 break 19 20 21 22 } 23 } 24 25 m e t h o d _ t a r g e t _ t i c k s [ cpu ] += 1 26 27 target_cycles [ tid , cpu ] += 1 28 29 } else { 30 31 if ( exe != " swapper " ) { 32 33 m e t h o d _ r e s t _ t i c k s [ cpu ] += 1 allcpus [ cpu ] = 1 34 35 36 } else { 37 38 m e t h o d _ s w a p p e r _ t i c k s [ cpu ] += 1 39 40 } 41 42 } all_cycles [ exe , cpu ] += 1 43 44 45 } Listing B.2: Hauptprüfpunkt des CPU-Profilers B.2. BTRACE-SKRIPT FÜR SORTIERALGORITHMEN B.2 165 BTrace-Skript für Sortieralgorithmen 1 2 package algorithms . sut ; 3 4 5 6 7 8 import import import import import com . sun . btrace . annotations . BTrace ; com . sun . btrace . annotations . Kind ; com . sun . btrace . annotations . Location ; com . sun . btrace . annotations . OnMethod ; com . sun . btrace . AnyType ; 9 10 11 12 13 14 15 16 @BTrace public class BTraceScript { @ O n M e t h o d ( clazz = " a l g o r i t h m s . Q u i c k S o r t " , method = " sort " ) public static void s o r t Q u i c k S o r t E n t r y ( AnyType [] args ) { System . out . println ( " a l g o r i t h m s : Q u i c k S o r t : sort : " + args [1] + " ; " + args [2] + " : m e t h o d _ e n t r : " ) ; } 17 @ O n M e t h o d ( clazz = " a l g o r i t h m s . Q u i c k S o r t " , method = " sort " , location = @ L o c a t i o n ( value = Kind . RETURN ) ) public static void s o r t Q u i c k S o r t E x i t ( AnyType [] args ) { System . out . println ( " a l g o r i t h m s : Q u i c k S o r t : sort : " + args [1] + " ; " + args [2] + " : m e t h o d _ e x i t : " ) ; } 18 19 20 21 22 23 @ O n M e t h o d ( clazz = " a l g o r i t h m s . B u b b l e S o r t " , method = " sort " ) public static void s o r t B u b b l e S o r t E n t r y ( AnyType [] args ) { System . out . println ( " a l g o r i t h m s : B u b b l e S o r t : sort : " + args [1] + " ; " + args [2] + " : m e t h o d _ e n t r : " ) ; } 24 25 26 27 28 29 @ O n M e t h o d ( clazz = " a l g o r i t h m s . B u b b l e S o r t " , method = " sort " , location = @ L o c a t i o n ( value = Kind . RETURN ) ) public static void s o r t B u b b l e S o r t E x i t ( AnyType [] args ) { System . out . println ( " a l g o r i t h m s : B u b b l e S o r t : sort : " + args [1] + " ; " + args [2] + " : m e t h o d _ e x i t : " ) ; } 30 31 32 33 34 35 36 @ O n M e t h o d ( clazz = " a l g o r i t h m s . H e a p S o r t " , method = " sort " ) public static void s o r t H e a p S o r t E n t r y ( AnyType [] args ) { System . out . println ( " a l g o r i t h m s : H e a p S o r t : sort : " + args [1] + " ; " + args [2] + " : m e t h o d _ e n t r : " ) ; } 37 38 39 40 41 42 @ O n M e t h o d ( clazz = " a l g o r i t h m s . H e a p S o r t " , method = " sort " , location = @ L o c a t i o n ( value = Kind . RETURN ) ) public static void s o r t H e a p S o r t E x i t ( AnyType [] args ) { System . out . println ( " a l g o r i t h m s : H e a p S o r t : sort : " + args [1] + " ; " + args [2] + " : m e t h o d _ e x i t : " ) ; } 43 44 45 46 47 48 49 } Listing B.3: BTrace-Skript für BubbleSort, QuickSort und HeapSort Erklärung Ich erkläre, dass ich die vorliegende Arbeit selbständig, unter Angabe aller Zitate und nur unter Verwendung der angegebenen Literatur und Hilfsmittel angefertigt habe. Dresden, den 31.März 2011