Fernuniversität in Hagen - Lehrgebiet Datenbanksysteme für neue

Werbung
Fernuniversität in Hagen
Sommersemster 2011
Seminar
–
MapReduce und Datenbanken
Prof. Dr. Ralf Hartmut Güting
Simone Jandt
c Fernuniversität in Hagen, Juni 2011
Inhaltsverzeichnis
1 Allgemeines
2
2 Themen
2.1 Klassische Parallele Datenbanken . . . . . . . . . . . . . . .
2.1.1 Thema 1: Parallele Datenbanken . . . . . . . . . . .
2.1.2 Thema 2: Parallele Anfrageauswertung in Volcano .
2.2 Thema 3: MapReduce und Hadoop . . . . . . . . . . . . . .
2.3 Thema 4: Parallele Datenbanken vs. MapReduce . . . . . .
2.4 Datenbanken und MapReduce . . . . . . . . . . . . . . . . .
2.4.1 Thema 5: HadoopDB . . . . . . . . . . . . . . . . .
2.4.2 Thema 6: Dryad . . . . . . . . . . . . . . . . . . . .
2.4.3 Thema 7: DryadLINQ . . . . . . . . . . . . . . . . .
2.4.4 Thema 8: PigLatin . . . . . . . . . . . . . . . . . . .
2.4.5 Thema 9: SCOPE . . . . . . . . . . . . . . . . . . .
2.4.6 Thema 10: Osprey . . . . . . . . . . . . . . . . . . .
2.5 Joins . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.5.1 Thema 11: Map-Reduce-Merge . . . . . . . . . . . .
2.5.2 Thema 12: Optimierung von Joins . . . . . . . . . .
2.5.3 Thema 13: Spatial Join . . . . . . . . . . . . . . . .
2.6 Thema 14: MapReduce für Multicore Rechner . . . . . . . .
2.7 Thema 15: Strom- bzw. Onlineverarbeitung mit MapReduce
2.8 Anwendungen . . . . . . . . . . . . . . . . . . . . . . . . . .
2.8.1 Thema 16: Ähnlichkeitssuche auf Mengen . . . . . .
2.8.2 Thema 17: Clustern von dynamischen Datenmengen
2.8.3 Thema 18: Mustererkennung in Netzwerken . . . . .
2.8.4 Thema 19: Effiziente Graphanalyse . . . . . . . . . .
2.8.5 Thema 20: Spezielle Anwendungen . . . . . . . . . .
2.9 Weitere mögliche Themen . . . . . . . . . . . . . . . . . . .
2.9.1 Thema 21: Performance in gemischten Umgebungen
2.9.2 Thema 22: Sicherheit verteilter Datenbanken . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
2
2
2
2
3
3
3
3
3
3
3
4
4
4
4
4
4
4
4
4
5
5
5
5
5
5
5
5
3 Ausarbeitungen
3.1 Parallele Anfrageauswertung mit Volcano
3.2 MapReduce und Hadoop . . . . . . . . . .
3.3 HadoopDB . . . . . . . . . . . . . . . . .
3.4 Dryad . . . . . . . . . . . . . . . . . . . .
3.5 DryadLINQ . . . . . . . . . . . . . . . . .
3.6 PigLatin . . . . . . . . . . . . . . . . . . .
3.7 SCOPE . . . . . . . . . . . . . . . . . . .
3.8 Osprey . . . . . . . . . . . . . . . . . . . .
3.9 Map-Reduce-Merge . . . . . . . . . . . . .
3.10 Optimierung von Joins . . . . . . . . . . .
3.11 Spatial Join with MapReduce . . . . . . .
3.12 MapReduce für Multicore Rechner . . . .
3.13 Stromverarbeitung mit MapReduce . . . .
3.14 Ähnlichkeitssuche auf Mengen . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
9
10
30
50
65
85
110
131
145
164
177
192
213
235
252
1
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
1
Allgemeines
Im Jahr 2004 wurde von Entwicklern der Firma Google ein einfaches Modell für die parallele
Programmierung und Verarbeitung sehr großer Datenmengen namens MapReduce vorgestellt. Ein
Programmierer implementiert dabei eine map-Methode, die ein Paar der Form (Schlüssel, Wert)
in eine Menge von (Schlüssel, Wert)-Paaren transformiert, sowie eine reduce-Methode, die eine
Menge von Werten zu einem gegebenen Schlüssel zu einem einzigen Wert reduziert.
Das MapReduce-Ausführungssystem sorgt dafür, dass die vom Programmierer definierten mapund reduce-Methoden parallel auf beliebig vielen Rechnern ausgeführt werden und dass eine reduceMethode alle Werte zu einem gegebenen Schlüssel erhält, die ja von vielen map-Methoden auf
anderen Rechnern erzeugt wurden. Gleichzeitig garantiert es Fehlertoleranz, d.h., Ausfälle einzelner Rechner werden vom Gesamtsystem abgefangen und führen nicht einmal zu nennenswerten
Verzögerungen. Das Gleiche gilt für einzelne Rechner, die nur sehr langsam arbeiten; ihre Aufgaben werden automatisch auf andere Rechner verlagert. Solche Eigenschaften sind essentiell für hoch
skalierbare Anwendungen, bei denen Tausende von Rechnern Terabytes von Daten verarbeiten.
Gleichzeitig ermöglicht das Modell Programmierern, ohne Vorkenntnisse der Parallelverarbeitung
mit einfachen Programmen enorme Datenmengen effizient zu verarbeiten.
Das Modell hat Furore gemacht, unter anderem dadurch, dass eine Open-Source-Implementierung
unter dem Namen Hadoop frei verfügbar ist. Die Datenbankwelt geriet zunächst in eine Defensivposition und versuchte zu zeigen, dass die in jahrzehntelanger Forschung entwickelten hochoptimierten
parallelen Datenbanksysteme doch noch Performanzvorteile haben. Es ließ sich aber nicht leugnen,
dass parallele Datenbanksysteme höchstens für einige Dutzend Computer konzipiert waren und
deshalb nicht die gleichen Fehlertoleranzqualitäten besitzen. Außerdem sind Lizenzen für solche
Systeme sehr teuer. Seit einigen Jahren sind Versuche der Kopplung von MapReduce-Techniken
bzw. Hadoop mit Datenbanken ein heißes Thema in der Datenbankforschung. Solche Ansätze und
die Bearbeitung von Datenbankaufgaben mit MapReduce-Techniken sind Thema des Seminars.
Im Abschnitt 2 stellen wir Ihnen die für das Seminar vorgesehen Themen kurz vor. Bevor wir die
Ausarbeitungen der Studenten zu den erfolgreich bearbeiteten Themen in Abschnitt 3 präsentieren.
2
2.1
Themen
Klassische Parallele Datenbanken
Der erste Vortragsblock bietet einen Überblick über die vor MapReduce existierenden Mechanismen
für parallele Datenbanken und Datenverarbeitung.
2.1.1
Thema 1: Parallele Datenbanken
[DG92] beschreibt die grundlegenden Ziele und Techniken der parallelen Datenverarbeitung.
2.1.2
Thema 2: Parallele Anfrageauswertung in Volcano
Volcano [Gra90,GD93] beschreibt eine Schnittstelle, die die eigentliche Hardware-Architektur kapselt und sich um die Details der parallelen Ausführung von Datenbankoperationen kümmert.
So können Operatoren, die diese Schnittstelle bedienen und die ursprünglich für Ein-ProzessorSysteme konzipiert und optimiert wurden auch in parallelen Datenbankumfeld eingesetzt werden.
2
2.2
Thema 3: MapReduce und Hadoop
Das von Google entwickelte MapReduce Framework [DG04] basiert auf dem Google File System [GGL03]. Seine Open-Source-Implementierung Hadoop [Had11] bringt mit HDFS ein eigenes
verteiltes Filesystem mit. 1 Die Grundidee des MapReduce ist in allen weiteren Vorträgen enthalten, deshalb soll es hier mehr um die technischen Hintergründe des MapReduce-Verfahrens und
die Sicherstellung seiner Zusicherungen der Fehlertoleranz und der Korrektheit gehen.
2.3
Thema 4: Parallele Datenbanken vs. MapReduce
Wie in der Seminarankündigung erwähnt, war der Einsatz von MapReduce im Datenbankumfeld
nicht unumstritten. Während die eine Seite die Vorteile der MapReduce-Techniken feierte und alles
über MapReduce lösen wollte, beriefen sich die Verfechter herkömmlicher paralleler Datenbanksysteme auf die Vorteile des Einsatzes von Indexen und andere Stärken, paralleler Datenbanksysteme,
die von MapReduce nicht unterstützt werden. Eine Diskussion der Stärken und Schwächen beider
Systeme bei der Verarbeitung großer Datenmengen findet sich u.a. in [PPR+ 09, SAD+ 10, DS08a]
und [DS08b].
2.4
Datenbanken und MapReduce
In diesem Vortragsblock werden verschiedene auf dem MapReduce Ansatz basierende bzw. von
ihm beeinflusste Datenbanksysteme und ihre Anfragesprachen vorgestellt.
2.4.1
Thema 5: HadoopDB
Hadoop selbst ist keine Datenbank, sondern nur eine Open-Source-Implementierung des MapReduce-Frameworks. [ABPA+ 09] beschreibt die Verknüpfung von PostgreSQL und Hadoop mittels
Hive [TSJ+ 09] zu einer kompletten Open-Source-Lösung für die verteilte Speicherung und parallele
Auswertung großer Datenmengen mittels MapReduce-Techniken in Rechnerclustern.
2.4.2
Thema 6: Dryad
Microsoft entwickelte mit Dryad [IBY+ 07] ein mit MapReduce vergleichbares System zur verteilten
parallelen Abwicklung von Datenbankabfragen in Clustern. Neben der Vorstellung des DryadSystems soll es hier auch um die Gemeinsamkeiten und Unterschiede der beiden Systeme gehen.
2.4.3
Thema 7: DryadLINQ
DryadLINQ [YIF+ 08, IY09] stellt auf der Basis von .NET-Objekten eine Menge von Spracherweiterungen zur Verfügung, die ein neues Programmiermodell für verteilte Anwendungen bilden.
2.4.4
Thema 8: PigLatin
Grundlage für den sinnvollen Einsatz von MapReduce im Datenbankumfeld ist die Schaffung entsprechender Schnittstellen zwischen der Datenbankanfragesprache und dem prozeduralen MapReduce-Framework. PigLatin von Yahoo! [ORS+ 08] ist ein Ansatz die Lücke zwischen SQL und
MapReduce zu schließen.
1 Wer für die Ausarbeitung seines Themas vorab tiefere Informationen zu MapReduce benötigt, als in seinem
Originaltext vorhanden sind, kann eine Kurzbeschreibung des MapReduce Frameworks in [DG08] finden.
3
2.4.5
Thema 9: SCOPE
SCOPE von Microsoft [CJL+ 08] versucht analog zu PigLatin die Lücke zwischen SQL und MapReduce zu schließen.
2.4.6
Thema 10: Osprey
[YYTM10] ist ein von der MapReduce Idee beeinflusstes System, das in verteilten Datenbanken
die fehlertolerante Ausführung von Anfragen, wie Sie bei MapReduce gegeben ist, sicherstellen soll.
2.5
Joins
Ein großes Thema in der Datenbankwelt ist der Verbund (Join) mehrerer Eingangsrelationen zu einer Ausgangsrelation. In den letzten Jahren wurden diverse Ansätze veröffentlicht, die den Verbund
von Datenmengen mittels MapReduce ermöglichen sollen.
2.5.1
Thema 11: Map-Reduce-Merge
[YDHP07] erweitert das MapReduce-Framework um eine Merge-Komponente, die die parallele
Ausführung relationaler Operationen, insbesondere auch von Joins, ermöglicht.
2.5.2
Thema 12: Optimierung von Joins
Die Optimierung der Ausführung von Operationen ist im Datenbankumfeld immer ein heißes Thema gewesen. So beschäftigt sich [AU10] mit der Optimierung von Join-Operationen im MapReduceUmfeld.
2.5.3
Thema 13: Spatial Join
Insbesondere im Umfeld geographischer Datenbanken ist die Zusammenführung von Daten nach
räumlicher Nähe ein Thema. [ZHL+ 09] beschreibt den Einsatz von MapReduce-Techniken für die
effiziente parallele Durchführung von Spatial-Join-Operationen.
2.6
Thema 14: MapReduce für Multicore Rechner
[CCZ10] verfeinert die MapReduce-Techniken, um die Datenanalyse auf Rechnern mit vielen
Prozessorkernen, die über eine gemeinsame Datenbasis verfügen, zu optimieren.
2.7
Thema 15: Strom- bzw. Onlineverarbeitung mit MapReduce
Das Grundkonzept von MapReduce ermöglicht zunächst keine Strom- bzw. laufende Online-Verarbeitung von Daten. [CCA+ 10, KAGW10] und [BAH10] sind drei Erweiterungen des MapReduce
Frameworks, die eine Strom- bzw. Online-Verabeitung auf der Basis von MapReduce-Techniken
ermöglichen. Hier reicht es, ein Modell ausführlich darzustellen und mit den anderen kurz zu
vergleichen.
2.8
Anwendungen
Inzwischen finden MapReduce-Techniken vielfältige Anwendungen im Datenbankumfeld. Ein paar
davon sollen im Folgenden vorgestellt werden.
4
2.8.1
Thema 16: Ähnlichkeitssuche auf Mengen
[VCL10] ermöglicht die Ermittlung und Gruppierung ähnlicher Datensätze mittels erweiterter
MapReduce-Techniken.
2.8.2
Thema 17: Clustern von dynamischen Datenmengen
Bei Google News [DDGR07] werden MapReduce-Verfahren eingesetzt, um ständig die aktuellen
Nachrichten verschiedener Quellen zu gleichen Themenkomplexen für Newsportale zusammenzuführen.
2.8.3
Thema 18: Mustererkennung in Netzwerken
Bei der Analyse großer Netzwerke treten wiederkehrende Muster auf. Diese können laut [LJC+ 09]
auch mit MapReduce-Techniken ermittelt werden.
2.8.4
Thema 19: Effiziente Graphanalyse
[LS10] beschäftigt sich mit der Optimierung der Analyse komplexer Graphen mit MapReduceTechniken.
2.8.5
Thema 20: Spezielle Anwendungen
Zum Schluß beschäftigen wir uns mit drei konkreten auf MapReduce basierenden Anwendungen, die
zum Teil auch über graphische Benutzerschnittstellen verfügen. Eins der Systeme sollte ausführlich
dargestellt werden, bei den anderen reicht eine kurze Beschreibung.
[WPR+ 08] stellt eine einfach zu bedienende Benutzerschnittstelle zur Verfügung, die es einfachen Anwendern ermöglicht große in einem Web Archiv abgelegte Dokumentmengen zu analysieren.
[Haz10] beschreibt den Einsatz von MapReduce-Techniken im Zusammenhang mit der Abfrage
von Protein Datenbanken.
[JVB09] beschreibt die Parallelisierung Genetischer Algorithmen mit MapReduce-Techniken.
2.9
2.9.1
Weitere mögliche Themen
Thema 21: Performance in gemischten Umgebungen
Die Fehlertoleranz von Hadoop ist auf Cluster mit identischer Hard- und Software ausgelegt.
In der Realität gibt es aber vielfach unterschiedliche Plattformen, die gemischt verwendet werden. [ZKJ+ 08] beschreibt, wie Hadoop verbessert werden kann, um auch in solchen gemischten
Rechnerumgebungen die Fehlertoleranz zu gewährleisten.
2.9.2
Thema 22: Sicherheit verteilter Datenbanken
MapReduce wird nicht zuletzt auch auf großen teilweise öffentlichen Clustern eingesetzt. Mit der
Frage, wie in diesen öffentlichen Umgebungen der Schutz der Daten und der Privatsphäre sichergestellt werden kann beschäftigt sich [RSK+ 10].
5
Literatur
[ABPA+ 09] Abouzeid, A. ; Bajda-Pawlikowski, K. ; Abadi, D. ; Silberschatz, A. ; Rasin,
A.: HadoopDB: An architectural hybrid of MapReduce and DBMS technologies for
analytical workloads. In: Proceedings of the VLDB Endowment 2 (2009), Nr. 1, S.
922–933. – ISSN 2150–8097
[AU10]
Afrati, F.N. ; Ullman, J.D.: Optimizing Joins in a Map-Reduce Environment. In:
Proceedings of the 13th International Conference on Extending Database Technology
ACM, 2010, S. 99–110
[BAH10]
Böse, J.H. ; Andrzejak, A. ; Högqvist, M.: Beyond Online Aggregation: Parallel
and Incremental Data Mining With Online Map-Reduce. In: Proceedings of the 2010
Workshop on Massive Data Analytics on the Cloud ACM, 2010, S. 1–6
[CCA+ 10]
Condie, T. ; Conway, N. ; Alvaro, P. ; Hellerstein, J.M. ; Elmeleegy, K. ;
Sears, R.: MapReduce Online. In: Proceedings of the 7th USENIX Conference on
Networked Systems Design and Implementation USENIX Association, 2010, S. 21
[CCZ10]
Chen, R. ; Chen, H. ; Zang, B.: Tiled-MapReduce: Optimizing Resource Usages
of Data-Parallel Applications on Multicore with Tiling. In: Proceedings of the 19th
international conference on Parallel architectures and compilation techniques ACM,
2010, S. 523–534
[CJL+ 08]
Chaiken, R. ; Jenkins, B. ; Larson, P.Å. ; Ramsey, B. ; Shakib, D. ; Weaver,
S. ; Zhou, J.: SCOPE: Easy and efficient parallel processing of massive data sets. In:
Proceedings of the VLDB Endowment 1 (2008), Nr. 2, S. 1265–1276. – ISSN 2150–8097
[DDGR07] Das, A.S. ; Datar, M. ; Garg, A. ; Rajaram, S.: Google News Personalization:
Scalable Online Collaborative Filtering. In: Proceedings of the 16th international conference on World Wide Web ACM, 2007, S. 271–280
[DG92]
DeWitt, D. ; Gray, J.: Parallel Database Systems: The Future of High Performance
Database Systems. In: Communications of the ACM 35 (1992), Nr. 6, S. 85–98. –
ISSN 0001–0782
[DG04]
Dean, J. ; Ghemawat, S.: MapReduce: Simplified Data Processing on Large Clusters. In: Proceedings of Operating Systems Design and Implementation (OSDI). San
Francisco, CA, 2004, S. 137 – 150
[DG08]
Dean, J. ; Ghemawat, S.: MapReduce: Simplified Data Processing on Large Clusters.
In: Communications of the ACM 51 (2008), January, Nr. 1, S. 107 – 113
[DS08a]
DeWitt, J. ; Stonebraker, M.:
MapReduce A Major Step Backwards. Web Blog. http://databasecolumn.vertica.com/database-innovation/
mapreduce-a-major-step-backwards/. Version: 2008
[DS08b]
DeWitt, J. ; Stonebraker, M.:
MapReduce II.
Web Blog.
//databasecolumn.vertica.com/database-innovation/mapreduce-ii/.
Version: 2008
[GD93]
Graefe, G. ; Davison, DL: Encapsulation of Parallelism and ArchitectureIndependence in Extensible Database Query Execution. In: IEEE Transactions on
Software Engineering 19 (1993), Nr. 8, S. 749–764. – ISSN 0098–5589
6
http:
[GGL03]
Ghemawat, S. ; Gobioff, H. ; Leung, S.-T.: The Google File System. In: 19th
Symposium on Operating Systems Principles. Lake George, New York, 2003, S. 29
–43
[Gra90]
Graefe, Goetz: Encapsulation of parallelism in the Volcano Query Processing System. In: Proceedings of the 1990 ACM SIGMOD international conference on Management of data. New York, NY, USA : ACM, 1990 (SIGMOD ’90). – ISBN 0–89791–
365–5, 102–111
[Had11]
Hadoop. Web Page. http://hadoop.apache.org. Version: 2011
[Haz10]
Hazelhurst, S.: PH2: An Hadoop-Based Framework for Mining Structural Properties from the PDB Database. In: Proceedings of the 2010 Annual Research Conference
of the South African Institute of Computer Scientists and Information Technologists
ACM, 2010, S. 104–112
[IBY+ 07]
Isard, M. ; Budiu, M. ; Yu, Y. ; Birrell, A. ; Fetterly, D.: Dryad: distributed
data-parallel programs from sequential building blocks. In: ACM SIGOPS Operating
Systems Review 41 (2007), Nr. 3, S. 59–72. – ISSN 0163–5980
[IY09]
Isard, M. ; Yu, Y.: Distributed data-parallel computing using a high-level programming language. In: Proceedings of the 35th SIGMOD international conference on
Management of data ACM, 2009, S. 987–994
[JVB09]
Jin, C. ; Vecchiola, C. ; Buyya, R.: MRPGA: An Extension of MapReduce for
Parallelizing Genetic Algorithms. In: eScience, 2008. eScience’08. IEEE Fourth International Conference on IEEE, 2009, S. 214–221
[KAGW10] Kumar, V. ; Andrade, H. ; Gedik, B. ; Wu, K.L.: DEDUCE: At the Intersection of MapReduce and Stream Processing. In: Proceedings of the 13th International
Conference on Extending Database Technology ACM, 2010, S. 657–662
[LJC+ 09]
Liu, Y. ; Jiang, X. ; Chen, H. ; Ma, J. ; Zhang, X.: Mapreduce-based pattern
finding algorithm applied in motif detection for prescription compatibility network.
In: Advanced Parallel Processing Technologies (2009), S. 341–355
[LS10]
Lin, J. ; Schatz, M.: Design Patterns for Efficient Graph Algorithms in MapReduce.
In: Proceedings of the Eighth Workshop on Mining and Learning with Graphs ACM,
2010, S. 78–85
[ORS+ 08]
Olston, C. ; Reed, B. ; Srivastava, U. ; Kumar, R. ; Tomkins, A.: Pig latin: a notso-foreign language for data processing. In: Proceedings of the 2008 ACM SIGMOD
International Conference on Management of Data ACM, 2008, S. 1099–1110
[PPR+ 09]
Pavlo, A. ; Paulson, E. ; Rasin, A. ; Abadi, D.J. ; DeWitt, D.J. ; Madden, S. ;
Stonebraker, M.: A Comparison of Approaches to Large-Scale Data Analysis. In:
Proceedings of the 35th SIGMOD International Conference on Management of Data
ACM, 2009, S. 165–178
[RSK+ 10]
Roy, I. ; Setty, S.T.V. ; Kilzer, A. ; Shmatikov, V. ; Witchel, E.: Airavat:
Security and Privacy for MapReduce. In: Proceedings of the 7th USENIX Conference
on Networked Systems Design and Implementation USENIX Association, 2010, S. 20
7
[SAD+ 10]
Stonebraker, M. ; Abadi, D. ; DeWitt, D.J. ; S.Madden ; Paulson, E. ; Pavlo,
A. ; Rasin, A.: MapReduce and Parallel DBMSs: Friends or Foes? In: Communications
of the ACM 53 (2010), January, Nr. 1, S. 64 – 71. http://dx.doi.org/10.1145/
1629175.1629197. – DOI 10.1145/1629175.1629197
[TSJ+ 09]
Thusoo, A. ; Sarma, J.S. ; Jain, N. ; Shao, Z. ; Chakka, P. ; Anthony, S. ; Liu,
H. ; Wyckoff, P. ; Murthy, R.: Hive: a warehousing solution over a map-reduce
framework. In: Proceedings of the VLDB Endowment 2 (2009), Nr. 2, S. 1626–1629.
– ISSN 2150–8097
[VCL10]
Vernica, R. ; Carey, M.J. ; Li, C.: Efficient parallel set-similarity joins using
MapReduce. In: Proceedings of the 2010 international conference on Management of
data ACM, 2010, S. 495–506
[WPR+ 08] Weigel, F. ; Panda, B. ; Riedewald, M. ; Gehrke, J. ; Calimlim, M.: Largescale collaborative analysis and extraction of web data. In: Proceedings of the VLDB
Endowment 1 (2008), Nr. 2, S. 1476–1479. – ISSN 2150–8097
[YDHP07]
Yang, H. ; Dasdan, A. ; Hsiao, R.L. ; Parker, D.S.: Map-reduce-merge: simplified
relational data processing on large clusters. In: Proceedings of the 2007 ACM SIGMOD
international conference on Management of data ACM, 2007, S. 1029–1040
[YIF+ 08]
Yu, Y. ; Isard, M. ; Fetterly, D. ; Budiu, M. ; Erlingsson, Ú. ; Gunda, P.K.
; Currey, J.: DryadLINQ: A System for General-Purpose Distributed Data-Parallel
Computing Using a High-Level Language. In: Proceedings of the 8th USENIX conference on Operating systems design and implementation USENIX Association, 2008,
S. 1–14
[YYTM10] Yang, C. ; Yen, C. ; Tan, C. ; Madden, S.R.: Osprey: Implementing MapReduceStyle Fault Tolerance in a Shared-Nothing Distributed Database. In: Data Engineering
(ICDE), 2010 IEEE 26th International Conference on IEEE, 2010, S. 657–668
[ZHL+ 09]
Zhang, S. ; Han, J. ; Liu, Z. ; Wang, K. ; Xu, Z.: SJMR: Parallelizing Spatial
Join with MapReduce on Clusters. In: Cluster Computing and Workshops, 2009.
CLUSTER’09. IEEE International Conference on IEEE, 2009. – ISSN 1552–5244, S.
1–8
[ZKJ+ 08]
Zaharia, M. ; Konwinski, A. ; Joseph, A.D. ; Katz, R. ; Stoica, I.: Improving MapReduce Performance in Heterogeneous Environments. In: Proceedings of the
8th USENIX conference on Operating systems design and implementation USENIX
Association, 2008, S. 29–42
8
3
Ausarbeitungen
9
Fern-Universität in Hagen
Seminar 01912
im Sommersemester 2011
„MapReduce und Datenbanken“
Thema 2
Parallele Anfrageauswertung in Volcano
Referent: Simon Geisbüsch
Gliederung zum Thema 2:
Parallele Anfrageauswertung in Volcano
1 Einführung
2 Designziele
2.1
Erweiterbarkeit
2.2
Parallelität
2.3
Unabhängigkeit
3 Grundlegendes Systemdesign
3.1
Dateisystem und Support-Funktionen
3.2
Operatoraufbau und der Operatorbaum
4 Inter-Operator-Parallelität
4.1
Der Exchange-Operator
4.2
Pipelining/Streaming
4.3
Teilbaum-Parallelität im Operatorbaum
4.4
Verwaltungsaufwand
5 Intra-Operator-Parallelität
5.1
Datenpartitionierung
6 Der erweiterte Exchange-Operator
6.1
Verarbeitungsknoten
6.2
Vektorbasierte Verarbeitung
6.3
Verwaltungsaufwand
7 Leistungsmerkmale und -fähigkeiten
7.1
Erweiterbarkeit, Parallelität, Unabhängigkeit, Skalierbarkeit
7.2
Verwaltungsaufwand
1 Einführung
Einzelne Datenbankanfragen in mehreren Prozessen parallel zu verarbeiten kann große
Leistungssteigerungen in einem Datenbanksystem erzeugen. Unterschiedliche Hardwaredesigns
von Verarbeitungsknoten (shared vs. distributed memory)1 und der Netzwerkübertragung führten
zu der Idee, die Hardwarekomponente so weit wie möglich von den Abläufen der parallelen
Anfrageauswertung zu kapseln. Ebenso wie die Hardwarebegebenheiten kann auch die
Datenverarbeitung in der Anfrageauswertung vollständig vom Verarbeitungsmechanismus zur
Erzeugung paralleler Prozesse in einer Anfrageauswertung gekapselt werden. Das Modul
Volcano wurde mit der Intention, genau diese weitreichende Flexibilität herzustellen, entworfen
und soll im Folgenden vorgestellt werden.2 Kapitel 2 beschreibt die Designziele und
Überlegungen zu den Themenschwerpunkten Erweiterbarkeit, Parallelität und Unabhängigkeit
im Bezug zum Systemaufbau der Anfrageauswertung. Kapitel 3 stellt die Grundlagen des
Dateisystems und der Datenverarbeitung sowie den Operatoraufbau und die Generierung eines
Operatorbaums aus einer Datenbankanfrage vor. Kapitel 4 beschäftigt sich ausführlich mit den
Abläufen zu parallelen Ausführung verschiedener Operatoren (Inter-Operator-Parallelität),
während Kapitel 5 die Möglichkeiten zur parallelen Ausführung innerhalb eines einzelnen
Operators beleuchtet (Intra-Operator-Parallelität). Kapitel 6 betrachtet die Verarbeitung einer
Anfrage im Falle auf mehrere Netzwerkknoten verteilter Prozesse. Kapitel 7 fasst die Ergebnisse
bezogen auf die Zielerreichung der Designideen in Volcano zusammen und schließt mit einer
Bewertung der Leistungsfähigkeit des Systems auch im Hinblick auf moderne Hardwaredesigns
und Netzwerkübertragungsgeschwindigkeiten ab.
2 Designziele
2.1 Erweiterbarkeit
Erweiterbarkeit kann in diesem Zusammenhang auf mehrere verschiedene Arten
interpretiert werden. Zum einen kann sich die Erweiterbarkeit auf die Datentypen beziehen, die
in einer Datenbank hinterlegt werden. Eine Anfrageauswertung sollte problemlos verschiedenste
Datentypen verarbeiten können, um flexibel eingesetzt werden zu können. Vor allem mit dem
Aufkommen objektorientierter Programmiersprachen und der Forderung nach persistenter
Speicherung verschiedenster Objekte und damit Datentypen ist diese Form der Erweiterbarkeit
ein wichtiges Merkmal geworden. Volcano wurde auch für die Möglichkeit geschaffen auch im
1 Im Falle von shared-memory teilen sich Prozesse einen gemeinsamen Pufferspeicher, bei distributed-memory ist
eine gemeinsame Adressierung und damit die Übergabe von Zeigern nicht möglich.
2 Grundlage der Erläuterungen sind [1] (Graefe 1990) und [2] (Graefe, Davidson 1993)
1
objektorientierten Umfeld eingesetzt zu werden.3 Daraus ergibt sich sofort die Forderung auch
Operatoren, die für verschiedene Datentypen nötig werden können, einfach in das System zu
integrieren. Ist diese Forderung bei einfacher sequenzieller Ausführung noch recht einfach
erfüllbar, ergeben sich durch eine parallele Auswertung unter Umständen erhebliche
Schwierigkeiten
bei
der Erstellung
neuer
Operatoren.
Schließlich
sollte auch
das
Hardwareumfeld der Datenbank erweiterbar und veränderbar sein. Die schnelle Entwicklung im
Hardwarebereich,
sowohl
bezüglich
der
Speicher
und
Prozessoren
als
auch
der
Netzwerktechnologie, macht eine einfache Anpassung unabdingbar.
2.2 Parallelität
Gerade durch die parallele Verarbeitung innerhalb einer einzelnen Anfrage lassen sich
enorme Leistungsverbesserungen erzielen. Unterstützt wird diese Entwicklung auch durch die
schnelle Entwicklung der Hardware. War ein Verarbeitungsnetz mit shared-memory und
mehreren Prozessoren bei der Entwicklung von Volcano noch ein komplizierter Aufbau mehrerer
einzelner Rechner, findet sich dieses Hardwaredesign mittlerweile in einem handelsüblichen PC
wieder. Parallelität kann dabei sehr verschiedene Formen annehmen. Die einfachste Form der
parallelen Auswertung ist das Streaming. Ein Operator beginnt dabei bereits mit Teilen des
Ergebnisses eines vorher aktiven Operators zu arbeiten, ohne dass dessen gesamtes Ergebnis
bereits erstellt und vollständig gespeichert wurde. Findet Streaming in einem einzigen Prozess
statt, beschreibt Pipelining die parallele Verarbeitung über Prozessgrenzen hinweg. Pipelining
und Streaming werden auch als vertikale Parallelität bezeichnet. Eine andere Form ist die
Teilbaum-Parallelität, also die Auswertung von verschiedenen unabhängigen Teilbäumen eines
Operatorbaums. Streaming und die Teilbaum-Parallelität gehören damit zu den Inter-OperatorParallelitäten. Eine weitere Form ist die parallele Auswertung innerhalb eines Operators, die z.B.
durch Datenpartitionierung erreicht werden kann. Verschiedene Partitionen werden gleichzeitig
von verschiedenen identischen Operatoren in verschiedenen Prozessen verarbeitet und das
Ergebnis zusammengetragen. Diese Form gehört damit zur Intra-Operator-Parallelität. Teilbaumund Intra-Operator-Parallelität werden auch als horizontale Parallelität bezeichnet. In Volcano
werden alle für die Erstellung dieser Arten von Parallelität nötigen Elemente bereitgestellt, unter
der Vorgabe, möglichst wenig Verwaltungsaufwand zu erzeugen.
2.3 Unabhängigkeit
Erweiterbarkeit und Parallelität werden in Volcano durch Kapselung, d.h. durch die
3 [3](Graefe 1994, III.B Seite 124)
2
Unabhängigkeit der verschiedenen Instrumente erreicht. [3] Graefe beschreibt diese Kapselung
von Parallelität des Verarbeitungsmechanismus zur Datenverarbeitung als „orthogonal“
zueinander.4 So werden die Datentypen in Volcano nicht definiert, sondern eine
datentypenunabhängige Verarbeitungsmethode geschaffen, die den Zugriff auf die Datensätze
ermöglicht, jedoch keine Annahmen über deren Aufbau oder Inhalt macht. Diese Linie verfolgt
auch der Operatoraufbau, der die Funktionsweise und die Verarbeitung regelt, die
Datenmanipulation aber gesonderten Konstrukten, den Support-Funktionen, überlässt. So kann
ein Operator auf beliebige Datentypen angepasst werden, während die interne Verarbeitung des
Operators in der Anfrageauswertung davon unabhängig bleibt. Parallelität wird in Volcano durch
einen eigens dafür entworfenen Operator erreicht, der sich nahtlos in einen Operatorbaum
integrieren lässt und der alle nötigen Anpassungen an die Parallelität vor allen
datenverarbeitenden Operatoren kapselt. Neue Operatoren können also frei von Überlegungen
zur Parallelität für eine sequenzielle Ausführung entworfen werden. Parallelität erschafft Volcano
unabhängig davon. Schließlich lassen sich das zugrundeliegende Dateisystem und die Parallelität
in Volcano auf die Hardwareumgebungen flexible anpassen, so dass Volcano nicht auf ein
Hardwaredesign festgelegt und damit unabhängig ist. Die Kapselung der Verarbeitungsmechanik
und der parallelen Anfrageauswertung sind also das beherrschende Designprinzip in Volcano.
Die Folgenden Abschnitte vertiefen die genauen Arbeitsweisen und Techniken.
3 Grundlegendes Systemdesign
3.1 Dateisystem und Support-Funktionen
Einzelne Datensätze sind über RID (record-identifier) direkt aufrufbar. Datensätze sind
zu Speicherseiten gruppiert. Eine oder mehrere Speicherseiten können flexibel zu einem Cluster
zusammengefasst werden. Die Clustergrößen werden in der Speicherdatei festgelegt, können
aber
von
Datei
zu
Datei
variieren.
Diese
Clustergrößen
können
damit
an
die
Hardwarebegebenheiten, also z.B. die Größe des Lese/Schreib-Puffers und im Falle verteilter
Datenbanken auch an die Paketgrößen des vorhandenen Netzwerkprotokolls angepasst werden.
Der Speichermanager stellt nur die wichtigsten Funktionen zur Verfügung wie das Sperren von
Datensätzen, das Überschreiben von Speicherseiten sowie Lese- und Schreibvorgänge. Eigene
Regeln sind nicht im Speichermanager implementiert. Die Steuerung dieser Funktionen wird
vollständig höheren Datenbankebenen anvertraut.
Dateien werden immer in fortlaufend zusammenhängendem Festspeicher abgelegt um
iterativen Zugriff auf die Datensätze zu erleichtern. Sie bieten auf der Ebene der Datei die
4 [3] (Graefe 1994, I)
3
Möglichkeit eines iterativen Durchlaufs (scan), also die Basisoperatoren open und close zur
Verwaltung des Zugangs zur Datei, next und rewind zur Iteration über die Datensätze, sowie
append zur Erstellung neuer Datensätze. Next liefert dabei die RID des nächsten Datensatzes.
Scans können darüber hinaus bereits auf der Dateiebene mit optionalen Prädikaten versehen
werden und ermöglichen eine Filterfunktion (selective scan). Da die Verarbeitungsmechanik
unabhängig von Datentypen sein soll, ist zunächst nicht klar, auf welche Daten sich ein solches
Prädikat überhaupt beziehen soll. Dazu wird die Prädikatfunktion über einen Zeiger dem
Operator übergeben. Diese Prädikatfunktion, die für die Prüfung des Prädikats zuständig ist, wird
als Support-Funktion bezeichnet und übernimmt hier die eigentliche Verarbeitung. Der
Filteroperator bleibt von der Ausgestaltung dieser Support-Funktion unberührt. Neben dem
iterativen Zugriff gestattet Volcano auch das Anlegen von Indices in Form von B+-Bäumen.
Einträge bestehen aus einem Schlüsselattribut und einem Datenattribut. Beide sind nicht an
Datentypen gebunden. Volcano gestattet die Suche über die Schlüsselattribute und unterstützt
dabei auch Bereichsabfragen. Support-Funktionen werden hier wie im Falle der selective scans
genutzt. Um Zwischenergebnisse zu speichern, verwendet Volcano virtuelle Laufwerke, die im
Pufferspeicher angelegt werden. Dabei ist die interne Verarbeitung dieses temporären Speichers
identisch mit der des Plattenspeichers. Wird ein Datensatz allerdings hier freigegeben, geht er
verloren.
Durch das variable Dateisystem kann die Verarbeitungsmechanik also an Lese/SchreibPuffergrößen sowie an die Größe der transferierbaren Netzwerkpakete optimal angepasst werden.
Die Dateien erlauben iterativen Zugriff und das Anlegen von Indices. Die
implementierte
Filterfunktion ist durch den Gebrauch von Support-Funktionen unabhängig von den Datentypen.
Die Verarbeitungsmechanik ist unabhängig von den Datenspezifikationen geblieben.5
3.2 Operatoraufbau und der Operatorbaum
Anfragen werden in Volcano als algebraische Ausdrücke dargestellt. Dabei sind die
Operatoren dieser Algebra alle direkt ausführbare Algorithmen. Greafe spricht daher von einer
„ausführbaren“ Algebra und unterscheidet diese von der „logischen“ Algebra relationaler Art. 6
Ausgehend von der untersten Verwaltungsebene im Dateisystem werden diese ausführbaren
Operatoren darauf definiert. Alle Operatoren fungieren als Iteratoren über Clustern von
Datensätzen und besitzen daher ebenfalls die Basisoperatoren open, next und close. Hinter dem
Operator open verbergen sich dabei alle Anweisungen, die die Funktionsweise des Operators
unterstützen. Das sind z.B. das Anlegen von Speicherstrukturen (z.B. Hash-Tabellen) oder
5 Beschreibungen des Dateisystems gehen auf [3] (Greafe 1994) zurück.
6 [3] (Greafe 1994, III.B, Seite 124)
4
Zwischenspeicherdateien im virtuellen Laufwerk, aber auch der Aufruf weiterer Operatoren, die
den Input, also die zu verarbeitenden Datensätze liefern. Jeder open-Aufruf endet mit dem
Aufruf von next, der an die Inputoperatoren den Befehl zur Übergabe des nächsten Datensatzes
richtet. Der anfordernde Operator tritt dabei also als Konsument von Datensätzen auf, der
übergebende als Produzent eines Outputs. Close wird nur ausgeführt, wenn vom konsumierenden
Operator die ausdrückliche Aufforderung zum Schließen des Operator ergeht bzw. von dem
Wurzeloperator eines Operatorbaums, der allein den Abschluss
einer Anfrageauswertung
feststellen kann.
Diese Basisoperatoren dienen also genau wie im Dateisystem ausschließlich der
Verwaltung des Datenflusses im Operator. Die eigentliche Datenverarbeitung wird wieder durch
das Konstrukt der bereits erwähnten Support-Funktionen „injiziert“. Dies gewährt jedem
Operator Datentypunabhängigkeit, da es die Verwaltung, also den Mechanismus von der
Typendefinition trennt. Ohne diese Support-Funktionen ist also keine Datenmanipulation oder
Interpretation möglich. Die passenden Support-Funktionen werden dem Operator samt ihrer
Ausführungsparamater als typlose Zeiger auf die Einstiegsstelle der Funktionsdefinition zur
Verfügung gestellt.
Gespeichert sind diese Zeiger im Status-
open;next;close;
OPERATOR
MODUL
Datensatz (state-record), der zusätzlich in
Support-Funktionen;
Argumente;
jedem Operator vorhanden ist. In diesem state-
open;next;close;
record sind alle Elemente hinterlegt, die für die
Status-record;
Bindings;
Ausführung
wichtig
sind.
übergebenen
Zeigern
auf
Neben
die
den
Support-
Funktionen und den Zeigern auf die open, next
Kap-3.2-Abb.1 - Operatorenaufbau
und close Basisoperatoren der Inputoperatoren
oder -dateien sind dies auch der verbleibend zu erstellende Operatorbaum und die grundlegenden
Hardwarebegebenheiten, die für die konkrete Anfragebearbeitung konstant bleiben und in den
sogenannten bindings gespeichert werden. Die Übergabe dieser Informationen erfolgt ebenfalls
über typlose Zeiger. Das heißt aber auch, dass die bindings nicht fest in den Operatoren definiert
sind. Hier wird die Trennung von Hardwarebegebenheiten und der Verarbeitungsmechanik als
Designziel verfolgt. State-records sind nur lokal definiert, d.h. werden für jeden Operator
speziell erstellt. Der grundlegende Operatoralgorithmus ist damit vielfach in einem
Operatorbaum aufrufbar, da er erst durch den passenden lokalen state-record und die SupportFunktionen sinnvoll definiert wird. Das Verfahren ähnelt dem Instanziieren eines Objektes in der
objektorientierten Programmierung. So können z.B. join, semi-join, outer-join, anti-join,
5
itersection, union, difference, anti-difference, aggregate und duplicate-elemination alle mit
einem einzigen Operatormodul umgesetzt werden, denn sie alle beruhen auf dem Vergleich
zweier Datenattribute aus zwei Listen (Dateien) von vergleichbaren Datensätzen (one-to-onematch). Sie unterscheiden sich lediglich in der Entscheidung, wie mit einer gefundenen
Übereinstimmung oder einem Unterschied umzugehen ist.7 Diese Entscheidung obliegt aber
einer Support-Funktion und wird dem Operator als Ausführungsargument mit übergeben.
Im folgenden Beispiel ist der Ablauf einer
Anfrageverarbeitung
SCAN
betrachteten
vollzogen.
PRINT
unter
den
bisher
Rahmenbedingungen
nach-
(Abbildung Abb-2)
Start
der
Anfrageverarbeitung ist der Aufruf des print-
JOIN
Operators. In Volcano ist dies ein einfacher
Filter-Operator. Dieser bekommt bindings
übergeben
SCAN
sowie
den
Zeiger
auf
den
Verwendungszweck des Operator, also ein
Aufrufweg
Datensatztransfer
Zeiger auf die Support-Funktion print. Ebenso
Kap-3.2.Abb-2 – Operatorbaum mit Streaming
wird
der
noch
zu
erstellende
restliche
Anfrageplan übergeben. All diese Informationen werden im status-record abgelegt. Der Aufruf
erfolgt über den Aufruf des (print)open-Basisoperators, der wiederum z.B. das Erstellen einer
Liste im Pufferspeicher veranlasst. Ebenfalls veranlasst (print)open die Erstellung des
Inputoperators, also eines Produzenten. In diesem Falle sollen der Output eines join gedruckt
werden. Daher ruft (print)open direkt einen (join)open-Basisoperator auf und übergibt wieder die
bindings sowie die Informationen über den noch aufzubauenden Operatorbaum. Danach ist der
print-Operator vollständig aufgebaut. Da nun im join-Operator der Basisoperator open
aufgerufen worden ist, beginnt dieser ebenfalls seine Verarbeitungsstrukturen im Speicher zu
erstellen. In Volcano wird ein join über einen One-to-one-match-Operatormodul erzeugt und
dieser Operator mit einem konkreten lokalen status-record aufgerufen. Soll z.B. als JoinVerfahren ein Hash-Join verwendet werden, so wird die entsprechende Support-Funktion bzw.
ein Zeiger auf die Einstiegsstelle mit übergeben, die einen solchen Join verwirklicht. Da der
join-Operator zwei Inputoperatoren benötigt, wird dieser Operator nun also entsprechend den
Anweisungen im verbleibenden Operatorbaum zwei Produzenten über den Aufruf der openBasisoperatoren aufrufen. In diesem Fall sind dies selective-scan-Operatoren. Diese können, da
nach ihnen keine weiteren Operatoren mehr aufgerufen werden, es sich also um eine
7 [3] (Graefe 1994, III.B2) )
6
gespeicherte Datei handelt, direkt über die Möglichkeit eines selective-scans auf Dateiebene
verwirklicht werden, wie in Kapitel 3.1 vorgestellt. Tatsächlich ist auch ein eigener Operator für
den selective-scan verfügbar, wenn z.B. über Zwischenergebnisse selektiert werden soll, die
nicht in einer Datei gespeichert sind. Da aber beide Strukturen dieselben Support-Funktionen
aufrufen können sind sie quasi identisch. Hier wird nochmals die eindeutige Trennung von
Verarbeitungsmechanik und Datenmanipulation deutlich. Der Aufruf des selective-scan auf der
Dateiebene verläuft dabei exakt nach dem selben Muster wie bei Operatoren, da auch die
Dateien über die Basisoperatoren open, next, und close verfügen. Daher kann der join-Operator
schlicht den open-Basisoperator aufrufen und die Datei wird entsprechend reagieren, ebenso wie
ein beliebiger anderer Operator. In diesem Fall damit, dass die Datei beginnt, ein erstes Cluster,
also eine gewisse Anzahl Speicherseiten, in den Speicher zu übertragen. Beim selective-scan
wird nun noch die Support-Funktion für das Auswahlprädikat ausgewertet. Dabei lässt Volcano
als Argument sowohl direkte Vergleiche in kompilierter Form (compiled) zu oder auch komplexe
Funktionen, deren Prädikate erst ausgewertet werden müssen (interpreted).8
Man sieht deutlich den rekursiven Verlauf, in dem der Operatorbaum von Operator zu
Operator aufgebaut wird. Wichtig ist dabei, zu beachten, dass es keinen kontrollierenden Prozess
gibt, der den Aufruf steuert, sondern das dies ausschließlich durch die open-Aufrufe der
konsumierenden an die produzierenden Operatoren geschieht. Das bedeutet auch, das jeder
Operator nur den gerade aktuellen Teil des Operatorbaums, der ihn selbst betrifft, interpretieren
muss. Oder anders gesagt, kein Operator kennt wirklich den ganzen Operatorbaum, im
Gegenteil, für die Operatoren ist es vollständig unerheblich, wofür ihr Output verwendet wird
und auch kennen sie nur den Zeiger auf den oder die aufgerufenen eigenen Inputoperatoren.
Dabei ist für sie der Aufbau und die Verfahrensweise ihrer Inputoperatoren ebenfalls völlig
unerheblich.
Alle open-Aufrufe enden mit dem Aufruf der next-Basisoperatoren auf den InputOperatoren. Da aber der Baum rekursiv erstellt wurde ist der erste next-Aufruf hier der auf einer
Datei, genauer der eines join-Operators auf einer Datei. Diese liefert nach Auswertung der
Bedingung als Antwort auf diesen next-Aufruf den Zeiger auf den RID des ersten
Ergebnisdatensatzes an den join-Operator. Ebenso bei der zweiten Datei. Dem join-Operator
liegt als Abschluss des open-Aufrufs des print-Operator ebenfalls ein next-Aufruf vor. Sobald
also der join-Operator ein Ergebnis weitergeben kann, wird dieser Aufruf bedient.9 Sobald ein
Operator einen Datensatz verarbeitet hat, ruft er erneut auf seinem Inputoperator den next8 [3] (Graefe 1994, III.A Seite 123)
9 Dabei wird ein join-Operator eine Datei im virtuellen Speicher anlegen und erst ein Cluster an
Ergebnisdatensätzen erstellen und dieses Cluster übergeben.
7
Basisoperator auf und fordert so den nächsten Datensatz an. Dies geschieht so lange, bis kein
Datensatz mehr vorhanden ist, der weitergegeben werden könnte und stattdessen ein End-ofStream vom Produzenten übergeben wird. Der Datensatzaufruf folgt also on-demand dann, wenn
weitere Daten im konsumierenden Operator verarbeitet werden können. Die Anfrageverarbeitung
endet schließlich, wenn alle Operatoren bis hin zum Wurzeloperator gemeldet haben, dass keine
weiteren Daten vorliegen. Dann ruft der Wurzeloperator, in diesem Falle der print-Operator, den
close-Basisoperator auf dem join-Operator auf. Auch dieser Aufruf wird rekursiv verarbeitet, so
dass sich zuerst die Dateien per close-Basisoperator schließen und die Speicher, bis hinauf zum
Wurzeloperator,
freigeben.
Mit
seiner Terminierung
endet
mittels
close
auch
die
Anfrageverarbeitung. Prinzipiell unterstützt das Iteratordesign in Volcano bis hinunter in das
Dateisystem also eine Datenverarbeitung in Streams, die mit sehr geringem Verwaltungsaufwand
möglich ist und eine schnelle Verarbeitung unterstützt.
4 Inter-Operator Parallelität
4.1 Der Exchange-Operator
Streng genommen haben wir es bei der Streamverarbeitung bereits mit einer Form von
Inter-Operator Parallelität zu tun, denn die Operatoren arbeiten „gleichzeitig“ in dem Sinne, das
noch kein vollständiges Ergebnis eines Operator vorliegt, wenn ein anderer bereits die Arbeit
aufnimmt. Allerdings findet diese Parallelität in einem einzigen Prozess statt, so dass die
Ausführung allein von den Möglichkeiten der Hardware zur Streamverarbeitung abhängt, also
außerhalb des Einflussbereichs von Volcano selbst ist. Im Folgenden wird dann auch Parallelität
als Prozessparallelität verstanden, die von der Verarbeitungsmechanik selbst erzeugt wird und
durch mehrere Ausführungsprozesse gekennzeichnet ist.
Bei der Einführung von Prozessparallelität soll nach Möglichkeit die Struktur der
Verarbeitungsmechanik, also die Selbstorganisation der Operatoren durch den rekursiven Aufruf
nicht angetastet und auch auf die Prozessorganisation übertragen werden. Eine übergeordnete
Struktur zu Verwaltung und Überwachung der Prozesse ist daher ungeeignet. Naheliegend ist
dann der in Volcano gewählte Ansatz, die Prozessverwaltung vollständig in einen neuen Operator
zu verlagern. Auf diese Weise fügt sich der Aufruf neuer Prozesse nahtlos in den Operatorbaum
ein und alle anderen Operatoren bleiben unabhängig von Einflüssen, die durch die parallelen
Prozesse entstehen. Sie können weiterhin ohne Rücksicht auf Parallelität entwickelt und dennoch
parallel ausgeführt werden. Genau das leistet der Exchange-Operator in Volcano.
Der Exchange-Operator beinhaltet keinerlei Anweisung zu Datenmanipulation oder
Interpretation. Seine Aufgabe ist es, neue Prozesse zur Verfügung zu stellen und zu schließen
8
sowie den Datentransfer zwischen den Prozessen zu gewährleisten. Dabei können dem
Exchange-Operator Argumente, wie z.B. der Grad der Parallelität, d.h. wie viele parallele
Prozesse eröffnet werden sollen, übergeben werden. Er ist auch als Iterator definiert, unterstützt
also ebenso die Basisoperatoren open, next und close und ist damit an einer beliebigen Stelle im
Operatorbaum einsetzbar.
4.2 Pipelining
Abbildung Abb-3 zeigt das Beispiel aus Kapitel 3 nachdem ein Exchange-Operator
eingefügt wurde.
SCAN
Port
PRINT
JOIN
EX-CH
SCAN
Aufrufweg
Datensatztransfer
Kap-4.2.Abb-3 – Operatorbaum mit zwei vertikal parallelen Prozessen (Pipelining)
Der Exchange-Operator wird vom print-Operator als Produzent über den (exchange)openBasisoperator aufgerufen. Darauf hin stellt er einen neuen Prozess zur Verfügung und richtet zum
Datenaustausch eine Datenstruktur im Speicher ein, den Port. Als Produzent ruft nun der
Exchange-Operator im neuen Prozess seinerseits den join-Operator wieder über (join)open auf
und führt den rekursiven Aufruf so fort. Im neuen Prozess werden also nun alle im
Operatorbaum tieferliegenden Operatoren aufgerufen. Um die Interaktion zwischen den
Prozessen zu verringern werden Datensätze, die als Output vom join-Operator übergeben
werden, im Port in Paketen gespeichert und übergeben. Die Paketgröße ist dabei aber flexibel
wählbar (1-32.000 Datensätze) und wird ebenfalls als Argument an den Exchange-Operator
übergeben. Der Exchange-Operator ist damit Bestandteil beider nun arbeitenden Prozesse. Zu
beachten ist, dass dies eine Verfahrensänderung der normalen Weitergabe von Daten ist, die
innerhalb eines Prozesses normalerweise über die Nachfrage (demand-driven) geregelt ist und
hier über die Verfügbarkeit der Daten (data-driven).10
Sollte der produzierende Prozess
10 Man beachte hier, dass die Weitergabe eines Pakets mit nur einem Datensatz dem aufrecht Erhalten des direkten
Streamings entspricht.
9
wesentlich schneller Datensätze zur Verfügung stellen als der konsumierende Prozess diese
anfragt, muss der Exchange-Operator immer mehr Pakete im Port hinterlegen. Das kann unter
Umständen den Speicher massiv belegen und die Systemleistung senken. Die Anzahl der im Port
hinterlegbaren Cluster ist daher ein weiteres Argument, das dem Exchange-Operator bei seiner
Erstellung übergeben wird. Da dieses Argument nur den lokalen Status eines Operators definiert,
sind innerhalb eines Operatorbaums beliebige Parametersetzungen für die verschiedenen
Exchange-Operatoren möglich. Die Verarbeitungsmechanik kann also erneut variabel von
höheren Softwareebenen angepasst werden. Die Kapselung bleibt erhalten.11
Diese einfache vertikale Parallelität wird auch Pipelining genannt und bezieht sich dabei auf den
Datentransfer zwischen Prozessen, der über die Übergabe von Clustern im Port durchgeführt
wird. Begrifflich zu trennen ist davon das Streaming, das ja innerhalb eines Prozesses stattfindet.
4.3 Teilbaum-Parallelität im Operatorbaum
Bei genauer Betrachtung fällt auf, das mit dieser Methode aber nicht nur vertikale
Parallelität sondern auch bereits horizontale Parallelität im Operatorbaum, die sog. TeilbaumParallelität (bushy-Parallelität), ohne weitere Anpassungen möglich ist. Abbildung Abb-4 zeigt
das Beispiel erweitert um zwei weitere Exchange-Operatoren, die jetzt parallel die selectivescans in einzelne Prozesse verlagern.
Port
SCAN
EX-CH
Port
PRINT
JOIN
EX-CH
Port
SCAN
Aufrufweg
Datensatztransfer
EX-CH
Kap-4.3. Abb-4 – Operatorbaum mit vertikal und horizontal parallelen Prozessen
Da sich die Operatoren eines Operatorbaums selbst verwaltend rekursiv aufrufen und der
Exchange-Operator diese Struktur vollständig mitträgt, ist horizontale Parallelität in Volcano
sehr leicht zu erreichen.
11 [2] (Greafe und Davidson 1993, VI, Seite775 ff.)
10
4.4 Verwaltungsaufwand
Der Exchange-Operator ist allein für alle Belange der Parallelität zuständig. Er regelt den
Datenaustausch zwischen den Prozessen, ist dabei aber durch seine Iteratorstruktur zu allen
anderen Operatoren kompatibel. Übergeordnete steuernde Prozesse sind nicht nötig. Auch
müssen die anderen Operatoren in keiner Weise für die parallele Ausführung vorbereitet werden.
Die parallele Ausführung ist hier sehr effizient gewährleistet. Über die Wahl der Paketgröße kann
eine Verzögerung durch den Wechsel von nachfrageorientiertem zu datengetriebenem Datenfluss
gering gehalten werden.
5 Intra-Operator Parallelität
5.1 Datenpartitionierung
Intra-Operator
Parallelität
bedeutet
zunächst,
dass ein
einziger
Operator im
Operatorbaum durch mehrere Prozesse verarbeitet wird. Wie in Kapitel 4 beschrieben, können
Prozesse in Volcano aber nur durch den Exchange-Operator bereit gestellt werden. Wie im bisher
betrachteten Beispiel können auch hier mehrere Operatoren innerhalb eines Prozesses verarbeitet
werden. Die Parallelität muss sich in diesem Falle also auf einen ganzen Teilbaum des
Operatorbaums beziehen, der für die Bearbeitung in einem Prozess vorgesehen war. Dabei ist
auch ein einzelner Operator parallel genau dann ausführbar, wenn sich der Operator in einem
P(0)
J(0)
S2(0)
P1
PRINT
P1
JOIN
SCAN
P2
J(1)
S2(1)
P2
JOIN
P1 P2
P3
S1(1)
S1(0)
SCAN
SCAN
SCAN
S1(2)
SCAN
Aufrufweg
Datensatztransfer
Kap-5.1. Abb-5 – Operatorbaum mit vertikal und horizontal parallelen Prozessen
Blatt des Operatorbaums befindet oder von zwei Exchange-Operatoren eingeschlossen wird.
11
Streng genommen müsste hier also von einer Intra-Teilbaum-Parallelität gesprochen werden.12
Der bisher beschriebene Exchange-Operator ist mit geringen Erweiterungen bereits in der Lage,
Intra-Teilbaum-Parallelität zu ermöglichen. Abbildung Abb-5 zeigt das bisherige Beispiel, wobei
die join-Operatoren auf partitionierten Dateien nun in parallelen Prozessen bearbeitet werden
sollen. Der Exchange-Operator bekommt als Argument den Grad der Parallelität mit z.B. 2
übergeben und wird den join-Operator in zwei getrennten Prozessen verarbeiten. Zu beachten ist,
dass der Exchange-Operator nicht einfach zwei Prozesse eröffnen kann, in denen der
Operatorbaum jeweils wie gewohnt weiter geöffnet wird, denn in diesem Falle würde der
Operatorbaum verändert. So würden die beiden Prozesse jeweils zwei Exchange-Operatoren
öffnen und darin die selective-scans ausführen. Um das zu vermeiden, eröffnet der ExchangeOperator zunächst nur einen einzigen neuen Prozess, z.B. J(0). Dieser wiederum eröffnet die
weiteren, zu sich selbst identischen, Prozesse als Unterprozesse zu sich selbst, hier z.B. J(1).
Damit fungiert J(0) als Master-Prozess in dieser Gruppe von intra-Teilbaum-parallelen Prozessen
und ist allein für den Aufruf der Produzentenprozesse zuständig. So bleibt der Operatorbaum
unverändert.
Dennoch
werden
alle intra-Teilbaum-parallelen
Unterprozesse mit
dem
übergeordneten Exchange-Operator verbunden, denn sie alle liefern gleichermaßen den Input für
den Exchange-Operator, der seinerseits keinerlei Datenmanipulation vornimmt und seinen Port
in die Partitionen P1 und P2 teilt, um die Daten aufzunehmen. Das bedeutet auch, das beide
Prozesse ein End-of-Stream Signal setzten müssen, bis der Exchange-Operator seinerseits dieses
Signal setzten darf. Die Anzahl der abzuwartenden Signale bestimmt dabei der Grad der
Parallelität.
Noch etwas komplizierter wird die Situation, falls, wie in diesem Beispiel, auch die
selective-scans auf partitionierten Daten aufgerufen und in mehreren Prozessen verarbeitet
werden. Nimmt man an, der nächste Exchange-Operator wird von dem Master-Join-Prozess, d.h.
dem Master-Prozess in der Gruppe der parallelen Join-Prozesse, der einen join-Operator
ausführt, mit einem Grad der Parallelität von 3 aufgerufen. Dann eröffnet dieser ExchangeOperator wiederum einen Prozess und darin den selective-scan, wobei er als Datei eine Partition
angibt. Dieser Selective-Scan-Prozess S1(0) übernimmt wiederum die Rolle des MasterProzesses und öffnet wieder intra-Teilbaum-parallele Unterprozesse S1(1) und S1(2), die
ihrerseits einen selective-scan über einer jeweils anderen Dateipartition ausführen.13 Würden nun
alle Prozesse mit dem selben Port im Exchange-Operator verbunden, um ihren Output
weiterzureichen, würde ein beträchtlicher Teil des Vorteils der parallelen Ausführung vergeudet,
12 [1] (Graefe 1990, 4.3 Seite 106) zeigt einen „BC“-Prozess, also einen Prozess aus den Operatoren B und C.
13 Der Index 1 bei den Prozessen S1(0)-S1(2) unterscheidet diese Scans von den anderen die ja über anderen Daten
ausgeführt werden nicht nur über verschiedenen Partitionen.
12
weil so sehr viele Zugriffe auf den selben Speicherbereich erfolgen müssten. Der ExchangeOperator ist daher in der Lage, für jeden Prozess eine Partition im Port anzulegen. Da in diesem
Falle auch mehrere parallele Prozesse die Daten verarbeiten können, wird dem nextBasisoperator, der von den übergeordneten Prozessen aufgerufen wird, ein Argument mit einer
Partitionsangabe mitgegeben. So können die übergeordneten Prozesse direkt mit einer Partition
des Port im Exchange-Operator verbunden werden. Anzumerken ist, das hier insgesamt 10 EndOf-Stream Signale gesetzt werden, denn jeder Prozess muss nun jeder Portpartition und damit
jedem übergeordneten Prozess mitteilen, dass keine Datensätze mehr vorliegen. Andernfalls
könnten Datensätze verloren gehen.14
6 Der erweiterte Exchange-Operator
6.1 Datenpartitionierung
Alle bisherigen Darstellung bezogen sich auf eine shared-memory Hardwarekonfiguration.
Daher war zu jedem Zeitpunkt die Übergabe von Speicheradressen ausreichend, um
Informationen von einem Operator zum nächsten und von einem Prozess zum nächsten zu
übergeben. Um größtmögliche Flexibilität bezüglich der Hardwarebegebenheiten zu erreichen,
soll Volcano aber auch in verteilt bzw. hierarchisch organisierten Speicherkonfigurationen
einzusetzen sein. Dies macht eine Erweiterung des Exchange-Operator notwendig. Im Folgenden
sei nun ein hierarchisches System beschrieben, bei dem in jedem Knoten ein shared-memory
System vorliegt und diese Knoten über ein Netzwerk verbunden sind. Abbildung Abb-6 zeigt das
bekannte Beispiel in einem solchen Umfeld, wobei einige Prozesse auf andere Knoten verteilt
wurden.15
Um den Datenaustausch zwischen den Knoten zu organisieren und dennoch die bisher erreichte
Unabhängigkeit zu erhalten ist eine Erweiterung des Exchange-Operators nötig. Bisher konnten
Daten von einem zum anderen Operator durch die Übergabe von Speicheradressen verwirklicht
werden. Zwischen Knoten funktioniert dies nicht. Daten müssen vollständig übertragen werden.
Für Datenpakete erzeugt der erweiterte Exchange-Operator auf dem Knoten des produzierenden
und des konsumierenden Prozesses jeweils einen Port zwischen denen der Datentransfer
innerhalb des Exchange-Operators stattfindet.
14 Der Übersicht halber sind die Portpartitionen in der Abbildung festen Operatoren und Prozessen zugeordnet. Da
aber die Wahl einer Portpartition ein Argument im next-Aufruf ist, ist nicht ausgeschlossen, das verschiedene
Konsumentenprozesse und -operatoren wahrend der Datenverarbeitung aus verschiedenen Partitionen Daten
beziehen können. Vgl. [1] (Graefe 1990, 4.3, Seite 106)
15 Die erste Ziffer in den Prozesskürzeln beschreibt den Knoten, auf dem der Prozess verarbeitet wird, z.b. J(1,0)
auf Knoten 1 als Master-Join-Prozess..
13
P(0,0)
PRINT
S2(2,0)
J(1,0)
P1
P1
JOIN
Knoten 0
P1
P1
P2
P2
J(1,1)
Aufrufw eg
Datensatztransfer
Netzw erkverbindung
Bindings Transfer
JOIN
Knoten 1
P1 P2
P3
P1 P2
P3
S1(3,0)
S1(3,1)
SCAN
SCAN
SCAN
S2(2,1)
SCAN
Knoten 2
S1(3,2)
SCAN
Knoten 3
Kap-6.1. Abb-6 – Operatorbaum mit vertikal und horizontal parallelen Prozessen auf verteilten Knoten
Neben den Daten gilt dies insbesondere auch für die Operatoren selbst, die bindings, die
Support-Funktionen und den verbleibenden Operatorbaum. Letztlich wird auf einem neuen
Knoten so eine autarke Anfrageauswertung gestartet, die über den erweiterten ExchangeOperator mit dem übergeordneten Operatorbaum verbunden wird. D.h. in jedem Knoten wird
durch den Exchange-Operator ein Prozess erstellt und in diesem alle notwendigen Daten wie z.B.
Operatoren, bindings und benötigte Support-Funktionen im tiefer liegenden Operatorbaum
übertragen. Dazu werden die Operatoren um drei Basisoperatoren erweitert, pack, unpack und
size. Dieser Prozess im entfernten Knoten dient dann als lokaler Master-Prozess für diesen
Operator auf dem entsprechenden Knoten. Der Vereinfachung wegen sei hier von einem
„Operator“ die Rede. Wie in Kapitel 5 dargelegt handelt es sich hierbei strenggenommen um
Teilbäume des Operatorbaums, die auch aus mehr als einem Operator bestehen können, die in
einem solchen Master-Prozess ausgeführt werden. Über ihn werden Kontrolldaten über das
Netzwerk ausgetauscht. Zu beachten ist, dass diese Netzwerkverwaltung von der tatsächlichen
Datenübergabe von einem produzierenden zu einem beliebigen konsumierenden Prozess, wie sie
der Operatorbaum vorgibt, unabhängig ist.
14
J(2,0)
P2
P(0,0)
PRINT
P2
P2
P1
JOIN
Knoten 2
J(1,0)
P1
P1
JOIN
P1
S2(2,0)
SCAN
P2
Knoten 0
S2(2,1)
SCAN
Knoten 1
P1 P2
P1 P2
P1
Aufrufweg
Datensatztransfer
Netzwerkverbindung
Bindings Transfer
P3
P3
S1(3,1)
S1(3,0)
SCAN
SCAN
S1(3,2)
SCAN
Knoten 3
Kap-6.1. Abb-7 – Operatorbaum mit vertikal und horizontal parallelen Prozessen auf verteilten Knoten
Komplexer wird die Situation, falls ein Operator parallel auf verschiedenen Knoten
ausgeführt wird. Dann gilt, dass jede Prozessgruppe neben einem globalen Master-Prozess auf
jedem Knoten einen lokalen Master-Prozess besitzt, die untereinander Kommunikationsnachrichten austauschen müssen. Allerdings gilt wieder, das jeder produzierende Prozess jedem
ihm im Operatorbaum direkt nachgestellten konsumierenden Prozess Daten senden kann.
Abbildung Abb-7 zeigt diese Situation.
6.2 Vektorbasierte Verarbeitung
Zur Verringerung der Datentransfers, vor allem zwischen einzelnen Knoten, kann es
vorteilhaft sein, Berechnungsergebnisse eines Teilbaums anderen Operatoren zur Verfügung zu
stellen, indem Ergebnis- oder Kontrollvektoren übergeben werden, z.B. beim bit-Vektor-Filtern. 16
Durch den iterativen Aufruf des Operatorbaums ist dies zunächst unmöglich, da die Ergebnisse
eines Teilbaums erst im verbindenden Operator mit denen eines anderen Teilbaums
zusammengebracht werden können. Durch einige kleine Veränderungen im Exchange-Operator
und dem Hinzufügen einer besonderen Support-Funktion ist auch diese Funktionalität
16 Ausführungen folgen [3] (Greafe und Davidson 1993, VII.C, Seite 759ff.)
15
herzustellen. Zunächst können Daten, wie z.B. ein Bit-Vektor nur über die Weitergabe in den
bindings baumabwärts transportiert werden. Da eine direkte Verbindung zwischen Teilbäumen
des Operatorbaums nicht herstellbar ist, muss ein solcher Vektor den Baum in Richtung Wurzel
weitergegeben werden, um an der Stelle der Verzweigung dann über die bindings in den anderen
Teilbaum übergeben werden zu können. Dabei fallen zwei Dinge sofort ins Auge. Erstens
müssen die bindings, die ja bisher als unveränderlich galten, verändert werden. Dazu ist eine
Support-Funktion nötig, die in Operator mit aufgerufen wird, der den bit-Vektor erstellt. Da die
bindings zwar innerhalb eines Knotens durch Zeiger übergeben werden, der Operator selbst aber
keinen Information darüber hat, ob nicht noch andere Knoten, die ja mit Kopien arbeiten würden,
im Operatorbaum beteiligt sind, müssen die bindings rekursiv baumaufwärts geändert werden.
Die einzige Möglichkeit, dies zu erreichen ist, die Übergabe als Parameter als Reaktion auf den
close-Aufruf zu übergeben. Dadurch ist zweitens klar, dass der entsprechende Teilbaum
vollständig bearbeitet werden muss, bevor die anderen Teilbäume überhaupt rekursiv aufgerufen
werden können. Der Exchange-Operator, der die Teilbäume also in verschiedene Prozesse leitet,
muss also einen veränderten open-Basisoperator haben, der erst den zweiten Teilbaum weiter
erstellt, wenn die Antwort des close-Aufrufs aus dem anderen Teilbaum mit den veränderten
bindings zurückerhalten wird. Graefe nennt dies einen synchronisierten-open-Operator.17 Es ist
klar, dass dies die Verarbeitung in Teilbaum-Parallelität unmöglich macht.
6.2 Verwaltungsaufwand
Da die grundsätzliche Struktur des rekursiven Operatoraufrufs erhalten geblieben ist,
entsteht weiterhin kein zusätzlicher Aufwand durch einen übergeordneten Kontrollprozess, wenn
verteilte Verarbeitungsknoten verwendet werden. Auch ist der Verwaltungsaufwand nicht
abhängig von der Menge der Datensätze, die zwischen Operatoren ausgetauscht werden sollen,
sondern nur von der Menge der beteiligten Verarbeitungsknoten und vom verwendeten
Netzwerkaufbau und -protokoll. Die grundsätzliche Verarbeitungsmechanik kommt damit
weiterhin mit geringem Verwaltungsaufwand aus.
7 Leistungsmerkmale und -fähigkeiten
7.1 Erweiterbarkeit, Parallelität, Unabhängigkeit, Skalierbarkeit
Volcano ist in hohem Maße erweiterbar. Neue Operatoren können im Umfeld
sequentieller Anfrageauswertung erstellt werden und problemlos in das Volcano aufgenommen
werden, solange das Iteratorprotokoll aus open, next und close eingehalten wird. Durch die
17 [3] (Greafe und Davidson 1993, VII.C, Seite 760ff.)
16
Kapselung der Datenmanipulation von der Verarbeitungsmechanik mittels der SupportFunktionen können problemlos neue Datentypen eingearbeitet werden, da dies nur einer neuen
Support-Funktion bedarf. Der Exchange-Operator ist in der Lage eine Vielzahl von Parallelitäten
in der Anfrageauswertung flexibel zu erzeugen und zu verwalten. Dateisystem und
Operatorbaum können ebenso flexibel auf verschiedene Hardwarekonfigurationen angepasst
werden. Dabei ist auch ein hohes Maß an Skalierbarkeit erreichbar. Ebenso sind die Operatoren
leicht auf besondere Bedingungen anzupassen, wie das Beispiel der bit-Vektor-Übertragung in
Kapitel 6.2 gezeigt hat.
7.2 Verwaltungsaufwand
Generell ist der Verwaltungsaufwand sehr gering, da Volcano ohne einen koordinierenden
Prozess und entsprechende Kommunikation auskommt. Das gilt besonders für einfache
Anfragen, die ohne Parallelisierung auskommen. Der Exchange-Operator hingegen bildet als
nicht datenverarbeitender Operator zunächst einen reinen Mehraufwand. Verglichen mit anderen
Systemen, die aber strikt jedem Operator einen eigenen Prozess zuordnen und diese Prozesse
dann koordinieren müssen, ist Volcano ebenfalls sehr effizient, da nicht jeder parallele Operator
in einem eigenen Prozess gestartet werden muss, sondern auch ganze Teilbäume des
Operatorbaum parallelisiert werden können. Auch der zusätzliche Aufwand für die
Netzwerkkommunikation bleibt überschaubar, da er nur von der Anzahl der involvierten
Knotenübergänge abhängt, also von der Anzahl der lokalen Master-Prozesse. Die große
Flexibilität in Volcano kann dabei allerdings auch zur Bremse werden, denn es sind durchaus
Operatorbäume denkbar, die ausgehend von der aufgezeigten Systematik, extrem viele
Exchange-Operator benötigen und so einen hohen Verwaltungsaufwand provozieren. Ein gut
informierter Anfrageoptimierer mit genauen Kosteninformationen über die Prozesserstellung und
die Netzwerkkommunikation ist daher essentiell, um eine effiziente Operatorbäume zu
generieren.
17
Literaturliste
[1] G.Graefe, „Encapsulation of Parallelism in the Volcano Query Processing System“ in
Proceedings of the 1990 ACM SIGMOD international conference on Management of data, Seite
102-111.
[2] G.Graefe, D.L. Davidson „Encapsulation of Parallelism and Architecture-Independence in
Extensible Database Query Execution“ in Journal IEEE Transactions on Software Engineering
Volume 19 Issue 8, August 1993, Seite 749-764.
[3] G.Graefe, „Volcano - An Extensible and Parallel Query Evaluation System“ in IEEE
Transactions on Knowledge an Data Engeneering, Vol 6. No.1, 1994: Seiten 120-135
18
FernUniversität in Hagen
Seminar 01912
im Sommersemester 2011
„MapReduce und Datenbanken“
Thema 3
MapReduce und Hadoop
Referentin: Noria Bellouch
Inhaltsverzeichnis
1 Einführung.........................................................................................................................................1
2 MapReduce........................................................................................................................................3
2.1.1 Map-Phase..........................................................................................................................3
2.1.2 Reduce-Phase.....................................................................................................................3
2.2 Ablaufübersicht..........................................................................................................................4
2.3 Fehlertoleranz............................................................................................................................6
2.3.1 Ausfall einer Worker-Instanz..............................................................................................6
2.3.2 Ausfall des Masters............................................................................................................7
3 Google File System...........................................................................................................................7
3.1 Architektur.................................................................................................................................7
3.2 Funktionsweise..........................................................................................................................8
3.2.1 Lesezugriffe........................................................................................................................8
3.2.2 Schreibzugriffe...................................................................................................................9
3.2.3 Metadaten und Masteroperationen...................................................................................10
3.3 Fehlertoleranz..........................................................................................................................12
3.4 Konsistenzmodell.....................................................................................................................15
4 Überblick Unterschied Hadoop – Google MapReduce...................................................................16
Literaturverzeichnis............................................................................................................................17
Abbildungsverzeichnis.......................................................................................................................18
1
Einführung
Immer mehr Anwendungen verarbeiten immer größer werdende Datenmengen. Dieses Phänomen kann das Unternehmen Google besonders gut beobachten. Um die Verarbeitung großer Datenmengen effizienter zu gestalten, hat Google unterschiedliche Verfahren entwickelt. Eines dieser Verfahren ist MapReduce. Das Verfahren sieht vor, dass Daten parallel auf tausenden von
Rechnern verarbeitet werden können. Hierbei stellen sich Herausforderungen bezüglich Parallelisierung, Load Balancing und Fehlertoleranz. Das MapReduce Framework bietet hierzu Lösungsansätze, so dass diese Probleme dem Entwickler weitestgehend verborgen bleiben.
Im Rahmen dieser Arbeit soll MapReduce mit dem Fokus auf dem Thema Fehlertoleranz vorgestellt werden.
MapReduce
MapReduce ist ein von Google entwickeltes und patentiertes Verfahren zur Verarbeitung von
sehr großen Datenbeständen in verteilten Umgebungen. Das Verfahren wurde in Anlehnung an
die Funktionen map() und reduce() aus der Welt funktionaler Programmiersprachen (z.B Lisp)
konzipiert [GOO11]. Dabei werden die Daten über zwei separate Phasen hinweg bearbeitet: Der
Map-Phase und der Reduce-Phase. In der Map-Phase werden die Ausgangsdaten auf mehreren
Rechnerinstanzen parallel eingelesen und verarbeitet. Die Zwischenergebnisse aus der Map-Phase fließen als Inputdaten in die Reduce Phase ein. Das Framework liefert die Infrastruktur und
übernimmt die Kontrolle über die Prozesse.
Google File System
Analog zum MapReduce Konzept von Google wurde das Google File System (GFS) entwickelt,
um den wachsenden Anforderung der Google Anwendungen bezüglich Datenverarbeitung Rechnung zu tragen. Google setzt beim Aufbau der eigenen Cluster auf Standard Hardware. Aus Erfahrung wurde bei der Entwicklung des GFS der Ausfall einzelner Clusterknoten als Regelfall
und nicht als Ausnahme angenommen. Fehlertoleranz gegenüber Ausfällen wurde damit eines
der wichtigsten Designziele für GFS. Ein weiteres Designziel war die Skalierbarkeit des Systems. Knoten sollten schnell und unkompliziert dem Cluster hinzugefügt werden können
[FIS10].
1
Hadoop
Bei Hadoop handelt es sich um eine OpenSource Java-Implementierung des von Google entwickelten MapReduce-Paradigmas. Mittlerweile ist Hadoop ein Top-Level Apache Software Foundation Projekt. Hadoop besteht dabei aus zwei Kernkomponenten:
•
ein verlässlicher Datenspeicher auf der Basis von Hadoop Distributed File System
(HDFS)
•
und dem MapReduce Konzept [CLO11]
Auf Unterschiede zwischen Hadoop und Google MapReduce wird in Kapitel 4 näher eingegangen.
Hadoop Distributed Filesystem
Das Hadoop Distributed Filesystem (HDFS) ist ein verteiltes Dateisystem und Bestandteil des
Apache Hadoop Core Projekt. HDFS ist stark fehlertolerant und wurde für den Einsatz auf Standard Hardware entwickelt. Fehleridentifizierung und schnelle Behebung dieser gehörte zu den
wichtigsten Designzielen.
Das HDFS wurde in Anlehnung an das Google File System (GFS) entwickelt. In den wichtigsten
Eigenschaften stimmt es mit den Ausführungen im von Google veröffentlichen Paper "The Google Filesystem [GGL03]" überein [FIS10]. Aus diesem Grund gehen wir im Rahmen dieser Arbeit vor allem auf das Google File System ein.
2
2
MapReduce
2.1.1 Map-Phase
Bei der Map-Funktion handelt es sich um vom Anwender individuell implementierte Tasks zur
Umwandlung von Inputdaten in intermediäre Daten. Das Framework gibt dabei lediglich die Signatur der Funktion vor:
map (k1, v1) → list(k2, v2)
Die Map-Funktion nimmt als Inputdaten ein Schlüsselwertpaar entgegen und erzeugt hieraus
eine Liste von Schlüsselwertpaaren (key/value). Aus einem Inputpaar können 0...n Outputpaare
generiert werden. Der Typ der Inputdaten muss nicht dem Typ der Outputdaten entsprechen.
[HAD11]. Die Outputdaten stellen Zwischenergebnisse dar, die wiederum als Inputdaten in die
Reduce-Phase einfließen.
Abbildung 1: Map-Phase [Eigene Darstellung]
2.1.2 Reduce-Phase
Der Anwender muss unter Berücksichtigung der Vorgaben des Frameworks eine Reduce-Funktion implementieren.
reduce (k2, list(v2)) → list(v2)
Als Input-Daten (Liste der Key-Value Paare) muss die die Reduce-Funktion die Outputdaten der
Map-Funktion verarbeiten. In der Reduce-Phase ruft das Framework die vom Anwender implementierte Reduce-Funktion für jedes Zwischenergebnis aus der Map Phase auf [DG04, Seite 2].
3
Dabei wird der Reduce Funktion ein Key und ein Set an Werten zu diesem Key übergeben. Ziel
der Funktion ist die Reduzierung (deshalb „reduce“) der Anzahl dieser Werte zu erreichen. Typischerweise erzeugt die Reduce-Funktion keinen oder einen Wert [DG04, Seite 2].
2.2 Ablaufübersicht
Im Rahmen der Map-Phase werden die zu verarbeitenden Daten in M Blöcke mit einer Größe
von 16-64 MB unterteilt und zur parallelen Verarbeitung auf unterschiedlichen Instanzen des
Rechner-Clusters den Map-Funktionen als Inputdaten übergeben. Auf den unterschiedlichen
Map-Instanzen wird jeweils die Map-Funktion ausgeführt. Anschließend ruft das Framework die
Reduce-Funktion zur weiteren Verarbeitung der Zwischenergebnisse aus der Map-Phase auf. Folgende Abbildung beschreibt diesen Ablauf ausführlicher:
Abbildung 2: Ablaufübersicht MapReduce [DG04, Seite 4]
4
Eine Instanz des Clusters ist der Master. Alle anderen fungieren als Worker. Der Master koordiniert den Ablauf sowie die teilnehmenden Worker-Instanzen.
1. Das Framework startet mehrere MapReduce Instanzen auf dem Rechnercluster.
2. Map-Tasks werden freien verfügbaren Worker-Instanzen zugewiesen. Es gibt entsprechend der Splittung der Inputdaten M Map-Tasks.
3. Erhält ein Worker einen Map-Task, so liest er die erhaltenen Inputdaten ein und übergibt
diese der vom Anwender implementierten Map-Funktion.
4. Die Ergebnisse der Map-Funktion werden im lokalen Hauptspeicher gehalten und regelmäßig lokal auf Platte geschrieben. Dabei werden die Daten so partitioniert, dass es R
Partitionen gibt und in jeder Partition die Werte für einen Key vorliegen. Dementsprechend gibt es R Reduce-Tasks, die jeweils die Daten einer Partition als Input für die Reduce-Funktion erhalten. Der Master erhält regelmäßig Informationen über den Speicherort der Zwischenergebnisse der Map-Tasks. Anschliessend werden Reduce-Tasks an verfügbare Worker-Instanzen vergeben. Hierzu übermittelt der Master den Speicherort der
Map-Zwischenergebnisse.
5. Der Reduce Worker greift über einen remote procedure call auf die lokale Festplatte des
Map-Workers und lädt die dort gespeicherten Map-Zwischenergebnisse herunter.
6. Der Reduce-Worker iteriert über alle Zwischenergebnisse und weisst alle Werte eines eindeutigen Keys der Reduce-Funktion als Inputdaten zu. Für jede Partition wird eine Ausgabedatei angelegt, in die die Ergebnisse der einzelnen Reduce-Funktion eingetragen
werden.
7. Nach Abschluss aller Map- und Reduce-Tasks weckt das Framework die aufrufende
Useranwendung auf. Die Anwendung erhält Zugriff auf die Ergebnisse und kann diese in
ihrem Programm verarbeiten [DG04, Seite 4].
Beispiel eines MapReduce Ablaufs
Eines der gängigsten Beispiele zur Illustration des MapReduce Ansatzes ist das Zählen von
Worthäufigkeiten innerhalb einer großen Datenmenge. In einem ersten Schritt muss die Daten5
menge in kleinere Einheiten gesplittet werden. Diese Dateneinheiten werden dann verteilt auf die
unterschiedlichen Worker-Instanzen und gehen dort als Inputdaten in die Map-Funktionen ein.
Entsprechend der Spezifikation des Frameworks sehen die Map-Funktion folgendermaßen aus:
Instanz 1: map(input1, „ein Satz mit Worten, Worten die ein“)
Instanz 2: map(input2, „ein anderer Satz mit noch mehr Worten“)
Instanz 3: map(input3, „alles hat ein Ende nur die“)
Auf allen Instanzen liegt die gleiche Implementierung der Map-Funktion vor. Es liegt in der Verantwortung des Anwenders die Daten innerhalb der Funktion zu verarbeiten. Unsere Beispielfunktion zählt die Häufigkeit eines Wortes und gibt als Ergebnis eine Liste mit Zwischenergebnissen aus, die entsprechend der Vorgaben des Frameworks folgende Outputdaten generiert:
Instanz1: [(ein,1), (Satz,1), (mit,1),(Worten,1), (Worten,1), (die,1), (ein, 1)]
Instanz2: [(ein,1),(anderer, 1),(Satz,1),(mit, 1)...]
Instanz3: [(alles, 1), (hat,1), (ein, 1),(Ende, 1), (nur, 1), (die, 1)]
Diese Outputdaten werden zu Zwischenergebnissen zusammengefasst, die als Inputdaten in die
Reduce-Phase eingehen. In der Reduce-Funktion wird dann die Häufigkeit eines Wortes über den
gesamten Datenbestand ermittelt.
reduce(ein, [(1,1),(1),(1)]) → 4
reduce(Satz, [(1),(1)]) → 2
2.3 Fehlertoleranz
Der MapReduce Ansatz wird herangezogen, um die Verarbeitung von grossen Datenmengen effizienter zu gestalten. Dabei werden Prozesse parallelisiert und auf eine Vielzahl (tausende) von
Rechnern verteilt. Mit wachsender Anzahl der involvierten Maschinen steigt die Anzahl der geplanten und ungeplanten Ausfälle. Der Ausfall wird damit zur Regel und dementsprechend im
MapReduce Ansatz von Google behandelt.
2.3.1 Ausfall einer Worker-Instanz
Der Master hat die Aufgabe Ausfälle von Worker-Instanzen zu identifizieren und Ausweichstrategien umzusetzen. Um ausgefallene Worker zu identifizieren, pingt der Master sämtliche Wor6
ker an. Antwortet der Worker nicht innerhalb einer bestimmten Zeit, gilt er als ausgefallen. Die
Ausweichstrategie des Masters sieht dann folgendes Vorgehen vor:
•
von ausgefallenen Workern abgeschlossene Map-Tasks werden zurückgesetzt
Die Ergebnisse der Map-Tasks werden lokal abgelegt und sind nach dem Ausfall nicht mehr verfügbar. Die Ergebnisse abgeschlossener Reduce Tasks liegen im globalen File System und müssen daher nicht wiederholt werden.
•
von ausgefallenen Workern begonnen Map- oder Reduce-Tasks werden zurückgesetzt
Diese können nun analog zu abgeschlossenen Map-Tasks von anderen Workern übernommen
werden [DG04, Seite 4].
2.3.2 Ausfall des Masters
Der Ausfall des Masters bedeutet den Ausfall des ganzen Systems, da die Prozesse zwischen
Map- und Reduce-Workern nicht mehr koordiniert werden. Damit ist der Master ein Single Point
of Failure.
Im MapReduce Ansatz von Google wird der Ausfall des Masters als eher unwahrscheinlich
betrachtet, da es es sich im Gegensatz zu den Worker Maschinen lediglich um eine einzige
Maschine handelt. Allerdings sieht das Framework die Möglichkeit vor, bei der Sicherungspunkte regelmäßig protokolliert werden. Bei einem Ausfall könnte eine neue Master-Instanz
anhand der protokollierten Sicherungspunkte neu gestartet werden [DG04, Seite 5].
3
Google File System
3.1 Architektur
Ein Google Filesystem Cluster besteht aus einem Master, vielen Chunkservern (Datenserver) und
wird in der Regel von vielen unterschiedlichen Clients genutzt. In der Regel werden für den Aufbau des Clusters Linux-Rechner eingesetzt.
Daten sind unterteilt in Chunks fester Grösse (64 MB). Jeder Chunk (Datenblock) verfügt über
7
eine 64 bit Chunk-Referenz (handle), über die der Chunk eindeutig identifiziert werden kann.
Die Chunkserver speichern Chunks als Linux Files auf der lokalen Festplatte. Jeder Chunk ist
zur Sicherheit auf mehreren (default = 3) Chunkservern repliziert. Der Master hält u.a Metadaten
über den aktuellen Ort der Chunks und über das Mapping von Files zu Chunks. Dabei setzt der
Master regelmäßig Heartbeat-Nachrichten an die Chunkserver ab, um deren Status abzufragen
und Anweisungen zu erteilen.
Abbildung 3: GFS Architektur [GGL03, Seite 3]
Clients, die das GFS nutzen kommunizieren mit dem Master und den Cunkservern, um Datenschreib- und Leseoperationen durchzuführen. Dabei werden mit dem Master lediglich Metadaten
(filename, chunk location etc.) ausgetauscht. Der tatsächliche Datenfluss (chunk data) erfolgt
ausschließlich direkt mit den Chunkservern [GGL03, Seite 30].
3.2 Funktionsweise
3.2.1 Lesezugriffe
Clients die das GFS nutzen greifen über die GFS API auf ihre Daten zu. Beim Lesezugriff durch
den Client sendet das Filesystem einen Request an den Master. Der Request enthält den Dateinamen und Informationen über den angeforderten Abschnitt innerhalb der Datei. Der Master ermittelt die Handle der betroffenen Chunks sowie sämtliche Chunkserver, auf denen die Chunks
8
repliziert sind. Der Client erhält vom Master die ermittelten Metadaten (Chunk-Handle, Liste aller betroffenen Chunkserver). Der Client wählt unter Berücksichtigung der eigenen Entfernung
zu den Chunkservern einen geeigneten Server aus und fordert bei diesem die Daten an. Ergeben
sich Probleme (korrupte Daten, Verfügbarkeit des Chunkservers) berichtet dies der Client dem
Master und fragt die Daten bei einem der anderen Chunkserver aus der Liste an.
Abbildung 4: Leseoperation GFS [PAS07, Seite 5]
3.2.2 Schreibzugriffe
Beim Schreibzugriff durch den Client sendet die API einen Request an den Master. Der Request
enthält den Dateinamen und Informationen über den betroffenen Abschnitt innerhalb der Datei.
Da beim Schreibzugriff Daten verändert werden, muss der Master den Vorgang mit anderen Modifikationen auf dem gleichen Chunk synchronisieren. Hierzu ernennt der Master einen Chunkserver zum primären Replikationsserver. Der primäre Replikationsserver erhält einen chunk lease. Dabei handelt es sich um das Recht, die Reihenfolge der Änderungen auf einem Chunk zu
bestimmen und diese Reihenfolge den anderen Chunkservern vorzugeben. Dieser Mechanismus
dient der Konsistenzsicherung der Daten.
Der Master übermittelt an den Client den Handle des Chunks, die Adresse des primären Replikationsservers sowie die aller weiteren Chunksever, auf denen der Chunk repliziert ist. Im nächsten
Schritt propagiert der Client die Daten an einen der betroffenen Chunkserver. Diese werden von
einem Chunkserver zum anderen übertragen.
9
Bisher ist noch keine Modifikation der Daten erfolgt. Erst wenn alle Chunkserver den Erhalt der
Daten bestätigen, setzt der Client einen Request mit einer Schreibaufforderung an den primären
Replikationsserver ab. Es kann mehrere simultane Modifikationsanfragen von Clients zum gleichen Chunk geben. Der primäre Chunkserver erstellt hierzu einen Ausführungsplan. Der Ausführungsplan wird dann auf die erhaltenen Daten angewendet. Anschließend übermittelt der primäre
Replikationsserver den Request mit der Schreibanweisung sowie dem Ausführungsplan an alle
weiteren betroffenen Chunkserver. Diese führen dann ebenfalls die Modifikationen entsprechend
des Ausführungsplans durch. Bei jeder Modifikation eines Chunks erhöht sich seine Versionsnummer. Anhand der Versionsnummer kann der Master veraltete Chunks identifizieren und ggf.
eine Neureplikation veranlassen. Nach Abschuss der Modifikation auf allen Chunkservern wird
dem Client der Erfolg kommuniziert.
Abbildung 5: Schreiboperation GFS [PAS07, Seite 6]
3.2.3 Metadaten und Masteroperationen
Metadaten
Der Master administriert drei Typen von Metadaten, die alle im Arbeitsspeicher des Masters gehalten werden:
•
Namespace des Files und des Chunks
10
•
File-to-Chunk Mappings (Verknüpfung zwischen Files und Chunks)
•
Adresse jeder Replikation eines Chunks
Metadaten über Namespaces und File-to-Chunk Mappings werden im Operation Log lokal persistiert und auf andere Rechner repliziert. Im Operation Log werden kritische Metadaten-Änderungen historisiert. Hierbei handelt es sich um eine zentrale Komponente des Google File Systems. Im Operation Log erfolgt die einzige Persistierung der Metadaten. Darüber hinaus fungiert
der Operation Log als logische Zeitachse, anhand derer die Abfolge konkurrierender Modifikationen nachvollzogen werden kann. Dies gilt für Files und Chunks sowie deren Versionierung.
Master Operationen
Ein Google File System Cluster besteht in der Regel aus hunderten von Chunkservern. Verteilt
über mehrere Maschinen und Server-Racks. Die Chunkserver werden wiederum von hunderten
von Clients rackübergreifend aufgerufen. Diese Art der Multi-Level Verteilung stellt eine große
Herausforderung bezüglich Zuverlässigkeit und Verfügbarkeit.
•
Verteilungsstrategie bei Replikation
Es reicht nicht die Daten über mehrere physische Rechner zu replizieren. Diese Strategie würde
lediglich vor dem Ausfall einer Festplatte oder eines Rechners schützen. Bei einem Stromausfall
oder Netzwerkproblem ist die Verfügbarkeit eines Chunks, der zwar auf mehreren Rechnern aber
auf nur einem vom Ausfall betroffenen Rack repliziert ist, nicht mehr gegeben. Aus diesem
Grund werden Chunks über mehrere Racks hinweg repliziert.
•
Chunk Replikation
Der Master repliziert einen Chunk immer dann, wenn die Anzahl der vorhandenen Replikas eines Chunks unterhalb einer vorgegeben Grenze fällt. Hierfür kann es unterschiedliche Gründe
geben:
– Ein Chunkserver ist nicht länger verfügbar
– Ein Chunkserver meldet korrupte Daten für den angeforderten Chunk
– Eine Festplatte des Chunkservers ist aufgrund von Problemen deaktiviert
11
– Die vorgegebene Grenze für die Anzahl der vorhandenen Chunkserver wurde erhöht
Muss ein neues Replikat eines Chunks erzeugt werden, wird diese Aufgabe vom Master priorisiert. Je grösser die Anzahl der fehlenden Replikas eines Chunks ist, desto höher ist die zugewiesene Priorität. Chunks von aktiven Files werden gegenüber denen von gelöschten Files höher
priorisiert. Die Priorität von Chunks, die Client Prozesse blockieren wird ebenfalls sehr hoch einstuft [GGL03, Seite 36].
3.3 Fehlertoleranz
Google setzt beim Aufbau großer Rechnercluster auf günstige Standard Hardware, so dass mit
wachsendem Cluster, der Ausfall einzelner Komponenten nicht die Ausnahme sondern die Regel
darstellt. Fehlertoleranz war damit einer der wichtigsten Herausforderung bei der Gestaltung des
Google File Systems. Folgende Ausfallursachen können unterschieden werden:
•
Ausfall einer Festplatte
•
Ausfall einer Maschine
•
Ausfall von Ressourcen (Strom, Netzwerk)
•
Missglückte Kommunikation (z.B. ein Chunkserver erhält Updatenachricht nicht)
Ausfälle von Komponenten führen dazu, dass Daten nicht verfügbar sind oder schlimmer korrupt
werden. Das Google File System berücksichtigt die Unvermeidbarkeit von Ausfällen und hat
unterschiedliche Mechanismen zur Gewährleistung von Hochverfügbarkeit und Datenintegrität
umgesetzt.
Heartbeat
Jeder Chunkserver setzt in definierten Abständen Hearbeat-Nachrichten an den Master ab. Die
Heartbeat Nachrichten dienen dazu, einen regelmäßigen Nachrichtenaustausch zwischen Master
und Chunkserver aufrecht zu erhalten. Der Chunkserver informiert den Master über die von ihm
gehaltenen Chunks. Als Antwort sendet der Master eine Liste der Chunks, die nicht länger in seinen Metadaten repräsentiert sind. Der Chunkserver kann diese Chunks daraufhin löschen. Vor
dem Hintergrund der Häufigkeit von Hardwareausfällen in grossen Clustern, nutzt der Master die
12
Heartbeat-Funktion zur Prüfung der Verfügbarkeit von einzelnen Chunkservern.
Bleibt die Heartbeat-Nachricht eines Chunkservers für einen vorgegeben Zeitraum aus, wird dieser als „tot“ deklariert. Der Master setzt eine Nachricht an die anderen Chunkserver, die über Replikate des betroffenen Chunks verfügen, ab. Inhalt der Nachricht ist die Anweisung sich zu replizieren [GGL03, Seite 37].
Chunk Replikation
Der Master erstellt Klone von jedem Chunk, die auf unterschiedlichen Chunkservern und Racks
verteilt werden. Die Standardkonfiguration sieht vor, jeden Chunk dreifach zu replizieren und
entsprechend der Verteilungsstrategie auf unterschiedlichen Chunkservern zu platzieren. Bei einem Ausfall eines Chunkservers geht nicht eine ganze Datei verloren, sondern lediglich die
Chunks der Datei, die sich auf dem betroffenen Chunkserver befunden haben. Zur Rekonstruktion der Datei greift der Master auf die Replikate des Chunks auf anderen Servern zu. Die Verteilungsstrategie gibt vor, dass die einzelnen Chunks rackübergreifend repliziert werden. Dadurch
ist sichergestellt, dass selbst beim Ausfall eines kompletten Racks ein Replikat immer auf einem
anderen Rack noch verfügbar ist [GGL03, Seite 37].
Verfügbarkeit des Masters
Bei dem Thema Verfügbarkeit ist der Master als Single Point of Failure ein wichtiger Faktor.
Sämtliche Anwendungen (z.B MapReduce oder Hadoop), die direkt das Verteilte Filesystem
(GFS oder HDFS) nutzen, sind durch die Verfügbarkeit des Masters limitiert [COL11]. Im
folgenden werden die wichtigsten Mechanismen zur Verfügbarkeit des Masters beschrieben:
•
Replikation des Operation Logs
Der Master wird nicht repliziert. Im ganzen System gibt es nur eine einzige Master-Instanz. Dies
macht den Master zu einem „Single Point of Failure“. Der Master speichert die Metadaten in seinem lokalen Filesystem. Beim Start des Masters werden sämtliche Metadaten in den Arbeitsspeicher geladen. Sämtliche Modifikationen der Metadaten werden zum einen lokal persistiert und
zum anderen in den Operation Log geschrieben. Dieser wird auf viele Rechner repliziert. Eine
Änderung gilt erst dann als abgeschlossen, wenn die Änderung auf sämtlichen Operation Log Instanzen ausgeführt wurde. Bei einem Ausfall des Masters kann dieser unverzüglich wieder gest-
13
artet werden und den Zustand vor dem Ausfall mittels des Operation Logs wieder herstellen.
Fällt der Master komplett aus, wird eine neue Master-Instanz erzeugt. Diese kann anhand einer
Replikation des Operation Logs den aktuellen Stand des Systems erlangen. Die Clients kennen
lediglich den DNS-Alias Namen des Masters, so dass der „Umzug“ des Masters für die Clients
transparent erfolgt.
•
Schadow Masters
Ein weiterer Mechanismus, der bei einem Ausfall greift, sind die sogenannten „shadow master“.
Shadow Master ermöglichen den Clients Leseoperationen auf das Filesystem, selbst wenn der
primäre Master ausgefallen ist. Dabei wird ein Replikat des Operation Logs vom Shadow Master
eingelesen und auf den eigenen Meta Daten die gleichen Operationen ausgeführt wie der primäre
Master zuvor. Dies erfolgt mit einer kleinen Zeitverzögerung. Bei einem Ausfall können dem
Shadow Master die letzten Änderungen fehlen. Für viele Applikationen ist das unkritisch. Vor allem, da sich die eigentlichen Daten auf den Chunkservern befinden. D.h unter Umständen sind
zwar die Metadaten, die dem Shadow Server vorliegen veraltet, die Daten, die den Applikationen
für Leseoperationen bereit gestellt werden aber nicht unbedingt [GGL03, Seite 37].
Datenintegrität
Jeder Chunkserver generiert Checksummen, um Datenkorruption der von ihm gespeicherten Daten identifizieren zu können. Die Spezifikation des Google File Systems garantiert nicht, dass
sämtliche Instanzen eines Chunks ständig identisch sind. Das Vergleichen sämtlicher Chunks, um
sicherzustellen, dass ein Chunk nicht korrupt ist, ist daher nicht möglich. Darüber hinaus kann
der Fall eintreten, dass mehrere Instanzen zu einem Zeitpunkt korrupt sind. Das Google File System sieht daher vor, dass jeder Chunkserver selbst verantwortlich ist, auf Datenkorruption zu
prüfen. Jeder Chunk ist ist in 64 KB Blöcke unterteilt. Jeder Datenblock verfügt über eine 32 bit
Checksumme. Die Checksumme wird als Meta Datum im Arbeitsspeicher gehalten und mit dem
Logging persistiert. Bei Lesezugriffen prüft der Chunkserver die Checksumme. Korrupte Daten
werden nicht an Clients oder andere Chunkserver propagiert. Der Master erhält eine Nachricht
über den Fehler, stößt eine Rereplikation (Chunk mit korrekten Daten wird an Chunkserver übermittelt) an und fordert den betroffenen Chunkserver auf, den korrupten Chunk zu entfernen
[GGL03, Seite 38]
14
3.4 Konsistenzmodell
Das Konsistenzmodell des Google File Systems wird als einfach und effizient beschrieben und
ist auf Anforderungen stark verteilter und zu Ausfällen neigender Cluster ausgelegt. Im folgenden werden die Zusagen beschrieben, die das Google File System bezüglich Konsistenz macht.
Änderungen an Namespaces (Erzeugung neuer Chunks) sind atomar und werden ausschließlich
durch den Master durchgeführt. Der Zustand einer Dateiregion nach einer Mutation hängt von
der Art und dem Erfolg der Mutation ab:
•
Eine Dateiregion ist konsistent, wenn alle Clients nach einer Mutation auf allen Kopien
dieselben Daten sehen.
•
Eine Dateiregion gilt als defined, wenn sie konsistent ist und die Clients das Ergebnis einer einzelnen Datenmutation in Gänze sehen können (d.h. es liegt keine konkurrierende
Mutation vor, bei der dem Client lediglich das Ergebnis aus allen Mutation ersichtlich ist)
•
Tritt bei einer Mutation ein Fehlerfall ein, ist die Dateiregion inkonsistent. Unterschiedliche Clients können unterschiedliche Daten erhalten.
Veraltete Replikate (Versionsnummer nicht aktuell) werden bei Mutationen nicht berücksichtigt.
Der Master gibt deren Adresse nie an Clients weiter. Veraltete Replikate werden von der Garbage
Collection beim nächst möglichen Zeitpunkt entfernt. Da Clients die Adresse von Chunks jedoch
cachen, ist es durchaus möglich, dass Clients die Daten von veralteten Replika anfragen, bevor
diese entfernt werden konnten. Das Zeitfenster, in dem der Client veraltete Daten erhält ist beschränkt durch den Time out des Cache Eintrags und durch das Öffnen des betroffenen Files (Cache ist gezwungen sämtliche Chunk-Informationen des Files zu löschen).
15
4
Überblick Unterschied Hadoop – Google MapReduce
Zunächst gilt es zu unterscheiden zwischen dem MapReduce Konzept, das von Google entwickelt und veröffentlicht wurde, und Googles eigener Implementierung des Konzepts „Google
MapReduce“. Hadoop und Google MapReduce implementieren beide das MapReduce Konzept.
Der wichtigste Unterschied zwischen Google MapReduce und Hadoop ist, dass es sich bei Hadoop um ein Open Source Projekt handelt, das von jedem sowohl genutzt als auch modifiziert
werden kann. Weitere Unterschiede sind:
•
Die MapReduce Implementierung von Google wurde für den internen Gebrauch in C++
geschrieben
•
Hadoop ist in Java implementiert
•
Hadoop umfasst sowohl die MapReduce-Implementierung als auch das Hadoop Distributed Filesystem
Weitere Unterschiede finden sich in den verteilten Dateisystemen HDFS und GFS
•
Architektur
Die Architektur von HDFS und GFS ist vom Aufbau nahezu gleich. Unterschiede finden sich in
den Bezeichnungen der einzelnen Komponenten:
•
Hadoop Filesystem
Google Filesystem
NameNode
Master
DataNode
Chunkserver
Block
Chunk
Appending-writes Operationen
Bei der Entwicklung von HDFS setzt man auf den Ansatz write-once-read-many. Dabei wird davon ausgegangen, dass Dateien, die einmal erstellt und beschrieben werden, nicht mehr verändert
werden müssen. Dies gilt vor allem für MapReduce Applikationen oder bspw. web crawler Applikationen. Appending-writes Operationen, wie sie das Google Filesystem anbietet, werden von
HDFS noch nicht unterstützt [BOR11, Seite 3].
16
Literaturverzeichnis
[BOR11] Borthakur, Dhruba: HDFS Architecture. Web Publikation. http://hadoop.apache.org/common/docs/r0.20.1/hdfs_design.pdf. Stand 21.06.2011
[CLO11] Cloudera: What is Hadoop. Web Publikation. http://www.cloudera.com/what-is-hadoop/.
Stand 07.06.2011
[COL11] Collins, Eli: Hadoop Availability. Web Publikation.
http://www.cloudera.com/blog/2011/02/hadoop-availability/. Stand 08.06.2011
[DG04] Dean,J.; Ghemawat,S.: MapReduce: Simplified Data Processing on Large Clusters. In:
Proceedings of Operating Systems Design and Implementation (OSDI). San Francisco, CA, 2004,
S. 137 – 150
[FIS10] Fischer, Oliver: Verarbeiten großer verteilter Datenmengen mit Hadoop. Web Publikation.
http://www.heise.de/developer/artikel/Hadoop-Distributed-File-System-964808.html, 2010. Stand
11.06.2011
[GGL03] Ghemawat, S.; Gobioff, H.; Leung, S.-T.: The GoogleFileSystem. In: 19th Symposium on
Operating Systems Principles. LakeGeorge, NewYork, 2003, S.29–43
[GOO11] Google. Web Publikation. http://code.google.com/intl/de-DE/edu/parallel/mapreduce-tutorial.html. Stand 07.06.2011
[HAD11] Hadoop. Web Publikation. http://hadoop.apache.org. Stand 08.06.2011
[PAS07] Passing, Johannes: The Google File System and its application in MapReduce. Web Publication. http://int3.de/res/GfsMapReduce/GfsMapReducePaper.pdf. Stand 29.05.2011
Abbildungsverzeichnis
Abbildung 1: Map-Phase [Eigene Darstellung]...................................................................................3
Abbildung 2: Ablaufübersicht MapReduce [DG04, Seite 4]................................................................4
Abbildung 3: GFS Architektur [GGL03, Seite 3]................................................................................8
Abbildung 4: Leseoperation GFS [PAS07, Seite 5].............................................................................9
Abbildung 5: Schreiboperation GFS [PAS07, Seite 6].......................................................................10
HadoopDB
a major step towards a dead end
Thema 5
Seminar 01912
Sommersemester 2011
Referent: Thomas Koch
June 23, 2011
1
Contents
1 HadoopDB
1.1 Scope and Motivation . . . . . . . . . . .
1.2 Architecture . . . . . . . . . . . . . . . .
1.3 Benchmarks . . . . . . . . . . . . . . . .
1.3.1 Data Sets . . . . . . . . . . . . .
1.3.2 Grep Task . . . . . . . . . . . . .
1.3.3 Selection Task . . . . . . . . . . .
1.3.4 Aggregation Tasks . . . . . . . .
1.3.5 Join Task . . . . . . . . . . . . .
1.3.6 UDF Aggregation Task . . . . . .
1.3.7 Fault tolerance and heterogeneous
1.4 Summary and Discussion . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
3
3
5
6
7
8
8
9
9
10
10
10
2 Hive
2.1 Data Model . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.2 Query Language . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.3 Architecture . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
11
11
12
13
2
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
environment
. . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
This paper presents and discusses two texts about HadoopDB[ABPA+ 09] and the Apache
Hive[TSJ+ 09] project which is used by the former.
1 HadoopDB
1.1 Scope and Motivation
The HadoopDB project aims to combine the scalability advantages of MapReduce with
the performance and efficiency advantages of parallel databases. Parallel databases in this
context are defined as “analytical DBMS systems [sic] that deploy on a shared-nothing
architecture”. Current parallel databases are usually scaled only into the tens of nodes. It
may be impossible to scale them into hundreds or thousand nodes like MapReduce for at
least three reasons:
1. Failures become frequent at this scale but the systems don’t handle them well.
2. A homogeneous cluster of machines is assumed which is impossible to provide at higher
scale.
3. The systems has not yet been tested at this scale.
MapReduce on the other hand is known to scale well into thousands of nodes. However,
according to work done by Stonebraker and others, MapReduce would neither be suitable
for analytical work[DD08] nor perform well[PPR+ 09].
A further advantage of HadoopDB should be low cost. This could be achieved by building
on existing open source solutions: The MapReduce implementation Apache Hadoop, Apache
Hive and the RDBMS PostgreSQL. It may however be argued whether these components
could be “used without cost” as claimed. The production use of software like Hadoop and
PostgreSQL still requires highly qualified and payed personal.
Desired Properties The following list presents the desired properties of the HadoopDB
project together with first comments. It is argued that neither MapReduce nor parallel
databases would provide all of these properties.
• Performance: It is claimed, that performance differences could “make a big difference
in the amount, quality, and depth of analysis a system can do”. Also performance
could significantly reduce the cost of an analysis if fewer hardware ressources were
used. However Stonebraker and Abadi concluded already in 2007 that the primary
factor for costs has shifted from hardware to personal.[SMA+ 07, 2.5 No Knobs] It
should also be considered that the costs for licenses of proprietary database systems
often outrun hardware costs by orders of magnitude. Finally it is not at all certain,
that in practice performance differences are a limitation factor for the range of possible
computations.
The paper suggests that current practice in analytical systems would be to load data
in a specialized database and to make calculations while an operator is waiting for the
results. In MapReduce systems however calculations are directly done on the original
data and could therefor run continuously during the normal operation of the system.
The performance of calculations may therefor not be a practical problem at all.
• Fault Tolerance: The probability, that at least one cluster node fails during a computation may be negligible for less then 100 nodes. With growing data size however the
cluster needs to grow. Therefor the probability of a single node failure grows as well
3
as the duration of algorithms. This continues until it’s not possible anymore to finish
calculations without a single node failing.
A system that restarts the whole computation on a single node failure, as typical in
RDBMSs, may never be able to complete on large enough data sizes.
• Ability to run in a heterogeneous environment: Most users of a large cluster would
not be able to provide a large number of totally identical computer nodes and to keep
there configuration in sync. But even then the performance of components would
degrade at different pace and the cluster would become heterogeneous over time. In
such an environment work must be distributed dynamically and take node capacity
into account. Otherwise the performance could become limited by the slowest nodes.
• Flexible query interface: The system should support SQL to integrate with existing
(business intelligence) tools. Ideally it also supports user defined functions (UDF) and
manages their parallel execution.
The above list does not include energy efficiency as a desired property although it is one of the
most important subjects in recent discussions about data centers.1 It has also been argued,
that standard performance benchmarks should be enhanced to measure the performance
relative to energy consumption.[FHH09]
Section 4 describes the parallel databases and MapReduce in further detail. From the
desired properties the former would score well on performance and the flexible interface and
the later on scalability and the ability to run in a heterogeneous environment. A paper
of Stonebraker, Abadi and others[PPR+ 09] is cited for the claim of MapReduces lack of
performance. This paper is cited over twenty times in the full HadoopDB text and is also
the source for the benchmarks later discussed. Other evidence for a lack of performance of
MapReduce is not provided.
It is also recognized that MapReduce indeed has a flexible query interface in that its
default usage requires the user to provide map and reduce functions. Furthermore the Hive
project provides a SQL like interface to Hadoop. Thus although it isn’t explicitly said it
could be concluded that MapReduce also provides a flexible query interface.
It can be summed up at this point, that MapReduce is excellent in two of four desired
properties, provides a third one and only lacks in some degree in performance. Hadoop is
still a very young project and will certainly improve its performance in the future. It may be
asked, whether a project like HadoopDB is still necessary, if Hadoop itself already provides
nearly all desired properties.
It is interesting, how the great benefit of not requiring a schema up-front is turned into
a disadvantage against MapReduce:[ABPA+ 09, 4.2]
By not requiring the user to first model and load data before processing, many
of the performance enhancing tools listed above that are used by database systems are not possible. Traditional business data analytical processing, that have
standard reports and many repeated queries, is particularly, poorly suited for the
one-time query processing model of MapReduce.
Not requiring to first model and load data provides a lot of flexibility and saves a lot of
valuable engineering time. This benefit should be carefully evaluated and not just traded
against performance. The quote also reveals another possible strategy to scale analytical
processing: Instead of blaming MapReduce for its favor of one-time query processing, the
analytical software could be rewritten to not repeat queries. It’ll be explained later that
Hive provides facilities to do exactly that by allowing the reuse of SELECT statements.
1
see Googles Efficient Data Center Summits http://www.google.com/corporate/datacenter/summit.
html (2011-06-19) and Facebooks open compute project http://opencompute.org (2011-06-19)
4
Figure 1: The Architecture of HadoopDB[ABPA+ 09]
1.2 Architecture
Hive is a tool that provides a SQL interface to MapReduce. It translates SQL queries into
apropriate combinations of map and reduce jobs and runs those over tabular data stored in
HDFS. HadoopDB extends Hive in that HDFS is replaced by node local relational databases.
Data Loader HadoopDB comes with two executable java classes and a python script2
necessary to partition and load the data into HadoopDB. The data loading is a cumbersome
process with several manual steps. Most of the steps must be executed in parallel on all
nodes by some means of parallel ssh execution. This leaves a lot of room for failures. It
is also necessary to specify the number of nodes in advance. A later change in the number
of nodes requires to restart the data load process from scratch. It is also not possible to
incrementally add data.
The following list describes the necessary steps to load data into HadoopDB. At each
step it is indicated how often the full data set is read (r) and written (w). It is assumed, that
the data already exists in tabular format in HDFS and that HDFS is used for this process
with a replication factor of 1. This means that in case of a hard drive failure during the
import process the process would need to be started again from the beginning. A higher
replication factor would lead to even higher read and write numbers in the following list.
1. Global Hasher: Partition the data set into as many parts as there are nodes in the
cluster. (2r, 2w + 1 network copy)3
2
3
http://hadoopdb.sourceforge.net/guide (2011-06-20)
The result of the map phase is written to disk and from there picked up by the reduce phase.
5
2. Export the data from HDFS into each nodes local file system. (1r, 1w)4
3. Local Hasher: Partition the data into smaller chunks. (1r, 1w)
4. Import into local databases. (1r, 1w + secondary indices writes)
We can see, that before any practical work has been done, HadoopDB implies five full
table scans and writes. In the same time at least 2.5 map and 2.5 reduce jobs could have
completed. If we assume that the results of typical map and reduce jobs are usually much
smaller then the input, even more work could have been done. There are indications that it
may be possible to optimize the data loading process to lower the number of necessary reads
and writes. It will be discussed in the benchmark section, why this doesn’t seem to be of
high priority for the HadoopDB authors.
Neither the HadoopDB paper nor the guide on sourceforge make it totally clear what
the purpose of the Local Hasher is. It is suggested that a partitioning in chunks of 1GB is
necessary to account for practical limitations of PostgreSQL when dealing with larger files.5
SQL to MapReduce to SQL (SMS) Planner The heart of HadoopDB is the SMS planner.
Hive translates SQL like queries into directed-acyclic graphs (DAGs) of “relational operators
(such as filter, select (project), join, aggregation)”. These operator DAGs (the query plan)
are then translated to a collection of Map and Reduce jobs and sent to Hadoop for execution.
In general before each JOIN or GROUP BY operation data must be shared between nodes.
The HadoopDB project hooks into Hive just before the operations DAG gets sent to
Hadoop MapReduce.(Figure 1) The planner walks up the DAG and transforms “all operators
until the first repartitioning operator with a partitioning key different from the database’s
key” back into SQL queries for the underlying relational database.
For those SQL queries where HadoopDB can push parts of the query into the database
one could expect a performance gain from secondary indices and query optimization. For
other SQL queries one should expect the same performance as with plain Hive. In contrast
to Hive HadoopDB can and does take advantage of tables that are repartitioned by a join
key and pushes joins over these keys in the database layer.
Auxiliary Components HadoopDB includes code to connect to the local databases and a
catalog that stores database connection parameters and metadata “such as data sets contained in the cluster, replica locations, and data partitioning properties”. The catalog must
also be generated manually from two input files which adds to the work necessary to setup
the data.
1.3 Benchmarks
Five different tasks are provided to benchmark two parallel databases (Vertica, DBMS-X),
Hadoop and HadoopDB on three different number of nodes (10, 50, 100). The tasks are
executed under the assumption of no node failures. Thus the replication factor of HDFS
respectively the number of replicas in HadoopDB is set to 1 and the task run is not counted
in case of a node failure. In a later task run node failures are also taken into account.
The tasks are borrowed from the already mentioned Stonebraker paper[PPR+ 09]. It is
therefor necessary to refer to Stonebrakers work for their description at some points.
6
CREATE TABLE Documents (
url VARCHAR(100) PRIMARY KEY,
contents TEXT
);
CREATE TABLE UserVisits (
sourceIP VARCHAR(16),
destURL VARCHAR(100),
visitDate DATE,
adRevenue FLOAT,
userAgent VARCHAR(64),
countryCode VARCHAR(3),
languageCode VARCHAR(6),
searchWord VARCHAR(32),
duration INT
);
CREATE TABLE Rankings (
pageURL VARCHAR(100) PRIMARY KEY,
pageRank INT,
avgDuration INT
);
Figure 2: Tables used for the analytical benchmark tasks.
1.3.1 Data Sets
The first task (Grep) uses a simple schema of a 10 bytes key and a random 90 bytes value field.
The next three analytical tasks work on a more elaborate schema of three tables. (Figure 2)
This schema was originally developed by Stonebraker and others to argue that parallel
databases could also be used in areas where MapReduce recently became popular.[PPR+ 09,
1.] The Documents table should resemble the data “a web crawler might find” while the
UserVisits table should “model log files of HTTP server traffic”. The Rankings table is not
further described.[PPR+ 09, 4.3]
It seems that Stonebraker has a very naive idea of the inner workings of a typical web
crawler and the information typically logged by a HTTP server. The BigTable paper from
2006 explains, that Google stores multiple versions of crawled data while a garbage collector
automatically removes the oldest versions.[CDG+ 06] This requirement alone would be very
complicate to model on top of a relational data store. Furthermore a web crawler would
certainly store a lot more data, for example the HTTP headers sent and received, which
may be several pairs due to HTTP redirects or a history when the page was last loaded
successfully along with the last failed loading attempts and the reasons for those failures.
Subsequental processes enrich the data for example with data extracted from HTML or the
HTTP headers and with a list of web sites pointing to this site (inlinks) together with the
anchor text and the page rank of the inlinks. The pageRank column from the Rankings
table would surely be inlined into the Documents table.
The UserVisits table is likely unrealistic. A HTTP server usually has no knowledge of
any adRevenue related to a web site just served, nor is anything known about a searchWord.
Web sites that highlight search words previously entered in a search engine to find that page
do so by parsing the HTTP referrer header. A HTTP server is usually not capable to extract
this information. The same is valid for the HTTP accept language header which provides
a languageCode. A countryCode can usually only be obtained by looking up the users IP
address in a so called geoIP database.
The most important flaw in the data sets however is the assumption of a separated, off
line data analytics setting. Todays search engines need to reflect changes in crawled pages
as soon as possible. It is therefor typical to run the whole process of crawling, processing
and indexing on the same database continuously and in parallel.[PD10]
It is understandable that Abadi choose to reuse the benchmarks to enable comparison.
However he wants to demonstrate the applicability of HadoopDB “for Analytical Workloads”.
4
It is not clear, why this step is necessary. The reduce output from the previous step could have been
written directly to the local file systems of the nodes running the reduce jobs.
5
“We recommend chunks of 1GB, because typically they can be efficiently processed by a database server.”
7
One should therefore expect benchmarks that use schemes related to financial, customer,
production or marketing data.
Data Loading It has already been described how complicated the data loading procedure
is. The benchmark measured a duration of over 8.3 hours for the whole process. Not
accounted for is the manual work done by the operator during the individual steps to start
each individual step, let alone the provisioning and preparation of virtual machines. Since
the process is not (yet) automatized and may even need to be restarted from scratch in case
of failures, it can in practice take two working days. The HadoopDB authors however do
not consider this to be a problem[ABPA+ 09, 6.3]:
While HadoopDB’s load time is about 10 times longer than Hadoop’s, this cost
is amortized across the higher performance of all queries that process this data.
For certain tasks, such as the Join task, the factor of 10 load cost is immediately
translated into a factor of 10 performance benefit.
This statement however ignores that data is often already stored on Hadoop in the first
place and that Hadoop wouldn’t require a load phase at all. Instead Hadoop could run
analytical tasks incrementally or nightly and provide updated results while HadoopDB still
struggles to load the data.
1.3.2 Grep Task
The Grep Task executes a very simple statement over the already described key value data
set:
SELECT * FROM Data WHERE field LIKE ’%XYZ%’;
Hadoop performs in the same area as HadoopDB and the parallel databases require less
then half the time. It is argued that this difference in performance in such a simple query
is mainly due to the use of data compression which results in faster scan times. Although
Hadoop supports compression and could therefore share the same benefits, it isn’t used. An
explanation is found in the Stonebraker paper. They did some experiments with and without
compression and in their experience Hadoop did not benefit from compression but even get
slower.
However it seems that there could be mistakes in their usage of Hadoop. Instead of using
the Hadoop SequenceFile format to write compressed data, they split the original files in
smaller files and used a separate gzip tool to compress them. There is no explanation why
they did the splitting nor do they provide the compression level used with gzip. It is very
likely that the raise in the number of files caused by the splits led to a higher number of
necessary disk seeks.
They also tried record-level compression. This is however pointless since the values in
the Grep Task only have a length of 90 bytes and may even get larger from the compression
header.
1.3.3 Selection Task
The selection task executes the following query:
SELECT pageUrl, pageRank FROM Rankings WHERE pageRank > 10;
The description and result diagram for this task is a bit confusing. The text says that
HadoopDB would outperform Hadoop while the diagram on first sight reports comparable
8
execution times for both. The solution is that there is a second bar for HadoopDB reporting
the duration for a data repartitioning that has been optimized for this task.
It is questionable whether the long and complicated data loading is still tolerable, if it
even has to be optimized for different queries. One would expect that the upfront investment
of data modeling and loading would be rewarded by a great variety of possible queries that
could be executed afterward with high performance.
1.3.4 Aggregation Tasks
These two tasks differ only in their grouping on either the full sourceIP or only a prefix of
it.
SELECT SUBSTR(sourceIP, 1, 7), SUM(adRevenue) FROM UserVisits
GROUP BY SUBSTR(sourceIP, 1, 7);
SELECT sourceIP, SUM(adRevenue) FROM UserVisits
GROUP BY sourceIP;
Hadoop and HadoopDB differ only in around 20% of their execution time while the
parallel databases are again significantly faster. It is once again argued that this would be
mainly caused by compression and therefor the same arguments apply as with the Grep
Task.
Furthermore this tasks points out the advantage of a high level access language. Hive
(and thereby HadoopDB) benefited from automatically selecting hash- or sort-based aggregation depending on the number of groups per rows. A hand coded MapReduce job will
most likely have only one of these strategies hard coded and not choose optimal strategies.
1.3.5 Join Task
The join task needed to be hand coded for HadoopDB too due to an implementation bug in
Hive. The equivalent SQL is:
SELECT sourceIP, COUNT(pageRank), SUM(pageRank),
SUM(adRevenue) FROM Rankings AS R, UserVisits AS UV
WHERE R.pageURL = UV.destURL AND
UV.visitDate BETWEEN ’2000-01-15’ AND ’2000-01-22’
GROUP BY UV.sourceIP;
In this task HadoopDB performs ten times faster then Hadoop but still significantly
slower then the parallel databases. The databases (including HadoopDB) are said to benefit
from a secondary index6 on visitDate, the selection predicate. There are however at least
three questions to be asked about this task.
First, why would anybody want to run this query only for the specified weeks and not
for all weeks? If this query would be run for all weeks then the databases would also need
to scan all data and maybe approach the duration of Hadoop. Hadoop on the other hand
would typically run a nightly run over its data to produce small daily statistics which in
turn could be easily imported into a traditional relational database.
Second, as already noted, the UserVisits table hardly resembles a typical server log. A
typical server log would be ordered by visitDate. If the visitDate would be the primary key,
then Hadoop should be able to execute the query much faster.
Third, since the UserVisits table already contains data from other sources (adRevenue,
countryCode, languageCode, searchWord), why isn’t there also a column for page rank?
6
Hive has also implemented support for secondary indices in the meanwhile.
9
This would of course duplicate data and break traditional database design but better match
real world scenarios. In this case the join wouldn’t be needed and the task would be mostly
the same as the selection task already discussed.
1.3.6 UDF Aggregation Task
This task represents the prime example of MapReduce. The data set consists of a large
corpus of HTML documents. Those are parsed and the occurrence frequency of every url
gets counted. Not only was it difficult to implement this task with the parallel databases,
they also performed significantly worse then Hadoop. HadoopDB wasn’t used with it’s SQL
layer but also queried by MapReduce.
1.3.7 Fault tolerance and heterogeneous environment
All discussed tasks so far have been executed optimistically without any precautions for
single node failures. To test the different performance of the systems in the presence of
failures and in a heterogeneous environment the aggregation task with grouping on the
prefixed sourceIP has been repeated. This time the replication factor was set to 2 and for
the fault tolerance test one random node was killed when half of the query was executed.
The heterogeneous environment was tested in another round by slowing done one node by
running an I/O intensive background job and frequent clears of the OS caches.
The performance loss is reported in relation to the normal performance. Vertica suffers
a dramatic slowdown of more then 100% in both cases since it restarts queries from the
beginning on failure and does not move straggling tasks to other nodes. HadoopDB and
Hadoop both profit from Hadoops inherent design for frequent failures. First Hadoop does
not restart the whole query from scratch when one node fails but only that part of the query
that the failed node performed. Second Hadoop runs second instances of straggling tasks
on already finished nodes. This is called speculative execution. The later started tasks may
finish earlier then tasks that run on faulty nodes with degraded performance.
HadoopDB slightly outperforms Hadoop in the case of failing nodes. Hadoop is slowed
down in this case because it is eager to create additional copies of the replicated data to
provide the replication factor again as soon as possible. HadoopDB on the other hand does
not copy data and would therefore suffer a data loss in case that two nodes containing the
same data would fail.
1.4 Summary and Discussion
The HadoopDB authors conclude that HadoopDB would outperform Hadoop in all but the
last tasks and that it would therefore be a useful tool for large scale analytics workloads.
The comments and discussions in the preceding subsections however challenge this conclusion. The long time to load data may be a problem. Some of the provided benchmarks are
rather unrealistic. The number of nodes tested is still a lower limit for typical MapReduce
installations. The usage of Hadoop may not have been optimal as seen in the discussion
about compression. An experienced Hadoop engineer may be able to achieve better results
with the given tasks. The fault tolerance of HadoopDB lacks the self healing mechanism of
HDFS. And HadoopDB is designed to be a read only database parallel to the production
databases. This seems to be a contradiction to the authors own remark that it would be
“a requirement to perform data analysis inside of the DBMS, rather than pushing data to
external systems for analysis”. [ABPA+ 09, 7.1]
If HadoopDB would be a useful contribution then it should probably have raised discussions, comments or recommendations on the internet. From this perspective however,
10
HadoopDB seems to have failed. Google reports7 less then ten independent mentions of
HadoopDB not counting the HadoopDB authors own publications.
Since HadoopDB uses PostgreSQL and provides a solution to cluster this DBMS, one
should expect some interest from the PostgreSQL community. There are however only a
total of 15 mails in the projects list archive mentioning HadoopDB from which only 4 are
from last year.8
The public subversion repository of HadoopDB on SourceForge has only 22 revisions.9
Although Hadoop is one of the most discussed award winning free software projects
right now, HadoopDB did not manage to profit from this and raise any significant interest
at all outside of academic circles. This may be one indicator for the correctness of critiques
presented in this paper.
An additional critique from a practical perspective may be the combination of two highly
complicate systems, Hadoop and PostgreSQL. Both require advanced skills from the administrator. It is hard enough to find personal that can administrate one of these systems. It
may be practically impossible to find personal competent in both. Both systems come with
their own complexities that add up for little or no benefit.
As already mentioned, Stonebraker already concluded that new databases must be easy
to use because hardware is cheaper then personal. The HadoopDB guide however expects
sophisticated manual work:10
”a careful analysis of data and expected queries results in a significant performance boost at the expense of a longer loading. Two important things to consider
are data partitioning and DB tuning (e.g. creating indices). Again, details of
those techniques are covered in textbooks. For some queries, hash-partitioning
and clustered indices improve HadoopDB’s performance 10 times”
2 Hive
Hive has been developed by Facebook as a petabyte scale data warehouse on top of Hadoop.
[TSJ+ 09] A typical use case for Hive is to store and analyze incrementally (daily) inserted
log data. It provides user interfaces and APIs that allow the submission of queries in a SQL
like language called HiveQL. The application then parses the queries to a directed-acyclic
graph (DAG) of Map Reduce jobs and executes them using Hadoop.
2.1 Data Model
Data in Hive is organized and stored in a three level hierarchy:
• The first level consists of tables analogous to those in relational databases. Each table
has a corresponding HDFS directory where it stores its data.
• Inside tables the data is further split into one or more partitions. Partitioning is done
by the different values of the partitioning columns which each gets their own directory
inside the table directory. Each subsequent partitioning level leads to an additional
sub directory level inside its parent partition.
• Data can be further split in so called buckets. The target bucket is choosen by a
hash function over a column modulo the desired number of buckets.11 Buckets are
7
as of April 7th, searching for the term “HadoopDB”
as of April 8th http://search.postgresql.org/search?m=1
9
as of June 14th http://hadoopdb.svn.sourceforge.net/viewvc/hadoopdb
10
http://hadoopdb.sourceforge.net/guide (2011-06-20)
11
http://wiki.apache.org/hadoop/Hive/LanguageManual/DDL/BucketedTables (2011-06-21)
8
11
representes as files in HDFS either in the table directory if there are no partitions or
in the deepest partition directory.
The following is an example path of a bucket in the table “daily status” with partitions
on the columns “ds” and “ctry”. The hive workspace is in the “wh” directory:
/wh/ daily status / ds = 20090101/ctry = U S / 0001
|
{z
} |
{z
} |{z}
table name
nested partitions
bucket
Buckets are mostly useful to prototype and test queries on only a sample of the full data.
This gives the user a meaningful preview whether a query is correct and could provide the
desired information in a small fraction of the time it would take to run the query over the
full data set. For this purpose Hive provides a SAMPLE clause that lets the user specify the
number of buckets that should be used for the query.
Although no reference has been found for this thesis, buckets may also be useful to
distribute the write and read load to multiple HDFS data nodes. Assuming that partitions
are defined on date and country as in the path example above and the daily traffic data of
web servers in the US should be imported into Hive. In the absence of buckets this import
would sequentially write one file after the other to HDFS. Only one HDFS data node would
be written to12 at any given time while the other nodes remaining idle. If the bucketing
function is choosen so that nearby data rows are likely to end up in different buckets then
the write load gets distributed to multiply data nodes.
Columns in Hive can have the usual primitive scalar types including dates. Additionally
Hive also provides nestable collection types like arrays and maps. Furthermore it is possible
to program user defined types.
2.2 Query Language
HiveQL looks very much like SQL.13 Recent versions of Hive even support Views14 and
Indices15 . A design goal of Hive was to allow users to leverage their existing knowledge of
SQL to query data stored in Hadoop.
However Hive can not modify already stored data. This was a design decision to avoid
complexity overhead. Thus there are no UPDATE or DELETE statements in HiveQL.
[TSJ+ 10] Regular INSERTs can only write to new tables or new partitions inside a table.
Otherwise a INSERT OVERWRITE must be used that will overwrite already existing data.
However since a typical use case adds data to hive once a day and starts a new partition for
the current date there is no practical limitation caused by this.
For extensibility Hive provides the ability for user defined (aggregation) functions. One
example provided is a Python function used to extract memes from status updates.
A noteworthy optimization and addition to regular SQL is the ability to use one SELECT
statement as input for multiple INSERT statements. This allows to minimize the number of
table scans:
FROM (SELECT ...) subq1
INSERT TABLE a SELECT subq1.xy, subq1.xz
INSERT TABLE b SELECT subq1.ab, subq1.xy
12
plus the few replica nodes for the current data block
This ticket tracks tracks the work to minimize the difference between HiveQL and SQL: https://issues.
apache.org/jira/browse/HIVE-1386 (2011-06-21)
14
since
October
2010,
http://wiki.apache.org/hadoop/Hive/LanguageManual/DDL#Create.
2BAC8-Drop_View (2011-06-21)
15
since March 2011, http://wiki.apache.org/hadoop/Hive/LanguageManual/DDL#Create.2BAC8-Drop_
Index (2011-06-21)
13
12
Figure 3: Hive architecture
2.3 Architecture
Hive consists of several components to interact with the user, external systems or programming languages, a metastore and a driver controlling a compiler, optimizer and executor.
(Figure 3)
The metastore contains the schema information for tables and information how data in
this tables should be serialized and deserialized (SerDe information). This information can
be overridden for individual partitions. This allows schemes and serialization formats to
evolve without the necessity to rewrite older data. It is also planned to store statistics for
query optimization in the metastore. 16
The compiler transforms a query in multiple steps to an execution plan. For SELECT
queries these steps are:
1. Parse the query string into a parse tree.
2. Build an internal query representation, verify the query against schemes from the
metastore, expand SELECT * and check types or add implicit type conversions.
3. Build a tree of logical operators (logical plan).
4. Optimize the logical plan.17
5. Create a DAG of MapReduce jobs.
16
The hive paper suggests in its introduction, that the metastore would already store statistics which is later
contradicted in section 3.1 Metastore. More information about the ongoing work for statistics is available
from the Hive wiki and in the jira issue HIVE-33: http://wiki.apache.org/hadoop/Hive/StatsDev
(2011-06-21) https://issues.apache.org/jira/browse/HIVE-33 (2011-06-21)
17
The optimizer currently supports only rule but not cost based optimizations. The user can however provide
several kinds of hints to the optimizer.
13
References
[ABPA+ 09] Abouzeid, Azza ; Bajda-Pawlikowski, Kamil ; Abadi, Daniel J. ; Rasin,
Alexander ; Silberschatz, Avi: HadoopDB: An Architectural Hybrid of
MapReduce and DBMS Technologies for Analytical Workloads. In: PVLDB
2 (2009), Nr. 1, 922-933. http://www.vldb.org/pvldb/2/vldb09-861.pdf
[CDG+ 06]
Chang, Fay ; Dean, Jeffrey ; Ghemawat, Sanjay ; Hsieh, Wilson C. ; Wallach, Deborah A. ; Burrows, Mike ; Chandra, Tushar ; Fikes, Andrew ;
Gruber, Robert E.: Bigtable: a distributed storage system for structured data.
In: Proceedings of the 7th USENIX Symposium on Operating Systems Design
and Implementation - Volume 7. Berkeley, CA, USA : USENIX Association,
2006 (OSDI ’06), 15-15
[DD08]
D. DeWitt, M. S.:
MapReduce:
A major step backwards.
http://databasecolumn.vertica.com/database-innovation/
mapreduce-a-major-step-backwards, 01 2008. – accessed 11-April-2011
[FHH09]
Fanara, Andrew ; Haines, Evan ; Howard, Arthur: The State of Energy and
Performance Benchmarking for Enterprise Servers. In: Nambiar, Raghunath
(Hrsg.) ; Poess, Meikel (Hrsg.): Performance Evaluation and Benchmarking .
Berlin, Heidelberg : Springer-Verlag, 2009. – ISBN 978–3–642–10423–7, Kapitel
Performance Evaluation and Benchmarking, S. 52–66
[PD10]
Peng, Daniel ; Dabek, Frank: Large-scale incremental processing using distributed transactions and notifications. In: Proceedings of the 9th USENIX
conference on Operating systems design and implementation. Berkeley, CA,
USA : USENIX Association, 2010 (OSDI’10), 1-15
[PPR+ 09]
Pavlo, A. ; Paulson, E. ; Rasin, A. ; Abadi, D. J. ; DeWitt, D. J. ;
Madden, S. ; Stonebraker, M.: A comparison of approaches to large-scale
data analysis. In: SIGMOD ’09 (2009)
[SMA+ 07]
Stonebraker, Michael ; Madden, Samuel ; Abadi, Daniel J. ; Harizopoulos, Stavros ; Hachem, Nabil ; Helland, Pat: The End of an Architectural Era (It’s Time for a Complete Rewrite). In: Koch, Christoph (Hrsg.) ;
Gehrke, Johannes (Hrsg.) ; Garofalakis, Minos N. (Hrsg.) ; Srivastava,
Divesh (Hrsg.) ; Aberer, Karl (Hrsg.) ; Deshpande, Anand (Hrsg.) ; Florescu, Daniela (Hrsg.) ; Chan, Chee Y. (Hrsg.) ; Ganti, Venkatesh (Hrsg.) ;
Kanne, Carl-Christian (Hrsg.) ; Klas, Wolfgang (Hrsg.) ; Neuhold, Erich J.
(Hrsg.): 33rd International Conference on Very Large Data Bases. Vienna,
Austria : ACM Press, sep 2007. – ISBN 978–1–59593–649–3, S. 1150–1160
[TSJ+ 09]
Thusoo, Ashish ; Sarma, Joydeep S. ; Jain, Namit ; Shao, Zheng ;
Chakka, Prasad ; Anthony, Suresh ; Liu, Hao ; Wyckoff, Pete ; Murthy,
Raghotham: Hive- A Warehousing Solution Over a Map-Reduce Framework.
In: Proceedings of the VLDB Endowment 2, Nr. 2, 2009, S. 1626–1629
[TSJ+ 10]
Thusoo, Ashish ; Sarma, Joydeep S. ; Jain, Namit ; Shao, Zheng ; Chakka,
Prasad ; 0002, Ning Z. ; Anthony, Suresh ; Liu, Hao ; Murthy, Raghotham:
Hive - a petabyte scale data warehouse using Hadoop. In: Li, Feifei (Hrsg.) ;
Moro, Mirella M. (Hrsg.) ; Ghandeharizadeh, Shahram (Hrsg.) ; Haritsa,
Jayant R. (Hrsg.) ; Weikum, Gerhard (Hrsg.) ; Carey, Michael J. (Hrsg.)
; Casati, Fabio (Hrsg.) ; Chang, Edward Y. (Hrsg.) ; Manolescu, Ioana
14
(Hrsg.) ; Mehrotra, Sharad (Hrsg.) ; Dayal, Umeshwar (Hrsg.) ; Tsotras,
Vassilis J. (Hrsg.): ICDE, IEEE, 03 2010 (Proceedings of the 26th International
Conference on Data Engineering, ICDE 2010, March 1-6, 2010, Long Beach,
California, USA). – ISBN 978–1–4244–5444–0, S. 996–1005
15
FernUniversität in Hagen
Seminar 01912
im Sommersemester 2011
„MapReduce und Datenbanken“
Thema 6
Dryad
Referent: Björn Draschoff
INHALTSVERZEICHNIS
1
2
Einführung ins Thema ............................................................................................................. 1
1.1
Problemdefinition ............................................................................................................. 1
1.2
Lösungsansatz .................................................................................................................. 1
1.3
Charakteristikum von Dryad ............................................................................................ 2
Systemarchitektur .................................................................................................................... 3
2.1
3
Beispiel SQL-Abfrage ...................................................................................................... 4
Beschreibung eines Dryad-Graphen ........................................................................................ 6
3.1
Generierung neuer Knoten ............................................................................................... 6
3.2
Hinzufügen neuer Kanten ................................................................................................. 6
3.3
Zusammenführung zweier Graphen ................................................................................. 7
3.4
Channel Typen ................................................................................................................. 8
3.5
Job Inputs und Outputs ..................................................................................................... 9
3.6
Job-Stadien ....................................................................................................................... 9
4
Erstellung eines Vertex-Programms ........................................................................................ 9
5
Ausführung des Jobs .............................................................................................................. 10
6
5.1
Fehlertoleranzstrategie ................................................................................................... 11
5.2
Optimierung zur Laufzeit ............................................................................................... 11
Experimentelle Beurteilung ................................................................................................... 12
6.1
verwendete Hardware ..................................................................................................... 12
6.2
SQL-Abfrage Experiment .............................................................................................. 13
6.3
Data Mining Experiment ................................................................................................ 14
6.3.1
7
8
Zusammenfassung des Optimierungsprozesses ...................................................... 15
Auf Dryad basierend .............................................................................................................. 16
7.1
Die Skriptsprache „Nebula“ ........................................................................................... 16
7.2
Integration mit SQL Server Integration Services (SSIS) ............................................... 16
7.3
Verteilte SQL-Abfragen ................................................................................................. 16
Dryad vs. MapReduce ........................................................................................................... 17
Literaturverzeichnis ...................................................................................................................... 18
Einführung ins Thema
1
1 EINFÜHRUNG INS THEMA
Das Thema Cloud Computing ist heutzutage in aller Munde. Es beschreibt die Verwendung einer
Technologie, welche große Berechnungen auf mehreren Computern in einem Netzwerk verteilt.
Der Begriff Cloud beschreibt demnach eine Farm von Computern, die sich zu einem Cluster
verbinden, was vergleichbar mit einer Wolke ist. Die verschiedenen Komponenten wie Rechenoder Speicherkapazitäten werden dynamisch im Netzwerk zur Verfügung gestellt. Ein
wesentlicher Aspekt des Cloud Computing ist die parallele Berechnung auf mehreren Computern
in der Cloud, woraus ein enormer Geschwindigkeitsvorteil entsteht.
1.1 PROBLEMDEFINITION
Heutzutage werden Daten vermehrt automatisiert erhoben. Als Beispiel seien hier Log-Dateien,
Social Networks, Wetterdaten oder auch Aktienkurse genannt. Da sich immer größere
Datenmengen anhäufen, wird es fortwährend schwerer diese auch in vertretbarer Zeit zu
verarbeiten oder Analysen darüber zu erstellen. Zwar sind in den letzten Jahren die
Speicherkapazitäten von Festplatten enorm angestiegen, jedoch die Zugriffszeiten nicht im
gleichen Maße. So betrug das durchschnittliche Volumen von Festplatten im Destkopbereich im
Jahr 2000 ca. 40 GB mit Zugriffzeiten von rund 32 MB/s. Für das Auslesen einer solchen Platte
wurden demnach knapp 21 Minuten benötigt. 2009 betrug die Speicherkapazität 1.000 GB,
schon das 25-fache im Vergleich zum Jahr 2000. Die Zugriffszeiten betrugen dabei 125 MB/s,
was nur ein 4-faches im Vergleich zum Jahr 2000 ist. Für das Auslesen einer solchen Platte
würden demnach 135 Minuten benötigt [WIKI].
1.2 LÖSUNGSANSATZ
Um diesem Flaschenhals entgegen zu wirken setzen große Unternehmen wie z.B. Microsoft,
Google oder Apache, auf die verteilte parallele Berechnung großer Datenmengen in Clustern.
Bei dieser Technologie werden mehrere Recheneinheiten mit eigenen Festplatten zu einem
Cluster verbunden. Die Daten werden in kleine Stücke aufgeteilt und verteilt auf dem Computer
gespeichert, der die Daten verarbeitet. Durch dieses Verfahren wird eine deutliche
Zeiteinsparung bei der Datenverarbeitung und der Zugriffsoperationen erzielt. Da immer mehr
parallele Architekturen Verwendung finden, müssen auch parallele Programme geschrieben
werden, die mit der Architektur effizient arbeiten können. Das Programmieren solcher
Anwendungen ist sehr aufwändig und muss angepasst werden, wenn sich die Laufzeitumgebung
ändert. Um diesen Aufwand zu vereinfachen sind zwei Komponenten nötig: ein vereinfachtes
Programmiermodell, in dem es möglich ist auf einem hohen Level zu arbeiten (sowie
Nebenläufigkeiten und deren Ort zu spezifizieren) und eine effiziente Laufzeitumgebung. Diese
muss Low Level Mapping unterstützen, Ressourcen managen und sollte unabhängig vom System
und dessen Größe bzw. Größenänderung sein. Diese beiden Komponenten sind dabei eng
miteinander verknüpft. Aufgrund dieser Tatsachen und der Komplexität der
Parallelprogrammierung kam die Idee auf, eine einfachere Grundlage zu bilden, in der die
Organisation der Parallelisierung automatisch vorgenommen wird.
In diesem Zusammenhang entwickelte Microsoft das Parallel-Computing-Projekt Dryad. Bill
Gates hatte Dryad im Jahr 2006 erstmals gegenüber der New York Times erwähnt. Sonst blieb es
still um das Projekt, das sich mit Konzepten für parallele und verteilte Programme beschäftigt,
2
Einführung ins Thema
die sowohl in kleinen Clustern als auch in riesigen Rechenzentren mit optimaler Leistung
ausgeführt werden können [ZDNet].
1.3 CHARAKTERISTIKUM VON DRYAD
Dryad ist Microsofts Antwort auf Techniken wie Google MapReduce oder Apache Hadoop. Am
Anfang war Dryad ein Microsoft-Research-Projekt. Die Forscher wollten Methoden entwickeln,
um Programme für verteiltes Rechnen zu entwickeln, die sich von kleinen Computerclustern bis
zu riesigen Rechenzentren skalieren lassen.
Dryad ist eine universelle, verteilte Ausführungsengine für parallele Datenverarbeitungen. Eine
Dryad-Anwendung kombiniert Auslastungsspitzen mit Kommunikationskanälen zu einem
Datenflussdiagramm. Dryad verteilt die Auslastungsspitzen auf verfügbare Computer, unter
Verwendung von Dateien, TCP-Pipes und Shared Memory FiFo’s.
Der Anwendungsentwickler erstellt normalerweise sequentielle Programme ohne Threads oder
Locks. Parallelität ergibt sich bei Dryad durch Verteilung auf mehrere PCs oder mehrere CPUKerne innerhalb eines Computers. Die Anwendung kann die Größe und die Platzierung von
Daten während der Laufzeit ermitteln, und ändert danach den Graph um alle Ressourcen
effizient zu nutzen.
Dryad wurde für Multi-Core Einzelplatzrechner, kleine Cluster von Computern und
Rechenzentren mit tausenden von Computern entwickelt. Die Dryad-Engine behandelt alle
Probleme die bei der Verarbeitung großer, verteilter paralleler Anwendungen auftreten:
Scheduling der verfügbaren PCs und deren CPUs, wiederherstellen von Verbindungen oder
Computerausfälle und Transport der Daten zwischen den Knoten.
Die ersten Versionen der Software wurden im Sommer 2009 an Universitäten für nicht
kommerzielle Anwendungen ausgegeben. Erst 2010 hat man Dryad von der Forschungsabteilung
in die Technical Computing Group verlegt. Laut einer im August 2010 veröffentlichten
Präsentation sollte die Community Technology Preview (CTP) bereits im November erscheinen.
Die finale Version für Windows HPC Server wird 2011 auf den Markt kommen.
Die aktuelle CTP wendet sich laut Microsoft an Entwickler, die sich mit datenintensiven
Anwendungen befassen. Die Systemvoraussetzung für Dryad ist ein Computercluster mit
Windows HPC Server 2008 R2 und installiertem Service Pack 1.
Zu dem Dryad-Projekt gehören eine Reihe von Komponenten, unter anderem der DryadLINQCompiler und die Laufzeitumgebung, sowie ein verteiltes Dateisystem mit dem Codenamen
"TidyFS", eine Reihe von Tools zur Datenverwaltung (Codename "Nectar") und ein
Steuerprogramm für verteilte Cluster (Codename "Quincy").
Systemarchitektur
3
2 SYSTEMARCHITEKTUR
Die Gesamtstruktur eines Dryad-Jobs wird durch den Kommunikationsfluss festgelegt. Ein Job
ist ein gelenkter azyklischer Graph, wobei jeder Knoten ein Programm und jede Kante einen
Datenkanal darstellt. Es ist ein logisches Berechnungsdiagramm, das automatisch auf den
physischen Ressourcen abgebildet wird. Im Einzelfall können mehr Knoten im Graph vorhanden
sein als CPUs im Cluster. Zur Laufzeit wird ein Kanal benutzt, um eine definierte Sequenz von
strukturierten Elementen zu transportieren. Die Implementierung der Kanalabstraktion basiert
auf Shared Memory, TCP-Pipes oder temporären Dateien im Filesystem. Ein Vertex-Programm
liest und schreibt seine Daten ebenso unabhängig davon, ob ein Kanal seine Daten über einen
Puffer auf Disk, TCP-Stream oder direkt in den Shared Memory überträgt. Das Dryad-System
verwendet kein primäres Datenmodell für die Abbildung von Objekten. Der Typ eines Elementes
wird durch die Anwendung festgelegt, so dass diese ihre eigene Abbildung des Objektes je nach
Programm unterstützen kann. Diese Entscheidung erlaubt es Anwendungen zu unterstützen, die
direkt auf vorhandenen Daten, einschließlich SQL-Exporttabellen und Textprotokolldateien,
funktionieren. In der Praxis verwenden die meisten Anwendungen einen Typ aus einem kleinen
Satz Libary-Elementen, die Microsoft als „newline-terminated“ Textstrings und Tupel von
Basistypen anbietet.
Der Workflow in Abbildung 1 zeigt die Struktur des Dryad Systems. Ein Dryad-Job wird durch
einen Jobmanager-Prozess (JM) koordiniert, welcher im Cluster oder auf einem Arbeitsplatz mit
Netzwerkzugang ausgeführt wird. Der Jobmanager enthält den anwendungsspezifischen Code,
um den Kommunikationsgraph zusammen mit dem Libary-Code zu konstruieren. Er plant,
verteilt und überwacht die Aufträge über die verfügbaren Ressourcen. Alle Daten werden direkt
zwischen Knoten gesendet, so dass folglich der Jobmanager nur für Steuerentscheidungen
verantwortlich ist und somit nicht zum Engpass für Datenübertragungen wird.
Abbildung 1 - Struktur des Dryad Systems [IB07 S. 61]
Das Cluster hat einen Name-Server (NS) der benutzt werden kann, um alle verfügbaren
Computer zu spezifizieren. Der Name-Server registriert auch die Position jedes Computers
innerhalb der Netztopologie und berücksichtigt diese bei der Aufgabenverteilung. Auf jedem
Computer im Cluster läuft ein Dämon (D), welcher für die Prozesserstellung im Namen des
Jobmanagers verantwortlich ist. Wird das erste Mal ein Vertex (V) auf dem Computer
4
Systemarchitektur
ausgeführt, erhält der Dämon die Anweisung vom Jobmanager geschickt und nachfolgende
Prozesse werden aus dem Cache aufgerufen. Der Dämon tritt als ein Proxy auf, so dass es dem
Jobmanager möglich ist mit den Prozessen zu kommunizieren und die Prozesse, sowie die
gelesenen und geschriebenen Daten zu monitoren. Es ist unkompliziert einen Nameserver und
Dämon auf einer Workstation laufen zu lassen, um somit ein Cluster zu simulieren und dadurch
die gesamten Jobs von einem Punkt zu aktivieren und zu prüfen. Ein Taskmanager steuert die
Batchjobs. Durch ein verteiltes Speichersystem, ähnlich dem Google Filesystem, werden große
Dateien in kleinere Elemente geteilt, welche repliziert und verteilt werden. Dryad unterstützt
auch die Verwendung von NTFS für das direkte zugreifen auf Dateien am lokalen PC, welches
für kleinere Blöcke mit niedrigen Verwaltungskosten von Vorteil sein kann.
2.1 BEISPIEL SQL-ABFRAGE
In diesem Abschnitt wird ein konkretes Beispiel für eine Dryad-Anwendung beschrieben, auf die
im weiteren Verlauf immer wieder eingegangen wird. Die vom Microsoft Autorenteam gewählte
Aufgabenstellung ist repräsentativ für eine Gruppe von eScience-Anwendungen, bei denen im
Rahmen wissenschaftlicher Untersuchungen große Mengen digital vorliegender Daten
verarbeitet werden. Die benutzte Datenbank ist abgeleitet aus den Daten von Sloan Digital Sky
Survey (SDSS), einem Projekt das eine Karte von einem großen Teil des Universums erstellt.
[SDSS]
Aus einer Studie wurde einer der zeitintensivsten Fragestellung (Q18) gewählt. Die Aufgabe ist
einen Gravitationslinseneffekt zu identifizieren. Es sollen alle Objekte in der Datenbank ermittelt
werden, bei denen mindestens eines der benachbarten Objekte im Umkreis von
30 Bogensekunden eine ähnliche Farbe wie das Primärobjekt hat. Die Abfrage kann in SQL wie
folgt gestellt werden:
select distinct p.objID
from photoObjAll p
join neighbors n
- call this join “X”
on p.objID = n.objID
and n.objID < n.neighborObjID
and p.mode = 1
join photoObjAll l
- call this join “Y”
on l.objid = n.neighborObjID
and l.mode = 1
and abs((p.u-p.g)-(l.u-l.g))<0.05
and abs((p.g-p.r)-(l.g-l.r))<0.05
and abs((p.r-p.i)-(l.r-l.i))<0.05
and abs((p.i-p.z)-(l.i-l.z))<0.05
Durch diese Abfrage wird auf zwei Tabellen zugegriffen. Die erste Tabelle photoObjAll hat
354.254.163 Einträge, für jedes identifizierte astronomische Objekt einen, gekennzeichnet durch
einen eindeutigen Bezeichner objID. Diese Datensätze umfassen auch die Objektfarben als
Magnitude (logarithmische Helligkeit) in fünf Bändern: u, g, r, i und z. Die zweite Tabelle
neighbors hat 2.803.165.372 Datensätze, einen für jedes Objekt welches innerhalb von
30 Bogensekunden eines anderen Objektes gelegen ist. Die mode Eigenschaften wählen nur
Primärobjekte aus. Das < Prädikat eliminiert Verdopplungen, die durch das symmetrische
Nachbarschaftsverhältnis verursacht werden. Die Ergebnisse der Joins „X“ und „Y“ sind
932.820.679 beziehungsweise 83.798 Datensätze, und das letztendliche Ergebnis umfasst 83.050
Datensätze.
Die Abfrage greift nur auf einige Spalte der Tabellen zu. Die komplette Tabelle photoObjAll
besitzt eine Größe von 2 KBytes pro Datensatz. Bei der Ausführung durch den SQL-Server,
verwendet die Abfrage einen Index auf den photoObjAll Key objID, mit zusätzlichen Spalten
Systemarchitektur
5
für mode, u, g, r, i und z, und einen Index auf den neighbors Key objID, mit einer zusätzlichen
Spalte neighborObjID. Der SQL-Server liest diese Indexe und lässt den Rest der Daten auf der
Disk. In der Microsoft Testdatenbank wurden die nicht benötigten Spalten der Tabellen weg
gelassen, um unnötigen Datentransfer im Bereich von Multi-Terrabyte zu vermeiden.
Für die gleichwertige Dryad-Berechnung wurden die Indizes in zwei Binärdateien „ugriz.bin“
und „neighbors.bin“ extrahiert, wobei beide mit der gleichen Reihenfolge der Indizes sortiert
wurden. Die „ugriz.bin“-Datei hat 36-Byte Records und somit eine ungefähre Größe von 11,8
GBytes. Die Datei „neighbors.bin“ hat 16-Byte Records, wodurch ihre Größe 41,8 GByte
entspricht. Der Output von Join „X“ beträgt 31,3 GBytes, von Join „Y“ 655 KByte und der finale
Output umfasst 649 KBytes.
Abbildung 2 stellt den Kommunikationsgraph der Abfrage für die Dryad-Berechnung dar. Die
beiden Dateien wurden anhand der objID-Ranges in ungefähr n-gleichgroße Teile aufgeteilt
(welche mit U1 bis Un und N1 bis Nn benannt sind). Des Weiteren wurden benutzerdefinierte
C++ Objekte für jeden Datensatz im Diagramm benutzt. Die
Vertexe Xi (1 ≤ i ≤ n) implementieren den Join „X“, indem die
verteilten Ui- und Ni-Inputs (basierend auf der objID und gefiltert
durch den Ausdruck < und p.mode=1) zu Records, welche
objID, neighborObjID und die Farbspalten entsprechend der
ObjID enthalten, vermischt. Die D Knoten verteilen ihre OutputDatensätze, viermal feiner als die verwendeten Eingangsdateien,
auf die M Knoten, über die neighborObjID unter Verwendung
einer Range Aufteilungsfunktion. Die Zahl vier wurde gewählt,
da jeder Computer über je vier Prozessoren verfügt und somit
vier Pipelines parallel ausgeführt werden können. Die M Knoten
führen ein nicht-deterministisches Mischen der Inputs durch und
die S Knoten sortieren auf Basis der neighborObjID, unter
Verwendung eines in-memory Quicksort. Die Ausgabedatensätze
von S4i-3 … S4i (i=1 bis n) werden in Yi zusammengezogen, in
dem sie mit den anderen vermischt werden, die von Ui gelesen
werden, um sie im Join „Y“ zu implementieren. Dieser Join
basiert auf der objID von U = neighborObjID von S, wird durch
den Rest des Prädikats gefiltert und bringt so die Farben
zusammen. Die Outputs der Y Knoten werden in einer HashTabelle am H Knoten vermengt, um das eindeutige Schlüsselwort
der Abfrage zu implementieren. Schließlich liefert die Auflistung
der Hash-Tabelle das Resultat. Im Verlauf werden weitere
Abbildung 2 Einzelheiten über die Implementierung dieses Dryad-Programmes
Kommunikationsgraph der SQLausgeführt.
Query [IB07 S.62]
6
Beschreibung eines Dryad-Graphen
3 BESCHREIBUNG EINES DRYAD-GRAPHEN
Dryad wurde entwickelt, um auf möglichst einfache Weise Kommunikationsidiome zu
spezifizieren. Es ist aktuell in C++ als Libary eingebettet und verwendet eine Mischung von
Methodenaufrufen und Operatoren-Überladung.
Die Erstellung von Graphen erfolgt indem einfachere Sub-Graphen, unter Verwendung weniger
Operationen miteinander kombiniert werden. Alle Operationen basieren darauf, dass der
Ergebnisgraph azyklisch ist. Das Basisobjekt der Sprache ist der Graph: G = VG, EG, IG, OG.
G enthält eine Sequenz von Knoten (Vertex) VG, einen Satz direkter Kanten EG und zwei Sätzen
IG  VG und OG  VG; welche die In- und Outputs darstellen. Die Input- und Output-Kanten
eines Knoten werden so geordnet, dass diese einen speziellen Port aus Knoten ergeben und somit
ein vorgegebenes Paar von Knoten an mehrere Ränder angeschlossen werden kann.
3.1 GENERIERUNG NEUER KNOTEN
Die Dryad-Libary definiert eine C++ Basisklasse, auf welcher alle Programmknoten basieren.
Jedes Programm ist durch einen uniquen Namen gekennzeichnet und besitzt eine statische
„Factory“ in der die Basis des Programms hinterlegt ist. Durch den Aufruf der passenden
statischen Programmfactory wird ein neuer Knoten erstellt. Alle erforderlichen
knotenspezifischen Parameter können beim Aufruf der Methode an das Programmobjekt
übergeben werden. Diese Parameter werden dann dem uniquen Knotennamen zugeordnet,
welche zu einem einfachen Komplex zusammengefasst werden und an den Remoteprozess zur
Ausführung gesendet werden.
Ein einfacher Graph G wird durch einen Knoten v generiert G= (v), 0, {v}, {v}. Ein Graph
kann durch Verwendung des –Operators in einen anderen Graphen mit k Kopien geklont
werden, wobei C = G  k wie folgt definiert ist:
Hierbei ist
ein Klon von G, welcher Kopien aller Knoten und Kanten
enthält ( kennzeichnet die Verkettung). Jeder geklonte Knoten übernimmt die Typen und
Parameter des entsprechenden Knotens in G.
3.2 HINZUFÜGEN NEUER KANTEN
Neue Kanten werden erstellt, indem man eine Verkettungsoperation auf zwei existierende
Graphen anwendet. Alle Teile der Verkettung müssen auf der gleichen Struktur basieren und
kreieren durch C=AB einen neuen Graph: C=VA  VB, EA  EB  Enew, IA, OB, wobei C die
Vereinigung aller Kanten und Knoten von A und B beinhaltet (mit A’s Inputs und B’s Outputs).
Zusätzlich entstehen Ränder von Enew aus den Knoten in OA und IB. VA und VB werden zur
Laufzeit disjunkt und da A und B beide azyklisch sind, ist es auch C.
Beim Erstellen der Kanten Enew die zu einem Graph hinzukommen, werden zwei Standards
unterschieden.

A >= B bildet einen punktweisen Aufbau wie in der nachfolgenden Abbildung 3 im
Abschnitt (c) dargestellt ist. Wenn |OA| ≥ |IB| wird eine einzelne abgehende Kante von
Beschreibung eines Dryad-Graphen

7
jedem Output A’s erstellt. Die Kanten werden mittels des Round-Robin zu B’s Input
zugeordnet. Einige der Knoten in IB können mit mehr als einem ankommenden Rand
enden. Wenn |IB| > |OA| wird eine eingehende Kante zu jedem Input B’s erstellt, welche
wiederum mittels Round-Robin von A’s Output zugewiesen werden.
A >> B erstellt einen vollständigen zweiteiligen Graphen zwischen OA und IB (vgl.
Abbildung 3, Abschnitt d).
Abbildung 3 – Operatoren [IB07 S. 63]
3.3 ZUSAMMENFÜHRUNG ZWEIER GRAPHEN
Die finale Operation der Sprache ist ||, wodurch zwei Graphen zusammengeführt werden.
C = A || B erstellt einen neuen Graphen: C = VA * VB, EA  EB, IA * IB, OA * OB, wobei es
im Gegensatz zur Komposition nicht erforderlich ist, dass A und B disjunkt sind. VA * VB ist
die Zusammenführung von VA und VB, wobei die Duplikate der zweiten Sequenz entfernt
werden. IA * IB beschreibt die Vereinigung der Eingänge von A und B. Wenn in einem Knoten
VA  VB Inputs beinhaltet sind, werden die ausgehenden Kanten so verkettet, dass die Kanten in
EA zuerst entstehen (mit niedriger Portnummer). Diese Vereinfachung verhindert Graphen mit
„Crossover“-Kanten.
Die Merge-Operation ist extrem leistungsfähig und ermöglicht einfache typische Muster zur
Kommunikation zu erstellen (vgl. Abbildung 3, Teil (f)-(h)). Ebenso werden Möglichkeiten
geboten, durch eine Kollektion von Knoten und Kanten, einen Graphen selbst zu erstellen. Zum
Beispiel kann ein Baum mit vier Knoten a, b, c und d zu G = (a>=b) || (b>=c) || (b>=d)
zusammengestellt werden.
8
Beschreibung eines Dryad-Graphen
Das Graph-Builder-Programm zur Erstellung des Graphen aus Abbildung 2, ist in der
nachfolgenden Abbildung 4 dargestellt.
GraphBuilder XSet = moduleX^N;
GraphBuilder DSet = moduleD^N;
GraphBuilder MSet = moduleM^(N*4);
GraphBuilder SSet = moduleS^(N*4);
GraphBuilder YSet = moduleY^N;
GraphBuilder HSet = moduleH^1;
GraphBuilder XInputs = (ugriz1 >= XSet) || (neighbor >= XSet);
GraphBuilder YInputs = ugriz2 >= YSet;
GraphBuilder XToY = XSet >= DSet >> MSet >= SSet;
for (i = 0; i < N*4; ++i)
{
XToY = XToY || (SSet.GetVertex(i) >= YSet.GetVertex(i/4));
}
GraphBuilder YToH = YSet >= HSet;
GraphBuilder HOutputs = HSet >= output;
GraphBuilder final = XInputs || YInputs || XToY || YToH || HOutputs;
Abbildung 4 – Beispiel Graph-Builder-Programm [IB07 S. 64]
3.4 CHANNEL TYPEN
Standardmäßig wird jeder Kanal so implementiert, dass eine temporäre Datei genutzt wird. Der
Producer schreibt typischer Weise auf die lokale Disk und der Consumer liest diese Datei aus. In
der Regel schreiben mehrere Knoten auf die lokalen Ressourcen eines Rechners, weshalb es
sinnvoll ist, diese innerhalb des gleichen Prozesses auszuführen. Die Sprache verfügt über einen
Verkapselungs-Befehl, welcher einen Graph G nimmt und diesen an einen neuen Knoten vG
zurückgibt. Wenn vG als Vertex-Programm läuft, bewertet der Jobmanager dies als periodische
Ausführung von G mit Anforderungsparametern, und es lässt alle Vertexe von G gleichzeitig
innerhalb des gleichen Prozesses verbundenen durch Kanten und unter Verwendung des sharedmemory FIFOs laufen. Da es immer möglich ist, ein eigenes Vertex-Programm mit der gleicher
Semantik wie G zu erstellen, ermöglicht die Verkapselung ein effizientes kombinieren einfacher
Libary-Knoten auf der Diagrammschicht.
Bei der Erstellung der Kanten kann der Entwickler optional spezifizieren welches
Transportprotokoll benutzt werden soll. Die unterstützten Protokolle sind File (Standard), TCPPipe oder Shared-memory FIFO.
Weil der Graph azyklisch ist, ist ein Deadlock unmöglich wenn alle Kanäle entweder in
temporäre Dateien schreiben oder den shared-memory FIFO verdeckt und innerhalb der
eingekapselten Subgraphen verwenden. Wenn es dem Entwickler möglich ist sichtbare FIFOs zu
verwenden, kann dies Deadlocks verursachen.
Erstellung eines Vertex-Programms
9
3.5 JOB INPUTS UND OUTPUTS
Große Datenmengen werden normalerweise geteilt und über die Rechner im Cluster verteilt.
Folglich müssen die logischen Eingänge in einem Graph G =  VP, , , VP  gruppiert werden,
wobei VP ein Sequenz virtueller Knoten entsprechend der Partitionen des Inputs darstellt. Analog
können zum Jobende die Outputs zu einer logisch verteilten Datei verkettet werden. Im
Normalfall wird zur Laufzeit die Anzahl der Teile ermittelt und dazu passend automatisiert der
Graph repliziert.
3.6 JOB-STADIEN
Bei der Erstellung eines Graphen wird jeder Knoten mit einem definierten Stadium initialisiert,
um das Jobmanagement zu vereinfachen. Die Topologie der Stadien kann als Grundgerüst oder
Zusammenfassung des Gesamtjobs gesehen werden. Die Topologie der Skyserver-QueryApplikation aus dem Beispiel wird in Abbildung 5 gezeigt.
Jeder eindeutige Typ eines Knoten ist in einer separaten Gruppe zusammengefasst. Die meisten
Stadien werden durch Verwendung des >= Operators verbunden, während D an M unter
Verwendung des >> Operators angeschlossen ist. Bei der Überwachung des Jobs wird dieses
Grundgerüst als Guide für die Erzeugung von Zusammenfassungen genutzt. Ebenso kann es für
automatisierte Optimierungen genutzt werden, wie sie im Abschnitt 5.2 beschrieben werden.
4 ERSTELLUNG EINES VERTEX-PROGRAMMS
Die grundlegenden API’s für die Erstellung eines Dryad-Vertex-Programms werden
als C++ Basisklassen und Objekte bereitgestellt. Eine Grundanforderung an Dryad
war, dass es sich in den allgemeinen Code bzw. Bibliotheken eingliedert und somit
dryadspezifische Konstrukte oder Sandboxen vermieden werden. Die meisten
existierenden Codebestandteile die in Dryad integriert wurden sind in C++ erstellt.
Es ist jedoch unkompliziert API-Wrapper zu implementieren, so dass auch
Entwicklungen aus anderen Sprachen eingebunden werden können (z.B. C#). Ebenso
kann es erforderlich sein, Programme direkt auszuführen zu können, was ebenfalls
unterstützt wird (vgl, Abschnitt 4.2).
Dryad verfügt über eine Laufzeitbibliothek, welche für die Ausführung der Knoten
während der verteilten Berechnung verantwortlich ist. Wie in Abschnitt 3.1
beschrieben, empfängt die Runtime eine Jobbeschreibung vom Jobmanager, wie der
Knoten auszuführen ist. URLs beschreiben die In- und Output-Kanäle, wobei aktuell Abbildung 5 keine Typprüfung für die Kanäle vorgenommen wird, so dass der Knoten in der Lage Stadien der
Dryadsein muss, mittels entsprechender Routinen festzustellen (statisch oder über Berechnung aus
Anforderungsparameter), welche Typen zum Lesen und Schreiben auf dem Kanal Abbildung 2 [IB07
S. 64]
übertragen werden. Der Body des Knoten wird über die Standard-Main-Methode
aufgerufen, in welcher Channel-Reader und –Writer in der Argumentliste enthalten sind. Der
Knoten berichtet an den Jobmanager über Status und Fehler.
Durch die Prozess-Wrapper-Libary wird die Ausführung mit Parametern unterstützt. Der
Wrapper-Vertex ist in der Lage willkürliche Arten von Datentypen zu verarbeiten. Somit sind
die Elemente einfache feste Puffer, die unverändert zum Prozess (unter Verwendung einer Pipe)
10
Ausführung des Jobs
im Dateisystem übertragen werden. Dies ermöglicht es bereits bestehende Binaries als DryadVertex-Programm laufen zu lassen. So ist es zum Beispiel möglich Perlskripte oder Grep im
Knoten eines Dryad-Jobs aufzurufen.
Die meisten Dryad Knoten enthalten einen sequentiellen Code. Es wird jedoch auch ein
ereignisbasierter Programmierstil, durch Verwendung eines geteilten Threadpools unterstützt.
Zur Laufzeit wird automatisch unterschieden, welche Knoten den Threadpool nutzen können,
und welche einen separaten Thread und folglich einen gekapselten Graphen benötigen.
Die Implementierung der Kanäle ermöglicht Lesen, Schreiben, Serialisierung und
Deserialisierung der Tasks auf einem geteilten Threadpool. Durch die Abstraktion von Lese- und
Schreibphasen wird zur Laufzeit versucht, dem Entwickler leistungsfähige Kanäle
bereitzustellen. Die Experimente in Abschnitt 6.2 bestätigen die Leistungsfähigkeit dieser
Abstraktion. Sogar einzelne Dryad-Knoten-Applikationen haben Durchsätze vergleichbar mit
kommerziellen Datenbanksystemen.
5 AUSFÜHRUNG DES JOBS
Der Scheduler des Jobmanager überwacht alle aktuellen und vergangenen Zustände eines jeden
Vertex im Graph. Durch Checkpoints oder Reproduktion können Ausfälle von Computern
kompensiert werden. Ein Vertex kann mehrmals über die Laufzeit des Jobs ausgeführt werden.
Jede Ausführung eines Vertex hat eine Versionsnummer und einen entsprechenden
Ausführungsdatensatz, welcher den Zustand der Ausführung und die Version des
Vorgängerknoten enthält. Jede Ausführung benennt seine dateibasierten Outputkanäle eindeutig
durch Benutzung der Versionsnummer und vermeidet somit Konflikte unter den Versionen.
Wenn der gesamte Job erfolgreich abgeschlossen ist, wählt jeder Vertex eine erfolgreiche
Ausführung und benennt diesen Output in den korrekten Dateinamen um.
Wenn alle Vertex-Inputkanäle den Status bereit haben, wird ein neuer Ausführungsdatensatz für
den Vertex erstellt und in die Queue gelegt. Ein plattenbasierter Kanal wird als bereit angesehen,
wenn die gesamte Datei gelesen wurde. Ein Kanal welcher eine TCP-Pipe oder shared-memory
FIFO ist wird als bereit angesehen, wenn der vorhergehende Vertex mindestens einen laufenden
Ausführungsdatensatz hat.
Ein Vertex und jeder seiner Kanäle können jeweils eine Liste von Computern spezifizieren, auf
welchen sie laufen möchten. Die Begrenzungen werden kombiniert und an den
Ausführungsdatensatz angefügt, wenn dieser zur Scheduler-Queue hinzugefügt wird. Somit sind
dem Programmierer hiermit auch Steuerungsmöglichkeiten gegeben.
Die Planung des Jobmanagers basiert aktuell auf der Annahme, dass es der einzige Job im
Cluster ist. Wenn ein Ausführungsdatensatz mit dem verfügbaren Rechner verbunden wird, weist
der Remotedämon den spezifizierten Vertex an und während der Ausführung empfängt der
Jobmanager periodisch Statusupdates vom Vertex. Wenn jeder Vertex abgeschlossen ist, gilt
auch der Job als erfolgreich abgeschlossen. Wurde ein Vertex öfter angestoßen, als per
Definition vorgegeben, gilt die Jobausführung als fehlgeschlagen.
Ausführung des Jobs
11
Dateien die temporäre Kanäle darstellen werden in Verzeichnissen gespeichert, die vom Dämon
gemanagt und aufgeräumt werden, wenn der Job abgeschlossen ist. Ebenso werden Vertices
durch den Dämon gekillt, wenn der „Parent“-Jobmanager abstürzt. Mittels einer einfachen
Graph-Visualisierer-Suite für kleine Jobs kann der Status jedes Vertex und die Menge der Daten
dargestellt werden, während die Berechnung weiter läuft. Eine webbasierte Schnittstelle zeigt
regelmäßig aktualisierte zusammenfassende Statistiken eines laufenden Jobs an und kann dazu
benutzt werden um große Berechnungen zu überwachen. Die Statistik beinhaltet die Anzahl der
Knoten, welche beendet oder neu ausgeführt wurden, die Anzahl des Datentransfers über die
Kanäle und die Fehler-Codes. Über Links ist es dem Programmierer möglich von einer
Übersichtsseite aus, die Log-Dateien zu lesen oder einen Crash Dump zur Fehlerdiagnose
herunterzuladen. Dieser ist mit einem Index gekennzeichnet, der es erlaubt den Vertex neu und
isoliert auf dem lokalen Rechner auszuführen.
5.1 FEHLERTOLERANZSTRATEGIE
Fehler können während der Ausführung der verteilten Anwendung zu jedem Zeitpunkt auftreten.
Die Ausfallstrategie ist darauf ausgelegt, dass alle Vertex-Programme deterministisch sind. Da
der Kommunikationsgraph azyklisch ist, kann man davon ausgehen, dass jede terminierende
Ausführung eines Jobs das gleiche Resultat liefert, unabhängig der Reihenfolge der Computeroder Disk-Fehlern. Schlägt die Ausführung eines Vertex fehl, leitet der Dämon dies an den
Jobmanager weiter. Für den Fall, dass ein Dämon fehlschlägt, bemerkt der Jobmanager dies über
den „Heartbeat Timeout“. Sollte der Fehler auf einen Lesefehler eines Inputkanals basieren, wird
der Datensatz mit der Version des Kanals entsprechend als ausgefallen markiert und der Prozess
beendet. Dadurch wird der Vertex der den Fehler erzeugt hat, neu ausgeführt und dann im
Anschluss an den beschädigten Kanal übergeben, welcher neu erstellt wurde. Somit hat ein
fehlerhafter Ausführungsdatensatz keine nicht-fehlerhaften Nachfolgedatensätze, so dass Fehler
nicht weiter propagiert werden müssen.
Wie im Abschnitt 3.6 beschrieben, wird jedem Vertex einem Stadium zugeordnet. Jedes Stadium
hat ein Managerobjekt, welches Rückinformationen (Callback) wie zum Beispiel
Zustandsübergänge eines Vertex in diesem Stadium oder einen Timer-Interrupt empfängt.
Innerhalb dieser Callback‘s hält der Stage-Manager einen globalen Lock auf dem Jobmanager
und kann dadurch Reaktionen implementieren. Zum Beispiel kann der Stage-Manager anhand
einer Heuristik Knoten identifizieren, die langsamer laufen als andere und im Scheduler die
Ausführungen verdoppeln. Dies verhindert, dass ein einzelner langsamer Computer zur
Verzögerung des gesamten Jobs führt.
5.2 OPTIMIERUNG ZUR LAUFZEIT
Wie im vorhergehenden Abschnitt beschrieben wird der Stage-Manager Callback-Mechanismus
benutzt um Laufzeitoptimierungsmaßnahmen zu implementieren.
In Abbildung 6 wird ein logischer Graph mit
einen Satz von Inputs zu einem DownstreamVertex zusammengefasst. In der Optimierung
wurde dies weiter entwickelt, indem eine neue
Schicht interner Knoten eingesetzt wurde. Jeder
interne Knoten liest vom Subset, die geschlossen
im Netzwerk vorliegen (z. B. auf dem gleichen
Computer oder im gleichen Rack). Durch die
Abbildung 6 – Optimierung (1) [IB07 S. 66]
12
Experimentelle Beurteilung
Datenreduktion welche die internen Vertices durchführen, kann der Gesamt-Traffic im Netzwerk
reduziert werden. Die Implementierung in Dryad erfolgt über einen benutzerspezifischen StageManager. Wenn dieser Manager die Callback Mitteilungen erhält, dass die aufwärts gerichteten
Vertices abgeschlossen sind, schreibt er dies in den Graph mit passenden Verfeinerungen. Die
Operation in Abbildung 6 kann rekursiv durchgeführt werden.
Analog hierzu gibt es eine Operation zur
„teilweisen Aggregation“ (vgl. Abbildung 7).
Hier wurden die Inputs in k-Sets gruppiert.
Durch die k-malige Replikation der Knoten
wurde es ermöglicht alle Sätze parallel zu
verarbeiten. Ein Beispiel für die Anwendung
dieser Technik ist im Data-Mining-Experiment
in Abschnitt 6.3 beschrieben.
Abbildung 7 – Optimierung (2) [IB07 S. 66]
Ein spezieller Fall der Optimierung kann zum Start durchgeführt werden, um die Größe der
Initialschicht eines Graphen festzulegen, so dass z.B. jeder Vertex mehrfache Inputs, jedoch
maximal so viele, dass alle auf dem gleichen Computer liegen, erhält.
Weil die Inputdaten auf mehrere Computer im Cluster verteilt werden können, ist der Computer,
auf welchem der Vertex geplant wurde, generell nicht deterministisch. Außerdem ist die Menge
der Daten, die in den Zwischenstadien geschrieben werden, gewöhnlich nicht bekannt, bevor
eine Berechnung beginnt. Folglich ist die dynamische Verfeinerung häufig leistungsfähiger als
der Versuch einer statischen Gruppierung.
Dynamische Verfeinerungen dieser Art betonen die Stärke der Überlagerung eines physischen
Graphen mit seinem Skelett. Für viele Anwendungen gibt es äquivalente Klasse von Graphen,
mit dem gleichen Aufbau, die das gleiche Ergebnis liefern.
6 EXPERIMENTELLE BEURTEILUNG
Dryad kann für eine Vielzahl von Anwendungen wie zum Beispiel relationale Abfragen, largescale Matrixberechnungen oder eine Vielzahl von Textverarbeitungs-Tasks benutzt werden. Im
Artikel von Isard (2009, Seite 67) wird die Effizienz von Dryad durch die Verarbeitung von zwei
Experimenten überprüft. Das erste Experiment überträgt eine SQL-Abfrage, wie im Abschnitt
2.1 beschrieben, in eine Dryad-Anwendung. Verglichen werden die Performance von Dryad und
einem herkömmlichen SQL-Server und die Leistung von Dryad bei der Verteilung des Jobs über
verschiedene Anzahlen von Computern. Das zweite Experiment ist eine einfache MapReduce
Datamining-Operation, welche in Dryad geschrieben und auf 10,2 TBytes Daten, unter
Verwendung eines Clusters von circa 1.800 Computern, ausgeführt wurde.
Es wurden die bekannten Kommunikationsflussgraphen adoptiert, partitionierbare Datensätze
hinzugefügt, die geleiteten parallelen Pipelines innerhalb der Prozesse genutzt und
Austauschoperationen zur Kommunikation von Teilergebnissen zwischen den Partitionen
angewendet.
6.1
VERWENDETE HARDWARE
Die SQL-Abfrageexperimente wurden auf einem Cluster von 10 Computern im Labor von
Microsoft Research ausgeführt. Die Datamining-Tests liefen auf einem Cluster von rund 1.800
Computern in einem angeschlossenen Rechenzentrum. Die Laborrechner verfügen über zwei
Dual-Core 2 GHz-Opteronprozessoren, 8 GB DRAM (jeweils 2 GB/CPU) und vier Festplatten.
Die 400 GByte Western Digital Festplatten (WD40 00 YR-01PLB0 SATA), sind mit einem
Experimentelle Beurteilung
13
Silicon Image 3114 PCI SATA Controller (66 MHz, 32-bit) verbunden. Die Netzwerkanbindung
ist 1 GBit/s, welche über einen non-blocking Switch erfolgt ist. Einer der Laborrechner wurde
zum SQL-Server und seine Daten wurden auf vier separaten 350 GB NTFS Volumes verteilt.
Jedes Laufwerk wurde mit dem SQL-Server für eigenes Datenstriping und temporäre Tabellen
konfiguriert. Alle anderen Laborcomputer wurden mit je einem 1,4 TByte NTFS-Volumes,
durch Softwarestriping über die 4 Laufwerke eingerichtet. Die Computer im Rechenzentrum
haben verschiedene Konfigurationen, sind aber ungefähr vergleichbar mit der Ausstattung der
Laborrechner. Alle Computer liefen unter Windows Server 2003 Enterprise x64 Edition mit SP1.
6.2 SQL-ABFRAGE EXPERIMENT
Die Abfrage für dieses Experiment ist in Abschnitt 2.1 beschrieben und benutzt den Dryad
Kommunikationsgraph aus Abbildung 2. Die Ausführungsmethode für den SQL-Server 2005
war ähnlich der Dryad-Berechnung, außer das ein externer Hash-Join für „Y“, anstatt des SortMerge, welches für Dryad benutzt wurde, verwendet worden ist. Der SQL-Server benötigte
geringfügig länger, als wenn für den Abfragetyp der Sort-Merge-Join erzwungen worden wäre.
Für das Experiment wurden zwei Varianten des Dryad-Graphen benutzt: „in-memory“ und „twopass“. In beiden Varianten ist die Kommunikation von Mi zu dem dazugehörigen Si nach Y
durch einen shared-memory FIFO erfolgt. Dieser zog vier Sorter in den gleichen Prozess, um
diese parallel auf den vier CPUs in jedem Computer auszuführen. Nur in der „in-memory“Variante erfolgte die Kommunikation von Di zu den vier korrespondieren Mj Vertices, auch
durch einen shared-memory FIFO und für die anderen Kanten von Di  Mk wurden TCP Pipes
benutzt. Die gesamte weitere Kommunikation erfolgte in beiden Varianten durch temporäre
NTFS-Dateien.
Die gute räumliche Lokalität in der Abfrage verbessert die Anzahl der Partitionen (n)
entscheidend. Für n=40 ergibt sich ein Durchschnitt von 80% von Di Ausgaben, was einhergeht
mit Mi, dies erhöht sich auf 88% für n=6. In anderen Varianten musste n groß genug sein, dass
jede Sort-Ausführung durch einen Vertex Si in den 8 GB DRAM des Computers passte. Mit den
gegenwärtigen Daten ist die Schwelle bei n=6.
Zu beachten ist, dass ein nicht-deterministisches Merge in M, zufällige Vertauschungen der
Ausgänge erzeugen kann, abhängig von der Reihenfolge der Eingänge auf dem Inputkanal. Dies
verletzt die Anforderung, dass alle Knoten deterministisch sind. Dadurch ergeben sich jedoch
keine Probleme für das Modell der Fehlertoleranz, weil Sort Si diese Veränderung „undoes“ und
da die Kante von Mi zu Si ein shared-memory FIFO in einem einzelnen Prozess ist, fallen die
zwei Vertex (wenn überhaupt) gleichzeitig aus und der Nicht-determinismus wird niemals
„escapen“.
Die in-memory Variante erfordert mindestens n Computer, weil andernfalls die S-Vertices auf
die Daten vom X-Vertex warten und es zum Deadlock kommt. Die „two-pass“-Variante läuft auf
einer beliebigen Anzahl von Computern. Ein Möglichkeit um diesen Zielkonflikt zu vermeiden
ist das Hinzufügen der Dateipufferung in der „two-pass“-Variante. Dadurch verwandelt sich die
Anwendung in eine Art zweiten Durchlauf. Zu beachten ist, dass die Umwandlung der „inmemory“- in die „two-pass“-Variante, einfach zwei Linien im Graph Konstruktionscode ändert,
ohne Änderungen in den Vertex Programmen.
Die „two-pass“-Variante wurde unter Verwendung von n=40 ausgeführt und die Anzahl der
Computer wurde zwischen eins bis neun variiert. Bei der „in-memory“-Variante wurde n=6 bis
einschließlich n=9 auf n Computern verwendet. Zum Vergleich wurde die Abfrage auf einem gut
optimierten SQL-Server laufen gelassen.
Computer
SQL-Server
Two-pass
In-memory
1
3780
2370
2
3
4
5
6
7
8
9
1260
836
662
523
463
217
423
203
346
183
321
168
Tabelle 1: Zeit in Sekunden zur Verarbeitung der SQL-Abfrage, bei Verwendung von n Computern [IB07 S. 67]
14
Experimentelle Beurteilung
Die Tabelle zeigt die benötigte Zeit in Sekunden für jedes Experiment. Bei den wiederholten
Durchläufen waren die durchschnittlichen Abweichungen bei 3,4%, ausgenommen der
Einzelcomputer bei dem die „two-pass“-Variante bei 9,4% war.
Abbildung 8 stellt diese Zeiten invers grafisch dar,
normalisiert um den Beschleunigungsfaktor im Verhältnis
zum „two-pass“-Einzelrechnerfall. Der „two-pass“-Dryadjob
arbeitet auf allen Clustergrößen, mit einer linearen
Beschleunigung. Die in-memory Variante arbeitet wie
erwartet für n=6 und mehr, ebenfalls mit einer linearen
Beschleunigung, aber ungefähr doppelt so schnell wie die
„two-pass“- Variante. Der SQL-Server bestätigt die
Erwartungen. Das spezialisierte Dryad Programm läuft
8 – Beschleunigungsfaktoren
signifikant schneller als die SQL-Server-Queryengine. Zu Abbildung
[IB07 S. 68]
beachten ist, das Dryad eine einfache Ausführungsmaschine
liefert, wohingegen die Datenbank viel mehr Funktionen (Logging, Transaktionen und
mutierbare Relationen) liefert.
6.3 DATA MINING EXPERIMENT
Das Datenerhebungsexperiment lehnt sich an das Muster von Map und Reduce an. Der Zweck
dieses Experimentes ist zu prüfen, ob Dryad bei großen Datenmengen leistungsstark gut genug
arbeitet.
Dieses Experiment liest die Logdateien, die durch
den MSN Suchservice erstellt werden ein, extrahiert
den Abfragestring und errichtet ein Histogramm der
Abfragesequenz. Der allgemeine Berechnungsgraph
ist in Abbildung 9 dargestellt. Die Logdateien sind
repliziert über die Festplatten verteilt. Jeder der P
Graphen liest seinen Part der Logdateien ein und
analysiert diesen, um den Abfragestring zu
extrahieren. Daraus folgende Einzelteile sind
Libary-Tupel, welche den Abfragestring, einen
Count und einen Hash des Strings enthalten. Jeder
D-Vertex verteilt auf k, basierend auf dem
Abfragestring-Hash, Outputs. S führt ein „inmemory Sort“ durch. C fasst die Counts für jede Abbildung 9 – Graph Data Mining Experiment [IB07
Query zusammen und MS führt ein streaming S. 69]
merge-sort durch. S und MS kommen von einer
Vertex-Bibliothek und nehmen eine Vergleichsfunktion als Parameter. In diesem Fall sortieren
sie, basiert auf dem Abfrage-Hash. Einfache Knoten wurden in Subgraphen gekapselt (in der
Abbildung 9 durch Rechtecke dargestellt) und damit die Gesamtzahl der Knoten im Job
reduziert.
Wie der Graph in Abbildung 9 zeigt, ist dieser nicht gut für große Datenmengen skalierbar. Es ist
kostspielig einen eigenen Q-Vertex für jeden Input durchzuführen. Jeder Teil umfasst nur ca.
100 MBytes, und der P Vertex führt eine erhebliche Datenverdichtung durch, so dass die Menge
der Daten, welche durch die S Vertices sortiert werden müssen erheblich geringer ist, als RAM
auf dem Computer. Auch hat jeder Sub-Graph R n Inputs und wenn n auf 100 oder 1.000
ansteigt, wird es schwierig in so vielen Kanälen gleichzeitig zu lesen.
Nachdem
Microsoft
einige
unterschiedliche
Verkapselungen
und
dynamische
Verfeinerungsentwürfe versucht hatte, kam man zum Kommunikationsgraph wie er in
Abbildung 10 dargestellt ist. Jeder Sub-Graph hat jetzt in der ersten Phase mehrfache Eingänge,
welche automatisch durch die Verfeinerung in Abbildung 7 gruppiert werden um sicherzustellen,
dass alle auf dem gleichen Computer liegen. Die Eingänge werden über den Parser P zu einem
Experimentelle Beurteilung
15
nicht-deterministischen Merge-Vertex M geschickt. Die Verteilung (Vertex D) ist aus der ersten
Phase herausgenommen worden, um einer anderen Schicht die Gruppierung und Anhäufung zu
erlauben (wieder unter Verwendung der Verfeinerung in Abbildung 7), bevor die Anzahl der
Output-Kanäle explodiert.
Dieses Experiment wurde in einem Rechenzentrum auf Cluster mit 1.800 Computern und mit
10.160.519.748 Bytes Inputdaten durchgeführt. Der Input wurde in 99.713 Elemente unterteilt
und über die Computer verteilt. Es wurde spezifiziert, dass die Applikation 450 R Sub-Graphen
benutzen soll. In der ersten Phase wurden Gruppen von max. 1 GB Inputs erstellt, die alle auf
dem gleichen Computer liegen, was im Ergebnis zu 10.405 Q' Sub-Graphen führt, die insgesamt
153.703.445.725 Bytes schreiben. Die Ausgänge der Q' Sub-Graphen wurden in höchstens
600 MB große Elemente auf dem gleichen Switch gruppiert, was zu 217 T Subgraphen führte.
Jedes T war mit jedem R Subgraph verbunden und diese schrieben 118.364.131.628 Byte. Die
gesamte Ausgabe der R Subgraphen war 33.375.616.713 Bytes und die Gesamtlaufzeit betrug
11 Minuten und 30 Sekunden. Für dieses Experiment wurden nur 11.072 Vertices benutzt.
Vergleichbare Experimente mit anderen Diagramm-Topologien bestätigten, dass Dryad
erfolgreich Jobs mit mehreren tausend Knoten ausführen kann.
6.3.1 ZUSAMMENFASSUNG DES OPTIMIERUNGSPROZESSES




An keinem Punkt während der Optimierung wurde der Code, der innerhalb der Vertices
läuft, abgeändert. Modifiziert wurde nur der Kommunikationsflussgraph des Jobs.
Der Kommunikationsgraph ist zu jeder möglichen Map-Reduce Berechnung mit
ähnlichen Eigenschaften angepasst: z.B. dass die Map-Phase (P-Vertex) erhebliche
Datenverdichtung durchführt und die Reduce-Phase (C Vertex) führt zusätzlich eine
etwas kleinere Datenverdichtung durch. Eine andere Topologie könnte bessere Leistung
für den Map-Reduce-Task mit anderem Verhalten geben; zum Beispiel wenn die ReducePhase durchgeführt ist, kann durch die erhebliche Datenverdichtung ein dynamischer
Baum wie in Abbildung 6 idealer sein.
Eine Erweiterung der Skalierung oder eine Änderung der Topologie (z.B. indem mehr
Schichten zwischen T und R eingeführt werden), ist durch Restrukturierung einfach
umzusetzen.
Eine gute Performance für große Datenverarbeitungsberechnungen zu erzielen ist nicht
einfach. Viele neue Eigenschaften im Dryad-System, einschließlich der SubgraphKapselung und der dynamischen Verfeinerung wurden dazu benutzt. Diese machen es
einfach mit verschiedenen Optimierungsschemen zu experimentieren. Unter
Verwendungen eines einfachen Systems wäre die Verwendung schwierig bzw.
unmöglich gewesen.
Abbildung 10 – Optimierungsprozess [IB07 S. 69]
16
Auf Dryad basierend
7 AUF DRYAD BASIEREND
Wie in der Einleitung ausgeführt, ist Dryad auf Entwickler, die Erfahrung im Umgang mit
höheren Programmiersprachen besitzen, ausgerichtet. In einigen Bereichen ist es von Vorteil,
wenn man große Datenverarbeitungsaufgaben einfacher abbildet, da dies auch den „NichtEntwicklern“ erlaubt den Datenspeicher direkt abzufragen. Microsoft hat mit Dryad eine
Plattform geschaffen, auf welcher es sehr viele Einschränkungen gibt, gleichzeitig aber einfache
Schnittstellen und zum anderen wurden innerhalb von Microsoft bereits Systeme entwickelt.
7.1 DIE SKRIPTSPRACHE „NEBULA“
NEBULA ist ein Skriptinterface, welches Dryad überlagert. Es erlaubt dem Anwender eine
Berechnung als Serie von Stadien zu spezifizieren (analog zu den Dryad-Stadien), wobei jedes
Stadium einen oder mehrere Inputs des vorherigen Stadiums aufnimmt. Mittels des NebulaFrontends ist es dem User möglich einen Job zu beschreiben. Diese Jobbeschreibung wird in ein
Nebula-Skript umgewandelt und unter Verwendung von Dryad ausgeführt. Nebula wandelt
Dryad in einen verallgemeinerten Unix Pipelinemechanismus und erlaubt dem Programmierer
riesige azyklische Graphen über mehrere Computer zu spannen. Der Nebula-Layer in
Kombination mit einer Perl-Wrapper-Funktion ist sehr effektiv für die Verarbeitung großer
Textmengen. Die Skripte laufen gewöhnlich auf tausenden von Computern und enthalten 515 Stadien, einschließlich mehrfacher Projektionen, Aggregationen und Joins. Nebula verbirgt
die meisten Details des Dryad-Programms vor dem Entwickler. Die Stadien werden mit den
vorhergehenden Stadien durch Operatoren verbunden, die implizit die Anzahl der erforderlichen
Knoten bestimmen. Bei der Implementierung der Nebula-Operatoren werden dynamische
Optimierungen benutzt, wie sie im Abschnitt 5.2 beschrieben sind. Durch die Abstraktion der
Operatoren, ist es nicht notwendig, dass der Entwickler Details dieser Optimierungen kennt. Alle
Nebula-Knoten führen einen Prozess-Wrapper aus, wie er in Abschnitt 4 beschrieben ist.
7.2 INTEGRATION MIT SQL SERVER INTEGRATION SERVICES (SSIS)
SSIS ist der Nachfolger der Data Transformation Services (DTS) und unterstützt die
Workflowbasierte Anwendungsprogrammierung auf einem Single-Instanz SQL-Server. Das
AdCenter-Team in MSN hat ein System entwickelt, welches mit Unterstützung von Dryad lokale
SSIS-Berechnungen in größere verteilte Kommunikationsgraphen, Scheduling und
Fehlertoleranz einbettet. Der SSIS Eingangsgraph kann auf einem Einzelplatzrechner, unter
Verwendung der vollständigen Verfügbarkeit der SQL-Entwicklerwerkzeuge erstellt und getestet
werden. Dies schließt einen graphischen Editor zur Erstellung der Jobtopologie und einem
integrierten Debugger ein. Wenn der Graph bereit ist, um auf einem größeren Cluster zu laufen,
verteilt ihn das System automatisch unter Verwendung einer Heuristik und errichtet ein DryadDiagramm, das dann in verteilter Form ausgeführt wird. Jeder Dryad Vertex ist eine Instanz des
SQL-Servers, der als SSIS Subgraph des kompletten Jobs läuft. Dieses System wird aktuell in
einem Produktivsystem als ein Teil von AdCenters Log-Verarbeitungspipelines entwickelt.
7.3 VERTEILTE SQL-ABFRAGEN
Eine zusätzliche Möglichkeit ist den Abfrageoptimierer für SQL oder LINQ anzupassen, damit
Pläne direkt in das Dryad-Flussdiagramm, unter Verwendung der passenden parametrisierten
Vertices für relationale Operationen, kompiliert werden könnten. Da das Fehlertoleranzmodell
nur erfordert, dass Inputs über die Dauer der Abfrage unabänderlich sein müssen, würde jedes
Dryad vs. MapReduce
17
darunterliegende Speichersystem, welches Snapshots anbietet, erlauben gleichbleibende
Abfrageergebnisse zu liefern. Dieses Themengebiet wird von Microsoft in der Zukunft weiter
betrachtet.
8 DRYAD VS. MAPREDUCE
Dryad wurde hauptsächlich für große Datenerhebungen und Auswertungen über Cluster von
tausenden Computern entworfen. Infolgedessen, teilt es sehr viele Ähnlichkeiten mit Google’s
MapReduce, welches ein vergleichbares Problemgebiet adressiert. Der grundlegende
Unterschied zwischen den beiden Systemen ist, dass Dryad die DAG-Kommunikation (directed
acyclic graph) eher spezifizieren kann, als die erforderliche Reihenfolge der Sequenz
Map  Verteilen  Sort  Reduce Operationen. Insbesondere können Knoten mehrfache
Eingänge von verschiedenen Typen besitzen und auch mehrfache Ausgänge von verschiedenen
Typen erzeugen. Für viele Anwendungen vereinfacht dies das Mapping vom Algorithmus zur
Implementierung. Dadurch lassen sich größere Bibliotheken der zugrundeliegenden Subroutinen
erstellen und zusammen mit der Möglichkeit TCP Pipes und shared-memory für Kanten
ausnutzen. Dadurch kann man erhebliche Leistungsgewinne erzielen. Gleichzeitig ist die
Implementierung allgemein genug, um alle Eigenschaften zu unterstützen die MapReduce
ermöglicht.
18
Literaturverzeichnis
LITERATURVERZEICHNIS
[IB07]
ISARD, M.; BUDIU M.: Dryad: Distributed Data-Parallel Programs from Sequential
Building Blocks. In EuroSys´07, 21.-23.März 2007, Lissabon, Portugal, S. 59-72
[WIKI]
Wikipedia: Festplattenlaufwerk, http://de.wikipedia.org/wiki/Festplatte,
Abrufdatum: 03.05.2011
[ZDNet]
ZDNet.de: Microsofts Parallel-Computing-Plattform Dryad erscheint 2011,
http://www.zdnet.de/news/wirtschaft_investition_software_microsofts_parallel_co
mputing_plattform_dryad_erscheint_2011_story-39001022-41536476-1.htm,
Abrufdatum: 11.03.2011
[SDSS]
Sloan Digital Sky Survey / SkyServer, http://skyserver.sdss.org
Abrufdatum: 12.03.2011
Oliver Kring, Thema 7: DryadLINQ
1/25
FernUniversität in Hagen
Seminar 01912
im Sommersemenster 2011
„MapReduce und Datenbanken“
Thema 7
DryadLINQ
Referent: Oliver Kring
Oliver Kring, Thema 7: DryadLINQ
2/25
Inhaltsverzeichnis
1 Überblick und Motivation............................................................................................................3
2 Einführung in LINQ.....................................................................................................................4
2.1 Was ist LINQ?......................................................................................................................4
2.2 Ein einfaches Beispiel..........................................................................................................5
2.3 Delegaten und Lambda-Ausdrücke......................................................................................5
2.4 Die Schnittstelle „IEnumerable“..........................................................................................7
2.5 Die Schnittstelle „IQueryable“.............................................................................................8
2.6 Weitere Beispiele..................................................................................................................9
3 Einführung in DryadLINQ.........................................................................................................10
3.1 Was ist DryadLINQ?..........................................................................................................10
3.2 Aufgaben von DryadLINQ.................................................................................................10
3.3 Abarbeitung von Queries....................................................................................................11
3.4 Datenpartitionierung..........................................................................................................12
4 Die DryadLINQ-API.................................................................................................................14
4.1 Die Klasse „PartitionedTable“...........................................................................................14
4.2 LINQ-Operatoren...............................................................................................................15
4.3 DryadLINQ-Operatoren.....................................................................................................15
4.4 DryadLINQ-Attribute........................................................................................................15
4.5 Einschränkungen unter DryadLINQ..................................................................................16
5 Weitere Beispiele in DryadLINQ...............................................................................................17
5.1 MapReduce........................................................................................................................17
5.2 Aggregatfunktionen............................................................................................................18
5.3 Joins...................................................................................................................................19
6 Optimierung von Anfragen durch DryadLINQ.........................................................................20
6.1 Statische Optimierungen....................................................................................................20
6.2 Dynamische Optimierungen..............................................................................................20
6.3 Optimierungen für „OrderBy“...........................................................................................21
6.4 Optimierungen für „MapReduce“......................................................................................22
7 Zusammenfassung und Ausblick...............................................................................................23
8 Abbildungsverzeichnis...............................................................................................................24
9 Literaturverzeichnis...................................................................................................................25
Oliver Kring, Thema 7: DryadLINQ
3/25
1 Überblick und Motivation
Die Antwort des Branchenriesen Microsoft auf Googles „MapReduce“-Framework [1] lautet
„Dryad“. Wie bereits im vorangegangenen Vortrag berichtet, stellt „Dryad“ eine
Laufzeitumgebung bereit, welche komplexe Datenbankabfragen in Clustern effizient ausführen
kann. Steht eine solche Laufzeitumgebung erst einmal zur Verfügung stellt sich jedoch noch die
Frage, wie Abfragen formuliert werden können, so dass die Laufzeitumgebung diese Abfragen
„versteht“ und effektiv in der verteilten „Dryad“-Umgebung abwickeln kann.
Eine von Microsofts Antworten auf diese Frage lautet „DryadLINQ“. Diese Abfragesprache
ermöglicht Applikationsentwicklern basierend auf der im .NET-Framework integrierten „LINQ“Syntax, Abfragen in einer beliebigen .NET-Sprache zu erstellen und „wie gewohnt“ auszuführen.
Kenntnisse über die zugrundeliegende Laufzeitumgebung werden dabei zunächst nicht benötigt.
Im Folgenden soll die Abfragesprache „DryadLINQ“ vorgestellt und die zugrundeliegenden
Konzepte genauer beleuchtet werden. Dazu jedoch müssen zunächst die notwendigen
Grundlagen von LINQ erarbeitet werden, einer in die .NET-Sprachen eingebettete
Abfragesprache mit eigener Syntax zur Formulierung allgemeiner Abfrageprobleme.
Oliver Kring, Thema 7: DryadLINQ
4/25
2 Einführung in LINQ
2.1 Was ist LINQ?
Die imperativen Programmiersprachen stellen keine besonderen Konstrukte zur Verfügung, um
Abfragen auf Datensätzen zu formulieren. Solche Abfragen werden in den objektorientierten
Sprachen im Allgemeinen dadurch realisiert, dass z.B. über Collections iteriert wird und dabei
die Elemente mit den gewünschten Eigenschaften herausgefiltert werden. Möchte man Abfragen
auf relationalen Datenbanken durchführen, so werden häufig SQL-Anweisungen in Form von
Strings im Programm codiert oder in Konfigurationsdateien hinterlegt und dann während der
Laufzeit ausgeführt. Liegen die Informationen in Dateiform vor, so müssen diese Dateien
zunächst geparst und die Informationen in geeigneter Weise weiterverarbeitet werden.
Die Nachteile dieser Verfahren liegen auf der Hand:
•
Erweiterbarkeit und Wartbarkeit (z.B. bei Änderung der Datenquelle),
•
mangelhafte Typsicherheit,
•
Überprüfung des Abfrage-Codes häufig erst zur Laufzeit möglich,
•
keine Compiler-Optimierungen,
•
Abfragen werden häufig unübersichtlich oder sind nicht intuitiv formuliert.
Um diese Probleme zu vermeiden, hat Microsoft ab dem .NET-Framework 3.5 die LINQ-Syntax
eingebettet. Die Abkürzung „LINQ“ steht dabei für „Language Integrated Query“. LINQ hat
dabei folgende Eigenschaften:
•
Der Code ist stark typisiert,
•
der Code wird vom Compiler geprüft, nicht erst zur Laufzeit,
•
die Syntax ist vergleichsweise intuitiv und dabei an SQL angelehnt,
•
ein Providermodell erlaubt die Anbindung verschiedener Datenquellen (z.B. Collections,
SQL-Datenbanken, XML-Dateien),
•
der anbietende Provider kann den Abfragecode optimieren.
In Folgenden sollen die Grundzüge von LINQ an einigen Beispielen erarbeitet werden. Die
Beispiele stammen von [4], [6] und [7].
Oliver Kring, Thema 7: DryadLINQ
5/25
2.2 Ein einfaches Beispiel
Im folgenden LINQ-Beispiel werden alle Produkte aufgelistet, deren Name mit einem „A“
beginnt und nach ihrer ID sortiert:
var query = from p in products
where p.Name.StartsWith("A")
orderby p.ID
select p;
Die hier vorgestellte „LINQ Query-Syntax“ ist stark an SQL angelehnt. Obwohl sie in der
Literatur für gewöhnlich zuerst vorgestellt wird, bietet sie nicht den vollen Funktionsumfang von
LINQ. Dieser kommt erst durch die Verwendung von Erweiterungsmethoden mit LambdaAusdrücken voll zur Geltung.
Das obige Beispiel kann unter Verwendung von Erweiterungsmethoden wie folgt umgeschrieben
werden:
var query = products
.Where(p => p.Name.StartsWith("A"))
.OrderBy(p => p.ID);
Zu nennen ist im Zusammenhang mit LINQ das Konzept der „späten Ausführung“. Eine LINQAbfrage wird nicht ausgeführt, wenn sie formuliert wird, sondern erst dann, wenn im darauf
folgenden Code (z.B. in einer „for“-Schleife) auf die Ergebnisse der Abfrage zugegriffen wird.
2.3 Delegaten und Lambda-Ausdrücke
Ähnlich wie in „C“ lässt sich auch in .NET eine Funktion referenzieren. Ein Delegat ist dabei ein
Objekt, welches eine Referenz auf eine Funktion enthält [5][6]. Zusätzlich besitzt der Delegat in
Form seines (generischen) Typs Informationen über die Signatur der Funktion, so dass die
Typprüfung des Compilers erhalten werden kann. Mittels eines Delegaten lassen sich also
Referenzen auf Funktionen typsicher zwischen verschiedenen Objekten übergeben.
Oliver Kring, Thema 7: DryadLINQ
6/25
Beispiel:
public static void Main()
{
// Instantiate delegate to reference ExtractWords method
Func<string, int, string[]> extractMethod = ExtractWords;
string title = "The Scarlet Letter";
}
// Display result
foreach (string word in extractMethod(title, 5))
Console.WriteLine(word);
private static string[] ExtractWords(string phrase, int limit)
{
...
}
Die Variable „extractMethod“ enthält also den Delegaten der Methode „ExtractWords“ vom Typ
„Func<string, int, string[]>“. Der Delegat repräsentiert damit die Methode „ExtractWords“ mit 2
Eingabe-Parametern (vom Typ „string“ und „int“) und dem Ausgabe-Parameter (vom Typ
„string[]“).
„Bei einem Lambda-Ausdruck handelt es sich um eine anonyme Funktion, die Ausdrücke und
Anweisungen enthalten und für die Erstellung von Delegaten oder Ausdrucksbaumstrukturen
verwendet werden kann.“ [6]. Ein Lambda-Ausdruck wird dabei durch die Verwendung des
Lambda-Operators „=>“ gekennzeichnet.
Im Beispiel aus Kapitel 2.2 werden an zwei Stellen Lambda-Ausdrücke verwendet:
: Ein Produkt p „wird abgebildet“ auf einen
•
p => p.Name.StartsWith("A")
•
bool'schen Ausdruck
p => p.ID : Ein Produkt p „wird abgebildet“ auf seine ID
In beiden Fällen wird durch den Lambda-Ausdruck eine anonyme Funktion definiert, die auch
einem Delegaten zugewiesen werden kann:
Func<int, bool> myFunc = x => x == 5;
bool result = myFunc(4); // returns false of course
Lambda-Ausdrücke unterliegen ebenfalls einer starken Typprüfung durch den Compiler.
Oliver Kring, Thema 7: DryadLINQ
7/25
2.4 Die Schnittstelle „IEnumerable“
Die Abfragesprache „LINQ“ wird in der Schnittstelle „IEnumerable“ bereitgestellt. Sie stellt eine
Iterator-Schnittstelle dar, durch die z.B. die Elemente einer Collection durchlaufen werden
können. „IEnumerable“ wird von allen .NET-Collections implementiert, aber auch von
Datencontainern, die aus anderen Datenquellen stammen (so z.B „Table<T>“, welches eine
SQL-Datenbanktabelle repräsentiert). Die folgende Übersicht gibt einen Überblick über den
bereitgestellten Funktionsumfang (vgl. [6],[9]):
c la s s L IN Q In te r fa c e s
« in te rfa c e »
IE n u m b e ra b le < T >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
A g g re g a te ()
A l l( )
A n y ()
A v e ra g e ()
C o n ta in s ()
C o u n t ()
D i s t i n c t( )
E x c e p t()
F irs t()
F i r s t O r D e f a u l t( )
G ro u p B y ()
G ro u p J o in ()
In t e r s e c t ( )
J o in ()
L a s t()
L a s tO rD e fa u lt()
L o n g C o u n t()
M a x ()
M in ()
O rd e rB y ()
O rd e rB y D e s c e n d in g ()
R e v e rs e ()
S e le c t()
S e le c tM a n y ()
S e q u e n c e E q u a l()
S in g le ()
S i n g l e O r D e f a u l t( )
S k ip ()
S k ip W h ile ()
S u m ()
T a ke ()
T a k e W h ile ()
U n io n ()
W h e re ( )
« in te rfa c e »
IQ u e ry a b le < T >
Abb. 1: Die Schnittstellen IEnumerable<T> und IQueryable<T>
Oliver Kring, Thema 7: DryadLINQ
8/25
Auffallend ist die Ähnlichkeit zur Abfragesprache „SQL“. Da aber die LINQ-Syntax der starken
Typprüfung durch den Compiler unterliegt, soll im Folgenden eine Auswahl an Funktionen, die
es auch unter SQL in ähnlicher (aber untypisierter) Form gibt, kurz erläutert werden:
•
Where<TSource>(Func<TSource, Boolean>) - filtert die Elemente des Iterators. Die
Elemente des Iterators sind vom Typ „TSource“. Das Filterkriterium wird durch einen
Delegaten von Typ „Func<TSource, Boolean>“ definiert.
•
Select<TSource, TResult>(Func<TSource, TResult>) - Projektion von Daten des Typs
„TSource“ auf Daten des Typs „TResult“. Die Projektionsvorschrift wird durch einen
Delegaten von Typ „Func<TSource, TResult>“ definiert.
•
OrderBy<TSource, TKey>(Func<TSource, TKey>) - sortiert die Elemente des Iterators
mit Elementen des Typs „TSource“. Der Sortierschlüssel ist von Typ „TKey“. Das
Sortierkriterium wird durch einen Delegaten von Typ „Func<TSource, TKey>“ definiert.
•
Min<TSource>() - Aggregatfunktionen über einen Iterator vom Typ „TSource“ .
•
Join<TOuter, TInner, TKey, TResult> (IEnumerable <TInner>, Func<TOuter, TKey>,
Func<TInner, TKey>, Func<TOuter, TInner, TResult>) - "Join" zwischen zwei
Iteratoren.
2.5 Die Schnittstelle „IQueryable“
Im Gegensatz zu „IEnumerable“, welches einen Iterator repräsentiert, stellt die Schnittstelle
„IQueryable“ eine Abfrage dar, die z.B. serialisiert und damit auch an nahezu beliebige
Empfänger übergeben werden kann. IQueryable verwendet selbst keine Delegaten, sondern
serialisierbare Expressions zur Definition der Abfrage. IQueryable ist aber von IEnumerable
abgeleitet und konvertiert Delegaten implizit in Expressions, so dass der Anwender von diesen
Konvertierungen nichts bemerkt und wie gewohnt mit LINQ arbeiten kann.
Oliver Kring, Thema 7: DryadLINQ
9/25
2.6 Weitere Beispiele
Im Folgenden werden weitere Beispiele dargestellt, wie Abfragen in LINQ formuliert werden
können. Diese und viele andere Beispiele finden sich unter [7].
•
Auswählen der ersten drei Elemente eines Iterators:
int[] numbers = { 5, 4, 1, 3, 9, 8, 6, 7, 2, 0 };
var first3Numbers = numbers.Take(3);
•
Überprüft, ob eine Zahl im Array an seiner „natürlichen“ Position steht (Verwendung des
„indizierten Selects“):
int[] numbers = { 5, 4, 1, 3, 9, 8, 6, 7, 2, 0 };
var numsInPlace =
numbers.Select((num, index) =>
new { Num = num, InPlace = (num == index) });
•
Bestimmung der Anzahl unterschiedlicher Faktoren der Zahl 300 (man beachte die
Anlehnung an SQL):
int[] factorsOf300 = { 2, 2, 3, 5, 5 }
int uniqueFactors = factorsOf300.Distinct().Count();
Oliver Kring, Thema 7: DryadLINQ
10/25
3 Einführung in DryadLINQ
LINQ stellt Sprachelemente zur Verfügung, mit deren Hilfe effizient und in relativ intuitiver
Form Abfragen auf Datencontainern durchgeführt werden können. Im Zusammenhang mit
DryadLINQ stellt sich die Frage, was DryadLINQ überhaupt ist, welche Funktionen es
übernimmt und wie es diese ausführt. Diese Fragen sollen in den folgenden Abschnitten
beantwortet werden. Die Ausführungen, Beispiele und Abbildungen basieren im Wesentlichen
auf den Abhandlungen [2], [3], [8], [9] und [10].
3.1 Was ist DryadLINQ?
•
DryadLINQ ist ein Compiler, welcher LINQ-Code so übersetzt, dass dieser verteilt auf
einem Dryad-CLuster ausgeführt werden kann. DryadLINQ kapselt dabei sämtliche
Details der Dryad-Plattform. Der Anwendungsprogrammierer wird dadurch von genauen
Kenntnissen über Dryad befreit und somit entlastet. Sein Code bleibt somit transparent
für andere Entwickler.
•
DryadLINQ ist ein LINQ-Provider, der Query-Objekte in LINQ-Syntax entgegennimmt,
übersetzt, optimiert und verteilt im Dryad-Cluster ausführt.
3.2 Aufgaben von DryadLINQ
Für die Ausführung einer Anfrage (Query) in einem Dryad-Cluster muss DryadLINQ die
folgenden Aufgaben durchführen:
•
Entgegennahme der Query (IQueryable),
•
Zerlegung der Query in Sub-Queries und Erstellen eines Query-Plans,
•
Generierung des Query-Codes und Compilierung in .NET-Assembys, die auf dem Cluster
verteilt werden können,
•
Erstellen eines Job-Managers, der den Dryad-Graphen erzeugt und die Ausführung des
Jobs im Cluster steuert,
•
Rückgabe der Query-Ergebnisse.
Oliver Kring, Thema 7: DryadLINQ
11/25
3.3 Abarbeitung von Queries
Die folgende Abbildung verdeutlicht die Abarbeitung von Abfragen durch DryadLINQ:
Abb. 2: Architektur von DryadLINQ (Quelle: Microsoft [8])
(1) Eine .NET-Anwendung läuft und erstellt eine LINQ-Abfrage auf einem DryadLINQObjekt.
(2) Bei Zugriff auf das Ergebnis der Abfrage wird der LINQ-Ausdruck an den DryadLINQCompiler übergeben.
(3) Der DryadLINQ-Compiler compiliert den LINQ-Audrucksbaum in einen verteilten
Ausführungsgraphen.
(4) DryadLINQ erstellt einen speziellen Job-Manager auf dem Cluster.
(5) Der Job-Manager erzeugt den Dryad-Job.
(6) Der Dryad-Job wird im Cluster verteilt ausgeführt.
(7) Die Ausgabe-Daten werden geschrieben.
(8) Die Kontrolle wird an DryadLINQ zurückgegeben. DryadLINQ liest die Ausgabedaten
ein.
(9) Die Resultate werden in Form von .NET-Objekten an die Anwendung zurückgegeben, die
diese dann weiter verarbeiten kann.
Oliver Kring, Thema 7: DryadLINQ
12/25
3.4 Datenpartitionierung
Eine grundlegende Eigenschaft großer Cluster ist, dass die Daten nicht auf jedem Rechner
vorliegen, sondern der Datenbestand in partitionierter Form auf den Rechnern vorzufinden ist.
Das folgende Beispiel soll dies verdeutlichen:
Gegeben sei folgende Konfiguration: 4 Maschinen - eine Datei, die in 5 Teilen auf die 4
Maschinen aufgeteilt wird.
Abb. 3: Partitionierung von Dateien (Quelle: Microsoft [8])
Eine Maschine „m0“ enthält Metadaten, also Informationen darüber, wie und wo Partitionen des
gesamten Datenbestands im Cluster gespeichert sind. Die Maschinen „m1“ bis „m4“ enthalten
jeweils einen Teil des Datenbestands, z.T. auch redundant zu anderen Maschinen.
Auf der vorgenannten Konfiguration wird nun folgende Query erstellt:
public static IQueryable<string> Match(string directory, string filename,
string tosearch)
{
string uri = "file://" + directory + "/" + filename);
PartitionedTable<LineRecord> table =
PartitionedTable.Get<LineRecord>(uri);
return table.Select(s => s.line).Where(s => s.IndexOf(tosearch) >= 0);
}
Oliver Kring, Thema 7: DryadLINQ
13/25
Dieser Job wird dann auf vier Maschinen gleichzeitig ausgeführt (Abb. 4). Dank der Klasse
“PartitionedTable” sowie des dahinter liegenden DryadLINQ-Providers „merkt“ das C#Programm nichts von den partitionierten Daten und arbeitet wie gewohnt auf einem
"IEnumerable"-Iterator.
Abb. 4: Ausführungsplan (Quelle: Microsoft [8])
Mit DryadLINQ können Daten neu partitioniert werden, d.h. neu auf den Knoten verteilt werden.
DryadLINQ untertützt dabei folgende Verfahren:
•
Hash-Partitionierung: Gleichmäßige Aufteilung der Daten auf Partitionen auf Basis der
Hash-Werte in einer oder mehrerer Spalten
•
Range-Partitionierung: Gleichmäßige Aufteilung der Daten auf Partitionen auf Basis des
Wertebereichs einer oder mehrerer Spalten
Oliver Kring, Thema 7: DryadLINQ
14/25
4 Die DryadLINQ-API
DryadLINQ erweitert LINQ um Klassen und Datentypen zur Unterstützung der parallelen
Ausführung von Abfragen auf Basis der Dryad-Platform. Im Folgenden soll ein kurzer Überblick
über diese Klassen und Datentypen gegeben werden. Die Angaben basieren auf den Texten [9]
und [10].
4.1 Die Klasse „PartitionedTable“
Über die Klasse „PartitionedTable“ kann auf persistente Daten, welche verteilt vorliegen,
zugegriffen werden. „PartitionedTable“ leitet von IQueryable ab. Somit können Abfragen auf
verteilten Daten in der gleichen Weise vorgenommen werden, wie in bekannter Weise unter
LINQ auf zusammenhängenden Daten.
Im Beispiel in Kapitel 3.4 wurde bereits mit Hilfe dieser Klasse eine Abfrage erstellt.
c la s s D ry a d L IN Q C la s s e s
P a r titio n e d T a b le < T >
« in te rfa c e »
IE n u m b e r a b le < T >
« in te rfa c e »
IQ u e ry a b le < T >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
A g g re g a te A sQ u e ry ( )
A llA sQ u e ry ()
A n y A sQ u e ry ( )
A p p ly ()
A v e ra g e A sQ u e ry ( )
C o n ta in sA s Q u e ry ( )
C o u n tA s Q u e ry ( )
F irstA sQ u e ry ( )
F irstO rD e fa u ltA sQ u e ry ( )
F o rk ( )
H a sh P a rtitio n ()
L a st A sQ u e r y ( )
L a stO rD e fa u lt A sQ u e ry ( )
L o n g C o u n tA s Q u e ry ( )
M a x A sQ u e ry ( )
M in A sQ u e ry ()
R a n g e P a rtiti o n ()
S e q u e n c e E q u a lA sQ u e ry ( )
S in g le A sQ u e ry ()
S in g le O rD e fa u l tA sQ u e ry ()
S u m A sQ u e ry ( )
Abb. 5: Die Klasse PartitionedTable<T>
Oliver Kring, Thema 7: DryadLINQ
15/25
4.2 LINQ-Operatoren
Die meisten LINQ-Operatoren können unter DryadLINQ in genau derselben Weise
weiterverwendet werden, ohne dass Änderungen im Programm vorgenommen werden müssen.
Trotzdem gibt es unter DryadLINQ für viele Operatoren eine Alternativimplementierung mit
dem Suffix „AsQuery“ (der „Max“-Operator hat zum Beispiel die Alternativimplementierung
„MaxAsQuery“). Diese ermöglicht es dem Compiler, Code der erzeugen, welcher effektiver in
der verteilten Dryad-Umgebung ausgeführt wird. Dies führt zu zum Teil deutlichen PerformanzSteigerungen.
4.3 DryadLINQ-Operatoren
Bestimmte Abfragen können in LINQ nur sehr umständlich formuliert werden, da LINQ die
Elemente einer Aufzählung isoliert betrachtet. Des Weiteren werden Operatoren benötigt, die nur
in verteilten Umgebungen Sinn ergeben und dort dann zu erheblichen Leistungssteigerungen
führen können. DryadLINQ führt daher die folgenden neuen Operatoren ein:
•
„Apply“: Dieser Operator ähnelt stark dem „Select“-Operator unter LINQ (Projektion).
Anstelle auf einzelne Elemente einer einzigen Collection kann „Apply“ jedoch auf
beliebig viele Elemente mehrerer Collections gleichzeitig zugreifen. Dies ermöglicht in
effizienter Weise die Programmierung z.B. des „Moving-Average“ über ein WerteFenster.
•
„Fork“: „Fork“ stellt gewissermassen die Umkehrung von „Apply“ dar. Es transformiert
eine Eingabe-Collection in mehrere Ausgabe-Collections. Dabei besteht ebenfalls Zugriff
auf beliebige Werte der Collections gleichzeitig.
•
Partitionier-Operatoren: Normalerweise kümmert sich DryadLINQ selbständig um die
Datenpartitionierung. Möchte der Entwickler aber explizit eine Datenpartitionierung
vorschreiben, so stehen die Partitionier-Operatoren „HashPartition<T, K> und
„RangePartition<T, K>“ zur Verfügung.
4.4 DryadLINQ-Attribute
DryadLINQ erzeugt im Allgemeinen sehr effizienten Code. Im manchen Fällen kann
DryadLINQ jedoch besseren Code erzeugen, wenn es über bestimmte Zusatzinformationen
verfügt.
Diese Zusatzinformationen können vom Entwickler in Form von Attributen dem Code
mitgegeben werden. DryadLINQ bietet die folgenden neuen Attribute an:
•
„Associative“: Der so bezeichnete Delegat einer Aggregatfunktion erfüllt das
Assoziativgesetz.
•
„Homomorphic“: Der so bezeichnete Delegat kommutiert über Partitionen hinweg, die
Operation kann also über Partitionen hinweg parallel ausgeführt werden.
•
„Resource“: Der so bezeichnete Delegat kann mit anderen Operationen zusammengefasst
werden (Pipelining).
•
„Nullable“: Das so bezeichnete Feld kann „Null“-Werte enthalten.
•
„FieldMapping“: Ermöglicht effizientere Typumwandlung durch DryadLINQ.
Oliver Kring, Thema 7: DryadLINQ
16/25
4.5 Einschränkungen unter DryadLINQ
DryadLINQ ist fast vollständig kompatibel zu LINQ, aber mit folgenden Einschränkungen:
•
DryadLINQ wurde von Microsoft überwiegend in C# getestet.
•
Operatoren dürfen keine Seiteneffekte haben, wenn sie als Knoten in einem Cluster
ausgeführt werden. Das betrifft insbesondere, wenn Objekte, auf die von mehreren
Stellen aus zugegriffen wird, modifiziert werden.
•
Die LINQ-Operatoren „Then“ und „ThenByDescending“ werden nicht unterstützt.
•
Ein „PartitionedTable<T>“ darf keine anonymen Typen enthalten.
•
Über „IEnumerable<T>“-Objekte kann in einem Knoten nur einmal iteriert werden
(Apply/Fork).
•
Zu tief verschachtelte Queries können in DryadLINQ zu einem „Stack Overflow“ führen.
•
Wenn die „PLINQ“ Option verwendet wird, kann es zu einer Umsortierung der Elemente
kommen.
Oliver Kring, Thema 7: DryadLINQ
17/25
5 Weitere Beispiele in DryadLINQ
5.1 MapReduce
DryadLINQ ist ein Compiler, in dem im Prinzip beliebige Algorithmen realisiert werden können.
Daher lässt sich im Vergleich zwischen DryadLINQ und MapReduce sagen:
•
DryadLINQ ist flexibler als MapReduce,
•
es gibt keine starre Zuordnung von Knoten zu Programm (Mapper/Reducer),
•
Joins sind problemlos möglich,
•
kein repliziertes Schreiben der Ausgebe-Dateien, sondern Neugenerierung von Daten
ausgefallener Knoten.
MapReduce lässt sich durch DryadLINQ leicht nachbilden (vgl. [3]):
public static IQueryable<S> MapReduce<T,M,K,S>(
IQueryable<T> input,
// Eingabemenge mit Objekten vom Typ T
Expression<Func<T, IEnumerable<M>>> mapper,
Expression<Func<M,K>> keySelector,
// Map-Funktion T->Ms
// Schlüssel-Funktion M->K
Expression<Func<IGrouping<K,M>,IEnumerable<S>>> reducer)
// Reducer-Funktion (K, Ms)-> Rs
{
var map = input.SelectMany(mapper);
var group = map.GroupBy(keySelector);
var result = group.SelectMany(reducer);
return result;
}
Oliver Kring, Thema 7: DryadLINQ
18/25
5.2 Aggregatfunktionen
DryadLINQ enthält bereits viele eingebaute optimierte Aggregatfunktionen (vgl. Kapitel 4.3).
Das folgende Beispiel zeigt, wie mit DryadLINQ leicht neue Aggregatfunktionen definiert
werden können [8]:
[Associative]
static double Add(double x, double y) { return x + y; }
…
double total = numbers.Aggregate((x, y) => Add(x, y));
Das „Associative“-Attribut sorgt für eine weitere Optimierung durch den Compiler. Die
folgenden Abbildungen zeigen den Effekt des „Associative“-Attributs:
Abb. 6: Ausführungsgraph ohne „Associative“-Attribut: Addition erfolgt nur auf einem Knoten
(Quelle: Microsoft [8])
Abb. 7: Ausführungsgraph mit „Associative“-Attribut: Addition erfolgt auf mehreren Knoten
(Quelle: Microsoft [8])
Oliver Kring, Thema 7: DryadLINQ
19/25
5.3 Joins
Joins sind bereits in LINQ standardmäßig vorhanden. Die Definition eines „Join“ in der
Schnittstelle „IQueryable“ lautet wie folgt:
public static IQueryable<TResult>
Join<TOuter, TInner, TKey, TResult>(
this IQueryable<TOuter> outer,
IEnumerable<TInner> inner,
Expression<Func<TOuter, TKey>> outerKeySelector,
Expression<Func<TInner, TKey>> innerKeySelector,
Expression<Func<TOuter, TInner, TResult>> resultSelector);
TOuter: Der Typ der ersten Sequenz
TInner: Der Typ der zweiten Sequenz
TKey: Der Schlüssel-Typ
TResult: Der Typ der Ergebniselemente
Im folgenden Beispiel werden Zeilen in einer Datei gesucht, die mit bestimmten Schlüsselworten
beginnen:
PartitionedTable<LineRecord> table =
PartitionedTable.Get<LineRecord>(metadata); // Daten
PartitionedTable<LineRecord> keywords =
PartitionedTable.Get<LineRecord>(keys); // Schlüsselworte
IQueryable<LineRecord> matches = table.Join(keywords,
l1 => l1.line.Split(' ').First(), /* first key */
l2 => l2.line, /* second key */
(l1, l2) => l1); /* keep first line */
DryadLINQ generiert dann folgenden Ausführungsplan:
Abb. 8: Ausführungsgraph für einen „Join“ (Quelle: Microsoft [8])
Die Daten liegen in vier Partitionen vor. Die Daten müssen zunächst umpartitioniert werden
(nach ihrem Schlüssel). Die Schlüsselwortdatei muss in gleicher Weise wie die Daten repartitioniert und auf die vier Knoten verteilt werden. Jeder Knoten erhält also nur einen Teil von
Daten und Schlüsselworten. Der Join kann jetzt verteilt durchgeführt werden.
Oliver Kring, Thema 7: DryadLINQ
20/25
6 Optimierung von Anfragen durch DryadLINQ
DryadLINQ ermöglicht die Entwicklung von verteilten Abfragen durch den Anwendungsprogrammierer, ohne dass dieser detaillierte Kenntnisse und Erfahrungen in der Erstellung
verteilter Anwendungen benötigt. Dabei übernimmt DryadLINQ auch die Erstellung eines
optimierten Ausführungsplans, sprich DryadLINQ sorgt dafür, dass die Anfrage optimal auf
möglichst vielen Knoten gleichzeitig und unter Verwendung von möglichst geringer Netzwerkund Festplatten-Bandbreite ausgeführt werden kann.
Im Folgenden sollen die Optimierungsmöglichkeiten von DryadLINQ aufgezeigt und an
Beispielen erläutert werden (vgl. [3]).
6.1 Statische Optimierungen
Wie bereits erwähnt dienen Optimierungen vor allem der Reduktion von I/O-Bandbreite, da
diese bei den allermeisten Systemen nur sehr begrenzt zur Verfügung steht. Statische
Optimierungen werden von DryadLINQ zur Compilierungszeit durchgeführt. Dabei werden die
vom Anwendungsprogrammierer geschriebenen LINQ-Anweisungen analysiert und in einen
Ausführungsgraphen übersetzt. Danach wird ein Regelwerk hinzugezogen, welches dazu dient,
den Ausführungsgraphen so umzuschreiben, dass der resultierende Graph optimal im Cluster
verteilt ausgeführt werden kann. DryadLINQ setzt zur Optimierung folgende Verfahren ein:
•
Pipelining: Verkettete LINQ-Operatoren können u.U. direkt hintereinander auf demselben
Knoten ausgeführt werden.
•
Redundanzvermeidung: DryadLINQ entfernt automatisch nicht benötigte
Partitionierungsschritte.
•
Datenverdichtung zum frühestmöglichen Zeitpunkt – wenn möglich werden
Aggregatfunktionen vor Partitionierungsschritte verschoben.
•
Vermeidung von Datenpersistenz: Zwischenergebnisse werden nicht abgespeichert – die
Daten ausgefallener Knoten werden neu generiert.
6.2 Dynamische Optimierungen
Statische Optimierungen analysieren und optimieren den Ausführungsgraphen zur
Übersetzungszeit ohne Betrachtung der Daten bzw. der Cluster-Topologie. Kennt man diese aber,
so lassen sich weitere Optimierungen durchführen indem z.B. Aggregatfunktionen zunächst auf
Prozessor-Ebene, dann auf Rack-Ebene und dann erst auf Cluster-Ebene durchgeführt werden.
Ferner kann erst durch Analyse der Daten die optimale Anzahl der Knoten für jeden
Berechnungsschritt des Ausführungsgraphen ermittelt werden, sowie die jeweils optimalen
Grenzen für jede Datenpartition.
DryadLINQ verwendet zur Laufzeit die Dryad API, um solche Optimierungen durchzuführen
und den Ausführungsgraphen dynamisch umzuschreiben.
Oliver Kring, Thema 7: DryadLINQ
21/25
6.3 Optimierungen für „OrderBy“
Wie zuvor beschrieben, führt DryadLINQ sowohl statische Optimierungen zur Übersetzungszeit
als auch dynamische Optimierungen zur Laufzeit durch. Anhand der Operation „OrderBy“, also
eines einfachen Sortiervorgangs, soll das Verfahren erläutert werden. Abb. 9 zeigt dabei die
Entwicklung des Ausführungsgraphen für einen Sortier-Knoten O durch die einzelnen
Optimierungsschritte für den allgemeinen Fall, dass nämlich die Daten in unsortierter Form
Vorliegen:
Abb. 9: Optimierungsschritte für „OrderBy“ (Quelle: [3])
In Optimierungsschritt (1) wird dabei die Sortier-Aufgabe „O“ in ihre Einzel-Aufgaben zerlegt.
Wichtigster Schritt ist dabei zunächst die Umpartitionierung der Daten. Dabei wird zunächst die
Datenbasis stichprobenartig analysiert (DS = Data Sampling) und ein Histogramm errechnet (H),
aus dem dann später eine optimale Partitionierung errechnet werden kann. Der Schritt (D) stellt
dann den eigentlichen Partitionierungsschritt dar. Der folgende Merge-Schritt (M) fügt die
Datensätze von den einzelnen Knoten zusammen und der Sortier-Schritt (S) führt die eigentliche
Sortierung durch.
Die Optimierungsschritte (2) und (3) sind dynamischer Natur.
Im Schritt (2) wird zunächst die optimale Anzahl der Knoten bestimmt, auf denen die
Datenpartitionierung DS + H + D durchgeführt wird. Diese wird im Wesentlichen bestimmt
durch die Anzahl der Partitionen, in denen die Eingabemenge vorliegt.
Schritt (3) berechnet dann die optimale Anzahl der Sortier-Knoten basierend auf dem
Datenvolumen.
Oliver Kring, Thema 7: DryadLINQ
22/25
6.4 Optimierungen für „MapReduce“
In Kap. 5.1 wurde bereits gezeigt, wie der „MapReduce“-Algorithmus durch DryadLINQ
umgesetzt werden kann. Die folgende Abbildung zeigt die Optimierung des „MapReduce“Algorithmus durch DryadLINQ:
Abb. 10: Optimierungsschritte für „MapReduce“ (Quelle: [3])
„MapReduce“ lässt sich durch drei LINQ-Anweisungen darstellen: Einer Map-Phase, realisiert
durch den „SelectMany“-Operator (SM), einer (globalen) Umgruppierung des so erhaltenen
Zwischenergebnisses (G) und der (globalen) Reduktion (R). Die nachfolgende Verarbeitung sei
durch (X) dargestellt. Ferner sei angenommen, dass die Daten unsortiert vorliegen und die
Reduktions-Funktion assoziativ und kommutativ ist.
In Optimierungsschritt (1) wird der Ausführungsplan statisch in eine Map und eine ReducePhase umgewandelt. In der Map-Phase wird zunächst die Mapping-Funktion ausgeführt (SM),
dann werden die Daten (lokal) sortiert (S) und gruppiert (G) und danach wird eine lokale
Reduktion durchgeführt (R) (wohlgemerkt, die Reduktions-Funktion sei assoziativ und
kommutativ). Das „Einschieben“ einer lokalen Reduktion entspricht der „Combine“-Phase beim
klassischen MapReduce, nur mit dem Unterschied, das DryadLINQ diese selbständig generiert.
Anschließend erfolgt das Umpartitionieren der Daten (D). All diese Operationen erfolgen auf
jeweils einem Knoten und beanspruchen daher kaum I/O-Bandbreite. In der sich anschließenden
Reduce-Phase werden die Datenpartitionen zusammengeführt, sortiert (MS=MergeSort) und neu
gruppiert (G). Am Schluss erfolgt dann die globale Reduktion (R), sowie die Nachverarbeitung
(X).
In Optimierungschritt (2) wird dann zur Laufzeit die optimale Anzahl der Knoten bestimmt und
zwar für die Map-Phase in Abhängigkeit von den vorliegende Eingabe-Partitionen und für die
Reduce-Phase abhängig vom Datenvolumen.
In Schritt (3) der Optimierung wird dann die Cluster-Tolopogie ausgenutzt und z.B. auf RackEbene bereits Teil-Reduktionen („partial Aggregations“) durchgeführt, um die Datenmenge zu
reduzieren, die über das Netzwerk übertragen werden muss.
Oliver Kring, Thema 7: DryadLINQ
23/25
7 Zusammenfassung und Ausblick
DryadLINQ ist ein Compiler welcher es dem Anwendungsprogrammierer die transparente
Entwicklung von verteilten Anfragen ermöglicht. DryadLINQ bietet:
•
Stark typisierten Code in einer beliebigen .NET-Sprache,
•
einfache LINQ-Syntax und sehr kompakter, aber gut lesbarer Code,
•
Anbindung verschiedener Datenquellen via Provider-Modell,
•
Optimierung des Anfrage-Codes und der Anfrage-Ausführung,
•
Entwicklung des Anfrage-Codes ohne Kenntnisse über die Cluster-Architektur.
DryadLINQ bietet aber nicht nur Vorteile:
•
Die Vorgaben an das Betriebssystem (Microsoft Windows Server 2008 R2 HPC für den
Master) machen den Einsatz von DryadLINQ oft unattraktiv für kleine Cluster. Bei
großen Clustern fällt das aber nicht so stark ins Gewicht, da die Knoten (mit Ausnahme
des Masters) bereits mit dem Betriebssystem Microsoft Windows 7 auskommen.
•
Der Startvorgang von DryadLINQ ist aufwändig – DryadLINQ ist daher ineffizient bei
kleinen Abfragen.
•
DryadLINQ arbeitet von Hause aus auf Dateien und ist daher gut geeignet für Daten, die
als Datei vorliegen und als Strom verarbeitet werden können. Ist ein zufälliger Zugriff auf
die Daten von Nöten, ist DryadLINQ weniger gut geeignet.
•
Der Optimizer kann nicht alle Probleme gänzlich zur völligen Zufriedenheit optimieren.
Daher ist manchmal eine Handoptimierung erforderlich. Diese erfordert jedoch genaue
Kenntnisse über die DryadLINQ Optimierungsstrategien, die aber oft nur schwer zu
durchleuchten sind.
•
Der Einsatz des „Apply“-Operators verringert die Fähigkeit von DryadLINQ zur
Optimierung der Abfragen.
•
DryadLINQ ist etwas langsamer als handoptimierter Dryad-Code.
Insgesamt sieht es aber so aus, als sei Microsoft mit dieser Entwicklung ein guter Wurf gelungen.
Die Entwicklung von verteilter Software für große Cluster dürfte sich damit drastisch
vereinfachen, bei gleichzeitig recht guter Performanz ohne aufwändige Optimierung. Ist dann
auch noch das Personal vorhanden, um ein professionelles Server-Betriebssystem zu
administrieren, ist sicherlich DryadLINQ eine Plattform, die auf jeden Fall mit in die engere
Wahl kommen sollte.
Oliver Kring, Thema 7: DryadLINQ
24/25
8 Abbildungsverzeichnis
Abb. 1: Die Schnittstellen IEnumerable<T> und IQueryable<T>
Abb. 2: Architektur von DryadLINQ (Quelle: Microsoft [8])
Abb. 3: Partitionierung von Dateien (Quelle: Microsoft [8])
Abb. 4: Ausführungsplan (Quelle: Microsoft [8])
Abb. 5: Die Klasse PartitionedTable<T>
Abb. 6: Ausführungsgraph ohne „Associative“-Attribut: Addition erfolgt nur auf einem Knoten
(Quelle: Microsoft [8])
Abb. 7: Ausführungsgraph mit „Associative“-Attribut: Addition erfolgt auf mehreren Knoten
(Quelle: Microsoft [8])
Abb. 8: Ausführungsgraph für einen „Join“ (Quelle: Microsoft [8])
Abb. 9: Optimierungsschritte für „OrderBy“ (Quelle: [3])
Abb. 10: Optimierungsschritte für „MapReduce“ (Quelle: [3])
Oliver Kring, Thema 7: DryadLINQ
25/25
9 Literaturverzeichnis
[1]
Dean, J.; Ghemawat, S.: MapReduce: Simplified Data Processing on Large Clusters. In:
Communications of the ACM 51 (2008), January, Nr. 1, S. 107-113
[2]
Isard, M.; Yu, Y.: Distributed data-parallel computing using a high-level programming
language. In: Proceedings of the 35th SIGMOID international conference on Management
of data ACM, 2009, S. 987-994
[3]
Yu, Y.; Isard, M.; Fetterly, D.; Budiu, M.; Erlingsson, Ú.; Gunda, P.K.; Currey, J.:
DryadLINQ: A System for General-Purpose Distributed Data-Parallel Computing Using a
High-Level Language. In: Proceedings of the 8th USENIX conference on Operating
systems design and implementation USENIX Association, 2008, S. 1-14
[4]
Wikipedia: LINQ. Weblink: http://de.wikipedia.org/wiki/LINQ Stand: 05/2011
[5]
Wikipedia: Delegat (.NET). Weblink: http://de.wikipedia.org/wiki/Delegat_(.NET) Stand:
05/2011
[6]
Microsoft: MSDN Library. Weblink: http://msdn.microsoft.com/de-de/library
[7]
Microsoft MSDN: 101 LINQ Samples.
Weblink: http://msdn.microsoft.com/en-us/vcsharp/aa336746
[8]
Yu, Y.; Isard, M.; Fetterly, D.; Budiu, M.; Erlingsson, Ú.; Gunda, P.K.; Currey, J.;
McSherry, F.; Achan, K.: Some sample programs written in DryadLINQ. In: Microsoft
Research Technical Report, MSR-TR-2008-74, May 2008, 37 Seiten
[9]
Microsoft Research: DryadLINQ: An Introduction. Weblink:
http://research.microsoft.com/en-us/collaboration/tools/dryad_and_dryadlinq-an_introduction.do
[10]
Microsoft Research: Dryad LINQ Tutorial. Weblink:
http://research.microsoft.com/en-us/projects/dryadlinq/dryadlinqtutorial.docx
FernUniversität in Hagen
Seminar 01912
im Sommersemester 2011
„MapReduce und Datenbanken“
Thema 8
PigLatin
Referent: Jürgen Koflerdes
Inhalt
1 Übersicht...........................................................................................................................................3
2 Einführung........................................................................................................................................3
2.1 Geschichte und Hintergründe...................................................................................................3
2.2 Installation und Start.................................................................................................................4
2.3 Erstes Beispiel...........................................................................................................................5
3 Datenschema.....................................................................................................................................6
3.1 Pig eats everything....................................................................................................................6
3.2 Ein explizites Datenschema definieren.....................................................................................7
3.2.1 Atomare Typen..................................................................................................................7
3.2.2 Tupel..................................................................................................................................7
3.2.3 Bag....................................................................................................................................7
3.2.4 Map...................................................................................................................................8
4 UDF – User Defined Functions........................................................................................................8
4.1 Filter Funktionen.......................................................................................................................8
4.2 Eval Funktionen........................................................................................................................9
4.3 Aggregat-Funktionen..............................................................................................................10
4.4 Funktionen für das Laden und Speichern...............................................................................10
4.5 Eigene Funktionen verwenden................................................................................................10
5 Die wichtigsten Operationen..........................................................................................................11
5.1 Laden und Definieren des Datenschemas...............................................................................11
5.2 Verarbeiten von Tupeln...........................................................................................................12
5.3 Filtern......................................................................................................................................12
5.4 Kombinieren und Splitten.......................................................................................................12
5.5 Gruppieren und Joinen............................................................................................................13
5.6 Sortieren..................................................................................................................................13
5.7 Speichern................................................................................................................................14
6 Übersetzen in MapReduce..............................................................................................................14
6.1 Logischer und physischer Plan...............................................................................................14
6.2 MapReduce Plan.....................................................................................................................14
7 Entwicklungsumgebung.................................................................................................................16
7.1 Grunt.......................................................................................................................................16
7.2 PigPen Eclipse Plugin.............................................................................................................16
7.3 Generieren von Beispiel-Daten...............................................................................................17
7.4 Debugging...............................................................................................................................17
8 Bewertung und Vergleich mit anderen Konzepten.........................................................................18
8.1 Anwendungsszenarien............................................................................................................18
8.2 Vergleich mit SQL..................................................................................................................18
8.3 Vergleich mit Hive..................................................................................................................19
8.4 Bewertung...............................................................................................................................20
9 Referenzen......................................................................................................................................20
Seminar MapReduce: Pig Latin
3/21
Jürgen Kofer
1 Übersicht
Bei der vorliegenden Seminararbeit über die Pig Latin1 möchte ich vor allem auf das praktische
Arbeiten mit der Sprache eingehen, und den Leser dazu einladen, die Beispiele auf dem eigenen
Rechner auszuprobieren. Dazu ist ein Rechner oder eine virtuelle Maschine mit Linux notwendig.
Die minimalen Voraussetzungen für das Verständnis dieses Texts sind Grundkenntnisse in SQL und
Java. Außerdem Kenntnis des MapReduce Algorithmus [WikiMR].
Zuerst werden wir auf die Geschichte und Hintergründe eingehen, dann wenden wir uns schon der
Installation zu und gehen ein erstes einfaches Beispiel durch.
Im Detail beleuchten wir dann Datenschemas in Pig Latin und das Erstellen von User Defined
Functions. Kapitel 5 stellt dann eine kleine Referenz für die existierenden relationalen Operationen
dar.
Dann schauen wir uns an, wie aus Pig Latin Skripten MapReduce Jobs werden. Und die
verschiedenen Entwicklungsumgebungen und Tools für das Arbeiten mit Pig Latin.
Ein Vergleich mit ähnlichen Konzepten und eine Bewertung schließt dann die Arbeit ab.
2 Einführung
2.1 Geschichte und Hintergründe
Das von Google2 eingeführte Programmiermodell MapReduce [DG04], für die Verarbeitung großer
Datenmengen, besticht durch seine Einfachheit und Parallelisierbarkeit, hat aber auch einige
Schwächen3:
•
•
•
•
•
•
Die meisten Analytiker sind es gewohnt das deklarative SQL zur Verarbeitung von Daten zu
verwenden
Abfragen können nur von Entwicklern vorgenommen werden, die mit imperativen Sprachen
wie Java vertraut sind.
Es gibt nur zwei Abstraktionen: map() und reduce(). Das lässt dem Entwickler zwar
maximalen Freiraum, gibt dem Anfänger aber keinerlei Unterstützung. Das führt zu einer
sehr steilen Lernkurve.
Selbstverständlichkeiten, wie das Filtern von Daten oder Projektionen, müssen manuell
implementiert werden.
Der Entwicklungszyklus mit Entwickeln, Testen und Produktivstellung ist relativ lange. Adhoc Analysen sind kaum möglich.
Das MapReduce Framework weiß nichts über die Semantik der implementierten map() und
reduce() Funktionen, kann also auch keine Optimierungen vornehmen, etwa um bei einer
mehrstufigen Verarbeitung die Schritte vorzuziehen, die die Datenmenge am stärksten
reduzieren.
1 Der englische Begriff „Pig Latin“ beschreibt eigentlich ein beliebtes Kinderspiel, bei dem durch verdrehen und
anhängen von Silben eine Art „Geheimsprache“ entsteht.
2 http://www.google.com
3 Vgl. [ORU+08], Kapitel Introduction
Seminar MapReduce: Pig Latin
4/21
Jürgen Kofer
Als Lösung für diese Schwächen wurde von Yahoo4 die neue Sprache Pig Latin entwickelt. Sie soll
die Kluft zwischen dem deklarativen Zugang mit SQL und dem prozeduralen, massiv
parallelisierbaren, mit MapReduce überbrücken. Um so die Vorteile beider Welten zu nutzen.
Pig Latin wurde als Datenfluss-Sprache5 realisiert. Das bedeutet, ein Script besteht aus mehreren
Schritten und jeder Schritt beschreibt eine Datentransformation.
Pig Latin ähnelt dabei dem prozeduralen Zugriffsplan (Query Execution Plan), den eine relationale
Datenbank aus einer SQL Abfrage generiert. Das ist ideal für große Datenmengen, da der
Entwickler der Abfrage sein Wissen über die Struktur der zu verarbeitenden Daten nutzen kann, um
das Skript zu optimieren. Bei SQL muss man sich darauf verlassen, dass die Datenbank einen
optimalen Zugriffsplan erstellt.
Beispiel: Der Teil “WHERE query is not null“ eines SQL Statements würde in Pig Latin zu einem
Schritt von vielen:
filtered = FILTER rows BY query is not null
Der Entwickler muss nun selber entscheiden, in welchem Schritt diese spezifische Transformation
am besten stattfindet.
Pig Latin unterstützt optional Datenschemas. Das heißt, die Spalten eingelesener Daten6 können mit
Namen und Typ versehen werden. Das Datenschema kann sich bei jedem Verarbeitungsschritt
verändern, bis hin zu verschachtelten Strukturen, bei denen etwa ein Schlüssel auf mehrere
untergeordnete Relationen verweist.
Neben den eingebauten Funktionen wie COUNT und MAX können eigene definiert werden, so
genannte UDFs (User Defined Functions).
Für die Ausführung wird ein Script in MapReduce Jobs übersetzt. Die können dann lokal (ohne
Parallelisierung) oder auf einem Hadoop [Hadoop] Cluster laufen. Das Laufzeitsystem wird
übrigens einfach Pig genannt. Pig Latin ist also die formale Sprache mit der Pig gefüttert wird.
Pig wurde 2007 der Apache Foundation übergeben [PigToAp] und wird dort als Open Source
Projekt weitergeführt [Pig].
2.2 Installation und Start
Voraussetzungen:
1. Unix System (Linux, BSD, etc)
2. Java installiert und JAVA_HOME gesetzt
3. * Hadoop 0.20.x installiert [Hadoop]
Pig ist nach folgenden, wenigen Schritten installiert:
1. Download eines Releases [Pig]
4 http://www.yahoo.com
5 Vgl. [ORU+08], Kapitel Dataflow Language
6 Im folgenden werden wird anstatt von „Daten“ immer von Relationen sprechen. Eine Relation bezeichnet eine
ungeordnete Menge von n-Tupeln.
* Nur notwendig für einen Betrieb mit Hadoop
Seminar MapReduce: Pig Latin
5/21
Jürgen Kofer
2. Das Archiv in einem beliebigen Verzeichnis entpacken
3. * Die Hadoop Konfigurationsdateien müssen zum Classpath hinzugefügt werden.
Dazu in der Datei <pig_installation>/bin/pig am Beginn hinzufügen:
export PIG_CLASSPATH=<hadoop_installation>/conf
Jetzt müsste sich nach dem Aufruf von <pig_installation>/bin/pig -x local die grunt7 Konsole
öffnen, die ein interaktives Erstellen von Pig Latin Skripten erlaubt. Der Parameter -x local bedeutet
hier, dass ein Pseudo-MapReduce Modus verwendet werden soll und die Dateien aus dem lokalen
Dateisystem kommen. Das ist nur für die Entwicklung gedacht, der Standard ist -x mapreduce und
die Ausführung auf einem Hadoop Cluster.
Ein Script kann mit bin/pig script.pig gestartet werden. Außerdem ist es Möglich Pig Skripen aus
Java Programmen heraus zu starten8.
Weitere Details über die Installation und Inbetriebnahme finden Sie hier: [PigSetup].
2.3 Erstes Beispiel
Mit Pig wird ein im Unterverzeichnis tutorial/data ein Logfile der Suchmaschine Excite9
mitgeliefert. Ein Tupel der Relation besteht aus drei Feldern: User ID, Zeitstempel im Format
YYMMDDHHMMSS, die Suchanfrage (Query) als Textstring.
Die einzelnen Felder sind durch einen Tabulator getrennt, das ist das Standardtrennzeichen von Pig.
Wir wechseln ins Pig Verzeichnis und starten im local Mode:
cd <pig_installation>
bin/pig -x local
grunt>
Laden der Datensätze und Angabe eines Datenschemas:
grunt> logs = LOAD 'tutorial/data/excite-small.log' AS (user, time, query);
grunt> DESCRIBE logs;
rows: {user: bytearray,time: bytearray,query: bytearray}
DESCRIBE zeigt das Datenschema. Als Typ wird standardmäßig bytearray angenommen, wenn
wir explizit einen chararray (String) als Typ wollen schreiben wir:
grunt> logs = LOAD 'tutorial/data/excite-small.log'
AS (user:chararray, time:chararray, query:chararray);
Als nächstes Filtern wir Log-Einträge heraus die eine leere Suchanfrage (Query) haben:
grunt> filtered = FILTER logs BY query is not null;
Es fällt vielleicht auf, dass die eingegebenen Befehle bis jetzt nicht ausgeführt wurden: Bei der
Eingabe eines Transformationsschritts in der grunt Konsole wird zwar die syntaktische Korrektheit
geprüft, aber ausgeführt wird das ganze erst bei der Eingabe eines DUMP oder STORE Befehls.
* Nur notwendig für einen Betrieb mit Hadoop
7 Grunt bedeutet übersetzt „Grunzen“. Die ganze Nomenklatur in Pig hat irgendwas mit Schweinen zu tun.
8 Siehe [PigSetup], der Abschnitt über Embedded Programs und Sample Code
9 http://msxml.excite.com/excite/ws/index
Seminar MapReduce: Pig Latin
6/21
Jürgen Kofer
Nun wollen wir herausfinden, welche Suchbegriffe in diesem Zeitraum am populärsten waren. Dazu
gruppieren wir zunächst nach den Suchbegriffen:
grunt> grouped = GROUP filtered BY query;
grunt> DESCRIBE grouped;
grouped: {group: chararray,filtered: {user: chararray,time: chararray,query:
chararray}}
DESCRIBE zeigt, dass wir nun ein verschachteltes Datenschema bekommen haben: Pro
Suchbegriff (group) kann es 1..n der ursprünglichen Tupel geben (filtered).
Wir zählen nun mit COUNT die Anzahl der zusammen gruppierten Relationen:
grunt> counts = FOREACH grouped GENERATE group, COUNT(filtered) AS count;
Als letztes sortieren wird noch die Anzahl der Sucheingaben absteigend:
grunt> ordered = ORDER counts BY count DESC;
grunt> DESCRIBE ordered;
ordered: {group: chararray,count: long}
Wir sind fertig und geben die ersten Zeilen der transformierten Relation auf der Konsole aus:
grunt> first = LIMIT ordered 6;
grunt> DUMP first;
(maytag,41)
(vanderheiden,27)
(change bowel habits,24)
(en vogue,23)
(running shoes,22)
(pregnant,20)
Die SQL Abfrage die wir mit diesem Beispiel realisiert haben ist:
SELECT query, COUNT(*)
FROM logdata
WHERE query is not null
GROUP BY query
ORDER BY COUNT(*) DESC
LIMIT 6;
3 Datenschema
3.1 Pig eats everything
Ein Datenschema ist in Pig Latin optional. Selbst wenn eines angegeben wurde, treten beim Laden
von unpassenden Daten keine Fehler auf. Steht etwa ein Buchstabe in einer Spalte, die mit long
definiert wurde, wird der Wert des entsprechenden Feldes einfach zu NULL.
Deshalb ist „Pig eats everything“ einer von drei Punkten der „Pig Philosophie“ [PigPhil].
Seminar MapReduce: Pig Latin
7/21
Jürgen Kofer
3.2 Ein explizites Datenschema definieren
Wie im Beispiel bereits gezeigt, wird das Datenschema mit einem AS Schlüsselwort definiert.
Dabei können die Ausgangsdaten auch ihn verschachtelte Strukturen geladen werden:
Nehmen wir an, die Ausgangsdaten liegen in folgender Form vor:
(3,8,9) (4,5,6)
(1,4,7) (3,7,5)
Dann könnte das Datenschema so definiert werden:
A = LOAD 'data'
AS (t1:tuple(t1a:int, t1b:int,t1c:int),t2:tuple(t2a:int,t2b:int,t2c:int));
Für die weitere Verarbeitung kann auf die einzelnen Felder mit <tupel_name>.<feld_name>
zugegriffen werden:
X = FOREACH A GENERATE t1.t1a,t2.$0;
$x kann verwendet werden um x'te Feld zuzugreifen. So können Felder referenziert werden, wenn
kein Datenschema definiert wurde.
Weitere Details über Datenschemas in Pig finden sich bei [White11], ab Seite 336 und [PigLatin] im
Abschnitt Date Types and More.
3.2.1 Atomare Typen
Folgende Datentypen für einzelne Felder werden unterstützt: int, long, float, double, chararray
(String) und bytearray.
3.2.2 Tupel
Ein Tupel ist eine geordnete Menge von Feldern. Jede „Zeile“ der Eingangsdaten entspricht genau
einem Tupel. Der Unterschied zu einer Zeile einer relationalen Tabelle ist, dass ein Feld eines
Tupels nicht atomar sein muss, sondern selbst wieder ein Tupel oder ein Bag oder eine Map sein
kann.
Hier ein Tupel, dessen erstes Feld einen atomaren Typ (String) hat, dessen zweites ein Bag ist, das
zwei Tupel enthält, und dessen drittes Feld eine Map mit einem einzelnen Eintrag ist:
Abbildung 1: Beispieltupel. Quelle: [ORU+08]
3.2.3 Bag
Ein Bag ist eine ungeordnete Menge von Tupeln. Innerhalb eines Bag können die Tupel eine
verschiedene Anzahl von Feldern haben, außerdem sind Duplikate erlaubt.
Die „ganze“ Relation entspricht einem sogenannten Outer Bag. Bags die als Felder von Tupeln
Seminar MapReduce: Pig Latin
8/21
Jürgen Kofer
auftreten, werden Inner Bags genannt.
3.2.4 Map
Eine Map ist eine Menge von Schlüssel/Wert Paaren. Genau wie eine Map Collection in Java. Also
zum Beispiel: [name → 'Karl',age → 23]. Ein einzelnes Feld würde angesprochen mit map#key.
Hier zum Beispiel map#'name' → 'Karl'.
Maps können nur aus Dateien erzeugt werden oder durch User Defined Functions. Es gibt keine
Möglichkeit sie aus den Standard-Transformationen zu gewinnen.
4 UDF – User Defined Functions
In Pig Latin gibt es nur eine kleine Zahl von eingebauten Funktionen, wie die Aggregat-Funktionen
COUNT, MAX, MIN, AVG und mathematische Funktionen wie ABS, SIN, COS.
Es können aber relative leicht neue Funktionen in Java geschrieben und in Statements eingesetzt
werden. Diese werden User Definied Functions genannt und es existieren verschiedene Typen von
ihnen. Weitere Einzelheiten zu UDFs finden sich auch hier: [PigUDF] und bei [White11], ab Seite
343.
Um die nachfolgenden Java-Beispiele kompilieren zu können, muss die Bibliothek pig-<pigversion>-core.jar in den Classpath aufgenommen werden. In Eclipse kann das im Projekt über
Properties → Java Build Path → Libraries gemacht werden.
4.1 Filter Funktionen
Eine Filterfunktion nimmt einen Wert oder einen Tupel von Werten entgegen und gibt einen
boolean Wert (aussagelogisches ja oder nein) zurück. Wird nein zurückgegeben, kommt der Tupel
in der Ergebnisrelation nicht mehr vor.
Wir wollen das an einer Funktion isEmpty demonstrieren, die prüft, ob ein atomarer Wert null oder
leer ist. Das Selbe könnte natürlich auch mit „is null“ erreicht werden, unsere Funktion ist also also
nicht wirklich sehr nützlich.
Wir schreiben dazu eine Java Klasse IsEmpty die org.apache.pig.FilterFunc erweitert. In der
Methode exec() holen wir uns den Wert aus dem Eingangstupel und geben entsprechend true oder
false zurück:
package de.fernunihagen.seminar1912.piglatin;
import java.io.IOException;
import org.apache.pig.FilterFunc;
import org.apache.pig.data.Tuple;
public class IsEmpty extends FilterFunc {
public Boolean exec(Tuple input) throws IOException {
String query = (String) input.get(0);
if (query == null || query.trim().isEmpty()) {
return true;
Seminar MapReduce: Pig Latin
9/21
Jürgen Kofer
}
return false;
}
}
Die Methode exec() hat als formalen Parameter immer einen Tupel, selbst wenn die Funktion
eigentlich einfache Felder verarbeitet. Pig erzeugt in dem Fall einen „künstlichen“ Tupel und steckt
das Feld da hinein. Wir bekommen also das tatsächlich übergebene Feld mit input.get(0).
4.2 Eval Funktionen
Eval Functions (Evaluate) sind die üblichsten und allgemeinsten Funktionen. Sie erwarten einen
beliebigen Datentyp als Input und geben einen beliebigen Datentyp zurück. Der Typ des
Rückgabewertes wird dabei über den Java Generics [WJavaGen] Mechanismus festgelegt.
Als Beispiel wollen wir eine Funktion realisieren, die einen String aus mehreren Wörtern entgegen
nimmt und daraus ein Bag mit einzelnen Wörtern erzeugt. Wir wollen damit die Suchanfrage aus
dem Excite Log in einzelne Wörter zerlegen, um herauszufinden welche Wörter am häufigsten in
den Anfragen vorkommen.
Der Grund dafür, dass wir einen Bag mit Tupeln erzeugen und nicht einfach einen einzelnen Tupel
mit den Wörtern als Felder, liegt darin, dass sonst die FLATTEN Operation nicht so funktionieren
würde wir wir uns das erwarten. Wir möchten nämlich eine Relation erzeugen, in der jeder
enthaltene Tupel nur noch ein einzelnes Wort enthält, damit wir diese durch Gruppierung und
Aggregierung zählen können.
Wir schreiben eine Klasse SplitWords die org.apache.pig.EvalFunc erweitert. Dann splitten wir den
Eingangsstring und befüllen in einer Schleife den Ausgangs-Bag:
package de.fernunihagen.seminar1912.piglatin;
import
import
import
import
import
import
java.io.IOException;
org.apache.pig.EvalFunc;
org.apache.pig.data.BagFactory;
org.apache.pig.data.DataBag;
org.apache.pig.data.Tuple;
org.apache.pig.data.TupleFactory;
public class SplitWords extends EvalFunc<DataBag> {
public DataBag exec(Tuple input) throws IOException {
String query = (String) input.get(0);
String[] words = query.split(" ");
DataBag output = BagFactory.getInstance().newDefaultBag();
for (String word : words) {
if (word.startsWith("+") || word.startsWith("-")) {
word = word.substring(1);
}
if (word.length() > 2) {
Tuple t = TupleFactory.getInstance().newTuple();
t.append(word);
output.add(t);
}
}
return output;
}
Seminar MapReduce: Pig Latin
10/21
Jürgen Kofer
}
Für das Erzeugen von Bags und Tupeln liefert Pig zwei Factory Klassen mit: BagFactory und
TupelFactory.
4.3 Aggregat-Funktionen
Aggregat-Funktionen, wie die eingebauten COUNT und MAX, sind zwar auch Eval Funktionen,
allerdings werden sie immer auf eine Gruppe von Tupeln angewendet. Das erste Feld des InputTupels ist daher ein DataBag, über den iteriert werden muss, um z.B. irgendwelche Werte
aufzusummieren.
Für viele Aggregat-Funktionen ist es möglich, dass jede Map-Instanz für sich „vorreduziert“, damit
die Daten, die zu den Reducer-Instanzen transferiert werden müssen, geringer werden. Zum
Beispiel könnte jede Map-Instanz für sich schon das „lokale“ Maximum bestimmen und die
entsprechende Reducer-Instanz bestimmt dann das „globale“ Maximum.
Pig bietet dazu ein spezielles Interface Algebraic mit drei Methoden, die die Namen von drei
verschiedenen Funktionen zurückgeben, die implementiert werden müssen, um solche
Aggregationen dreistufig mit einem Combiner10 auszuführen:
•
•
•
getInitial(): Erwartet den Namen einer Eval Function, die für jede Map-Instanz genau
einmal zu Beginn ausgeführt wird
getIntermed(): Erwartet den Namen einer Eval Function, die beliebig oft während der
Combine-Stufe ausgeführt werden kann. Sie generiert Zwischendaten.
getFinal(): Erwartet den Namen einer Eval Function, die am Schluss für jede ReduceInstanz genau ausgeführt werden soll. Sie bekommt alle Zwischendaten und berechnet den
endgültigen Wert.
4.4 Funktionen für das Laden und Speichern
Es können die zwei abstrakten Klassen LoadFunc und StoreFunc erweitert werden, um Funktionen
zu schreiben, die Daten aus speziellen Quellen oder in speziellen Formaten laden und speichern
können.
4.5 Eigene Funktionen verwenden
Die obigen Java Klassen müssen als JAR gebündelt und am besten in das Installationsverzeichnis
von Pig Latin kopiert werden. In grunt können die Funktion dann so registriert werden:
grunt> REGISTER pig-seminar.jar
Jetzt könnten die Funktionen schon mit de.fernunihagen.seminar1912.piglatin.IsEmpty() verwendet
werden, wir wollen das aber kürzer haben:
grunt> DEFINE isEmpty de.fernunihagen.seminar1912.piglatin.IsEmpty();
grunt> DEFINE splitWords de.fernunihagen.seminar1912.piglatin.SplitWords();
Nun versuchen wir herauszufinden, nach welchen einzelnen Wörtern am meisten gesucht wurde:
10 Sie [MapReduce] der Abschnitt über Combine
Seminar MapReduce: Pig Latin
11/21
Jürgen Kofer
grunt> logs = LOAD 'tutorial/data/excite-small.log'
AS(user:chararray, time:chararray, query:chararray);
grunt> filtered = FILTER logs BY not isEmpty(query);
grunt> splitted = FOREACH filtered GENERATE user, splitWords(LOWER(query));
grunt> DUMP splitted;
(C5D01E05FF9CA265,{(mary),(lou),(allgood)})
(DB38E7AF26F3AD9A,{(microsoft),(excel)})
...
Was wir allerdings wollen ist, dass drei Tupel entstehen, wenn die Suchanfrage aus drei Wörtern
besteht, sonst können wir die Worte nicht zählen. Wir benutzen dazu das Schlüsselwort FLATTEN:
grunt> splitted = FOREACH filtered GENERATE user,
FLATTEN(splitWords(LOWER(query)) )AS word;
grunt> DUMP splitted;
(C5D01E05FF9CA265,mary)
(C5D01E05FF9CA265,lou)
(C5D01E05FF9CA265,allgood)
(DB38E7AF26F3AD9A,microsoft)
(DB38E7AF26F3AD9A,excel)
…
Nun können wir nach den Worten gruppieren und sie zählen:
grunt> grouped = GROUP splitted BY (chararray) word;
grunt> counts = FOREACH grouped GENERATE group, COUNT(splitted) AS count;
grunt> ordered = ORDER counts BY count DESC;
grunt> first = LIMIT ordered 6;
grunt> DUMP first;
(and,188)
(the,95)
(free,68)
(pics,50)
(maytag,41)
(pictures,36)
Beim Gruppieren war es hier notwendig die Spalte word mit (chararray) zu casten, das hat mit
einem offenen Bug zu tun: [PigBug919].
Vor dem Splitten wurde die eingebaute String-Funktion LOWER benutzt, um die eingegebene
Suchanfrage in Kleinbuchstaben zu verwandeln. Sonst würden z.B. die Worte „pig“ und „Pig“ als
verschieden behandelt werden.
5 Die wichtigsten Operationen
Im folgenden die wichtigsten relationalen Operationen im Überblick, mit denen Daten eingelesen
und verarbeitet werden können.
Eine vollständige Referenz bietet [PigLatin] im Abschnitt Relational Operators.
5.1 Laden und Definieren des Datenschemas
Seminar MapReduce: Pig Latin
12/21
Jürgen Kofer
Syntax für den Ladebefehl: alias = LOAD 'data' [USING function] [AS schema]
function ist hier eine Load UDF. Wenn nichts angegeben wird, wird PigStorage mit Tabulator als
Trennzeichen angenommen. PigStorage liest Tupel von Textdateien ein. Es könnte ein anderes
Trennzeichen gewählt werden mit:
[…] USING PigStorage(",") AS [...]
5.2 Verarbeiten von Tupeln
Der Befehl FOREACH … GENERATE iteriert über alle Tupel und generiert neue. Das entspricht
dem SELECT bei relationalen Datenbanken.
Syntax: alias = FOREACH alias GENERATE expression [AS schema] [expression [AS schema]….]
Eine expression ist hier typischerweise ein Feld der Ausgangsrelation, eine mathematische Funktion
wie ABS(x), COS(x), eine String Funktion wie LOWER(str) oder eine beliebige Eval UDF.
Wie bei LOAD kann mit AS das Datenschema für das Ergebnis eines Ausdrucks angegeben werden.
5.3 Filtern
Die Operation FILTER entspricht einer WHERE Klausel bei SQL.
Syntax: alias = FILTER alias BY expression
expression ist hier ein bool'scher Ausdruck, also z.B:
[…] BY temperature > 20 and temperature < 25 or temperature > 30
Die expression könnte natürlich auch eine Filter UDF beinhalten.
5.4 Kombinieren und Splitten
Die Operation SPLIT ermöglicht es, eine Relation in mehrere Relationen aufzuspalten. Ein Tupel
kann dabei in mehreren Ergebnisrelationen landen oder auch in gar keiner.
Syntax: SPLIT alias INTO alias IF expression, alias IF expression [, alias IF expression …]
So würden zum Beispiel die Log-Daten der Excite Suchmaschine auf zwei Relationen aufgeteilt, je
nachdem, ob die Suchanfrage leer war oder nicht:
grunt> rows = LOAD 'tutorial/data/excite-small.log' AS (user, time, query);
grunt> SPLIT rows INTO empty IF query is null, notEmpty IF query is not null;
UNION ist das Gegenstück zu SPLIT und vereinigt mehrere Relationen zu einer.
Syntax: alias = UNION alias, alias [, alias …]
Um die gesplittete Relation im Beispiel wieder zu vereinen:
Seminar MapReduce: Pig Latin
13/21
Jürgen Kofer
grunt> rows = UNION empty, notEmpty;
5.5 Gruppieren und Joinen
GROUP und JOIN verhalten sich bei Pig Latin beinahe gleich. Beide gruppieren Relationen nach
dem Inhalt von einem oder mehreren Feldern. Die Unterschiede sind:
•
•
GROUP kann auch auf einer einzelnen Relation arbeiten (siehe Einführungsbeispiel)
JOIN filtert Tupel heraus, bei denen das Feld, über das gruppiert werden soll, den Wert
NULL enthält. Verhält sich also wie ein INNER JOIN in SQL. Das Verhalten kann geändert
werden, durch die Verwendung des Schlüsselwortes OUTER:
JOIN a BY $0 LEFT OUTER, b BY $0
•
Es werden andere Ausgangsdaten erzeugt: GROUP erzeugt eine Menge von verschachtelten
Tupeln, JOIN eine Menge von „flachen“ Tupeln.
Beispiel:
Wir erzeugen zwei kleine Textdateien die folgendes enthalten (Spalten durch Tabulator getrennt):
test1.txt:
1
2
3
4
A
B
C
D
test2.txt
3
2
1
1
y
x
w
v
grunt> test1 = LOAD 'test1.txt';
grunt> test2 = LOAD 'test2.txt';
grunt> groupSingle = GROUP test2 BY $0;
grunt> DUMP groupSingle;
(1,{(1,w),(1,v)})
(2,{(2,x)})
(3,{(3,y)})
grunt> grouped = GROUP test1 BY $0, test2 BY $0;
grunt> DUMP grouped:
(1,{(1,A)},{(1,w),(1,v)})
(2,{(2,B)},{(2,x)})
(3,{(3,C)},{(3,y)})
(4,{(4,D)},{})
grunt> joined = JOIN test1 BY $0, test2 BY $0;
(1,A,1,w)
(1,A,1,v)
(2,B,2,x)
(3,C,3,y)
Hier sieht man gut die wesentlichen Unterschiede: Bei GROUP werden die Tupel, die
zusammengehören (das angegebene Feld hat den selben Wert), in ein Bag gesteckt. Dabei wird pro
Ausgangsrelation ein Bag erzeugt (bei der Ausgabe mit DUMP wird ein Bag übrigens mit
geschwungenen Klammer gekennzeichnet). Die Ausgangsrelation hat bei GROUP also immer 1 + n
Felder, wenn n die Anzahl der Eingangsrelationen ist.
Bei JOIN werden die passenden Tupel einfach verschmolzen. Die Ausgangsrelation hat genau so
viele Felder wie alle Eingangsrelationen zusammen. Das Verhalten ist ähnlich dem JOIN in SQL.
5.6 Sortieren
Seminar MapReduce: Pig Latin
14/21
Jürgen Kofer
Die Operation ORDER BY entspricht genau der gleichnamigen Operation in SQL.
Syntax: alias = ORDER alias BY { * [ASC|DESC] | field_alias [ASC|DESC] [, field_alias [ASC|
DESC] …] } [PARALLEL n]
Interessant ist hier die optionale Angabe PARALLEL, die angibt, auf wie viele MapReduce Jobs die
Aufgabe parallel verteilt werden soll.
5.7 Speichern
STORE ist das Gegenstück zum LOAD.
Syntax: STORE alias INTO 'directory' [USING function]
6 Übersetzen in MapReduce
Wir schauen uns nun an wie das Einführungsbeispiel in MapReduce Jobs umgewandelt würde. Wir
reduzieren das Beispiel dazu auf vier Schritte:
1.
2.
3.
4.
Laden (LOAD)
Filtern der leeren Abfragen (FILTER)
Gruppieren nach Abfragen (GROUP)
Zählen der Häufigkeit der Abfragen (COUNT)
Pig Latin hat einen speziellen Operator um sich die drei verschiedenen Ausführungspläne anzeigen
zu lassen: EXPLAIN. Für unser Beispiel würden wir eingeben:
grunt> EXPLAIN counts;
6.1 Logischer und physischer Plan
Pig prüft die semantische und syntaktische Korrektheit des Skripts und erstellt als erstes einen
logischen Plan. Der logische Plan stellt die vorhandenen Operationen in die richtige Reihenfolge
und wird so zur Basis für die Verarbeitungs-Pipeline.
Der physische Plan hat grundsätzlich die selbe Struktur, allerdings werden nicht mehr logische
Namen von Operationen und Feldern verwendet, sondern physische. Also der „interne“ Name einer
Funktion und die Nummer der Spalte eines Feldes, statt des Alias. Der physische Plan kann also
relativ leicht in Java Code umgewandelt werden.
6.2 MapReduce Plan
Der dritte und letzte Plan ist der MapReduce Plan und beschreibt das Umwandeln des physischen
Planes in MapReduce Jobs. Aus ihnen werden schlussendlich Java Klassen, die kompiliert und in
den Hadoop Cluster eingespielt werden.
Wenn man sich vor Augen führt, dass der MapReduce Algorithmus genau einem Gruppieren von
Seminar MapReduce: Pig Latin
15/21
Jürgen Kofer
Daten nach Schlüsseln (map) und dem anschließenden Verarbeiten dieser Gruppen (reduce)
entspricht, ist es nicht verwunderlich wie Pig vorgeht:
Als erstes werden die GROUP und JOIN Operationen herausgesucht und für jede von ihnen ein
MapReduce Job erstellt. Im Map-Teil werden die Daten gruppiert, im Reduce-Teil geschieht beim
GROUP vorerst gar nichts, beim JOIN werden die gruppierten Daten „flachgedrückt“ zu Tupeln.
Fast immer ist GROUP gefolgt von einer Aggregierung mit FOREACH GENERATE, diese
Verarbeitung landet dann im Reduce-Teil.
Wenn vor einem GROUP/JOIN ein LOAD oder FILTER ist, kommt das zum Map-Teil dazu. Das
STORE kommt zum letzten Reduce-Schritt dazu.
SPLIT funktioniert für die einzelnen Ergebnisrelationen wie FILTER und wird Teil der Map-Stufe.
Ebenso UNION, das einfach mehrere Ausgangsrelationen durch die restliche Verarbeitung im MapTeil schickt.
ORDER wird zu zwei verschiedenen MapReduce Jobs. Im ersten Job wird die statistische
Verteilung der Sortierungsschlüssel bestimmt. Im zweiten Schritt werden die Daten entsprechend
der Verteilung partitioniert, um sie dann lokal sortieren zu können. Das alles ist notwendig, da wir
ja „global“ sortierte Daten haben wollen.
Wie bereits bei den Aggregat-Funktionen erwähnt: Bei Algebraic-UDFs wird ein Combiner der
Map-Stufe nach geschalten, um die Daten bereits in der Map-Stufe zu „reduzieren“.
Zum genauen Ablauf der Kompilation gibt es leider recht wenige Referenzen. Eine kurze Übersicht
findet sich bei [PigLatin] unter dem Abschnitt Map-Reduce Plan Compilation.
Schauen wir uns nun in unserem Beispiel an, welche Jobs erzeugt werden:
Pig beginnt mit dem dritten Schritt und erzeugt einen Job für die Gruppierung über die Suchanfrage
(Query). Die Gruppierung landet dabei in der Map-Stufe. Ebenfalls in der Map-Stufe, noch vor der
Gruppierung, landet das Laden und das Filtern.
Im Reduce-Schritt werden die Gruppierten Relationen gezählt. Allerdings ist COUNT eine
Algebraic Function, d.h. in einem Combiner-Schritt werden gleiche Suchanfragen bereits gezählt
und die Reducer erreichen Tupel aus (Suchanfrage, Anzahl), die einfach noch weiter aufsummiert
werden müssen. Es gibt also insgesamt nur einen einzelnen Job.
Load
Filter
Goup
Count.Initial
Load
Filter
Goup
Count.Initial
Load
Filter
Goup
Count.Initial
Combine
Count.Intermediate
Count.Intermediate
Count.Intermediate
('super bowl', 112)
Reduce
Count.Final
Store
Count.Final
Store
Count.Final
Store
('super bowl', 5422)
Map
('super bowl', {('F1...', '2011...',
'super bowl'), ('F2...', '2011...',
'super bowl'), ...})
Abbildung 2: Zählen gleicher Suchabfragen ohne ORDER übersetzt in MapReduce Jobs
In dem Beispiel wäre also 5422 mal der Begriff „super bowl“ gesucht worden. Auf jeder
Map/Combine-Instanz werden die lokalen Daten verarbeitet, d.h. jeder Combiner zählt für sich wie
Seminar MapReduce: Pig Latin
16/21
Jürgen Kofer
oft „super bowl“ gesucht wurde. Aber nur eine einzige Reduce-Instanz zählt alle „super bowl“
Zwischenergebnisse zusammen.
7 Entwicklungsumgebung
7.1 Grunt
Grunt ist eine interaktive Entwicklungsumgebung, die den inkrementelle Aufbau von Skripten
erlaubt und eine einfache Textkonsole bietet.
Die grunt Konsole hat außerdem einige Befehle für das Dateisystem und kann direkt das Hadoop
Filesystem ansprechen, wenn Pig im MapReduce-Modus gestartet wurde:
•
•
•
•
•
help: Zeigt alle verfügbaren Kommandos und Erläuterungen an
ls: Listet den Inhalt des derzeitigen Verzeichnisses auf
cd: Wechselt das Verzeichnis
fs: Hadoop-Dateisystem Shell [HadoopFsShell]. Entspricht dem Aufruf von hadoop
fs auf der Kommandozeile.
quit: Beenden von grunt
Beispiel:
grunt> cd tutorial/data
grunt> ls
file:/opt/pig-0.8.1/tutorial/data/excite.log.bz2<r 1>
10408717
file:/opt/pig-0.8.1/tutorial/data/excite-small.log<r 1> 208348
Mehr dazu bei [White11], Seite 324-325 und bei [PigLatin] der Abschnitt Shell Commands.
7.2 PigPen Eclipse Plugin
PigPen [PigPen] ist ein Eclipse11 Plugin, dass einen eigenen Editor mitbringt für Pig Skripten und
das Starten von diesen aus Eclipse heraus erlaubt.
Das JAR wird einfach in das /plugins Verzeichnis von Eclipse kopiert. Allerdings scheint das Plugin
nicht mit der derzeit aktuellen Pig/Hadoop Kombination 0.8.1/0.20.2 zu funktionieren. Zumindest
das Starten von Skripten ist nicht möglich. Und auch der Operator Graph funktioniert nicht, der die
Transformationen graphisch darstellen sollte.
Außerdem ist die derzeitige Versionsnummer von PigPen, nämlich 0.0.1, nicht gerade
vertrauenerweckend.
11 http://www.eclipse.org
Seminar MapReduce: Pig Latin
17/21
Jürgen Kofer
Abbildung 3: Screenshot PigPen Plugin
7.3 Generieren von Beispiel-Daten
Ein wirklich interessantes Feature von Pig ist die Möglichkeit Beispieldaten für ein gegebenes
Skript zu generieren. In der Praxis ist es nämlich gar nicht so einfach, aussagekräftige Beispieldaten
aus Milliarden von Datensätzen zu gewinnen.
Der Datengenerator [PigDGen] läuft dabei als Hadoop Job und versucht ein möglichst
aussagekräftiges, und gleichzeitig möglichst kleines Set, aus den realen Daten zu extrahieren.
7.4 Debugging
Die sehr nützlichen Operatoren DESCRIBE, DUMP und EXPLAIN haben wir ja bereits
kennengelernt.
Außerdem gibt es für das Debugging die Operation ILLUSTRATE, die den Beispieldaten-Generator
benutzt, um ein kleines Datensample zu erzeugen, mit dem die Ausführung eines Skripts Schritt für
Schritt illustriert wird.
Leider wird dieses Feature im Moment nicht mehr gewartet oder weiterentwickelt. Es könnte also in
zukünftigen Versionen entfallen12.
Ein kleines Beispiel:
grunt> test2 = LOAD 'test2.txt' AS (key:long,value:chararray);
grunt> grouped = GROUP test2 BY key;
grunt> ILLUSTRATE grouped;
| test2
| key: long | value: chararray
|
-----------------------------------------------|
| 1
| w
|
|
| 1
| v
|
12 Laut [PigLatin], Abschnitt über ILLUSTRATE.
Seminar MapReduce: Pig Latin
18/21
Jürgen Kofer
---------------------------------------------------------------------------------------------_------------------------------| grouped
| group: long
| test2: bag({key: long,value: chararray})
|
-----------------------------------------------------------------------------|
| 1
| {(1, w), (1, v)}
|
------------------------------------------------------------------------------
Zu beachten ist: ILLUSTRATE benötigt unbedingt ein Datenschema und die Operationen LIMIT
und SPLIT dürfen im Skript nicht vorkommen!
8 Bewertung und Vergleich mit anderen Konzepten
8.1 Anwendungsszenarien
Da Pig im Hintergrund MapReduce Jobs ausführt, liegt es in der Natur der Sache, dass Pig überall
da Vorteile gegenüber einem relationalen Datenbanksystem hat, wo riesige Datenmengen anfallen.
Im speziellen da, wo täglich neue Daten anfallen und einfach „hinten“ dazu gehängt werden, wie
etwa bei Log-Daten von Web-Diensten. Wo also nie etwas gelöscht oder aktualisiert wird.
Das gilt aber eben ganz allgemein für Hadoop und ähnliche Systeme. Hier ein paar Szenarien, bei
denen der Einsatz von Pig Latin Vorteile birgt gegenüber prozedural implementierten MapReduce
Jobs:
•
•
•
•
•
Wenn laufend verschiedene Aggregationen über riesige Datenmengen gefahren werden
müssen. Pig Latin bietet mit den eingebauten Aggregationsfunktionen, wie COUNT, MAX
und MIN, eine flexible Lösung. Ein Beispiel wäre das Analysieren von Log-Daten um die
durchschnittliche Antwortzeit eines Servers zu ermitteln. Oder Besucherstatistiken.
Wenn es darum geht verschiedene Daten zu vergleichen. Die JOIN und GROUP
Operationen erleichtern solche Aufgaben ungemein. Das wird zum Beispiel oft bei
temporalen Analysen benötigt, wenn verschiedene Zeitbereiche miteinander verglichen
werden sollen. Etwa Besucherstatistiken nach Tagen aufgeschlüsselt.
Wenn Daten in einem Hadoop Cluster liegen und Ad Hoc Auswertungen notwendig sind.
Zum Beispiel wenn Kunden kurzfristig Statistiken darüber benötigen, wie oft ihre Werbung
gesehen wurde.
Wenn Hadoop die adäquate Lösung wäre, aber keine Entwickler da sind um die MapReduce
Jobs prozedural zu implementieren. Oder wenn viele Analytiker da, sind die sich mit SQL
auskennen, aber nicht Java programmieren können und wollen.
Wo riesige Mengen von Daten verarbeitet werden müssen, die korrupt oder unvollständig
sind. Pig ist sehr tolerant und robust beim Umgang mit den Eingangsdaten und kann auch
noch Datenschemas anwenden, wo Daten fehler- und lückenhaft sind.
Eine ausführliche Übersicht über Usage Scenarios bei Yahoo, wo Pig entwickelt wurde und
entsprechend intensiv genutzt wird, findet sich bei [ORU+08].
8.2 Vergleich mit SQL
Der größte Unterschied zu SQL ist sicher der, dass SQL rein deklarativ beschreibt was man als
Ergebnisrelation haben will, während man bei Pig Latin Schritt für Schritt die Ausgangsrelationen
Seminar MapReduce: Pig Latin
19/21
Jürgen Kofer
transformieren und überführen muss in die gewünschte Ergebnisrelation. Wie bereits erwähnt ist
Pig Latin eine Datenfluss-Sprache, irgendwo zwischen prozeduraler Programmierung und SQL
angesiedelt.
Ein weiterer Unterschied betrifft das Datenschema: Bei relationalen Datenbanken wird das Schema
beim Schreiben von Daten validiert und angewendet. Diese Vorgehensweise wird auch schema on
write13 genannt. Schemas die in Pig Latin definiert wurden, werden allerdings erst beim Lesen
angewendet. Das wird schema on read genannt und erlaubt das Verarbeiten von beliebig
strukturierten Ausgangsdaten. Allerdings ist schema on read deutlich rechenintensiver, was aber
kaum etwas ausmacht, da Abfragen in den typischen Anwendungsfällen nicht zeitkritisch sind.
Noch ein wichtiger Unterschied: Pig Latin hat keine Befehle zur Manipulation von Daten (DML,
Data Manipulation Language), wie INSERT oder UPDATE. Updates sind prinzipiell nicht möglich,
weil das Hadoop File-System HDFS [HDFS] ein write-once-read-many Modell verwendet und nur
in ganzen Blöcken arbeitet, die einmal geschrieben nicht mehr verändert werden können.
Siehe dazu auch den Vergleich mit SQL bei [White11], Seite 328.
8.3 Vergleich mit Hive
Hive [Hive] wurde von Facebook14 aus ähnlichen Gründen ins Leben gerufen, wie Pig von Yahoo.
Es sollte Analytikern mit viel SQL Erfahrung ermöglicht werden, die gigantischen Log-Daten, die
täglich bei Facebook anfallen und in einem Hadoop Cluster liegen, zu durchsuchen.
Hive ist allerdings sehr viel näher bei SQL angesiedelt als Pig Latin. HiveQL [HiveQL], die
Abfragesprache von Hive, ist sogar ein Subset des SQL-Standards SQL-92 [SQL92]. Der
Analytiker beschreibt also, genau wie bei SQL, deklarativ, was er als Ergebnisrelation haben
möchte.
Das bedeutet aber auch, dass Entwickler weniger Kontrolle über den Ablauf, über den Weg zur
Gewinnung eines Ergebnisses, haben, als bei Pig Latin. Dafür ist HiveQL für Mitarbeiter mit SQL
Hintergrund einfacher zu erlernen.
Wie Pig, bietet Hive ebenfalls die Möglichkeit die Abfragesprache mit User Defined Function's zu
erweitern.
Auch Hive verwendet schema on read, um das Datenschema festzulegen. Allerdings existieren sehr
wohl Data Definition Language (DDL) Befehle wie CREATE TABLE, die bei Pig Latin gänzlich
fehlen. Die Informationen über das Schema einer Tabelle werden aber extern als Metadaten
angelegt und nicht direkt auf die Daten angewendet. Beim Laden von Daten ist Hive nicht so
tolerant wie Pig und korrupte Daten führen dazu, dass das Laden fehlschlägt.
Von den DML Befehlen bei SQL kennt Hive nur den INSERT Befehl, der kopiert die Daten einfach
in den internen Hive Datastore. So etwas wie UPDATE kann es natürlich auch nicht geben.
Siehe dazu auch den Vergleich mit Hive hier bei [White11], Seite 329.
13 Vgl. [White11], Seite 376
14 http://www.facebook.com/
Seminar MapReduce: Pig Latin
20/21
Jürgen Kofer
8.4 Bewertung
Als erstes fällt bei Pig der unglaublich schnelle Einstieg positiv auf. Wenn man sich auf den local
Mode beschränkt, reicht es, das ZIP File zu entpacken und schon kann man anfangen zu
experimentieren. Auch der Anschluss an das Hadoop Filesystem und den MapReduce Cluster
gestaltet sich sehr einfach.
Das mitgelieferte, interaktive Entwicklungswerkzeug grunt ist absolut ausreichend für Ad-hoc
Abfragen und um Skripte inkrementell aufzubauen. Die Debugging-Tools wie DESCRIBE und
ILLUSTRATE erweisen sich als sehr nützlich dabei.
Pig ist für die typischen Anwendungsfälle, wie das Analysieren großer Mengen von Log-Dateien,
sehr gut geeignet und bei Yahoo dahingehend auch schon jahrelang erprobt. Gegenüber Hive bietet
es vor allem da Vorteile, wo mehr Kontrolle über den Ablauf der Datentransformationen notwendig
oder erwünscht ist. Wenn also ein Algorithmus nicht in der Lage sein kann die ideale Reihenfolge
der Verarbeitungsschritte zu erkennen.
Beim Aufbau eines Hadoop Datastores erpart Pig Latin sehr viel Arbeit und Zeit, da die StandardDatenverarbeitungsschritte nicht erst in einer imperativen Sprache programmiert werden müssen.
So kommt man recht schnell zu Ergebnissen und kann produktiv arbeiten.
Allerdings wird man kaum ohne Java-Programmierer auskommen, denn mit hoher
Wahrscheinlichkeit wird man die eine oder andere Funktion (UDF) selber schreiben müssen. Wenn
die Funktionen allgemein gehalten werden, können sie aber sehr gut wiederverwendet werden.
Alles in allem ein interessanter Ansatz und ein wertvolles Projekt im Hadoop-Umfeld.
9 Referenzen
[ORU+08] Christopher Olston ; Benjamin Reed ; Utkarsh Srivastava ; Ravi Kumar ; Andrew
Tomkin: Pig Latin: A Not-So-Foreign Language for Data Processing. Yahoo, 2008.
http://research.yahoo.com/files/sigmod08.pdf
[White11] Tom White: Hadoop: The Definitive Guide. O'Reilly, 2011. Kapitel Pig, Seiten 321 –
364.
[DG04] Jeffrey Dean ; Sanjay Ghemawat: MapReduce: Simplified Data Processing on Large
Clusters. http://labs.google.com/papers/mapreduce.html. Google Inc., 2004.
[HadoopMR] http://wiki.apache.org/hadoop/HadoopMapReduce
[WikiMR] http://en.wikipedia.org/wiki/MapReduce
[Pig] http://pig.apache.org/
[PigSetup] http://pig.apache.org/docs/r0.8.1/setup.html
[PigLatin] http://pig.apache.org/docs/r0.8.1/piglatin_ref2.html
Seminar MapReduce: Pig Latin
21/21
[PigUDF] http://pig.apache.org/docs/r0.8.1/udf.html
[PigToAp] http://developer.yahoo.com/blogs/hadoop/posts/2007/11/pig_into_incubation/
[PigBug919] https://issues.apache.org/jira/browse/PIG-919
[PigPhil] http://pig.apache.org/philosophy.html
[PigPen] http://wiki.apache.org/pig/PigPen
[PigDGen] http://wiki.apache.org/pig/DataGeneratorHadoop
[Hadoop] http://hadoop.apache.org/
[HaFsShell] http://hadoop.apache.org/common/docs/r0.20.0/hdfs_shell.html
[HDFS] http://hadoop.apache.org/common/docs/r0.20.2/hdfs_user_guide.html
[Hive] http://hive.apache.org
[HiveQL] http://wiki.apache.org/hadoop/Hive/HiveQL
[WJavaGen] http://en.wikipedia.org/wiki/Generics_in_Java
[SQL92] http://en.wikipedia.org/wiki/SQL-92
Jürgen Kofer
FernUniversität in Hagen
Seminar 01912
im Sommersemester 2011
„MapReduce und Datenbanken“
Thema 9
SCOPE
Referentin: Silke Rüter
1 / 14
1 Einleitung
Im Zeitalter von Internet wächst der Bedarf, riesige Mengen an Daten, die auf verschiedenen
Rechnern verteilt sind, zu analysieren. Beispiele hierfür sind das Suchen nach bestimmten
Begriffen, das Zählen von Zugriffen auf bestimmte URLs oder das Durchsuchen von LogDateien für weitere Auswertungen. Ziele solcher Analysen können beispielsweise bessere
Unterstützung von Services, Bereitstellung neuer Features oder die Aufdeckung betrügerischer
Aktivitäten sein. In diesem Zusammenhang ist ein Programmiermodell gefragt, das auf einfache
und effiziente Art und Weise erlaubt, Auswertungen riesiger Datenmengen auf hunderten oder
tausenden miteinander vernetzten Rechnern parallel durchzuführen.
Die Sprache SCOPE von Microsoft soll diese Anforderungen erfüllen. SCOPE ist die Abkürzung
für „Structured Computations Optimized for Parallel Execution“. Dahinter verbirgt sich eine
deklarative und erweiterbare Script-Sprache, die auf einfache Art und Weise die Analyse von
sehr großen Datenmengen ermöglicht. Ähnlichkeiten mit SQL vereinfachen das Erlernen und die
Anwendung von SCOPE. Die Möglichkeit C#-Ausdrücke und -Bibliotheken einzubinden
machen SCOPE zudem zu einer flexiblen und mächtigen Sprache.
Die vorliegende Seminararbeit stellt SCOPE auf Basis von [CJL+08] vor. Bevor auf die Sprache
und ihre Elemente eingegangen wird, wird zunächst die zugrundeliegende Software-Plattform
Cosmos beschrieben. An einem kleinen Beispiel wird zum Schluss die Anwendung und
Arbeitsweise von SCOPE veranschaulicht.
2 Cosmos: Die Basis-Software-Plattform
Die Basis von SCOPE ist die von Microsoft entwickelte Software-Plattform Cosmos, die im
Folgenden beschrieben wird. Sie dient dazu, große Datensätze zu speichern und zu analysieren.
Cosmos wurde so konzipiert, dass sie auf großen Clustern, die aus tausenden von Servern
bestehen, laufen kann.
Beim Design von Cosmos wurde gemäß [CJL+08] auf die folgenden Aspekte besonderer Wert
gelegt:
•
Verfügbarkeit
Um Datenverlust durch Hardware-Ausfälle so gut wie möglich zu vermeiden, werden die
Daten mehrfach im System repliziert und durch eine Quorum-Gruppe1 von 2ƒ+1 Servern
organisiert, so dass ƒ Ausfälle verkraftet werden können.
•
Zuverlässigkeit
Die Zuverlässigkeit wird durch regelmäßiges Überprüfen der Systemdaten anhand von
Prüfsummen gewährleistet. Die Prüfung findet jeweils vor der Nutzung der Daten statt.
•
Skalierbarkeit
Cosmos wurde von vorne herein so ausgelegt, dass es Petabytes2 von Daten speichern
und verarbeiten kann. Die Kapazität kann dabei einfach durch das Hinzufügen weiterer
Server vergrößert werden.
1 Unter Quorum versteht man „eine Komponente des Cluster Managers eines Computerclusters zur Wahrung der
Datenintegrität im Fall eines Teilausfalls“ [Quorum].
2 1 Petabyte = 1015 Bytes
2 / 14
•
Performanz
Cosmos läuft auf Clustern von tausenden Servern, auf denen die Daten verteilt sind. Ein
Job wird auf möglichst viele CPUs aufgeteilt, so dass er entsprechend schnell
abgearbeitet werden kann.
•
Kosten
Cosmos verteilt die Arbeit auf eine hohe Anzahl von Servern, weswegen die Leistung
eines einzelnen Servers bei der Abarbeitung eines Jobs nicht so sehr ins Gewicht fällt.
Daher können für die Server leistungsschwächere und somit preiswertere Rechner
hergenommen werden. In Summe ist es dadurch günstiger, die Last auf viele preiswerte
Server zu verteilen, als auf wenige leistungsstarke, die entsprechend teuer sind.
Die folgende Abbildung (aus [CJL+08], S. 1266) zeigt den Aufbau der Cosmos SoftwarePlattform.
Die einzelnen Komponenten der Plattform werden im Folgenden beschrieben.
2.1 Cosmos Storage System
Das Cosmos Storage System ist ein „verteiltes Ablagesystem, entworfen, um zuverlässig und
effizient extrem große sequentielle Dateien zu speichern“ ([CJL+08], S. 1267).
Es handelt sich dabei um ein append-only Dateisystem. Petabytes von Daten können dort
zuverlässig gespeichert werden. Für deren sequentielle Ein- und Ausgaben wurde das System
außerdem optimiert. Gleichzeitige Schreibzugriffe werden vom System automatisch serialisiert.
Die Daten werden verteilt, repliziert und darüber hinaus auch noch komprimiert um
Speicherplatz zu sparen und gleichzeitig den Datenfluss zu erhöhen.
Das Cosmos Storage System erlaubt fortlaufende Dateien unbegrenzter Größe zu speichern. Eine
Datei besteht dabei aus einer Folge von sog. Extents. Extents bezeichnen „die Einheit von
allokiertem Speicher und sind üblicherweise einige 100 Megabytes groß“ ([CJL+08], S. 1267).
Die Daten innerhalb so eines Extents werden in komprimierter Form in Blöcken abgespeichert.
Komprimieren und Dekomprimieren erfolgen transparent auf dem Client.
In eine Datenbearbeitung sind in der Regel einige wenige nebeneinander angeordnete Extents
3 / 14
einbezogen.
2.2 Cosmos Execution Environment
Das Cosmos Execution Environment „stellt eine Programmierschnittstelle und ein
Laufzeitsystem zur Verfügung, das automatisch Optimierungsdetails, Fehlertoleranz, Aufteilung
der Daten, Ressourcen Verwaltung und Parallelisierung vornimmt“ ([CJL+08], S. 1267).
Eine Applikation wird als ein gerichteter azyklischer Graph modelliert. Die Knoten stellen dabei
die einzelnen Prozesse dar und die Kanten den zugehörigen Datenfluss. Der sog. Job Manager ist
dafür zuständig, den Graphen zu konstruieren und die Abarbeitung der einzelnen Prozessknoten
innerhalb einer Applikation zu koordinieren. Dazu plant er die Abarbeitung eines Knotens ein,
sobald die Eingabedaten für diesen bereitstehen, protokolliert den Fortschritt der Abarbeitung
und veranlasst im Fehlerfall die wiederholte Ausführung des Teilgraphen, in dem der Fehler
aufgetreten ist.
2.3 Die SCOPE-Komponenten
Das SCOPE System besteht aus den Komponenten Compiler, Optimizer und Runtime und setzt
auf der Software-Plattform Cosmos auf.
SCOPE Runtime stellt einige gebräuchliche Operatoren bereit, auf die der Anwender in seinem
SCOPE-Script zurückgreifen kann, ohne sie selbst nochmals implementieren zu müssen.
Die Aufgabe des SCOPE Optimizers und Compilers ist es, das SCOPE-Script in einen
effizienten parallelen Ausführungsplan zu übersetzen.
3 SCOPE: Die Script-Sprache
SCOPE ist die Abkürzung für „Structured Computations Optimized for Parallel Execution“.
Es handelt sich dabei um eine deklarative und erweiterbare Script-Sprache, die auf einfache Art
und Weise die Analyse von sehr großen Datenmengen ermöglicht.
Der deklarative Ansatz der Sprache erlaubt dem Anwender, sich bei seiner Problemlösung auf
die eigentliche Datentransformation zu konzentrieren, ohne sich dabei um die Komplexität der
zugrundeliegenden Software-Plattform Cosmos kümmern zu müssen.
Im Folgenden werden zunächst ein paar allgemeine Charakteristika von SCOPE beschrieben.
Anschließend wird etwas näher auf SCOPE-Kommandos und deren Verwendung in SCOPEScripts eingegangen.
3.1 Allgemeine Charakteristika
3.1.1
Ähnlichkeiten mit SQL
SCOPE hat einige Ähnlichkeiten mit SQL. Dies gilt sowohl für die Organisation der Daten wie
auch für die Kommandosyntax.
Die Daten basieren - wie bei relationalen Datenbanken - auf einem wohl definierten Schema. Ein
Datensatz besteht somit aus einer Zeile, die sich aus Spaltenwerten zusammensetzt.
Sowohl der Kommadoumfang wie auch die Kommandosyntax sind an SQL angelehnt. So gibt es
beispielsweise ein Select-Statement, innere und äußere Joins, Aggregation und Views. Letztere
erlauben unter anderem den Zugriff auf sensible Daten einzuschränken.
4 / 14
Die Ähnlichkeiten mit SQL erleichtern nicht nur das Erlernen der Sprache für Anwender, die
über SQL-Kenntnisse verfügen, sondern ermöglichen auch die einfache Portierung bestehender
SQL-Scripts nach SCOPE.
3.1.2
Einbindung von C#
SCOPE bietet die Möglichkeit C#-Ausdrücke und -Bibliotheken in die Kommandos
einzubinden, was die Flexibilität der Anwendung der Sprache gegenüber Standard-SQL enorm
erhöht. Das heißt, alle Funktionen und Operatoren, die in C# zur Verfügung stehen, sind auch in
SCOPE verfügbar und können somit in SCOPE-Scripts genutzt werden. Dies gilt für skalare
Ausdrücke und Prädikate, wie auch für Funktionen und Operatoren, egal ob Standard oder
Eigenimplementierung.
In eingebundene C#-Klassen können beispielsweise anwendungsspezifische Berechnungen oder
auch die Bearbeitung ganzer Datensätze ausgelagert werden.
Zu den Operatorklassen, die in den in Kapitel 3.2 beschriebenen SCOPE-Kommandos
eingebunden und anwendungsspezifisch angepasst werden können, gehören die Folgenden:
•
Extractor
Dient dazu, aus der Eingabequelle (z.B. Text-Datei, Datenbank) die gewünschten Datensätze
zu extrahieren und zu konstruieren, die dann im SCOPE-Kommando weiter verarbeitet
werden sollen.
•
Outputter
Wandelt das Ergebnis der Datenauswertung in das gewünschte Ausgabeformat um (z.B. TextDatei, Datenbank).
•
Processor
Kümmert sich um die zeilenweise Bearbeitung der Daten.
•
Reducer
Reduziert, d.h. fasst die Zwischenergebnisse zusammen.
•
Combiner
Fügt die Ergebnisse mehrerer Eingabequellen zusammen.
Diese Operatoren nehmen also Daten-Transformationen vor. Ein Operator nimmt in der Regel
einen oder mehrere Datensätze entgegen, bearbeitet diese und gibt anschließend einen Datensatz
als Ergebnis wieder zurück.
Für Implementierungsbeispiele zu den Operatoren siehe auch [CJL+08].
Auf die Verwendung der genannten Operatoren innerhalb eines SCOPE-Kommandos wird in den
folgenden Abschnitten noch weiter eingegangen.
3.2 SCOPE-Kommandos
Prinzipiell besteht ein SCOPE-Script aus einer Folge von Kommandos. Die Ausgabe eines
Kommandos ist normalerweise die Eingabe des nachfolgenden.
Im Folgenden werden einige der Kommandos und der darin optional verwendbaren Operatoren
bzw. Operatorklassen beschrieben.
5 / 14
3.2.1
Bereitstellung der Eingabedaten: EXTRACT
Wie bereits erwähnt, müssen die Eingabedaten eines SCOPE-Scripts in einem wohldefinierten
Schema vorliegen, damit sie ausgewertet werden können. Aus welcher Quelle diese Daten
kommen ist allerdings unerheblich. Die Daten können beispielsweise aus Text-Dateien oder auch
aus Datenbanken stammen.
Die Überführung der Daten in ein Format, das ausgewertet bzw. weiter verarbeitet werden kann,
ist die Aufgabe eines Operators, dem sog. Extractor. Standardmäßig bietet SCOPE bereits welche
für normale Text-Dateien wie auch für Log-Dateien an. Es können aber auch
anwendungsspezifische in C# geschrieben werden, indem einfach die Klasse „Extractor“
überschrieben und entsprechend angepasst wird.
Mit dem SCOPE-Kommando EXTRACT und dem entsprechenden Extractor können die
Eingabedaten für nachfolgende SCOPE-Kommandos wie folgt gewonnen werden ([CJL+08], S.
1268):
EXTRACT column[:<type>] [, ...]
FROM <input_stream(s)>
USING <Extractor> [(args)]
[HAVING <predicate>]
Das Ergebnis ist ein Datensatz bestehend aus Zeilen und Spalten, auf dem im SCOPE-Script
Auswertungen vorgenommen werden können.
3.2.2
Die Auswertung: SELECT und JOIN
Nachdem mit dem EXTRACT-Kommando die Eingabedaten im richtigen Format vorliegen,
kann die Auswertung der Daten beginnen. Dazu stellt SCOPE das Kommando SELECT bereit.
Anstatt mit dem EXTRACT-Kommando explizit die Daten im gewünschten Format
bereitzustellen, kann dies aber auch alternativ innerhalb des Selects per eingebundenem
Extractor gemacht werden.
Die Syntax des SELECT-Kommandos ist an die von SQL angelehnt und lautet wie folgt
([CJL+08], S. 1268):
SELECT [DISTINCT] [TOP count] select_expression [AS <name>] [, ...]
FROM {
<input stream(s)> USING <Extractor> |
{<input> [<joined input> [...]]} [, ...]
}
[WHERE <predicate>]
[GROUP BY <grouping_columns> [, ...]]
[HAVING <predicate>]
[ORDER BY <select_list_item> [ASC | DESC] [, ...]]
joined input:
<join_type> JOIN <input> [ON <equijoin>]
join_type:
[INNER | {LEFT | RIGHT | FULL] OUTER]
Im Gegensatz zu SQL sind hier allerdings keine Sub-Selects erlaubt. Diese Einschränkung wird
6 / 14
aber durch die Möglichkeit von Outer-Joins wieder entschärft, da dadurch das Ergebnis des
ersten Selects entsprechend weiter eingeschränkt werden kann.
Zur Aggregation der Daten stehen Funktionen wie COUNT, COUNTIF, MIN, MAX, SUM,
AVG, STDEV, VAR, FIRST und LAST bereit.
3.2.3
Die Ausgabe: OUTPUT
Das Ergebnis der Auswertung kann anschließend mit einem sog. Outputter in das gewünschte
Format der Datensenke überführt werden. Diese kann – analog zur Datenquelle – wieder beliebig
sein, also beispielsweise eine Text-Datei oder auch eine Datenbank. Standardmäßig wird das
Ergebnis als Text ausgegeben.
Mit dem SCOPE-Kommando OUTPUT und dem entsprechenden Outputter als Operator kann
das Auswertungsergebnis wie folgt in die gewünschte Ausgabe überführt werden ([CJL+08], S.
1268):
OUTPUT [<input> [PRESORT column [ASC | DESC] [, ...]]]
TO <output_stream>
[USING <Outputter> [(args)]]
3.2.4
Anwendungsspezifische Kommandos
PROCESS, REDUCE und COMBINE sind drei weitere SCOPE-Kommandos, die ebenfalls
durch build-in C#-Komponenten anwendungsspezifisch erweitert werden können. Sie können
zusammen mit dem SELECT-Kommando verwendet werden und bieten damit vielfältige
Möglichkeiten die Daten zu filtern, zu verbinden, zu berechnen oder zu aggregieren.
Damit übernehmen sie die selben Aufgaben wie die Operationen map, reduce und merge im
MapReduce Programmiermodell (siehe auch [DG08]) .
Im Folgenden wird näher auf diese drei Kommandos eingegangen.
3.2.4.1 PROCESS
Das PROCESS-Kommando dient dazu, einen Datensatz zu bearbeiten und/oder umzuformen.
Die eigentliche Arbeit übernimmt hierbei der sog. Processor, der - genau wie die bereits
beschrieben Operatorklassen - ebenfalls überschrieben und damit an die Bedürfnisse der
Anwendung angepasst werden kann.
Der Processor nimmt genau eine Zeile des Datensatzes entgegen, bearbeitet diese und gibt dann
keine, eine oder auch mehrere Zeilen wieder zurück.
Ein Beispiel für die Anwendung des PROCESS-Kommandos ist, einen Eingabe-Such-String in
eine Folge von einzelnen Wörtern aufzuteilen. Die Ausgabe des Kommandos wäre dann eine
Menge von Zeilen, die jeweils eins dieser Wörter enthält, ggf. mit zusätzlichen Informationen,
die für die weitere Verwendung benötigt werden.
Die Kommandosyntax sieht wie folgt aus ([CJL+08], S.1270):
PROCESS [<input>]
USING <Processor> [(args)]
[PRODUCE column [, ...]]
[WHERE <predicate>]
[HAVING <predicate>]
7 / 14
3.2.4.2 REDUCE
Das REDUCE-Kommando nimmt einen Datensatz, der nach bestimmten Kriterien gruppiert ist,
entgegen. Die einzelnen Zeilen des Datensatzes werden dann pro Gruppe bearbeitet und als
Ergebnis zurückgegeben.
Die eigentliche Arbeit übernimmt auch hier wieder eine eigene Operatorklasse, der sog. Reducer,
der auch wieder anwendungsspezifisch angepasst werden kann.
Ein einfaches Beispiel für die Anwendung des REDUCE-Kommandos ist die Anzahl der Zeilen
pro Gruppe zu zählen und die dadurch entstandene Zusammenfassung wieder zurückzugeben.
Die Kommandosyntax sieht wie folgt aus ([CJL+08], S.1270):
REDUCE [<input> [PRESORT column [ASC | DESC] [, ...]]]
ON grouping_column [, ...]
USING <Reducer> [(args)]
[PRODUCE column [, ...]]
[WHERE <predicate>]
[HAVING <predicate>]
3.2.4.3 COMBINE
Das COMBINE-Kommando nimmt zwei Datensätze entgegen und fügt diese nach bestimmten
Kriterien zusammen. Das Zusammenfügen der Datensätze erfolgt im sog. Combiner, der
ebenfalls an die Bedürfnisse der Anwendung angepasst werden kann. Das Ergebnis dieses
Kommandos ist eine Menge von neuen Zeilen.
Ein Beispiel für die Anwendung des COMBINE-Kommandos ist die Berechnung der Differenz
bestimmter Spaltenwerte von zwei Datensätzen.
Die Kommandosyntax sieht wie folgt aus ([CJL+08], S.1271):
COMBINE [<input1> [AS <alias1>] [PRESORT ...]
WITH [<input2> [AS <alias2>] [PRESORT ...]
ON <equality_predicate>
USING <Combiner> [(args)]
[PRODUCE column [, ...]]
[WHERE <predicate>]
[HAVING <expression>]
3.2.5
Import-Scripts: Das View-Konzept von SCOPE
In SQL sind Views quasi in der Datenbank abgespeicherte Abfragen. Beim Zugriff auf eine View
werden die zu diesem Zeitpunkt gültigen Ergebnisse dieser Datenbankabfrage bereitgestellt.
Etwas vergleichbares gibt es auch in SCOPE.
Dem Ergebnis eines SCOPE-Kommandos kann ein Name zugewiesen werden, über den es im
weiteren Verlauf dann referenziert und somit in folgenden SCOPE-Kommandos weiterverwendet
werden kann. Dieses benannte Ergebnis entspricht einer View in SQL.
Das IMPORT-Kommando von SCOPE erlaubt es, ein so benanntes Ergebnis in einem anderen
Kommando zu verwenden.
8 / 14
Die Kommandosyntax sieht wie folgt aus ([CJL+08], S.1271):
IMPORT <script_file>
[PARAMS <par_name> = <value> [, ...]]
Der Unterschied zu einem SQL-View ist, dass das Import-Script parametrisiert werden kann.
Ein Beispiel für ein Import-Script ist das Heraussuchen von bestimmten Abfragen aus einer LogDatei und Berechnung der Häufigkeit pro Abfrage. Ein Parameter dieses Scripts wäre die zu
durchsuchende Log-Datei. Per weiterem Parameter könnte dann noch abgefragt werden, welche
Abfragen mehr oder weniger als eine bestimmte Anzahl vorgenommen wurden.
Import-Scripts sind ein mächtiges Instrument von SCOPE.
Sie bieten zum einen Modularität und unterstützen gleichzeitig das Geheimnisprinzip und die
Wiederverwendung von Code. Zum anderen können sie aber auch dazu genutzt werden, um
beispielsweise Daten vor unbefugtem Zugriff zu schützen.
4 Ein Beispiel
Im Folgenden soll an einem Beispiel die Umsetzung einer Auswertung mit einem SCOPE-Script
erläutert werden.
4.1 Die Aufgabe
Aufgabe des angegebenen SCOPE-Scripts ist es, in der Log-Datei „search.log“ die Abfragen
herauszusuchen, die mindestens 1000 mal abgesetzt worden sind.
Da es sich um eine Text-Datei handelt, wird der Standard-Extractor „LogExtractor“ genutzt, um
aus der Log-Datei die Abfragen zu extrahieren.
Die Ausgabe des Scripts besteht aus zwei Spalten. Die eine Spalte enthält die Abfrage und die
andere die jeweils zugehörige Anzahl der Aufrufe. Die Ausgabe wird absteigend nach der Anzahl
der Aufrufe sortiert und in die Cosmos-Datei „qcount.result“ geschrieben. Da die Ausgabe im
Textformat erfolgen soll, kann der Standard-Outputter hergenommen werden.
4.2 Das SCOPE-Script
Die beschriebene Aufgabe kann schrittweise mit folgendem Script bewerkstelligt werden (aus
[CJL+08], S.1266):
e = EXTRACT query
FROM "search.log"
USING LogExtractor;
s1 = SELECT query, COUNT(*) as count
FROM e
GROUP BY query;
s2 = SELECT query, count
FROM s1
WHERE count > 1000;
9 / 14
s3 = SELECT query, count
FROM s2
ORDER BY count DESC;
OUTPUT s3 TO "qcount.result";
Das Script enthält insgesamt fünf SCOPE-Kommandos. Wie bereits erwähnt, ist das Ergebnis
eines SCOPE-Kommandos in der Regel die Eingabe des folgenden SCOPE-Kommandos.
Hier werden zunächst die Abfragen mit Hilfe des „LogExtractors“ aus der Log-Datei
„search.log“ extrahiert und dem Namen e zugewiesen. Anschließend werden in s1 die
extrahierten Abfragen in e gruppiert und aufsummiert. In s2 werden dann alle Abfragen, die
mehr als 1000 mal aufgerufen wurden, herausgesucht. s3 sortiert dann nur noch das Ergebnis von
s2 absteigend nach Anzahl der Aufrufe. Zum Schluss wird das Gesamtergebnis mit Hilfe des
Kommandos OUTPUT in die Datei „qcount.result“ geschrieben. Da kein Outputter explizit
angegeben worden ist, wird der Standard hergenommen, d.h. es erfolgt eine Ausgabe im
Textformat.
Die obigen Schritte e, s1, s2 und s3 kann man alternativ auch in einem SELECT
zusammenfassen, so dass folgendes kurzes Script entsteht (aus [CJL+08], S.1266):
SELECT query, COUNT() AS count
FROM "search.log"
USING LogExtractor
GROUP BY query
HAVING count > 1000
ORDER BY count DESC;
OUTPUT TO "qcount.result";
4.3 Compilierung und Optimierung
Zunächst parsed der Compiler das Script, überprüft die Syntax und löst Namen auf. Das Ergebnis
des Compile-Vorgangs ist ein sog. interner Parse-Baum. Dieser kann direkt in einen
Ausführungsplan überführt werden. Letzterer kann dann vom Optimizer anschließend nochmal
optimiert werden, wobei die beste Ausführungsstrategie ermittelt wird. Für die Optimierung
gelten die gleichen Regeln, wie auch für die Optimierung von Datenbankabfragen, wie
beispielsweise das Löschen von nicht benötigten Spalten, frühestmögliche Aggregation und das
Zurückstellen von Auswahlkriterien.
4.4 Der Ausführungsplan
Der Ausführungsplan zum obigen Beispiel könnte wie folgt aussehen (entnommen aus
[CJL+08], S. 1272):
10 / 14
Er besteht aus acht Abschnitten, in denen gemäß [CJL+08] (S.1270) folgendes gemacht wird:
1. Extrahieren
Die zu durchsuchende Log-Datei ist auf viele verschiedene Dateien verteilt. Daher laufen
parallel entsprechend viele Extractors um die jeweiligen Datei-Fragmente einzulesen.
2. Partielle Aggregation
Bei Rechnern, die im selben Rack laufen, werden die Ergebnisdaten der Extractors
bereits partiell aggregiert um das Datenvolumen zu reduzieren.
3. Verteilung
Die bisherigen Ergebnisse der vorherigen Schritte, die jetzt pro Rack vorliegen, werden
schon mal nach den einzelnen Abfragen gruppiert.
4. Volle Aggregation
Alle Ergebnisse werden nun pro Abfrage parallel voll aggregiert, bleiben aber immer
noch auf mehrere Server verteilt.
5. Filterung
Nachdem jetzt alle Ergebnisse voll aggregiert sind, werden die herausgefiltert, die
entsprechend der Bedingung, weniger als 1000 mal aufgerufen wurden.
6. Sortierung
Die verbliebenen Ergebnisse werden nun absteigend nach der Anzahl der Aufrufe sortiert.
7. Zusammenfügen
Die Ergebnisse, die parallel auf mehreren Servern ermittelt worden sind, werden nun auf
einem Server zum Endergebnis zusammengefügt.
11 / 14
8. Ausgabe
Das Endergebnis wird als Cosmos-Datei in Textform bereitgestellt.
Der beschriebene Ausführungsplan wird an das Cosmos Execution Environment (siehe Kapitel
2.2) übergeben. Dort werden alle notwendigen Ressourcen bereitgestellt und die Ausführung
geplant. Der Job Manager überwacht die Ausführung und veranlasst im Fehlerfall die
Wiederholung der fehlgeschlagenen Ausführung.
5 SCOPE und MapReduce
Ein alternatives Programmiermodell, das ebenfalls die Analyse von riesigen verteilten
Datenmengen erlaubt, ist MapReduce von Google (Details siehe auch [DG08]).
Was nun MapReduce von SCOPE unterscheidet soll im Folgenden - ohne Anspruch auf
Vollständigkeit - kurz angedeutet werden.
Um mit MapReduce eine parallelisierte Auswertung großer Datenmengen vornehmen zu können,
müssen zwei Funktionen map und reduce der MapReduce-Schnittstelle implementiert werden.
Aufgabe der map Funktion ist es, Werte nach bestimmten Regeln zu gruppieren und diese
Gruppierung als Zwischenergebnisse bereitzustellen. Diese Zwischenergebnisse werden dann an
die reduce Funktion übergeben und dort aggregiert.
MapReduce liegt ein Laufzeitsystem zugrunde, das die Parallelisierung der Bearbeitung durch
Aufteilung der zu analysierenden Daten auf verschiedene Rechner bewerkstelligt. Entsprechend
werden dann auch die implementierten Funktionen auf den verschiedenen Rechnern parallel
ausgeführt.
Um also Parallelisierung bei der Datenauswertung mit MapReduce nutzen zu können, muss die
Applikation an das Programmiermodell entsprechend angepasst werden. Bei komplexeren
Analysen kann es dabei notwendig sein, dass über mehrere Stufen hinweg die Daten gruppiert
und anschließend aggregiert werden müssen, d.h. in solchen Fällen müssen mehrere map und
reduce Funktionen geschrieben werden.
Ein bisschen was von MapReduce steckt auch in SCOPE, wie bereits in Kapitel 3.2.4 kurz
erwähnt. Die Kommandos PROCESS, REDUCE und COMBINE, die anwendungsspezifisch
angepasst werden können, übernehmen genau die selben Aufgaben, wie die Funktionen map und
reduce. Sie zerlegen die Daten, gruppieren sie und aggregieren sie zum Schluss zu einem
Endergebnis.
Ein Unterschied zu MapReduce ist allerdings, dass die o.g. Kommandos erst zum Einsatz
kommen, wenn das normale SELECT zur Auswertung nicht ausreicht, d.h. wenn komplexere
Möglichkeiten benötigt werden, um Daten zu filtern, zu berechnen, zu verbinden und/oder zu
aggregieren. In den anderen Fällen kann die Auswertung mit einem „üblichen“ SELECT erledigt
werden und SCOPE übernimmt die Gruppierung und Aggregation. Bei MapReduce müssen die
beiden Funktionen map und reduce dagegen immer implementiert werden.
6 Fazit
SCOPE bietet eine gute Möglichkeit, große Datenmengen parallelisiert auszuwerten.
Die Anlehnung an SQL und die Einbindung von C# erleichtern die Erlernbarkeit und auch die
Anwendung. Anwender, die sich mit SQL und relationalen Datenbanken auskennen, finden sich
12 / 14
in SCOPE schnell zurecht.
Bei der Anwendung von SCOPE kann man sich auf die direkte Auswertung der Daten per „SQL“
konzentrieren. Erst bei komplexeren Angelegenheiten kann es notwendig werden,
anwendungsspezifische Operatoren zu programmieren.
Bei MapReduce ist das Risiko der Fehleranfälligkeit und der geringen Wiederverwendbarkeit der
implementierten Funktionen map und reduce nicht zu verachten und kann daher als gravierender
Nachteil von MapReduce gegenüber SCOPE angesehen ([CJL+08], S. 1265).
Das in Kapitel 4 zitierte Beispiel verdeutlicht zudem, wie viel kürzer man sich in SCOPE fassen
kann als in MapReduce. Dasselbe Beispiel wird in [DG08Sample] (siehe Anhang) mit
MapReduce und C++ realisiert und fällt dort deutlich länger aus.
Literaturverzeichnis
[CJL+08]:
Ronnie Chaiken, Bob Jenkins, Per-Ake Larson, Bill Ramsey, Darren Shakib,
Simon Weaver, Jingren Zhou, SCOPE: Easy and Efficient Parallel Processing of
Massive Data Sets, VLDB Endowment, 2008, 1265-1276
[Quorum]:
Quorum (Informatik), http://de.wikipedia.org/wiki/Quorum_(Informatik),
15.6.2011
[DG08]:
Jeffrey Dean and Sanjay Ghemawat, MapReduce: Simplified Data Processing on
Large Clusters, 2008, 107-113
[DG08Sample]: Jeffrey Dean and Sanjay Ghemawat, MapReduce: Simplified Data Processing
on Large Clusters, OSDI'04: Sixth Symposium on Operating System Design and
Implementation, San Francisco, CA, 2004, 1-13
13 / 14
Anhang
Beispiel WordCounter aus [DG08Sample]:
#include "mapreduce/mapreduce.h"
// User's map function
class WordCounter : public Mapper {
public:
virtual void Map(const MapInput& input) {
const string& text = input.value();
const int n = text.size();
for (int i = 0; i < n; ) {
// Skip past leading whitespace
while ((i < n) && isspace(text[i]))
i++;
// Find word end
int start = i;
while ((i < n) && !isspace(text[i]))
i++;
if (start < i)
Emit(text.substr(start,i-start),"1");
}
}
};
REGISTER_MAPPER(WordCounter);
// User's reduce function
class Adder : public Reducer {
virtual void Reduce(ReduceInput* input) {
// Iterate over all entries with the
// same key and add the values
int64 value = 0;
while (!input->done()) {
value += StringToInt(input->value());
input->NextValue();
}
// Emit sum for input->key()
Emit(IntToString(value));
}
};
REGISTER_REDUCER(Adder);
int main(int argc, char** argv) {
ParseCommandLineFlags(argc, argv);
MapReduceSpecification spec;
// Store list of input files into "spec"
for (int i = 1; i < argc; i++) {
MapReduceInput* input = spec.add_input();
input->set_format("text");
input->set_filepattern(argv[i]);
input->set_mapper_class("WordCounter");
}
// Specify the output files:
// /gfs/test/freq-00000-of-00100
// /gfs/test/freq-00001-of-00100
// ...
MapReduceOutput* out = spec.output();
out->set_filebase("/gfs/test/freq");
out->set_num_tasks(100);
out->set_format("text");
out->set_reducer_class("Adder");
// Optional: do partial sums within map
// tasks to save network bandwidth
out->set_combiner_class("Adder");
// Tuning parameters: use at most 2000 machines and 100 MB of memory per task
spec.set_machines(2000);
spec.set_map_megabytes(100);
spec.set_reduce_megabytes(100);
// Now run it
MapReduceResult result;
if (!MapReduce(spec, &result)) abort();
// Done: 'result' structure contains info about counters, time taken, number of
// machines used, etc.
return 0;
}
14 / 14
FernUniversität in Hagen
Seminar 01912
Im Sommersemester 2011
„MapReduce und Datenbanken“
Thema 10
Osprey
Referent: Markus Zander
1
Markus Zander
Thema 10 – Osprey
Inhalt
1
Einleitung ................................................................................................................................ 3
1.1
2
3
1.1.1
Data Warehouse ........................................................................................................ 3
1.1.2
Middleware ............................................................................................................... 3
1.1.3
OLAP ........................................................................................................................ 3
1.1.4
Osprey ....................................................................................................................... 3
1.1.5
Shared-Nothing-Database ......................................................................................... 3
1.2
Motivation ........................................................................................................................ 4
1.3
Ansatz ............................................................................................................................... 4
1.4
Osprey und MapReduce ................................................................................................... 4
Konzept ................................................................................................................................... 6
2.1
Teile und Herrsche ........................................................................................................... 6
2.2
Möglichst wenig Zusatzsoftware...................................................................................... 6
2.3
Verkettete Replikation ...................................................................................................... 6
2.4
Planungsalgorithmen ........................................................................................................ 6
2.5
Architektur........................................................................................................................ 6
Umsetzung .............................................................................................................................. 7
3.1
Datenmodell ..................................................................................................................... 7
3.1.1
Aufteilung der Faktentabelle ..................................................................................... 7
3.1.2
Die Datenverteilung aus Sicht des Koordinators ...................................................... 8
3.2
Abfragen auf aufgeteilten Daten ...................................................................................... 9
3.2.1
Transformation der Abfrage...................................................................................... 9
3.2.2
Aggregats-Funktionen ............................................................................................... 9
3.2.3
Ausführung von Abfragen im Überblick ................................................................ 10
3.3
4
Begriffe ............................................................................................................................. 3
Ablaufkoordination- und Planung .................................................................................. 11
3.3.1
Zufällige Verteilung ................................................................................................ 11
3.3.2
LQF ......................................................................................................................... 11
3.3.3
Majorisierung .......................................................................................................... 12
3.3.4
Vergleich der Algorithmen ..................................................................................... 12
Fehlertoleranz und Leistungsfähigkeit ................................................................................. 12
4.1
Umgang mit langsamen Knoten ..................................................................................... 12
4.2
Umgang Fehlern während der Ausführung .................................................................... 13
4.2.1
Ausfall des Koordinators ........................................................................................ 13
4.2.2
Ausfall eines Knotens ............................................................................................. 13
4.2.3
Fehler bei der Ausführung einer Teilabfrage .......................................................... 13
4.3
Experimentelle Ergebnisse ............................................................................................. 14
4.3.1
Umgebung ............................................................................................................... 14
Thema 10 – Osprey
5
6
Markus Zander
2
4.3.2
Skalierbarkeit .......................................................................................................... 14
4.3.3
Mehraufwand .......................................................................................................... 15
4.3.4
Lastausgleich ........................................................................................................... 15
4.3.5
Vergleich der Ausführungsplanungs-Algorithmen ................................................. 16
Zusammenfassung ................................................................................................................. 16
5.1
Vorteile ........................................................................................................................... 16
5.2
Kritische Betrachtung ..................................................................................................... 16
5.3
Ausblick .......................................................................................................................... 17
Literaturverzeichnis ............................................................................................................... 18
3
1
Markus Zander
Thema 10 – Osprey
Einleitung
Zur Kontrolle und Steuerung von Unternehmen ist es erforderlich, dass Daten und Kennzahlen
des Unternehmens unter unterschiedlichen Gesichtspunkten ausgewertet werden können. Hierbei
werden sogenannte OLAP-Systeme eingesetzt. OLAP steht dabei für „Online Analytical
Processing“. Die vorliegende Arbeit beschäftigt sich mit „Osprey“, einem verteilten
Datenbankmanagementsystem (DBMS), dass für OLAP-Abfragen eine Steigerung der
Performanz und der Fehlertoleranz gegenüber herkömmlichen DBMS verspricht.
1.1 Begriffe
Um dem Leser das Verständnis des Textes zu erleichtern, soll zunächst eine Auswahl der im
Folgenden verwendeten Fachbegriffe näher erläutert werden.
1.1.1 Data Warehouse
Eine logisch zentrale Speicherung einer einheitlichen und konsistenten Datenbasis zur
Entscheidungsunterstützung aller Mitarbeiter eines Unternehmens wird als Data-Warehouse
bezeichnet. Die Datenbasis eines Data-Warehouses wird getrennt von den operativen
Datenbanken des Unternehmens vorgehalten. (Mönch 2010) Bei Osprey ist damit eine
Datenbank in einem relationalen DBMS gemeint, bei dem die Daten in einem Sternschema
(siehe 3.1) angeordnet sind.
1.1.2 Middleware
Ein allgemein anwendbarer Dienst, der sich zwischen Plattformen und Anwendungen befindet,
wird als Middleware bezeichnet. (Mönch 2010) Osprey verwendet einen Koordinator, der sich
zwischen der Anwendung und den Arbeiter-Knoten (siehe 2.5) befindet. Aufgrund dieser
Zwischenschicht nennen die Entwickler von Osprey ihre Architektur einen Middleware-Ansatz,
auch wenn das nach der vorgenannten Definition nicht ganz zutreffend ist.
1.1.3 OLAP
Online Analytical Processing ermöglicht den Nutzern eines Data Warehouses die Daten unter
verschiedenen Blickwinkeln (sog. „Dimensionen“) und Darstellungsweisen zu sichten. Es dient
der dynamischen Analyse der Daten eines Unternehmens. Osprey nutzt hier insbesondere den
Aspekt, dass in OLAP-Systemen die Daten fast ausschließlich gelesen werden und die
Datenbasis üblicherweise eine Data Warehouse ist.
1.1.4 Osprey
„Osprey“ ist Englisch und bedeutet zu Deutsch „Fischadler“. In der vorliegenden Arbeit
bezeichnet Osprey ein verteiltes DBMS, das von Christine Yen unter Mitarbeit von Christopher
Yang, Ceryen Tan und Samuel R. Madden als Beitrag zur International Conference on Data
Engineering (ICDE) am 3. März 2010 unter dem Titel „Osprey - MapReduce-Style Fault
Tolerance in a Shared-Nothing Distributed Database“ vorgestellt wurde.
1.1.5 Shared-Nothing-Database
Bei einer Shared-Nothing-Database handelt es sich um ein verteiltes System, das keine
gemeinsamen Komponenten hat. Deshalb kann der Ausfall einer Komponente nicht das ganze
System stören und eine Skalierung ist typischerweise durch das Hinzufügen von weiteren
Knoten zu erreichen, da es keinen Flaschenhals gibt. Die Entwickler von Osprey ordnen das von
ihnen entwickelte System trotz der Existenz eines zentralen Koordinators als Shared-NothingDBMS ein.
Thema 10 – Osprey
Markus Zander
4
1.2 Motivation
OLAP-Abfragen beziehen sich in der Regel auf große Datenbestände. Es ist nicht unüblich, dass
eine solche Abfrage mehrere Minuten oder gar einige Stunden dauert. Tritt hierbei ein Fehler
auf, so muss im Normalfall die gesamte Abfrage erneut gestellt werden. Bei Datenbanksystemen
mit nur einem Knoten ist das offensichtlich, aber auch bei verteilten Datenbanksystemen ist es
üblich, dass die gesamte Abfrage verworfen wird, wenn auf einem Knoten ein Fehler auftritt.
DBMS, die Data Warehouses beheimaten, bestehen typischerweise aus sehr vielen Knoten. So
bietet beispielsweise der Spezialist „Teradata“ Data Warehouses mit bis zu 4096 Knoten an
(Teradata Corporation 2011). Ein solch komplexes System ermöglicht zwar performante
Abfragen, gleichzeitig steigt aber auch die Wahrscheinlichkeit, dass während einer lang
laufenden Abfrage irgendwo im System ein Fehler auftritt. In einem konstruierten Beispiel
schreiben die Osprey-Entwickler, dass der Ausfall eines Betriebssystems mit 35%
Wahrscheinlichkeit und der Ausfall einer Festplatte mit 13% Wahrscheinlichkeit
Größenordnungen darstellen, die nicht vernachlässigt werden können. Solch ein Fehler führt
nämlich dazu, dass die gesamte Abfrage erneut gestellt werden muss, was zu nicht hinnehmbaren
Verzögerungen führen kann.
Osprey ist angetreten, um einerseits einen mit der Anzahl der Knoten nahezu linear steigenden
Geschwindigkeitszuwachs zu erzielen und andererseits mit Fehlern, die während der Abfrage auf
den einzelnen Knoten auftreten, tolerant umzugehen, so dass nicht die gesamte Abfrage erneut
gestellt werden muss.
1.3 Ansatz
Osprey legt sich als Zwischenschicht zwischen die Anwendung, die die Abfragen stellt und die
eigentlichen Datenbank-Knoten. Ein zentraler „Koordinator“ nimmt dabei die Anfrage in Form
von gewöhnlichem SQL an. Diese Anfrage wird dann in Teilabfragen zerlegt und diese
Teilabfragen werden auf den einzelnen Datenbank-Knoten ausgeführt.
Die einzelnen Knoten bestehen dabei aus Hardware „von der Stange“. Da Osprey die Aufträge
flexibel auf die einzelnen Arbeiter aufteilt, stellen auch heterogene Hardware-Umgebungen kein
Problem dar. Die Autoren von (Christopher Yang 2010) sprechen dabei von einem „natürlichem
Lastausgleich“. Auf den einzelnen Knoten werden dabei je eine Instanz des DBMS PostgreSQL
(PostgreSQL Global Development Group 2011) ausgeführt. Außer PostgreSQL kann aber auch
jedes andere SQL-basierte DBMS verwendet werden.
1.4 Osprey und MapReduce
Osprey und MapReduce sind nur schwach miteinander verwandt. Die Osprey-Autoren sagen
selbst, sie haben sich durch MapReduce lediglich inspirieren lassen. Was dabei herauskam ist ein
System, das ähnlich wie MapReduce einen großen Auftrag in mehrere kleine Teile aufteilt und
diese dann parallel ausführt und dabei sogenannte „gierige Arbeiter“ verwendet, was zu dem
bereits erwähnten „natürlichen Lastausgleich“ führt.
Auch die Fehlerbehandlung wurde durch MapReduce angeregt: Wenn ein Auftrag fehlschlägt
oder ein Arbeiter zu lange für die Ausführung benötigt, so wird der Auftrag erneut zur
Ausführung bereitgestellt und kann so von einem anderen Arbeiter ausgeführt werden.
Die wichtigsten Gemeinsamkeiten und Unterschiede zeigt die nachfolgende Tabelle. Die
Informationen zu MapReduce stammen dabei aus (Jeffrey Dean 2008).
5
Markus Zander
Osprey
Der wesentliche Mechanismus zum Abfangen
von Fehlern bei der Ausführung ist das erneute
Ausführen der fehlgeschlagenen Teilabfrage.
Anfragen werden in Form von Standard-SQL
gestellt.
Die Daten werden in Standard-DBMS (z.B.
PostgreSQL) gespeichert.
Um Ausfälle einzelner Knoten abzufangen,
werden
die
Daten
mittels
„chained
declustering“ auf mehrere Knoten verteilt und
repliziert.
Die Daten werden schon beim Einspielen in
Osprey in Partitionen und Blöcke aufgeteilt.
Diese Aufteilung wird unverändert von allen
späteren Abfragen verwendet.
Osprey verwendet einen Koordinator, um die
Arbeit zu koordinieren und die Ergebnisse
zusammenzufassen.
Die Zwischenergebnisse der Teilabfragen
werden im Speicher des Koordinators
abgelegt.
Das Verhalten von Osprey im Falle eines
Speicherüberlaufs beim Zusammenfügen oder
Zwischenspeichern der Ergebnisse ist nicht
spezifiziert.
Das Gesamtergebnis wird im Speicher des
Koordinators vorgehalten.
Es gibt genau ein Gesamtergebnis.
Die Aufträge können neu, zugewiesen oder
fertiggestellt sein.
Die Verfügbarkeit der Knoten wird mittels
Ping geprüft.
Fällt während der Ausführung einer Anfrage
ein Knoten aus, so bleiben die Ergebnisse der
von diesem Knoten bereits fertiggestellten
Aufträge gültig und die Aufträge selbst im
Zustand fertiggestellt.
Um mit langsamen Knoten umzugehen werden
am Ende der Bearbeitung einer Anfrage
Aufträge, die im Zustand „zugewiesen“ sind
möglichst an andere Arbeiter ebenfalls
zugewiesen.
Thema 10 – Osprey
MapReduce
Der wesentliche Mechanismus zum Abfangen
von Fehlern bei der Ausführung ist das erneute
Ausführen des fehlgeschlagenen Map- oder
Reduce-Jobs.
Der Anwender erstellt eine Map- und eine
Reduce-Funktion, die er dem MapReduceFramework übergibt.
Die Daten werden in dem von Google
entwickelten verteilten Dateisystem GFS
gespeichert.
Um Ausfälle einzelner Rechner abzufangen
werden die Daten durch das GFS auf mehrere
Rechner verteilt und repliziert.
Die Daten werden in dem Moment
partitioniert, in dem der MapReduce-Job
gestartet wird.
MapReduce verwendet einen Master-Knoten,
um die Arbeit zu koordinieren. Die Ergebnisse
werden auf den Arbeitern zusammengefasst.
Die Zwischenergebnisse der Map-Funktionen
werden im Speicher der Arbeiter abgelegt (und
periodisch auf die Platte geschrieben).
Wenn der Speicher beim Sortieren der
Zwischenergebnisse (ein Teilschritt eines
Reduce-Jobs) nicht ausreicht, so wird das
Sortieren auf der Festplatte durchgeführt.
Das Ergebnis eines Reduce-Jobs wird in eine
Datei auf der lokalen Festplatte des Arbeiters,
der den Reduce-Job ausführt, gespeichert.
Es gibt so viele End-Ergebnisse, wie es
Reduce-Jobs gibt.
Die Aufträge können im Leerlauf, in
Bearbeitung oder fertiggestellt sein.
Die Verfügbarkeit der Knoten wird mittels
Ping geprüft.
Fällt während der Ausführung eines
MapReduce-Jobs ein Rechner aus, so sind die
von diesem Rechner ermittelten Ergebnisse
nicht mehr zugreifbar. Deshalb werden alle
von diesem Rechner bereits bearbeiteten und
als fertiggestellt markierten Aufträge wieder
zurück in den Zustand „im Leerlauf“ versetzt.
Um mit langsamen Rechnern umzugehen
werden am Ende der Bearbeitung eines
MapReduce-Jobs Aufträge, die im Zustand „in
Bearbeitung“ sind möglichst an andere
Rechner ebenfalls zugewiesen.
Thema 10 – Osprey
2
Markus Zander
6
Konzept
Der folgende Abschnitt soll einen grundlegenden Überblick über das DBMS Osprey aus der
Vogelperspektive schaffen. Osprey besteht aus einem zentralen Koordinator, der die Schnittstelle
zur aufrufenden Anwendung bereitstellt und für die Kommunikation und Koordination der
Datenbank-Knoten zuständig ist. Diese Datenbank-Knoten sind logisch gesehen sternförmig um
den Koordinator angeordnet.
2.1 Teile und Herrsche
Osprey teilt eine eingehende SQL-Abfrage in viele kleine Abfragen, die dann von den
Datenbanksystemen abgearbeitet werden. Dies führt zu einer besseren Auslastung des verteilten
Datenbanksystems und zu minimalen Verlusten im Fehlerfall. Der Koordinator behält jedoch
stets den Überblick über den Gesamtstatus der Abfrage.
2.2 Möglichst wenig Zusatzsoftware
Osprey benötigt lediglich auf dem Koordinator spezielle Software. Alle anderen beteiligten
Systeme, vom Client bis zu den DBMS auf den Arbeiter-Knoten sind Standardsoftwareprodukte,
die sich nicht um die Existenz von Osprey kümmern müssen.
2.3 Verkettete Replikation
Als „verkettete Replikation“ (engl. chained declustering) bezeichnet man eine Methode, bei der
die Daten auf die vorhandenen Datenbank-Knoten aufgeteilt werden. Jede Partition wird dabei
auf mehreren Datenbank-Knoten gespeichert, um den Ausfall eines Knotens kompensieren zu
können. Das Verfahren ist in 3.1.1 detailliert beschrieben.
2.4 Planungsalgorithmen
Für die Gesamtperformance ist es wichtig, nach welchen Regeln die Teilaufträge auf die
einzelnen Arbeiter verteilt werden. Osprey stellt drei verschiedene Ausführungsplaner zu
Verfügung, die in 3.3 ausführlich diskutiert werden.
2.5 Architektur
Ein Anwender kommuniziert mittels Standard-SQL mit dem Koordinator von Osprey. Dieser
kommuniziert wiederum mittels Standard-SQL mit den einzelnen Arbeitern.
Abb. 1: Architektur von Osprey; Quelle: (Christopher Yang 2010)
7
3
Markus Zander
Thema 10 – Osprey
Umsetzung
Nach dem groben Überblick in Kapitel 2 stellt Kapitel 3 die wesentlichen Punkte von Osprey im
Detail dar.
3.1 Datenmodell
In Data Warehouses werden die Daten üblicherweise in Hyperwürfeln, also Würfeln mit beliebig
vielen Dimensionen, abgespeichert. In einer relationalen Datenbank werden solche Hyperwürfel
in Form eines Stern-Schemas, bestehend aus einer großen Fakten-Tabelle und mehreren
verhältnismäßig kleinen Dimensions-Tabellen, abgelegt. Dabei bildet die zentrale FaktenTabelle den Mittelpunkt des Sterns.
Abb. 2: Sternschema für einen Hyperwürfel mit 6 Dimensionen; Quelle: (Mönch 2010)
3.1.1 Aufteilung der Faktentabelle
In Osprey wird die Faktentabelle mit Hilfe der „verketteten Replikation“ über mehrere Knoten
verteilt. Die Faktentabelle wird dabei in kleinere Teile, die Partitionen, zerlegt. Jedem Knoten ist
eine Partition zugeordnet. D.h., es gibt so viele Partitionen wie es Knoten gibt. Weiterhin kann
eine Partition aus mehreren Blöcken bestehen. Jeder Block wird dabei in einer eigenen
Datenbanktabelle gespeichert, die einen systemweit eindeutigen Namen nach dem Schema
Faktentabelle_Partitionsnummer_Blocknummer hat. Für diese Aufteilung benötigt Osprey neben
der Anzahl der Knoten weitere Parameter:
-
Den Namen der Faktentabelle,
die Angabe eines Schlüsselfeldes,
den Backupfaktor und
die Anzahl der Blöcke je Partition.
Osprey muss dabei für jeden Datensatz der Faktentabelle entscheiden, zu welcher Partition er
gehört. Dazu wird von dem Wert des Schlüsselfeldes ein 32-Bit-Hashwert gebildet. Diese Zahl
wird durch die Anzahl der vorhandenen Knoten geteilt und der Rest der Division ergibt dann die
Thema 10 – Osprey
Markus Zander
8
Nummer der Partition, der der Datensatz zuzuordnen ist. Dieses Verfahren ist in (Hui-I Hsiao
1990) beschrieben. Es kann allerdings nicht zweifelsfrei belegt werden, dass Osprey wirklich
dieses Verfahren einsetzt.
Die Blöcke innerhalb der Partition werden reihum gefüllt. Der erste Datensatz einer Partition
landet also im ersten Block, der nächste im zweiten und wenn der letzte Block erreicht ist, wird
der nächste Datensatz wieder im ersten Block gespeichert. Der Sinn dieser Unterteilung in
Blöcke liegt darin, eine höhere Parallelisierung zu erreichen, da eine Teilabfrage, die sich auf
eine bestimmte Partition bezieht in so viele kleinere Teilabfragen zerlegt werden kann, wie es
Blöcke gibt. Diese Teilabfragen können dann auf mehreren Knoten parallel bearbeitet werden.
Der Backupfaktor k gibt an, auf wie vielen zusätzlichen Knoten eine Partition gespeichert
werden soll. Bei n Knoten befindet sich eine Partition i dann auf den Knoten i und (i+1 mod n)
bis (i+k mod n). Somit ist eine Partition erst dann nicht mehr verfügbar, wenn alle k+1 Knoten,
die die Partition enthalten, gleichzeitig ausfallen.
Osprey ist als „read only“-System ausgelegt. Das Einfügen der Daten ist im Normalbetrieb nicht
vorgesehen. Das System bietet lediglich einen sogenannten „bulk load“ an. Dabei ist der
Abfrageteil von Osprey inaktiv und der gesamte Datenbestand wird auf einmal in das System
geladen.
Das Nachfolgende Beispiel soll den Vorgang verdeutlichen. Wir treffen hierzu folgende
Annahmen:
-
Anzahl der Knoten: 4 (A, B, C, D)
Backup-Faktor: 1
Anzahl der Blöcke je Partition: 2
Feld zur Bildung des Hash-Wertes: Identifikations-Feld, das eine fortlaufende Zahl
enthält
Hash-Verfahren: Wert des Identifikationsfeldes Modulo 4
Aufgrund des Hash-Verfahrens landen also die Datensätze in folgenden Partitionen: ID 0, 4, 8,
usw. in Partition 0; ID 1, 5, 9, usw. in Partition 1; ID 2, 6, 10, usw. in Partition 2; ID 3, 7, 11,
usw. in Partition 3. Wegen des Backupfaktors befindet sich die Partition 0 auf den Knoten A und
B, Partition 1 auf B und C, Partition 2 auf C und D sowie Partition 3 auf D und A. Die
Aufteilung der Datensätze auf die Blöcke sowie einen Gesamtüberblick zeigt die nachstehende
Tabelle:
Block 1
Block 2
Block 1 (Backup)
Block 2 (Backup)
Knoten A
ID 0, 8, …
ID 4, 12, …
ID 3, 11, …
ID 7, 15, …
Knoten B
ID 1, 9, …
ID 5, 13, …
ID 0, 8, …
ID 4, 12, …
Knoten C
ID 2, 10, …
ID 6, 14, …
ID 1, 9, …
ID 5, 13, …
Knoten D
ID 3, 11, …
ID 7, 15, …
ID 2, 10, …
ID 6, 14, …
3.1.2 Die Datenverteilung aus Sicht des Koordinators
Für den Koordinator (genauer: den Ausführungsplaner) ist es wichtig zu wissen, welcher Knoten
auf welche Daten Zugriff hat, damit er diesem Knoten nur die Teilabfragen zuteilt, die dieser mit
seinem „Wissen“ auch bearbeiten kann.
Jeder Knoten hat eine vollständige Kopie aller Dimensionstabellen und Zugriff auf seine eigene
Partition und die k Backups seiner Nachbarn. Abbildung 3 verdeutlicht diesen Zusammenhang.
Die Abkürzung „PWQ“ steht dabei für „Partition Work Queue“, eine Warteschlange für
Teilabfragen, die in 3.2.3 näher erläutert wird.
9
Markus Zander
Thema 10 – Osprey
Abb. 3: Datenverteilung aus Sicht des Koordinators; Quelle: (Yen 2010)
3.2 Abfragen auf aufgeteilten Daten
3.2.1 Transformation der Abfrage
Um die Teilabfragen für die einzelnen Arbeiter generieren zu können, muss Osprey die Abfrage
des Anwenders umbauen. Dies gelingt nur, wenn die Abfrage sich an einige Einschränkungen
hält. Osprey schreibt deshalb vor, dass die Faktentabelle nur einmal in der Abfrage vorkommen
darf und self joins nicht gestattet sind. Wie genau Osprey die Abfrage verändert, hängt von deren
Aufbau ab. Eine Abfrage, in der nur Dimensions-Tabellen aber nicht die Faktentabelle
vorkommt, wird unverändert an einen zufällig gewählten Arbeiter durchgereicht. Sobald die
Faktentabelle verwendet wird, generiert Osprey so viele Teilabfragen, wie es Blöcke gibt (siehe
3.1.1). Dabei wird der Name der Faktentabelle durch den Namen der Tabellen, die die einzelnen
Blöcke erhalten ersetzt. Heißt z.B. die Faktentabelle facttable und es gibt vier Partitionen zu
jeweils zwei Blöcken, so werden insgesamt acht Teilabfragen generiert in denen der Name der
Faktentabelle von facttable_0_0 bis facttable_3_1 durchnummeriert ist. So lange in der Abfrage
keine Aggregatsfunktionen oder Gruppierungen enthalten sind, ist das Ergebnis, das an den
Aufrufer zurückgegeben wird die Vereinigung aller Teilergebnisse.
3.2.2 Aggregats-Funktionen
Sobald Aggregats- oder Gruppierungs-Funktionen in der Abfrage genutzt werden, ist die
Transformation der Abfrage etwas aufwendiger.
Gruppierungs-Funktionen (GROUP BY) werden in die Teilabfragen unverändert übernommen.
Beim Zusammenfassen der Teilergebnisse führt Osprey erneut eine Gruppierung aus, um so
Gruppen, die in mehreren Teilergebnissen vorkommen wiederum zusammenzufassen.
Aggregatsfunktionen behandelt Osprey unterschiedlich. Die Entwickler schreiben nicht, welche
Aggregatsfunktionen Osprey unterstützt, führen aber beispielhaft sum, count, min, max und avg
an.
Sum (Summierung) wird in die Teilabfragen unverändert übernommen. Beim Zusammenführen
der Teilergebnisse summiert Osprey die Werte der Teilergebnisse, um das Gesamtergebnis zu
erhalten.
Count (Zählen) wird in die Teilabfragen unverändert übernommen. Auch hier summiert Osprey
die Werte der Teilergebnisse, um das Gesamtergebnis zu erhalten.
Thema 10 – Osprey
Markus Zander
10
Min (Minimum) wird in die Teilabfragen unverändert übernommen. Beim Zusammenführen der
Abfragen ermittelt Osprey das Minimum der Werte der Teilergebnisse, um das Gesamtergebnis
zu erhalten.
Max (Maximum) wird in die Teilabfragen unverändert übernommen. Beim Zusammenführen der
Abfragen ermittelt Osprey das Maximum der Werte der Teilergebnisse, um das Gesamtergebnis
zu erhalten.
Avg (Durchschnitt) kann nicht unverändert in die Teilabfragen übernommen werden, da beim
Zusammenführen nicht einfach der Durchschnitt der Durchschnitte gebildet werden kann.
Stattdessen wird die Funktion durch zwei getrennte Funktionen sum und count gebildet. Aus
avg(Price) wird also sum(Price), count(Price). Beim Zusammenführen der Ergebnisse berechnet
Osprey selbst den Durchschnitt aus den Werten der Teilergebnisse und verwendet diesen Wert
für das Gesamtergebnis.
3.2.3 Ausführung von Abfragen im Überblick
Beim Eintreffen einer Abfrage des Anwenders generiert Osprey daraus Teilabfragen (3.2.1). Zu
jeder Partition existiert eine korrespondierende Warteschlange, die Partition Work Queue. Die
generierten Teilabfragen werden anhand der Partitionsnummer im Tabellennamen der
Faktentabelle der entsprechenden Partition Work Queue zugeordnet. Eine Teilabfrage mit der
Tabelle „facttable_0_x“ wird demnach der Partition Work Queue 0 zugeordnet.
Aggregatsfunktionen werden nötigenfalls modifiziert (3.2.2).
Der Ausführungsplaner verteilt nun, in Abhängigkeit von dem jeweils gewählten
Planungsalgorithmus, die ersten Teilabfragen aus den Partition Work Queues an die Arbeiter
(3.3). Anschließend prüft er zyklisch alle Arbeiter, ob diese noch „leben“ und noch beschäftigt
sind. Die Arbeiter bearbeiten die gestellte Teilabfrage und liefern das Ergebnis an den
Koordinator zurück. Dieser merkt sich die Teilergebnisse und sobald alle Teilabfragen
abgearbeitet sind, generiert der Result Merger daraus durch Vereinigung und Aggregation das
Gesamtergebnis (3.2.2). Abbildung 4 illustriert den Vorgang.
Abb. 4: Ausführung einer Abfrage in Osprey; Quelle: (Yen 2010)
11
Markus Zander
Thema 10 – Osprey
3.3 Ablaufkoordination- und Planung
Der Ausführungsplaner kann verschiedene Algorithmen nutzen, um die Teilaufgaben so schnell
wie möglich von den Arbeitern abarbeiten zu lassen. Die in Osprey implementierten
Algorithmen sollten im Folgenden vorgestellt werden. Ein wesentlicher Punkt ist, dass Osprey
mit sogenannten „gierigen Arbeitern“ arbeitet. Dies bedeutet, dass nicht nur noch nicht
angefangene Aufgaben verteilt werden: Sobald eine Partition Work Queue leer ist, beginnt der
Scheduler damit, alle noch nicht abgeschlossenen Aufgaben dieser Queue erneut zu verteilen.
Auf diese Art wird einerseits umgangen, dass der langsamste Arbeiter das ganze System aufhält,
andererseits werden so fehlgeschlagene Teilanfragen auf anderen Arbeitern erneut durchgeführt.
Man erhält die Fehlertoleranz von Osprey sozusagen als Nebenprodukt geschenkt.
Die Ausführungsplanung ist nur deshalb sinnvoll, weil es im Normalfall immer mehrere Knoten
gibt, die eine Teilabfrage bearbeiten können. Bei den weiter unten beschriebenen
Auswahlverfahren geht es also immer darum, für einen Arbeiter aus einer der für ihn möglichen
Partition Work Queues einen Auftrag auszuwählen.
Das Abarbeiten der Partition Work Queues läuft wie folgt ab:
1. Partition Work Queues füllen (siehe 3.2.3)
2. Verfügbarkeit der Arbeiter mittels Ping prüfen
3. Für jeden verfügbaren Arbeiter mit einem der weiter unten beschriebenen Verfahren eine
Partition Work Queue auswählen, dieser einen Auftrag entnehmen, diesen dem Arbeiter
zuteilen und ihn zusätzlich in eine separate Partition Work Queue für zugeteilte Aufträge
übernehmen. Sind für diesen Arbeiter keine Aufträge mehr verfügbar weiter mit 5.
4. Sobald ein Arbeiter ein Teilergebnis zurückliefert, dieses zwischenspeichern, den Auftrag
als „erledigt“ markieren und für diesen Arbeiter Vorgang ab Schritt 2 wiederholen.
Liefert der Arbeiter statt eines Ergebnisses einen Fehler, wird der Auftrag wieder zurück
in die Partition Work Queue für unbearbeitete Aufträge gestellt.
5. Für den verfügbaren Arbeiter gemäß der verwendeten Zuteilungsmethode einen Auftrag,
der sich im Status „zugeteilt“ befindet, auswählen und zuteilen. Sind für keinen Arbeiter
mehr Aufträge vorhanden weiter mit 7.
6. Sobald ein Arbeiter ein Teilergebnis zurückliefert, dieses zwischenspeichern, den Auftrag
als „erledigt“ markieren und für diesen Arbeiter Vorgang ab Schritt 2 wiederholen.
Liefert der Arbeiter statt eines Ergebnisses einen Fehler, wird der Auftrag wieder zurück
in die Partition Work Queue für unbearbeitete Aufträge gestellt.
7. Aus den zwischengespeicherten Ergebnissen das Gesamtergebnis mittels Vereinigung,
Gruppierung und Aggregation zusammensetzen (siehe auch 3.2.2) und an den Aufrufer
zurückgeben.
Sollten in den Schritten 4 und 7 das Ergebnis einer Teilabfrage eintreffen, das zuvor schon
ein anderer Knoten geliefert hat, so wird es verworfen. Die Ergebnisse unterschiedlicher
Teilabfragen können sich nicht überschneiden. Dies ist durch die Partitionierung der
Faktentabelle gewährleistet.
3.3.1 Zufällige Verteilung
Dies ist die einfachste Art der Verteilung. Aus den für einen Arbeiter in Frage kommenden
Partition Work Queues wird zufällig eine ausgewählt.
3.3.2 LQF
Bei der Methode „längste Warteschlange zuerst“ (engl. longest queue first, LQF) wird aus den
für einen Arbeiter in Frage kommenden Partition Work Queues diejenige ausgewählt, die am
längsten ist. Die Länge einer Warteschlange ist dabei definiert durch die Anzahl der enthaltenen
Aufträge.
Thema 10 – Osprey
Markus Zander
12
3.3.3 Majorisierung
Ziel der Majorisierungs-Methode ist es, zu vermeiden, dass es gleichzeitig Arbeiter gibt, die
einer hohen Last ausgesetzt sind, während andere leer laufen, weil es keine Aufgaben mehr gibt,
die sie bearbeiten könnten. Dies soll dadurch erreicht werden, dass der Algorithmus versucht die
Unterschiede in der Belastung für den einzelnen Arbeiter auszugleichen. Dazu muss neben der
Länge der für den jeweiligen Arbeiter direkt verfügbaren Warteschlangenlänge auch die Länge
der Warteschlangen seiner Nachbarn, mit denen er sich Warteschlangen teilt, berücksichtigt
werden. Es wird die Partition Work Queue gewählt, die sich der Arbeiter gemeinsam mit dem
Nachbarn teilt, der die größte Last hat. Gibt es kein eindeutiges Maximum, so kommt LQF zum
Einsatz. Dieses Verfahren geht auf (Golubchik 1991) zurück, es wurde jedoch für den Einsatz in
Osprey leicht abgewandelt.
Das Verfahren soll mit einem Beispiel erläutert werden. Angenommen, es existieren vier
Arbeiterknoten A, B, C und D. Das System arbeitet mit einem Backup-Faktor von 1. Die Länge
der Warteschlangen zum betrachteten Zeitpunkt sind wie folgt: PWQ0 enthält sechs Aufträge,
PWQ1 enthält einen Auftrag, PWQ2 enthält drei Aufträge und PWQ3 enthält zwei Aufträge. In
diesem Moment soll Arbeiter C ein Auftrag zugeteilt werden. Dieser Arbeiter hat aufgrund des
Backupfaktors Zugriff auf die Partitionen 1 und 2. Käme jetzt die LQF-Strategie zum Einsatz,
würde ihm ein Auftrag aus PWQ2 zugeordnet, da PWQ2 mehr Aufträge enthält als PWQ1.
Stattdessen wird aber die „Last“ der Knoten herangezogen.
Der Knoten C selbst hat Zugriff auf PWQ1 und PWQ2. Seine Last sind also 4 Aufträge. Da er
sich mit Knoten B und D jeweils eine PWQ teilt, muss auch die Last dieser Knoten
berücksichtigt werden. Knoten B hat Zugriff auf PWQ 0 und PWQ 1 und somit eine Last von 7
Aufträgen, für Knoten D ergibt sich durch PWQ2 und PWQ 3 eine Last von fünf Aufträgen. Da
also Knoten B die größte Last trägt wird der Auftrag für Knoten C aus der Partition Work Queue
genommen, die er sich mit Knoten B teilt: PWQ1.
3.3.4 Vergleich der Algorithmen
Die Algorithmen sind alle so trivial zu berechnen, dass der durch den Algorithmus verursachte
Aufwand vernachlässigt werden kann. Was bleibt, ist ein Vergleich, wie schnell die
Warteschlangen abgearbeitet werden.
Die zufällige Verteilung schneidet hier am schlechtesten ab: Geht man von einer
Gleichverteilung aus, so muss zum Schluss auf den langsamsten Knoten gewartet werden, da die
anderen Knoten keine Unterstützung bei der Abarbeitung der Warteschlange des langsamen
Knotens geleistet haben. Die beiden anderen Algorithmen scheinen auf den ersten Blick recht
ähnlich zu sein. Da die Länge der Warteschlangen bestimmt, von wo der nächste Auftrag
genommen wird, ist hier eine Unterstützung langsamer Knoten gewährleistet. Gemäß (Golubchik
1991) bietet die Majorisierung gegen über LQF einen Geschwindigkeitsvorteil von rund 19%.
4
Fehlertoleranz und Leistungsfähigkeit
4.1 Umgang mit langsamen Knoten
Damit am Ende einer Abfrage nicht auf die Ergebnisse der langsamsten Knoten gewartet werden
muss geht Osprey wie folgt vor: Teilabfragen können sich im Zustand „neu“, „zugewiesen“ und
„fertiggestellt“ befinden. Wird eine Teilabfrage einem Knoten zugewiesen, wird sie zunächst als
„zugewiesen“ markiert. Erst nachdem das Ergebnis einer Teilabfrage vorliegt, wird sie als
„fertiggestellt“ markiert. Wenn nun keine Teilabfragen mehr den Zustand „neu“ haben, werden
leerlaufenden Knoten nach Möglichkeit Teilabfragen im Zustand „zugewiesen“ zugeteilt. Eine
13
Markus Zander
Thema 10 – Osprey
solche Abfrage wird nun zeitgleich von mehreren Knoten bearbeitet. Osprey markiert die
Abfrage als „fertiggestellt“, sobald einer der Knoten, die die Teilabfrage bearbeiten, ein Ergebnis
zurückliefert. Das bedeutet auch, dass der Koordinator gar nicht merkt, wenn ein Knoten zu
langsam ist, denn dies ist aufgrund der beschriebenen Methode nicht notwendig. Lediglich der
komplette Ausfall von Knoten wird erkannt, um unnötige Zuteilungsversuche zu vermeiden.
Dies ist in 4.2.2 beschrieben.
4.2 Umgang Fehlern während der Ausführung
Hier muss man verschiedene Klassen von Fehlern unterscheiden:
4.2.1 Ausfall des Koordinators
Der Koordinator ist der zentrale Punkt von Osprey. Sollte hier ein Fehler auftreten, z.B. der
Absturz der Koordinationssoftware oder des darunterliegenden Betriebssystems, so kann das
System nicht mehr arbeiten. Gegen solche Ausfälle ist Osprey also nicht geschützt. Bei Fehlern
in der vom Benutzer gestellten Abfrage, die eine Bearbeitung unmöglich machen, wäre es
sinnvoll, wenn der Koordinator eine geeignete Fehlermeldung an den Aufrufer zurückgeben
würde - spezifiziert ist dieses Verhalten jedoch nicht.
4.2.2 Ausfall eines Knotens
Osprey überwacht die Verfügbarkeit der Arbeiter, in dem es regelmäßig reihum einen Ping an
die Arbeiter schickt. Wird dieser nicht beantwortet, wird der entsprechende Knoten als „tot“
markiert. Diesem Arbeiter teilt der Ausführungsplaner fortan keine neuen Aufträge mehr zu. Wie
und ob ein „toter“ Knoten weiter überwacht wird und eine erneute Verfügbarkeit gehandhabt
wird, ist nicht spezifiziert. Es scheint sinnvoll, diese Knoten weiterhin regelmäßig, wenn auch
seltener auf Verfügbarkeit zu testen und den Knoten wieder in die Ausführung von Aufträgen
einzubinden, wenn er wieder verfügbar ist. Aufträge für einen toten Knoten werden fortan von
denjenigen anderen Knoten bearbeitet, die eine Kopie der betroffenen Partition haben. Hierzu ist
jedoch kein gesonderter Mechanismus auf dem Koordinator oder im Ausführungsplaner
erforderlich. Dies geschieht durch die in 3.3 beschriebenen Zuordnungsalgorithmen automatisch.
Es ist also kein Unterschied, ob ein Knoten einfach nur zu langsam oder vollständig ausgefallen
ist.
Die Entwickler von Osprey beschreiben nicht, wie das System reagiert, wenn auch alle BackupKnoten ausgefallen sind, so dass eine Partition gar nicht mehr zur Verfügung steht. Da in diesem
Fall eine korrekte Bearbeitung der Anfrage nicht garantiert 1 werden kann, scheint es sinnvoll,
dass Osprey die Ausführung der Anfrage abbricht und dem Aufrufer eine Fehlermeldung
zurückliefert.
4.2.3 Fehler bei der Ausführung einer Teilabfrage
Tritt bei der Abarbeitung einer Teilabfrage auf einem Knoten ein Fehler auf, so wird dieser
Teilauftrag vom Zustand „zugewiesen“ in den Zustand „neu“ versetzt und wieder in die
zugehörige Partition Work Queue eingereiht. Damit kann der Teilauftrag von einem anderen
(oder ggfs. auch von dem selben) Knoten später erneut bearbeitet werden. Leider schreiben die
Entwickler nicht, wie und ob sie Fehler bei denen eine erneute Zuweisung sinnvoll ist von
solchen unterscheiden, bei denen es angebracht ist den Fehler an den Aufrufer weiterzugeben
und die Abarbeitung der Anfrage abzubrechen. Ein Beispiel für einen Fehler erster Art wäre z.B.
eine Störung der Festplatte des Knotens, ein Beispiel für einen Fehler der zweiten Art die
1
Da die Teilabfragen, die die ausgefallene Partition betreffen unter Umständen gar keine Datensätze zurückliefern
würden, z.B. weil sie die Bedingung des WHERE-Statements nicht erfüllen, kann es Konstellationen geben, in
denen auch ohne diese Daten das Ergebnis korrekt wäre.
Thema 10 – Osprey
Markus Zander
14
Abfrage einer nicht vorhandenen Spalte durch den Anwender. Der Koordinator selbst kann das
nicht feststellen, da er selbst keine Daten, auch keine Metadaten, kennt.
Zusammenfassend kann man festhalten: Fehler, die auf einzelnen Knoten entstehen und nicht
schon in der Abfrage begründet sind, werden von Osprey korrekt abgefangen und bleiben für den
Anwender unbemerkt, für alle anderen Fehler ist das Verhalten nicht spezifiziert.
4.3 Experimentelle Ergebnisse
Die hier vorgestellten Ergebnisse beziehen sich auf die Daten, die die Entwickler von Osprey
selbst ermittelt und in (Christopher Yang 2010) publiziert haben.
4.3.1 Umgebung
Als Testumgebung wurde ein Zusammenschluss von neun Rechnern verwendet, die jeweils über
zwei Pentium-4-Prozessoren mit 3,06GHz, 2GB Hauptspeicher und Linux-Betriebssystem
verfügten. Einer der Rechner wurde als Koordinator genutzt, die anderen acht waren ArbeiterKnoten auf denen PostgreSQL als DBMS zum Einsatz kam. Die Rechner waren mittels GigabitEthernet verbunden.
Die Tests wurden mit dem Star-Schema-Benchmark (Pat O'Neil 2009) durchgeführt. Die
Testdaten bestanden aus einer Faktentabelle mit 60 Millionen Datensätzen und einer Größe von
5,4GB sowie insgesamt 105MB Daten für die vier Dimensions-Tabellen. Der Benchmark stellt
insgesamt dreizehn ausgewählte Abfragen zur Verfügung, die die typischen Anforderungen in
einer Data-Warehouse-Umgebung simulieren sollen. Die Abfragen sind so gestaltet, dass die
getesteten DBMS möglichst nicht durch ihren Cache die Ergebnisse verfälschen können.
Zusätzlich haben die Entwickler von Osprey bei den PostgreSQL-Instanzen den Cache auf
512MB begrenzt.
Da Osprey als Read-Only-System ausgelegt
Einspielen der Daten Aufwand. Aus diesem
relevanten Feldern, sowohl für Faktentabelle
Indizes beschleunigen die Abfragen; Für den
jedoch keine Rolle spielen.
ist, verursachen Indizes nur beim erstmaligen
Grund wurden für den Test Indizes auf allen
als auch für die Dimensions-Tabellen erstellt.
relativen Vorteil, den Osprey bietet, sollten sie
Zum Aufteilen der Faktentabelle wurde das Feld lo_orderdate benutzt. Das dabei verwendete
Hashverfahren haben die Osprey-Entwickler nicht beschrieben.
Vor dem Testlauf wurden die PostgreSQL-Instanzen mit dem Befehl „ANALYZE“ dazu
aufgefordert, die Daten zu analysieren, damit der PostgreSQL-Ausführungsplaner besser arbeiten
kann.
Um langsame, überlastete Knoten zu simulieren, griffen die Osprey-Entwickler auf ein
sogenanntes Burn-In-Tool zurück. Dieses erzeugt künstlich eine hohe CPU-, Speicher- und
Festplattenlast, so dass PostgreSQL auf solchen Knoten nur noch mit sehr begrenzten
Ressourcen arbeiten kann.
4.3.2 Skalierbarkeit
In dieser Disziplin geht es darum, dass die Geschwindigkeit linear mit der Anzahl der Arbeiter
wächst, also die Ausführungszeit sich umgekehrt proportional zur Anzahl der Arbeiter verhält.
Das nachfolgende Diagramm bestätigt dieses Verhalten, jedoch fehlt ein Vergleich mit einem
einfachen DBMS ohne zwischengeschalteten Osprey-Koordinator.
15
Markus Zander
Thema 10 – Osprey
Abb. 5: Zusammenhang zwischen Anzahl der Arbeiter und Ausführungszeit; Quelle: (Christopher Yang 2010)
4.3.3 Mehraufwand
Osprey selbst verursacht zusätzlichen Aufwand gegenüber einem einfachen DBMS. Dieser
Aufwand stammt hauptsächlich aus dem Transformieren der Abfrage, der Ausführungsplanung,
der Übermittlung der Teilabfrage an die Arbeiter, dem Abholen der Teilergebnisse sowie deren
Zusammenführen zu einem Gesamtergebnis.
Die Osprey-Entwickler wählten hier verschiedene Block-Größen der Partitionen und somit eine
unterschiedliche Menge von Blöcken und verglichen jeweils die Ausführungszeit von den
Arbeitern mit der Gesamtzeit der Abfrage. Hier schnitt Osprey gut ab, auch wenn konkrete
Zahlen nicht bekannt sind. Jedoch scheint dieser Ansatz nicht geeignet, um den OspreyOverhead zu zeigen. Ein Vergleich von Osprey mit einem Arbeiter und einem einfachen DBMS
hätte den Overhead besser gezeigt. Aus diesem Grunde wird auf eine ausführliche Darstellung
der Ergebnisse an dieser Stelle verzichtet.
4.3.4 Lastausgleich
Bei diesem Test soll Osprey zeigen, dass es mit überlasteten oder ausgefallenen Knoten
umgehen kann. Im unten gezeigten Diagramm ist ein Stressfaktor zu sehen, der Werte zwischen
0 und 3 annehmen kann. 0 bedeutet, dass der Knoten keine zusätzliche Last verkraften muss, 3
bedeutet, dass der Knoten vollständig ausgefallen ist. Für die Werte dazwischen liegen keine
genauen Definitionen vor.
Im ersten Versuch wird in einem System, dass aus vier Arbeitern besteht, ein Arbeiter immer
stärker belastet. Die nachfolgende Grafik zeigt das Verhalten von Osprey. Der Backupfaktor ist
mit k bezeichnet. Der Anstieg der Ausführungszeit von s=0 zu s=3 ist etwa 4/3, was dem
theoretisch zu erwartenden Wert entspricht. Schaut man genau hin, so sieht man, dass mit
steigendem k auch die Ausführungszeit ansteigt. Eine Erklärung hierfür bleiben die OspreyEntwickler schuldig.
In weiteren Testreihen zeigten die Osprey-Entwickler außerdem, dass Osprey sich bei
Überlastung mehrerer Arbeiter ähnlich verhält und die Arbeit möglichst auf die anderen Arbeiter
verteilt und auch, dass die Verteilung bei Ausfall eines Knotens mitten in einer laufenden
Abfrage dynamisch angepasst wird.
Thema 10 – Osprey
Markus Zander
16
Abb. 6: Laufzeit bei steigender Last eines Knotens; Quelle (Christopher Yang 2010)
4.3.5 Vergleich der Ausführungsplanungs-Algorithmen
Wurden in 3.3.4 die Algorithmen aufgrund theoretischer Überlegungen verglichen, folgt hier der
experimentelle Vergleich. Die Entwickler von Osprey fanden heraus, dass bei der von ihnen
verwendeten homogenen Hardware-Umgebung und ohne zusätzliche Last auf den einzelnen
Knoten alle drei getesteten Algorithmen (Zufällig, LQF, Majorisierung) ungefähr das gleiche
Ergebnis liefern. Sobald ein Knoten überlastet ist, sind LQF und Majorisierung rund 10% besser
als die Zufallsauswahl. Der erwartete Vorteil des Majorisierungs-Algorithmus von rund 19%
gegenüber LQF trat nicht ein – im Gegenteil: Majorisierung war minimal langsamer als LQF.
Die Entwickler vermuten den höheren Berechnungsaufwand bei der Majorisierung als Ursache,
gehen dieser Vermutung aber nicht weiter nach.
5
Zusammenfassung
5.1 Vorteile
Osprey ermöglicht durch das Aufteilen der Daten auf mehrere Knoten eine parallelisierte
Ausführung einer Abfrage und erreicht dadurch einen linearen Geschwindigkeitszuwachs. Der
Mehraufwand, der hauptsächlich aus dem Aufteilen der Abfragen und dem Zusammenführen der
Teilergebnisse besteht, ist im Vergleich zum Aufwand der eigentlichen Abfrage klein und somit
zu verschmerzen. Die bereits erwähnte Parallelisierung bringt, unterstützt durch den
Ausführungsplaner, einen Lastausgleich der Knoten und eine Fehlertoleranz mit, die
herkömmlich verteilte DBMS so nicht leisten können.
5.2 Kritische Betrachtung
Trotz dieser Vorteile darf bezweifelt werden, ob Osprey es jemals in den Produktiveinsatz
schaffen wird. Zu sehr merkt man ihm die Entstehung am runden Tisch an. Im jetzigen Zustand
eignet sich Osprey nur für Datenbanken im Stern-Schema. Wäre dies im angepeilten DataWarehouse-Einsatz noch akzeptabel, wiegen die weiteren Einschränkungen schwerer:
17
Markus Zander
Thema 10 – Osprey
1. Konkurrierende Schreibzugriffe können das Ergebnis verfälschen. Die Entwickler von
Osprey räumen diese Möglichkeit zwar ein, halten sie aber für unkritisch, da es im
OLAP-Umfeld aufgrund der starken Aggregation auf ein, zwei Datensätze mehr oder
weniger nicht ankäme. Dieser Punkt wird natürlich erst relevant, wenn Osprey
Schreibzugriffe unterstützt.
2. Der Ausfall des Koordinators legt das gesamte System lahm. Auf diese Möglichkeit
gehen die Entwickler nicht ein.
3. Eine Auswertung der Fehlermeldungen der Teilknoten scheint zu fehlen, was zu
Endlosschleifen führen kann. Dieser Punkt wird von den Osprey-Entwicklern nicht
erläutert.
4. Eine Strategie für die geschickte erneute Zuweisung bereits zugewiesener Teilabfragen
scheint ebenso zu fehlen. Auch hierauf gehen die Osprey-Entwickler nicht ein.
Nicht zuletzt muss gefragt werden, ob das System mit einem zentralen Koordinator von den
Entwicklern zu Recht als „shared nothing“-Ansatz bezeichnet wird. Außerdem sind die unter 5.3
beschriebenen Erweiterungen für einen Produktiveinsatz unumgänglich.
Osprey ist auch nicht für das OLTP-Umfeld geeignet, da es hier meist um relativ kleine
Abfragen geht, bei denen Osprey seine Vorteile nicht ausspielen kann und die durch die
Einschränkungen bei der Formulierung der Abfragen bis zur Unmöglichkeit eingeschränkt
würden. Dies sehen die Entwickler von Osprey jedoch auch nicht als Zielanwendung.
5.3 Ausblick
Einige der erwähnten Einschränkungen waren den Entwicklern von Osprey durchaus bewusst
und sie schrieben in (Christopher Yang 2010) und (Yen 2010), dass folgende weitere Arbeiten
erforderlich seien:
1. Umgang mit verschachtelten joins oder self-joins
2. Verbesserung des Ausführungsplaners, damit die Vorteile der Caches auf den einzelnen
Knoten genutzt werden können.
3. Verbesserte Abschätzung des Aufwandes in den Partition Work Queues nach dem realen
Aufwand (Abfragen mit leerem Ergebnis vs. Abfragen mit großem Ergebnissatz).
4. Implementierung von updates, inserts, deletes
Leider muss man derzeit davon ausgehen, dass die Arbeiten an Osprey nach Veröffentlichung
des Papers eingestellt wurden. Auch die Entwickler selbst waren nicht für eine Stellungnahme zu
erreichen.
Thema 10 – Osprey
6
Markus Zander
18
Literaturverzeichnis
Christopher Yang, Christine Yen, Ceryen Tan, Samuel R. Madden. Osprey: Implementing
MapReduce-Style Fault. Massachusetts: CSAIL, MIT, 2010.
Golubchik, Leana et al. „Chained Declustering: Load balancing and robustness to skew and
failures.“ 07 1991. http://ftp.cs.ucla.edu/tech-report/1991-reports/910055.pdf (Zugriff am
27. 05 2011).
Hui-I Hsiao, David J. DeWitt. „Scientific Literature Digital Library and Search Engine.“
Chained Declustering: A New Availability Strategy for Multiprocssor Database
machines. 03 1990.
http://citeseer.ist.psu.edu/viewdoc/download;jsessionid=11DF3E52668E62A12F81F29F
B202B028?doi=10.1.1.11.658&rep=rep1&type=pdf (Zugriff am 03. 06 2011).
Jeffrey Dean, Sanjay Ghemawat. „MapReduce: Simplified data processing on large clusters.“
COMMUNICATIONS OF THE ACM, 01 2008: 107-113.
Mönch, Prof. Dr. Lars. „Grundprinzipien betrieblicher Informationssysteme.“ Betriebliche
Informationssysteme. Bd. 1. Hagen: FernUniversität in Hagen, 2010.
Pat O'Neil, Betty O'Neil, Xuedong Chen. „UMASS Boston.“ Department of Computer Science.
05. 06 2009. http://www.cs.umb.edu/~poneil/StarSchemaB.PDF (Zugriff am 01. 06
2011).
PostgreSQL Global Development Group. PostgreSQL. 18. 04 2011. http://www.postgresql.org/
(Zugriff am 31. 05 2011).
Teradata Corporation. „Teradata Purpose-Built Platform Pricing.“ Teradata Corporation. 25. 04
2011. http://www.teradata.com/t/brochures/Teradata-Purpose-Built-Platform-Pricingeb5496/ (Zugriff am 24. 05 2011).
Yen, Christine. Osprey. 03. 03 2010. http://www.scribd.com/full/36821833?access_key=key1as0z5uf0m97zuf8yidq (Zugriff am 31. 05 2011).
Fernuniversität in Hagen
Seminar in Informatik (1912, SS2011)
Map Reduce
Map Reduce Merge
Marcel Endberg
[email protected]
1.
Einführung:
Suchmaschinen verarbeiten und verwalten enorme Datenmengen, welche sie im gesamten World
Wide Web gesammelt haben. Damit diese Aufgaben gleichzeitig effizient und dennoch kostengünstig
ausgeführt werden können, sind Suchmaschinen in riesigen shared-nothing Clustern wie parallele
DV-Systeme aufgebaut. Diese Cluster bestehen aus herkömmlicher Handelsware und nicht aus
teuren Serversystemen.
DBMS sind normalerweise mit viel zu vielen unnötigen Features gerade für spezielle Applikationen
wie z.B. Suchmaschinen ausgestattet. Suchmaschinenanbieter sind daher einen eigenen Weg
gegangen und haben eine "vereinfachte" Infrastruktur für verteilten Speicher und parallele
Programmierung entwickelt und betreiben diese erfolgreich. Dazu gehören u.a. Google File System
GFS [1], Map Reduce [2], Big Table [3] oder Microsofts Dryad [4].
Dieser einfachen Infrastruktur folgt auch die gesamte Datenverarbeitung bei Map Reduce:
a) Eine Map-Funktion verarbeitet ein Schlüssel/Wert Paar als Eingabe und liefert als Ausgabe eine
Liste von Schlüssel/Wert Paaren als Zwischenergebnis für die zugewiesenen Reducer.
b) Eine Reduce-Funktion verschmilzt die Zwischenergebnisse mit jeweils gleichem Schlüssel und
liefert danach seine Ausgabe.
Map Reduce ist mit seinen beiden Primitiven "Map" und "Reduce" ausreichend allgemein gehalten,
um eine Vielzahl an Aufgaben zu meistern. Das Hauptaugenmerk des Map Reduce Frameworks liegt
jedoch in der Bearbeitung von homogenen Datensätzen. Wie in [5] gezeigt wird, passt der Join
mehrerer heterogener Datensätze nicht so ganz in das Konzept von Map Reduce. Obwohl technisch
mit den beiden Primitiven Map und Reduce realisierbar, lohnt sich der Aufwand durch zusätzliche
Map und Reduce Schritte nicht.
Kurz gesagt: Die Bearbeitung von Datenrelationen, worin ein RDBMS Map Reduce eindeutig
überlegen ist, ist nicht gerade die Stärke von Map Reduce.
Für Suchmaschinen können bereits viele Datenverarbeitungsprobleme mit diesem simplen Map
Reduce Framework gelöst werden. Allerdings gibt es auch einige Aufgaben, welche am besten als
Joins modelliert werden:
Eine Suchmaschine z.B. speichert ihre gesammelten URLs mit deren Inhalt in einer CrawlerDatenbank. Invertierte Indexe werden in einer Index-Datenbank gespeichert und Klicks oder
Ausführungslogs in verschiedenen Log-Datenbanken. URL Links werden mit diversen Eigenschaften
wie z.B. Inlinks und Outlinks in einer Webgraph-Datenbank gespeichert. Diese Datenbanken haben
riesige Ausmaße und sind über mehrere Cluster verteilt. Bei der Erstellung der Datenbanken greifen
diese wiederum untereinander auf sich zu:
Die Index-Datenbank benötigt Daten aus der Crawler- und Webgraph-Datenbank, die WebgraphDatenbank wiederum benötigt Daten aus der Crawler Datenbank und einer früheren Version der
Webgraph-Datenbank usw.
Diese Aufgaben sind mit dem Standard Map Reduce Framework kaum handhabbar.
Die Verarbeitung von Datenrelationen ist jedoch allgegenwärtig - vor allem in unternehmerischen ITsystemen. Ein Fokus der populären relationalen Algebra und RDBMS ist die effiziente Modellierung
und Verwaltung relationaler Daten. Neben Suchmaschinenanbietern sind auch andere Bereiche an
einem Join-fähigen Map Reduce Framework interessiert:
Beispielsweise haben Fluglinien und Hotelketten beide riesige Datenbanken. Das Verschmelzen
dieser Datenbanken erlaubt es Data Minern umfassendere Regeln aufzustellen, als sie es bei der
jeweils nur individuellen Betrachtung der Datenbanken könnten. Viele traditionelle RDBMS wurden
mittlerweile erfolgreich in OLAP Systemen eingesetzt, jedoch stellt ein Join-fähiges Map Reduce
Framework eine kostengünstige Alternative dar.
Eine Optimierung des Map Reduce Frameworks besteht nun darin, es zu erweitern und relationale
Algebra zu integrieren. Dabei soll jedoch die existierende Allgemeinheit und Einfachheit des Map
Reduce Frameworks nicht angetastet werden.
Das Map Reduce Framework (Abb. 1) wird zum Map Reduce Merge Framework (Abb. 2) erweitert.
Dieses neue Framework ermöglicht die gleichzeitige Verarbeitung heterogener Datensätze.
Außerdem wird eine neue Merge-Phase hinzugefügt, welche zwei Ausgaben miteinander
verschmelzt. Die endgültige Ausgabe erfolgt nun nicht mehr nach der Reduce-Phase, sondern nach
der Merge-Phase.
Abb.1: Map Reduce Datenfluss
Ein Treiberprogramm (Driver) initiiert einen KoordinatorProzess (Coordinator), welches Mapper und Reducer
verwaltet. Jeder Mapper liest Datenfragmente vom GFS,
führt benutzerdefinierte Logiken aus und erzeugt mehrere
Ausgabe-Partitionen, jeweils eine pro Reducer. Ein
Reducer liest Daten von jedem Mapper, sortiert und
gruppiert die Daten und führt ebenfalls benutzerdefinierte
Logiken aus. Die Ausgabe erfolgt auf GFS.
Abb. 2: Map Reduce Merge Datenfluss
Der Koordinator verwaltet für zwei verschiedene Sätze
Mapper und Reducer. Nachdem die Aufgaben erledigt
sind startet er eine Reihe von Mergern, welche die
Ausgaben von ausgewählten Reducern lesen und die
Daten mit benutzerdefinierten Logiken verschmelzen.
Eine
Optimierung
ist
bereits
implementiert:
ReduceMerge (Erläuterung folgt in Abschnitt 6).
Die Map Reduce Philosophie der Einfachheit bleibt bestehen, das Framework wird jedoch um eine
Merge-Phase erweitert. Diese Erweiterung ermöglicht die effizientere und einfachere Verarbeitung
heterogener Datensätze. Heterogene Datensätze sind dabei solche Datensätze, die in ihrem Aufbau
teils große Unterschiede ausweisen. Die Attribute zweier Eingaberelationen sind z.B. bis auf ein
gemeinsames Schlüsselattribut grundverschieden.
Map Reduce Aufgaben werden normalerweise nacheinander ausgeführt und bilden so einen linearen
Arbeitsablauf. Das Hinzufügen der Merge-Phase kann dabei zu neuen, hierarchischen
Arbeitsabläufen für einen kompletten Datenverarbeitungsprozess führen. Ein Map Reduce Merge
Ablauf ist vergleichbar mit dem Ablaufplan eines RDBMS, allerdings können Entwickler hier eigene
Programmlogiken einbauen und Map Reduce Merge ist speziell für die parallele Datenverarbeitung
ausgelegt.
In einer Parallelen Konfiguration können relationale Operatoren so modelliert werden, dass sie die
drei Primitive Map, Reduce und Merge in verschiedenen Kombinationen nutzen. In einer korrekten
Konfiguration können diese drei Primitive zur Implementierung einiger Join-Algorithmen genutzt
werden: Sort-Merge, Hash und Block Nested Loop. Weitere Informationen zu diesen Joins folgen in
Abschnitt 4.
2.
Homogenisierung:
Ohne diese neue Merge-Komponente ist es dem Standard-Modell zwar möglich, heterogene
Datensätze zu verarbeiten, der Aufwand ist allerdings erheblich. Datensätze müssten dafür zunächst
in einem aufwendigen Prozess "homogenisiert" werden. Außerdem wäre diese Methode lediglich auf
solche Anfragen anwendbar, die sich als Equi-Joins übertragen lassen. Der Prozess der
"Homogenisierung" sieht im Einzelnen wie folgt aus:
1. Jeweils ein Map Reduce Durchlauf pro Datensatz ist zur Vorbereitung notwendig.
2. Jeder dieser bearbeiteten Datensätze erhält eine Markierung für die Datenquelle.
3. Ein Schlüsselattribut wird aus diesen Daten extrahiert. Dieses Schlüsselattribut müssen alle
Datensätze gemein haben.
4. Alle so bearbeiteten Datensätze haben nun zwei gleiche (neue) Attribute: Schlüssel und
Datenquelle. Diese Daten sind nun "homogenisiert".
5. Ein finaler Map Reduce Durchlauf ist nun auf diese bearbeiteten Datensätze erforderlich.
6. Benutzerdefinierte Logiken können aus den Daten deren Quellen herauslesen, so dass Daten
verschiedener Quellen verschmolzen werden können.
Dieses Verfahren benötigt erheblich mehr Speicherplatz und sehr viel Map Reduce Kommunikation.
Außerdem ist dieses Verfahren auf Equi-Join Anfragen beschränkt. Es liegt daher nahe, dass ein
effizienterer Weg für die Bearbeitung heterogener Datensätze gefunden werden muss.
3.
Map Reduce Merge:
Das Map Reduce Merge Modell ermöglicht die Verarbeitung mehrerer heterogener Datensätze. Die
Signaturen der Map Reduce Merge Primitive sieht wie folgt aus, wobei α, β, γ unterschiedliche
Datenstämme repräsentieren (also Daten verschiedener Quellen, z.B. aus verschiedenen
Datenbanken wie Index-Datenbank, Crawler-Datenbank etc.):
Map:
(k1,v1)α  [(k2,v2)]α
Reduce: (k2,[v2])α  (k2,[v3])α
Merge: (k2,[v2])α, (k3,[v4])β  [(k4,v5)] γ
In diesem neuen Modell transformiert die Map-Funktion ein Schlüssel/Wert Paar (k1,v1) als Eingabe
zu einer Liste von Schlüssel/Wert Paaren [(k2,v2)] als Ausgabe. Die Reduce-Funktion sammelt die Liste
aller Werte [v2], welche mit dem Schlüssel k2 assoziiert sind und erstellt eine Liste von Werten [v3],
welche ebenfalls mit dem Schlüssel k2 assoziiert ist. Eingaben und Ausgaben beider Funktionen
haben jeweils denselben Datenstamm, also die gleiche Datenquelle (hier: α). Diese Map und Reduce
Vorgänge werden gleichzeitig auf einem anderen Datenstamm (hier: β) durchgeführt, wobei als
Ausgabe der Reduce-Funktion (k3,[v4])β resultiert.
Ein Merger kombiniert nun die beiden Reducer Ausgaben basierend auf den Schlüsseln k2 und k3 zu
einer Liste von Schlüssel/Wert Paaren [(k4,v5)] und bildet damit einen eigenen Datenstamm (hier: γ).
Ein Sonderfall tritt ein, wenn die Datenstämme beider paralleler Map Reduce Durchläufe gleich ist
(also α = β). In diesem Fall führt die Merge-Funktion einen Self-Merge durch, welches vergleichbar
mit dem Self-Join der relationalen Algebra ist.
Die Map und Reduce Funktionen des neuen Modells entsprechen weitestgehend denen des alten
Map Reduce Modells. Die einzigen Unterschiede sind die Datenstämme und die Ausgabe der
Reducer, welche nun eine Schlüssel/Werte Liste erstellt statt lediglich einer Liste von Werten. Diese
Änderungen sind notwendig, da die Merge-Funktion nach Schlüsseln organisierte (also partitionierte
und dann sortierte oder gehashte) Datensätze als Eingabedaten benötigt. Im ursprünglichen Map
Reduce Modell ist die Ausgabe nach dem Reducer final und die Weiterleitung des Schlüssels k2 ist
nicht notwendig.
Der jeweilige Schlüssel wird also zunächst vom Mapper an den Reducer weitergegeben, danach an
den Merger. Dieses Vorgehen stellt sicher, dass die Daten nach dem gleichen Schlüssel partitioniert
und danach sortiert (oder gehasht) werden, bevor diese in geeigneter Weise durch einen Merger
verschmolzen werden können.
3.1. Beispiel:
Ein Map Reduce Merge Durchlauf soll nun an einem kleinen, einfachen Beispiel demonstriert
werden. Es zeigt auch, wie Map, Reduce und Merge miteinander arbeiten. In diesem Beispiel gibt es
zwei Ausgangstabellen: Angestellte und Abteilung. Das Schlüsselattribut der Angestelltentabelle ist
ANG_id, das Schlüsselattribut der Abteilungstabelle ABT_id.
Angestellte
ANG_id ABT_id Bonus
1
B
Innovationspreis (€100)
1
B
ANG des Monats (€50)
2
A
NULL (€0)
3
A
Fleißpreis (€150)
3
A
Innovationspreis (€100)
€100
€50
€0
€150
€100
Reduce: Sortierung nach (ABT_ID,
ANG_ID) und Aufsummierung aller Boni
ANG_id ABT_id Bonus
2
A
3
A
1
B
Map: Bonusfaktoren abrufen
ABT_id
B
A
Map: Bonus berechnen
ANG_id ABT_id Bonus
1
B
1
B
2
A
3
A
3
A
ABT_id
A
B
€0
€250
€150
Abb. 3: Beispiel eines Joins zweier Tabellen und
Berechnung von Angestellten-Boni
Abteilung
Bonusfaktor
1.1
0.9
Bonusfaktor
0.95
1.15
Reduce: Sortierung nach ABT_ID,
modifiziert Bonusfaktoren
ABT_id
A
B
Bonusfaktor
1.15
0.95
Merge:
Ein
Sort-Merge
Merger
verschmelzt beide Reducer Ausgaben
anhand des Schlüssels ABT_id und
berechnet endgültige Boni
ANG_id Bonus
2
€0
3
€237.5
1
€172.5
Die Tabelle Angestellte bildet unseren Datenstamm α, die Tabelle Abteilung den Datenstamm β. Die
Ausgabe vom Merger bildet einen eigenen Datenstamm γ. In diesem Beispiel werden die Daten
zweier Tabellen per Join verbunden und die Boni jedes Angestellten berechnet.
Bevor die Daten beider Tabellen vom Merger verschmolzen werden können, werden die Daten von
einem Paar Mapper und Reducer bearbeitet. Der Ablauf ist in Abb. 3 dargestellt. Auf der linken Seite
liest ein Mapper die Daten der Tabelle Angestellte und errechnet den Bonus für jeden Eintrag. Ein
Reducer erhält die Daten vom Mapper und summiert alle Boni eines jeden Angestellten auf.
Außerdem sortiert der Reducer die Daten nach ABT_id, dann nach ANG_id. Auf der rechten Seite liest
ein Mapper die Daten der Tabelle Abteilung und berechnet die einzelnen Bonusfaktoren. Ein Reducer
sortiert diese Daten nach ABT_id. Am Ende verschmelzt ein Merger die Ausgaben der beiden Reducer
nach dem Schlüssel ABT_id und errechnet anhand der Bonusfaktoren die endgültigen Boni jedes
Angestellten.
Pseudocode für Mapper und Reducer jeder Seite sind in den Algorithmen 1-4 angegeben. Der Code
für Merger wird in Abschnitt 3.2.1. näher erläutert:
Alg. 1: Map Funktion (Angestellten-Datensatz)
Alg. 2: Map Funktion (Abteilungs-Datensatz)
1: map(const Key& key, /* emp id */
2:
const Value& value /* emp info */) {
3: emp id = key;
4: dept id = value.dept id;
5: /* compute bonus using emp info */
6: output key = (dept id, emp id);
7: output value = (bonus);
8: Emit(output key, output value);
9: }
1: map(const Key& key, /* dept id */
2:
const Value& value /* dept info */) {
3: dept id = key;
4: bonus adjustment = value.bonus adjustment;
5: Emit((dept id), (bonus adjustment));
6: }
Alg.3: Reduce Funktion (Angestellten-Datensatz)
Alg. 4: Reduce Funktion (Abteilungs-Datensatz)
1: reduce(const Key& key, /* (dept id, emp id) */
2:
const ValueIterator& value
3:
/* an iterator for a bonuses collection */) {
4: bonus sum = /* sum up bonuses for each emp id */
5: Emit(key, (bonus sum));
6: }
1: reduce(const Key& key, /* (dept id) */
2:
const ValueIterator& value
3:
/* an iterator on a bonus adjustments collection */) {
4: /* aggregate bonus adjustments and
5: compute a final bonus adjustment */
6: Emit(key, (bonus adjustment));
7: }
3.2. Implementierung:
Das Map Reduce Merge Framework übernimmt mit Ausnahme kleiner Signatur-Änderungen
komplett die Komponenten von Map Reduce. Das neue Modell liefert zusätzlich weitere
Komponenten: Eine Merge-Funktion, Prozessor-Funktion, Partitions-Selektor und konfigurierbare
Iteratoren. Diese neuen Komponenten sollen nun etwas näher erläutert werden.
Die Merge-Funktion (Merger) kann genau wie Map und Reduce mit benutzerdefinierten
Datenverarbeitungslogiken ausgestattet werden. Während eine Map-Funktion ein Schlüssel/Wert
Paar verarbeitet und die Reduce-Funktion eine nach einem Schlüssel geordnete Werteliste,
verarbeitet der Merger zwei Schlüssel/Werte-Paare. Diese Schlüssel/Wert-Paare sind durch deren
angegebenen Datenstamm eindeutig unterscheidbar.
In der Merge-Phase wollen Benutzer je nach Datenquelle eventuell verschiedene Logiken für die
Datenverarbeitung implementieren. Ein Beispiel sind die Build und Probe Phasen eines Hash-Joins.
Die Build-Logik betrifft dann z.B. nur eine Tabelle und Probe die andere. In solchen Fällen hilft die
Prozessor-Funktion, welche nichts anderes als eine benutzerdefinierte Funktion darstellt, die lediglich
die Daten einer bestimmten Quelle verarbeitet. Beim Merger können insgesamt zwei ProzessorFunktionen implementiert werden, da dieser Daten aus zwei verschiedenen Quellen verarbeitet.
Nachdem die Map- und Reduce-Aufgaben beendet sind, startet ein Map Reduce Merge-Koordinator
(s. Abb. 2) die Merger. Sobald ein Merger gestartet wird, erhält er eine eindeutige Merger-Nummer.
Mit Hilfe dieser Nummern kann ein benutzerdefiniertes Modul - der Partitions-Selektor - bestimmen,
von welchen Reducern ein Merger seine Eingabedaten erhält. Mapper und Reducer erhalten dafür
ebenfalls eindeutige Nummern: Die Mapper erhalten eine Nummer je nach Datenfragment, das sie
bearbeiten. Die Reducer hingegen erhalten eine Nummer je nach Eingabebehälter, in denen die
zugewiesenen Mapper ihre Ausgaben partitionieren und speichern. Im normalen Map Reduce
Framework sind diese Nummern nichts anderes als systeminterne Details, aber im Map Reduce
Merge Framework können Benutzer diese Nummern verwenden, um Eingaben und Ausgaben
zwischen Mergern und Reducern zu steuern.
Merger können genau wie Mapper und Reducer so behandelt werden, als hätten sie logische
Iteratoren, welche Daten aus Eingaben lesen. Jeder Mapper und Reducer hat einen logischen
Iterator, welcher vom Anfang bis zum Ende des jeweiligen Datenflusses mitläuft (standardmäßig wird
ein Iterator um 1 erhöht). Ein Merger liest Daten von zwei unterschiedlichen Quellen, daher kann ein
Merger zwei solcher Iteratoren enthalten. Diese beiden Iteratoren rücken üblicherweise wie ihre
Mapper- oder Reduce-Pendants vor. Deren relative Bewegung gegeneinander kann jedoch dazu
benutzt werden, benutzerdefinierte Merge-Algorithmen zu implementieren. Unser Map Reduce
Merge Framework stellt hierzu ein eigenes Modul zur Verfügung (Iterator-Manager), welches die
Bewegungen dieser konfigurierbaren Iteratoren ermöglicht. Um diese Iteratoren zu koordinieren
wird ein Merge-Phase Treiber benötigt, dessen Pseudocode in Algorithmus 5 angegeben ist:
Alg. 5: Merge-Phase Treiber.
1:
PartitionSelector partitionSelector; // user-defined logic
2:
LeftProcessor leftProcessor; // user-defined logic
3:
RightProcessor rightProcessor; // user-defined logic
4:
Merger merger; // user-defined logic
5:
IteratorManager iteratorManager; // user-defined logic
6:
int mergerNumber; // assigned by system
7:
vector<int> leftReducerNumbers; // assigned by system
8:
vector<int> rightReducerNumbers; // assigned by system
9:
// select and filter left and right reducer outputs for this merger
10: partitionSelector.select( mergerNumber,
11:
leftReducerNumbers,
12:
rightReducerNumbers);
13: ConfigurableIterator left = /*initiated to point to entries
14:
in reduce outputs by leftReducerNumbers*/
15: ConfigurableIterator right =/*initiated to point to entries
16:
in reduce outputs by rightReducerNumbers*/
17: while(true) {
18:
pair<bool,bool> hasMoreTuples =
19:
make pair(hasNext(left), hasNext(right));
20:
if (!hasMoreTuples.first && !hasMoreTuples.second) {break;}
21:
if (hasMoreTuples.first) {
22:
leftProcessor.process(leftkey, leftvalue); }
23:
if (hasMoreTuples.second) {
24:
rightProcessor.process(rightkey, rightvalue); }
25:
if (hasMoreTuples.first && hasMoreTuples.second) {
26:
merger.merge( leftkey, leftvalue,
27:
rightkey, rightvalue); }
28:
pair<bool,bool> iteratorNextMove =
29:
iteratorManager.move(leftkey, rightkey, hasMoreTuples);
30:
if (!iteratorNextMove.first && !iteratorNextMove.second) {
31:
break; }
32:
If (iteratorNextMove.first) { left++; }
33:
if (iteratorNextMove.second) { right++; }
34: }
Die Aufgaben dieser vier Komponenten sollen nun anhand des Angestellten- und Abteilungs-Beispiels
genauer dargestellt werden.
3.2.1. Merger
In der Merge-Funktion können Benutzer eigene Logiken zur Datenverarbeitung implementieren. Der
Merger bedient sich dazu zweier Datenquellen, nämlich der Reducer-Ausgaben der
Angestelltentabelle und der Abteilungstabelle. Der folgende Algorithmus 6 zeigt im Pseudocode die
Vorgehensweise des Mergers in unserem Beispiel:
Alg. 6 Merge Funktion für den Angestellten-Abteilungs-Join
1: merge(const LeftKey& leftKey,
2:
/* (dept id, emp id) */
3:
const LeftValue& leftValue, /* sum of bonuses */
4:
const RightKey& rightKey, /* dept id */
5:
const RightValue& rightValue /* bonus-adjustment */){
6: if (leftKey.dept id == rightKey) {
7:
bonus = leftValue * rightValue;
8: Emit(leftKey.emp id, bonus); }
9: }
Die Aufgabe des Mergers besteht also darin, die Daten der Reducer per Join über die Abteilungs-ID
zusammenzufügen. Dabei werden die erzielten Boni mit den entsprechenden Bonusfaktoren
multipliziert und als Ergebnis erhalten wir eine dritte Tabelle (unser neuer Datenstamm γ) mit den
endgültig berechneten Boni jedes einzelnen Angestellten.
3.2.2. Prozessoren
Hier können Benutzer eigene Logiken zur Bearbeitung von Daten einzelner Quellen einfügen. Diese
Prozessoren können nur angewandt, wenn der Hash-Join Algorithmus in der Merge-Funktion
implementiert wurde (jeweils ein Prozessor für Build und Probe). Da in unserem Beispiel der SortMerge Algorithmus angewendet wird, bleiben diese Prozessoren leer.
3.2.3. Partitions-Selektor
In einem Merger kann ein Benutzer bestimmen, welche Reducer-Partitionen überhaupt verarbeitet
werden sollen. Nicht benötigte Partitionen werden dabei aus der Datenkollektion entfernt. Der
Partitions-Selektor benötigt hierzu die Nummern von zwei Reducern und eine vom entsprechenden
Merger, der die Ausgaben dieser beiden Reducer lesen und verschmelzen soll. Die Nummern der
Reducer, die sich nicht mehr in der Datenkollektion befinden, werden dabei außer Acht gelassen.
3.2.4. Konfigurierbare Iteratoren
Benutzer können den relativen Durchlauf der beiden logischen Iteratoren eines Mergers
manipulieren. Dieses Vorgehen erlaubt eigene Merge-Algorithmen, öffnet allerdings auch Tür und
Tor für ungewollte Endlosschleifen.
Bei Algorithmen wie dem Nested-Loop Join sind Iteratoren so konfiguriert, dass sie sich innerhalb
einer geschachtelter Schleife bewegen. Bei Algorithmen wie dem Sort-Merge Join wechseln sich
Iteratoren ab, wenn sie die beiden Datensätze durchlaufen. Bei Algorithmen wie dem Hash-Join
scannen die beiden Iteratoren ihre jeweiligen Daten in verschiedenen Durchgängen: Der erste scannt
die Daten und erstellt die Hash-Tabelle (Build), der zweite scannt seine Daten und gleicht diese mit
der zuvor erstellten Hash-Tabelle ab (Probe).
4.
Map Reduce Merge Implementierung von Relationalen Join Algorithmen
Der Join stellt den wahrscheinlich wichtigsten relationalen Operator dar. Drei der geläufigsten Join
Algorithmen werden wir etwas genauer betrachten:
4.1 Sort-Merge Join
Map Reduce ist sehr effektiv wenn es um die parallele Sortierung von Daten geht [2]. Bei der
Konfigurierung des Frameworks können Benutzer angeben, dass die Mapper einen Range-Partitioner
statt eines Hash-Partitioners verwenden sollen. Mit diesem auf Map Reduce basierenden
Partitionierer kann das Map Reduce Merge Framework als ein paralleler Sort-Merge Join Operator
implementiert werden. Die einzelnen Phasen sehen wie folgt aus:
Map:
Der Mapper verwendet einen Range-Partitioner (siehe Abb. 4), um die Daten in geordnete
Behälter (Buckets) zu partitionieren. Jeder dieser Behälter deckt einen exklusiven
Schlüsselbereich ab und wird genau einem Reducer zugeordnet.
Reduce: Für jeden vorhandenen Datenstamm in der Kollektion liest ein Reducer alle ihm
zugewiesenen Map-Partitionen ein. Diese Daten werden zu einem sortierten Datensatz
zusammengefügt. Die Sortierung der Daten kann entweder komplett beim Reducer
erfolgen - wenn nötig auch mit externen Sortierverfahren - oder bereits beim Mapper. Falls
der Mapper diese Aufgabe übernehmen soll, so werden die Daten in den Partitionen
sortiert, bevor sie an die Reducer übergeben werden. Der Benutzer kann selbst
entscheiden, in welcher Funktion er die Sortierung durchführen lassen möchte.
Merge: Ein Merger liest die Ausgaben zweier Reducer, wobei beide Ausgaben denselben
Schlüsselbereich haben müssen. Da die Sortierung bereits beim Mapper oder Reducer
erfolgt ist, muss der Merger sich lediglich noch um den Merge-Part von Merge-Sort
kümmern.
4.1.1. Range-Partitioner
Der Range-Partitioner wird in der Map-Funktion benutzt, um die Eingabedaten zu partitionieren. Die
Daten werden in größtenteils gleichgroße Partitionen aufgeteilt, wobei jede Partition einen
exklusiven Schlüsselbereich abdeckt. Diese Methode stellt außerdem sicher, dass ähnliche Daten in
derselben Partition gespeichert werden. Die Range-Partitionierung wird häufig auch zur
Vorsortierung vor einer kompletten Sortierung von Daten benutzt.
Da mit dieser Methode beinahe eine Gleichverteilung der Daten in den Partitionen sichergestellt
wird, wird auch die Arbeit der zugewiesenen Reducer gleichmäßig verteilt. Ein Beispiel einer solchen
Partitionierung ist in Abb. 4 zu sehen. Die Partitionierung erfolgt über den Schlüssel "Alter". Jede
Partition enthält einen exklusiven Schlüsselbereich und deckt somit einen gewissen Altersbereich ab.
Der (Alters-)Schlüsselbereich ist in den jeweiligen Partitionen angegeben. Die Höhe eines Balkens
spiegelt die Größe der jeweiligen Partition wieder, was gleichzeitig der Anzahl Datensätze in dieser
Partition entspricht.
Abb. 4: Range-Partitioner [6]
Damit dieses Verfahren angewendet werden kann, benötigt man eine Range-Map. Die Erstellung
einer Range-Map erfolgt mithilfe von probabilistischen Split-Techniken. Eine solche Technik wird z.B.
in [8] beschrieben. Um die Bereichsgrenzen einer Partition zu bestimmen wird zunächst ein kleiner
sortierter Teil-Datensatz übergeben. Mit dieser Datenprobe errechnet der Range-Partitioner
daraufhin die endgültigen Bereichsgrenzen für den gesamten Datensatz.
4.2 Hash Join
Ein wichtiger Aspekt in verteilten Systemen und parallelen Datenbanken ist die gleichmäßige
Auslastung und Speichernutzung über alle Verarbeitungsknoten. Eine Strategie ist die Verteilung von
Daten anhand ihrer Hash-Werte. Dieses Verfahren ist gerade bei Suchmaschinen und parallelen
Datenbanken weit verbreitet und stellt gleichzeitig das Standard-Partitionierungsverfahren von Map
Reduce dar [2]. Die Implementierung eines Hash-Joins im Map Reduce Merge Framework sieht die
folgenden Aufgaben vor:
Map:
Zwei Mapper verwenden jeweils denselben Hash-Partitioner (siehe Abb. 5), um ihre Daten
in geordnete Hash-Behälter zu partitionieren. Jede Partition wird genau einem Reducer
zugewiesen.
Reduce: Für jeden Datenstamm in der Kollektion liest ein Reducer alle ihm zugewiesenen MapPartitionen ein. Die Daten werden mit jeweils derselben Hash-Funktion, die der HashPartitioner zum Partitioniern der Daten verwendet hat, in einer Hash-Tabelle gruppiert und
zusammengefügt. Diese Hash-Sortierung stellt eine Alternative zum Standardverfahren dar.
Eine Sortierung ist nicht mehr notwendig, jedoch muss nun eine Hash-Tabelle verwaltet
werden (intern, sofern die Tabelle komplett in den Hauptspeicher passt, extern sonst).
Merge: Ein Merger liest die Ausgaben zweier Reducer, wobei beide Ausgaben jeweils in gleichen
Hash-Behältern liegen müssen. Eine Ausgabe wird als Build verwendet, die andere als
Probe. Die Partitionierung und Sortierung erfolgte bereits bei den Mappern bzw. Reducern,
so dass der Build-Datensatz recht klein ausfallen kann und ggf. komplett im Hauptspeicher
per Hash-Join verschmolzen werden kann. Dies reduziert Zugriffszeiten enorm, daher sollte
man unbedingt auf eine optimale (große) Anzahl an Reduce/Merge-Sätzen achten. Passt
der Build-Datensatz nicht komplett in den Hauptspeicher, so erfolgt der Hash-Join extern.
4.2.1. Hash Partitioner
Die Partitionierung der Werte erfolgt mittels einer Hash-Funktion über den oder die Schlüssel. Dazu
untersucht der Hash-Partitioner einen oder mehrere Felder (die Hash-Schlüssel) einer Eingabe und
verteilt Werte mit denselben Hash-Werten in dieselben Hash-Behälter (Buckets). Diese Methode
stellt sicher, dass zusammenhängende Daten in derselben Partition gespeichert werden.
Ein Beispiel ist das Entfernen von Duplikaten. Die Daten werden dabei anhand ihrer Hash-Werte
partitioniert, wobei Werte mit gleichem Hash-Wert in derselben Partition abgelegt werden. Die
Werte innerhalb jeder Partition werden anhand des Hash-Schlüssels sortiert, wobei doppelte Werte
entfernt werden. Obwohl die Daten über viele Partitionen verteilt sind, stellt der Hash-Partitioner
sicher, dass Werte mit gleichem Schlüssel in derselben Partition landen, um z.B. doppelte Einträge zu
finden.
Eine mögliche Partitionierung von Daten ist in Abb. 5 angegeben. Die Partitionierung erfolgt über den
Schlüssel "Alter". Jede Partition enthält Daten mit denselben Hash-Werten. Die Hash-Werte sind in
den jeweiligen Partitionen angegeben. Die Höhe eines Balkens spiegelt die Größe der jeweiligen
Partition wieder, was gleichzeitig der Anzahl Datensätze in dieser Partition entspricht.
Abb. 5: Hash-Partitioner [7]
Der Hash-Partitioner stellt nicht immer eine Gleichverteilung von Daten über alle Partitionen sicher.
Ein Beispiel für eine ungleichmäßige Verteilung ist ein großer Datensatz mit Kundendaten, wobei
Name, Telefon und Anschrift gespeichert sind. Ist der Schlüssel, anhand dessen die Daten
partitioniert werden sollen z.B. die Postleitzahl (PLZ), so kann es durchaus zu einer ungleichmäßigen
Verteilung kommen, wenn viele Kunden die gleiche PLZ haben. So können durchaus Engpässe
entstehen, da einige Reducer (sehr viel) mehr Arbeit leisten müssen als andere.
4.3 Block-Nested Loop Join
Dieser Join entspricht größtenteils dem Algorithmus des Hash-Joins. Anstatt einen Hash-Join im
Hauptspeicher durchzuführen wird hier ein Nested-Loop benutzt.
Map:
siehe Hash-Join
Reduce: siehe Hash-Join
Merge: siehe Hash-Join. Hier wird allerdings ein Nested Loop-Join statt eines Hash-Joins verwendet.
5.
Map Reduce Merge Implementierung von Relationalen Operatoren
Wir nehmen für unser Modell an, dass ein Datensatz in eine Relation R mit Attributen (Schema) A
abgebildet wird. In den Funktionen Map, Reduce und Merge wählen Benutzer die gewünschten
Attribute aus und bilden damit zwei Teilmengen K (Schema von k) und V (Schema von v).
Jedes Tupel t aus R besteht aus einem Satz mit zwei Feldern: k (key) und v (value). Mit diesen
Annahmen lassen sich relationale Operatoren folgendermaßen implementieren:

Projektion:
Für jedes Tupel t = (k, v) der Eingaberelation kann der Benutzer einen Mapper bestimmen,
der dieses Tupel in die projizierte Ausgabe t' = (k', v') transformieren soll. k', v' werden dabei
durch die Schemata K' und V' geschrieben, wobei diese eine Teilmenge von A sind.
Nur Mapper können eine Projektion durchführen und somit die Größe eines Datensatzes
verringern.

Aggregation:
In der Reduce-Phase führen Map Reduce und auch Map Reduce Merge die Funktionen "sortby-key" und "group-by-key" aus, um sicherzustellen, dass die Eingaben eines Reducers ein
Satz von Tupeln der Form t = (k, [v]) bilden. v entspricht dabei allen Werten des Schlüssels k.
Ein Reducer kann nun Aggregationsfunktionen auf diese gruppierten Wertelisten ausführen,
also COUNT, SUM, AVERAGE, MAXIMUM, MINIMUM und GROUP BY.

Selektion:
Die Selektion kann in allen drei Phasen (Map, Reduce oder Merge) erfolgen.
Hängt die Selektion von Attributen einer Datenquelle ab, so wird die Selektion im Mapper
implementiert. Hängt sie hingegen von Aggregationen ab, so wird die Selektion im Reducer
implementiert. Sollte die Selektion von Attributen oder Aggregationen mehr als einer
Datenquelle abhängen, so wird sie im Merger implementiert.

Set Union (Vereinigung):
Nehmen wir an, eine Set Union Operation (oder eine der beiden weiter unten behandelten
Set Operationen) wird auf zwei Relationen angewandt. Im Map Reduce Merge Modell wird
jede Relation zunächst von Map und Reduce bearbeitet und die sortierten sowie gruppierten
Ausgaben eines Reducers werden an den zugewiesenen Merger weitergereicht.
Jeder Reducer kann Duplikate von Tupeln der gleichen Quelle sehr einfach überspringen. Die
Mapper beider Datenquellen sollten denselben Range-Partitioner verwenden, damit die
zugewiesenen Merger ausschließlich Daten desselben Schlüsselbereichs erhalten. Der
Merger kann danach gleichzeitig über beide Eingaben iterieren und nur solche Tupel
erstellen, die auch tatsächlich in den verschiedenen Eingaben vorkommen. Tupel, die nicht in
beiden Eingaben gleichzeitig vorkommen, werden mit diesem Merger ebenfalls erstellt.

Set Intersection (Schnitt):
Die partitionierten und sortierten Map Reduce Ausgaben werden wie oben beschrieben an
die Merger weitergeleitet. Ein Merger kann dann gleichzeitig über beide Eingaben iterieren
und Tupel erstellen, die in beiden Reducer-Ausgaben enthalten sind.

Set Difference (Differenz):
Die partitionierten und sortierten Map Reduce Ausgaben werden wie oben beschrieben an
die Merger weitergeleitet. Ein Merger kann dann gleichzeitig über beide Eingaben iterieren
und Tupel erstellen, welche die Differenz beider Reducer-Ausgaben darstellen.

Set-Operationen allgemein:
Map und Reduce werden zum Sortieren und Gruppieren der Daten benötigt.
Merger kümmern sich dann lediglich noch um die gewünschte Operation (siehe oben).

Kartesisches Produkt:
Ein Merger ist so konfiguriert, dass er eine Partition des ersten Reducers (F) und alle
Partitionen des zweiten Reducers (S) erhält. Dieser Merger kann nun einen Nested Loop
bilden, um die Daten der einzigen Partition von F mit allen Partitionen von S zu
verschmelzen.

Joins:
Wurden oben bereits ausführlich beschrieben.
6.
Optimierung: Phasen kombinieren
Die Verarbeitung von Daten ist in der Regel nicht mit einem Map Reduce (-Merge) Durchlauf erledigt.
Häufig kommt es vor, dass mehrere Durchläufe benötigt werden, wobei die Ausgaben eines
Durchlaufs die Eingaben des darauffolgenden Durchlaufs sind.

ReduceMap, MergeMap:
Die Ausgaben von Reducern und Mergern werden normalerweise an den folgenden Mapper
weitergereicht. Diese Ausgaben können direkt an den zugewiesenen Mapper weitergereicht
werden, ohne die Daten zunächst temporär abzuspeichern.

ReduceMerge:
Ein Merger liest die Ausgaben zweier Reducer. Der zugewiesene Merger kann nun mit einem
der beiden Reducer kombiniert werden und erhält dessen Ausgaben direkt, wobei nur noch
die Ausgaben des zweiten Reducers wie gehabt per Fernzugriff eingelesen werden müssen.

ReduceMergeMap:
Eine direkte Kombination aus ReduceMerge und MergeMap wird zu ReduceMergeMap.
ReduceMerge und ReduceMergeMap sind in Abb. 6 zu sehen. Das erste Bild zeigt das Standard-Map
Reduce Vorgehen. Im zweiten Bild ist die direkte Verbindung zwischen einem Merger und einem
seiner zwei zugewiesenen Reducer mit einer gestrichelten Verbindungslinie dargestellt. Im dritten
Bild ist die Ausgabe der jeweils ersten Merger die Eingabe der darauffolgenden Mapper.
Abb. 6: Vergleich der Optimierungen ReduceMerge und ReduceMergeMap gegenüber dem Standard Map Reduce (-Merge)
Modell. Der Datenstamm ist jeweils mit angegeben, wird im Standard Map Reduce Modell jedoch nicht benötigt.
7.
Verbesserungen
Neben den genannten Optimierungen können auch die folgenden Verbesserungen das
Programmieren vereinfachen.
7.1. Map Reduce Merge Bibliothek
Es gibt einige Varianten und Vorlagen für das Merge-Modul, z.B. solche mit eingebauten JoinAlgorithmen. Die Selektoren und konfigurierbaren Iteratoren der geläufigsten Merge-Module können
in einer Bibliothek zusammengefasst werden, so dass Benutzer einfacher auf die verschiedenen
Varianten zugreifen können, ohne das Rad gleich neu erfinden zu müssen.
7.2. Map Reduce Merge Workflow
Map Reduce folgt einem strengen Zwei-Phasen-Ablauf, so folgt z.B. die Reduce Phase nach einer
Map-Phase. Benutzer können zwar bestimmte Standardkonfigurationen ändern, allerdings können
einige Operationen wie z.B. die Partitionierung und Sortierung nicht übersprungen werden. Gerade
wenn es um einfaches Debugging geht, möchte man für das Debugging unnötige Operationen
überspringen, um den Ablauf zu beschleunigen. Auch wenn Benutzer lediglich das Mapping
durchführen wollen (oder nur Reduce), müssen sie dennoch die anderen Phasen komplett
durchlaufen. Dieses Vorgehen macht Map Reduce zwar simpel und erlaubt eine einheitliche
Durchführung, erfahrene Benutzer wollen aber manchmal ihre eigenen Abläufe planen.
Bei Map Reduce kann man den Workflow nicht großartig anpassen: Erst kommt Map, dann folgt
Reduce. Nimmt man allerdings eine dritte Komponente wie dem Merge hinzu, so ergeben sich ganz
neue Gestaltungsmöglichkeiten für einen eigenen Ablauf. Zwei Map Reduce Merge Workflows sind in
Abb. 6 zu sehen, wobei weitere Varianten natürlich möglich sind.
Eine Verbesserung von Map Reduce Merge besteht in der Bereitstellung einer konfigurierbaren API,
um eigene Workflows zu erstellen. Eine Workflow-Variante könnte z.B. wie in Abb. 7 aussehen:
Abb. 7: Variante eines Map Reduce Merge Workflows.
Neben den gezeigten hierarchischen Workflows in Abb. 6
und Abb. 7 sind auch rekursive Workflows denkbar.
8.
Fazit:
Das Map Reduce Merge Modell behält die vielen Vorteile des Map Reduce Modells bei und fügt neue
relationale Operationen in dieses Modell ein. Es enthält einige neue und frei konfigurierbare
Komponenten, was dem Benutzer ganz neue Möglichkeiten eröffnet.
Ein nächster Schritt besteht nun darin, ein SQL-ähnliches Interface und Optimierer einzubinden.
Schlussendlich wird die Idee der "Einfachheit", welche hinter dem Map Reduce Modell steckt, nicht
eingeschränkt.
9.
[1]
[2]
[3]
[4]
[5]
[6]
[7]
[8]
Literatur
Text basiert auf:
H. Yang, A. Dasdan, R. Hsiao, D. Parker:
Map-Reduce-Merge: Simplified Relational Data Processing on Large Clusters,
S. 1029-1040, 2007
S. Ghemawat, H. Gobioff, and S.-T. Leung:
The Google file system. In SOSP, Seiten 29–43, 2003
Dean, Ghemawat:
Map Reduce: Simplified Data Processing on Large Clusters. In OSDI S 137-150, 2004
F. Chang et al. Bigtable:
A Distributed Storage System for Structured Data. In OSDI, Seiten 205–218, 2006.
M. Isard et al. Dryad:
Distributed Data-Parallel Programs from Sequential Building Blocks. In EuroSys, 2007
R. Pike et al.
Interpreting the Data: Parallel Analysis with Sawzall.
Scientific Programming Journal, 13(4): Seiten 227–298, 2005.
IBM InfoSphere Information Center, 06/2011
http://publib.boulder.ibm.com/infocenter/iisinfsv/v8r5/index.jsp?topic=/com.ibm.swg.im.iis.d
s.parjob.dev.doc/topics/rangepartitioner.html
IBM InfoSphere Information Center, 06/2011
http://publib.boulder.ibm.com/infocenter/iisinfsv/v8r5/index.jsp?topic=/com.ibm.swg.im.iis.d
s.parjob.dev.doc/topics/hashpartitioner.html
DeWitt, Naughton, Schneider:
Parallel Sorting on a Shared- Nothing Architecture Using Probabilistic Splitting. In:
Lu, Ooi, Tan: Query Processing in Parallel Relational Database Systems
IEEE Computer Society Press, 1994
FernUniversität in Hagen
Seminar 01912
im Sommersemester 2011
„MapReduce und Datenbanken“
Thema 12
Optimierung von Joins
Referent: Oliver Schöner
12 Optimierung von Joins | Oliver Schöner
1
Inhaltsverzeichnis
1
Problemlage und Zusammenfassung
2
2
Beispielhafte Erläuterung eines normalen Joins mit MapReduce
3
3
Joins mit mehr als zwei Relationen
3.1 Hintereinanderausführung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.2 Mehrfachabbildung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4
4
4
4
Kosten
4.1 Mehrfachabbildung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4.2 Hintereinanderausführung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4.3 Kostenvergleich . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
6
6
8
8
5
Der Algorithmus
8
5.1 Problem Nullwerte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
5.2 Dominierte Attribute . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
6
Spezialfälle
11
6.1 Stern-Join . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
6.2 Ketten-Join . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
7
Ausblick und Kritik
12
7.1 Geringer Nutzen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
7.2 Beschränkung auf Natural Joins . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
7.3 Fragwürdigkeit des Kostenmodells . . . . . . . . . . . . . . . . . . . . . . . . . . 13
8
Literatur
14
2
1
12 Optimierung von Joins | Oliver Schöner
Problemlage und Zusammenfassung
Auch und gerade bei sehr großen Datenbanken stellt sich die Frage, wie Joins effizient durchgeführt werden können. MapReduce bietet auch in diesem Bereich Lösungen, wie auf natürlichem
Weg eine Vielzahl von Prozessoren nutzbringend eingesetzt werden kann. Im Folgenden werde ich
zunächst darlegen, wie ein einfacher Join intuitiv mit MapReduce durchgeführt wird. Sodann diskutiere ich, welche Vorgehensweise sich empfiehlt, wenn mehr als zwei Relationen bei einem Join
involviert sind. Kern dieses Vorgehens ist die Idee, bestimme Tupel auf mehrere Reduce-Prozesse
abzubilden. Davon ausgehend werde ich das Kostenmodell diskutieren und einen einfachen allgemeinen Algorithmus vorstellen, der die Kosten generell zu minimieren trachtet. Die Zahl der
Reduce-Prozesse zu bestimmen, auf die die Tupel einer Relation abgebildet werden, ist der Kern
des Algorithmus. Dieser Algorithmus hat einige (theoretische) Probleme, deren Lösungen ich kurz
vorstelle. Nach der Vorstellung zweier Spezialfälle und ein paar kritischer Überlegungen möchte
ich die Diskussion eröffnen.1
Es wird nun ein wenig mathematisch. Zur besseren Übersicht erläutere ich kurz die Konventionen,
deren ich mich bediene:
• Großbuchstaben R, S, T . . . etc. bezeichnen Relationen.
• Kleinbuchstaben r, s,t . . . ∈ N bezeichnen die Zahl der Tupel der Relationen R, S, T . . .
• Großbuchstaben A, B,C . . . sind die Attribute, die in einer Relation vorhanden sind.
• Griechische Buchstaben α, β , γ . . . sind Werte von Attributen.
• Kleinbuchstaben a, b, c . . . ∈ N bezeichnen die Zahl der Reduce-Prozesse, auf die diejenigen Tupel abgebildet werden, in denen die Join-Attribute A, B,C . . . nicht vorkommen. Wir
nennen a, b, c . . . Share-Variablen.
• Der Kleinbuchstabe k ist die Gesamtzahl der Reduce-Prozesse.
• Der Kleinbuchstabe h ist die Hashfunktion, mit der ein Key-Value-Paar auf einen ReduceProzess abgebildet wird.
• Der Kleinbuchstabe f bezeichnet die Kostenfunktion.
• Der Kleinbuchstabe p bezeichnet die Wahrscheinlichkeit, dass Tupel zweier Relationen gejoint werden.
1 Die
Basis meiner Ausführungen bildet der Artikel von Afrati und Ullman [AU10]. Sie sind daraufhin konzipiert worden, ohne Zuhilfenahme dieses Artikels oder weiterer Literatur gelesen werden zu können. Wo dennoch auf
[AU10] Bezug genommen wird, dient dies lediglich als Quellenangabe.
12 Optimierung von Joins | Oliver Schöner
2
3
Beispielhafte Erläuterung eines normalen Joins mit MapReduce
Wie kann nun ein normaler Join mit MapReduce durchgeführt werden?2 Betrachten wir zwei Relationen, denen ein Attribut gemein ist:
R(A, B) o
n S(B,C).
Der Join soll also über das Attribut B durchgeführt werden. Jedes Tupel der beiden Relationen
R und S wird zunächst in ein Key-Value-Paar transformiert, wobei die Werte der B-Komponente,
über die der Join ausgeführt wird, den Key bilden. Als Value werden Paare aus den Werten des jeweils anderen Attributs und der Relation verwendet. Es entstehen somit Key-Value-Paare der Form
(β1 , (α1 , R)), . . . , (βr , (αr , R)), (βr+1 , (γr+1 , S)), . . . , (βr+s , (γr+s , S)).
Beispiel: Sind A-Werte Strings, B-Werte natürliche Zahlen und befindet sich das Tupel (Heinrich, 8)
in der Relation R, so wird daraus das Key-Value-Paar (8, (Heinrich, R)). Nun wenden wir unsere
Hashfunktion h auf jeden B-Wert an (im Beispiel auf 8, es wird der Wert h(8) berechnet). Die
Hash-Funktion sorgt dafür, dass alle entstandenen Key-Value-Paare auf die maximal k zur Verfügung stehenden Reduce-Prozesse verteilt werden. Wir nehmen einfach an, wir hätten eine geeignete Hash-Funktion h, die gut streut. Tupel mit gleichen B-Werten werden auf ein und denselben
Reduce-Prozess abgebildet – aber dass sich zwei Tupel im gleichen Reduce-Prozess befinden,
heißt nicht, dass sie auch den gleichen B-Wert haben. Folgende Tabelle mit der Zuordnung der
Key-Value-Paare könnte entstehen:
Reduce-Prozess 1 Reduce-Prozess 2 . . .
(βx , (αx , R))
...
(βy , (γy , S))
(βz , (γz , S))
(βu , (αu , R))
...
...
Reduce-Prozess k
...
Jetzt sehen wir auch den Grund, weswegen die Relationen R und S als Elemente in den Value-Teil
unserer Tupel aufgenommen wurden. Sehen wir uns Reduce-Prozess Nr. 2 kurz an, dann erkennen
wir, dass die ersten beiden Key-Value-Paare nicht darauf geprüft werden müssen, ob sie identische
B-Werte haben – sie stammen ja aus der gleichen Relation S. Geprüft werden muss nur, ob βy = βu
und βz = βu ist. Ist nun zum Beispiel βy = βu , dann gelangt das Tupel (αu , βy = βu , γy ) in das
Ergebnis unseres Joins.
Um bei unserem Beispiel von vorhin zu bleiben: Es enthalte die Relation R das Tupel (Heinrich, 8)
und die Relation S die Tupel (8, Anne) und(7, Berta). Wenn nun weiterhin h(8) = h(7) = 2 ist, dann
werden alle drei Tupel auf den gleichen Reduce-Prozess Nr. 2 abgebildet. Da 8 6= 7 ist, fließt das
Tripel (Heinrich, 8, Berta) nicht in das Ergebnis unseres Joins ein, wohl aber (Heinrich, 8, Anne).
2 [AU10],
2.1
4
12 Optimierung von Joins | Oliver Schöner
Halten wir fest: Bei einem Join mit MapReduce werden Key-Value-Paare gebildet, wobei das oder
die Attribute, über die der Join gebildet wird, den Key bilden. Auf den Key wird dann eine Hashfunktion angewendet und so das Key-Value-Paar auf einen Reduce-Prozess abgebildet; innerhalb
jedes Reduce-Prozesses muss dann geprüft werden, ob Tupel aus unterschiedlichen Relationen den
gleichen Key besitzen.
3
Joins mit mehr als zwei Relationen
Wir wollen nun sehen, wie wir bei einem komplizierteren Join vorgehen, nämlich wenn mehrere
Relationen involviert sind. Hier können wir entweder das soeben betrachtete Verfahren wiederholt
anwenden – oder, wie wir gleich sehen werden, ein ganz anderes.
3.1
Hintereinanderausführung
Betrachten wir den „zyklischen“ Join3
R(A, B) o
n S(B,C) o
n T (A,C).
Gemäß dem bisherigen Vorgehen könnten wir zunächst die Relationen R und S mit Key B joinen.
Es entsteht einen Zwischenrelation (nennen wir sie RS) mit den Attributen A, B,C. Diese Zwischenrelation RS joinen wir dann mit der Relation T und Key (A,C).4 Es entsteht die Ergebnisrelation
RST, die wiederum die Attribute A, B,C enthält:
R(A, B) o
n S(B,C) = RS(A, B,C)
RS(A, B,C) o
n T (A,C) = RST (A, B,C).
3.2
Mehrfachabbildung
Es gibt aber noch einen anderen Weg, und der besteht darin, jedes Tupel einer Relation auf mehrere
Reduce-Prozesse abzubilden. Wir erinnern uns: Es gibt k Reduce-Prozesse. Betrachten wir ein
beliebiges Tupel (α, β ) ∈ R. Wir bilden nun dieses Tupel auf c verschiedene Reduce-Prozesse ab.
Wir nennen diese Variable c, weil sie von dem Attribut C abhängt. Das Attribut C gehört nämlich
zum Key (schließlich wird auch über C gejoint), doch in der Relation R kommt C gerade nicht
vor. Doch welchen Wert hat c? Nun ist c sicher nicht größer als k; im Moment können wir uns
irgendeinen Wert für c denken. Wir werden später einen Weg sehen, einen optimalen Wert für c zu
finden.
Im Folgenden wird die Nummer eines Reduce-Prozesses nicht als natürliche Zahl angegeben, sondern als ein Tripel (x, y, z) – als ein Tripel deshalb, weil über drei Attribute gejoint wird. Auch hier
möge ein Beispiel zur Veranschaulichung dienen: Angenommen, wir haben sechs Reduce-Prozesse
3 Für
dieses Beispiel und die Grundidee siehe [Au10], 2.4.
dann die Hashfunktion h geeignet intelligent auf eine Kombination der Werte aus A und C angewendet
werden muss.
4 Wobei
12 Optimierung von Joins | Oliver Schöner
5
zur Verfügung. Dann können wir diese Prozesse mit 1 bis 6 durchnummerieren. Wir können uns
aber auch das Leben künstlich schwer machen und Tripel zur Nummerierung verwenden. Wenn x
nur den Wert 1, y die Werte 1 oder 2 und z Werte zwischen 1 und 3 annehmen kann, dann ist die
Nummer unseres Reduce-Prozesses durch ein Tripel (x, y, z) eindeutig bestimmt, es gibt 1 · 2 · 3 = 6
Reduce-Prozesse:
Nummer Reduce-Prozess als natürliche Zahl Nummer Reduce-Prozess als Tripel (x, y, z)
1
2
3
4
5
6
(1, 1, 1)
(1, 1, 2)
(1, 1, 3)
(1, 2, 1)
(1, 2, 2)
(1, 2, 3)
Doch warum um Himmels willen machen wir das so kompliziert? Die Antwortet lautet, dass es
die Zählung vereinfacht. Erinnern wir uns: Wir wollen das Tupel ((α, β ), R) auf c verschiedene
Reduce-Prozesse abbilden. Wir wenden nun unsere Hash-Funktion h auf α und β an und lassen
den z-Wert variabel, das heißt, z wird alle Werte zwischen 1 und c annehmen. Es landet dann unser
Tupel ((α, β ), R) in den Reduce-Prozessen
(h(α), h(β ), 1), (h(α), h(β ), 2), . . . , (h(α), h(β ), c).
Noch einmal ein Beispiel: Sei wieder (Heinrich, 8) Element der Relation R. Weiterhin gelte h(8) =
1 und h(Heinrich) = 2. Wie oben sei c = 3. Dann wird das Key-Value-Paar ((Heinrich, 8), R)
auf die drei Reduce-Prozesse (1, 2, 1), (1, 2, 2) und (1, 2, 3) abgebildet. Kehren wir wieder zur
abstrakteren Sicht zurück:
Für die Tupel aus den Relationen S und T gilt Entsprechendes wie für die Tupel aus R: Jedes (β , γ) ∈ S – oder genauer: jedes Key-Value-Paar ((β , γ), S) – wird auf die Reduce-Prozesse
(x | 1 ≤ x ≤ a, h(β ), h(γ)) und jedes (α, γ) ∈ T auf die Reduce-Prozesse (h(α), y | 1 ≤ y ≤ b, h(γ))
gemappt. Alle Attribute A, B,C sind in diesem Fall Teil des Keys (über alle Attribute wird gejoint).
Wie viele Reduce-Prozesse gibt es nun insgesamt? Jeder Reduce-Prozess hat ja eine Nummer
(x, y, z), wobei x den Minimalwert 1 und den Maximalwert a, y den Minimalwert 1 und den Maximalwert b und z den Minimalwert 1 und den Maximalwert c hat. Es gilt also:
a · b · c = k.
Innerhalb jedes Reduce-Prozesses müssen nun die Key-Value-Paare darauf geprüft werden, ob
sie bezüglich des Keys übereinstimmen und aus verschiedenen Relationen stammen. Dass das
Verfahren korrekt arbeitet, machen wir uns wie folgt klar. Damit es einen „Treffer“ gibt, muss
es Attributwerte α, β und γ geben mit (α, β ) ∈ R, (β , γ) ∈ S und (α, γ) ∈ T . Es landet also
das Key-Value-Paar ((α, β ), R) in den Reduce-Prozessen (h(α), h(β ), 1 . . . c). Das Key-Value-
6
12 Optimierung von Joins | Oliver Schöner
Paar ((β , γ), S) wird auf die Reduce-Prozesse (1 . . . a, h(β ), h(γ)) gemappt. Und schließlich landet ((α, γ), T ) in (h(α), 1 . . . b, h(γ)). Insbesondere befinden sich in dem Reduce-Prozess mit der
Nummer (h(α), h(β ), h(γ)) alle drei Key-Value-Paare, sodass dieser das korrekte Join-Resultat
(α, β , γ) finden kann.
4
Kosten
In unserem Kostenmodell zählen wir immer nur die Kosten, die bei der Kommunikation zwischen
der Map- und der Reduce-Phase anfallen.5 Die Kosten des Reduce-Prozesses selbst werden vernachlässigt (wir gehen davon aus, dass diese Operation im Hauptspeicher durchgeführt wird); es
sei denn, das Ergebnis des Reduce-Prozesses wird per Mapping einem neuen Reduce-Prozess zugeführt. Das wird in unserem Beispiel R(A, B) o
n S(B,C) o
n T (A,C) gleich klarer werden, wenn
wir die Kosten der Hintereinanderausführung mit denen der Mehrfachabbildung vergleichen. Betrachten wir zuerst den Join, bei dem die Tupel auf mehrere Reduce-Prozesse verteilt werden.
4.1
Mehrfachabbildung
In unserem Beispiel R(A, B) o
n S(B,C) o
n T (A,C) müssen wir die Kosten einer jeden Relation
addieren. Betrachten wir die erste Relation R. Sie hat r Tupel und jedes Tupel wird auf c ReduceProzesse abgebildet. Die Kosten für diese Relation betragen also r · c. Übertragen wir diese Überlegung auf die anderen Tupel, so erhalten wir die Kosten6
r · c + s · a + t · b.
Auf die Anzahl der Tupel einer Relation haben wir keinen Einfluss, wohl aber auf die Anzahl der
Reduce-Prozesse, auf die die Tupel einer Relation abgebildet werden. Folglich können wir diesen
Ausdruck als Funktion von a, b, c auffassen:
f (a, b, c) = rc + sa + tb.
Für diese Funktion gilt es nun ein Minimum zu finden. Da a · b · c = k ist, können wir diese Funktion auch schreiben als
f (a, b, c) = rc + sa + tb − λ (abc − k),
wobei λ eine beliebige reelle Zahl sei. Wir leiten nun diese Funktion nacheinander nach a, b und c
ab und setzen das Ergebnis gleich 0, um das Minimum zu erhalten.7 Daraus ergeben sich die drei
Gleichungen
5 Siehe
[AU10], 1.2.
[AU10], 2.4.
7 Afrati und Ullman erläutern diesen Punkt in [AU10] nicht näher, sondern setzen das Verfahren als gegeben voraus.
Ob es wirklich tauglich ist, möchte ich an dieser Stelle offen lassen. Siehe auch meine Kritik unter 7.1.
6 Siehe
12 Optimierung von Joins | Oliver Schöner
7
s − λ bc = 0
t − λ ac = 0
r − λ ab = 0.
Wir multiplizieren die erste Gleichung mit a, die zweite mit b und die dritte mit c und erhalten
sa − λ abc = 0
tb − λ abc = 0
rc − λ acb = 0
oder, da abc = k ist,
sa = λ k
tb = λ k
rc = λ k.
Wenn wir diese drei Gleichungen miteinander multiplizieren, erhalten wir rstabc = λ 3 k3 oder
λ=
r
3
rst
.
k2
An λ selbst sind wir natürlich nicht interessiert.qSetzen wir diesen Wert für λ in die obigen Glei· k oder
chungen sa = λ k etc. ein, so erhalten wir sa = 3 rst
k2
r
rtk
2
rs
3 rsk
b =
2
rt
3 stk
c =
.
r2
a =
3
Durch erneutes Einsetzen in unseren zu minimierenden Term rc + sa + tb sehen wir:
√
3
rc + sa + tb = 3 · rstk.
Wir nehmen vereinfachend an, dass alle drei Relationen gleich groß sind, also r = s = t gilt. Dann
sind die Gesamtkosten bei der Mehrfachabbildung zunächst 3r (jede Relation muss einmal kom√
3
plett durchlaufen werden, um den Map-Key zu bilden) plus den Kommunikationskosten 3 · r3 k.
Es ergeben sich die Gesamtkosten
8
12 Optimierung von Joins | Oliver Schöner
√
√
3
3
3 · r + 3 · r3 k = O(r k).
4.2
Hintereinanderausführung
Wir blicken noch einmal zurück, was wir bei der Hintereinanderausführung8 des zyklischen Joins
R(A, B) o
n S(B,C) o
n T (A,C) getan haben. Im ersten Schritt werden nur die beiden Relationen R
und S durchlaufen; Kosten also 2r, sofern alle Relationen die Größe r haben. Bei dem Zwischenjoin
R(A, B) o
n S(B,C) wird im schlechtesten Fall jedes Tupel von R mit jedem Tupel von S daraufhin
geprüft, ob sie bezüglich des Attributs B übereinstimmen. Die Wahrscheinlichkeit für einen Treffer
sei mit p angegeben, folglich beträgt die Größe des Zwischenergebnisses – unabhängig von dem
verwendeten Join-Verfahren – r2 · p. Im dritten Schritt kommt noch einmal die Größe r der Relation T hinzu. Wenn wir annehmen, dass r2 · p größer als 3r ist – das wird der Fall sein, wenn die
Relationen wie im richtigen Leben hinreichend groß sind –, dann ergeben sich als Gesamtkosten
2r + r2 · p + r = O(r2 · p).
4.3
Kostenvergleich
√
Die Gegenüberstellung von O(r 3 k) bei der Mehrfachabbildung und O(r2 · p) bei der Hinterein√
anderausführung zeigt: Wenn r 3 k r2 · p bzw. k (rp)3 ist, dann ist die Mehrfachabbildung
günstiger.
5
Der Algorithmus
Aus dem konkreten zyklischen Join R(A, B) o
n S(B,C) o
n T (A,C) lässt sich leicht der allgemeine
Fall9 darlegen, wie ein gegebener Join durch Mehrfach-Mapping optimiert werden kann. Wir nehmen an, wir haben die Relationen R1 , . . . , Rn und die Join-Attribute A1 , . . . , Am .
1. Bilde den Kostenausdruck
τ1 + τ2 + · · · + τn − λ (a1 a2 . . . am − k).
Für ein beliebiges τi gilt:
τi = ri · a j | ∀ j : A j ∈
/ Ri .
Jedes ri wird also genau mit den Share-Variablen multipliziert, deren zugehörige Attribute nicht in
der Relation Ri enthalten sind.
2. Leite den Ausdruck nach allen ai ab, multipliziere die entstehenden Terme wieder mit ai und
setze sie gleich 0. Es entstehen Gleichungen der Form
8 Siehe
9 Siehe
[AU10], 2.5.
[AU10], 3.1.
12 Optimierung von Joins | Oliver Schöner
9
Sai = λ k,
wobei die Sai Summen sind.
3. Multipliziere alle Gleichungen miteinander und löse diese nach den Share-Variablen ai auf,
wobei insbesondere alle λ eliminiert werden.
5.1
Problem Nullwerte
Bei der Berechnung der Werte von ai kann es einige Probleme10 geben. Es ist nämlich nicht garantiert, dass bei der Auflösung auch ganze Zahlen entstehen – aber ganze Zahlen werden natürlich
gefordert: Die Tupel einer Relation können keinesfalls auf dreieinhalb Reduce-Prozesse abgebildet
werden, sondern nur auf drei oder vier. In einem solchen Fall liegt es natürlich nahe zu runden.
Dass eine solche Näherung zu guten Ergebnissen führt, klingt zwar plausibel, ist aber keineswegs erwiesen.11 Ein noch ernsthafteres Problem stellen die Fälle dar, in denen sich für bestimmte
Share-Variablen der Wert 0 ergibt. Betrachten wir folgendes Beispiel:
R(A, B,C) o
n S(A, B, D) o
n T (A, D, E) o
n U(D, F).
Wir identifizieren die Attribute A, B und D als Elemente des Map-Keys (nur über sie wird gejoint).
Folglich ergibt sich die Kostenfunktion
f (a, b, d) = rd + s + tb + uab − λ (abd − k).
Die Ableitungen nach a, b und d mit anschließendem Multiplizieren und Nullsetzen führen zu den
drei Gleichungen
uab = λ k
tb + uab = λ k
rd = λ k
Es folgt
tb = 0.
Das kann natürlich nicht sein. Dies würde nämlich bedeuten, dass entweder t oder b gleich 0 ist.
Die Relation T dürfte folglich entweder überhaupt keine Tupel enthalten oder die Tupel der beiden
Relationen T und U würden auf keine Reduce-Prozesse abgebildet. Was ist hier das Problem?
10 [AU10],
3.2
11 Insbesondere
dann, wenn der Wert viel kleiner als 1 und nahezu 0 ist, stellt sich die Frage, wie man vorgehen soll.
Es bleibt einem dann nichts anderes übrig, als diesen Wert gleich 1 zu setzen.
10
12 Optimierung von Joins | Oliver Schöner
5.2
Dominierte Attribute
Zumindest in vorliegendem Fall12 lässt sich dieses Problem dadurch lösen, dass sogenannte dominierte Attribute aus dem Map-Key entfernt werden. Wir sagen: Ein Attribut X dominiert ein
Attribut Y , wenn jede Relation, die Y enthält, auch X enthält.
Solche dominierten Attribute werden aus dem Map-Key entfernt. Die dahinterstehende Idee sollte
intuitiv klar sein: Wenn über dominierte Attribute gejoint wird, dann wird der Join auch immer
über die entsprechenden dominierenden Attribute durchgeführt. Eine zusätzliche Vermehrung der
Reduce-Prozesse ist dann nicht notwendig. Für unser Beispiel
R(A, B,C) o
n S(A, B, D) o
n T (A, D, E) o
n U(D, F)
bedeutet das: Attribut B wird von A dominiert, denn B ist nur in R und S enthalten, darin kommt
aber auch immer A vor. Hingegen wird A nicht von B dominiert, denn nicht in allen Relationen,
in denen A vorkommt, kommt auch B vor – in der Relation T ist zwar A enthalten, nicht aber B.13
Die Folge ist, dass die Variable b nicht mehr als Argument in unserer Kostenfunktion auftritt. Die
neue Kostenfunktion lautet somit:
f (a, d) = rd + s + t + ua − λ (ad − k).
Da dann auch nicht mehr nach b abgeleitet wird, entstehen bei der Ableitung nur noch die beiden
Gleichungen
ua = λ k
rd = λ k.
Die störende Null-Lösung ist somit verschwunden. Dass durch das Entfernen dominierter Attribute immer ein mindestens ebenso günstiger Kostenausdruck entsteht wie zuvor, lässt sich sogar
beweisen.14 An dieser Stelle verzichten wir darauf, das zu tun.
Leider zeigt sich, dass mit dem Konzept der dominierten Attribute nicht alle Null-Lösungen eliminiert werden können.15 Vielmehr gibt es einen recht komplizierten Algorithmus, der beschreibt,
was zu tun ist, wenn es weitere Null-Lösungen gibt.16 Die Idee ist auch hier, eine der ShareVariablen zu eliminieren und eine mindestens ebenso günstige Lösung zu suchen. Dazu muss das
ursprüngliche Problem in Teilprobleme zerlegt werden, von denen wiederum jedes einzelne optimiert wird. Auf Details verzichte ich hier.
12 [AU10],
3.2
gibt hier noch ein paar andere Dominanzen, so wird F von D dominiert. Das ist hier aber nicht von Belang.
14 [AU10], 3.3
15 [AU10], 3.4
16 [AU10], 3.5
13 Es
12 Optimierung von Joins | Oliver Schöner
6
11
Spezialfälle
Im Folgenden seien zwei Spezialfälle kurz angerissen, bei denen der Kostenausdruck besonders
interessant wird: der Stern-Join und der Ketten-Join.
6.1
Stern-Join
Beim Stern-Join17 gibt es eine in der Regel große, zentrale, sogenannte Faktentabelle und mehrere
sogenannte Dimensionstabellen, die sich – bildlich gesprochen – sternförmig um die Faktentabelle
gruppieren. Betrachten wir folgendes Beispiel: Die Faktentabelle sei die Relation R(A, B, D,C),
die Dimensionstabellen die Relationen S(A, E), T (B, F), U(C, G) und V (D, H). Wir sehen, dass
jede Dimensionstabelle ein gemeinsames Attribut mit der Faktentabelle hat. Der Join
R(A, B, D,C) o
n S(A, E) o
n T (B, F) o
n U(C, G) o
n V (D, H)
führt zu der Kostenfunktion
f (a, b, c, d) = r + sbcd + tacd + uabd + vabc.
Wenn wir diese Funktion in der gewohnten Weise nach a, b, c und d ableiten und die Gleichungen
nacheinander auflösen, sehen wir nicht nur, dass
a=
r
4
ks3
tuv
ist. (Für die Share-Variablen b,c und d gibt es ähnlich aussehende Lösungen). Bemerkenswert ist
auch, dass sich aus den Ableitungen der Kostenfunktionen die Verhältnisse
s
t
u v
= = =
a b c d
ergeben. Dies zeigt uns, dass je mehr Tupel eine Relation hat (S hat s Tupel), desto größer auch
die dazugehörige Share-Variable a (deren Attribut A sich in der gleichen Relation S befindet) sein
muss. Je größer wiederum a ist, desto kleiner müssen b, c und d sein (da ja abcd = k gilt). Daraus
folgt, dass die (vielen) Tupel der Relation S auf vergleichsweise wenige (b · c · d) Reduce-Prozesse
abgebildet werden, oder allgemein: Je größer eine Relation, auf desto weniger Reduce-Prozesse
werden ihre Tupel abgebildet. Im Sinne der Kostenminimierung vermag dieser Sachverhalt nicht
zu überraschen.
17 Siehe
[AU10], 4.1.
12
12 Optimierung von Joins | Oliver Schöner
6.2
Ketten-Join
Bei einem Ketten-Join18 werden die Relationen „verkettet“:
R1 (A0 , A1 ) o
n R2 (A1 , A2 ) o
n ··· o
n Rn (An−1 , An ).
Wir sehen, dass nur die Attribute A1 bis An−1 Teil des Map-Keys sind, nicht aber A0 und An .
Entsprechend gilt es, die Werte der Share-Variablen a1 bis an−1 zu bestimmen. Hier zeigt sich19
ein überraschendes Ergebnis: Es kommt nämlich darauf an, ob wir es mit einer geraden oder einer
ungeraden Zahl an Relationen zu tun haben. Hierzu nehmen wir wieder vereinfachend an, dass alle
Relationen die gleiche Größe r haben.
Fall 1: n ist gerade. Dann bekommen alle „geraden“ Share-Variablen den Wert 1:
a2 = a4 = · · · = an−2 = 1,
oder anders ausgedrückt: Die dazugehörigen Join-Attribute A2 , . . . , An−2 werden aus dem Map-Key
entfernt. Für die „ungeraden“ Share-Variablen wiederum gilt: Auch sie haben alle den gleichen
Wert, der nur duch k und n bestimmt ist:
a1 = a3 = · · · = an−1 =
√
n
k2 .
Fall 2: n ist ungerade. Dann sollte zunächst der Wert von a2 bestimmt werden; und in Abhängigkeit
davon gilt für alle i von i = 0 bis20 i = n−3
2 :
a2i = (a2 )i und
a2i+1 = (a2 )((n−1)/2)−i
Das bedeutet: Je weiter hinten in der Kette die „geraden“ a stehen, desto größer werden sie; und je
weiter hinten die „ungeraden“ a in der Kette stehen, desto kleiner werden sie.
7
Ausblick und Kritik
Wir haben gesehen, wie die Kosten eines Joins unter Verwendung von MapReduce verringert werden können, sofern man in Kauf nimmt, die Tupel einer Relation auf mehrere Reduce-Prozesse
abzubilden. Doch der vorgestellte Algorithmus weist bei näherem Hinsehen einige Schwächen
auf, die hier kurz vorgestellt seien: Zum ersten wird doch ein erheblicher algorithmischer Aufwand für einen beschränkten Nutzen betrieben. Zweitens eignet sich das Vorgehen lediglich für
Natural Joins. Drittens schließlich bleiben die Kosten recht abstrakt.
18 Siehe
[AU10], 4.3.
führe das hier nicht aus, sondern stelle nur das Ergebnis dar. Für Details siehe [AU10], 4.4.
20 Afrati und Ullman zählen in [AU10], 4.4.2 erst ab i = 1. Für i = 0 stimmen die Gleichungen aber ebenso, und es
wird auch der Fall von a1 korrekt erfasst.
19 Ich
12 Optimierung von Joins | Oliver Schöner
7.1
13
Geringer Nutzen
Der vorgestellte Algorithmus scheint auf den ersten Blick recht gut umsetzbar: Join-Attribute identifizieren, Kostenfunktion ableiten und die entsprechenden Gleichungen zu lösen scheint nicht
schwer. Allerdings ist der Nutzen nicht klar. Natürlich wird auf diese Weise ein Minimum der
Kostenfunktion gefunden, aber man findet eben nur ein Minimum. Dass dieses Minimum nicht nur
ein lokales Minimum ist, dafür gibt es keine Garantie. Eventuell müsste der Schritt zur Minimumbestimmung erheblich verfeinert werden – unter Verwendung etwas anspruchsvollerer mathematischer Methoden. Afrati und Ullman plädieren denn auch dafür, die gefundenen Lösungen nicht
zu verabsolutieren.21 Vielmehr seien die Lösungen eher so aufzufassen, dass die Tupel mancher
Relationen eben auf viele und die Tupel anderer Relationen auf wenige Reduce-Prozesse abgebildet werden. Aber das lässt sich natürlich auch mit scharfem Nachdenken ohne einen komplizierten
Algorithmus plausibel machen: Wenn ich in einer Relation viele Tupel habe, kann ich die Kosten dadurch verringern, dass ich jedes einzelne dieser Tupel nicht auf zu viele Reduce-Prozesse
verteile. Interessanter erscheint daher der Vorschlag, die Zahl der Reduce-Prozesse nicht als Konstante aufzufassen. Gegebenenfalls kann es sinnvoll sein, insgesamt weniger Reduce-Prozesse zu
verwenden – damit sinken auch die Gesamtkosten.
7.2
Beschränkung auf Natural Joins
Ein Punkt, der bei Afrati und Ullman nur einmal ganz kurz anklingt,22 scheint mir doch von erheblicher Bedeutung zu sein: Das Verfahren, die Tupel einer Relation per Hashfunktion auf bestimmte
Reduce-Prozesse abzubilden, funktioniert selbstverständlich nur bei Natural Joins mit Gleichheitsbedingung. Soll ein Join durchgeführt werden mit einer Bereichsbedingung, so ist überhaupt nicht
klar, wie MapReduce hier sinnvoll eingesetzt werden kann.
7.3
Fragwürdigkeit des Kostenmodells
Schließlich ist das Kostenmodell als solches fragwürdig. Ginge es nämlich darum, nur die erwähnten Kosten einzusparen, dann wäre es am einfachsten, überhaupt nur einen einzigen ReduceProzess einzusetzen (also auf MapReduce zu verzichten). Tatsächlich würde dann jedes Tupel nur
auf diesen einen Reduce-Prozess abgebildet und die Kosten wären minimal. Dafür würde aber der
Zeitaufwand erheblich steigen. Wenn wir auf der anderen Seite beliebig viele Reduce-Prozesse
hätten, würden die theoretischen Kosten zwar stark ansteigen (viele, viele Tupel in vielen ReduceProzessen), aber insgesamt würde der Join natürlich schneller gehen. Diese reziproke Verhältnis
zwischen Kommunikationskosten wird zwar kurz erwähnt;23 dass der Faktor Zeit in dem Kostenmodell aber keinerlei Rolle spielt, ist schon zu hinterfragen: Kommt es nicht auch darauf an, den
Join möglichst schnell durchzuführen?
21 [AU10],
3.6
4
23 [AU10], 2.6
22 [AU10],
14
8
12 Optimierung von Joins | Oliver Schöner
Literatur
[AU10] Afrati, Foto N.; Ullman, Jeffrey D.: Optimizing Joins in a Map-Reduce Environment. In:
Proceedings of the 13th International Conference on Extending Database Technology ACM, 2010,
S. 99–110.
FernUniversität in Hagen
Seminar 01912
im Sommersemester 2011
„MapReduce und Datenbanken“
Thema 13:
Spatial Join with MapReduce on Clusters
Referentin:
Katharina Eberl
Gliederung
1.
Einleitung ................................................................................................................................... 3
2.
Einführung in Spatial Join ........................................................................................................... 3
2.1.
3.
Erklärung des Spatial Joins .................................................................................................. 3
2.1.1.
Filter Step .................................................................................................................... 4
2.1.2.
Refinement Step .......................................................................................................... 5
2.2.
Verwendete Algorithmen..................................................................................................... 5
2.3.
Probleme bei der Anwendung mit MapReduce .................................................................... 5
Vorstellung Spatial Join mit MapReduce (SJMR) ........................................................................ 6
3.1.
Determinierung der Partitionen ............................................................................................ 6
3.2.
Map stage ............................................................................................................................ 6
3.2.1.
Homogenizing Step ..................................................................................................... 7
3.2.2.
Spatial Splitting Step ................................................................................................... 7
3.3.
Reduce stage ..................................................................................................................... 12
3.3.1.
Filter Step .................................................................................................................. 12
3.3.2.
Refinement Step ........................................................................................................ 13
3.4.
Vor- und Nachteile ............................................................................................................ 14
4.
Vermeidung von Duplikaten im Ergebnis .................................................................................. 14
5.
Beurteilung der Performance ..................................................................................................... 15
6.
Folgerungen .............................................................................................................................. 18
I.
Quellen ..................................................................................................................................... 19
II.
Abbildungsverzeichnis .............................................................................................................. 20
Seite | 2
1. Einleitung
MapReduce ist ein von Google eingeführtes Framework, um Berechnungen über große
Datenmengen auf Rechnercluster durchführen zu können. Eine Möglichkeit des Joins stellt
die Kombination von MapReduce mit Spatial Join dar. „Spatial Join with MapReduce on
Cluster“ (kurz SJMR) stellt eine Methode vor, wie dieser Join effizient implementiert und
angewendet werden kann. Um die Zusammenhänge besser aufzeigen zu können, erfolgt zuerst
eine allgemeine Einführung in Spatial Join, bevor anschließend auf SJMR im Detail
eingegangen wird.
2. Einführung in Spatial Join
Ein Spatial Join ermöglicht es Informationen aus einem oder mehreren Datenbeständen zu
kombinieren und so neue Informationen zu erzeugen. Die Beziehung zwischen den
Datenbeständen wird durch geometrische Daten dargestellt. Als Kombinationskriterium dient
nicht die Gleichheit, sondern das Enthalten sein bzw. die Überlappung. Deshalb brauchen die
geometrischen Wertebereiche nicht identisch zu sein. Sie sollten sich nur auf das gleiche
Koordinatensystem beziehen. Beim Spatial Join handelt es sich um ein sehr mächtiges
Instrument zur Kombination von Daten unterschiedlicher Herkunft.1
2.1. Erklärung des Spatial Joins
Der Spatial Join besteht aus zwei mehrdimensionalen Objekte. Eine Abfrage mit Spatial
Join findet alle Paar von Objekten, die eine gegebene Spatial Relation erfüllt. 2
Beispiel:
Die Fragestellung könnte z. B. sein, dass alle Paare von Flüssen und Städten gefunden
werden müssen, die sich kreuzen. Kreuzt man die Flüsse {R1, R2} und die Städte {C1,
C2, C3, C4, C5}, so ist das Ergebnis { (R1, C1), (R2, C5)}.3 Das Beispiel wird in Abb. 1
veranschaulicht.
1
vgl. [2]
vgl. [3] Seite 2
3
vgl. [1] Seite 1
2
Seite | 3
Abbildung 1: Beispiel für einen Spatial Join
Quelle: [1] Seite 1
Die Ausführung des Spatial Joins erfolgt in zwei Schritten. Der erste Schritt ist der Filter
Step und der zweite wird Refinement Step bezeichnet. Auf den Algorithmus wird nun
detaillierter eingegangen.
2.1.1. Filter Step
Ein wichtiger Begriff für das Verständnis von Spatial Joins ist das „minimal
umgebende Rechteck“, kurz MBR genannt (engl.: minimum bounding rectangle). Es
beschreibt das kleinstmögliche Rechteck, das eine vorgegebene Menge von Objekten
umrandet. Dabei beschränkt sich das MBR nicht auf zweidimensionale Flächen,
sondern es kann auch auf andere Dimensionen erweitert werden. Ein Beispiel hierfür
wird in [6] dargestellt:
Abbildung 2: Beispiel für ein minimal umgebendes Rechteck
Quelle: [6] Seite 6
Durch die Anwendung des MBR soll eine Annäherung bzw. Approximation an die
Tupel, die der Abfrage des Spatial Joins entsprechen, erreicht werden. Die
identifizierten Objekte, die nicht der Abfrage entsprechen werden eliminiert. Dieser
Schritt ist nicht sehr rechenaufwendig, im Gegensatz zum nächsten Schritt, dem
Refinement Step.1
1
vgl. [1] Seite 2
Seite | 4
2.1.2. Refinement Step
Wie der Name schon verrät wird das Ergebnis des Filter Steps noch weiter verfeinert.
Die verbleibenden Paare werden geprüft, ob sie den Anforderungen des Spatial Joins
auch tatsächlich genügen. Durch die Vorfilterung hat sich die Anzahl der Tupel bereits
deutlich reduziert. Dadurch kann dieser rechenintensive Schritt beschleunigt werden.
2.2. Verwendete Algorithmen
Für den Filter Step stehen mehrere Algorithmen zur Verfügung. Allerdings verwenden
moderne Algorithmen spezielle Spatial-Indexe auf beide Inputdatensätze an. Bei
MapReduce wird der Index dynamisch aufgebaut, was zur Folge hat, dass der Index
online auf einen oder beide Inputdatensätze angewendet werden muss. Durch den Einsatz
von online aufgebauten Indexen kann die index-basierte Spatial Join Technik wieder
verwendet werden. Ziel dieser Technik ist es, die Daten so klein zu partitionieren, dass sie
in den internen Speicher passen.1
Somit eigen sich besonders Shared-Nothing-Architekturen. Bei der Shared-NothingArchitektur kann jeder Knoten unabhängig und eigenständig seine Aufgaben mit seinem
eigenen Prozessor und den zugeordneten Speicherkomponenten wie Festplatte und
Arbeitsspeicher erfüllen.
Es kann aber auch eine Shared-Memory-Architektur verwendet werden. Bei dieser Art
nutzen zwei oder mehrere Prozesse einen bestimmten Teil des Arbeitsspeichers
gemeinsam. Für alle beteiligten Prozesse liegt dieser gemeinsam genutzte Speicherbereich
in deren Adressraum und kann mit normalen Speicherzugriffsoperationen ausgelesen und
verändert werden.
Hypercube Architekturen stehen ebenfalls zur Diskussion.
Der große Vorteil ist, dass bei Verwendung dieser Architekturen die Aufgaben parallel
ausgeführt werden können, wodurch sich die Bearbeitungszeit verkürzt wird.
Der Refinement Step kann nicht so einfach parallelisiert werden. Daher wird hier ein
Prozessor dazu verwendet, die Kandidaten in eine lineare Reihenfolge zu bringen, d.h. die
Kandidaten werden in ein einfaches zeitliches nacheinander geordnet. Auf die weiteren
Prozessoren werden die Kandidaten mit dem Round Robin Verfahren verteilt. Das Round
Robin Verfahren, oder auch Rundlauf-Verfahren genannt, gewährt allen Prozessen
nacheinander für jeweils einen kurzen Zeitraum Zugang zu den benötigten Prozessoren.2
2.3. Probleme bei der Anwendung mit MapReduce
Grundsätzlich ist es nicht einfach MapReduce mit Spatial Join zu kombinieren. Während
MapReduce normalerweise auf gleichen Datentypen arbeitet, kann der Spatial Join auch
mit heterogenen Datentypen umgehen. MapReduce verwendet als Objekte meist Wörter,
1
2
vgl. [3] Seite 2 u. 3
vgl. [10]
Seite | 5
oder URLs. Spatial Objekte sind hingegen generell länger und komplexer, was dazu führt
dass Spatial Joins sehr zeitaufwendig und umfangreich werden können. Ein weiteres
Problem ist, dass beim Spatial Join doppelte Datensätze entstehen können, welche die
Parallelisierung von Spatial Join Operationen schwierig macht.1 Darauf wird aber in
Kapitel 4 „Vermeidung von Duplikaten“ genauer eingegangen.
3. Vorstellung Spatial Join mit MapReduce (SJMR)
Trotz der Differenzen zwischen MapReduce und Spatial Joins wurde ein neuer Algorithmus
genannt „Spatial Join with MapReduce (SJMR)“ entworfen, der vor allem auf Clustern
eingesetzt wird. Der Algorithmus besteht aus drei Teilen:
der Determinierung, dem sogenannten Map Stage und dem Reduce Stage.
Um die Ausführungen zu verdeutlichen wird folgendes Beispiel verwendet:
Es gibt zwei Relationen R und S, in denen die beiden Inputdatensätze enthalten sind. Für
jeden Datensatz gibt es einen eindeutigen Schlüssel, genannt OID (engl.: object identifier).
Jeder Datensatz oder auch Tupel genannt, besitzt mindestens einmal das Variablenpaar
(k2,v2). In k2 wird die Nummer der Partition, in v2 wird der eindeutige Schlüssel gespeichert.2
3.1. Determinierung der Partitionen
Die minimale Anzahl von Partitionen (Reduce Tasks) wird von vielen Faktoren
determiniert. Um sie zu kalkulieren geht man wie folgt vor:
Zuerst benötigt man die Kardinalitäten der Relationen R und S. Die Kardinalität gibt die
Anzahl der Elemente einer Menge wieder und wird dargestellt als: ||R|| und ||S||. Der
vervielfachende Koeffizient wird p genannt. M gibt den zur Verfügung stehenden
Speicherplatz im Arbeitsspeicher an und Sizekp stellt die Größe des Keypointer Elements
dar, das aus dem MBR, OID und Feldinformationen besteht, dar.
Somit ergibt sich folgende Formel zur Berechnung der minimalen Anzahl: 3
P= [(||R||+||S||) * (1+p) * Sizekp/M]
3.2. Map stage
Das Ziel von Map Stage ist es, die Tupel so zu verteilen, dass jede Reduce Task nahezu
gleich arbeiten kann und die Verteilung die Validität des Ergebnisses nicht beeinflusst.
Als Verteilungsstrategie wird HDFS (Hadoop’s Disstributed FileSystem) verwendet.
HDFS baut auf den MapReduce-Algorithmus auf und beinhaltet auch Vorschläge aus dem
Google-Dateisystem. Es wurde für skalierbare, verteilt arbeitende Software entwickelt und
eignet sich besonders für rechenintensive Prozesses in Verbindung mit großen
Datenmengen.4
1
vgl. [3] Seite 1
vgl. [3] Seite 3
3
vgl. [3] Seite 3
4
vgl. [4]
2
Seite | 6
Im Rahmen des Master-Slave-Prinzips übernimmt die Rolle des Masters beim HDFS der
NameNode, der alle Metadaten des Filesystems, wie Verzeichnisstrukturen und Dateien
verwaltet. Die sogenannten DataNodes übernehmen die Rolle der Slaves und sind für die
Verwaltung der Nutzdaten im HDFS zuständig. Die Tupel der Relationen R und S werden
somit auf die DataNodes verteilt. Nachdem dies geschehen ist, ist die nächste Aufgabe
von SJMR die Tupel von R und S in verschiedene Reduce Tasks zu verteilen.
Die Verteilung auf die Reduce Tasks erfolgt in zwei Schritten, dem Homegenizing Step
und dem Spatial Splitting Step. Beim zweiten Schritt kommt eine neue Technik genannt n
Spatial Partitioning Function (SPF) zum Einsatz.
3.2.1. Homogenizing Step
MapReduce konzentriert sich vor allem auf die Verarbeitung von homogenen
Datensätzen. Somit ist der erste Schritt die Datensätze zu vereinheitlichen. Dazu wird
ein Zusatz über die Datenquelle an jedes Tupel angehängt. Somit enthält jedes bereits
homogenisierte Tupel vier gemeinsame Attribute: OID, MBR, spatial property und die
Information zur Datenquelle.1
3.2.2. Spatial Splitting Step
Der Gegenstandbereich, auch Universum genannt, wird definiert als das minimale
Rechteck, dass alle Tupel von R und S bedeckt. Das Universum wird in mehrere
Partitionen P aufgeteilt. Falls es keine einheitliche Regel zur Verteilung geben würde,
könnte es dazu kommen, dass manche Partitionen mehr Tupels und andere weniger
enthalten. Wie in Abbildung 3 dargestellt, ist der Großteil in Partition 2 enthalten, die
anderen Partitionen haben weniger Elemente. Das führt zu unterschiedlichen Speicher
und CPU-Verbräuchen.2
Abbildung 3: Verteilung des Daten
Quelle: [3] Seite 4
1
2
vgl. [3] Seite 3
vgl. [3] Seite 4
Seite | 7
Laut [3] baut SPF aus drei Achsen auf:
-
Feldnummer (tile number)
Feldkodierungsmethode (tile coding methode)
Feld-zu-Partition Zuordnungsschema (tile-to-partition mapping scheme)
SJMR übernimmt davon die feldbasierte Methode, die das Universum in Nt
gleichgroße Felder (engl: tile) aufteilt. Allerdings verwendet SJMR eine andere
Feldkodierungsmethode und ein anderes Feld-zu-Partition Zuordnungsschema.
Für die Feldkodierungsmethode wird Spatial-Clustering Technologie verwendet um
die Verteilung möglichst ausgeglichen zu gestallten. Das Spatial-Clustering ist ein
Prozess der Vereinigung einer Menge von Objekten in Cluster. Hierbei geht es darum,
die Objekte die hohe Ähnlichkeit ihn ihren Eigenschaften besitzen zu vergleichen und
diese dann zu gruppieren. Andere Cluster mit ihren Objekten sollten wiederum keine
Ähnlichkeiten in ihren Eigenschaften zu anderen Cluster Objekten besitzen.
Abbildung 4: Menge von Objekten
Quelle: [6]
Wie in der Abbildung zu sehen, befinden sich dort Bereiche, wo sich die Daten
gruppieren. Durch diese Gruppierung kann man erkennen, dass bestimmte Daten in
irgendeiner Weise Ähnlichkeit zu anderen Daten aufweisen.1 Zwischen den
Gruppierungen ergeben sich viele Lücken. Diese Lücken müssen durch Kurven gefüllt
werden. Die hierfür am besten geeigneten Methoden sind die Z-Kurve und die HilbertKurve.
Für das Feld-zu-Partition Zuordnungsschema können verschiedene Verfahren
verwendet werden. Besonders eignen sich Round Robin und Hashfunktionen.
Um die Zusammenhänge besser zu verstehen, wird jede Methode kurz vorgestellt. Das
Verfahren Round Robin wurde bereits erläutert.
1
vgl. [6]
Seite | 8
Unter einer Hashfunktion oder zu Deutsch Streuwertfunktion versteht man eine
Funktion, die aus einer großen Eingabemenge eine kleinere Zielmenge als Ausgang
erzeugt. Diese Funktion verstreut die Daten anhand eines Hashcodes. Ein einfaches
Beispiel ist ein Adressbuch. Ein Name wird anhand seines ersten Buchstabens in das
Adressbuch eingeordnet. Das Alphabet wäre somit der Hashcode. Dadurch kann eine
große Anzahl an Namen zerkleinert werden.1 Beim Feld-zu-Partition
Zuordnungsschema werden nicht Namen, sondern Feldern in Partitionen eingeordnet.
Der Hashcode kann unterschiedlich lang sein. Desto länger er ist, umso kleiner wird
die Ausgabe. Im Folgenden werden 4-bit und16-bit lange Hashcodes verwendet.
Die Z-Kurve kann als raumfüllende Kurve beschrieben werden, die man auch für
mehrdimensionale Datenstrukturen verwenden kann. Ein Raumpunkt ergibt sich durch
bitweises Verschränken der Koordinatenwerte.
Abbildung 5: Beispiel für die Anwendung der Z-Kurve
Quelle: [3] Seite 4
Das letzte Verfahren ist die Hilbert-Kurve. Sie ist ebenfalls eine raumfüllende Kurve.
Ziel ist es, einem beliebigen Punkt auf einer quadratischen Fläche beliebig nahe zu
kommen und die Fläche vollständig auszufüllen. Erreicht wird dies durch
Wiederholung des Konstruktionsverfahren.3
Abbildung 6: Beispiele für die Hilbert-Kurve
Quelle: [9]
1
vgl. [7]
vgl. [8]
3
vgl. [9]
2
Seite | 9
Um die beste Kombination der Methoden für SPF zu finden wird ein Experiment
anhand von TIGER/-Line Feldern durchgeführt. TIGER/-Line ist ein Verfahren,
welches unter anderem zum Aufzeichnen von Straßen in Kalifornien verwendet wird.
(siehe auch Kapitel 5 „Beurteilung der Performance“). Durch das Ausführen des
Experiments werden die Methoden Round Robin, Hashing, Z-Kurve und HilbertKurve miteinander vergleichen.
Abbildung 7 zeigt den Aufwand von den aufgeführten Methoden abhängig vom
Faktor, der die Variationsmöglichkeiten festlegt. Eine perfekte Methode würde gleiche
Tupel einer Partition zuordnen und die Anzahl der Variation wäre 0.
Abbildung 7: Anzahl der Variationen
Quelle: [3] Seite 4
Aus Abbildung 7 können folgende Schlussfolgerungen gezogen werden:
Als Kodierungsmethode eignet sich am besten Z-Kurve und Hilbert Kurve, kombiniert
mit Round Robin für das Zuordnungsschema. Hier ist der Koeffizient der Variationen
am niedrigsten. Was allerdings auffällig ist, ist das sich die Performance von allen mit
zunehmender Anzahl der Felder verbessert.1
1
vgl. [3] Seite 4 u. 5
Seite | 10
Abbildung 8 zeigt den Replication Overhead bei Erhöhung der Anzahl der Felder.
Quelle: [3] Seite 5
Mit Zunahme der Feldnummern steigt der Replication Overhead. Am geringsten ist er
allerdings bei Round Robin.
Auf Grund dieses Experiments wird die Z-Kurve als Feldkodierungsmethode
kombiniert mit Round Robin als Feld-zu-Partition Zuordnungsschema für den SPF in
SJMR verwendet.
Die Vorgehensweise, wie SPF von SJMR im Detail arbeitet wird nun im Folgenden
zusammengefasst:1
1. Schritt: Universum in N T Felder aufteilen, wobei N T >>P
2. Schritt: Die Felder werden nummeriert von 0 bis N T – 1 mit Hilfe der Methode der ZKurve und werden einer Partition p (0 ≤ p ≤ P - 1) mit Round Robin zugeordnet.
3. Schritt: SPF überprüft für jedes Tupel das MBR, um alle Felder zu determinieren,
dessen MBRs sich überschneiden. Außerdem wird die Variable k2 als die Nummer der
Partitionen, zu denen das Feld gehört, gesetzt. Falls sich das MBR des Tupels mit
mehreren Partitionen überschneidet, wird k2 entsprechen oft generiert und befüllt.
4. Schritt: Als nächstes wird die Variable v2 befüllt. Für die Vermeidung von
Verdopplungen und für die Filterschritte vom Reduce stage werden allerding auch die
Feldinformationen von jedem Tupel benötigt. Somit muss die Feldinformation als ein
weiteres Attribut von jedem Tupel gespeichert werden. Somit besteht der Wert v2 von
jedem Tupel aus fünf gemeinsamen Attributen: OID, MBR, spatial property,
Informationen zur Datenquelle und die Feldinformation.
5. Schritt: Tupel von verschiedenen Datensätzen mit demselben Schlüssel k2, werden zur
selben Reduce Task zugeordnet. User definierte Logik kann die Quelle der Daten
bestimmen um die Herkunft festzustellen. Die Einträge von verschiedenen Quellen
können somit verbunden werden.
6.
1
vgl. [3] Seite 5
Seite | 11
3.3. Reduce stage
Der Reduce stage setzt sich, wie der Spatial Join selbst, aus dem Filter Step und dem
Refinement Step zusammen.
3.3.1. Filter Step
Das Ziel des Filter Step ist es, Tupel derselben Partition so zu paaren, dass sich ihr
MBR überschneidet. Dazu wird wie folgt beschreiben vorgegangen:1
1. Zuerst werden alle Werte von v2, die zur selben Partition k2 gehören,
eingelesen.
2. Die Keypointer Elemente werden für jeden Wert von v2 in einem von zwei
temporäre Relationen Rkp und Skp dem Arbeitsspeicher hinzugefügt. Die
anderen Informationen des Tupels werden in einem von zwei temporären
Relationen RT und ST auf die Festplatte gespeichert.
3. Nun wird nach MBRs in Rkp gesucht, welche sich mit MBRs in Skp kreuzen.
4. Gegeben sind zwei Rechtecke. Wenn beide in den Hauptspeicher passen,
dann kann ein effektiver sweeping Algorithmus verwendet werden um alle
Paare der überlappenden Rechtecke zu ermittelt. Kern eines Sweep im
zweidimensionalen ist die line sweeping (Sweep-Gerade) bzw. im
dreidimensionalen die plane sweeping (Sweep-Ebene). Durch sie wird der
Raum "ausgefegt". Man bewegt sie durch den gesamten Raum bis alle
Objekte des Problems besucht und verarbeitet wurden.2 Durch Map Stage ist
sichergestellt, dass Rkp und Skp in den Speicher passen, somit kann ein plane
sweeping Algorithmus angewendet werden um alle Paare von Keypointer
Elementen in Rkp und Skp zu finden, die überlappende MBRs haben. Für
diese „passenden“ Keypointer Elemente Paare, wird die OID-Information
extrahiert und als Output von diesem Schritt erfasst.
5. Um die Feldinformation der Tupels voll ausnutzen zu können, werden die
Tupel gesplittet. SJMR adoptiert eine neue streifenbasierte plane sweeping
Methode. In dieser Methode wird jede Partition in gleich große Streifen
geteilt. Die Streifen sind nebeneinander und parallel zur X-Achse. Dann wird
jeder Streifen mit dem plane sweeping Algorithmus gefiltert. Jedes Tupel
wird auf einem Steifen anhand seiner Feldinformation zugeordnet. Der KeyParameter vom streifenbasierten plane sweeping Algorithmus ist die
Streifennummer.
Eine Aufteilung des Universums in zwei Streifen könnte beim Beispiel zur ZKurve wie in Abbildung 5 bereits eingezeichnet aussehen.
1
2
vgl. [3] Seite 5
vgl. [11]
Seite | 12
Der Vollständigkeit halber folgt ein Beispiel, wie die streifenbasierte plane
sweeping Methode implementiert werden kann:
Abbildung 9: streifenbasierter plane sweeping Algorithmus
Quelle: [3] Seite 5
3.3.2. Refinement Step
Das Ergebnis des Filter Step sind zwei temporäre Relation, deren Tupel die Form
<OIDR, OIDs> haben. Die Relationen sagen aus, welches MBR von OID(R) sich mit
dem MBR von OID(S) überschneiden. Im Refinement Step muss nun geprüft werden,
in wieweit das Ergebnis die Join-Bedingungen erfüllt. Um eine chaotische Suche beim
Auslesen der Tupel R und S, die in RT und ST auf der Festplatte gespeichert sind, zu
vermeiden, wird eine weitere Strategie eingeführt:
Zuerst werden die OID-Paare nach OIDR als primärer Sortierungsschlüssel und OIDs
als zweiten sortiert. Anschließend werden so viele Tupeln von R wie möglich
zusammen mit den dazugehörigen <OIDR, OID S> Paaren in den Arbeitsspeicher
eingelesen. Der OIDR-Teil von dieser Reihung zeigt auf die R-Tupel im
Arbeitsspeicher. Dann wird die Reihung nach OIDs sortiert. Die S Tuples in ST auf der
Festplatte werden anschließend sequentiell in den Arbeitsspeicher eingelesen. Die
Spatial Eigenschaften von R und S Tupels werden im Memory geprüft und es wird
festgestellt, ob sie die Join-Bedingungen erfüllen.1
Um zu vermeiden, dass Duplikate im Ergebnis generiert werden, verwendet SJMR
eine feldbasierte Verdopplungsvermeidungstechnologie. Darauf wird im Punkt 4
„Vermeidung von Duplikationen“ noch genauer eingegangen.
1
vgl. [3] Seite 6
Seite | 13
3.4. Vor- und Nachteile
Bei SJMR können die Vorteile von MapReduce mit denen des Spatial Join kombiniert
werden. Der Spatial Join ist extrem effizient und kann mit der Hilfe von MapReduce
parallel auf Clustern ausgeführt werden. Dadurch verkürzen sich die Bearbeitungszeiten.
Des Weiteren können auf diese Weiße auch heterogene Daten miteinander kombiniert
werden. Allerdings ist diese Methode sehr kompliziert und komplex. Ein weiterer Nachteil
ist, dass Duplikate erzeugt werden. Hierzu muss sich überlegt werden, wie mit den
Duplikaten umgegangen wird. Mehr dazu im nächsten Kapitel.
4. Vermeidung von Duplikaten im Ergebnis
Es gibt zwei Möglichkeiten das Join-Ergebnis von zwei Tupeln TR und Ts mehrmals zu
erzeugen.
- Falls TR und TS im Map stage verschiedenen Partitionen zugeordnet werden,
können im Reduce Task Duplikate erzeugt werden.
oder
-
Beim Reduce Task, wenn TR und TS in mehrere Streifen repliziert werden, dann
werden ebenfalls Duplikate generiert.
Die Duplikate können durch zwei Methoden vom Ergebnis entfernen werden:
Elimination und Vermeidung von Duplikaten.
Bei der Eliminierung von Duplikaten wird zuerst das Ergebnis sortiert und anschließend
werden die Duplikate entfernt. Da dieser Schritt erst am Ende des Reduce stage ausgeführt
werden kann, führt dies zu einer Verlängerung der Bearbeitungszeit und somit auch zu einer
Erhöhung der Kosten. Somit ist es besser die Generierung von Duplikaten bei jeder Reduce
Task online zu vermeiden. Deshalb wird der Filter Step mit einem simplen Test erweitert, der
durchgeführt wird, wenn die Rechtecke auf Überschneidung geprüft werden.
Die Technik wird referenzierende Feldmethode genannt. Falls ein Paar von TR und TS in
mehrere Reduce Tasks repliziert wurden, werden sie nur in einem Task gejoint. Ausgehend
von der Feldinformation von jedem Tupel, berechnet die referenzierende Feldmethode das
kleinste gemeinsame Feld von zwei Tupeln. Das Ergebnispaar wird nur berichtet, wenn das
kleinste gemeinsame Feld innerhalb der aktuellen Partition und des aktuellen Streifen liegt.1
Beispiel siehe Abbildung 9: Das Universum ist in 16 Felder aufgeteilt. Das kleinste
gemeinsame Feld von TR und TS liegt in Feld 3. Somit werden TR und TS nur gejoint, wenn
sowohl der Reduce Task als auch der Streifen das Feld 3 beinhalten.
1
vgl. [3] Seite 6
Seite | 14
Abbildung 10: Referenzierende Feldmethode
Quelle: [3] Seite 6
5. Beurteilung der Performance
Zur Beurteilung der Performance wird ein Experiment durchgeführt. Dazu wird wieder HDFS
verwendet. Ausgeführt wird das Experiment auf einem DELL Power Edge Sc430 Server. Die
Ergebnisse werden auf einem ein Konten, 2 Knoten, 4 Knoten und 8 Knoten großem Cluster
erzielt.
Als Datenbeispiel werden wieder Datensätze aus dem bereits erwähnte TIGER/-Line
verwendet. Einer beinhaltet die Straßeninformationen von Kalifornien. Das andere beinhaltet
das Gewässernetz von Kalifornien.
Abbildung 11: Übersicht über die Daten
Quelle: [3] Seite 6
Es wird nun untersucht, wie sich eine Erhöhung der Streifen und der Knoten auf die
Performance auswirkt.
Als erstes wird der Einfluss der Streifenanzahl auf den streifenbasierten plane sweeping
Algorithmus ermittelt. Der Algorithmus wird auf einem Cluster mit nur einem Knoten
implementiert.
Seite | 15
Abbildung 12: Einfluss der Streifenanzahl
Quelle: [3] Seite 7
Man kann erkennen, dass die Anzahl der Streife definitiv Einfluss auf die Performance hat.
Desto höher die Anzahl der Streifen steigt, umso besser arbeitet der streifenbasierte plane
sweeping Algorithmus. Eine gute Performance wird erzielt, wenn die Streifenanzahl acht ist.
Daher verwendet der streifenbasierte plane sweeping Algorithmus von SJMR auch acht
Steifen.
Als nächstes wird der Einfluss der Knotenanzahl und der Anzahl der Reduce Task untersucht.
Abbildung 13: Einfluss der Knotenanzahl
Quelle: [3] Seite 7
Die Anzahl der Knoten und der Reduce Tasks stehen zueinander in Beziehung. Mit dem
Anstieg der Anzahl der Knoten verbessert sich die Performance deutlich. Außerdem
verbessert sich die Performance mit zunehmender Anzahl der Reduce Task, bei einer
bestimmten Anzahl von Knoten N. Allerdings muss gelten, dass die Anzahl der Reduce Tasks
Seite | 16
P kleiner oder gleich sein muss als 2N, d.h. (P≤2N). Aber wenn P>2N gilt, sinkt die
Performance mit Zunahme der Reduce Task Anzahl.1
Um die Performance von SJMR fundiert beurteilen zu können, wird SJMR auch noch mit
zwei verwandten Verfahren verglichen.
Das erste Verfahren zum Vergleichen ist Partition-Based Spatial-Merge kurz PPBSM. Die
Arbeitsweise ist ähnlich wie bei SJMR, nur die Duplikate werden nach dem Spatial Join
eliminiert. Die Performance von SJMR und PPBSM verbessert sich mit der Zunahme der
Reduce Tasks P, falls P≤2N. Aber wenn P>2N verschlechtern sich beide. Aus der Abbildung
14 kann man ablesen, dass die Performance von SJMR grundsätzlich um 20 Sekunden besser
ist. Das ist darauf zurückzuführen, dass das Eliminieren von Duplikaten ca. 20 Sekunden in
Anspruch nimmt.
Abbildung 14: Vergleich der Performance von SJMR und PPBSM (8 nodes/Knoten)
Quelle: [3] Seite 7
Das zweite Verfahren nennt sich SJMR-Large Mem.
Beim Reduce stage von SJMR werden die spatial property von jedem Tupel in lokale Dateien
TR und TS auf die Festplatte kopiert. Um die Schreibzugriffe auf die Festplatte zu reduzieren
wird der Arbeitsspeicher (Memory) groß genug gewählt. SJMR-LargeMem versucht dies
noch zu verbessern, indem er das Schreiben der spatial property auf die Festplatte von Anfang
an unterbindet. Abbildung 15 stellt den Vergleich bei einer Reduce Task Anzahl von 8 dar.
Man kann erkennen, das SJMR besser arbeitet als SJMR-Large Mem. Das liegt vor allem
daran, dass SJMR-Large Mem Konvertierungen durchführen muss. Es muss die spatial
property von jedem Tupel von einem String zu einen spatial object konvertieren, während
SJMR die spatial propertys einfach nur ausließt. Somit bring das Reduzieren der
Schreiboperationen nicht den gewünschten Performancegewinn bei SJMR-Large Mem.
1
vgl. [3] Seite 7
Seite | 17
Mit Zunahme der Knoten verbessert sich allerdings bei beiden die Performance, da mehr
Reduce Tasks gleichzeitig ausgeführt werden können.
Abbildung 15: Vergleich der Performance von SJMR und SJMR-LargeMem bei einer Anzahl
der Reduce Tasks von 8
Quelle: [3] Seite8
6. Folgerungen
In der vorliegenden Ausarbeitung wurde erklärt, wie Spatial Join effektiv implementiert und
mit MapReduce auf Clustern beschleunigt werden kann. Der entwickelte Algorithmus wird
SJMR (Spatial Join with MapReduce) genannt. Soweit bekannt ist, ist SJMR der erste
parallele Spatial Join Algorithmus für MapReduce, der es Spatial Joins erlaubt MapReducePlattformen in Clustern zu verwenden.
Die Strategie von SJMR kann auch bei anderen parallelen Umgebungen eingesetzt werden,
besonders wenn keiner der Inputdatensätze einen Spatial Index hat.
In Performancetests wurde auch die Realisierbarkeit und die Effizienz von SJMR
nachgewiesen. Es hat sich herausgestellt, dass MapReduce auch in rechenintensiven SpatialApplikationen und kleinen skalierten Clustern anwendbar ist.
Seite | 18
I.
Quellen
[1] http://www-users.cs.umn.edu/~npramod/biplob_enc.doc
(zuletzt eingesehen am 15.06.2011)
[2] http://v.hdm-stuttgart.de/~riekert/vortraege/01gisnet/tsld036.htm
(zuletzt eingesehen am 15.06.2011)
[3] SJMR: Parallelizing Spatial Join with Map Reduce on Clusters, Shubin Zhang, Jizhong
Han, Zhiyong Liu, Kai Wang
[4] http://www.heise.de/developer/artikel/Hadoop-Distributed-File-System-964808.html
(zuletzt eingesehen am 15.06.2011)
[5] Spatial Join Techniques, Edwin H. Jacox and Hanan Samet, Computer Science
Department, Center for Automation Research, Institute for Advanced Computer Studies,
University of Maryland, College Park, Maryland 20742
[6] http://wikis.gm.fh-koeln.de/wiki_db/Datenbanken/Spatial-Clustering
(zuletzt eingesehen am 15.06.2011)
[7] http://de.wikipedia.org/wiki/Hashfunktion
(zuletzt eingesehen am 15.06.2011)
[8] http://de.wikipedia.org/wiki/Z-Kurve
(zuletzt eingesehen am 15.06.2011)
[9] http://de.wikipedia.org/wiki/Hilbert-Kurve
(zuletzt eingesehen am 15.06.2011)
[10] http://de.wikipedia.org/wiki/Round_Robin_(Informatik)
(zuletzt eingesehen am 15.06.2011)
[11] http://de.wikipedia.org/wiki/Sweep_(Informatik)
(zuletzt eingesehen am 15.06.2011)
Seite | 19
II.
Abbildungsverzeichnis
Abbildung 1: Beispiel für einen Spatial Join…………………………...……………Seite 4
Abbildung 2: Beispiel für ein minimal umgebendes Rechteck……………..………..Seite 4
Abbildung 3: Verteilung des Daten………………………………………….………Seite 7
Abbildung 4: Menge von Objekten………………………………………….….……Seite 8
Abbildung 5: Beispiel für die Anwendung der Z-Kurve……………………...……..Seite 9
Abbildung 6: Beispiele für die Hilbert-Kurve……………………………….………Seite 9
Abbildung 7: Anzahl der Variationen……………………………………….….......Seite 10
Abbildung 8: Replication Overhead bei Erhöhung der Anzahl der Felder…………Seite 11
Abbildung 9: streifenbasierter plane sweeping Algorithmus……………………….Seite 13
Abbildung 10: Referenzierende Feldmethode…………………………………..….Seite 15
Abbildung 11: Übersicht über die Daten……………………………………..…….Seite 15
Abbildung 12: Einfluss der Streifenanzahl……………...…………………..……...Seite 16
Abbildung 13: Einfluss der Knotenanzahl…………………………………..……...Seite 16
Abbildung 14: Vergleich der Performance von SJMR und PPBSM……………….Seite 17
Abbildung 15: Vergleich der Performance von SJMR und SJMR-LargeMem bei einer
Anzahl der Reduce Tasks von 8……………………………………Seite 18
Seite | 20
Seite | 21
FernUniversität in Hagen
Seminar 01912
Summer semester 2011
“Map/Reduce and Databases”
Topic 14
Map/Reduce for multi-core machines
Speaker: Frank Thiele
Table of Contents
1 Introduction to Map/Reduce on SMP systems.......................................................................................2
1.1 The programming model Map/Reduce...........................................................................................2
1.2 Phoenix 2 - Map/Reduce using shared-memory............................................................................4
2 Applied optimizations for shared-memory based Map/Reduce.............................................................7
2.1 Performance related properties of the input data............................................................................7
2.2 Optimizations implemented in Phoenix 2......................................................................................8
2.3 Parameter tuning in Phoenix 2.......................................................................................................9
2.4 Ostrich..........................................................................................................................................10
2.4.1 Tiled-Map/Reduce................................................................................................................10
2.4.2 Design related performance improvements..........................................................................11
2.4.3 Implementation related performance improvements............................................................12
2.4.4 Fault tolerance......................................................................................................................13
2.4.5 Test results for Ostrich..........................................................................................................13
2.5 Metis.............................................................................................................................................14
2.6 Phoenix++....................................................................................................................................15
2.7 Additional optimizations..............................................................................................................17
2.7.1 IO Overlapping.....................................................................................................................17
2.7.2 Extending the Pipelining concept of Ostrich........................................................................18
2.7.3 Optimizing the Final-merge phase........................................................................................18
3 Combination of cluster and shared-memory based Map/Reduce........................................................19
3.1 Hadoop and Multi-core systems...................................................................................................19
3.2 Optimization of Apache Hadoop using Phoenix..........................................................................19
4 Summary and Conclusions...................................................................................................................20
References................................................................................................................................................21
1
1 Introduction to Map/Reduce on SMP systems
This article is about the optimization of symmetric multi-processing (SMP) and chip multi-processing
(CMP) based Map/Reduce framework implementations. The 1st chapter will give a short introduction to
the algorithm of Map/Reduce and introduces Phoenix 2 as the example to explain some optimizations
with. Those optimizations are explained in more detail in the first sub chapters of chapter 2. Chapter
2.4, 2.5 and 2.6 describe some more sophisticated frameworks which were released after Phoenix 2.
Chapter 2.7 describes some additional ideas to generate more speedup. The 3rd chapter shows up some
ideas of how to integrate the high performance SMP Map/Reduce frameworks with the cluster based
framework Apache Hadoop. The last chapter summarizes this article.
1.1 The programming model Map/Reduce
The idea behind the parallel Map/Reduce programming model is to support a lightweight application
design easing the efficient use of the emerging multi-core and many-core systems such as CMP/SMP
machines and cloud-based clusters.
The reason for the simplicity is the small interface given by the Map/Reduce model which enables a
framework to encapsulate all parallel activities like socket communication and thread synchronization.
The two basic functions of Map/Reduce are map() and reduce().
The map functions normally expects a single parameter called key. The output of the map function is a
pair (or a vector) which represents the transformation of the key to a <key, value> pair. The value can
contain any simple data type, but also more sophisticated data structures such as trees or hash maps.
The keys are retrieved from the input of the problem domain and passed to the map() function. The
processing of the input is designed to be done in parallel as the programming model assumes the input
data to be independent of each other. This is mostly achieved by splitting up the input into several splits
and processing each split in parallel by a separate mapping process called the Mapper. The phase of
applying the map() function is called Map phase.
The output of each Mapper is usually stored into several partitions to support the parallelization of the
reduce() function. The assignment of a key to a partition in the Map phase is done by applying a
function on the key, e.g. a hash. Each key of a split is so put into a set of r partitions containing the
<key, value> pairs generated by the map() function. In the Merge phase those sets are merged by the
Map/Reduce framework into a single set of r partitions which is the input for the Reduce phase. Equal
keys (defined by the user) are now often grouped to a <key, values> pair within this phase. This
merging is based on the fact that each Mapper has used the same partitioning schema so that a certain
key can always be found in the same partition created by any of the various Mappers (if it exists). After
the Merge phase (and of course after the Map phase, too) each partition can contain several values for
the the same key which needs to be aggregated now.
In the Reduce phase it is the reduce() function which aggregates the set of values of a key into a single
value. The reduce() function reduces a pair of type <key, set of values> into a pair of type <key, value>.
Of course, it is also allowed to generate more than a single record or no record (which applies for the
map() function, too). The reduce functions are working on r partitions, for each partition created in the
Merge phase there is a single Reducer processing its content. The output generated by the Reducers is
split up into r parts which needs to be put together by the framework.
As the moving of data from one node to another (e.g. from the Mapper to the Reducer) is one of the
2
most important cost factors, an optional Combine phase can be included which is executed between the
Map and Reduce phase. By applying a function called combine() by the Combiner (which in most
cases is equal to the reduce() function) the local amount of data to copy over the network to another
machine can be decreased significantly. If the available memory to a CMP/SMP is limited, this method
can also be used to reduce the amount of required memory. The usability of the Combine phase
depends on the application: If the reduce() function does not require the exact count and order of the
values for a key, the combine() method can be invoked.
To summarize this: Each Map/Reduce model implementation consists of 3 (or 4) phases which are
executed sequentially: Map phase → Combine phase (optional) → Merge phase → Reduce phase.
If one can apply the application based problem into an algorithm using those two or three little
methods, he can make use of an easy to implement way of parallel processing, either on clusters or on a
single CMP/SMP host.
Illustration 1: Basic Map/Reduce design principle
3
1.2 Phoenix 2 - Map/Reduce using shared-memory
The maybe most popular implementation of the Map/Reduce model based on shared-memory system is
Phoenix. The most frequently used version of the framework is 2.0.0 and was released in May of 2009.
It is written in C++ and uses the PThread library. This enables an application using Phoenix to spawn
several threads instead of processes to let the Mappers and Reducers communicate via the process
internal memory space which results in the lowest overhead possible. The framework supports the
Map/Reduce model for all type of data structures where a value can be mapped to a key which is of
type “Comparable”.
The difference compared to a normal Map/Reduce implementation designed for cluster computation is
that the communication between the Mappers and Reducers is done using shared-memory (the OS
system shared-memory model is not meant here). This enables the framework to do a lot of
optimizations and grouping of communication related data structures. In Phoenix, the main data
structures are the Intermediate buffer and the Final buffer. The Intermediate buffer is filled by the
Mapper threads and read by the Reducer (and Combiner threads). The Final buffer is filled by the
Reducer threads.
As an optimization, the framework allocates a configurable number of threads before starting
processing. Those are summarized in the Thread pool and can be used to process data. The data set a
thread can processes is called a Task. Phoenix differentiates 3 types of tasks, Map tasks, Reduce tasks
and Merge tasks.
The processing and the phases are easily comparable to the general Map/Reduce model. The processing
is sequential as like as in the original model, meaning that before a phase has not finished, the next one
cannot start. The order of the Map, Merge and Reduce phase is also equal. Because the Map and
Combine phase both work on the Intermediate buffer, they are summarized in the illustration 2 as a
single phase. The Merge phase in between the Map and Reduce phase is done by the framework within
the Reduce phase and therefore not explicitly mentioned in the illustration. The little difference
between the generic model and Phoenix is the second Merge phase at the end of the processing which
is used for sorting the output by key. This one does not exist in the generic model and therefore I named
it Final-merge phase.
To get an idea of the design of Phoenix the illustration 2 demonstrates the flow of data and the usage of
multi-threading. It shows an input data structure that is divided into 4 splits which form the Mapper
tasks. Those Mapper tasks are run in parallel by the threads of the Thread pool. There is a so called
Dispatcher thread that is responsible for allocating the tasks of the current phase and assigning them the
the worker threads. The results (pairs of <key, values>, that means that equal keys are grouped together
in Phoenix 2) for each Map task are stored in a “row” of the intermediate buffer and partitioned into a
fixed number if “columns” which is based on the number of Reduce tasks defined by the user or
framework. Then follows the optional Combine phase in which each bucket of the Intermediate buffer
is reduced based on the user-defined combine() function. The next step is to execute the Reducers in
the Reduce phase for each Reduce task in parallel. Such a task corresponds to the data stored in a
column of the Intermediate buffer. As already mentioned, the Merging of the different Mapper outputs
is done within this phase by merging the already sorted buckets together (as like as in the normal
Merge-sort algorithm). The output of the Reduce phase is put into the Final buffer which keeps
<number of Reduce tasks> sorted results (pairs of <key, value>). In the Final-merge phase, those sorted
results are then Merge-sorted into the Result buffer which can be used by the application/user.
4
Illustration 2: The Phoenix 2 design (based on a figure in [OSTRICH])
The interface to the Phoenix framework consists of a set of functions and data structures accepted and
returned by those functions.
The important functions of the interface are:
–
int map_reduce_init() and int map_reduce_finalize()
–
int map_reduce(map_reduce_args_t * args)
–
This one is used to start the Map/Reduce job which is further defined by the data structure
pointer given as the only argument.
–
The following points are a part of the supported interface options defined by the argument:
–
Pointer and length of the memory location where the data is to be read from
–
Splitter function pointer, to define an application-specific splitting of the input data
–
Mapper function pointer
–
Combiner function pointer, optional
5
–
Reducer function pointer, optional
–
Comparator function pointer, to define application-specific key comparing
–
Partitioner function pointer, to define application-specific partitioning of the keys (used
in the Map phase)
–
Result set pointer, used to hand over the pointer to the actual data to the user
–
Flag configuring the amount of Map rows stored in the Intermediate buffer
–
Flag configuring the amount of Reduce task results stored in the Final buffer
void emit_intermediate(void *key, void *val, int key_size)
–
–
–
This function is used by the user-defined map() function to emit (intermediate) output pairs.
void emit(void *key, void *val);
–
This function is used by the user-defined reduce() function to emit (final) output pairs.
6
2 Applied optimizations for shared-memory based Map/Reduce
2.1 Performance related properties of the input data
According to the [PHOENIX++] paper, there are several types of input data that can occur in real-life
applications and for each of them a different set of optimizations is required.
They categorize the input by using 3 dimensions:
Dimension 1
The amount of keys and its distribution emitted by a Map task are described with this dimension.
There are 3 sub-types:
*:*
Each Mapper emits an unknown number of keys, the distribution e.g. Word count
of the keys is unpredictable.
*:k
Each Mapper emits up to k different keys, but the amount of keys e.g. Histogram, String match
is not known.
1:1
Each Mapper creates a single output key.
e.g. Matrix multiply
Word count is a good example for the *:* distribution type. For this one, Phoenix 2 has been optimized.
This can be seen in the Intermediate buffer implementation. It uses a fixed hash partitioning which fits
pretty good for normal distributions (if the buckets do net get too big and there is no key that occurs
more often than others). But for the other two types, a much smaller, easier to use and more efficient
data structure should be used, e.g. a simple and maybe sorted array or a B-tree.
Dimension 2
This dimension describes the number of emitted values per key. There are two general sub-types, one
or a few values per key and a lot of values per key (e.g. Word count). The latter one is best optimized
by using Combiners as those reduce the amount of required memory. A typical property of this
dimension is that is scales with the size of the input – if more input data is processed, then more values
for the same key are generated.
As Phoenix 2 was optimized for Word count, it enables the user to insert a Combine phase between the
Map and Reduce phase. A Combiner task operates on a bucket of the Intermediate buffer.
This dimension becomes more and more important if you take into account, that one could store the
<key, value> pairs separately. As Phoenix 2 groups equal keys together (in the buckets), this is also
some kind of optimization to reduce the amount of required memory.
Dimension 3
The per task computation describes the share of computation time for the custom code (e.g. the map()
function) compared to the framework code.
Applications like Histogram spend pretty less time in the custom code (low task computation) an so the
7
library performance is more affecting the overall performance compared to a typical “high task
computation” application like Matrix multiply or even Word count.
2.2 Optimizations implemented in Phoenix 2
From the algorithmic and implementation point of view Phoenix 2 has been steadily optimized until its
stable release 2.0.0. The following listing shall summarize the main points grouped by optimization
type which I found during studying the source code.
Implementation optimizations
(PIO1) The keys are grouped together in the Intermediate buffer (<key; values> pairs). This
saves memory and reduces the Cache pressure.
• (PIO2) Instead of copying the whole object (a key or a value) only pointers are used. This
enables fix-length data structure implementations, reduces the Memory and Cache pressure and
increases Data locality. The latter one is achieved because the pointers are often moved within
the bucket which benefits from the less amount of memory space required fitting now better
into the limited Caches.
• (PIO3) The buckets of the Intermediate buffer and Final buffer are pre-allocated using the
default value (10 keys). If the bucket gets full, its size is doubled. This reduces the amount of
concurrent memory allocations which may be a bottleneck in a multi-threading environment
(see [OSTRICH]).
• (PIO4) The Map tasks (input data splits) and Reduce tasks are tried to be created to fit into the
first-level Cache to increase the Data locality of the Mappers and Reducers. [Due to an
implementation defect this does work only for the Map task allocation, see function env_init()
of map_reduce.c]
• (PIO5) A typical C data structure optimization is applied, too: Cache-line alignment.
• (PIO6) A worker is bound to a CPU/Core. In case of CMP, this enables a thread to always work
on the same CPU and Core which again enables it to always use the same Cache. This reduces
first the duplication of Cache-lines which decreases the Cache pressure and second it speeds up
the thread as it is not waiting for the Cache to be filled again with the working set of its current
tasks.
Algorithmic optimizations
• (PAO1) The buckets of the Intermediate and Final buffer storing either <key; values> or <key;
value> pairs are implemented using a Binary Search based array insert method (before it was a
linear search!).
• (PAO2) The task concept is used to limit the unfairness between the worker threads (Mappers,
Reducers and Mergers). If there were no tasks, then the input would be split up into #workers
and the partitions into #workers. This tends to be unfair as the input as well as the partitions
may have a bad distribution of keys. Also, due to scheduling issues, page faults and so on the
workers are never equally fast. Because Phoenix 2 uses the strict phase concept of Map/Reduce
(no phase begins before the previous one has completely finished), each phase will be as slow
as the slowest worker. The impact of this limitation is reduced by using smaller jobs which
Phoenix refers to as tasks.
• (PAO3) The Intermediate and Final buffer are created in a way preventing the requirement of
locks between the different workers of phase. This enables faster processing as the waiting time
for RW accesses to the buffer is eliminated and the OS is not involved. The partitioning of the
•
8
•
•
keys is one example which is further explained in chapter 2.5.
(PAO4) Optionally, the user can decide to use the option single_output_per_thread instead of
single_output_per_task (for the Map and Reduce phase).
This enables the reduction of the internal bucket fragmentation as less buckets are used. But I
think that this can reduce the Data locality as the bucket will become pretty big (factor: #Tasks /
#Threads), especially if there are only a few values per key.
As this is impacted by PIO3, too, the advantage or disadvantage depends on the input
(dimensions 1 and 2) and needs to be tested by the user.
(PAO5) There is no lacy master thread dispatching the work only. Even this main thread
consumes tasks for processing after having dispatched the work of a phase to the “normal”
worker threads.
2.3 Parameter tuning in Phoenix 2
There is tuning notes document attached to the framework tar ball which gives several hints regarding
optimizations which are based on application, library and OS points.
For the application, it suggests to increase the L1 Cache size parameter which directly affects the Map
task count (and ideally the Reduce task count, too, see PIO4). This one replaces the automatically
applied L1 Cache optimization mentioned in PIO4. By manually finding the best size for the
application this optimization can be applied.
The input data can be read using the mmap or read system call. The document tries to explain a scaling
issue in case of using mmap with too many threads and suggests to try to replace the mmap with a
simple read system call. What it does not mention is the disadvantage of this version: The increased
memory usage of the read system call which is caused by the OS buffer pages and File cache pages as
well as the application pages. So the user needs to find the optimal way by sacrificing free memory for
enhanced scalability.
With respect to library optimizations the document reports a problem with the Intermediate buffer. If
the number of partitions is too small, then the number of reallocations grows significantly and reduces
performance (especially in case of multi-threading, see PIO3). Too large values on the other hand lead
to “empty” partitions which generate processing overhead in the Reduce phase. The user needs to tune
the parameter default_num_reduce_tasks / extended_num_reduce_tasks (related to
single_output_per_thread vs. single_output_per_task, see PAO4). This directly refers to the dimension
1 of the input data types.
Another library optimization the user can apply is to change the scheduling policies which is not further
explained here.
The more interesting “library optimization” is the usability of the combine() function which needs to be
turned on by a flag which requires recompilation! This looks like another defect in the framework as
the user is already able to configure the usage of combine() by using the C interface.
The operating system tuning comments are about the adaptation of the page size and the switch of the
memory allocation library. The former one is said to be of less impact than the application-specific
optimization of the L1 Cache size parameter. So because of that and the fact that you are “hacking”
your OS which likely creates “unwanted” side-affects I cannot re-recommend this optimization.
The latter one is said to be critical as the Map phase puts heavy pressure on the allocator. With focus on
concurrent memory allocations, the resulting scaling problem is also mentioned in [OSTRICH]. So by
changing the memory allocator the user can experiment with the best allocator for his environment.
9
2.4 Ostrich
2.4.1
Tiled-Map/Reduce
The Tiled-Map/Reduce functionality is described in the [OSTRICH] paper. It introduces the application
of an Compiler optimization technique known as Blocking to the Map/Reduce model. This shall
increase Data locality and reduce the memory requirements by processing the input splits fully and
merging the final results together. This is best explained with the following graphic that is based on the
figures of the original paper. This design was implemented in Ostrich which is based on Phoenix 2.
Illustration 3: Tiled-Map/Reduce - The design of Ostrich (based on a figure in [OSTRICH])
The difference to the design of Phoenix 2 is the Iteration buffer and the usage of so called Iterations.
The splits of the input done in Phoenix 2 are now grouped into Iterations. In theory, this requires the
input not to be completely in memory – only the current Iteration is needed to work on it. Those
iteration groups are processed in the Map and Combine phase. All rows of a partition (represented by a
column) are in contrast to Phoenix 2 reduced in the Combine phase. The reduced results of an Iteration
are then stored in the Iteration buffer.
10
After having finished the multiple Map and Combine phases, the Reduce phase is invoked. There, the
different Iteration results are reduced (and internally merged, too) into the already explained Final
buffer. The rest of the processing is now equal to Phoenix 2.
The big problem of Ostrich is its “licensing model”. Neither the code nor the compiled library were
published by the Chinese authors.
2.4.2
Design related performance improvements
Reducing the required memory size
The performance of Ostrich over Phoenix 2 is increased by several changes. With respect to design the
major point is the decreased memory requirement. There are three reasons why Ostrich requires less
memory than Phoenix.
The first is that Ostrich does not require the input file be be completely in memory all the time as the
keys are copied. This seems to be configurable but is not explained directly. To configure this sounds
reasonable as there is always a trade between the memory copy costs (time for copying and allocating
and the required space) and the costs of the higher memory amount required (time with respect to
allocate it and paging/swapping as well as required space (which is also limited by the size of the
virtual memory!)) to cache the complete file.
The memory copying is done in the Combine phase and becomes more efficient with an increasing
share of duplicated keys between the different input splits because the required memory in the Iteration
buffer decreases. Furthermore, the access to “copied together” keys tends to be faster as the keys in the
input are more spread around and not compressed together which reduces the Data locality when those
keys are accessed. The implementation of this is not mentioned in the paper but a good idea to do this
is the following: The unique input keys are copied into a (maybe cache-aligned) length-value buffer (to
not negatively impact the Iteration buffer implementation which can store fixed-length pointers only if
a separate buffer is used) which exists for each Iteration and each partition of it (to be lock-free usable
by Combiners and Compressors). Those buffers are allocated based on the size of the previous Iteration
to reduce the allocation time. Within the Compress phase (and maybe the Reduce phase) those buffers
are then “merged” or re-created as well as the “normal” buffers.
If there are applications that use the value as a pointer into the input, too, than the value needs to be
copied as well. This of course requires a lot more memory, especially if the value object is pretty big. In
such cases the next optimization is even more a valuable improvement.
The second reason for efficient memory usage is that the results of the Map phase are earlier reduced
by exploiting the Combine phase. All Map task outputs belonging to a certain partition are reduced into
a single bucket of the Iteration buffer representing this partition. Before, the Combiners only reduced
buckets and they were called only once after the complete Map phase has finished.
In addition to better exploit the Combine phase, the Iteration buffer is pre-reduced within the new
Compress phase by applying the reduce() function before the Reduce phase formally begins. I think
that instead of using the reduce() function here, the combine() function is also functionally correct. The
only difference is the use case: According to the “hints” of the authors, the combine() function is
invoked on the Intermediate buffer, the reduce() function in contrast to that to the Iteration buffer. So
by differentiating the user-supplied functionality from the framework functionality, the combine()
function can replace the reduce() function.
11
The Compress phase is triggered by the framework transparently to the user (e.g. when a threshold is
reached). Both, the Compress and the Combine phase require the application provided reduce() and
combine() function to be independent of the number and order of the values. This may be a reason not
to use Tiling for some applications, but for most this will not matter. As a rule of thumb one can say
that whenever you can use a combine() function in a cluster based Map/Reduce environment, this can
be used in a shared-memory environment, too.
Pipelining
What is also pushing up the performance is the Pipelining concept of Ostrich. The Map/Reduce model
is based on a strict phase concept. If for example a Mapper has not finished all its tasks, the Reduce
phase cannot be started. As the tasks in Phoenix 2 are retrieved from a global task buffer created by the
Dispatcher, the unbalance is restricted more or less to the very last tasks. The Tiled-Map/Reduce model
uses several iterations of the Map and Combine phase. But as the amount of tasks the worker threads
are working on has been decreased (Iterations group together less tasks than before) and the step from
the Map and Combine phase is invoked several times, the unbalance between the splits/tasks may sum
up to an even bigger problem than without using iterations.
Ostrich does not solve this problem completely. But Ostrich tries to handle the unbalance between the
Combiners (which exists for similar reasons) by allocating two Intermediate buffers. They are used in a
round-robin fashion. If one Reducer thread has no tasks left, it is going back to the Map phase and
becomes a Mapper of the next Iteration which uses the other Intermediate buffer. After the last Reducer
of the current Iteration has finished, he is the last one to switch over/back the the Map phase of the next
Iteration.
2.4.3
Implementation related performance improvements
General topics
Regarding implementation, Ostrich has done also some steps forward. One thing is the reduction of the
memory allocations and deallocations which is achieved by reusing the Intermediate buffer. Between
two iterations, the data structure is only reset by (more or less) setting its length parameters to zero.
Another optimization is the dynamically adaptation of the task size of a Mapper process which is only
indicated in the paper. My interpretation of this is the following: The Mappers process the first Iteration
using a default or estimated task size (equals the input split size). After this Iteration has finished, the
memory requirement is retrieved and based on that the optimized task size is calculated for the next
Iterations. This should be equal to the first-level Cache size (see Phoenix optimization PIO4) but also
depends on the relation between task count per Iteration (n times the number of first-level Cache) and
the size of the output of the Combine phase (the Iteration buffer requires space in the Cache lines, too).
Also the Intermediate data structure requires some Cache lines and therefore can reduce the task size or
count per Iteration, too.
CMP optimizations
There is also a way for Ostrich to improve the scaling on CMP systems, too. The problem there is the
duplication of Cache lines reducing the Cache efficiency and performance as well as the non-uniform
memory access (NUMA) caused by the architecture. The authors suggestion is to further group the
Iterations. Each Iteration group is now processed from the Map phase up to the Reduce phase on a
static CPU (with many cores). To balance the Jobs for each CPU, a work steal algorithm is applied.
Effectively, the complete Map/Reduce model (map()->combine()->merge()->reduce()) is applied to
12
each Iteration group. This enables each CPU to work more autonomic which in general should prevent
access to memory of different CPUs or Cache lines. To reach this without a central Dispatcher being
likely the bottleneck in a highly scalable environment there is a Dispatcher thread (being a worker, too)
on each CPU which starts the phases for the worker threads of each CPU. What is not fully explained
but indicated in a figure is the Reduce phase. My interpretation of this is that the Reduce phase is split
into two parts. Each CPU reduces there own Iteration buffer into an intermediate Final buffer. Then
those intermediate Final buffers are merged and reduced into the (full) Final buffer by Reducers which
work on data attached to different CPUs. Instead of splitting the Reduce phase, the Compress phase
could also be used to create the intermediate Final buffers.
As the data exchanged here has already been minimized, the costs are less then processing the data
without taking the dependency between memory, Cache and tasks into account.
2.4.4
Fault tolerance
Ostrich also introduces the ability to store intermediate results which can be restored in the case of an
error. As most of the jobs do not take longer than a couple of seconds, this is not really required for
typical applications. But as Ostrich can process files larger then the virtual memory and because of
applications with a very high computation per task value (dimension 3), this option can be useful.
2.4.5
Test results for Ostrich
The following benchmark results are taken over from the paper and are based on the original Phoenix 2
benchmarks.
Performance impact of the input size
For the Word count example used in the paper, the speedup is increasing with the size of the input file.
For a 1GiB file, the factor is 2.75, for a 4GiB file it is 3.25. The impact on the distributed sort example
is comparable, but the speedup is not that high (2 to 2.25).
For applications like Log statistics and Inverted index, the possible performance improvement of up to
50% is still significant, but much lower. This seems to be caused by the different required memory
space in the Intermediate buffer. The more space is required there, the higher the speedup is.
Memory space required
The required memory space is reported to be around 25% compared to the Phoenix 2 library. For
applications like Log statistics it is much less, but for Distributed sort it is higher (about 70%).
Performance impact of the core count
The test setup is based on a SMP machine with up to 16 cores. Throughout all applications, the
scalability is improved. The highest speedup was reached for Distributed sort, the smallest for Inverted
index.
13
2.5 Metis
Metis is as like as Phoenix and Ostrich a Map/Reduce framework for multi-core machines. Metis is
based on the design of Phoenix 2 but adapts the data structures and some other implementation details
to improve the performance.
Hash and B+Tree
First, the Metis library optimizes the Intermediate buffer. This one is according to the authors of Metis
(and Phoenix++) the main bottleneck of a multi-core using Map/Reduce implementation. This is
because of the different input data types, the varying formats the Map and Reduce phases require and
the potential of locking between different workers. As Phoenix 2 has addressed this with a fast, lockfree and easy to implement Matrix data structure for the buckets to store the key-value pairs in, there
was nothing to optimize in the transformation principle itself.
But the problem of Phoenix 2 is the data structure of the bucket itself. There, a simple sorted array is
used where the insert (and lookup) is boosted with a Binary search. But in the average case the insert
itself requires half of the bucket to be moved in memory.
Metis addresses this with a B+Tree within each bucket. This data structure can be accessed in a sorted
order (as like as in Phoenix 2) which is required by the Reducers to Merge-sort the different columns of
the Intermediate buffer representing a partition. But what is more important is the lower complexity:
•
Phoenix insert costs into a bucket:
O(log n) + O(n) [Binary search + memory copying]
•
Metis insert costs into a bucket:
O(log n) + O(1) [Tree search & adapt + memory copying]
What has not been mentioned in the Metis paper is the higher memory usage of a B+Tree which results
in worse Data locality. But as the copy operation of an insert is required only in the leafs instead of the
complete bucket and because of the little amount of B+containers to write into (log n), the negative
influence of less Data locality is smaller than the advantages of the B+Tree access.
The paper does not explain that only in the case of an odd key distribution, the B+Tree is advantageous
over the sorted array of Phoenix 2. This is because by using a higher partition count, the advantage of
the B+Tree in case of an even distribution is gone because the buckets can all be equally filled with the
same amount of keys that fit into a leaf of the B+Tree (and so the copying costs are equal).
As already indicated, static hashing is used in both libraries as an extension to the B+Tree/sorted array.
The key is hashed into one of several partitions which on the one hand enables the lock-free scaling of
the Reducer phase. On the other hand allows hashing to reduce the Insert costs because instead of using
a single B+Tree (or a sorted array) several buckets are used which reduces the amount of keys per Tree
or array. The Metis paper states that by using a well sized partitioning schema to hash the keys into
buckets, the usage of a B+Tree does not really matter as the complexity is nearly constant (which must
apply for the same reason for Phoenix, too). The usage of static hashing in Metis makes Metis optimal
for *:* key distributions, but sub-optimal for others (according to the thoughts of [PHOENIX++]).
Metis compares this also against a so called append-only buffer. This one simply appends the keys and
sorts them at the end of the Map phase. This data structure is better suited for key distributions with
less repeated keys (dimension 2 of the input data types as only a few values for a key are expected) as
the final sort depends on the number of keys and the size of the Intermediate buffer is not affected too
much. The sorting part can benefit from better Data locality within the bucket compared to the B+Tree.
But with an increasing share of repeated keys, the advantage of this structure becomes a disadvantage
14
as the B+Tree and the Phoenix 2 sorted array are both grouping together keys which reduces effectively
the sorting and inserting complexity.
As the optimal prediction of the number of keys and its repetition share is often not possible, the Hash
and B+Tree solution fitting good but not perfect for all situations is a sensible one. This has been
proved in the performance related tests of Metis.
Parallel Sorting by Regular Sampling
The Final-merge phase in Phoenix 2 sorts the keys and puts them into the output. The problem there is
the Merge-sort algorithm which does not use all of the worker threads. Between each phase of the
Merge-sort process, the number of workers is divided by two!
Metis addresses this by using the “Parallel Sorting by Regular Sampling” algorithm which scales better
because it uses all cores during the sorting process.
Memory allocator
As like as documented in the [OSTRICH] paper, the Metis authors found the memory allocator to be a
bottleneck in a multi-threaded environment. The authors have tested some alternative allocators and
found with Streamflow the best-fitting allocator as it performs synchronization-free allocations in most
of the cases.
Tiling?
Metis does not require the input file to be in memory all the time which enables the application to
process input files larger than the virtual memory. How this is achieved (e.g. by tiling) is not explained,
but a part of the means to realize the required independence is to copy the keys into an input
independent data structure. There is a copy function introduced with Metis which is invoked for every
new key that was found. This is also a difference to Ostrich which copies the keys later in the Combine
phase.
Combiner
The Combine phase seems to be adapted, too. The Combiners are invoked much more frequently. For
each key that is inserted into the Intermediate buffer, the combine() function is called. This directly
optimizes the dimension 2 of the input data type. By calling the combiner() function more often, the
memory usage is lower. This results in less memory allocation calls and increases Data locality.
The overhead of calling the Combiner more often depends on 2nd dimensions of the input data type (a
few or a lot of values per key). I think it is sensible to build in some kind of Heuristic, e.g. calling
combine() for every second (or so) insert of a value to a key and after the Map phase has finished (like
in Ostrich where the combine() method is used to fill the Iteration buffer). This divides the number of
Combiner calls by 2 but does not increase the memory usage too much.
2.6 Phoenix++
In June of 2011, a team of the Stanford University has released a new version of Phoenix, called
Phoenix++ (or Phoenix 3). The changes are described in a paper and will be summarized here.
Intermediate buffer
The authors found out that the static hash function is sub-optimal for a lot of applications. Even for
Word count, where Phoenix 2 was optimized for, the static hash lead to the problem that the buckets
15
were not optimally filled. With Phoenix++ they introduced 3 different types of Intermediate storages.
The first one is a dynamic hashing algorithm, which creates a hash store for each Mapper thread (not
task) which fits best for *:* key distributions. This enables the store to be used much more efficiently
as the framework tries to reach O(1) insertions at any time. The problem here is the combination of the
possible different hash sizes into a common one the Reducers can work on. This is achieved by
recreating a common hash structure. The authors say that this costs some time, but the overall
performance shall still be higher compared to a fixed-length table. I think the authors could have used a
more sophisticated algorithm to avoid copying, e.g. when using linear hashing, “dynamic hashing” or
expandable hashing one should always find a simple function that transforms the common hash bucket
number into the bucket number of a certain Mapper thread.
The second alternative is to use a fixed-length array for each Mapper thread which fits best the *:k key
distribution.
For 1:1 key distributions, a common array is used (one for all Mapper threads).
There is also an interface for the user to implement its own Intermediate buffer data structure which
best fits the applications input data type.
The <key, value> pair is now stored in a so called container. Each hash bucket can contain several
containers (or pointers to them) – the authors leave the question of sorting the containers within bucket
open. This is important to have a fast access to the keys inside. The fixed-length array does only
include a single container as there is only a single key per container.
Combiners
The combine function is now called after every emit of pair from a Mapper. This reduces the memory
pressure a lot. It can be compared to the Metis improvements as there the same optimization was
applied. Also, the same possible additional improvements I have mentioned could be applied here.
Tests have shown that the performance degradation is less than the performance improvement due to
better Data locality and less memory usage (which can cause swapping!).
C++
Phoenix++ uses C++ as programming language. This means that the usability of Phoenix has been
improved severely, but the performance could be negatively affected because of the several micro
functions that are called in a object oriented programming language. The authors say that this is
prevented by using method inlining.
Custom memory allocators
The already known bottleneck was now addressed by being able to configure the memory allocator at a
central point, the library Interface. Therefore, the allocator needs to implement the STL custom
allocator interface and can then be used by the Containers (Mappers) and Combiners.
Sorting
Sorting is now optional. It can be turned off or a custom sorting algorithm can be used instead of the
default one (see also chapter 2.5 (Metis) and 2.7.3).
16
2.7 Additional optimizations
2.7.1
IO Overlapping
The Tiled-Map/Reduce model using only parts of the input in memory loads the input data of the
current Iteration and blocks until this has been done. This can consume much time and the worker
threads are idle in between.
The input file loading can be overlapped with the processing of the data already loaded into the
memory which reduces the waiting time of the workers. In the case of not using mmap, a double-buffer
(like in the Pipelining concept of Ostrich) and a new user-defined load() method (which was also
introduced in Ostrich, too (called acquire())) can be used to achieve the IO overlapping the following
way:
1 The user-defined load() function is invoked to store the input data into an input buffer which is
allocated by this function.
2 This buffer is then split to generate the Mapper tasks of the first Iteration. The Map phase is
now started. The pointer to this buffer is stored by the framework as current Input Iteration
Buffer Pointer (current IIBP).
3 While the Map phase of the current Iteration is running, the input data of the next Iteration is
preloaded by invoking the user-defined load() function again (in a separate thread). This
function allocates again a new buffer and stores the input for the next Iteration in it. The pointer
is returned to the framework which refers to this buffer as next IIBP.
4 After the current Iteration has finished the current Map and Combine phase, the next Iteration is
dispatched. This means that the “next” Iteration becomes the “current” and therefore the next
IIBP becomes the current IIBP. The old current IIBP is saved as old IIBP. Please note that this
requires the keys (and values) to be copied into a separate buffer, see chapter 2.4.2.
5 The old IIBP is used as an argument to the user-defined load() function which is invoked now to
preload the input for the next Iteration. As the buffer is already defined, no new space is
allocated – the buffer is reused instead (this reduces memory allocations). The old IIBP is
returned and becomes the next IIBP.
If the returned pointer is zero, then the application has no more input left for processing in the
next Iteration.
6 Go back to step 4) until there is something to load, otherwise go to 7) after the current Iteration
has finished.
7 The current and old IIBP are used as an argument for the new user-defined function release().
This one deallocates the memory of those buffers.
As like as the user-defined input splitter() function which splits up the input according to the keys
found, the load() function must be aware about the key (and value) structure, too. This can best be
explained by an example where the input is a text file and the keys are single words. If the framework
requests the load() method to load data from the input beginning from position P with a length L then
the end of this part of the file may split a single key effectively into two keys. To prevent this, the
load() method needs to be aware about the keys in the input. Skipping not complete or likely not
17
complete (which prevents the loading of an additional file block) keys and loading them in the next
Iteration can solve this. This can be implemented with returning the effectively used part (P+L*) of the
input to the framework that takes this into account when calling load() for the next Iteration (P_next =
P+L*).
Using mmap does not make sense here, as the blocks of the file are not preloaded. They are normally
loaded on access and therefore IO Overlapping cannot be applied.
2.7.2
Extending the Pipelining concept of Ostrich
As already mentioned in chapter 2.4.2 the unequal distribution of Map tasks to Mappers is not solved
with Ostrich. By using the idle Mapper threads of the current Iteration to start with the Map phase of
the next Iteration (the data of the next Iteration has been preloaded by the IO Overlapping optimization
and therefor there no waiting is required) as long as the last Mapper has not finished its task of the
current Iteration, a theoretical performance improvement can be achieved. This requires a more
sophisticated task priority queue where the current Iteration has higher priority than the next one (2
priorities for current and next phase should be enough). Also, this queue needs to include the
Information of the task type (Map and Combine), so that the Mappers which are working on the next
Iteration can go back and “transform” themselves into Combiners of the current Iteration after having
stored their results in the Intermediate buffer of the next Iteration.
Unfortunately, this will reduce the advantages of the Cache optimizations of the Tiled-Map/Reduce
model because the data of the next Iteration is filling up the Cache-lines which likely overwrites the
blocks of the current phase. This performance degradation can be limited by using only a fraction of the
waiting Mappers and/or by decreasing the Iteration size (e.g. the 90% of the normal size, which
increases the unbalancing but this is solved by Pipelining).
2.7.3
Optimizing the Final-merge phase
The Final-merge phase reads the data from all partitions of the Final buffer several times because it
applies the Merge algorithm. This reduces the Cache efficiency because there is less Data locality than
in a Mapper job and much more data is read than in a typical task of the Map phase (and even of the
Combine phase). The problem becomes worse in a CMP environment as the memory accessed may be
attached to a different CPU which results in higher latencies.
What also consumes are lot of time is the comparing of the keys. This is done first indirectly as the
Final-merge buffer includes only pointers to keys which further decreases Data locality. And second is
this related to a function call of the user-defined compare() function which may itself take a while to
produce the result (e.g. in the case of string comparing).
By simply copying together the results in the Final buffer and so skipping first the multiple reading of
heavily distributed memory locations and second the comparing of the keys, the performance can be
increased. The disadvantage of this solution is that the result is not sorted any more. If the application
does not require sorted keys, then a good framework can make the sorting in the Final-merge phase
optional.
Please note that the [PHOENIX++] paper suggests this kind of improvement, too. But as I found this
idea “in parallel” to the authors of Phoenix 3, I leave this here as my own idea.
18
3 Combination of cluster and shared-memory based
Map/Reduce
3.1 Hadoop and Multi-core systems
Hadoop supports the processing on a single multi-core host, too. In general, one can say that the
performance is much slower compared to a native SMP framework. Ostrich for example shall be more
than 50x times faster then Hadoop (v0.19.1) on a 16 core machine. The reason for this is according to
[OSTRICH] the slow communication between Mappers and Reducers over the file system and the JVM
itself which is not able to perform as good as optimized C code. In addition to that, Java byte code and
the JIT optimized ASM code does not make use of Cache hierarchy information. The interesting point
here is that only in the multi-threading mode where a single buffer with a “big lock” is used between
the workers, Hadoop was able to perform faster than in the single core mode (but the speedup is not
that huge as the speedup of Ostrich vs. Hadoop).
3.2 Optimization of Apache Hadoop using Phoenix
One can now think about extending the Hadoop framework with Phoenix (or Ostrich and Metis). This
seems to be possible as Hadoop offers two features:
•
Hadoop Streaming
•
Hadoop Pipes
The Pipes feature creates a socket based communication between the framework and a user program
that is based on the Hadoop Pipes C library. The user program could be extended to be based on a
native SMP library like Phoenix, too. The problem with Hadoop Pipes is the deprecation of most of its
modules! Before using Pipes, one needs to re-factor the Java code (e.g. org.apache.hadoop.mapred >
org.apache.hadoop.mapreduce) and fix a lot of bugs (Authentication does not work, input file reading
either, ...) and think about the optimal solution to pass the data to the user code. The easiest way is to
use a (even standard) Java reader that “pipes” its output to the user process map() function that buffers
this and begins processing after the data is fully available. By applying the idea of the IO Overlapping
on this approach this seems to be pretty promising.
The more sophisticated way is to use a custom record reader which can work as like as in a normal
CMP/SMP framework use case. Another open point that needs to be solved here and which is tied to
the user-based file reading is the response time to the framework. If the user-defined code takes too
long for processing an input file, then the Job tracker will kill the job if there was no response given.
Also, does Hadoop offer some kind of progress reporting which needs to be integrated in a Hadoop
Pipes based solution where a custom input file reader is used (already prepared by the Pipes interface,
but due to buffering this can generate problems).
All those things refer to the Map phase only. In general, this can be applied to the Reduce phase, too,
but the customizable input file reading must be skipped as the Reducer input is stored in an Hadoop
internal format. To reduce the amount of data send around, the Combine phase should be used as well
(which is of course supported by Pipes). This can be considered as the Iteration optimization applied in
Ostrich.
Hadoop Streaming, the other feature of Hadoop, pipes the input via STDIN (supported by Pipes, too) to
19
the user process which can buffer those <key, value> pairs and process them as described above for
Pipes. In general, I would prefer Hadoop Streaming over Pipes as it seems to be the successor of Pipes
and is better maintained by the developers.
If you have a cluster which is based on several multi-core machines and want to use it for “cluster
based SMP Map/Reducing”, then you need to also think about how to solve the statically configured
parameters like the split size, the first-level Cache size, the core count and so on. This can be solved for
example by using a configuration which is included in the “job.xml” configuration file of the Hadoop
Map/Reduce job that is exploited by the user code (via environment variables). Phoenix 2 requires most
of the tuning parameters to be set before the compilation of the program. If Phoenix 2 or another library
with a comparable feature shall be used, the framework must be extended to dynamically use those
parameters. Another problem to be solved is the heterogeneous ISA and OS a cluster (or grid) can
contain. This can be implemented by using an intermediate step in the form of a platform independent
programming language like Perl that loads the binary according to the environment detected.
4 Summary and Conclusions
A lot of general and pretty specific optimization have been explained in this paper. It has been shown
that the simple porting of the Map/Reduce model developed for clusters is working fine, but is far away
from being efficient. A lot of points needs to be considered to enhance the throughput. Most of them
belong to the implementation of data structures and using better algorithms. Some others refer to the
design.
What is interesting to see is the taking over of some principles from one framework into another, e.g.
the combine() methods are called more frequently – this was introduced by Metis and applied in
Phoenix++. But there are still some significant differences, for example between Ostrich using TiledMap/Reduce and the new Phoenix++ release. The reason for not including Tiling in Phoenix++ is the
adaptation of the Map/Reduce model, so the authors of Phoenix++. I do not share this opinion because
the Tiling approach is normally using combine() to end an Iteration. If one recalls the cluster based
Map/Reduce model, he can see that it uses map() followed by combine() only on a part of the input.
This is effectively the same like Tiling, which becomes even more clear if you have a look at my
explanations of the Compress phase and the CMP optimization used in Ostrich.
But in general, this attitude of the Phoenix++ authors demonstrates that there is a branching of the
Map/Reduce frameworks going on – into a branch which will not adapt the model of Phoenix 2 and
focus on the details of storing data and so on, and another branch which will adapt the design as well to
reach further performance improvements.
20
References
[PHOENIX]
http://mapreduce.stanford.edu/, last visited in June 2011
[OSTRICH]
Chen, R. ; Chen, H. ; Zang, B.: Tiled-MapReduce: Optimizing Resource Usages of
Data-Parallel Applications on Multicore with Tiling. In: Proceedings of the 19th
international conference on Parallel architectures and compilation techniques ACM,
2010, S. 523–534
[METIS]
Yandong Mao; Robert Morris; M. Frans Kaashoek: Optimizing MapReduce for
Multicore Architectures, June 2010. Published on:
http://pdos.csail.mit.edu/papers/metis:mittr10.pdf, last visited in June 2011
[PHOENIX++]Justin Talbot, Richard M. Yoo, and Christos Kozyrakis : Phoenix++: Modular
MapReduce for Shared-Memory Systems , June 2011. Published on:
http://csl.stanford.edu/~christos/publications/2011.phoenixplus.mapreduce.pdf, last
visited in June 2011
21
FernUniversität in Hagen
Seminar 01912
im Sommersemester 2011
MapReduce und Datenbanken“
”
Thema 15
Strom- bzw. Onlineverarbeitung mit MapReduce
Referent: Jan Kristof Nidzwetzki
2
Jan Kristof Nidzwetzki, Thema 15: Strom- bzw. Onlineverarbeitung mit MapReduce
Inhaltsverzeichnis
1 Abstract
3
2 Einleitung
2.1 MapReduce . . . . . . . . . . . . . . . .
2.2 Exkurs: Strom- bzw. Onlineverarbeitung
2.2.1 Einsatzbereiche . . . . . . . . . .
2.2.2 IBM InfoSphere Streams . . . . .
.
.
.
.
3
3
3
3
4
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
4
4
5
5
6
7
7
8
8
8
9
10
10
11
11
12
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
3 Map-Reduce Online
3.1 Arbeitsweise des klassischen Hadoop . . . . . . . . . . . . . . .
3.1.1 Probleme der Onlineverarbeitung . . . . . . . . . . . . .
3.2 Pipelining . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.2.1 Weitere Probleme beim Pipelining . . . . . . . . . . . . .
3.2.2 Pipelining zwischen verschiedenen MapReduce-Jobs . . .
3.2.3 Fehlertoleranz . . . . . . . . . . . . . . . . . . . . . . . .
3.3 Online Aggregation . . . . . . . . . . . . . . . . . . . . . . . . .
3.3.1 Online Aggregation innerhalb eines MapReduce-Jobs . .
3.3.2 Online Aggregation zwischen mehreren MapReduce-Jobs
3.3.3 Einsatz in der Praxis . . . . . . . . . . . . . . . . . . . .
3.4 Continuous Queries . . . . . . . . . . . . . . . . . . . . . . . . .
3.4.1 Fehlertoleranz . . . . . . . . . . . . . . . . . . . . . . . .
3.4.2 Beispielanwendung: Aufbau eines Monitoring Systems . .
3.5 Performance von Map-Reduce Online . . . . . . . . . . . . . . .
3.5.1 Performance-Tests . . . . . . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
4 Weitere Arbeiten
4.1 DEDUCE: At the Intersection of MapReduce and Stream Processing . . . .
4.1.1 SPADE . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4.1.2 Eine Beispielanwendung . . . . . . . . . . . . . . . . . . . . . . . . .
4.2 Beyond Online Aggregation: Parallel and Incremental Data Minding with Online Map-Reduce . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4.2.1 Architektur des MapReduce Framework . . . . . . . . . . . . . . . .
4.2.2 Ermittlung des Job-Fortschritts und der Genauigkeit . . . . . . . . .
13
13
13
14
5 Vergleich der vorgestellten Arbeiten
15
14
15
15
Jan Kristof Nidzwetzki, Thema 15: Strom- bzw. Onlineverarbeitung mit MapReduce
3
1 Abstract
Das von Jeffrey Dean und Sanjay Ghemawat [DG04] vorgestellte MapReduce Framework
ermöglicht es, große Datenmengen parallel auf mehreren Computern zu verarbeiten. Von seinem Aufbau her, gestattet es das MapReduce Framework zunächst nicht, für die Strom- bzw.
Onlineverarbeitung eingesetzt zu werden. In dieser Semiararbeit werden drei Arbeiten MapReduce Online [CCA+ 09], DEDUCE: At the Intersection of MapReduce and Stream Processing [KAGW10] und Beyond Online Aggregation: Parallel and Incremental Data Minding
with Online Map-Reduce [BAH10] vorgestellt, welche das Framework um diese Fähigkeiten
erweitern. Der Schwerpunkt dieser Seminararbeit liegt auf der Arbeit Map-Reduce Online.
2 Einleitung
2.1 MapReduce
Durch das von [DG04] entwickelte MapReduce Framework ist es auch mit wenig Kenntnissen
im Bereich der Parallelen- und Verteilten-Programmierung möglich, große Datenmengen auf
einem Cluster von Computern effektiv zu verarbeiten. Um Tätigkeiten wie die notwendige
Parallelisierung, Fehlertoleranz, Verteilung der Daten auf die Systeme und Lastverteilung
muss sich der Programmierer nicht kümmern. Diese Aufgaben werden vom MapReduceFramework übernommen.
2.2 Exkurs: Strom- bzw. Onlineverarbeitung
Unter dem Begriff der Stromverarbeitung versteht man das fortlaufende Verarbeiten von Datenströmen. Im Gegensatz zu traditionellen Auswertungen, auf statischen Datenbeständen,
wird hierbei der Datenbestand fortlaufend neu ausgewertet.
2.2.1 Einsatzbereiche
Der Einsatz der Stromverarbeitung ist über all dort interessant, wo sich ändernde Datenbestände zeitnah ausgewertet werden müssen um Entscheidungen zu ermöglichen. Die Einsatzgebiete sind sehr weit gefächert und umfassen unter anderem die folgenden Bereiche:
• Finanzmärke (Auswertung von Aktienkursen)
• Medizintechnik (Überwachung von Körperfunktionen)
• Straßenverkehr (Auswertung von Verkehrsströmen)
• Radioastronomie (Auswertung von Telemetriedaten)
• Logistik (Auswertung von Positionsdaten)
Unter der dem Begriff der Onlineverarbeitung wird verstanden, dass die Verarbeitung der
Daten im Dialog mit dem Anwender erfolgt. Die Onlineverarbeitung kann beispielsweise
dadurch erreicht werden, dass dem Benutzer der Fortschritt seiner Berechnung angezeigt
wird oder ihm ein vorläufiges Ergebnis präsentiert wird. Dieser Form der Datenverarbeitung
steht die klassische Stapelverarbeitung (Batchverarbeitung) gegenüber.
4
Jan Kristof Nidzwetzki, Thema 15: Strom- bzw. Onlineverarbeitung mit MapReduce
2.2.2 IBM InfoSphere Streams
Die Firma IBM hat für die kontinuierliche Auswertung von Datenströmen das Produkt
IBM InfoSphere Streams (ehemals System S ) entwickelt. Die Auswertung der Datenströme
wird mittels Data-Flow Graphen beschrieben. Ein Data-Flow Graph besteht aus einer Menge von Processing Elements (PEs) welche miteinander verbunden werden. Die PEs führen
verschiedene Operationen auf den Datenströmen aus. PEs können auf verschiedenen Computern ausgeführt werden. Dies ermöglicht es, die Auswertung der Datenströme über mehrere
Computer zu verteilen. Für weitere Einzelheiten zu dieser Software siehe [RM] und [BAH10].
Die in dieser Seminararbeit vorgestellte Arbeit DEDUCE: At the Intersection of MapReduce
and Stream Processing erweitert das Produkt IBM InfoSphere Streams um die Fähigkeit,
Daten mittels MapReduce auszuwerten.
3 Map-Reduce Online
In der Arbeit Map-Reduce Online [CCA+ 09] wird das Open Source MapReduce-Framework
Hadoop um die Möglichkeit der Onlineverarbeitung (Online Aggregation) erweitert. Das klassische Hadoop unterstützt bislang nur die Form der Batchverarbeitung. Durch die Onlineverarbeitung ist es Anwendern möglich, vorzeitige Ergebnisse (Early returns) zu erhalten,
während Ihr MapReduce-Job noch ausgeführt wird.
Die Autoren haben im Rahmen dieser Arbeit die Software Hadoop Online Prototype (HOP)
entwickelt. Die Software HOP unterstützt neben der Onlineverarbeitung ebenfalls Continuous Queries, welche es ermöglichen, MapReduce-Programme für Dienste wie Ereignisüberwachung oder die Stromverarbeitung zu nutzen. Dabei behält HOP die von MapReduce bekannte Fehlertoleranz bei und erlaubt es, bestehende MapReduce-Programme auszuführen.
Zudem senkt HOP die Ausführungszeiten von klassischen MapReduce-Programmen, da sich
nun die Map- und die Reduce-Phase überlappen und somit zur Verfügung stehende Ressourcen besser ausgelastet werden können.
3.1 Arbeitsweise des klassischen Hadoop
Das zu Hadoop gehörende Dateisystem Hadoop Distributed File System (HDFS) wird bei
vielen Hadoop-Jobs dazu eingesetzt, die Eingabedaten für die Map-Funktion bereitzustellen.
Die meisten Jobs speichern zudem das Ergebnis der Reduce-Funktion wieder im HDFS.
In einer Hadoop-Installation existiert eine Master-Node, der so genannte JobTracker und
mehrere Worker-Nodes. Der JobTracker nimmt Jobs entgegen, zerteilt diese in einzelne Tasks
und weist diese Tasks den Worker-Nodes zu. Auf den Worker-Nodes laufen wiederum ein
oder mehrere Map- und Reduce-Tasks.
Eine detaillierte Beschreibung der Architektur von Hadoop ist z.B. in [Whi09] zu finden.
Jan Kristof Nidzwetzki, Thema 15: Strom- bzw. Onlineverarbeitung mit MapReduce
5
3.1.1 Probleme der Onlineverarbeitung
Die Architektur von Hadoop sieht es vor, dass die Ausgabe der Map-Funktion und die Ausgabe der Reduce-Tasks erst vollständig vorliegen müssen, bevor diese Daten weiterverarbeitet
werden können. Dies vorgehen erlaubt es Hadoop mit recht einfachen Mitteln für Fehlertoleranz zu sorgen. Falls ein Map- oder Reduce-Task aufgrund eines Fehlers nicht vollständig
ausgeführt werden kann, muss der JobTracker von Hadoop lediglich den Task erneut starten.
Die Ausgabe des fehlgeschlagenen Tasks wird von Hadoop verworfen und nur das Ergebnis
des neuen Tasks wird zur Weiterverarbeitung genutzt.
Erst wenn es möglich ist, Ergebnisse von Map- oder Reduce-Tasks umgehend zu nutzen, kann
eine Online-Verarbeitung der Daten erfolgen. Wie dieses genau durchgeführt werden kann,
ohne die Fehlertoleranz zu beeinträchtigen, wird in den kommenden Abschnitten erläutert.
Die Ausgaben eines Map-Tasks bestehen aus einzelnen Records. Es existiert in Hadoop eine Partition-Funktion, welche für jeden Record die Partition ermittelt, zu der der Record
gehört. Jedem Reduce-Task wird beim Start eine Partition zugewiesen, die er verarbeiten
soll. Die Partition-Funktion bestimmt somit, wie die einzelnen Records auf die Reduce-Tasks
aufgeteilt werden.
Die erste Aufgabe eines Reduce-Tasks ist es, alle für seine Partition relevante Daten der
Map-Tasks einzusammeln. Da diese Daten nicht im HDFS vorhanden sind, müssen diese von den entsprechenden Rechnern, auf den der Map-Tasks lief, abgeholt werden1 . Der
JobTracker sorgt dafür, dass alle TaskTracker bekannt sind, auf welchen Ausgaben eines
Map-Tasks liegen.
Der Reduce-Task kann erst mit der Verarbeitung der Daten beginnen, wenn alle Daten
für die von ihm zu bearbeitende Partition vorliegen.
3.2 Pipelining
Um die Strom- bzw. Onlineverarbeitung zu ermöglichen, wird die Entkopplung der Map- und
Reduce-Tasks aufgehoben. Statt es dem Reduce-Tasks zu überlassen, die Daten der MapTasks einzusammeln, werden bei HOP die Ausgaben der Map-Tasks direkt an die ReduceTasks weitergeleitet (Pipelining).
Sobald ein Client einen neuen Job übermittelt, sorgt HOP dafür, dass jeder Reduce-Tasks
umgehend alle Map-Tasks kontaktiert und eine TCP-Verbindung zu ihnen aufbaut. Über
diese Verbindung leitet der Map-Task umgehend die von ihm produzierten Daten an den
zuständigen Reduce-Task weiter. Der Reduce-Task nimmt diese Daten an und speichert diese in einem Pufferspeicher zwischen. Sobald der Reduce-Tasks darüber benachrichtigt wird,
dass alle Map-Tasks beendet sind, führt er, wie im klassischen Hadoop, die Reduce-Funktion
aus.
Dieses Design vertraut darauf, dass den gestarteten Jobs sofort von Hadoop genügend freie
Map- und Reduce-Tasks zur Verfügung gestellt werden können. Zudem wird davon ausgegangen, dass eine beliebige Anzahl von TCP-Verbindungen aufgebaut werden kann.
1
Dies geschieht bei Hadoop durch das Protokoll HTTP.
6
Jan Kristof Nidzwetzki, Thema 15: Strom- bzw. Onlineverarbeitung mit MapReduce
Beide Annahmen sind in der Praxis nicht immer zu erfüllen. Um das Pipelining trotzdem zu
ermöglichen, bedient sich HOP eines einfachen Tricks: wenn nicht genügend freie Ressourcen
zur Verfügung stehen, um alle Reduce-Tasks sofort zu starten, schreiben die Map-Tasks die
Daten, wie im klassischen Hadoop, auf die lokale Festplatte. Sobald der Reduce-Task ausgeführt werden kann, sammelt dieser die Daten von den Map-Tasks ein.
Um die Anzahl der verwendeten TCP-Verbindungen zu reduzieren, lässt sich die Anzahl der
maximalen TCP-Verbindungen der Reduce-Tasks festlegen. Müssen mehr Map-Tasks kontaktiert werden, als dieses Limit zulässt, so baut der Reduce-Task so viele TCP-Verbindungen
auf, wie maximal möglich. Die Daten von den restlichen Map-Tasks werden wieder wie im
klassischen Hadoop eingesammelt, sobald der Map-Task beendet ist.
3.2.1 Weitere Probleme beim Pipelining
Da die vom Map-Task produzierten Daten sofort an den Reduce-Task übermittelt werden,
kann die aus dem klassischen Hadoop bekannte Combiner-Funktion nicht angewendet werden. Sie sorgt dafür, dass Ergebnisse des Map-Tasks zusammengefasst werden und weniger
Daten zwischen dem Map- und Reduce-Tasks zu übertragen sind.
Zudem werden im klassischen Hadoop die Daten für die Reduce-Tasks von den Map-Tasks
vorab sortiert. Da in HOP die Ausgaben der Map-Tasks direkt an die Reduce-Tasks übermittelt werden, müsste diese Arbeit ebenfalls von den Reduce-Tasks erledigt werden.
Um nicht zu viel Arbeit von den Map-Tasks auf die Reduce-Tasks zu übertragen und um das
Netzwerk nicht übermäßig zu belasten, werden die Daten von den Map-Tasks nicht unmittelbar an die Reduce-Tasks übertragen. Stattdessen hält der Map-Tasks eine gewisse Menge
an Ausgaben im Speicher. Wenn diese Datenmenge einen Schwellwert übersteigt, wird die
Combiner-Funktion angewendet und der Speicherinhalt nach Partition und Map-Key sortiert in eine Datei geschrieben.
In HOP wurde der Task-Tracker um Funktionen für die Verarbeitung derartiger Dateien
erweitert. Zudem wurde das Übertragen der Daten von dem Map-Task in den Task-Tracker
verlegt. Der Map-Task benachrichtigt lediglich den Task-Traker, dass eine neue Datei mit
Ausgaben vorhanden ist. Der Track-Tracker übermittelt nun die Datei umgehend an den
zuständigen Reduce-Task.
Bevor der Map-Task eine neue Datei dem Task-Tracker meldet, wird der Task-Tracker nach
der Anzahl der noch nicht übertragenen Dateien gefragt. Übersteigt das Ergebnis einen
Schwellwert, so registriert der Map-Task die Datei vorerst nicht beim Task-Tracker. Erst
wenn die Anzahl der ungesendeten Dateien unter diesen Schwellwert sinkt, fügt der MapTask alle angesammelten Dateien zusammen. Die zusammengefassten Daten werden sortiert,
der Combiner-Funktion unterworfen und das Ergebnis in eine neue Ausgabedatei geschrieben. Lediglich diese eine Datei wird dann dem Task-Tracker gemeldet.
Durch dieses Vorgehen wird ein Teil der Arbeitslast zwischen den Map- und Reduce-Tasks
dynamisch verteilt. Je nachdem, ob das System für den Map-Task oder Reduce-Task stärker
ausgelastet ist, wird dem stärker belasteten System ein Teil der Arbeitslast abgenommen.
Jan Kristof Nidzwetzki, Thema 15: Strom- bzw. Onlineverarbeitung mit MapReduce
7
3.2.2 Pipelining zwischen verschiedenen MapReduce-Jobs
Viele gängige Berechnungen bestehen aus mehreren hintereinander ausgeführten Map-Reduce
Jobs. In der traditionellen Hadoop-Architektur wird das Ergebnis eines Jobs im HDFS gespeichert. Sobald dieses Ergebnis vollständig vorliegt, wird es vom nächsten Map-Task als
Eingabe verwendet.
Durch HOP ist es möglich, dass die Reduce-Tasks ihre Ergebnisse direkt in die Map-Tasks
des nächsten Jobs weiterleiten. Das aufwändige Schreiben des Ergebnisses des ersten Jobs
ins HDFS entfällt somit.
Zu beachten ist jedoch, dass sich der Reduce-Task des ersten Jobs nicht mit dem MapTask des zweiten Jobs überlappen kann, da das vollständige Ergebnis des ersten Jobs erst
nach Beendigung des Reduce-Tasks vorhanden ist. Erst in diesem Moment können die Daten vom nachfolgenden Job gelesen werden. Diese Einschränkung verhindert ein effektives
Pipelining zwischen zwei verschiedenen Jobs.
Im Abschnitt 3.3.1 wird beschrieben, wie mittels Snapshot-Outputs trotzdem schon vorab
die Daten des ersten Jobs vom zweiten Job verarbeitet werden können.
3.2.3 Fehlertoleranz
Auch HOP ist in der Lage, mit fehlgeschlagenen Map- und Reduce-Tasks umzugehen. Um
einen fehlgeschlagenen Map-Task ausgleichen zu können, führen die Reduce-Tasks Buch
darüber, von welchem Map-Task sie welche Daten erhalten haben. Den Reduce-Tasks ist
es nur erlaubt, Daten von dem gleichen Map-Task zu kombinieren. Erst wenn der ReduceTask die Information erhält, dass der Map-Task vollständig durchgeführt worden ist, dürfen
die Daten aus diesem Map-Task mit Daten aus andern abgeschlossenen Map-Tasks kombiniert werden. Falls ein Map-Task fehlschlägt, kann der Reduce-Task alle Daten die er bis
zum Ausfall von dem Map-Task erhalten hat, vollständig ignorieren. Der Task-Tracker muss
nun lediglich einen neuen Map-Task starten, der die gleiche Berechnung wiederholt, um den
Ausfall zu kompensieren.
Wenn ein Reduce-Task fehlschlägt, wird ein neuer Reduce-Task für die gleiche Partition
gestartet. Die Map-Tasks müssen nun den neuen Reduce-Task erneut mit Ihren Ausgaben
versorgen. Damit der Map-Task die Berechnung nicht vollständig wiederholen muss, speichert er seine Ergebnisse solange zwischen, bis der zugehörige Reduce-Task erfolgreich beendet worden ist.
Diese Art der Fehlertoleranz ist sehr einfach zu implementieren. Jedoch besitzt Sie eine
Limitierung. Die Daten der Map-Tasks können erst von den Reduce-Tasks verarbeitet werden, wenn der Map-Task erfolgreich beendet worden ist. Um diese Limitierung zu umgehen
wurde das Konzept der Checkpoints eingeführt. Ein Map-Task informiert periodisch den
JobTracker dass dieser an dem Offset x in den Eingabedaten angekommen ist. Der JobTracker Informiert darauf hin alle Reduce-Tasks, die Daten von diesem Map-Task konsumieren,
dass diese alle Daten verarbeiten können, die vor diesem Offset liegen. Wenn ein Map-Task
fehlschlägt, muss dieser lediglich an dem zuletzt bekannten Offset seine Arbeit aufnehmen.
8
Jan Kristof Nidzwetzki, Thema 15: Strom- bzw. Onlineverarbeitung mit MapReduce
3.3 Online Aggregation
MapReduce wurde entwickelt, um Daten mittels Batchverarbeitung zu verarbeiten. Häufig
würden Benutzer jedoch gerne MapReduce verwenden, um interaktiv Daten auszuwerten.
Derzeit sieht der typische Arbeitsablauf wie folgt aus: ein MapReduce-Job wird gestartet.
Sobald das Ergebnis vorliegt, schaut der Benutzer sich diese Daten an, um zu überprüfen, ob
diese seinen Vorstellungen entsprechen. Danach wird entweder ein veränderter Job gestartet,
falls das Ergebnis nicht den Vorstellungen des Benutzers entspricht oder die Daten werden
weiterverarbeitet.
Das klassische Hadoop bietet keine Möglichkeiten, Daten interaktiv auszuwerten. Die Ausgabe eines Jobs kann erst angesehen werden, wenn der Job abgeschlossen worden ist. Häufig
wünschen sich jedoch Anwender, ein vorläufiges Ergebnis (Early return) ansehen zu können.
Dieses Ergebnis ist zwar noch nicht sehr genau, häufig genügt es jedoch für die Einschätzung,
ob der laufende MapReduce-Job die gewünschten Ergebnisse liefert.
3.3.1 Online Aggregation innerhalb eines MapReduce-Jobs
In HOP werden die vom Map-Task produzierten Daten umgehend an die Reduce-Tasks weitergeleitet. Jedoch kann der Reduce-Task erst mit seiner Arbeit anfangen, wenn alle Daten
von den Map-Tasks vorliegen. Um die Online Aggregation in HOP zu ermöglichen, wurden
Snapshots eingeführt. In einem Snapshot sind alle bislang von den Map-Tasks gelieferten
Daten enthalten. Der Reduce-Task beginnt seine Arbeit auf diesen Daten und liefert ein
vorläufiges Ergebnis, welches im HDFS abgelegt wird.
Anwender möchten bei solchen Snapshots häufig wissen, wie genau dieser ist. Das Problem,
zu ermitteln, wie genau ein solches Zwischenergebnis ist, ist selbst für normale SQL Abfragen
nur schwer zu bestimmen. HOP gibt daher nur den Fortschritt der laufenden Berechnung an.
Eine genauere Betrachtung dieser Fragestellung wird in der Arbeit Beyond Online Aggregation: Parallel and Incremental Data Minding with Online Map-Reduce [BAH10] nachgegangen,
welche im Abschnitt 4.2 kurz vorgestellt wird.
Der Anwender kann in HOP definieren, wie oft ein solcher Snapshot berechnet werden soll.
So kann er beispielsweise einen Snapshot bei 20% , 40%, 60% und 80% Fortschritt eines
Jobs anfordern. Darüber hinaus kann der Benutzer angeben, ob nur Daten von vollständig
abgeschlossenen Map-Tasks in das Ergebnis einfließen sollen oder ob auch Daten aus noch
laufenden Map-Tasks in das Ergebnis aufgenommen werden sollen.
3.3.2 Online Aggregation zwischen mehreren MapReduce-Jobs
Wie bereits beschrieben, erweitert HOP das klassische Hadoop um die Möglichkeit, Daten
aus den Map-Task direkt an den Reduce-Task weiterzuleiten. Diese Erweiterung kann ebenfalls dazu genutzt werden, um die Online Aggregation zwischen zwei oder mehr Jobs zu
ermöglichen.
Im folgenden Beispiel wird angenommen, dass die zwei Jobs j1 und j2 ausgeführt werden sollen und j2 die Ausgaben von j1 als Eingabe ließt. Um die Online Aggregation über
mehrere Jobs hinweg zu ermöglichen, produziert der Job j1 wie im Abschnitt 3.3.1 erläutert
regelmäßige Snapshots. Der Snapshot wird wie bereits beschreiben im HDFS abgelegt. Zu-
Jan Kristof Nidzwetzki, Thema 15: Strom- bzw. Onlineverarbeitung mit MapReduce
9
dem werden die Daten direkt an die Map-Tasks des Jobs j2 weitergeleitet. Der Task j2
berechnet nun, wie ein normaler Map-Reduce Job, sein Ergebnis. Mit dieser Methode lassen
sich auch mehr als zwei Jobs an einander Reihen.
Dieses Verfahren baut jedoch darauf auf, dass alle Berechnungen von j2 für jeden Snapshot
von j1 vollständig wiederholt werden müssen. Bereits berechnete Zwischenergebnisse lassen
sich bislang nicht weiterverwenden. Eine genaue Untersuchung wie Zwischenergebnisse bei
bestimmten Funktionen weiterverwendet werden können, steht bislang noch aus.
3.3.3 Einsatz in der Praxis
Im Rahmen der Entwicklung von HOP wurde die Online Aggregation an einem praktischen
Beispiel getestet. In 5,5 GB der englischsprachigen Wikipedia sollte ermittelt werden, welches die K häufigsten Wörter sind. Diese Berechnung wurde in der Amazon Elastic Computer
Cloud (EC2) durchgeführt. Dort ist es möglich, sich für eine gewisse Zeit, eine bestimmte
Menge an Ressourcen zu mieten.
Zum Einsatz für diese Berechnung kamen 60 Systeme vom Typ high-CPU Medium mit
jeweils 1,7 GB Arbeitsspeicher und 2 virtual Cores. Ein virtueller Core entspricht der Leistung eines 2,5 GHz Intel Xeon Prozessor aus dem Jahre 2007.
Die 5,5 GB an Eingabedaten wurden ins HDFS kopiert. Auf diesen Daten wurden zwei
MapReduce-Jobs durchgeführt. Der erste Job zählt die Häufigkeit der Wörter und der zweite Job ermittelt die K häufigsten Wörter. In dem Job werden oft Snapshots erzeugt und
ausgewertet. Ziel ist es zu ermitteln, nach wie vielen Sekunden des Jobs bereits die finalen
k häufigsten Wörter feststehen.
Die Ergebnisse dieser Berechnungen sind in Abbildung 1 zu sehen. So ist zu sehen, dass
nach ca. 30 Sekunden schon die Top 5 Wörter, nach ca. 50 Sekunden die Top 10 Wörter und
nach ca. 80 Sekunden die Top 20 Wörter ermittelt waren.
Abbildung 1: Häufigste K Wörter in 5.5 GB Text der englischsprachigen Wikipedia. Quelle:
[CCA+ 09] S. 07
10
Jan Kristof Nidzwetzki, Thema 15: Strom- bzw. Onlineverarbeitung mit MapReduce
3.4 Continuous Queries
MapReduce wird häufig dazu eingesetzt, Datenströme wie Logdateien auszuwerten. Durch
die Architektur von Hadoop ist es nur möglich, die Logdateien als Momentaufnahme zu verarbeiten. Veränderungen an den Logdateien können erst mit der nächsten Auswertung mit
berücksichtigt werden. Wünschenswert wäre es, derartige Ströme von Eingaben kontinuierlich in Echtzeit auszuwerten.
Bei jeder Auswertung der Logdatei muss zudem die komplette Berechnung wiederholt werden, sofern der Anwender nicht selber dafür sorgt, die Ergebnisse der letzten Berechnung
zwischenzuspeichern und mit der nächsten Berechnung wieder zu laden. HOP bietet für die
Stromverarbeitung eine Alternative an. In HOP können Jobs fortwährend laufen und neue
Daten auswerten, sobald diese verfügbar sind.
Die Umsetzung der Continuous Queries für die Stromverarbeitung gestaltet sich durch die
bisher implementierten Funktionen einfach. Genau wie bei der Online Aggregation werden
die Ausgaben der Map-Tasks direkt an die Reduce-Tasks weitergeleitet. Lediglich um eine API-Funktion musste HOP erweitert werden. Diese API-Funktion sorgt dafür, dass die
Map-Tasks ihr aktuelles Ergebnis unmittelbar an die Reduce-Tasks weiterleiten.
Für die Stromverarbeitung wird die Reduce-Funktion periodisch aufgerufen und mit neuen Daten versorgt. Wie oft die Reduce-Funktion aufgerufen wird, hängt von der jeweiligen
Anwendung ab. Der Aufruf der Reduce-Funktion kann entweder in einem festen Zeitintervall
erfolgen, bei Auftreten eines bestimmten Wertes in der Eingabe der Map-Funktion oder beim
erreichen einer bestimmten Anzahl von unverarbeiteten Eingabedaten.
Das Ergebnis der Reduce-Tasks kann genau wie bei der Online Aggregation in HDFS geschrieben werden. Auch alternative Ausgabemöglichkeiten sind in der Praxis denkbar. Ein
Beispiel hierfür ist die im Abschnitt 3.4.2 beschriebene Anwendung.
3.4.1 Fehlertoleranz
In dem vom Hadoop implementierten Fehlertoleranz-Modell ist es einfach, mit fehlgeschlagenen Map- oder Reduce-Tasks umzugehen. Im Bereich der Stromverarbeitung ist dies jedoch
nicht so einfach möglich. Es ist in der Praxis nicht realisierbar, alle von der Map-Funktion
erzeugten Daten aufzubewahren und bei einem Fehler erneut auszuwerten.
Viele Reduce-Tasks benötigen keine vollständige Historie, um Ihre Arbeit fortsetzen zu
können. So benötigt ein Reduce-Task, der einen gleitenden 30 Sekunden Durchschnitt berechnet, nur die Eingaben der letzten 30 Sekunden um nach einem Ausfall weiterarbeiten zu
können.
Um Reduce-Tasks in Ihrer Fehlertoleranz zu unterstützen, wurde in HOP der JobTracker
erweitert. Dieser speichert nun, welche Eingabedaten von den Reduce-Tasks verarbeitet worden sind und wie lange diese Daten vorgehalten werden müssen. Sobald bestimmte Daten
nicht mehr vorgehalten werden müssen, informiert der JobTracker die Map-Tasks, dass diese Daten entfernt werden können. In unserem Beispiel mit der Berechnung des gleitenden
Durchschnitts müssen nur Daten der letzten 30 Sekunden vorgehalten werden. Ältere Daten
sind für die Berechnung nicht relevant.
Jan Kristof Nidzwetzki, Thema 15: Strom- bzw. Onlineverarbeitung mit MapReduce
11
Es gibt jedoch auch Berechnungen, die eine vollständige Historie der Eingabedaten benötigen, um nach einem Fehler korrekt weiterarbeiten zu können. Diese Map-Tasks müssen
derzeit Ihren eigenen Zustand im HDFS speichern und bei einem Neustart diesen Zustand
korrekt wieder einlesen. HOP könnte prinzipiell derartige Aufgaben unterstützen. Derartige
Funktionen sind jedoch bislang noch nicht in HOP implementiert.
3.4.2 Beispielanwendung: Aufbau eines Monitoring Systems
Im Rahmen dieser Arbeit wurde die Nutzung der Stromverarbeitung im praktischen Einsatz
erprobt. Für einen HOP-Cluster wurde ein Monitoring System entworfen, welches sich die
Stromverarbeitung von HOP zu nutze macht. Auf jedem System in diesem Cluster kommt
ein Agent zum Einsatz, welcher einen Map-Task darstellt, der die Eingabedaten für das
Monitoring System liefert. Als Eingabedaten werden Informationen über die Speicher- und
CPU-Auslastung, IO-Operationen, etc. eingelesen.
Alle Agents liefern Ihre Ergebnisse an einen Aggregator, welcher als Reduce-Tasks implementiert worden ist. Dieser vergleicht die Systemlast der letzten 20 Sekunden mit der Systemlast des gesamten Clusters in den letzten 120 Sekunden. Weichen die Werte eines Agents
um mehr als zwei Standardabweichungen vom Durchschnitt des gesamten Systems ab, so
wird ein Alarm erzeugt.
Das ganze System wurde wieder auf einem Amazon EC2 Cluster mit 7 Systemen eingesetzt. Auf diesen Systemen wurde ein MapReduce-Job ausgeführt, welcher auf 5,5 GB an
Daten der Wikipedia die Anzahl der Wörter zählt. 10 Sekunden nachdem dieser Job gestartet worden ist, wurde auf einem der Systeme ein Programm gestartet, was die Systemlast
deutlich ansteigen ließ. Das Monitoring System meldete diesen Ausreißer in der Auslastung
nach weniger als 5 Sekunden.
3.5 Performance von Map-Reduce Online
In einem MapReduce-Job stellt die Map-Phase den größten Teil der Arbeit dar. Die MapFunktion wird auf alle Eingabedaten angewendet. Danach werden die Ausgaben der MapFunktion sortiert und dem TaskTracker gemeldet.
Die Reduce-Funktion besteht aus drei Phasen. In der ersten Phase Shuffle werden die Daten
des Map-Tasks eingesammelt und sortiert. In der Reduce Phase werden die Daten durch die
Reduce-Funktion verarbeitet und in der anschließenden Commit Phase ausgegeben. 75% der
gesamten Arbeit des Map-Tasks entfallen auf die Shuffle Phase. Die restlichen 25% entfallen
auf die Reduce und Commit Phase.
Durch das in HOP eingeführte Pipelining zwischen den Map- und den Reduce-Tasks ist
es dem Reduce Task möglich, die eintreffenden Daten kurz nach Ihrem eintreffen zu sortieren. Wenn der letzte Map-Task seine Arbeit abgeschlossen hat und die Daten an den
Reduce-Task übermittelt worden sind, muss dieser die Daten noch ein letztes mal sortieren
und kann danach in die Reduce Phase eintreten. Im klassischen Hadoop werden die Daten
erst dann sortiert, wenn alle Daten der Map-Tasks vorliegen.
12
Jan Kristof Nidzwetzki, Thema 15: Strom- bzw. Onlineverarbeitung mit MapReduce
3.5.1 Performance-Tests
Als Performance-Test wurde wieder ein MapReduce-Job auf einem Amazon EC2 Cluster
ausgeführt. Zum Einsatz kamen, bei diesem Test, 10 Systeme mit jeweils 16 GB Arbeitsspeicher und vier virtuellen Cores. Als MapReduce-Job wurde ein Wordcount über 10 GB an
Eingabedaten ausgeführt. Einmal kam Hadoop zum Einsatz, das andere mal wurde HOP
mit Pipelining eingesetzt. Der Job wurde jeweils zwei mal ausgeführt. Einmal mit 20 MapTasks und 5 Reduce-Tasks und einmal mit 20 Map-Tasks und ebenfalls 20 Reduce-Tasks.
Die Ergebnisse sind in den Abbildungen 2 und 3 dargestellt.
Abbildung 2: Wordcount über 10 GB mit 20 Map-Tasks und 5 Reduce-Tasks. Links: Hadoop
ohne Pipelining (551 Sekunden), Rechts: HOP mit Pipelining (462 Sekunden).
Quelle: [CCA+ 09] S. 11
Abbildung 3: Wordcount über 10 GB mit 20 Map-Tasks und 20 Reduce-Tasks. Links:
Hadoop ohne Pipelining (361 Sekunden), Rechts: HOP mit Pipelining (290
Sekunden). Quelle: [CCA+ 09] S. 11
Sowohl beim klassischen Hadoop als auch bei HOP ist im ersten Test, bei der Ausführung
des Reduce-Taks, nach 75% eine gewisse Zeit zu erkennen, in der der Job scheinbar nicht
fortschreitet. Der Map-Tasks hat zu diesem Zeitpunkt alle Daten eingesammelt und sortiert
diese, bevor die Reduce Phase gestartet wird. Es ist jedoch deutlich zu erkennen, dass durch
das Pipelining das sortieren schneller abgeschlossen werden kann, da die Daten schon nach
Jan Kristof Nidzwetzki, Thema 15: Strom- bzw. Onlineverarbeitung mit MapReduce
13
dem Eintreffen vorsortiert worden sind. Der Job welcher Pipelining nutzen konnte, kann somit schneller abgeschlossen werden.
Der gleiche MapReduce-Job wurde nochmals laufen gelassen. Dieses mal jedoch mit 20 Mapund 20 Reduce-Tasks. Die Ergebnisse sind in der Abbildung 3 zu sehen. Durch die größere
Anzahl von Reduce-Tasks ist die Datenmenge für jeden Reduce-Tasks geringer, die verarbeitet werden muss. Auch in diesem Setup kann der Job, welcher Pipelining nutzen konnte
schneller abgeschlossen werden.
Die Autoren der Arbeit haben noch weitere vergleiche zwischen dem klassischen Hadoop
und HOP mit Pipelining durchgeführt. Diese Tests haben ergeben, dass durch Pipelining die
Auslastung auf den Systemen gesteigert wird und somit die MapReduce Jobs schneller abgeschlossen werden können. Somit ist HOP auch für den Einsatz mit klassischen MapReduceJobs interessant.
4 Weitere Arbeiten
Auch andere Gruppen haben sich mit der Nutzung von MapReduce zur Strom- bzw. Onlineverarbeitung auseinandergesetzt. Zwei weitere Arbeiten werden in diesem Abschnitt kurz
vorgestellt. In dem Abschnitt 5 werden alle drei Arbeiten miteinander verglichen.
4.1 DEDUCE: At the Intersection of MapReduce and Stream
Processing
Die Arbeit DEDUCE: At the Intersection of MapReduce and Stream Processing [KAGW10]
erweitert das von IBM angebotene und Abschnitt 2.2.2 vorgestellte System IBM InfoSphere
Streams (ehemals System S ) um die Möglichkeit, Daten mittels MapReduce zu verarbeiten.
4.1.1 SPADE
SPADE die Stream Processing Application Declarative Engine stellt im IBM System S
die verwendete Sprache zum beschreiben einer Anwendung dar. SPADE selber besitzt die
Möglichkeit, durch UBOPs - user-defined build-in operators erweitert zu werden.
Diese Fähigkeit macht sich DEDUCE zu nutze. DEDUCE führt einen MapReduce-Operator
ein, welcher als Eingabe eine Liste von Dateien und Verzeichnissen erwartet und als Ausgabe die im System S enthaltenen stream processing operators mit Daten versorgt. Dieser
Operator erlaubt es zudem, kaskadiert eingesetzt zu werden um auf diese Weise die Ausgabe
eines MapReduce-Jobs als Eingabe eines neuen MapReduce-Jobs zu verwenden. Hierbei ist
zu beachten, dass der DEDUCE selber nicht für die Strom- bzw. Onlineverarbeitung eingesetzt wird, sondern lediglich als Datenlieferant eingesetzt wird.
Die Daten für diesen MapReduce-Opreator werden aus dem schon bekannten Hadoop Distributed Filesystem - HDFS gelesen. An einer Unterstützung für die OpenSource Implementation des Google Filesystems Kosmos File System - KFS wird derzeit gearbeitet.
14
Jan Kristof Nidzwetzki, Thema 15: Strom- bzw. Onlineverarbeitung mit MapReduce
4.1.2 Eine Beispielanwendung
Die Autoren von DEDUCE beschreiben in Ihrer Arbeit eine Beispielanwendung für den
MapReduce-Operator. Diese Anwendung wird dazu eingesetzt, Aktienmärkte auszuwerten
und Aktien mit bestimmten Kriterien zu ermitteln. Die Stromverarbeitung des System S
wird dazu eingesetzt, die sich ständig verändernden Daten auf einem Aktienmarkt wie Preis
und Umsatz auszuwerten. Diese Daten werden mit aktuellen Nachrichten kombiniert.
Für die Auswertung dieser Nachrichten, wie Analysen und Ad-Hoc-Meldungen2 , wird ein
MapReduce-Job eingesetzt. Dieser wertet periodisch die leicht mehrere Gigabyte umfassenden Daten aus und stellt das Ergebnis für die weitere Analyse zur Verfügung. Ein Schema
dieser Anwendung ist in der Abbildung 4 dargestellt.
In diesem Schema sind drei wichtige Komponenten zu erkennen. Unten links ist der MapReduce-Job dargestellt. Dieser wertet periodisch Daten aus externen Quellen aus. Das Ergebnis wird durch einen ModelReader gelesen, welcher diese Daten aus dem Dateisystem ließt
zur weiteren Verarbeitung im System S bereitstellt. In dem Schema ist zudem ein Normalizer
zu sehen. Dieser ist dafür zuständig, die aus der Stromverarbeitung stammenden Ergebnisse
mit den Daten des MapReduce-Jobs zu kombinieren.
Abbildung 4: Schematische Darstellung einer Anwendung in System S mit MapReduceOperator. Quelle: [KAGW10] S. 5
4.2 Beyond Online Aggregation: Parallel and Incremental Data
Minding with Online Map-Reduce
In der Arbeit Beyond Online Aggregation: Parallel and Incremental Data Minding with Online Map-Reduce [BAH10] wird ein MapReduce-Framework zur Strom bzw. Onlineverarbeitung
vorgestellt, um Probleme wie die Angabe der Genauigkeit einer vorläufigen Berechnung oder
die Ermittlung der Laufzeit des gesamten Jobs untersuchen zu können.
2
§ 15 WpHG verpflichtet börsennotierte Unternehmen zur sofortigen Veröffentlichung von Nachrichten, die
den Aktienkurs erheblich beeinflussen können. Diese Nachrichten werden als Ad-Hoc-Meldung bezeichnet.
Jan Kristof Nidzwetzki, Thema 15: Strom- bzw. Onlineverarbeitung mit MapReduce
15
4.2.1 Architektur des MapReduce Framework
Das von den Autoren entwickelte MapReduce-Framework basiert auf einer Shared-Memory
Architektur und ist als Testumgebung zur Untersuchung der oben genannten Fragestellungen
gedacht. Die Daten werden in diesem Framework lediglich im Arbeitsspeicher gehalten. Dies
ist der Grund dafür, dass diese Implementation nicht in einem Cluster von Computern
eingesetzt werden kann und nur ebenfalls nur eine sehr geringe Fehlertoleranz aufweist.
Die Autoren sehen diese Implementation als reine Testumgebung an und empfehlen für den
produktiven Einsatz das in Abschnitt 3 vorgestellte Hadoop Online Prototype - HOP.
4.2.2 Ermittlung des Job-Fortschritts und der Genauigkeit
Es wird vorgeschlagen, dass der Entwickler eines MapReduce-Jobs bestimmte Methoden
implementiert, welche es dem MapReduce-Framework ermöglichen, Aussagen über den die
Genauigkeit einer vorläufigen Berechnung oder die noch zu erwartende Laufzeit zu geben.
Diese Methoden müssen vom jeweiligen Entwickler implementiert werden, da jeder Job unterschiedliche Charakteristiken aufweist und ohne diese Angabe keine genaue Ermittlung
dieser Werte möglich wäre.
Das vorgestellte MapReduce-Framework ist in der Lage convergence curves zu zeichnen,
welche es dem Benutzer erlauben, die Genauigkeit eines vorläufigen Ergebnisses abschätzen
zu können. Eine convergence curve stellt einen Graphen dar. Auf der X-Achse wird der Fortschritt des Jobs in dem Intervall von [0,1] angegeben auf der Y-Achse hingegen der Wert der
dif ()-Funktion.
Sobald das MapReduce-Framework ein Zwischenergebnis eines Jobs berechnet hat, wird
zusätzlich die Signatur sig() berechnet. Dabei handelt es sich um eine durch den Entwickler des MapReduce-Jobs bereitgestellte Funktion, die (sofern möglich) alle für das Ergebnis
relevanten Informationen zusammenfasst. Wenn beispielsweise die häufigsten K-Wörter in
einem Text ermittelt werden sollen, würde die Funktion genau diese Wörter zurück liefern.
Wenn hingegen die Anzahl der Wörter ermittelt werden soll, liefert die Funktion nur die bislang gezählten Wörter zurück. Diese Funktion wurde eingeführt, um eine speichersparende
Repräsentation des Zwischenergebnisses zu erhalten.
Die Funktion dif () nimmt als Parameter zwei derartige Signaturen entgegen und berechnet den Abstand dieser beiden Signaturen. Um eine convergence curve zu Zeichnen, wird
der Abstand aller ermittelten Zwischenergebnisse mit dem letzten berechneten Ergebnis ermittelt und die Differenz als Graph dargestellt.
In der Abbildung 5 ist eine convergence curve für einen MapReduce-Job dargestellt, welcher
die Wörter eines Textes zählt. Man sieht, dass die Abweichung eines vorläufigen Ergebnisses
sehr stark abnimmt.
5 Vergleich der vorgestellten Arbeiten
In der Arbeit DEDUCE: At the Intersection of MapReduce and Stream Processing wird
das vom IBM entwickelte System S um die Möglichkeit ergänzt, Daten mittels MapReduce zu verarbeiten. Die im System S enthaltene Beschreibungssprache SPADE wird um
16
Jan Kristof Nidzwetzki, Thema 15: Strom- bzw. Onlineverarbeitung mit MapReduce
Abbildung 5: Wordcount über den Text der englischen Bücher auf Gutenberg Books
(http://www.gutenberg.org). Quelle: [BAH10] S. 5
einen MapReduce-Operator erweitert. Dieser eignet sich dafür, bestehende Daten periodisch
auszuwerten und die Ergebnisse an die Stromverarbeitungs-Operatoren weiterzuleiten. Eine
Strom- bzw. Onlineverarbeitung mittels MapReduce findet jedoch nicht statt.
Die Arbeit Beyond Online Aggregation: Parallel and Incremental Data Minding with Online
Map-Reduce hingegen stellt ein MapReduce-Framework vor, welches um Aspekte wie Fehlertoleranz oder Skalierbarkeit reduziert worden ist, sich jedoch gut zur Evaluation von Erweiterungen eignet. Die Autoren dieses Systems nutzen es, um die Genauigkeit von vorläufigen
Ergebnissen (Online Aggregation / Snapshots) zu Berechnen sowie Aussagen über den Berechnungsfortschritt zu geben.
Die Arbeit Map-Reduce Online beschreibt eine Erweiterung der Software Hadoop. Diese
Erweiterung mit dem Namen Hadoop Online Prototype (HOP) erweitert Hadoop um die
Möglichkeiten der Strom bzw. Onlineverarbeitung.
HOP ermöglicht es, Daten aus Map-Tasks schnell an Reduce-Tasks weiterzuleiten. Hierdurch
werden Jobs schneller ausgeführt, da sich nun die Bearbeitung der Map- und Reduce-Tasks
überlappen kann. Zudem führt HOP das Konzept der Snapshots ein. Hierbei handelt es
sich um vorzeitige Ergebnisse eines Jobs. Diese erlauben es, MapReduce-Jobs auch in der
Online-Verarbeitung einzusetzen. Darüber hinaus werden Continuous Queries eingeführt,
welche HOP die Fähigkeit geben, Datenströme auszuwerten. All diese Erweiterungen bewahren die von Hadoop bekannte Fehlertoleranz.
Jan Kristof Nidzwetzki, Thema 15: Strom- bzw. Onlineverarbeitung mit MapReduce
17
Literatur
[BAH10]
Joos-Hendrik Böse, Artur Andrzejak, and Mikael Högqvist. Beyond online aggregation: parallel and incremental data mining with online map-reduce. In
Proceedings of the 2010 Workshop on Massive Data Analytics on the Cloud,
MDAC ’10, pages 3:1–3:6, New York, NY, USA, 2010. ACM.
[CCA+ 09] Tyson Condie, Neil Conway, Peter Alvaro, Joseph M. Hellerstein, Khaled Elmeleegy, and Russell Sears. Mapreduce online. Technical Report UCB/EECS2009-136, EECS Department, University of California, Berkeley, Oct 2009.
[DG04]
Jeffrey Dean and Sanjay Ghemawat. Mapreduce: Simplified data processing on
large clusters. In OSDI, pages 137–150, 2004.
[KAGW10] Vibhore Kumar, Henrique Andrade, Bugra Gedik, and Kun-Lung Wu. Deduce:
at the intersection of mapreduce and stream processing. In Ioana Manolescu,
Stefano Spaccapietra, Jens Teubner, Masaru Kitsuregawa, Alain Léger, Felix
Naumann, Anastasia Ailamaki, and Fatma Özcan, editors, EDBT, volume 426
of ACM International Conference Proceeding Series, pages 657–662. ACM, 2010.
[RM]
Roger Rea and Krishna Mamidipaka, editors. IBM InfoSphere Streams - Redefining Real Time Analytics. IBM Software Group.
[Whi09]
Tom White. Hadoop: The Definitive Guide. O’Reilly, first edition edition, june
2009.
MapReduce und Datenbanken - Ähnlichkeitssuche auf Mengen
FernUniversität in Hagen
Seminar 01912
im Sommersemester 2011
„MapReduce und Datenbanken“
Thema 16
Ähnlichkeitssuche auf Mengen
Referent: Andreas Kühl
MapReduce und Datenbanken - Ähnlichkeitssuche auf Mengen
MERKMALSEXTRAKTION..................................................................................................................................................4
MERKMALSREDUKTION....................................................................................................................................................4
KLASSIFIKATION...............................................................................................................................................................4
MUSTERERKENNUNG UND MAPREDUCE..........................................................................................................................5
ÄHNLICHKEITSSUCHE.......................................................................................................................................................5
Jaccard-Koeffizient.....................................................................................................................................................5
PHASE 1 – TOKEN ORDERING...........................................................................................................................................7
Hauptvariante Basic Token Ordering (BTO)........................................................................................................................... 7
Alternative Variante Using One Phase to Order Tokens (OPTO)............................................................................................ 8
PHASE 2 – RID-PAAR GENERIERUNG...............................................................................................................................9
Variante Basic Kernel............................................................................................................................................................. 9
Alternative Umsetzungen..................................................................................................................................................... 10
PHASE 3 – RECORD JOIN................................................................................................................................................ 11
Variante Basic Record Join (BRJ)......................................................................................................................................... 11
Alternative Variante One-Phase Record Join (OPRJ)............................................................................................................ 11
TECHNIK UND TESTDATEN.............................................................................................................................................13
HERAUSFORDERUNG UNZUREICHENDER SPEICHER.......................................................................................................13
WAS BRINGT MAPREDUCE IN DER ÄHNLICHKEITSSUCHE?............................................................................................14
MAPREDUCE UND ALGORITHMEN..................................................................................................................................14
Alternative Varianten............................................................................................................................................................ 14
Einführung
In der vorliegenden Veröffentlichung wird beschrieben, wie eine Ähnlichkeitssuche mit MapReduce
umgesetzt werden kann.
Ähnlichkeitssuche wird in verschiedenen Bereichen der Mustererkennung eingesetzt. Die Autoren
haben sich den Bereich der Textvergleiche ausgesucht. Als mögliche Einsatzgebiete geben sie dabei
an
• die Clustern von Dokumenten
• die Erkennung von Plagiaten
• der Vergleich von ähnlichen Suchanfragen durch verschiedene Anwender
• die Erkennung von Betrugsversuchen in Zusammenhang mit Online-Werbung
Schwerpunkt und Stärken des Artikels sind
1. Beschreibung der Umsetzung der Map-Reduce-Blöcke (nicht der Algorithmen selbst)
2. Alternative Vorgehensweisen zur Optimierung von Performance und Speicherplatzverbrauch
3. Auswertung der Auslastung der einzelnen Knoten in den Algorithmen.
Der letzte Punkt wird hier ausgelassen, der Schwerpunkt liegt auf der Betrachtung der Punkte 1 und
2.
Nicht besprochen werden im Artikel die Details der Algorithmen der Mustererkennung, obwohl die
Qualität hauptsächlich von der Parametrierung dieser Algorithmen abhängt. Ebenso wurde auf eine
qualitative Analyse der Ergebnisse verzichtet.
Mustererkennung
Die Mustererkennung lässt sich in mehrere Schritte unterteilen:
•
Merkmalsextraktion
•
Merkmalsreduktion
•
Klassifikation
Merkmalsextraktion
Die Merkmale sind stark vom Kontext abhängig. In dem Fall der Texterkennung oder der
Vergleiche von Texten kann es Merkmale geben, die mehr oder weniger aussagekräftig sind. Je
bekannter die möglichen Eingangsdaten sind, desto bessere Merkmale lassen sich für gute Suchen
definieren.
•
Beispiel - Suche nach Themen:
o Ein Merkmal kann hier ein einzelner Begriff, speziell Fachbegriff sein (siehe
Suche in Suchmaschinen)
•
Beispiel – Suche nach Textpassagen
o Einzelne Begriffe sind hier unzureichend, da gerade Fachbegriffe kein Indiz
dafür sind, ob etwas kopiert worden ist. Statt dessen kommt es hier auf ganze
Sätze an.
Merkmalsreduktion
Welche Merkmale sind in dem aktuellen Kontext wirklich aussagekräftig?
•
Beispiel Fachbegriff
o Im Titel ist dieser Fachbegriff sehr aussagekräftig, auch wenn er sonst nicht mehr
im Text vorkommt.
o Wenn der Fachbegriff im Text nur einmal vorkommt, ist er vermutlich eher
irrelevant
o Wenn der Fachbegriff im Text mehrmals vorkommt, erhöht sich dagegen seine
Relevanz, selbst wenn er im Titel nicht erwähnt wird.
Anmerkung:
Das Wissen über Merkmalsextraktion und Merkmalsreduktion ist in der Realität bedeutend. Es wird
beispielsweise ausgenutzt, um die Ergebnisse von Google zu manipulieren. Man muss also die
eigenen Bewertungsalgorithmen immer wieder in Frage stellen.
Klassifikation
Bei Klassifizierung werden die Merkmale und ihre Ausprägungen in Klassen zusammengefasst.
Mustererkennung und MapReduce
Im vorliegenden Beispiel sollen Algorithmen aus der Mustererkennung mit MapReduce verknüpft
werden. Mustererkennung funktioniert auch ohne MapReduce und kommt auch auf die gleichen
Ergebnisse. Der Vorteil liegt also nicht in der Qualität der Ergebnisse. Die Qualität der Ergebnisse
hängt allein von den verwendeten Algorithmen der Mustererkennung ab. Allerdings sind die
Datenmengen eine große Herausforderung für die Mustererkennung, und an dieser Stelle kommt
MapReduce ins Spiel. Es gibt der Mustererkennung eine Möglichkeit, mit der großen Datenmenge
zurecht zu kommen.
Im einfachsten Fall kann man die Extraktion der Merkmale in die Map-Phase stecken und die
Reduktion in die Reduce-Phase. Oft ist jedoch sinnvoller, mehrere Map-Reduce-Blöcken
miteinander zu koppeln.
Ähnlichkeitssuche
Es gibt verschiedene Algorithmen für Berechnung von Ähnlichkeiten. Im vorliegenden Text werden
folgende Algorithmen erwähnt:
•
Jaccard-Koeffizient
•
Cosinus-Koeffizient
•
Tanimoto-Koeffizient
•
PPJoin+Algorithmus
Alle Algorithmen haben ihre Stärken und Schwächen. Der Jaccard-Koeffizient ist ein sehr einfacher
Algorithmus, an dem man die Problematik gut erläutern kann.
Die Autoren des vorliegenden Textes verwenden den PPJoin+ Algorithmus und den JaccardAlgorithmus.
Jaccard-Koeffizient
Mit dem Jaccard-Koeffizienten kann die Ähnlichkeit zwischen zwei Elementen berechnet werden.
Dabei wird die Summe der gemeinsamen Merkmale durch die Summe aller Merkmale geteilt.
J ( A, B ) =
A∩ B
A∪ B
Dazu ein Beispiel mit Texten:
Ausgangssatz A: „I will call you back“
Vergleichsatz B: “I will phone you”
Vergleichsatz C: “I will drive you back home”
Vergleichssatz D: “I will call you back as soon as I am back from London”
Daraus ergeben sich folgende Koeffizienten:
•
A + B : 3 / 6 = 0.5
•
A + C : 4 / 7 = 0.57
•
A + D : 5 / 10 = 0.5
Wie man sehen kann, erreicht den besten Wert ein Beispiel, welches vom Sinn am schlechtesten ist.
Beim Jaccard-Koeffizienten hängt viel auch von der Länge der Vergleichssätze ab. Dass dabei ein
Satzteil einfach nur den andere ergänzt, wird bestraft, weil er zu der Gesamtmenge viele ungleiche
Worte liefert.
Das heisst nicht, dass der Jaccard-Koeffizient prinzipiell ungeeignet ist. Es hängt sehr stark von den
Aufgaben ab.
Annahme A: Beim Kopieren von Sätzen werden nur wenige Worte geändert.
Folge A: Das würde bedeuten, dass sich auch bei langen Sätzen klare Übereinstimmungen finden
lassen.
Annahme B: Bei wissenschaftlichen Texten werden oft lange Sätze verwendet.
Folge B: die bessere Beurteilung der kurzen Sätze würde nicht ins Gewicht fallen.
Annahme C: Bei Interviews werden eher kurze Sätze verwendet
Folge C: Beim Vergleich von Interviews würden wir schlechtere Ergebnisse finden.
Die Beispiele lassen sich hier beliebig fortsetzen. Es ist daher wichtig für die Qualität der
Ergebnisse, dass man sich über zwei Punkte im Klaren ist
•
Wie sind Qualität und Eigenschaften der Eingangsdaten
•
Welche Antworten will ich haben
Dies gilt generell in der Mustererkennung und der Vergleich von Texten bildet da keine Ausnahme.
Umsetzung mit MapReduce
Der Ablauf der Ähnlichkeitssuche besteht aus mehreren Phasen, die im folgenden Detailliert
beschrieben werden
• Phase 1 – Token Ordering
o Diese Phase kann man mit der Merkmalsdefinition und Merkmalsextraktion aus dem
Kapitel Mustererkennung gleichsetzen
• Phase 2 - RID-Pair Generation
o Diese Phase entspricht der Merkmalsreduktion und Klassifikation aus der
Mustererkennung
• Phase 3 – Record Join
Phase 1 – Token Ordering
Token lassen sich am besten mit Merkmalen übersetzen. Über die Summe der Merkmale eines
Textes oder Satzes lassen sich diese identifizieren und beschreiben. Das Problem dabei ist, dass es
fast unendlich viele Merkmale gibt, man muss sich also für bestimmte Merkmale entscheiden.
Im vorliegenden Text werden 2 Arten von Merkmalen erwähnt, Worte und q-Gramme. Q-Gramme
sind ein wichtiges Element, was in vielen Bereichen von Google eingesetzt wird. Im Prinzip schaut
man nicht auf Worte, sondern auf Wortteile einer festen Länge.
Um eine einfache – und auch im Text beschriebene Alternative – ist, die einzelnen Worte eines
Textes als Merkmale zu betrachten. Der Satz „I will call back“ hätte also die 4 Merkmale „I“,
„will“, „call“ und „back“.
Aufgabe des Token Orderings ist: „It scans the data, computes the frequency of each token, and
sorts the tokens based on frequency“ [aus [4], Seite 497]
Der erste Schritt besteht aus dem Extrahieren von Merkmale. Anschliessend werden diese
Merkmale sortiert. Jeder dieser beiden Schritte lässt sich in einem eigenen MapReduce-Block
realisieren.
Hauptvariante Basic Token Ordering (BTO)
MapReduce-Block „Extrahieren der Merkmale“
•
Map-Funktion
o Eingangsparameter sind die Originaldaten.

Man könnte hierbei unterteilen in komplette Texte, in Textpassagen oder gar
nur in einzelne Sätze. Auf diese Unterscheidungen wird jedoch nicht
eingegangen.
o Für jeden Datensatz werden alle Merkmale extrahiert und in den Zwischenspeicher
geschrieben. Für jedes Merkmal wird dabei ein paar produziert: (Merkmal, 1). Die
“1” ist dabei die Häufigkeit, die hier immer „1“ ist.
•
Combine-Funktion (Zwischenschritt)
o Dieser Zwischenschritt wird vorgenommen, um die Last im Netzwerk zu reduzieren.
o Alle Merkmalspaare des lokalen Zwischenspeichers werden auf dem Knoten
durchgegangen und aufsummiert: (Merkmal, Anzahl)
•
Reduce-Funktion
o Die Ergebnisse der einzelnen Knoten werden nach Merkmalen aufsummiert
MapReduce-Block „Sortieren der Merkmalslisten“
•
Map-Funnktion
o Für alle Merkmalspaare werden Anzahl und Merkmal miteinander vertauscht und
nach Häufigkeit sortiert

•
(Merkmal, Anzahl) => (Anzahl, Merkmal)
Reduce-Funktion
o Zusammenfassen der Ergebnisse der einzelnen Zwischenspeicher und erstellen einer
vollständig sortierten Liste aller Merkmales
Hier noch mal eine schematische Darstellung des Ablaufs der Phase 1
[aus [4], S. 498]
Alternative Variante Using One Phase to Order Tokens (OPTO)
Ein Alternativer Ansatz der Autoren für die erste Phase ist, sich auf einen MapReduce -Block zu
beschränken. Bei diesem Ansatz wird der MapReduce-Block „Sortieren der Merkmalslisten“ in die
Reduce-Funktion des ersten Blocks integriert.
Der Vorteil an diesem Ansatz ist die Reduzierung der Rechenschritte. Der Nachteil ist der
Speicherverbrauch. Die Autoren haben festgestellt, dass in ihren Fällen keine Speicherprobleme
auftreten.
Phase 2 – RID-Paar Generierung
Prefix-Filterung wird im Deutschen auch als Taubenschlags-Algorithmus bezeichnet.
Grundannahme ist dabei, dass man Elemente hat, die zu Sortieren sind, als dass es Kategorien gibt,
denen sie zugeordnet werden sollen.
Ein Beispiel: Man hat 50 Personen, die im Mai Geburtstag haben und sortiert diese nach den
einzelnen Tagen im Mai. Man bekommt entsprechend eine Gruppierung der Personen nach ihren
Geburtstagen, wobei es mindestens einen Fall gibt, in dem mehr als eine Person nicht alleine in
einer Kategorie ist.
Entsprechend ist es auch hier: Man nimmt eine Anzahl von Merkmalen und sucht, welche
Datensätze in mindestens einem Merkmal übereinstimmen. Diese Merkmale werden zusammen auf
einem Knoten verarbeitet.
Aufgabe der zweiten Phase ist es, die Ausgangsdaten zu scannen, und die „Prefixes“ zu berechnen,
und zwar auf Grundlage der Merkmale aus Phase 1. Grundannahme ist dabei, dass die Anzahl der
Tokens kleiner ist als die Anzahl der Datensätze. Die Token bilden dabei die Kategorien, denen die
Datensätze zugeordnet werden. Ergebnis ist dann eine Liste von Datensätze, die mindestens ein
Merkmal gemeinsam haben, und in der Folgephase auf einem Knoten verarbeitet werden sollen.
Variante Basic Kernel
•
Map-Funktion
o Eingangsdaten sind die Originaldaten imklusive ihrer Join-Attribute und die nach
Häufigkeit sortierten Merkmale der Texte.
o Aus den Ausgangsdaten werden nacheinander alle RIDs und Join-Attribute
extrahiert.

Join-Attribute können beispielsweise ganze Sätze oder Satzanfänge oder
bestimmte Worte eines Satzes sein.
o Die Join-Attribute werden nach Merkmalen analysiert
o Die Präfix-Länge und die Präfix-Merkmale werden berechnet
o Ergebnis der Map-Funktion ist ein paar (Merkmal, RID+Join-Attribut)
•
Reduce-Phase
o Die Zwischenergebnisse werden zusammengefasst und ein erstes Ähnlichkeitsmass
berechnet
o Dabei werden weitere Filter eingesetzt, um die Anzahl der weiterzuverarbeitenden
Daten zu reduzieren.
o Für die Filter und die Berechnung der Ähnlichkeit werden beispielsweise PPJoin+
Algorithmen einsetzt
o Ergebnis ist dann eine Liste (RID 1, RID2, Ähnlichkeit)
Hier noch mal eine schematische Darstellung des Ablaufs der Phase 1
[aus [4], S. 499]
Alternative Umsetzungen
Auch bei diesem Schritt gibt es Möglichkeiten, die Netzwerklast zu verringern. Beschrieben wird
hier die Variante, dass für jedes Merkmal in der Map-Funktion ein eigener Ergebnis-Satz generiert
wird. Es gibt jedoch auch die Möglichkeit, mehrere Merkmale zusammenzufassen. Dies lässt sich
schon in der Map-Funktion realisieren, so dass keine eigene Combine-Funktion wie in Phase 1nötig
ist.
Eine weitere Alternative sind hier wie schon oben erwähnt andere Ähnlichkeitsmasse. In der
alternativen Umsetzung der Autoren verwenden sie den PPJoin+ Algorithmus. Wie schon erwähnt
ist die Auswahl des Ähnlichkeitsmasses wesentlich für die Qualität der Ergebnisse. Die Autoren
machen hierzu keine Angaben, allerdings verweisen sie auf die Literatur. Zusätzlich muss man
beachten, dass die Qualität auch von der Zielsetzung abhängt. Beachtenswert ist jedoch die
Feststellung der Autoren, dass der PPJoin+ Algorithmus zu einer guten Ausnutzung des Speichers
führt. Insbesondere in diesem Zusammenhang
In der Phase 2 kann es passieren, dass die Knoten ungleichmäßig ausgelastet werden. Grund dafür
ist die unterschiedliche Häufigkeit der Merkmale, die in Phase 1 berechnet wurden. Allerdings lässt
sich über die Häufigkeit der Merkmale auch gut eine Verteilung auf die einzelnen Knoten
berechnen.
Phase 3 – Record Join
Nach Phase 2 könnte man sagen, dass die Aufgabe erledigt ist. Die Ähnlichkeit zwischen den
Eingangsdatensätzen wurde berechnet und nach einem Schwellwert gefiltert. Allerdings wird im
Ergebnis nur auf die Originaldaten verwiesen, die Originaldaten werden nicht direkt ausgegeben.
In Phase 2 fand der Ähnlichkeitsvergleich nicht auf den Originaldaten statt, sondern auf den JoinAttributen. Genaugenommen muss es für Phase 2 noch einen weiteren Map-Reduce-Block geben,
der diese Join-Attribute erzeugt.
Phase 3 könnte damit als optional betrachtet werden. Leider ist die Mustererkennung keine exakte
Methode. Es werden Wahrscheinlichkeiten berechnet, keine eindeutigen Ergebnisse. Hier kommt
wieder das Wissen eines Menschen ins Spiel, der viel komplexere Vergleiche durchführen kann. Die
Kandidaten, die eine hohe Ähnlichkeit haben, sollen komplett und mit der berechneten Ähnlichkeit
angezeigt werden, damit der Anwender als letzte Instanz sich diese Daten ansehen und eine letzte
Entscheidung treffen kann. Oder damit ein anderes Programm einfacher die Daten weiterverarbeiten
kann.
Eine generelle Anmerkung noch, warum nicht auf den Originaldaten in Phase 2 gearbeitet wurde:
der Grund dafür sind der verfügbare Speicher und die Geschwindigkeit der Verarbeitung. Man
reduziert die Daten, die verarbeitet werden müssen, auf die Daten, die man als relevant ansieht,
beispielsweise auf die ersten 5 Worte eines Satzes oder alle Nomen eines Satzes. Auf diese Weise
kann man die Datenlast deutlich reduzieren.
Variante Basic Record Join (BRJ)
Eingangsdaten für Phase 2 sind
•
Die Ausgangsdatensätze
•
Die Paare aus Phase 2 (RID1, RID2, Ähnlichkeit)
Die Autoren beschreiben zwei Ansätze, die RIDs mit den Ausgangsdaten in Verbindung zu bringen.
Hier soll nur Variante 1 beschrieben werden.
Basic Record Join:
•
(RID1, RID2, Ähnlichkeit) wird aufgespalten in (RID1, Referenz-ID) und (RID2, ReferenzID)
o Referenz-ID ist dabei die ID des Ergebnis-Datensatzes aus Phase 2. Anhand dieser
ID werden dann die beiden Teilergebnisse wieder zusammengeführt
•
Beide RIDs werden auf getrennten Knoten bearbeitet
•
Beide Ergebnisse werden wieder miteinander vereinigt
Zur Verarbeitung werden wieder 2 MapReduce-Blöcke verwendet. Im ersten wird die Aufteilung
der Datensätze durchgeführt, im zweiten werden die Originaldaten ermittelt und zusammengeführt.
Alternative Variante One-Phase Record Join (OPRJ)
Die Alternative Variante besteht darin, dass das Mapping zwischen den Originaldaten und den RIDs
nicht einzeln passiert. Wieder wird die zweite Phase in die Reduce-Phase mit hineingezogen.
Hier noch mal eine schematische Darstellung des Ablaufs der Phase 3
[aus [4], S. 500]
Praxis
Technik und Testdaten
Die beschriebene Vorgehensweise wurde mit Hadoop-Datenbank verwirklicht. Das RechnerNetzwerk besteht auf 40 Recheneinheiten mit eigenen Festplatten.
Als Testdatensatz wurden zum einen die DBLP mit 1,2 M Veröffentlichungen und die CITESEERX
mit 1,3 M Veröffentlichungen verwendet.
Herausforderung Unzureichender Speicher
Eine der Herausforderungen bei großen Datenmengen ist die Speichergröße. Dies ist bei der
Ähnlichkeitssuche nicht anders. Speziell in Phase 2 ist bei den Autoren dieses Problem aufgetreten.
Durch Anpassung der Algorithmen und Merkmale haben die Autoren es jedoch geschafft, diese
Probleme in den Griff zu bekommen.
Lösungsansätze sind
•
Zusammenfassen von mehreren Merkmalen zu einem neuen
•
Es werden nicht mehr die Orginale der Sätze verwendet, sondern nur noch deren Merkmale
und RIDs.
•
Es werden Filter eingesetzt (z.B. Längenfilter für die Merkmale)
Die Autoren haben diese Varianten alle ausprobiert und damit Verbesserungen erreicht. Auch
Modifikationen der Map- und Reduce-Funktionen werden vorgestellt.
Insgesamt sind mit Hilfe dieser Herangehensweisen die Probleme der Speicherverwaltung in den
Griff zu bekommen.
Fazit
Was bringt MapReduce in der Ähnlichkeitssuche?
Wie schon zuvor erwähnt, bringt verändert MapReduce nichts an der Qualität der Ergebnisse. Die
Qualität hängt allein von der Kombination aus verwendeten Algorithmen, Eingangsdaten und
Fragestellungen ab. MapReduce fügt hier kein weiteres Element hinzu, wodurch die
Ähnlichkeitssuche verbessert werden kann.
Der wesentliche Unterschied besteht in der Parallelisierung der Verarbeitung. MapReduce stellt hier
ein Framework zur Verfügung, was sich für die Ähnlichkeitssuche sehr gut eignet. Dies hängt auch
damit zusammen, dass die Ähnlichkeitssuche sehr stark auf dem Map-Mechanismus basiert: über
viele Dokumente wird immer wieder die gleiche Operation ausgeführt.
MapReduce und Algorithmen
Man konnte am Beispiel der Ähnlichkeitssuche gut erkennen, dass sich der MapReduceMechanismus an vielen Stellen der Verarbeitung verwenden lässt. In dieser Umsetzung finden sich
beispielsweise 4 MapReduce-Blöcke. Es kann ein Ansatz sein, sich bei großen Datenmengen daher
von dem roten Faden aus MapReduce leiten zu lassen: welche Operation lässt sich so
generalisieren, dass ich sie immer wieder auf alle Daten anwenden kann? Der Unterschied ist, dass
man nicht mehr auf sehr spezielle Umsetzungen der Algorithmen schaut, sondern sie allgemeiner
fasst und dadurch eine einfachere Parallelisierung erreicht.
Alternative Varianten
Die Autoren stellen einige Alternative Umsetzungen vor, die alle ihre Vor- und Nachteile haben. Im
wesentlichen läuft es hier auf Architektur-Entscheidungen hinaus: wie sieht es mit dem
Speicherverbrauch aus, wie wichtig ist Performance, wie wichtig ist Wiederverwertbarkeit. In
einigen Beispielen der Autoren führen die Alternativen zu einem besseren Laufzeit-Verhalten auf
Kosten einer erhöhten Komplexität der Algorithmen. Die Frage, was die bessere Lösung ist, ist
nicht allgemeingültig zu beantworten, sondern ist - wie schon bei der Auswahl der
Ähnlichkeitsmasse - eine persönliche Einschätzung und hängt sehr stark mit der Aufgabenstellung
zusammen.
Literatur
[1]
Karin Haenelt, IR-Modelle: Vektor-Modell, 25.10.2009,
http://kontext.fraunhofer.de/haenelt/kurs/folien/Haenelt_IR_Modelle_Vektor.pdf
[2]
Karin Haenelt: Ähnlichkeitsmaße für Vektoren,
http://kontext.fraunhofer.de/haenelt/kurs/folien/Haenelt_VektorAehnlichkeit.pdf
[3]
Arvind Arasu, Venkatesh Ganti, Raghav Kaushik: Efficient Exact Set-Similarity Joins
[4]
C. Xiao, W. Wang, X. Lin, and J. X. Yu. Efficient similarity joins for near duplicate
detection. In WWW, pages 131–140, 2008.
[5]
Vernica, R. ; Carey, M.J. ; Li, C.: Efficient parallel set-similarity joins using
MapReduce. In: Proceedings of the 2010 international conference on Management of
data ACM, 2010, S. 495–506
[6]
Reginald Ferber: Information Retrieval, Suchmodelle und Data-Mining-Verfahren fuer
Textsammlungen und das Web, Heidelberg, dpunkt-Verlag, http://information-retrieval.de/
[7]
Harald Jele: Erkennung Bibliographischer Dubletten mittels Trigrammen,
http://wwwu.uni-klu.ac.at/hjele/publikationen/ngramme/2009_ngramme_main.pdf
Herunterladen