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=AB 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(leftkey, leftvalue); } 23: if (hasMoreTuples.second) { 24: rightProcessor.process(rightkey, rightvalue); } 25: if (hasMoreTuples.first && hasMoreTuples.second) { 26: merger.merge( leftkey, leftvalue, 27: rightkey, rightvalue); } 28: pair<bool,bool> iteratorNextMove = 29: iteratorManager.move(leftkey, rightkey, 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