Sei sulla pagina 1di 406

BIG DATA CON HADOOP

Garry Turkington, Gabriele Modena


Apogeo - IF - Idee editoriali Feltrinelli s.r.l.
Socio Unico Giangiacomo Feltrinelli Editore s.r.l.

ISBN: 9788850317424

Copyright (c) Packt Publishing 2014. First published in the English language under
the title Learning Hadoop 2 (9781783285518).
Il presente file pu essere usato esclusivamente per finalit di carattere personale.
Tutti i contenuti sono protetti dalla Legge sul diritto dautore.
Nomi e marchi citati nel testo sono generalmente depositati o registrati dalle
rispettive case produttrici.
Ledizione cartacea in vendita nelle migliori librerie.
~
Sito web: www.apogeonline.com
Scopri le novit di Apogeo su Facebook
Seguici su Twitter @apogeonline
Rimani aggiornato iscrivendoti alla nostra newsletter
Introduzione
Questo libro vi guider per mano nellesplorazione del fantastico mondo di
Hadoop 2 e del suo ecosistema in continua crescita. Basato sulle solide
fondamenta delle versioni precedenti della piattaforma, Hadoop 2 consente
lesecuzione di pi framework di elaborazione dei dati su un unico cluster
Hadoop.
Per darvi unidea della portata dellevoluzione, studieremo sia il funzionamento
dei nuovi modelli sia come si applicano nellelaborazione di grandi volumi di dati
con algoritmi batch, iterativi e quasi in tempo reale.
Struttura del libro
Il Capitolo 1, Per iniziare, fornisce le basi di Hadoop e per affrontare i
problemi dei big data che intende risolvere. Vedremo anche dove Hadoop 1 ha
spazio di miglioramento.
Il Capitolo 2, Storage, entra nel dettaglio dellHadoop Distributed File
System, dove vengono memorizzati i dati elaborati da Hadoop. Esamineremo le
caratteristiche specifiche di HDFS, spiegheremo come utilizzarle e vedremo come
migliorato in Hadoop 2. Presenteremo anche ZooKeeper, un altro sistema di
storage allinterno di Hadoop, su cui si basano molte funzionalit cruciali.
Il Capitolo 3, Elaborazione: MapReduce e oltre, affronta innanzitutto il
tradizionale modello di elaborazione di Hadoop e come viene utilizzato. Vedremo
poi come Hadoop 2 ha generalizzato la piattaforma per utilizzare pi modelli
computazionali, tra i quali MapReduce solo uno dei tanti.
Il Capitolo 4, Computazione in tempo reale con Samza, approfondisce uno di
questi modelli di elaborazione alternativi abilitati da Hadoop 2. In particolare,
vedremo come elaborare dati in streaming in tempo reale con Apache Samza.
Il Capitolo 5, Computazione iterativa con Spark, entra nel merito di un
modello di elaborazione molto diverso. In questo capitolo parleremo dei mezzi
forniti da Apache Spark per effettuare lelaborazione iterativa.
Il Capitolo 6, Analisi dei dati con Apache Pig, mostra come Apache Pig
semplifichi luso del modello computazionale tradizionale di MapReduce
attraverso un linguaggio che descrive i flussi di dati.
Il Capitolo 7, Hadoop e SQL, analizza come il familiare linguaggio SQL
stato implementato sui dati salvati in Hadoop. Attraverso luso di Apache Hive e
la descrizione di alternative come Cloudera Impala, vedremo come rendere
possibile lelaborazione dei big data usando le competenze e gli strumenti
esistenti.
Il Capitolo 8, Gestione del ciclo di vita dei dati, d unocchiata generale a
come gestire tutti i dati che devono essere elaborati in Hadoop. Attraverso Apache
Oozie, illustreremo come costruire dei workflow per ottenere, elaborare e gestire i
dati.
Il Capitolo 9, Facilitare il lavoro di sviluppo, si concentra sulla scelta degli
strumenti che devono aiutare lo sviluppatore a raggiungere rapidamente dei
risultati. Attraverso Hadoop Streaming, Apache Crunch e Kite, vedremo come
luso dello strumento giusto pu velocizzare il ciclo di sviluppo o fornire nuove
API con una semantica pi ricca e meno ridondanze.
Il Capitolo 10, Eseguire un cluster Hadoop, considera il lato operativo di
Hadoop. Concentrandosi sugli ambiti di primo interesse degli sviluppatori, come
la gestione dei cluster, il monitoraggio e la sicurezza, questo capitolo vi aiuter a
lavorare meglio con il vostro staff operations.
Il Capitolo 11, Come proseguire, vi guida in un breve tour tra alcuni progetti e
strumenti che riteniamo utili ma che non possiamo trattare nel dettaglio per ragioni
di spazio. Vi forniremo anche alcune indicazioni su dove trovare altre informazioni
e come unirvi alle varie community open source.
Cosa serve per questo libro
Considerato che poche persone dispongono di una serie di computer di scorta,
useremo la macchina virtuale Cloudera QuickStart per la maggior parte degli
esempi del libro. unimmagine di una macchina su cui preinstallato un cluster
Hadoop completo. Pu essere eseguita su qualsiasi macchina host che supporta
VMware o la tecnologia di virtualizzazione VirtualBox.
Esploreremo anche la piattaforma Amazon Web Services (AWS) e vedremo
come eseguire alcune delle tecnologie Hadoop sul servizio AWS Elastic
MapReduce. I servizi AWS sono gestibili attraverso un browser web o
uninterfaccia Linux a riga di comando.
Lo scopo del libro
Questo libro rivolto perlopi a sviluppatori di sistemi e applicativi che
vogliono imparare a risolvere problemi pratici usando il framework Hadoop e i
relativi componenti. Nonostante mostreremo gli esempi in linguaggi di
programmazione diversi, il requisito fondamentale una conoscenza solida di
Java. Gli ingegneri e gli architetti dei dati troveranno utile il materiale riguardante
il ciclo di vita dei dati, i formati dei file e i modelli computazionali.
Convenzioni
In questo libro abbiamo applicato stili di testo diversi per distinguere i vari tipi
di informazioni. Ecco alcuni esempi con una spiegazione del loro significato.
Le parti del codice e i nomi dei file sono resi con un carattere monospaziato. Gli
elementi dellinterfaccia sono invece in corsivo, cos come i nomi delle directory
e le parole nuove o importanti.
Un blocco di codice appare cos:
topic_edges_grouped = FOREACH topic_edges_grouped {
GENERATE
group.topic_id as topic,
group.source_id as source,
topic_edges.(destination_id,w) as edges;
}

Gli input da riga di comando o loutput appaiono cos:


$ hdfs dfs -put target/elephant-bird-pig-4.5.jar hdfs:///jar/
$ hdfs dfs put target/elephant-bird-hadoop-compat-4.5.jar hdfs:///jar/
$ hdfs dfs put elephant-bird-core-4.5.jar hdfs:///jar/

NOTA
Note, suggerimenti e avvertimenti appaiono in questa forma.
Codice degli esempi
Il codice sorgente del libro si trova su GitHub allindirizzo https://github.com/
learninghadoop2/book-examples. Gli autori applicheranno le eventuali correzioni al

codice e lo manterranno aggiornato di pari passo con levoluzione della


tecnologia.
Gli autori
Garry Turkington da oltre 15 anni si dedica alla progettazione e
allimplementazione di sistemi distribuiti su larga scala. Nel suo ruolo attuale di
Chief Technology Officer di Improve Digital, responsabile soprattutto della
realizzazione di sistemi che memorizzano, elaborano ed estraggono valore dai
grandi volumi di dati aziendali. Prima di approdare a Improve Digital, ha lavorato
per Amazon.co.uk come leader di vari team di sviluppo software, creando sistemi
che elaborano i dati dellintero catalogo Amazon a livello globale. Prima ancora
ha lavorato per una decina danni per il governo sia nel Regno Unito sia negli Stati
Uniti.
Ha conseguito la laurea e il dottorato di ricerca in Scienze Informatiche presso
la Queens University di Belfast in Irlanda del Nord e un master in Ingegneria dei
Sistemi presso lo Stevens Institute of Technology. lautore di Hadoop Beginners
Guide, edito da Packt Publishing nel 2013, ed tra i committer del progetto
Apache Samza.
Desidero ringraziare mia moglie Lea e mia madre Sarah per il loro sostegno e la loro pazienza mentre
scrivevo questo libro, e mia figlia Maya per avermi fatto coraggio e avermi posto domande difficili.
Grazie anche a Gabriele, il fantastico co-autore di questo progetto.

Gabriele Modena un data scientist presso Improve Digital. Attualmente


utilizza Hadoop per gestire, elaborare e analizzare dati comportamentali generati
da macchine. Ama servirsi di metodi statistici e computazionali per individuare
dei pattern in grandi quantit i dati. In precedenza ha ricoperto diversi ruoli nel
campo accademico e industriale, compiendo ricerche sullapprendimento delle
macchine e lintelligenza artificiale.
Ha conseguito la laurea in Scienze Informatiche presso lUniversit di Trento, e
un dottorato di ricerca in Sistemi di Apprendimento nellIntelligenza Artificiale
allUniversit di Amsterdam.
Prima di tutto, e soprattutto, voglio ringraziare Laura per il suo sostegno, lincoraggiamento continuo e la
pazienza infinita nel dover affrontare tanti adesso non posso, devo lavorare sul libro di Hadoop. la
mia roccia, e dedico questo libro a lei.
Un grazie speciale ad Amit, Atdhe, Davide, Jakob, James e Valerie, i cui riscontri e i commenti
impareggiabili hanno reso possibile questo lavoro.
Infine, grazie al mio co-autore Garry per avermi preso a bordo. stato un piacere lavorare insieme.
I revisori
Atdhe Buja un hacker etico certificato, DBA (MCITP, OCA11g) e uno
sviluppatore con competenze gestionali. DBA presso lAgency for Information
Society/Ministero della Pubblica Amministrazione, dove gestisce anche alcuni
progetti di e-governance, e ha oltre dieci anni di esperienza nel lavoro su SQL
Server.
editorialista per UBT News. Ha conseguito un dottorato di ricerca in Scienze
e Ingegneria Informatiche e una laurea in Gestione delle Informazioni.
specializzato e certificato in molte tecnologie, come SQL Server (tutte le versioni),
Oracle 11g, CEH, Windows Server, MS Project, SCOM 2012 R2, BizTalk e nei
processi di integrazione business.
stato revisore del libro Microsoft SQL Server 2012 with Hadoop, edito da
Packt Publishing. E sa molto di pi di tutto questo!
Grazie a Donika e alla mia famiglia per lincoraggiamento e il sostegno.

Amit Gurdasani ingegnere software in Amazon. Struttura i sistemi distribuiti


per elaborare i dati del catalogo dei prodotti. In precedenza ha seguito tutta la
catena della progettazione software, sia come sviluppatore di sistemi in Ericsson e
in IBM, sia come sviluppatore di applicazioni presso Manhattan Associates. Le
sue passioni sono lelaborazione di grosse quantit di dati, lo streaming dei dati e
le architetture software orientate ai servizi.
Jakob Homan lavora da pi di cinque anni con i big data e lecosistema
Apache Hadoop. un committer di Hadoop, oltre che dei progetti Apache Giraph,
Spark, Kafka e Tajo, ed un membro di PMC. Ha lavorato sui progetti che hanno
scalato tutti questi sistemi in Yahoo! e LinkedIn.
James Lampton un professionista navigato per tutto quanto riguarda i dati
(big data o meno), con dieci anni di esperienza diretta nella costruzione e nelluso
delle piattaforme di elaborazione e storage dei dati su larga scala. un fautore
dellapproccio olistico nella risoluzione dei problemi usando lo strumento giusto
per il lavoro giusto. Tra i suoi strumenti preferiti ci sono Python, Java, Hadoop,
Pig, Storm e SQL. Ha appena conseguito un dottorato di ricerca presso
lUniversit del Maryland con il rilascio di Pig Squeal, un meccanismo per
lesecuzione di script Pig su Storm.
Voglio ringraziare mia moglie Andrea e mio figlio Henry per avermi dato il tempo di leggere a casa tutto
quello che mi sarebbe servito per questo libro. Grazie anche a Garry, Gabriele e alle persone di Packt
Publishing per avermi dato la possibilit di revisionare il manoscritto e per la loro pazienza e
comprensione nel permettermi di usare del tempo per scrivere la mia tesi di dottorato.

Davide Setti, dopo la laurea in Fisica presso lUniversit di Trento, si unito


ai ricercatori di SoNet presso la Fondazione Bruno Kessler nella stessa citt, dove
ha applicato le tecniche di analisi dei dati su vasta scala alla comprensione del
comportamento umano nei social network e in grandi progetti collaborativi come
Wikipedia.
Nel 2010, si trasferito alla Fondazione Kessler, dove ha condotto lo sviluppo
di strumenti di analisi dei dati per sostenere la ricerca sui media civici, sul
giornalismo partecipativo e sui media digitali.
Nel 2013, diventato Chief Technology Officer di SpazioDati, dove si occupa
dello sviluppo di strumenti per lanalisti semantica di grosse quantit di dati nel
settore dellinformazione aziendale.
Quando non risolve problemi complessi, ama prendersi cura della vigna di
famiglia e giocare con i suoi figli.
Capitolo 1
Per iniziare

Questo libro vi insegner a realizzare sistemi eccezionali utilizzando la


versione pi recente di Hadoop. Prima di cambiare il mondo, per, vi serviranno
un po di basi. In queste pagine introduttive affronteremo quanto segue.
Un breve ripasso su Hadoop.
Una panoramica sullevoluzione di Hadoop.
Gli elementi chiave di Hadoop 2.
Le distribuzioni Hadoop che utilizzeremo in questo libro.
Il dataset che impiegheremo per gli esempi.
Una nota sulle versioni
In Hadoop 1, la storia delle versioni piuttosto complessa, con pi diramazioni
nel range 0.2x che portano a situazioni insolite, in cui una versione 1.x potrebbe, in
alcune situazioni, avere meno funzioni di una 0.23. Nella base di codice della
versione 2 tutto molto pi diretto, per fortuna, ma importante chiarire
esattamente quale versione utilizzeremo in questo libro.
Hadoop 2.0 stato rilasciato nelle versioni alfa e beta, e nel tempo sono state
introdotte alcune modifiche non compatibili. In particolare, si assistito a uno
sforzo di stabilizzazione dellAPI principale tra la versione beta e la release
finale.
Hadoop 2.2.0 stata la prima release general availability (GA) della base di
codice di Hadoop 2, e le sue interfacce sono ormai dichiarate stabili e compatibili
per il futuro. In questo libro utilizzeremo quindi la versione e le interfacce 2.2.
Sebbene gli stessi principi siano applicabili a una 2.0 beta, ci saranno delle
incompatibilit con le API. La cosa particolarmente importante perch
MapReduce v2 stata oggetto di backporting su Hadoop 1 da parte di molti
produttori della versione, ma questi prodotti si basavano sulla beta e non sulle API
GA. Se utilizzate uno di questi prodotti, noterete lincompatibilit delle modifiche.
Consigliamo di utilizzare una release basata su Hadoop 2.2 o versioni successive
sia per lo sviluppo sia per la distribuzione di qualsiasi carico di lavoro di Hadoop
2.
Panoramica su Hadoop
In questo libro diamo per scontato che la maggior parte dei lettori abbia un
minimo di familiarit con Hadoop, o almeno con i sistemi di elaborazione dei dati.
Non spiegheremo quindi i dettagli del suo successo n affronteremo il tipo di
problemi che aiuta a risolvere. Considerati per alcuni degli aspetti di Hadoop 2 e
di altri prodotti che impiegheremo nei vari capitoli, utile dare unidea di come
Hadoop rientra nel panorama tecnologico e quali sono le aree problematiche
specifiche in cui pu essere vantaggioso utilizzarlo.
Una volta, prima che il termine big data facesse la sua comparsa (quindi pi o
meno dieci anni fa), erano poche le possibilit di elaborare dataset nellordine dei
terabyte e oltre. Alcuni database commerciali potevano essere scalati a questi
livelli con dei setup hardware molto specifici e costosi, ma le competenze e gli
investimenti necessari lo rendevano unopzione praticabile solo per le
organizzazioni pi grandi. In alternativa, si poteva costruire un sistema
personalizzato mirato al problema contingente, ma questo non eliminava gli
inconvenienti legati alle competenze e ai costi, senza considerare i rischi insiti in
ogni sistema allavanguardia. Daltra parte, se un sistema era ben costruito,
probabilmente si sarebbe adattato alla perfezione alle esigenze per cui era nato.
Alcune societ di piccole e medie dimensioni si preoccupavano per quanto
riguardava lo spazio, non solo perch le soluzioni non erano alla loro portata, ma
anche perch in genere i loro volumi di dati non raggiungevano i requisiti richiesti
da tali soluzioni. Con il crescere della capacit di generare grossi database,
cresceva anche la necessit di elaborare i dati.
La diffusione di grandi quantit di dati, non pi esclusiva di pochi, port con s
lesigenza di alcune modifiche rilevanti nellarchitettura dei sistemi di
elaborazione anche per le aziende pi piccole. La prima modifica importante fu la
riduzione dellinvestimento anticipato di capitale sul sistema, quindi niente
hardware di alto livello n costose licenze software. In precedenza, si sarebbero
utilizzati hardware high-end in un numero relativamente piccolo di server e sistemi
di storage molto grandi, ciascuno dei quali aveva approcci diversi per evitare i
crash. Per quanto impressionanti, questi sistemi erano costosissimi, e spostarsi
verso un numero pi esteso di server di livello pi basso sarebbe stato il modo
pi rapido per ridurre drasticamente il costo dellhardware di un nuovo sistema. Il
passaggio a un hardware di base invece che a unattrezzatura aziendale
tradizionale avrebbe anche comportato una riduzione delle capacit in termini di
recupero e tolleranza ai guasti, responsabilit che sarebbero passate al livello
software. (Software pi intelligente, hardware pi sciocco.)
Google diede via al cambiamento che sarebbe poi diventato noto come Hadoop
quando, nel 2003 e nel 2004, rilasci due documenti accademici che descrivevano
il Google File System (GFS) (http://research.google.com/archive/gfs.html) e
MapReduce (http://research.google.com/archive/mapreduce.html). I due documenti
fornivano una piattaforma per lelaborazione dei dati su larga scala in un modo
eccezionalmente efficiente. Google aveva seguito lapproccio fai da te, ma
invece di costruire qualcosa di mirato alla risoluzione di un problema specifico o
di un determinato dataset, aveva creato una piattaforma sulla quale potevano
essere implementate pi applicazioni di elaborazione. In particolare, utilizzava
numerosi server di base e costruiva il GFS e MapReduce in modo che
presumessero che i guasti dellhardware fossero comuni e quindi qualcosa con cui
il software avrebbe avuto spesso a che fare.
Nello stesso tempo, Doug Cutting stava lavorando sul crawler web open source
chiamato Nutch, e in particolare su alcuni elementi nel sistema che ricoprirono una
notevole rilevanza nel momento in cui i documenti su GFS e MapReduce furono
pubblicati. Doug aveva iniziato a lavorare sulle implementazioni open source
delle idee di Google, e presto nacque Hadoop, inizialmente un progetto derivato di
Lucene e poi un progetto di alto livello indipendente sotto legida dellApache
Software Foundation.
Yahoo! assunse Doug Cutting nel 2006 e divenne rapidamente tra i primi
sostenitori del progetto Hadoop. Oltre a pubblicizzare in tutto il mondo alcune
delle pi grandi distribuzioni Hadoop, Yahoo! consent a Doug e ad altri ingegneri
di contribuire ad Hadoop durante il periodo del loro impiego, per non parlare di
tutti i miglioramenti e le estensioni ad Hadoop sviluppati internamente.
Componenti di Hadoop
Hadoop costituito da una serie di progetti secondari, molti dei quali verranno
affrontati nel corso del libro. Di base, Hadoop fornisce due servizi: lo storage e il
calcolo. Un flusso di lavoro tipico di Hadoop implica il caricamento dei dati
nellHadoop Distributed File System (HDFS) e la loro elaborazione tramite lAPI
MapReduce o i numerosi strumenti che si basano su MapReduce come framework
di esecuzione.
Entrambi i livelli sono implementazioni dirette delle tecnologie GFS e
MapReduce di Google.

Figura 1.1 Hadoop 1: HDFS e MapReduce.

Componenti comuni
Sia HDFS sia MapReduce adottano molti dei principi architetturali descritti nel
paragrafo precedente, e in particolare quelli che seguono.
Entrambi sono concepiti per lesecuzione su cluster di server di base (cio
con specifiche da medie a basse).
Entrambi scalano la loro capacit aggiungendo altri server (scale-out)
rispetto allabitudine precedente di utilizzare un hardware pi grande (scale-
up).
Entrambi hanno meccanismi per identificare e risolvere i problemi.
Entrambi forniscono la maggior parte dei loro servizi in modo trasparente,
consentendo allutente di concentrarsi sul problema del momento.
Entrambi hanno unarchitettura in cui un cluster software risiede sui server
fisici e gestisce aspetti come il bilanciamento del carico di unapplicazione e
la tolleranza ai guasti, senza affidarsi allhardware high-end per applicare
queste capacit.

Storage
HDFS un file system, sebbene non compatibile con POSIX. Questo significa
che non ha le stesse caratteristiche di un file system ordinario ma altre peculiarit.
Salva i file in blocchi di almeno 64 MB o, ancora pi spesso, di 128 MB,
dimensioni ben superiori ai 4-32 KB della maggior parte dei file system.
ottimizzato per il throughput a sfavore della latenza; molto efficiente
nella lettura in streaming di file molto grossi ma scadente quando si tratta di
cercarne di piccoli.
ottimizzato per carichi di lavoro del tipo scrivi una volta e leggi pi
volte.
Invece di gestire i guasti del disco tramite le ridondanze fisiche nelle serie di
dischi o strategie analoghe, HDFS utilizza la replica. Ciascuno dei blocchi
che costituisce un file viene salvato su pi nodi nel cluster, e il servizio
chiamato NameNode li monitora costantemente per garantire che gli eventuali
errori o problemi non abbiano cancellato qualche blocco al di sotto del
fattore di replica desiderato. Se accade, NameNode programma la creazione
di unaltra copia allinterno del cluster.

Calcolo
MapReduce unAPI, un motore di esecuzione e un paradigma; rende possibile
una serie di trasformazioni da una sorgente in un dataset di risultati. Nel caso pi
semplice, i dati di input vengono immessi tramite una funzione map, mentre i dati
temporanei risultanti vengono forniti attraverso una funzione reduce.
MapReduce lavora al meglio su dati non strutturati o semi-strutturati. Non serve
che i dati siano conformi a schemi rigidi; il requisito che possano essere forniti
alla funzione map come una serie di coppie chiave-valore. Loutput della funzione
map un set di altre coppie chiave-valore, mentre la funzione reduce esegue

laggregazione per assemblare il set finale di risultati.


Hadoop offre una specifica (cio uninterfaccia) per le fasi di map e reduce, la cui
implementazione in genere chiamata mapper e reducer. Una tipica applicazione
MapReduce comprender un certo numero di mapper e reducer, e non insolito
che diversi di questi siano molto semplici. Lo sviluppatore si focalizza sulla
trasformazione tra i dati sorgente e i dati risultanti, mentre il framework di Hadoop
gestisce tutti gli aspetti dellesecuzione e del coordinamento del lavoro.

Meglio se insieme
HDFS e MapReduce possono essere utilizzati singolarmente, ma quando
lavorano insieme fanno emergere il meglio luno dellaltro, e questa interrelazione
stato il fattore principale del successo e delladozione di Hadoop 1.
Quando si progetta un job di MapReduce, Hadoop deve decidere su quale host
eseguire il codice per poter elaborare il dataset nel modo pi efficiente. Non conta
molto se gli host dei cluster di MapReduce traggono i loro dati da un unico host o
array di storage, perch il sistema una risorsa condivisa. Se il sistema di storage
fosse pi trasparente e consentisse a MapReduce di manipolare i suoi dati pi
direttamente, ci sarebbe lopportunit di eseguire lelaborazione dei dati pi da
vicino, in base al principio secondo cui meno costoso spostare lelaborazione
che spostare i dati.
Il modello pi comune di Hadoop vede la distribuzione dei cluster HDFS e
MapReduce sullo stesso gruppo di server. Ogni host che contiene i dati e il
componente HDFS che li gestisce ospita anche un componente MapReduce che
pu programmare ed eseguire lelaborazione. Quando un job viene inviato ad
Hadoop, questo pu utilizzare lottimizzazione della posizione per programmare il
pi possibile i dati sugli host in cui risiedono i dati, riducendo cos il traffico di
rete e ottimizzando le prestazioni.
Hadoop 2: dov laffare?
Se consideriamo i due componenti principali della distribuzione Hadoop, lo
storage e il calcolo, vediamo che Hadoop 2 ha un impatto diverso su ciascuno di
essi. Laddove lHDFS in Hadoop 2 un prodotto pi ricco di funzionalit e pi
resiliente di quello in Hadoop 1, le modifiche per MapReduce sono pi profonde,
e hanno cambiato di fatto il modo in cui Hadoop viene percepito come piattaforma
di elaborazione in generale. Vediamo prima HDFS in Hadoop 2.

Storage in Hadoop 2
Discuteremo larchitettura HDFS nel dettaglio nel Capitolo 2; per ora
sufficiente pensare a un modello master-slave. I nodi slave (i DataNode)
contengono i dati veri e propri del file system. In particolare, ogni host che esegue
un DataNode ha solitamente uno o pi dischi su cui sono scritti i file che
contengono i dati per ogni blocco HDFS. Il DataNode di per s non sa nulla del
file system globale; il suo ruolo quello di memorizzare, servire ed assicurare
lintegrit dei dati di cui responsabile.
Il nodo master (il NameNode) deve sapere quale dei DataNode contiene un
determinato blocco e come quei blocchi sono strutturati a formare il file system.
Quando un client considera il file system per recuperare un file, attraverso una
richiesta al NameNode che ottiene lelenco dei blocchi richiesti.
Questo modello funziona bene ed stato scalato su cluster con decine di
migliaia di nodi in realt come quella di Yahoo!. Per quanto scalabile, tuttavia, c
un rischio di resilienza; se il NameNode diventa non disponibile, allora lintero
cluster diventa inutile. Nessuna operazione HDFS pu essere svolta, e poich la
maggioranza delle installazioni usa HDFS come livello di storage dei servizi,
come MapReduce, anche questi diventano non disponibili, anche se sono in piena
esecuzione senza problemi.
Ancora peggio, il NameNode memorizza i metadati del file system in un file
persistente sul suo file system locale. Se lhost del NameNode va in crash in un
modo tale per cui i dati non sono recuperabili, allora tutti i dati sul cluster sono
irrimediabilmente perduti. Continueranno a esistere sui vari DataNode, ma la
mappatura di quali blocchi contenevano quali file non pi disponibile. Ecco
perch in Hadoop 1 la best practice era quella che il NameNode scrivesse i dati
del suo file system contemporaneamente sui dischi locali e almeno su un volume di
rete remoto (solitamente tramite NFS).
Alcuni produttori di terze parti offrono diverse soluzioni high-availability (HA)
di NameNode, ma il prodotto Hadoop centrale non fornisce questa resilienza nella
Versione 1. Considerati il singolo punto di fallimento architetturale e il rischio di
perdita dei dati, non sar una sorpresa scoprire che la NameNode HA una delle
funzioni principali di HDFS in Hadoop 2, come vedremo nei prossimi capitoli.
Questa caratteristica offre un NameNode in standby che pu essere promosso
automaticamente a soddisfare tutte le richieste qualora il NameNode attivo
fallisse, e garantisce unulteriore resilienza per i dati critici del file system.
HDFS in Hadoop 2 un file system non ancora compatibile con POSIX; ha una
dimensione di blocchi molto grande e baratta ancora la latenza con il throughput.
Tuttavia, ha ora alcune capacit che possono farlo assomigliare di pi a un file
system tradizionale. In particolare, lHDFS core in Hadoop 2 pu essere montato
da remoto su un volume NFS, unaltra funzione che era prima offerta come
proprietaria da fornitori di terzi parti ma che ora parte integrante della base di
codice principale di Apache.
Complessivamente, lHDFS in Hadoop 2 pi resiliente e pu essere integrato
pi facilmente nei processi e nei flussi di lavoro esistenti. una grande evoluzione
del prodotto che era in Hadoop 1.

Calcolo in Hadoop 2
Il lavoro su HDFS 2 iniziato prima che la direzione di MapReduce fosse
stabilita definitivamente. Questo soprattutto perch funzioni come la NameNode
HA erano una strada talmente ovvia che la community conosceva gi gli ambiti pi
critici da affrontare. Tuttavia, MapReduce non contemplava altrettante aree di
miglioramento, ed ecco perch non fu subito chiaro lo scopo di uniniziativa come
quella di MRv2.
Lobiezione principale a MapReduce in Hadoop 1 riguardava il fatto che il suo
modello di elaborazione in batch mal si adattava ai domini problematici in cui
erano necessari tempi di risposta pi rapidi. Hive, per esempio, che vedremo nel
Capitolo 7, fornisce uninterfaccia del tipo SQL sui dati HDFS, ma dietro le quinte
le istruzioni vengono convertite in job di MapReduce che vengono poi eseguiti
come tutti gli altri. Altri prodotti o strumenti adottavano un approccio simile,
fornendo uninterfaccia utente specifica che nascondeva il livello di traduzione di
MapReduce.
Sebbene questo approccio ebbe successo, e nonostante la realizzazione di
prodotti notevoli, rimane il fatto che il pi delle volte c una discrepanza, poich
tutte queste interfacce, alcune delle quali si aspettano un certo tipo di reattivit,
dietro le quinte vengono eseguite su una piattaforma di elaborazione in batch. E
tali discrepanze rimanevano anche se potevano essere apportati a MapReduce dei
miglioramenti a favore di una corrispondenza pi precisa. Questa situazione port
a un cambiamento significativo del focus delliniziativa MRv2. Forse non era
MapReduce ad aver bisogno di modifiche; la vera necessit era quella di
consentire diversi modelli di elaborazione sulla piattaforma Hadoop. Fu cos che
nacque Yet Another Resource Negotiator (YARN).
MapReduce in Hadoop 1 faceva due cose piuttosto diverse: forniva il
framework di elaborazione per eseguire i calcoli di MapReduce, ma gestiva anche
lallocazione della computazione sul cluster. Non solo indirizzava i dati a e tra
operazioni specifiche di map e reduce, ma determinava anche dove ogni attivit
sarebbe stata eseguita, e gestiva lintero ciclo di vita del job, monitorando la
salute di ogni attivit e nodo, riprogrammando in caso di fallimenti e cos via.
Non unoperazione banale, e la parallellizzazione automatizzata dei carichi di
lavoro sempre stato uno dei vantaggi di Hadoop. Se consideriamo MapReduce in
Hadoop 1, dopo che lutente definisce i criteri chiave per il job, qualsiasi altra
cosa di responsabilit del sistema. Da un punto di vista della scala, lo stesso job
di MapReduce pu essere applicato a dataset di qualsiasi volume sui cluster di
qualsiasi dimensione. Se abbiamo 1 GB di dati su un unico host, allora Hadoop
programmer lelaborazione di conseguenza, e far lo stesso anche se abbiamo 1
PB di dati su mille macchine. Dal punto di vista dellutente, la scala effettiva dei
dati e dei cluster trasparente, e al di l del tempo necessario a elaborare il job,
linterfaccia con cui si interagisce con il sistema non cambia.
In Hadoop 2, il ruolo di programmazione dei job e di gestione delle risorse
separato da quello dellesecuzione dellapplicazione vera e propria, ed svolto
da YARN.
YARN responsabile della gestione delle risorse del cluster, quindi
MapReduce esiste in quanto applicazione che gira sul framework di YARN. In
Hadoop 2 linterfaccia di MapReduce completamente compatibile con quella in
Hadoop 1, sia semanticamente sia praticamente. Tuttavia, dietro le quinte,
MapReduce diventata unapplicazione ospitata sul framework YARN.
Il senso di questa discrepanza che possono essere scritte altre applicazioni
che forniscono modelli di elaborazione centrati sul problema contingente
scaricando al contempo su YARN le responsabilit di gestione delle risorse e di
programmazione. Le versioni pi recenti di molti motori di esecuzione sono state
portate su YARN, sia in uno stato pronto per la produzione sia sperimentale; tale
approccio permette che un singolo cluster Hadoop esegua tutto, dai job di
MapReduce orientati al batch attraverso query SQL a risposta rapida fino a stream
di dati continui, oltre a implementare modelli come lelaborazione dei grafici e la
Message Passing Interface (MPI) del mondo dellHigh Performance Computing
(HPC). La Figura 1.2 mostra larchitettura di Hadoop 2.
Figura 1.2 Hadoop 2.

Ecco perch gran parte dellattenzione e dellentusiasmo su Hadoop 2 si


concentrata su YARN e sui framework che vi risiedono, come Apache Tez e
Apache Spark. Con YARN, il cluster Hadoop non pi solo un motore di
elaborazione in batch; una singola piattaforma sulla quale possono essere
applicate varie tecniche di elaborazione alle enormi quantit di dati salvate in
HDFS. Inoltre le applicazioni possono essere costruite su questi paradigmi
computazionali e modelli di esecuzione. Si pu pensare a YARN come al kernel di
elaborazione su cui possono essere costruite applicazioni specifiche. Affronteremo
YARN nel dettaglio nei Capitoli 3, 4 e 5.
Distribuzioni di Apache Hadoop
Agli albori di Hadoop, il peso dellinstallazione (spesso dalla sorgente) e la
gestione di ogni componente e delle sue dipendenze ricadevano sullutente. Con la
diffusione del sistema e dellecosistema degli strumenti e delle librerie di terze
parti, la complessit dellinstallazione e della gestione di una distribuzione
Hadoop aument drasticamente, fino al punto che fornire unofferta coerente di
package software, documentazione e formazione attorno allApache Hadoop core
diventato un modello di business. Entriamo allora nel mondo delle distribuzioni
per Apache Hadoop.
Le distribuzioni Hadoop sono concettualmente simili al modo in cui le
distribuzioni Linux forniscono un set di software integrato attorno a un core
comune. Si accollano il compito di assemblare e raccogliere il software e di
fornire allutente una modalit per installare, gestire e distribuire Apache Hadoop
e un numero selezionato di librerie di terze parti. In particolare, le release
forniscono una serie di versioni del prodotto che sono certificate come mutuamente
compatibili. Storicamente, assemblare una piattaforma basata su Hadoop era
unoperazione resa complessa dalle varie interdipendenze delle versioni.
Cloudera (http://www.cloudera.com), Hortonworks (http://www.hortonworks.com) e MapR
(http://www.mapr.com) sono tra le prime ad aver raggiunto il mercato, ognuna con
approcci e punti di vendita specifici. Hortonworks si posiziona come player open
source; anche Cloudera rivolta allopen source ma aggiunge elementi proprietari
per la configurazione e la gestione di Hadoop; MapR fornisce una distribuzione
Hadoop ibrida open source/proprietaria caratterizzata da un livello NFS
proprietario invece che HDFS e un focus sulla fornitura di servizi.
Un altro player importante nellecosistema distribuito Amazon, che offre una
versione di Hadoop chiamata Elastic MapReduce (EMR) sullinfrastruttura
Amazon Web Services (AWS).
Con lavvento di Hadoop 2, il numero di distribuzioni disponibili per Hadoop
aumentato esponenzialmente, ben oltre le quattro che abbiamo citato. Un elenco
non completo delle offerte software che include Apache Hadoop si trova
allindirizzo http://bit.ly/1MnahAV.
Un doppio approccio
In questo libro, tratteremo la costruzione e la gestione di cluster Hadoop locali e
illustreremo come portare lelaborazione sul cloud attraverso EMR.
La ragione duplice: sebbene EMR renda Hadoop molto pi accessibile, ci
sono aspetti della tecnologia che diventano palesi solo con lamministrazione
manuale del cluster. Per quanto sia possibile utilizzare EMR in un modo pi
manuale, in genere per tali esplorazioni si utilizza un cluster locale. In secondo
luogo, molte organizzazioni usano un insieme di capacit a met tra lin-house e il
cloud, a volte per il timore di affidarsi a un unico provider esterno, anche se, in
termini pratici, spesso conveniente sviluppare e testare su piccola scala la
capacit locale e poi distribuire il prodotto su vasta scala sul cloud.
In uno degli ultimi capitoli in cui vedremo altri prodotti che si integrano con
Hadoop, mostreremo alcuni esempi di cluster locali e vedremo che, a prescindere
da dove vengono distribuiti, non c differenza tra come i vari prodotti lavorano.
AWS: infrastruttura on demand di
Amazon
AWS un set di servizi di cloud computing offerto da Amazon. Nel libro ne
utilizzeremo molti.

Simple Storage Service (S3)


Simple Storage Service (S3) di Amazon ( http://aws.amazon.com/s3/) un servizio
di storage che fornisce un semplice modello di memorizzazione chiave-valore.
Usando interfacce web, a riga di comando o di programma per creare oggetti da
file di testo, a immagini, a MP3 , potete memorizzare e recuperare i dati in base a
un modello gerarchico in cui create dei bucket che contengono gli oggetti. Ogni
bucket ha un identificatore unico, e allinterno di ciascun bucket ogni oggetto ha un
nome univoco. Questa strategia elementare abilita un servizio potentissimo di cui
Amazon si assume la totale responsabilit (per scalare il servizio e per
laffidabilit e disponibilit dei dati).

Elastic MapReduce (EMR)


Elastic MapReduce di Amazon ( http://aws.amazon.com/elasticmapreduce/) non altro
che Hadoop sul cloud. Usando una qualsiasi delle varie interfacce (console web,
riga di comando o API), viene definito un flusso di lavoro Hadoop con attributi
come il numero di host Hadoop richiesti e la posizione dei dati sorgente. Viene
fornito il codice Hadoop che implementa i job di MapReduce, e viene premuto il
pulsante virtuale Vai.
Nella sua modalit pi potente, EMR pu trarre i dati sorgente da S3, elaborarli
su un cluster Hadoop che crea sul servizio di host virtuale on demand EC2 di
Amazon, riportare i dati in S3 e terminare il cluster Hadoop e le macchine virtuali
EC2 che lo ospitano. Ovviamente ognuno di questi servizi ha un costo (solitamente
in base ai GB memorizzati e al tempo di utilizzo del server), ma la capacit di
accedere a queste funzionalit cos elevate di elaborazione dei dati senza che
occorra un hardware dedicato non da trascurare.
Come iniziare
Descriveremo ora i due ambienti che utilizzeremo nel libro. La macchina
virtuale di QuickStart Cloudera sar il nostro punto di riferimento su cui
mostreremo tutti gli esempi; tuttavia, alcuni casi particolarmente interessanti che
vale la pena eseguire su un servizio on demand li illustreremo su EMR di Amazon
Sebbene il codice e gli esempi forniti siamo il pi possibile generici e
portabili, quando si tratta di cluster locali, la nostra configurazione di riferimento
sar quella di Cloudera eseguita su CentOS Linux.
La maggior parte delle volte ci rifaremo a esempi che utilizzano o che vengono
eseguiti dal prompt del terminale. Per quanto le interfacce grafiche di Hadoop
siano molto migliorate negli anni (vedi per esempio, gli ottimi HUE e Cloudera
Manager), quando si tratta di sviluppo, automazione e accesso programmatico al
sistema, la riga di comando rimane ancora lo strumento pi potente per lavorare.
Tutti gli esempi e il codice sorgente presentati in questo libro possono essere
scaricati allindirizzo https://github.com/learninghadoop2/book-examples. Inoltre
disponibile un piccolo sito web (in inglese) dedicato a questo libro dove trovare
aggiornamenti e materiale correlato: lindirizzo http://learninghadoop2.com.

Cloudera QuickStart VM
Uno dei vantaggi delle distribuzioni Hadoop che consentono laccesso a
package software facili da installare. Cloudera va anche oltre, e fornisce
unistanza di Virtual Machine scaricabile gratuitamente, nota come CDH
QuickStart VM, distribuita su CentOS Linux.
Nel resto del libro utilizzeremo la CDH5.0.0 VM come riferimento e come
sistema di base per eseguire gli esempi e il codice sorgente disponibile per i
sistemi di virtualizzazione VMware (http://www.vmware.com/nl/products/player/), KVM
(http://www.linux-kvm.org/page/Main_Page) e VirtualBox (https://www.virtualbox.org/).

Amazon EMR
Prima di utilizzare Elastic MapReduce, dobbiamo impostare un account AWS e
registrarci per i servizi necessari.
Creare un account AWS
Amazon ha integrato i suoi account generali con AWS; se quindi avete gi un
account per uno qualsiasi dei siti di vendita online di Amazon, lo utilizzerete anche
per i servizi AWS.
NOTA
I servizi AWS hanno un costo; dovrete quindi aver associata allaccount una carta di credito
attiva su cui possano essere effettuati gli addebiti.

Se richiedete un nuovo account Amazon, andate su http://aws.amazon.com,


selezionate Create a new AWS account e seguite le istruzioni. Amazon ha aggiunto
un livello gratuito (Free Tier) per alcuni servizi, quindi nei primi giorni di prova
ed esplorazione le vostre attivit rientreranno in questa versione. Lambito del
livello gratuito si sta ampliando, quindi verificate quello che volete, e non vi
faranno pagare niente.

Sottoscrivere i servizi necessari


Una volta ottenuto un account Amazon, dovrete registrarlo per poterlo utilizzare
con i servizi AWS necessari, cio Simple Storage Service (S3), Elastic Compute
Cloud (EC2) ed Elastic MapReduce. Ladesione gratuita; la procedura serve
solo per rendere disponibile i servizi al vostro account.
Aprite le pagine di S3, EC2 ed EMR da http://aws.amazon.com, fate clic sul
pulsante Sign up in ogni pagina e seguite le istruzioni.

Utilizzare Elastic MapReduce


Una volta creato un account con AWS e dopo aver sottoscritto i servizi
necessari, possiamo procedere a configurare laccesso programmatico a EMR.

Rendere Hadoop operativo


AT T ENZIONE
Costa soldi veri!

Prima di proseguire, fondamentale tenere presente che luso dei servizi AWS
implica il pagamento di una tariffa che avverr addebitata sulla carta di credito
associata allaccount di Amazon. In genere le cifre sono basse, ma aumentano con
laumentare dellentit dellinfrastruttura consumata; lo storage di 10 GB di dati in
S3 costa dieci volte pi di 1 GB, ed eseguire 20 istanze di EC2 costa venti volte
una sola istanza. Va poi considerato che i costi effettivi tendono a subire degli
aumenti marginali pi piccoli a livelli pi elevati. In ogni caso, prima di utilizzare
un servizio, leggete con attenzione le sezioni riguardanti i prezzi. Considerate
anche che i dati che vengono trasferiti allesterno dei servizi AWS, come C2 e S3,
sono addebitabili, mentre i trasferimenti tra servizi non lo sono. Questo significa
che spesso pi conveniente progettare luso degli AWS in modo da mantenere i
dati al loro interno per la maggior parte dellelaborazione. Per informazioni su
AWS ed EMR, consultate la pagina http://aws.amazon.com/elasticmapreduce/#pricing.

Come utilizzare EMR


Amazon fornisce interfacce sia web sia a riga di comando per EMR. Entrambi i
tipi di interfaccia sono solo un front-end del sistema vero e proprio; un cluster
creato da riga di comando pu essere esplorato e gestito con gli strumenti web e
viceversa.
In genere utilizzeremo strumenti a riga di comando per creare e manipolare i
cluster in modo programmatico, mentre torneremo allinterfaccia web quando ha
senso farlo.

Credenziali AWS
Prima di utilizzare gli strumenti programmatici o a riga di comando, dovremo
capire come il possessore di un account si autentica sugli AWS per le richieste.
Ogni account AWS ha numerosi identificatori, come quelli elencati di seguito,
che vengono utilizzati quando si accede ai vari servizi.
ID dellaccount: ogni account AWS ha un ID numerico.
Chiave di accesso: la chiave di accesso associata viene usata per identificare
laccount che effettua la richiesta.
Chiave di accesso segreta: fa il paio con la chiave di accesso. La chiave di
accesso normale non segreta e pu essere esposta nelle richieste, mentre
quella segreta quella che utilizzate per validarvi come possessori
dellaccount. Trattatela con la stessa cura con cui trattate la vostra carta di
credito.
Coppie di chiavi: sono utilizzate per il login agli host EC2. possibile
generare coppie di chiavi pubbliche/private in EC2 o importare nel sistema
le chiavi generate allesterno.
Le credenziali e i permessi degli utenti sono gestiti tramite un servizio web
chiamato Identity and Access Management (IAM), che dovrete sottoscrivere per
poter ottenere le chiavi di accesso e segreta.
Sembra tutto un po confuso, e lo , almeno allinizio. Quando si usa uno
strumento per accedere a un servizio AWS, solitamente viene subito richiesto di
aggiungere le credenziali corrette a un file configurato, dopodich tutto funziona.
Se per decidete di esplorare gli strumenti programmatici o a riga di comando,
vale la pena investire un po di tempo per leggere la documentazione relativa a
ciascun servizio per capire come funziona sotto laspetto della sicurezza. Trovate
ulteriori informazioni sulla creazione di un account AWS e sullottenimento delle
credenziali di accesso alla pagina http://docs.aws.amazon.com/iam.

Linterfaccia AWS a riga di comando


Ogni servizio AWS ha da sempre il proprio set di strumenti a riga di comando.
Tuttavia, di recente, Amazon ha creato un unico strumento unificato che consente
laccesso alla maggior parte dei servizi, lAmazon CLI (Command Line
Interface), che si trova allindirizzo http://aws.amazon.com/cli.
Pu essere installata da un tarball o tramite i package manager pip o easy_install.
Sulla CDH QuickStart VM, possiamo installare awscli usando il seguente
comando:
$ pip install awscli

Per accedere allAPI, dobbiamo configurare il software per autenticarci per gli
AWS usando le nostre chiavi di accesso e segreta.
anche il momento giusto per impostare una copia di chiavi EC2 seguendo le
istruzioni fornite allindirizzo https://console.aws.amazon.com/ec2/home?region=us-east-
1#c=EC2&s=KeyPairs. Per quanto una coppia di chiavi non sia strettamente necessaria

per eseguire un cluster EMR, ci da l possibilit di effettuare un login da remoto al


nodo master e di ottenere un accesso di basso livello al cluster.
Il prossimo comando ci guider attraverso una serie di passi di configurazione e
poi di salvataggio della configurazione definitiva nel file .aws/credential:
$ aws configure

Una volta impostata la CLI, possiamo interrogare AWS con aws <service>
<arguments>. Per creare e interrogare un bucket S3 usate un comando come quello

che segue. Notate che i bucket S3 devono essere univoci tra tutti gli account AWS,
quindi i nomi pi comuni come s3://mybucket non saranno disponibili:
$ aws s3 mb s3://learninghadoop2
$ aws s3 ls

Possiamo dotare un cluster EMR di cinque nodi m1.xlarge usando i comandi


seguenti:
$ aws emr create-cluster --name EMR cluster \
--ami-version 3.2.0 \
--instance-type m1.xlarge \
--instance-count 5 \
--log-uri s3://learninghadoop2/emr-logs

Qui --ami-version lID di un template Amazon Machine Image


(http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/AMIs.html) e --log-uri dice a EMR di
raccogliere i log e di memorizzarli nel bucket S3 learninghadoop2.
NOTA
Se non avete specificato una regione predefinita quando avete impostato lAWS CLI, dovrete
aggiungerne una alla maggior parte dei comandi EMR nella CLI usando largomento --region.
Per esempio, region eu-west-1 relativo allarea dellIrlanda nellUnione Europea. Trovate
dettagli sulle regioni AWS disponibili allindirizzo
http://docs.aws.amazon.com/general/latest/gr/rande.html.

Possiamo inviare i flussi di lavoro aggiungendo dei passi a un cluster in


esecuzione tramite questo comando:
$ aws emr add-steps --cluster-id <cluster> --steps <steps>

Per terminare il cluster, usate questa riga di comando:


$ aws emr terminate-clusters --cluster-id <cluster>

Negli ultimi capitoli vi mostreremo come aggiungere dei passi per eseguire job
di MapReduce e script Pig.
Trovate altre informazioni sullAWS CLI alla pagina
http://docs.aws.amazon.com/ElasticMapReduce/latest/DeveloperGuide/emr-manage.html.
Eseguire gli esempi
Allindirizzo https://github.com/learninghadoop2/book-examples trovate il codice di tutti
gli esempi. Vengono forniti gli script e le configurazioni Gradle
(http://www.gradle.org/) per compilare la maggior parte del codice Java. Lo script
gradlew incluso nellesempio caricher Gradle e lo utilizzer per recuperare le

dipendenze e il codice di compilazione.


I file JAR possono essere creati invocando lattivit jar tramite uno script
gradlew, cos:

./gradlew jar

I job vengono solitamente eseguiti inviando un file JAR usando il comando


hadoop jar:

$ hadoop jar example.jar <MainClass> [-libjars $LIBJARS] arg1 arg2 argN

Il parametro facoltativo -libjars specifica le dipendenze di runtime di terze parti


da inviare ai nodi remoti.
NOTA
Alcuni dei framework con cui lavoreremo, come Apache Spark, hanno strumenti propri di build
e di gestione dei package. Informazioni e risorse specifiche verranno segnalate per questi casi
particolari.

Lattivit copyJar Gradle pu essere usata per scaricare le dipendenze di terze


parti in build/libjars/<example>/lib, come segue:
./gradlew copyJar

Per comodit, forniamo unattivit fatJar Gradle che accorpa le classi di


esempio e le loro dipendenze in un unico file JAR. Sebbene questo approccio sia
sconsigliato a favore delluso di libjar, pu diventare comodo quando si devono
gestire le questioni legate alle dipendenze.
Il prossimo comando genera build/libs/<example>-all.jar:
$ ./gradlew fatJar
Elaborazione dei dati con Hadoop
Nei prossimi capitoli del libro presenteremo i componenti principali
dellecosistema di Hadoop, oltre ad alcuni strumenti e librerie di terze parti che
renderanno la scrittura di codice robusto e distribuito unattivit accessibile e
divertente. Leggendo, imparerete a raccogliere, elaborare, memorizzare ed estrarre
informazioni da grandi quantit di dati strutturati o meno.
Ci serviremo di un dataset generato dal firehose in tempo reale di Twitter
(http://www.twitter.com). Questo approccio ci permetter di fare qualche esperimento
locale con dataset relativamente piccoli e, una volta pronti, di scalare gli esempi
verso lalto a un livello di produzione.

Perch Twitter?
Grazie alle sue API programmatiche, Twitter fornisce un modo semplice per
generare dataset di dimensioni arbitrarie e per immetterli nei nostri cluster
Hadoop locali o sul cloud. Il dataset che utilizzeremo avr alcune propriet che si
adattano a numerosi casi di modellazione ed elaborazione dei dati.
I dati di Twitter hanno le seguenti propriet.
Non sono strutturati: ogni aggiornamento dello stato un messaggio testuale
che pu contenere riferimenti a un contenuto multimediale, come URL e
immagini.
Sono anche strutturati: i tweet sono record consecutivi con una data e unora.
Sono rappresentabili graficamente: le relazioni come le risposte e le
menzioni possono essere modellate come una rete di interazioni.
Sono gelolocalizzabili: si conosce la posizione da cui un tweet stato inviato
o dove un utente risiede.
Sono in tempo reale: tutti i dati generati su Twitter sono disponibili attraverso
un flusso in tempo reale (firehose).
Queste propriet si rifletteranno nel tipo di applicazione che possiamo costruire
con Hadoop, e includono esempi di sentiment e trend analysis e social network.
Creare il primo dataset
Le condizioni duso di Twitter vietano la ridistribuzione dei dati generati
dallutente in qualsiasi forma; per questa ragione, non possiamo rendere
disponibile un dataset comune. Utilizzeremo allora uno script di Python per
accedere in modo programmatico alla piattaforma e creare un deposito di tweet
degli utenti raccolti da uno stream live.

Un servizio, pi API
Gli utenti di Twitter condividono oltre 200 milioni di tweet al giorno, noti anche
come aggiornamenti dello stato. La piattaforma offre laccesso a questo corpus di
dati attraverso quattro tipi di API, ciascuna delle quali rappresenta una
sfaccettatura di Twitter e mira a soddisfare casi duso specifici, come il
collegamento e linterazione con il contenuto di Twitter da fonti di terze parti (Per
prodotto), laccesso programmatico al contenuto di utenti o siti specifici (REST),
le funzionalit di ricerca tra le timeline di utenti o siti (Cerca) e laccesso a tutto il
contenuto creato sulla rete di Twitter in tempo reale (API Streaming).
LAPI Streaming consente un accesso diretto allo stream di Twitter, tenendo
traccia delle parole chiave, recuperando i tweet geotaggati da una determinata
regione e altro ancora. In questo libro useremo questa API come sorgente di dati
per illustrare le capacit sia in batch sia in tempo reale da Hadoop. Tuttavia non
interagiremo direttamente con essa; utilizzeremo invece librerie di terze parti per
scaricarci di compiti come la gestione dellautenticazione e delle connessioni.

Anatomia di un tweet
Ogni oggetto restituito da una chiamata allAPI in tempo reale rappresentato
da una stringa JSON serializzata che contiene un set di attributi e metadati oltre al
messaggio di testo.
Questo contenuto aggiuntivo include un ID numerico che identifica in modo
univoco il tweet, la posizione da cui stato condiviso, lutente che lha condiviso
(loggetto utente), se stato ripubblicato da altri utenti (cio se stato ritiwittato)
e quante volte (conteggio dei tweet), il linguaggio macchina del testo, se il tweet
stato inviato in risposta a qualcuno e, in questo caso, gli ID dellutente e del tweet
a cui stato inviato e cos via.
La struttura di un tweet e degli altri oggetti eventualmente esposti dallAPI in
costante evoluzione. Trovate una guida aggiornata alla pagina
https://dev.twitter.com/docs/platform-objects/tweets.

Credenziali di Twitter
Twitter si serve del protocollo OAuth per autenticare e autorizzare laccesso
alla sua piattaforma da software di terze parti.
Lapplicazione ottiene tramite un canale esterno, per esempio un form web, le
seguenti coppie di codici:
un consumer key;
un consumer secret.
Il consumer secret non viene mai trasmesso direttamente alla terza parte perch
viene usato per firmare ogni richiesta.
Lutente autorizza lapplicazione ad accedere al servizio tramite un processo a
tre vie che, una volta completato, fornisce allapplicazione un token costituito da:
un access token;
un access secret.
Analogamente al codice consumer, laccess secret non viene mai trasmesso
direttamente alla terza parte e viene usato per firmare ogni richiesta.
Per usare lAPI Streaming, dovremo prima registrare unapplicazione e ottenere
per essa laccesso programmatico al sistema. Se richiedete un nuovo account
Twitter, accedete alla pagina https://twitter.com/signup e inserite le informazioni
richieste. A seguire, dovremo creare unapplicazione desempio che acceder
allAPI per nostro conto assegnandole i permessi opportuni. Per farlo ci serviremo
del form web alla pagina https://dev.twitter.com/apps.
Quando si crea una nuova app, ci viene chiesto di darle un nome, di fornire una
descrizione e un URL. La schermata che segue mostra le impostazioni di
unapplicazione desempio chiamata Learning Hadoop 2 Book Dataset. Per gli scopi di
questo libro non occorre specificare un URL valido, quindi utilizzeremo un
segnaposto.
Una volta completato il form, controlliamo e accettiamo le condizioni duso e
facciamo clic sul pulsante Create Application nellangolo inferiore sinistro.
Comparir una pagina che riassume i dettagli della nostra applicazione, come
mostrato nella figura. Le credenziali di autenticazione e di permesso si trovano
nella scheda OAuth Tool.
Ed eccoci pronti a generare il nostro primo vero dataset di Twitter.

Accesso programmato con Python


In questo paragrafo utilizzeremo Python e la libreria tweepy
(https://github.com/tweepy/tweepy) per raccogliere i dati di Twitter. Il file stream.py
nella directory ch1 dellarchivio di codice del libro istanzia un listener al firehose
in tempo reale, cattura un campione di dati e ripete il testo di ciascun tweet
nelloutput standard.
La libreria tweepy pu essere installata usando sia i package manager easy_install
o pip sia clonando il repository allindirizzo https://github.com/tweepy/tweepy.
Sulla CDH QuickStart VM, possiamo installare tweepy con il seguente comando:
$ pip install tweepy

Quando invocato con il parametro -j, lo script genera un tweet JSON


nelloutput standard; -t estrae e visualizza il campo di testo. Specifichiamo quanti
tweet visualizzare con n <num tweets>. Quando n non specificato, lo script verr
eseguito a tempo indeterminato. Lesecuzione pu essere interrotta premendo
Ctrl+C.
Lo script si aspetta che le credenziali OAuth vengano memorizzate come
variabili dambiente della shell; le credenziali che seguono dovranno essere
impostate nella sessione del terminale da cui stream.py verr eseguito:
$ export TWITTER_CONSUMER_KEY=your_consumer_key
$ export TWITTER_CONSUMER_SECRET=your_consumer_secret
$ export TWITTER_ACCESS_KEY=your_access_key
$ export TWITTER_ACCESS_SECRET=your_access_secret

Dopo che la dipendenza richiesta stata installata e i dati OAuth nellambiente


della shell sono stati impostati, possiamo eseguire il programma come segue:
$ python stream.py t n 1000 > tweets.txt

Ci affidiamo allI/O della shell di Linux per reindirizzare loutput con


loperatore > di stream.py a un file chiamato tweets.txt. Se tutto stato eseguito
correttamente, dovreste vedere una sequenza di testo in cui ogni riga un tweet.
Notate che in questo esempio non abbiamo mai usato Hadoop. Nei prossimi
capitoli vi mostreremo come importare un dataset generato dallAPI Streaming in
Hadoop e ne analizzeremo il contenuto su un cluster locale e su EMR.
Per ora, diamo unocchiata al codice sorgente di stream.py, che trovate
allindirizzo https://github.com/learninghadoop2/book-examples/blob/master/ch1/stream.py:
import tweepy
import os
import json
import argparse

consumer_key = os.environ[TWITTER_CONSUMER_KEY]
consumer_secret = os.environ[TWITTER_CONSUMER_SECRET]
access_key = os.environ[TWITTER_ACCESS_KEY]
access_secret = os.environ[TWITTER_ACCESS_SECRET]

class EchoStreamListener(tweepy.StreamListener):
def __init__(self, api, dump_json=False, numtweets=0):
self.api = api
self.dump_json = dump_json
self.count = 0
self.limit = int(numtweets)
super(tweepy.StreamListener, self).__init__()

def on_data(self, tweet):


tweet_data = json.loads(tweet)
if text in tweet_data:
if self.dump_json:
print tweet.rstrip()
else:
print tweet_data[text].encode(utf-8).rstrip()

self.count = self.count+1
return False if self.count == self.limit else True

def on_error(self, status_code):


return True

def on_timeout(self):
return True

if __name__ == __main__:
parser = get_parser()
args = parser.parse_args()

auth = tweepy.OAuthHandler(consumer_key, consumer_secret)


auth.set_access_token(access_key, access_secret)
api = tweepy.API(auth)
sapi = tweepy.streaming.Stream(
auth, EchoStreamListener(
api=api,
dump_json=args.json,
numtweets=args.numtweets))
sapi.sample()

Come prima cosa, importiamo tre dipendenze: tweepy e i moduli os e json, presenti
nellintepreter di Python versione 2.6 o successiva.
Definiamo poi la classe EchoStreamListener, che eredita ed estende StreamListener da
tweepy. Come il nome pu far intuire, StreamListener ascolta gli eventi e i tweet che

vengono pubblicati nello stream in tempo reale e agisce di conseguenza.


Ogni volta che individua un nuovo evento, innesca una chiamata a on_data(). In
questo metodo, estraiamo il campo text da un oggetto tweet e lo mostriamo
nelloutput standard con una codifica UTF-8. In alternativa, se lo script invocato
con -j, mostriamo lintero tweet JSON. Quando lo script viene eseguito,
istanziamo un oggetto tweepy.OAuthHandler con le credenziali OAuth che identificano
il nostro account Twitter, e poi usiamo questo oggetto per lautenticazione con i
codici access e secret key dellapplicazione. Utilizziamo poi loggetto auth per
creare unistanza della classe tweepy.API (api).
Se lautenticazione ha successo, diciamo a Python di ascoltare gli eventi sullo
stream in tempo reale usando EchoStreamListener.
Una richiesta http GET allendpoint statuses/sample viene eseguita da sample() e
restituisce un campione casuale di tutti gli stati pubblici.
AT T ENZIONE
Di default, sample() viene eseguita a tempo indeterminato. Ricordate di terminare
esplicitamente la chiamata al metodo premendo Ctrl+C.
Riepilogo
In questo capitolo abbiamo compiuto una rapida panoramica sulla storia di
Hadoop: da dove viene, la sua evoluzione e perch il rilascio della versione 2
stato cos fondamentale. Abbiamo anche descritto il mercato emergente delle
distribuzioni di Hadoop e abbiamo spiegato come nel libro utilizzeremo una
combinazione tra distribuzioni locali e cloud.
Infine abbiamo descritto come configurare il software necessario, gli account e
gli ambienti richiesti per i prossimi capitoli, oltre a illustrare come trarre dallo
stream di Twitter quei dati che ci serviranno per gli esempi.
Fissate queste basi, passiamo a esaminare nel dettaglio il livello dello storage
in Hadoop.
Capitolo 2
Storage

Dopo la panoramica presentata nel capitolo precedente, siamo pronti per


studiare i componenti di Hadoop nel dettaglio. In questo capitolo partiremo dal
fondo, cio dai meccanismi per lo storage dei dati. In particolare, vedremo quanto
segue.
Descriveremo larchitettura di Hadoop Distributed File System (HDFS).
Mostreremo i miglioramenti ad HDFS introdotti in Hadoop 2.
Vedremo come accedere ad HDFS usando strumenti a riga di comando e
lAPI Java.
Forniremo una breve descrizione di ZooKeeper, un altro tipo di file system in
Hadoop.
Faremo alcune considerazioni sullo storage dei dati in Hadoop e vedremo
quali sono i formati di file disponibili.
Nel Capitolo 3, illustreremo come Hadoop fornisce il framework per consentire
lelaborazione dei dati.
Funzionamento interno di HDFS
Nel Capitolo 1, abbiamo compiuto una panoramica su HDFS; ora lo
esploreremo nel dettaglio. HDFS pu essere considerato un file system, per quanto
con prestazioni e semantica molto specifiche. Viene implementato con due
processi principali sul server: il NameNode e i DataNode, configurati secondo un
modello master/slave. Se vedete che il NameNode contiene tutti i dati del file
system e i DataNode i dati effettivi del file system (i blocchi), siete a buon punto.
Ogni file in HDFS verr suddiviso in pi blocchi che possono risiedere su diversi
DataNode, ed il NameNode che capisce come questi blocchi possono essere
combinati per costruire i file.

Avvio del cluster


Analizziamo le responsabilit di questi nodi e la comunicazione tra di essi
presumendo di avere un cluster HDFS che stato chiuso ed esaminando il
comportamento al riavvio.

Avvio del NameNode


Partiamo dallavvio del NameNode (sebbene non ci sia un ordine obbligatorio
da seguire e la nostra scelta abbia solo una motivazione didattica). Il NameNode
memorizza due tipi di dati sul file system:
la struttura del file system, ossia i nomi delle directory e dei file, le posizioni
e gli attributi;
i blocchi che comprendono ciascun file nel file system.
Questi dati sono memorizzati in file che vengono letti dal NameNode allavvio.
Notate che questo non memorizza in modo persistente la mappatura dei blocchi che
sono salvati su DataNode specifici; vedremo tra poco come queste informazioni
vengono comunicate.
Poich il NameNode si basa su questa rappresentazione in memoria del file
system, ha in genere requisiti hardware diversi rispetto ai DataNode. Affronteremo
i dettagli della scelta dellhardware nel Capitolo 10; per ora ricordate
semplicemente che il NameNode tende a consumare parecchia memoria,
soprattutto sui cluster pi grandi con milioni di file, tanto pi se questi hanno nomi
molto lunghi. Il limite alla scala del NameNode ha portato a una funzione
aggiuntiva di Hadoop 2 che tratteremo solo superficialmente: la federazione di
NameNode, nella quale pi NameNode (o coppie di NameNode HA) collaborano
per fornire i metadati complessivi per lintero file system.
Il file principale scritto dal NameNode si chiama fsimage, ed lunico elemento
fondamentale di tutto il cluster: senza di esso, non si ha la possibilit di ricostruire
i blocchi di dati nel file system. Questo file viene letto in memoria e tutte le
modifiche al file system vengono salvate in tale rappresentazione del file system. Il
NameNode non scrive nuove versioni di fsimage se vengono applicate nuove
modifiche dopo la sua esecuzione; scrive invece un altro file chiamato edits, un
elenco delle modifiche apportate da quando stata scritta lultima versione di
fsimage.

Il processo di avvio del NameNode consiste innanzitutto nella lettura del file
fsimage, poi nella lettura del file edits e infine nellapplicazione di tutte le modifiche

salvate in edits nella copia in memoria di fsimage. Infine scrive su disco una nuova
versione aggiornata del file fsimage ed pronto per ricevere le richieste da parte
del client.

Avvio dei DataNode


Allavvio, i DataNode catalogano i blocchi di cui conservano delle copie.
Solitamente questi blocchi sono scritti sotto forma di file sul file system del
DataNode locale. Il DataNode effettuer una sorta di controllo di coerenza sui
blocchi e riporter al NameNode lelenco dei blocchi di cui ha copie valide.
cos che il NameNode costruisce la mappatura finale che gli serve, apprendendo
quali blocchi sono salvati su quali DataNode. Una volta che il DataNode si
coordinato con il NameNode, tra i nodi viene inviata una serie continua di
richieste heartbeat per consentire al NameNode di individuare i DataNode che
sono stati chiusi, che non sono pi raggiungibili o che sono appena entrati a far
parte del cluster.

Replica dei blocchi


HDFS replica ciascun blocco su pi DataNode; il fattore di replica predefinito
3, ma si pu impostare file per file. HDFS pu essere configurato anche in modo
che possa determinare se dei DataNode specifici sono nello stesso rack hardware
fisico oppure no. Grazie a una disposizione intelligente dei blocchi e alla
conoscenza della topologia dei cluster, HDFS cercher di collocare la seconda
replica su un host diverso ma nello stesso rack della prima, e la terza su un host
esterno al rack.
In questo modo il sistema pu sopravvivere ai guasti della maggior parte
dellattrezzatura del rack e mantenere almeno una replica di ciascun blocco. Come
vedremo nel Capitolo 3, conoscere la disposizione dei blocchi permette ad
Hadoop anche di programmare lelaborazione il pi vicino possibile alla replica
di ogni blocco, migliorando notevolmente le prestazioni.
Ricordate che la replica una strategia di resilienza, e non un meccanismo di
backup; se in HDFS avete dei dati critici, dovrete provvedere al backup o
considerare altri approcci che vi proteggano da errori da cui la replica non pu
difendervi, come leliminazione accidentale dei file.
Quando il NameNode si avvia e riceve i report sui blocchi dai DataNode,
rimane in una modalit sicura finch una soglia configurabile di blocchi (di default
99,9%) viene indicata come attiva. In questa modalit, i client non possono
apportare modifiche al file system.
Accedere al file system HDFS tramite riga
di comando
Allinterno della distribuzione Hadoop, si trova unutility a riga di comando
chiamata hdfs, che rappresenta il metodo principale per interagire con il file system
dalla riga di comando. Eseguitela senza argomenti per vedere i vari sottocomandi
disponibili. Ce ne sono parecchi, e molti vengono utilizzati per compiere
operazioni come avviare o interrompere i vari componenti di HDFS. La sintassi
generale del comando hdfs :
hdfs <sottocomando> <comando> [argomenti]

I due sottocomandi principali che utilizzeremo in questo libro sono i seguenti.


dfs: viene utilizzato per laccesso generale e la manipolazione del file system,
compresi lettura/scrittura e accesso a file e directory.
dfsadmin: viene utilizzato per lamministrazione e la manutenzione del file

system. Qui non lo tratteremo nel dettaglio. Date unocchiata al comando -


report, che fornisce un elenco dello stato del file system e di tutti i DataNode:

$ hdfs dfsadmin -report

NOTA
I comandi dfs e dfsadmin possono essere utilizzati anche con lutility a riga di comando
principale di Hadoop, come in hadoop fs -ls /. Questo era lapproccio nelle versioni precedenti
di Hadoop , ma ora stato deprecato a favore del comando hdfs.

Esplorare il file system HDFS


Eseguite la prossima riga per ottenere un elenco di tutti i comandi disponibili
forniti dal sottocomando dfs:
$ hdfs dfs

Molti dei comandi sono simili a quelli standard del file system Unix e
funzionano come ci si aspetta. Sulla nostra VM di test abbiamo un account utente
chiamato cloudera. Tramite questo utente possiamo ottenere una lista della root del
file system:
$ hdfs dfs -ls /
Found 7 items
drwxr-xr-x - hbase hbase 0 2014-04-04 15:18 /hbase
drwxr-xr-x - hdfs supergroup 0 2014-10-21 13:16 /jar
drwxr-xr-x - hdfs supergroup 0 2014-10-15 15:26 /schema
drwxr-xr-x - solr solr 0 2014-04-04 15:16 /solr
drwxrwxrwt - hdfs supergroup 0 2014-11-12 11:29 /tmp
drwxr-xr-x - hdfs supergroup 0 2014-07-13 09:05 /user
drwxr-xr-x - hdfs supergroup 0 2014-04-04 15:15 /var

Loutput molto simile a quello del comando ls di Unix. Gli attributi del file
funzionano come quelli di user/group/world su un file system Unix (compreso lo
sticky bit t) e in pi forniscono i dettagli sul proprietario, il gruppo e lora di
modifica delle directory. La colonna tra il nome del gruppo e la data di modifica
la dimensione; per le directory 0, mentre per i file avr un valore, come si vede
nel prossimo segmento di codice.
NOTA
Se vengono utilizzati percorsi relativi, vengono tratti dalla directory home dellutente. Se non c
una directory home, possiamo crearla attraverso questi comandi:
$ sudo -u hdfs hdfs dfs mkdir /user/cloudera
$ sudo -u hdfs hdfs dfs chown cloudera:cloudera /user/cloudera

I passi mkdir e chown richiedono i privilegi di superutente (sudo -u hdfs).


$ hdfs dfs -mkdir testdir
$ hdfs dfs -ls
Found 1 items
drwxr-xr-x - cloudera cloudera 0 2014-11-13 11:21 testdir

Possiamo poi creare un file, copiarlo in HDFS e leggerne il contenuto


direttamente dalla sua posizione in HDFS, cos:
$ echo Hello world > testfile.txt
$ hdfs dfs -put testfile.txt testdir

Tenete conto che c un comando pi vecchio chiamato -copyFromLocal, che


funziona esattamente come -put; potreste incontrarlo nella documentazione online
meno recente. Ora eseguite il prossimo comando e controllate loutput:
$ hdfs dfs -ls testdir
Found 1 items
-rw-r--r-- 3 cloudera cloudera 12 2014-11-13 11:21 testdir/testfile.txt

Osservate la nuova colonna tra gli attributi di file e il proprietario: il fattore di


replica del file. Per finire, eseguite questo comando:
$ hdfs dfs -tail testdir/testfile.txt
Hello world

Quasi tutti gli altri sottocomandi di dfs sono piuttosto intuitivi; esplorateli. Pi
avanti nel capitolo affronteremo le snapshot e laccesso programmatico ad HDFS.
Proteggere i metadati del file system
Considerata limportanza del file fsimage per il file system, perderlo una
catastrofe. In Hadoop 1, dove il NameNode era un unico point of failure, la best
practice era quella di configurare il NameNode perch scrivesse fsimage e
contemporaneamente modificasse i file su unarea di storage locale e in unaltra
posizione su un file system remoto (in genere NFS). In caso di fallimento del
NameNode, si poteva avviare un NameNode sostituivo usando questa copia
aggiornata dei metadati del file system. La procedura era per piuttosto complessa,
e comportava un periodo in cui il cluster non sarebbe stato disponibile.

Il Secondary NameNode non ci salva


Il componente dal nome pi sfortunato di tutti in Hadoop 1 il Secondary
NameNode. Comprensibilmente, molti si aspettano che sia una sorta di NameNode
di backup o in standby, ma non lo . Il Secondary NameNode responsabile
esclusivamente di una lettura periodica della versione pi recente e delle
modifiche del file fsimage e della creazione di un nuovo fsimage aggiornato con le
modifiche esposte applicate. Su un cluster molto occupato, questo check point
poteva velocizzare significativamente il riavvio del NameNode riducendo il
numero delle modifiche che doveva implementare prima di poter servire i client.
In Hadoop 2, la denominazione pi chiara; ci sono nodi Checkpoint, che
svolgono il ruolo che era dei Secondary NameNode oltre che quello di backup,
mantenendo una copia locale aggiornata dei dati del file system, anche se il
processo che promuove un nodo di backup a NameNode principale ancora
manuale e si svolge in pi passi.

NameNode HA di Hadoop 2
Nella maggior parte dei cluster di produzione di Hadoop 2, tuttavia, ha pi
senso utilizzare la soluzione completa High Availability (HA) invece che affidarsi
ai nodi Checkpoint e di backup. un errore provare a combinare NameNode HA
con i meccanismi degli altri due tipi di nodi.
Lidea di base quella di una coppia di NameNode (attualmente non ne sono
supportati pi di due) configurati in un cluster attivo/passivo. Un NameNode
agisce come il master attivo che serve le richieste dei client, mentre il secondo
rimane in attesa di sostituirlo qualora il primo dovesse fallire. In particolare,
HDFS di Hadoop 2 consente HA attraverso due meccanismi:
fornendo un sistema tale per cui entrambi i NameNode possono avere delle
viste coerenti del file system;
fornendo un mezzo ai client per connettersi sempre al NameNode master.

Mantenere sincronizzati gli HA NameNode


Ci sono due soluzioni tramite cui i NameNode attivi e in standby possono
ottenere uniformit nelle viste del file system: il ricorso a una condivisione NFS o
il Quorum Journal Manager (QJM).
Nel primo caso, c ovviamente una richiesta su una condivisione file NFS
remota esterna; notate che, poich luso di NFS era una best practice in Hadoop 1
per una seconda copia dei metadati del file system, molti cluster ne avevano gi
uno. Se la preoccupazione quella della disponibilit, va tenuto conto che per
rendere NFS sempre disponibile occorre un hardware di alto livello e molto
costoso. In Hadoop 2, HA utilizza NFS, la cui posizione diventa per quella
principale dei metadati. Poich il NameNode scrive tutte le modifiche al file
system sulla condivisione NFS, il nodo in standby individua tali modifiche e
aggiorna la sua copia dei metadati di conseguenza.
QJM utilizza un servizio esterno (Journal Manager) invece di un file system. Il
cluster del Journal Manager un numero dispari di servizi (in genere 3, 5 e 7) che
girano su quel numero di host. Tutte le modifiche al file system vengono inviate al
servizio QJM, e una modifica considerata confermata solo quando ritenuta tale
dalla maggioranza dei nodi QJM. Il NameNode in standby riceve gli aggiornamenti
delle modifiche dal servizio QJM e le usa per mantenere aggiornata la sua copia
dei metadati.
QJM non richiede hardware aggiuntivo perch i nodi Checkpoint sono leggeri e
possono trovarsi nella stessa posizione di altri servizi. Il modello non prevede poi
alcun point of failure. Ecco perch QJM HA spesso lopzione privilegiata.
In ogni caso, tanto nelle HA basate su NFS quanto in quelle basate su QJM, i
DataNode inviano report sullo stato dei blocchi a entrambi i NameNode per
garantire che abbiano le informazioni aggiornate relativamente alla mappatura dei
blocchi sui DataNode. Ricordate che queste informazioni sullassegnazione dei
blocchi non sono contenute nei dati di edits/fsimage.

Configurazione del client


I client del cluster HDFS rimangono quasi del tutto alloscuro del fatto che
venga utilizzato il NameNode HA. I file di configurazione devono includere i
dettagli di entrambi i NameNode, ma i meccanismi per determinare qual il
NameNode attivo e quando passare al nodo in standby sono tutti incapsulati
nelle librerie del client. Il principio fondamentale che, invece di fare riferimento
a un host di un NameNode esplicito come in Hadoop 1, HDFS in Hadoop 2
identifica un ID di nameservice per il NameNode allinterno del quale i singoli
NameNode (ognuno con il suo ID) sono definiti per HA. Il concetto di ID di
nameservice utilizzato anche dalla federazione di NameNode a cui abbiamo fatto
cenno in precedenza.

Come funziona un failover


Il failover pu essere sia manuale sia automatico. Un failover manuale richiede
un amministratore che attivi il passaggio che promuove il NameNode in standby a
NameNode attivo. Per quanto il failover automatico migliori notevolmente la
gestione della disponibilit del sistema, ci possono essere situazioni in cui non
desiderabile. Lattivazione di un failover manuale implica lesecuzione di pochi
comandi, e anche in questa modalit il failover decisamente pi semplice che
non con Hadoop 1 o con i nodi di backup di Hadoop 2, in cui la transizione a un
nuovo NameNode richiede un notevole sforzo manuale.
A prescindere da come viene avviato, comunque, il failover si svolge in due
fasi: conferma che il master precedente non servir pi le richieste e la
promozione del nodo in standby a nodo master.
Il rischio pi grande in un failover avere un periodo in cui entrambi i
NameNode servono le richieste. In casi del genere, possibile ritrovarsi con
modifiche al file system che entrano i conflitto o con i due nodi che perdono la
sincronizzazione. Per quanto questo non dovrebbe avvenire se si usa QJM (che
accetta sempre connessioni da un unico client), si potrebbe finire con il servire
informazioni non aggiornate ai client, che potrebbero quindi prendere decisioni
non corrette in base a questi dati fallaci. La cosa ovviamente pi probabile se
quello che era il NameNode master continua a comportarsi correttamente, motivo
per il quale fondamentale capire innanzitutto perch un failover necessario.
Per assicurarsi che ci sia un solo NameNode attivo alla volta, viene utilizzato
un meccanismo di fencing per avere la conferma che il NameNode master esistente
stato chiuso. La soluzione pi semplice cerca di eseguire ssh nellhost del
NameNode e di sopprimere direttamente il processo, anche se pu essere eseguito
uno script personalizzato; quindi un meccanismo flessibile. Il failover non
continuer finch il fencing ha successo e il sistema ha confermato che il
NameNode master fuori gioco e ha rilasciato le risorse necessarie.
Al termine, il NameNode in standby diventer il master, e inizier a scrivere nel
file fsimage montato su NFS e nei log delle modifiche se stato usato NFS per HA,
oppure diventer lunico client per QJM se questo il meccanismo per HA.
Prima di affrontare il failover automatico, dovremo fare una deviazione per
introdurre un altro progetto Apache utilizzato per abilitare questa funzione.
Apache ZooKeeper: un file system
diverso
Quando si tratta di Hadoop, parleremo perlopi di HDFS quando tratteremo i
file system e lo storage dei dati. Tuttavia, allinterno di quasi tutte le distribuzioni
Hadoop 2, c un altro servizio che assomiglia a un file system, ma che fornisce
capacit cruciali per il funzionamento appropriato dei sistemi distribuiti. Questo
servizio Apache ZooKeeper (http://zookeeper.apache.org), ed essendo una parte
fondamentale dellimplementazione di HDFS HA, lo presenteremo in questo
capitolo. Nondimeno, viene utilizzato anche da molti altri componenti e progetti
correlati di Hadoop, quindi lo toccheremo diverse volte in tutto libro.
ZooKeeper nato come sottocomponente di HBase ed era utilizzato per
abilitare numerose funzionalit del servizio. Quando si costruisce un sistemo
distribuito complesso, necessario compiere una serie di attivit che spesso
difficile fare bene, cose come la gestione dei lock condivisi, lindividuazione dei
guasti nei componenti e il supporto allelezione di un servizio leader in un gruppo
di servizi che collaborano. ZooKeeper stato creato come servizio di
coordinamento di una serie di operazioni di base sulle quali HBase pu
implementare questo tipo funzionalit critiche dal punto di vista operativo.
ZooKeeper trae anche ispirazione dal sistema Google Chubby descritto
allindirizzo http://research.google.com/archive/chubby-osdi06.pdf.
ZooKeeper gira come un cluster di istanze chiamato ensemble. Lensemble
fornisce una struttura di dati, che in qualche modo analoga a un file system. Ogni
posizione nella struttura chiamata ZNode, e pu avere dei figli come se fosse una
directory e un contenuto come se fosse un file. Notate che ZooKeeper non la
soluzione migliore per memorizzare grandi quantit di dati; di default il massimo
di dati che pu contenere uno ZNode 1 MB.
Un server nellensemble ha il ruolo di master e prende tutte le decisioni
riguardanti le richieste dei client. Le sue responsabilit sono regolate da norme
ben definite; per esempio, deve garantire che una richiesta sia caricata solo
quando la maggioranza dellensemble lha confermata, e far s che una volta
ottenuta la conferma ogni modifica successiva venga rifiutata.
Potete installare ZooKeeper sulla vostra Cloudera Virtual Machine, altrimenti
ricorrete a Cloudera Manager per installarlo come nodo singolo sullhost. Nei
sistemi di produzione, ZooKeeper ha una semantica molto specifica per quanto
riguarda la votazione a maggioranza assoluta, quindi la logica ha senso solo negli
ensemble pi grandi (3, 5, o 7 nodi sono le dimensioni pi diffuse).
Nella Cloudera VM esiste un client a riga di comando per ZooKeeper chiamato
zookeeper-client (nella distribuzione vanilla di ZooKeeper chiamato zkCli.sh). Se lo

eseguite senza argomenti, si connetter al server di ZooKeeper in esecuzione sulla


macchina locale. Da qui, potete digitare help per ottenere un elenco dei comandi.
I comandi pi interessanti sono create, ls e get che, rispettivamente, creano uno
ZNode, elencano gli ZNode in un dato punto del file system e recuperano i dati
salvati in uno specifico ZNode. Vediamo qualche esempio duso.
Create uno ZNode senza dati:
$ create /zk-test

Create un figlio del primo ZNode e memorizzatevi del testo:


$ create /zk-test/child1 sampledata

Recuperate i dati associati a un particolare ZNode:


$ get /zk-test/child1

Il client pu anche registrare un watcher (un osservatore) su uno ZNode


specifico che generer un avvertimento se lo ZNode in questione cambia nei suoi
dati o nei suoi nodi figlio.
Potrebbe non sembrare particolarmente utile, ma gli ZNode possono essere
creati tanto come nodi sequenziali quanto come nodi effimeri, ed qui che inizia la
magia.

Implementare un lock distribuito con ZNode


sequenziali
Se uno ZNode viene creato allinterno della CLI con lopzione -s, verr creato
come nodo sequenziale. ZooKeeper apporr al nome fornito un suffisso con un
intero di dieci cifre garantito come univoco e maggiore di qualsiasi altro nodo
figlio sequenziale dello stesso ZNode. Possiamo utilizzare questo meccanismo per
creare un lock distribuito. Lo stesso ZooKeeper non contiene il lock effettivo; il
client deve capire cosa significano i vari stati in ZooKeeper in termini di
mappatura sui lock dellapplicazione in questione.
Se si crea uno ZNode (non sequenziale) in /zk-lock, allora qualsiasi client che
desidera avere il lock creer un nodo figlio sequenziale. Per esempio, il comando
create -s /zk-lock/locknode potrebbe creare il primo nodo /zk-lock/locknode-0000000001

con degli interi che incrementano per le chiamate successive. Quando un client
crea uno ZNode sotto il lock, controller se il relativo nodo sequenziale ha il
suffisso con lintero pi basso. In caso affermativo, viene trattato come se avesse
il lock, altrimenti dovr attendere finch il nodo che possiede il lock non viene
eliminato. Il client in genere registra un watcher sul nodo con il suffisso pi basso
e viene avvisato quando quel nodo viene rimosso; ora sar quello ZNode ad avere
il lock.

Implementare ladesione a un gruppo e


lelezione di un leader usando ZNode effimeri
Qualsiasi client ZooKeeper invia degli heartbeat al server durante la sessione,
dimostrando che attivo. Degli ZNode che abbiamo discusso finora, possiamo
dire che sono persistenti e che sopravvivono tra le varie sessioni. Tuttavia,
possiamo creare uno ZNode come effimero, indicando cio che sparir una volta
che il client che lha creato si disconnette o se il nodo viene identificato come
morto dal server ZooKeeper. Nella CLI, uno ZNode effimero viene creato
aggiungendo il flag -e al comando di creazione.
Gli ZNode effimeri sono un ottimo sistema per implementare ladesione a un
gruppo in un sistema distribuito. Nel caso di un sistema in cui i nodi possono
fallire, aggiungersi o uscire senza preavviso, scoprire quali nodi sono attivi in un
dato momento nel tempo spesso difficile. In ZooKeeper, possiamo favorire
questa ricerca facendo in modo che ogni nodo crei uno ZNode effimero in un certo
punto del file system di ZooKeeper. Gli ZNode possono contenere dati sui nodi di
servizio, come il nome dellhost, lindirizzo IP, il numero della porta e cos via.
Per ottenere una lista dei nodi attivi, possibile elencare semplicemente i nodi
figlio dello ZNode del gruppo genitore. Considerata la natura dei nodi effimeri,
possiamo essere certi che la lista dei nodi attivi recuperata sia sempre aggiornata.
Se facciamo in modo che ogni nodo di servizio credi ZNode figlio che non sono
solo effimeri ma anche sequenziali, allora possiamo costruire un meccanismo per
lelezione di un leader per quei servizi che devono avere un unico nodo master
alla volta. Il meccanismo lo stesso dei lock; il nodo del servizio client crea lo
ZNode sequenziale ed effimero e poi controlla se ha il numero consecutivo pi
basso. In caso affermativo il nodo master, altrimenti registrer un watcher sul
nodo con il numero sequenziale pi basso successivo per essere avvisato quando
quello diventa il maser.

API Java
La classe org.apache.zookeeper.ZooKeeper il principale client programmatico per
accedere a un ensemble di ZooKeeper. Consultate la documentazione Java per i
dettagli; in ogni caso, linterfaccia di base piuttosto intuitiva, con corrispondenze
immediate ai comandi nella CLI. Per esempio:
create lequivalente di create della CLI;
getChildren lequivalente di ls della CLI;

getData lequivalente di get della CLI.

Componenti
Come appena visto, ZooKeeper fornisce un piccolo numero di operazioni
definite con una semantica molto forte che pu essere costruita in servizi di alto
livello, quali i lock, la partecipazione ai gruppi e lelezione di un leader.
ZooKeeper pu essere considerato come un toolkit di funzioni affidabili e ben
ingegnerizzate fondamentali per i sistemi distribuiti sui quali si pu costruire senza
doversi preoccupare delle tortuosit della loro implementazione. Linterfaccia
fornita da ZooKeeper piuttosto semplice, e sono poche le recenti interfacce di
alto livello che forniscono qualcosa di pi nella mappatura delle primitive a basso
livello nella logica dellapplicazione. Il progetto Curator (http://curator.apache.org/)
un buon esempio.
ZooKeeper era usato con moderazione in Hadoop 1, ma ora piuttosto diffuso.
Viene utilizzato sia da MapReduce sia da HDFS per lelevata disponibilit dei
loro componenti JobTracker e NameNode. Hive e Impala, che tratteremo in
seguito, lo usano per inserire dei lock sulle tabelle di dati a cui accedono pi job
contemporaneamente. Kafka, di cui discuteremo nellambito di Samza, impiega
ZooKeeper per i nodi (broker nella sua terminologia), lelezione del leader e la
gestione dello stato.

Per saperne di pi
Non abbiamo descritto ZooKeeper troppo nei dettagli, e abbiamo del tutto
omesso alcuni aspetti come la sua capacit di applicare quote e ACL agli ZNode
nel file system e i meccanismi per costruire i callback. Il nostro scopo era quello
di fornire dettagli sufficienti a darvi unidea di come verr utilizzato nei servizi
Hadoop che esploriamo nel libro. Per ulteriori informazioni, consultate la home
page del progetto.
Failover automatico dei NameNode
Ora che abbiamo presentato ZooKeeper, possiamo mostrare come viene
utilizzato per abilitare il failover automatico dei NameNode.
Il failover automatico dei NameNode porta nel sistema due nuovi componenti;
un quorum e lo ZooKeeper Failover Controller (ZKFC), che gira su ciascun host
di NameNode. ZKFC crea uno ZNode effimero in ZooKeeper e lo detiene finch
rileva il NameNode locale come attivo e correttamente funzionante. Per
determinarlo, continua a inviare semplici richieste di controllo della salute al
NameNode; se questo non risponde correttamente in breve tempo, ZKFC presume
che fallito. Se una macchina NameNode va in crash o fallisce in altro modo, la
sessione di ZKFC in ZooKeeper verr chiusa e anche lo ZNode effimero verr
rimosso.
I processi di ZKFC monitorano anche gli ZNode degli altri NameNode nel
cluster. Se lo ZKFC sullhost del NameNode in standby vede sparire lo ZNode
master, deduce che questo fallito e tenter un failover. Per farlo, cercher di
acquisire il lock per il NameNode (tramite il protocollo illustrato nel paragrafo su
ZooKeeper), e se ci riuscir inizier il failover attraverso lo stesso meccanismo di
fencing/promozione descritto in precedenza.
Snapshot HDFS
Abbiamo gi accennato al fatto che la replica HDFS da sola non una buona
strategia di backup. Nel file system di Hadoop 2, sono state inserite le snapshot,
delle istantanee che aggiungono un altro livello di protezione dei dati ad HDFS.
Le snapshot del file system sono state utilizzate per qualche tempo in diverse
tecnologie. Lidea di base che diventa possibile vedere lo stato esatto del file
system in momenti specifici nel tempo. Questo risultato si ottiene prendendo una
copia dei metadati del file system nel punto in cui si cattura la snapshot, e la si
rende disponibile perch possa essere visualizzata nel futuro.
Man mano che il file system viene modificato, qualsiasi intervento che influisce
sulla snapshot verr trattato in modo speciale. Per esempio, se in una snapshot c
un file che viene eliminato, anche se verr rimosso dallo stato corrente del file
system i suoi metadati rimarranno nella snapshot, e i blocchi associati ai suoi dati
rimarranno nel file system, anche se non saranno accessibili attraverso una vista
del sistema diversa dalla snapshot.
Un esempio illustrer il punto. Immaginate che il vostro file system contenga
questi file:
/data1 (5 blocchi)
/data2 (10 blocchi)

Potete catturare una snapshot e poi eliminare il file /data2. Se visualizzate lo


stato corrente del file system, solo /data1 risulter visibile. Se esaminate la
snapshot, vedrete entrambi i file. Dietro le quinte, tutti e 15 i blocchi continuano a
esistere, ma solo quelli associati al file /data1 non eliminato sono parte del file
system corrente. I blocchi per il file /data2 saranno rilasciati solo quando la
snapshot verr cancellata (le snapshot sono viste di sola lettura).
In Hadoop 2 le snapshot possono essere applicate sia a tutto il file system sia a
percorsi specifici. Un percorso deve essere impostato come sottoponibile a
snapshot, e questo non possibile se uno qualsiasi dei suoi percorsi figlio o
genitore esso stesso sottoponibile a snapshot.
Vediamo un semplice esempio basato sulla directory che abbiamo creato in
precedenza per illustrare luso delle snapshot. I comandi che descriveremo devono
essere eseguiti con privilegi di superutente, ottenibili con sudo -u hdfs.
Come prima cosa, eseguite il comando dfsadmin dellutility CLI hdfs per abilitare
le snapshot di una directory:
$ sudo -u hdfs hdfs dfsadmin -allowSnapshot \
/user/cloudera/testdir
Allowing snapshot on testdir succeeded

Ora creiamo la snapshot ed esaminiamola; le snapshot sono disponibili


attraverso la sottodirectory .snapshot della directory sottoponibile a snapshot.
Notate che .snapshot non sar visibile nel normale elenco delle directory. Ecco
come procedere:
$ sudo -u hdfs hdfs dfs -createSnapshot \
/user/cloudera/testdir sn1
Created snapshot /user/cloudera/testdir/.snapshot/sn1

$ sudo -u hdfs hdfs dfs -ls \


/user/cloudera/testdir/.snapshot/sn1

Found 1 items -rw-r--r-- 1 cloudera cloudera 12 2014-11-13 11:21


/user/cloudera/testdir/.snapshot/sn1/testfile.txt

Rimuoviamo il file di prova dalla directory principale e verifichiamo che


questa sia vuota:
$ sudo -u hdfs hdfs dfs -rm \
/user/cloudera/testdir/testfile.txt
14/11/13 13:13:51 INFO fs.TrashPolicyDefault: Namenode trash configuration: Deletion interval
= 1440 minutes, Emptier interval = 0 minutes. Moved:
hdfs://localhost.localdomain:8020/user/cloudera/testdir/testfile.txt to trash at:
hdfs://localhost.localdomain:8020/user/hdfs/.Trash/Current
$ hdfs dfs -ls /user/cloudera/testdir
$

Osservate il riferimento alle directory del cestino (Trash); di default, HDFS


copia i file eliminati in una directory .Trash nella directory home dellutente, cos
che sia possibile recuperarli in caso di bisogno. Questi file possono essere
cancellati con hdfs dfs expunge, altrimenti verranno eliminati automaticamente dopo
sette giorni.
Analizziamo la snapshot nella quale il file appena eliminato ancora
disponibile:
$ hdfs dfs -ls testdir/.snapshot/sn1
Found 1 items drwxr-xr-x - cloudera cloudera 0 2014-11-13 13:12
testdir/.snapshot/sn1
$ hdfs dfs -tail testdir/.snapshot/sn1/testfile.txt
Hello world

Adesso possiamo eliminare la snapshot, liberando gli eventuali blocchi che


contiene:
$ sudo -u hdfs hdfs dfs -deleteSnapshot \
/user/cloudera/testdir sn1
$ hdfs dfs -ls testdir/.snapshot
$

I file in una snapshot sono completamente disponibili per la lettura e la copia,


fornendo accesso allo stato storico del file system nel punto in cui stata catturata
listantanea. Ogni directory pu avere fino a 65.535 snapshot, e HDFS gestisce le
snapshot in modo piuttosto efficiente in termini di impatto sulle normali operazioni
del file system. Sono un meccanismo eccezionale da usare prima di qualsiasi
attivit che potrebbero avere degli effetti sfavorevoli, come provare una nuova
versione di unapplicazione che accede al file system. Se il nuovo software
corrompe i file, possibile ripristinare lo stato precedente della directory; se
dopo un periodo di validazione il software viene accettato, allora la snapshot pu
essere eliminata.
File system di Hadoop
Finora abbiamo fatto riferimento ad HDFS come allunico file system di
Hadoop. In realt, Hadoop ha una nozione piuttosto astratta di file system. HDFS
solo una delle numerose implementazioni della classe Java astratta
org.apache.hadoop.fs.File system. Trovate un elenco dei file system disponibili

allindirizzo https://hadoop.apache.org/docs/r2.5.0/api/org/apache/hadoop/fs/FileSystem.html.
La tabella che segue ne riepiloga alcuni, insieme allo schema URI corrispondente
e alla classe di implementazione Java.
File system Schema URI Implementazione Java
Locale file org.apache.hadoop.fs.LocalFileSystem

HDFS hdfs org.apache.hadoop.hdfs.DistributedFileSystem

S3 (nativo) s3n org.apache.hadoop.fs.s3native.NativeS3FileSystem

S3 (basato su blocchi) s3 org.apache.hadoop.fs.s3.S3FileSystem

Esistono due implementazioni del file system S3. Quella nativa s3n usata
per leggere e scrivere file normali. I dati salvati con s3n sono accessibili da
qualsiasi strumento, e possono essere utilizzati per leggere i dati generati da altri
strumenti S3. s3n non pu gestire file pi grandi di 5 TB n rinominare le
operazioni.
Analogamente ad HDFS, il file system S3 basato su blocchi memorizza i file in
blocchi e richiede un bucket S3 dedicato al file system. I file memorizzati in un file
system S3 possono superare i 5 TB, ma non ammessa linteroperabilit con altri
strumenti S3. LS3 basato su blocchi supporta la rinomina delle operazioni.

Interfacce di Hadoop
Hadoop scritto in Java, e ovviamente tutte le interazioni con il sistema
avvengono attraverso linterfaccia a riga di comando. Quella che abbiamo usato
con il comando hdfs negli esempi precedenti unapplicazione Java che usa la
classe FileSystem per svolgere operazioni di input/output sui file system disponibili.

API FileSystem Java


LAPI Java, fornita dal package org.apache.hadoop.fs, espone i file system di
Hadoop. org.apache.hadoop.fs.FileSystem la classe astratta implementata da ciascun
file system e fornisce uninterfaccia generica per interagire con i dati in Hadoop.
Tutto il codice che utilizza HDFS dovrebbe essere scritto con la capacit di
gestire un oggetto FileSystem.

Libhdfs
Libhdfs una libreria C che pu essere impiegata per accedere a qualsiasi file
system di Hadoop e non solo ad HDFS. scritta utilizzando la Java Native
Interface (JNI) e imita la classe FileSystem di Java.

Thrift
Apache Thrift (http://thrift.apache.org) un framework per costruire software
cross-language attraverso meccanismi di serializzazione dei dati e di invocazione
remota dei metodi. LAPI Hadoop Thrift, disponibile in contrib, espone i file
system di Hadoop come un servizio Thrift. Questa interfaccia facilita al codice non
Java laccesso ai dati memorizzati in un file system di Hadoop.
Al di l delle interfacce citate, ne esistono altre che permettono laccesso ai file
system di Hadoop via HTTP e FTP (queste solo per HDFS), oltre che come
WebDAV.
Gestire e serializzare i dati
Avere un file system va bene, ma serve un meccanismo che rappresenti i dati e li
memorizzi al suo interno. Vediamo alcune di queste soluzioni.

Linterfaccia Writable
Agli sviluppatori fa comodo poter manipolare tipi di dati a un livello pi alto e
lasciare che sia Hadoop a occuparsi dei processi necessari per serializzarli in
byte da scrivere su un file system e poi ricostruirli da uno stream di byte quando
vengono letti dal file system.
Il package org.apache.hadoop.io contiene linterfaccia Writable, che fornisce questo
meccanismo ed specificata come segue:
public interface Writable
{
void write(DataOutput out) throws IOException ;
void readFields(DataInput in) throws IOException ;
}

Lo scopo principale di questa interfaccia quello di offrire un metodo per


serializzare e deserializzare i dati quando vengono passati in rete o vengono scritti
e letti dal disco.
Quando prenderemo in esame i framework di elaborazione su Hadoop nei
prossimi capitoli, vedremo spesso delle situazioni in cui si richiede che un
argomento di dati sia di tipo Writable. Se usiamo strutture di dati che forniscono
unimplementazione adatta a questa interfaccia, allora Hadoop potr gestire
automaticamente la serializzazione e deserializzazione del tipo di dati senza che
debba sapere nulla di quello che rappresentano o di come vengono utilizzati.

Le classi wrapper
Fortunatamente, non dovete partire da zero e costruire le varianti di Writable
per tutti i tipi di dati che userete. Hadoop offre delle classi che racchiudono i tipi
di primitive Java e implementano linterfaccia Writable. Si trovano nel package
org.apache.hadoop.io.

Queste classi sono concettualmente simili alle classi wrapper primitive, come
Integer e Long, che si trovano in java.lang. Ammettono un unico valore primitivo
che pu essere impostato sia in fase di costruzione sia tramite un metodo setter.
Sono le seguenti:
BooleanWritable ;
ByteWritable ;
DoubleWritable ;
FloatWritable ;
IntWritable;
LongWritable;
VIntWritable: un tipo integer a lunghezza variabile;

VLongWritable: un tipo long a lunghezza variabile;

C poi Text, che contiene java.lang.String.

Classi wrapper per gli array


Hadoop fornisce anche classi contenitore basate su collezioni che offrono
wrapper Writable per gli array di altri oggetti Writable. Per esempio, unistanza
potrebbe contenere un array di IntWritable o DoubleWritable ma non array dei tipi
grezzi int o float. necessaria una sottoclasse specifica per la classe richiesta,
come le seguenti:
ArrayWritable;
TwoDArrayWritable.

Le interfacce Comparable e
WritableComparable
Quando abbiamo detto che le classi wrapper implementano Writable siamo stati
leggermente imprecisi; in realt implementano uninterfaccia composta chiamata
WritableComparable nel package org.apache.hadoop.io che combina Writable con

linterfaccia standard java.lang.Comparable:


public interface WritableComparable extends Writable, Comparable
{}

Lutilit di Comparable diventer chiara quando esploreremo MapReduce nel


prossimo capitolo; per ora baster ricordare che le classi wrapper forniscono i
meccanismi per poter essere serializzate e ordinate da Hadoop o da uno qualsiasi
dei suoi framework.
Storage dei dati
Finora abbiamo presentato larchitettura di HDFS e abbiamo mostrato come
memorizzare e recuperare in modo programmatico i dati usando gli strumenti a
riga di comando e lAPI Java. In tutti gli esempi abbiamo dato per scontato che i
nostri dati fossero salvati come file di testo. In realt, alcune applicazioni e dataset
richiedono strutture ad hoc per racchiudere il contenuto dei file. Negli anni, sono
stati creati formati di file per assecondare sia i requisiti di elaborazione di
MapReduce (vogliamo, per esempio, che i dati siano divisibili), sia per soddisfare
lesigenza di modellare dati strutturati e non strutturati.
Attualmente ci si concentrati molto sullindividuazione di casi duso dello
storage e della modellazione dei dati relazionali. Nel resto del capitolo vedremo i
formati di file pi adottati nellecosistema di Hadoop.

Serializzazione e contenitori
Quando parliamo di formati di file, ci riferiamo a due tipi di scenari.
Serializzazione: vogliamo codificare le strutture di dati generate e manipolate
in fase di elaborazione in un formato che possiamo salvare su un file,
trasmettere e, pi estesamente, recuperare e ritradurre per un unulteriore
manipolazione.
Contenitori: una volta che i dati sono serializzati nei file, i contenitori offrono
un modo per raggruppare pi file e aggiungere altri metadati.

Compressione
Quando si lavora con i dati, la compressione pu far risparmiare molto in
termini di spazio necessario per salvare i file e di I/O dei dati in rete e dai/sui
dischi locali.
Quando si utilizza un framework di elaborazione, la compressione pu avvenire
in tre punti della pipeline:
nei file di input da elaborare;
nei file di output risultato dellelaborazione;
nei file intermedi/temporanei prodotti nella pipeline.
Quando aggiungiamo la compressione in una di queste fasi, abbiamo
lopportunit di ridurre drasticamente la quantit di dati da leggere e scrivere su
disco o sulla rete. La cosa si rivela molto utile con i framework come MapReduce
che potenzialmente possono, per esempio, produrre volumi di dati temporanei che
sono pi grossi dei dataset di input o di output.
Apache Hadoop offre diversi codec di compressione, quali gzip, bzip2, LZO,
snappy, ognuno con i suoi pro e i suoi contro. La scelta del codec dipende dal tipo
di dati da elaborare e dalla natura del framework di elaborazione.
Diversamente dal consueto compromesso tra spazio e tempo, in cui un aumento
dello spazio corrisponde a un calo della velocit di compressione e
decompressione (e viceversa), dovremo considerare che i dati memorizzati in
HDFS saranno oggetto dellaccesso di software parallelo e distribuito, e che ci
sono software che aggiungono anche requisiti propri sui formati di file.
MapReduce, per esempio, pi efficiente sui file che possono essere suddivisi in
sottofile validi.
Questo pu rendere pi complesso il decidere se comprimere e quale codec
usare nel caso, poich la maggior parte dei codec (come gzip) non supporta la
suddivisione dei file, mentre qualcuno lo fa (come LZO).

Formati di file general purpose


La prima classe di formati di file quella general purpose; questi formati
possono essere applicati a qualsiasi dominio e prescindono dalla struttura dei dati
o dai pattern di accesso.
Testo: lapproccio pi semplice allo storage dei dati su HDFS luso di file
piatti. I file di testo possono essere utilizzati per contenere dati sia non
strutturati (una pagina web o un tweet) sia strutturati (un file CSV lungo
qualche milione di righe). I file di testo possono essere suddivisi, anche se
bisogna pensare a come gestire i confini tra i vari elementi nel file (per
esempio le righe).
SequenceFile: un SequenceFile una struttura di dati piatti costituita da
coppie binarie chiave/valore introdotta per soddisfare requisiti specifici
dellelaborazione basata su MapReduce. ancora molto utilizzato in
MapReduce come formato di input/output. Come vedremo nel Capitolo 3,
internamente, gli output temporanei delle mappe sono memorizzati usando
SequenceFile.
SequenceFile fornisce le classi Writer, Reader e Sorter rispettivamente per
scrivere, leggere e ordinare i dati. A seconda del meccanismo di compressione in
uso, se ne possono distinguere tre varianti.
Record chiave/valore non compressi.
Record chiave/valore compressi. Solo i valori vengono compressi.
Record chiave/valore di blocco compressi. Le chiavi e i valori sono raccolti
in blocchi di dimensioni arbitrarie e compressi separatamente.
In tutti questi casi la struttura SequenceFile rimane comunque divisibile, cosa
che costituisce uno dei suoi principali punti di forza.

Formati di dati orientati alle colonne


Nel mondo dei database relazionali, vi sono data store che organizzano e
memorizzano le tabelle in base alle colonne; in parole povere, i dati di ciascuna
colonna verranno memorizzati insieme. un approccio decisamente diverso da
quello della maggior parte dei DBMS relazionali, che organizzano i dati per riga.
Lo storage per colonna garantisce notevoli vantaggi a livello di prestazioni; per
esempio, se una query deve leggere solo due colonne in una tabella molto grande
che ne contiene centinaia, acceder solo ai dati delle colonne richieste. Un
database tradizionale orientato alle righe leggerebbe tutte le colonne per ciascuna
riga di cui sono richiesti i dati. Limpatto sui carichi di lavoro notevole, poich
le funzioni aggregate sono calcolate su numeri molto grossi di voci simili, come
il caso dei carichi di lavoro OLAP tipici dei sistemi di data warehouse.
Nel Capitolo 7, vedremo come Hadoop si stia trasformando nel backend SQL
nel mondo dei data warehouse grazie a progetti come Apache Hive e Cloudera
Impala. Sono stati infatti sviluppati alcuni nuovi formati di file tanto per le
necessit della modellazione relazionale quanto per le esigenze dei data
warehouse.
RCFile, ORC e Parquet sono tre dei formati di file orientati alle colonne pi
recenti sviluppati tenendo conto di questi casi duso.

RCFile
Il formato Row Columnar File (RCFile) fu sviluppato originariamente da
Facebook per lo storage di backend del suo sistema di data warehouse Hive, che
fu il primo sistema mainstream SQL su Hadoop disponibile come open source.
RCFile punta a offrire quanto segue:
caricamento veloce dei dati;
elaborazione rapida delle query;
uso efficiente dello storage;
adattabilit ai carichi di lavoro dinamici.
Trovate ulteriori informazioni su RCFile alla pagina http://bit.ly/1ANuO5Q.

ORC
Il formato di file Optimized Row Columnar (ORC) ha lo scopo di combinare le
prestazioni di RCFile con la flessibilit di Avro. studiato soprattutto per
lavorare con Apache Hive ed stato sviluppato inizialmente da Hortonworks per
superare i limiti percepiti degli altri formati di file disponibili. Trovate ulteriori
informazioni alla pagina http://bit.ly/11efF3t.

Parquet
Parquet (http://parquet.incubator.apache.org) il frutto dello sforzo congiunto di
Cloudera, Twitter e Criteo, e ora stato donato allApache Software Foundation. I
suoi obiettivi sono quelli di fornire un formato di file colonnare moderno e a
prestazioni elevate da usare con Cloudera Impala. Come Impala, stato ispirato
dal documento su Dremel (http://research.google.com/pubs/pub36632.html). Permette di
lavorare con strutture di dati complesse e annidate e consente una codifica
efficiente a livello di colonne.

Avro
Apache Avro ( http://avro.apache.org) un contenitore di file e un formato di
serializzazione di dati binari orientato allo schema. la nostra scelta in tutto il
libro per quanto riguarda il formato binario. Pu essere suddiviso e compresso, il
che lo rende efficace per lelaborazione dei dati con framework come
MapReduce.
Sono molti i progetti che includono un supporto specifico e unintegrazione con
Avro, che si pu quindi applicare diffusamente. Quando i dati vengono salvati in
un file Avro, con esso viene salvato anche il suo schema, definito come un oggetto
JSON. Un file pu essere elaborato successivamente da una terza parte senza una
conoscenza preliminare di come i dati sono codificati. Questo permette
lautodescrizione e ne agevola luso con i linguaggi dinamici e di scripting. Il
modello di schema in lettura favorisce anche lefficienza dello storage dei record,
poich non occorre che i singoli campi vengano taggati.
Nei prossimi capitoli vedremo come queste propriet possono facilitare la
gestione del ciclo di vita dei dati e consentire operazioni complesse come la
migrazione dei dati.

Utilizzare lAPI Java


Vedremo ora luso dellAPI Java per il parsing degli schemi di Avro, la lettura e
la scrittura dei file Avro e luso degli strumenti di generazione del codice Avro. Il
formato intrinsecamente indipendente dal linguaggio: ci sono API per la maggior
parte dei linguaggi, e i file creati da Java possono essere letti senza problemi da
qualsiasi altro linguaggio.
Gli schemi di Avro sono descritti come documenti JSON e sono rappresentati
dalla classe org.apache.avro.Schema. Per illustrare lAPI di manipolazione dei
documenti Avro, considereremo una specifica che utilizzeremo per una tabella di
Hive nel Capitolo 7. Il codice che segue si trova alla pagina http://bit.ly/1NounZJ.
Qui utilizzeremo lAPI Java Avro per creare un file Avro che contiene un record
di un tweet, e poi rileggeremo il file usando lo schema in esso contenuto per
estrarre i dettagli dei record memorizzati:
public static void testGenericRecord() {
try {
Schema schema = new Schema.Parser()
.parse(new File(tweets_avro.avsc));
GenericRecord tweet = new GenericData
.Record(schema);

tweet.put(text, The generic tweet text);

File file = new File(tweets.avro);


DatumWriter<GenericRecord> datumWriter =
new GenericDatumWriter<>(schema);
DataFileWriter<GenericRecord> fileWriter =
new DataFileWriter<>( datumWriter );

fileWriter.create(schema, file);
fileWriter.append(tweet);
fileWriter.close();

DatumReader<GenericRecord> datumReader =
new GenericDatumReader<>(schema);
DataFileReader<GenericRecord> fileReader =
new DataFileReader(file, datumReader);
GenericRecord genericTweet = null;

while (fileReader.hasNext()) {
genericTweet = (GenericRecord) fileReader
.next(genericTweet);

for (Schema.Field field :


genericTweet.getSchema().getFields()) {
Object val = genericTweet.get(field.name());

if (val != null) {
System.out.println(val);
}
}

}
} catch (IOException ie) {
System.out.println(Error parsing or writing file.);
}
}

Lo schema tweets_avro.avsc (http://bit.ly/17XuP0i) descrive un tweet con pi campi.


Per creare un oggetto Avro di questo tipo, analizziamo prima il file dello schema,
quindi sfruttiamo il concetto di Avro di GenericRecord per costruire un documento
Avro che vi si attiene. In questo caso impostiamo un unico attributo, cio il testo
del tweet.
Per scrivere il file Avro che contiene un unico oggetto useremo le capacit
di I/O di Avro. Per leggere il file non serve iniziare con lo schema, poich
possiamo estrarlo dal GenericRecord che leggiamo dal file. Procediamo quindi nella
struttura dello schema ed elaboriamo dinamicamente il documento in base ai campi
noti. La funzionalit particolarmente potente, perch ci che abilita i client che
rimangono indipendenti dallo schema Avro e dalle sue evoluzioni nel tempo.
Se per gi disponiamo di un file di schema, possiamo usare la generazione del
codice Avro per creare una classe personalizzata che faciliti la manipolazione dei
record Avro. Per generare il codice, utilizzeremo la classe di compilazione in avro-
tools.jar, passandole il nome del file dello schema e la directory di output

desiderata:
$ java -jar /opt/cloudera/parcels/CDH-5.0.0-1.cdh5.0.0.p0.47/lib/avro/avro-tools.jar compile
schema tweets_avro.avsc src/main/java

La classe verr collocata in una struttura di directory che si basa su uno


qualsiasi dei namespace definiti nello schema. Poich questo stato creato nel
namespace com.learninghadoop2.avrotables, vedremo quanto segue:
$ ls src/main/java/com/learninghadoop2/avrotables/tweets_avro.java

Con questa classe, rivisitiamo la creazione delloperazione di lettura e scrittura


degli oggetti Avro:
public static void testGeneratedCode() {
tweets_avro tweet = new tweets_avro();
tweet.setText(The code generated tweet text);

try {
File file = new File(tweets.avro);
DatumWriter<tweets_avro> datumWriter =
new SpecificDatumWriter<>(tweets_avro.class);
DataFileWriter<tweets_avro> fileWriter =
new DataFileWriter<>(datumWriter);

fileWriter.create(tweet.getSchema(), file);
fileWriter.append(tweet);
fileWriter.close();

DatumReader<tweets_avro> datumReader =
new SpecificDatumReader<>(tweets_avro.class);
DataFileReader<tweets_avro> fileReader =
new DataFileReader<>(file, datumReader);

while (fileReader.hasNext()) {
tweet = fileReader.next(tweet);
System.out.println(tweet.getText());
}
} catch (IOException ie) {
System.out.println(Error in parsing or writing
files.);
}
}

Poich ci siamo serviti della generazione del codice, usiamo ora il meccanismo
dello SpecificRecord di Avro insieme alla classe generata che rappresenta loggetto
nel nostro modello di dominio. Possiamo poi istanziare direttamente loggetto e
accedere ai suoi attributi attraverso i consueti metodi get/set.
Scrivere il file unoperazione simile alla precedente, tranne per il fatto che si
utilizzano classi specifiche e si recupera lo schema direttamente dalloggetto tweet
quando necessario. Anche la lettura viene facilitata in modo analogo grazie alla
capacit di creare istanze di una classe specifica e alluso dei metodi get/set.
Riepilogo
In questo capitolo abbiamo compiuto una panoramica dello storage su un cluster
Hadoop. Nello specifico, abbiamo visto quanto segue.
Larchitettura di alto livello di HDFS, il file system principale utilizzato in
Hadoop.
Come funziona HDFS, e in particolare il suo approccio allaffidabilit.
Come Hadoop 2 si integrato in HDFS, soprattutto nella forma del
NameNode HA e delle snapshot del file system.
Cos ZooKeeper e come viene utilizzato in Hadoop per abilitare funzioni
come il failover automatico dei NameNode.
Un giro tra gli strumenti a riga di comando usati per accedere a HDFS.
LAPI per i file system in Hadoop e come HDFS sia, a livello di codice, solo
unimplementazione di unastrazione di un file system pi flessibile.
Come serializzare i dati su un file system Hadoop e qual il supporto fornito
nelle classi core.
I vari formati di file con cui i dati vengono memorizzati pi spesso in Hadoop
e alcuni casi duso.
Nel prossimo capitolo vedremo nel dettaglio come Hadoop fornisce i
framework che possono essere impiegati per elaborare i dati in esso memorizzati.
Capitolo 3
Elaborazione: MapReduce e oltre

In Hadoop 1, la piattaforma aveva due componenti ben distinti: HDFS per lo


storage dei dati e MapReduce per la loro elaborazione. Nel capitolo precedente
abbiamo descritto levoluzione di HDFS in Hadoop 2; in questo capitolo,
affronteremo lelaborazione dei dati.
Il panorama dellelaborazione Hadoop 2 cambiato in modo pi significativo
dello storage, e Hadoop ora supporta diversi modelli di elaborazione a pieno
titolo. Nelle prossime pagine studieremo MapReduce e altri modelli
computazionali di Hadoop 2. In particolare vedremo quanto segue.
Cos MapReduce e lAPI Java richiesta per scrivere applicazioni per esso.
Come MapReduce viene implementato nella pratica.
Come Hadoop legge i dati nei e dai suoi job di elaborazione.
Cos YARN, il componente di Hadoop 2 che consente lelaborazione sulla
piattaforma a prescindere da MapReduce.
Unintroduzione ai diversi modelli computazionali implementati in YARN.
MapReduce
MapReduce il modello di elaborazione principale supportato in Hadoop 1.
Segue un principio divide et impera per lelaborazione dei dati reso popolare nel
2006 da un documento di Google (http://research.google.com/archive/mapreduce.html), e
ha le sue fondamenta nella programmazione funzionale e nella ricerca nei
database. Il nome stesso riflette i due passaggi distinti applicati a tutti i dati in
input, una funzione map e una funzione reduce.
Ogni applicazione di MapReduce una sequenza di job costruiti su questo
modello semplicissimo. A volte lapplicazione nel suo complesso pu richiedere
pi job, in cui loutput della fase reduce di uno linput della fase map di un altro, e
possono esserci anche pi funzioni map o reduce, ma il concetto rimane lo stesso.
Illustreremo MapReduce partendo dalla natura di map e reduce e poi descriveremo
lAPI Java necessaria per costruire le implementazioni delle due funzioni. Dopo
aver mostrato alcuni esempi, passeremo a un esempio di esecuzione di
MapReduce per fornire ulteriori indicazioni su come il suo framework esegue il
codice in runtime.
Imparare il modello di MapReduce pu non essere immediato; spesso difficile
cogliere come delle funzioni cos semplici possano, se combinate, fornire
unelaborazione articolata su dataset enormi. Ma cos, fidatevi!
Per esplorare la natura delle funzioni map e reduce, le valuteremo come se
venissero applicate a un flusso di record recuperato da un dataset sorgente.
Vedremo come pi avanti. Per ora immaginate i dati sorgente come se fossero
suddivisi in segmenti pi piccoli, ognuno dei quali alimenta unistanza specifica
della funzione map. A ciascun record applicata map, e questo produce un set di dati
intermedi. I record vengono recuperati da questo dataset temporaneo, e tutti i
record associati vengono passati attraverso la funzione reduce. Loutput finale di
reduce per tutti i set di record il risultato complessivo del job completo.

Da un punto di vista funzionale, MapReduce trasforma le strutture di dati da un


elenco di coppie (chiave, valore) in un altro. Durante la fase Map, i dati vengono
caricati da HDFS, e una funzione viene applicata in parallelo a ciascun input
(chiave, valore); loutput sar una nuova lista di coppie (chiave, valore):
map(k1,v1) -> list(k2,v2)
Il framework raccoglie poi tutte le coppie con la stessa chiave da tutte le liste e
le aggrega, creando un gruppo per ciascuna coppia. Una funzione Reduce viene
applicata in parallelo a ciascun gruppo, che a sua volta produce un elenco di
valori:
reduce(k2, list (v2)) -> k3,list(v3)

Loutput viene poi scritto nuovamente su HDFS come mostrato nella Figura 3.1.
API Java per MapReduce
LAPI Java per MapReduce esposta dal package org.apache.hadoop.mapreduce.
Scrivere un programma MapReduce non altro che compiere un subclassing delle
classi di base Mapper e Reducer fornite da Hadoop e poi un overriding dei metodi
map() e reduce() con la propria implementazione.

Figura 3.1 Fasi di Map e Reduce.

La classe Mapper
Per le nostre implementazioni di Mapper, si effettuer il subclassing della classe
di base Mapper e loverride del metodo map(), cos:
class Mapper<K1, V1, K2, V2>
{
void map(K1 key, V1 value Mapper.Context context)
throws IOException, InterruptedException
...
}

La classe viene definita in termini di tipi di input e output chiave/valore,


dopodich il metodo map prende una coppia chiave/valore con suo parametro.
Laltro parametro unistanza della classe Context che fornisce diversi meccanismi
per comunicare con il framework Hadoop, uno dei quali la generazione dei
risultati di un metodo map o reduce.
Notate che il metodo map fa riferimento a una sola istanza delle coppie
chiave/valore K1 e V1. Questo un aspetto fondamentale del paradigma di
MapReduce, in cui a noi spetta scrivere le classi che elaborano i singoli record,
mentre il framework responsabile di tutto il lavoro necessario per convertire il
dataset in uno stream di coppie chiave/valore.
Non dovrete mai scrivere classi map o reduce che provino a gestire lintero
dataset. Hadoop fornisce anche dei meccanismi che, attraverso le sue classi
InputFormat e OutputFormat, consentono limplementazione di formati di file comuni ed

eliminano la necessit di dover scrivere dei parser per tutti i tipi di file a
accezione di quelli personalizzati.
A volte pu essere necessario loverriding di altre tre metodi:
protected void setup( Mapper.Context context)
throws IOException, InterruptedException

Questo metodo viene chiamato prima che qualsiasi coppia chiave/valore venga
presentata al metodo map. Limplementazione di default non fa nulla:
protected void cleanup( Mapper.Context context)
throws IOException, InterruptedException

Questo metodo viene chiamato dopo che tutte le coppie chiave/valore sono state
presentate al metodo map. Limplementazione di default non fa nulla:
protected void run( Mapper.Context context)
throws IOException, InterruptedException

Questo metodo controlla il flusso complessivo dellelaborazione delle attivit


in una JVM. Limplementazione di default chiama una volta il metodo setup prima
di chiamare ripetutamente il metodo map per ciascuna coppia chiave/valore nel
segmento, e infine chiama il metodo cleanup.

La classe Reducer
La classe di base Reducer funziona in modo molto simile alla classe Mapper e
richiede solitamente solo delle sottoclassi per loverriding di un unico metodo
reduce(). Vediamo la sua definizione ridotta:

public class Reducer<K2, V2, K3, V3>


{
void reduce(K2 key, Iterable<V2> values,
Reducer.Context context)
throws IOException, InterruptedException
...
}

Notate anche qui la definizione della classe in termini di flusso di dati pi


ampio (il metodo reduce accetta K2/V2 come input e fornisce K3/V3 come output),
mentre il metodo reduce vero e proprio prende solo una chiave singola simile e il
suo elenco associato di valori. Loggetto Context anche in questo caso il
meccanismo per generare il risultato di questo metodo.
Questa classe ha anche i metodi setup, run e cleanup (con implementazioni
predefinite simili a quella della classe Mapper) che possono essere esclusi:
protected void setup(Reducer.Context context)

throws IOException, InterruptedException

Il metodo setup() viene chiamato una volta prima che le coppie chiave/valore
vengano presentate al metodo reduce. Limplementazione di default non fa nulla:
protected void cleanup(Reducer.Context context)
throws IOException, InterruptedException

Il metodo cleanup() viene chiamato una volta dopo che tutte le coppie
chiave/valore sono state presentate al metodo reduce. Limplementazione di default
non fa nulla:
protected void run(Reducer.Context context)
throws IOException, InterruptedException

Il metodo run() controlla il flusso complessivo dellelaborazione delle attivit in


una JVM. Limplementazione di default chiama una volta il metodo setup prima di
chiamare ripetutamente il metodo reduce per ciascuna coppia chiave/valore fornita
alla classe Reducer; infine chiama il metodo cleanup.

La classe Driver
La classe Driver comunica con il framework di Hadoop e specifica gli elementi
di configurazione necessari per eseguire il job di MapReduce. Questo implica dire
ad Hadoop quali classi Mapper e Reducer utilizzare, dove trovare i dati di input e in
che formato e dove collocare i dati di output e come formattarli.
La logica del driver solitamente gi presente nel metodo principale della
classe scritta per incapsulare un job di MapReduce. Non esiste una classe Driver
genitore da cui creare sottoclassi:
public class ExampleDriver extends Configured implements Tool
{
...
public static void run(String[] args) throws Exception
{
// Crea un oggetto Configuration che viene utilizzato
// per impostare altre opzioni
Configuration conf = getConf();

// Ottiene argomenti da riga di comando


args = new GenericOptionsParser(conf, args)
.getRemainingArgs();

// Crea loggetto che rappresenta il job


Job job = new Job(conf, ExampleJob);

// Imposta il nome della classe principale nel file JAR del job
job.setJarByClass(ExampleDriver.class);
// Imposta la classe mapper
job.setMapperClass(ExampleMapper.class);

// Imposta la classe reducer


job.setReducerClass(ExampleReducer.class);

// Imposta i tipi per loutput finale chiave/valore


job.setOutputKeyClass(Text.class);
job.setOutputValueClass(IntWritable.class);

// Imposta i percorsi dei file di input e output


FileInputFormat.addInputPath(job, new Path(args[0]));
FileOutputFormat.setOutputPath(job, new Path(args[1]));

// Esegue il job e aspetta che sia completato


System.exit(job.waitForCompletion(true) ? 0 : 1);
}

public static void main(String[] args) throws Exception


{
int exitCode = ToolRunner.run(new ExampleDriver(), args);
System.exit(exitCode);
}
}

Nelle righe di codice precedenti, org.apache.hadoop.util.Tool uninterfaccia per la


gestione delle opzioni da riga di comando. La gestione vera e propria delegata a
ToolRunner.run, che esegue con Tool la Configuration utilizzata per ottenere e impostare

le opzioni di configurazione di un job. Con il subclassing di


org.apache.hadoop.conf.Configured, possiamo impostare loggetto Configuration

direttamente dalla riga di comando attraverso GenericOptionsParser.


Considerato quello che sappiamo sui job, non dovrebbe sorprendere che gran
parte del setup implichi operazioni su un oggetto job; si tratta di impostare il nome
del job e di specificare quali classi dovranno essere utilizzate per le
implementazioni del mapper e del reducer.
Vengono impostate alcune configurazioni di input/output e, infine, vengono usati
gli argomenti passati al metodo principale per specificare le posizioni di input e
output per il job. un modello molto diffuso che incontrerete spesso.
Per le opzioni di configurazione sono possibili alcuni valori predefiniti, e ne
abbiamo utilizzati implicitamente alcuni nella classe precedente. Va notato che non
abbiamo detto niente circa il formato dei file o di come devono essere scritti i file
di output. Questi sono definiti attraverso le classi InputFormat e OutputFormat prima
citate; le vedremo nel dettaglio successivamente. I formati di input e output di
default sono file di testo che ben si adattano ai nostri esempi. Vi sono molti modi
per esprimere il formato nei file di testo, oltre che formati binari particolarmente
ottimizzati.
Un modello comune per i job di MapReduce meno complessi quello in cui le
classi Mapper e Reducer sono interne alloggetto driver. Questo permette di mantenere
tutto in un unico file, semplificando la distribuzione del codice.

Combiner
Hadoop consente luso di una classe combiner per eseguire un primo
ordinamento delloutput dal metodo map prima che sia recuperato dal reducer.
Gran parte del design di Hadoop si fonda sulla riduzione delle parti costose di
un job, che di solito coincidono con le operazioni di I/O su disco e di rete.
Loutput per il mapper spesso corposo; non insolito che arrivi a molte volte le
dimensioni dellinput originario.
Hadoop fornisce alcune opzioni che permettono di limitare limpatto del
trasferimento in rete di segmenti di dati grandi da parte dei reducer. La casse
combiner adotta un approccio diverso, dove possibile, per eseguire
unaggregazione preventiva per ridurre la mole di dati da trasferire.
La classe combiner non ha una propria interfaccia, deve avere la stessa
segnatura del reducer e quindi implica il subclassing della classe Reduce dal
package org.apache.hadoop.mapreduce. Leffetto quello di eseguire una mini-riduzione
sul mapper delloutput destinato a ciascun reducer.
Hadoop non garantisce che la classe combiner venga eseguita. A volte questo
potrebbe non accadere affatto, mentre altre volte potrebbe essere utilizzata una
volta, due volte o pi volte a seconda delle dimensioni e del numero dei file di
output file generati dal mapper per ogni reducer.
Partizionamento dei file
Una delle certezze implicite dellinterfaccia di Reduce che tutti i valori
associati a una data chiave vengono consegnati a un unico reducer. Quando ci sono
pi attivit di reduce in corso su un cluster, ogni output mapper deve essere
suddiviso in output separati destinati a ciascun reducer. Questi file partizionati
vengono salvati sul file system del nodo locale.
Il numero di attivit dei reducer sul cluster non dinamico come quello dei
mapper, tant che possiamo specificare il valore come parte dellinvio del job.
Hadoop sa quindi quanti reducer sono necessari per completare il job, e da questo
dedurr il numero di partizioni in cui dovr essere suddiviso loutput del mapper.

La funzione di partizionamento facoltativo


Nel package org.apache.hadoop.mapreduce si trova la classe Partitioner, una classe
astratta con la seguente segnatura:
public abstract class Partitioner<Key, Value>
{
public abstract int getPartition(Key key, Value value,
int numPartitions);
}

Di default, Hadoop utilizza una strategia di hash sulla chiave delloutput per
eseguire il partizionamento. Questa funzionalit presente nella classe
HashPartitioner nel package org.apache.hadoop.mapreduce.lib.partition, ma a volte pu

essere necessario fornire una sottoclasse personalizzata di Partitioner con una


logica di partizionamento specifica per lapplicazione. La funzione getPartition
prende la chiave, il valore e il numero delle partizioni come parametri, e ognuno
di essi pu essere utilizzato dalla logica di partizionamento personalizzata.
Una strategia di questo tipo si rivela utile, per esempio, quando i dati hanno
prodotto una distribuzione molto irregolare dopo lapplicazione della funzione di
hash standard. Una conseguenza del partizionamento irregolare che alcune
attivit eseguono molto pi lavoro di altre, allungando notevolmente i tempi
dellesecuzione complessiva del job.

Implementazione dei mapper e dei reducer


forniti da Hadoop
Non dobbiamo scrivere sempre da zero le nostre classi Mapper e Reducer. Hadoop
ce ne fornisce numerose implementazioni che possiamo utilizzare nei nostri job.
Senza loverride dei metodi in queste due classi, le implementazioni predefinite
sono le classi di identit Mapper e Reducer, che generano semplicemente linput senza
modifiche.
I mapper si trovano in org.apache.hadoop.mapreduce.lib.mapper e includono i seguenti.
InverseMapper: restituisce (valore, chiave) come output, ossia la chiave di input
prodotta in output come valore, e il valore di input prodotto in output
come chiave.
TokenCounterMapper: conta il numero di token distinti in ciascuna riga di input.

IdentityMapper: implementa la funzione di identit, mappando gli input

direttamente sugli output.


I reducer si trovano in org.apache.hadoop.mapreduce.lib.reduce e attualmente
includono i seguenti.
IntSumReducer: genera la somma della lista dei valori integer per chiave.
LongSumReducer: genera la somma della lista dei valori long per chiave.

IdentityReducer: implementa la funzione di identit, mappando gli input

direttamente sugli output.

Condividere i dati di riferimento


Di tanto in tanto, potremmo voler condividere i dati tra le attivit. Per esempio,
se dobbiamo eseguire unoperazione di ricerca su una tabella di traduzione ID-
stringa, potremmo volere che una sorgente di dati sia accessibile al mapper o al
reducer. Un approccio diretto quello di memorizzare i dati a cui vogliamo
accedere su HDFS e utilizzare lAPI FileSystem per interrogarlo come parte delle
fasi di Map o Reduce.
Hadoop offre un meccanismo alternativo per raggiungere lobiettivo di
condivisione dei dati tra le attivit nel job, ossia la DistributedCache definita
dalla classe org.apache.hadoop.mapreduce.filecache.DistributedCache. Pu essere utilizzata
per rendere disponibile in modo efficiente a tutti i nodi i file comuni di sola lettura
che sono usati dalle attivit di map o reduce.
I file possono essere dati di testo, come in questo caso, ma possono anche
essere dei JAR, dei dati binari o degli archivi; tutto possibile. I file da
distribuire vengono collocati su HDFS e aggiunti a DistributedCache nel driver
del job. Hadoop li copia sul file system locale di ogni nodo prima dellesecuzione
del job, cos che ogni attivit possa accedere localmente ai file.
In alternativa, si possono raccogliere i file necessari nel JAR del job inviato ad
Hadoop. I dati risultano cos vincolati al JAR, rendendo pi difficile la
condivisione tra job e necessaria la ricostruzione del JAR se i dati cambiano.
Scrivere programmi MapReduce
In questo capitolo ci concentreremo sui carichi di lavoro in batch; preso un set
di dati storici, ne esamineremo le propriet. Nei Capitoli 4 e 5 spiegheremo come
un tipo di analisi simile pu essere eseguita su uno stream di testo raccolto in
tempo reale.

Come iniziare
Negli esempi che seguono, ci baseremo su un dataset generato raccogliendo
1000 tweet usando lo script stream.py illustrato nel Capitolo 1:
$ python stream.py t n 1000 > tweets.txt

Copiamo il dataset in HDFS con:


$ hdfs dfs -put tweets.txt <destination>

NOTA
Finora abbiamo lavorato solo con il testo dei tweet. Nel resto del libro estenderemo stream.py in
modo da generare altri metadati dei tweet nel formato JSON. Tenetelo presente prima di
scaricare terabyte di messaggi con questo script.

Il nostro primo programma MapReduce sar il classico WordCount. Per


determinare i trending topic (cio gli argomenti di tendenza), utilizzeremo una sua
variante, dopodich analizzeremo il testo associato ai topic per stabilire se
esprime un sentiment positivo o negativo. Infine utilizzeremo un pattern di
MapReduce ChainMapper per radunare il tutto e presentare una pipeline di
dati per ripulire e preparare i dati di testo che invieremo al modello di analisi dei
trending topic e del sentiment.

Eseguire gli esempi


Il codice sorgente completo degli esempi descritti in questo paragrafo si trova
allindirizzo https://github.com/learninghadoop2/book-examples/tree/master/ch3.
Prima di eseguire il job in Hadoop, dobbiamo compilare il nostro codice e
riunire i file delle classi in un unico file JAR che invieremo al sistema.
Usando Gradle, potete costruire il file JAR cos:
$ ./gradlew jar
Cluster locale
I job vengono eseguiti su Hadoop tramite lopzione JAR nellutility a riga di
comando di Hadoop. Per farlo, specifichiamo il nome del file JAR, la classe
principale al suo interno e gli eventuali argomenti che verranno passati alla classe
principale, come mostrato in questo comando:
$ hadoop jar <job jarfile> <main class> <argument 1> <argument 2>

Elastic MapReduce
Come accennato nel Capitolo 1, Elastic MapReduce si aspetta che il file JAR
del job e i suoi dati di input si trovino in un bucket S3, e in S3 generer loutput.
AT T ENZIONE
Tutto questo costa dei soldi! Per questo esempio utilizzeremo la configurazione di cluster pi
piccola possibile disponibile per EMPR, ovvero un cluster con un unico nodo.

Come prima cosa, copiamo in S3 il dataset di tweet e lelenco di parole


positive e negative usando lutility a riga di comando aws:
$ aws s3 put tweets.txt s3://<bucket>/input
$ aws s3 put job.jar s3://<bucket>

Possiamo eseguire un job tramite lo strumento EMR a riga di comando come


segue, caricando il file JAR in s3://<bucket> e aggiungendo i passaggi per creare il
CUSTOM_JAR con la CLI aws:

$ aws emr add-steps --cluster-id <cluster-id> --steps \


Type=CUSTOM_JAR,\
Name=CustomJAR,\
Jar=s3://<bucket>/job.jar,\
MainClass=<class name>,\
Args=arg1,arg2,argN

Qui cluster-id lID di un cluster EMR in esecuzione, <class name> il nome


pienamente qualificato della classe principale e arg1,arg2,,argN sono gli argomenti
del job.

WordCount, lHello World di MapReduce


WordCount conta le occorrenze delle parole in un dataset. Trovate il codice
sorgente di questo esempio alla pagina http://bit.ly/18nBpwW. Considerate il seguente
blocco di codice:
public class WordCount extends Configured implements Tool
{
public static class WordCountMapper
extends Mapper<Object, Text, Text, IntWritable>
{
private final static IntWritable one = new IntWritable(1);
private Text word = new Text();
public void map(Object key, Text value, Context context
) throws IOException, InterruptedException {
String[] words = value.toString().split( ) ;
for (String str: words)
{
word.set(str);
context.write(word, one);
}
}
}
public static class WordCountReducer
extends Reducer<Text,IntWritable,Text,IntWritable> {
public void reduce(Text key, Iterable<IntWritable> values,
Context context
) throws IOException, InterruptedException {
int total = 0;
for (IntWritable val : values) {
total++ ;
}
context.write(key, new IntWritable(total));
}
}

public int run(String[] args) throws Exception {


Configuration conf = getConf();

args = new GenericOptionsParser(conf, args)


.getRemainingArgs();

Job job = Job.getInstance(conf);

job.setJarByClass(WordCount.class);
job.setMapperClass(WordCountMapper.class);
job.setReducerClass(WordCountReducer.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(IntWritable.class);

FileInputFormat.addInputPath(job, new Path(args[0]));


FileOutputFormat.setOutputPath(job, new Path(args[1]));

return (job.waitForCompletion(true) ? 0 : 1);


}

public static void main(String[] args) throws Exception {


int exitCode = ToolRunner.run(new WordCount(), args);
System.exit(exitCode);
}
}

Questo il nostro primo job di MapReduce completo. Guardando la struttura


dovreste riuscire a riconoscere gli elementi che abbiamo introdotto in precedenza:
la classe globale Job con la configurazione del driver nel suo metodo principale e
le implementazioni di Mapper e Reducer definite come classi annidate statiche.
Approfondiremo i meccanismi di MapReduce nel prossimo paragrafo, ma per
ora consideriamo il codice precedente e vediamo come realizza le trasformazioni
chiave/valore di cui abbiamo parlato.
Linput alla classe Mapper comprensibilmente il pi difficile da capire, perch
in effetti la chiave non viene usata. Il job specifica TextInputFormat come formato dei
dati di input e, di default, questo consegna al mapper quei dati in cui la chiave
loffset dei byte nel file e il valore il testo di quella riga.
Nella realt, potreste non vedere mai un mapper che usa questa chiave di offset
dei byte, ma comunque fornito.
Il mapper viene eseguito una volta per ciascuna riga di testo nella sorgente
dellinput, e ogni volta prende la riga e la suddivide in parole. A seguire usa
loggetto Context per produrre (emettere, come si dice) ogni nuova coppia
chiave/valore nella forma (parola/1). Questi sono i nostri valori K2/V2.
Abbiamo gi detto che linput al reducer una chiave e una lista di valori
corrispondenti; loperazione facilitata da una sorta di magia che avviene tra i
metodi map e reduce che raccolgono i valori da ciascuna chiave: la fase di shuffle,
che per ora non descriveremo. Hadoop esegue il reducer una volta per ogni
chiave, e limplementazione precedente del reducer conta semplicemente i numeri
nelloggetto Iterable e produce un output per ogni parola nella forma (parola,
conteggio). Questi sono i nostri valori K3/V3.
Guardiamo le segnature delle nostri classi mapper e reducer: la classe
WordCountMapper accetta IntWritable e Text come input e fornisce Text e IntWritable come

output. Nella classe WordCountReducer Text e IntWritable sono accettati sia come input
sia come output. Anche questo un pattern piuttosto comune, in cui il metodo map
esegue uninversione sulla chiave e i valori ed emette una serie di coppie di dati
su cui il reducer effettua laggregazione.
Il driver qui ha molto pi senso, visto che abbiamo dei valori reali per i
parametri. Utilizziamo gli argomenti passati alla classe per specificare le
posizioni di input e output.
Eseguite il job con:
$ hadoop jar build/libs/mapreduce-example.jar com.learninghadoop2.mapreduce.WordCount \
twitter.txt output

Esaminate loutput con un comando come il seguente; il nome di file effettivo


potrebbe essere diverso, quindi guardate nella directory chiamata output nella
vostra directory home in HDFS:
$ hdfs dfs -cat output/part-r-00000

Co-occorrenze di parole
Le parole che ricorrono insieme sono come frasi, ed probabile che le frasi pi
comuni e che ritornano spesso siano importanti. Nellelaborazione del linguaggio
naturale (la NLP), una lista di co-occorrenze di un termine chiamata n-gramma.
Gli n-gramma sono alla base di numerosi metodi statistici di analisi del testo.
Vedremo qui un esempio del caso speciale in cui un n-gramma una metrica che
spesso si incontra nelle applicazioni analitiche composto da due termini
(bigramma).
Unimplementazione elementare in MapReduce potrebbe essere unestensione di
WordCount che emette una chiave multicampo costituita da due parole separate da
tabulazione:
public class BiGramCount extends Configured implements Tool
{
public static class BiGramMapper
extends Mapper<Object, Text, Text, IntWritable> {
private final static IntWritable one = new IntWritable(1);
private Text word = new Text();

public void map(Object key, Text value, Context context


) throws IOException, InterruptedException {
String[] words = value.toString().split( );

Text bigram = new Text();


String prev = null;

for (String s : words) {


if (prev != null) {
bigram.set(prev + \t+\t + s);
context.write(bigram, one);
}

prev = s;
}
}
}

@Override
public int run(String[] args) throws Exception {
Configuration conf = getConf();

args = new GenericOptionsParser(conf, args).getRemainingArgs();


Job job = Job.getInstance(conf);
job.setJarByClass(BiGramCount.class);
job.setMapperClass(BiGramMapper.class);
job.setReducerClass(IntSumReducer.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(IntWritable.class);
FileInputFormat.addInputPath(job, new Path(args[0]));
FileOutputFormat.setOutputPath(job, new Path(args[1]));
return (job.waitForCompletion(true) ? 0 : 1);
}

public static void main(String[] args) throws Exception {


int exitCode = ToolRunner.run(new BiGramCount(), args);
System.exit(exitCode);
}
}

In questo job, sostituiamo WordCountReducer con


org.apache.hadoop.mapreduce.lib.reduce.IntSumReducer, che implementa la stessa logica. Il

codice sorgente di questo esempio si trova alla pagina http://bit.ly/1aQcgwP.

Trending topic
Il simbolo #, chiamato hashtag, usato per contrassegnare le parole o i topic in
un tweet. Originariamente fu creato dagli utenti di Twitter come sistema per
classificare i messaggi. Twitter Search (https://twitter.com/search-home) ha reso
popolare luso degli hashtag come metodo per connettersi e trovare il contenuto
correlato a topic specifici e le persone che ne parlano. Contando la frequenza con
cui un hashtag citato in un dato periodo di tempo, possiamo determinare quali
topic fanno tendenza nel social network:
public class HashTagCount extends Configured implements Tool
{
public static class HashTagCountMapper
extends Mapper<Object, Text, Text, IntWritable>
{
private final static IntWritable one = new IntWritable(1);
private Text word = new Text();

private String hashtagRegExp =


(?:\\s|\\A|^)[##]+([A-Za-z0-9-_]+);

public void map(Object key, Text value, Context context)


throws IOException, InterruptedException {
String[] words = value.toString().split( ) ;

for (String str: words)


{
if (str.matches(hashtagRegExp)) {
word.set(str);
context.write(word, one);
}
}
}
}

public int run(String[] args) throws Exception {


Configuration conf = getConf();

args = new GenericOptionsParser(conf, args)


.getRemainingArgs();

Job job = Job.getInstance(conf);


job.setJarByClass(HashTagCount.class);
job.setMapperClass(HashTagCountMapper.class);
job.setCombinerClass(IntSumReducer.class);
job.setReducerClass(IntSumReducer.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(IntWritable.class);

FileInputFormat.addInputPath(job, new Path(args[0]));


FileOutputFormat.setOutputPath(job, new Path(args[1]));

return (job.waitForCompletion(true) ? 0 : 1);


}

public static void main(String[] args) throws Exception {


int exitCode = ToolRunner.run(new HashTagCount(), args);
System.exit(exitCode);
}
}

Come nellesempio di WordCount, il testo viene scomposto nel mapper. Usiamo


unespressione regolare (hashtagRegExp) per individuare la presenza di un hashtag nel
testo di Twitter ed emettere lhashtag e il numero 1 quando viene trovato un
hashtag. Nella fase del reducer, contiamo poi il numero totale di occorrenze
emesse dellhashtag usando IntSumReducer.
Il codice sorgente completo di questo esempio si trova alla pagina
http://bit.ly/1ANB4ug.

Questa classe compilata si trover nel file JAR che abbiamo costruito in
precedenza con Gradle, quindi ora eseguiamo HashTagCount con il seguente comando:
$ hadoop jar build/libs/mapreduce-example.jar \
com.learninghadoop2.mapreduce.HashTagCount twitter.txt output

Analizziamo loutput come prima:


$ hdfs dfs -cat output/part-r-00000

Dovreste vedere un output simile a questo:


#whey 1
#willpower 1
#win 2
#winterblues 1
#winterstorm 1
#wipolitics 1
#women 6
#woodgrain 1

Ogni riga costituita da un hashtag e dal numero di volte che appare nel dataset
di tweet. Come potete vedere, il job di MapReduce ordina i risultati per chiave.
Se vogliamo trovare i topic pi citati, dovremo ordinare il set di risultati.
Lapproccio pi semplice quello di eseguire un ordinamento totale dei valori
aggregati selezionando poi i primi 10.
Se il dataset di output piccolo, possiamo convogliarlo nelloutput standard e
ordinarlo attraverso lutility sort:
$ hdfs dfs -cat output/part-r-00000 | sort -k2 -n -r | head -n 10

Una soluzione alternativa quella di scrivere un altro job di MapReduce per


percorrere lintero set dei risultati e ordinarlo per valore. Quando i dati diventano
tanti, questo tipo di ordinamento globale pu diventare costoso. Nel prossimo
paragrafo presenteremo un pattern pi efficiente per ordinare i dati aggregati.

Il pattern Top N
Nel pattern Top N, i dati ordinati vengono conservati in una struttura di dati
locale. Ogni mapper calcola un elenco dei record top N nel suo split e ne invia
lelenco al reducer. Unattivit del reducer trova i record globali top N.
Applicheremo questo pattern di design per implementare un job TopTenHashTag che
trova i primi dieci topic nel dataset. Il job prende come input i dati di output
generati da HashTagCount e restituisce una lista dei 10 hashtag pi citati.
In TopTenMapper usiamo TreeMap per mantenere un elenco degli hashtag in ordine
crescente. La chiave di questa mappa il numero di occorrenze; il valore una
stringa di hashtag separati da tabulazione e la loro frequenza. In map(), per ogni
valore, aggiorniamo la mappa topN. Quando topN ha pi di 10 voci, rimuoviamo la
pi piccola:
public static class TopTenMapper extends Mapper<Object, Text,
NullWritable, Text> {

private TreeMap<Integer, Text> topN = new TreeMap<Integer, Text>();


private final static IntWritable one = new IntWritable(1);
private Text word = new Text();
public void map(Object key, Text value, Context context) throws
IOException, InterruptedException {

String[] words = value.toString().split(\t) ;


if (words.length < 2) {
return;
}
topN.put(Integer.parseInt(words[1]), new Text(value));
if (topN.size() > 10) {
topN.remove(topN.firstKey());
}
}

@Override
protected void cleanup(Context context) throws IOException,
InterruptedException {
for (Text t : topN.values()) {
context.write(NullWritable.get(), t);
}
}
}

Non emettiamo alcuna coppia chiave/valore nella funzione map. Implementiamo


un metodo cleanup() che, una volta che il mapper ha consumato tutti i suoi input,
emette i valori (hashtag, conteggio) in topN. Usiamo una chiave NullWritable perch
vogliamo che tutti i valori siano associati alla stessa chiave, cos da poter eseguire
un ordinamento globale su tutte le liste top N dei mapper. Questo implica che il
nostro job eseguir solo un reducer.
Il reducer implementa una logica simile a quella che abbiamo in map().
Istanziamo TreeMap e usiamolo per mantenere una lista ordinata dei primi 10 valori:
public static class TopTenReducer extends
Reducer<NullWritable, Text, NullWritable, Text> {

private TreeMap<Integer, Text> topN = new TreeMap<Integer,


Text>();

@Override
public void reduce(NullWritable key, Iterable<Text> values,
Context context) throws IOException, InterruptedException {
for (Text value : values) {
String[] words = value.toString().split(\t) ;

topN.put(Integer.parseInt(words[1]),
new Text(value));

if (topN.size() > 10) {


topN.remove(topN.firstKey());
}
}

for (Text word : topN.descendingMap().values()) {


context.write(NullWritable.get(), word);
}
}
}

Infine, percorriamo topN in ordine discendente per generare la lista dei trending
topic.
NOTA
In questa implementazione, ignoriamo gli hashtag che hanno un valore di frequenza gi
presente in TreeMap quando si chiama topN.put(). A seconda dei casi, si consiglia di utilizzare
una struttura di dati diversa (come quella offerta dalla libreria,
https://code.google.com/p/guava-libraries/) o di adattare la strategia di aggiornamento.

Nel driver, applichiamo un unico reducer impostando job.setNumReduceTasks(1):


$ hadoop jar build/libs/mapreduce-example.jar \
com.learninghadoop2.mapreduce.TopTenHashTag \
output/part-r-00000 \
top-ten
Ora possiamo analizzare la top ten dei trending topic:
$ hdfs dfs -cat top-ten/part-r-00000
#Stalker48 150
#gameinsight 55
#12M 52
#KCA 46
#LORDJASONJEROME 29
#Valencia 19
#LesAnges6 16
#VoteLuan 15
#hadoop2 12
#Gameinsight 11

Trovate il codice sorgente di questo esempio alla pagina http://bit.ly/1MdZWE4.

Sentiment degli hashtag


Il processo di identificazione delle informazioni soggettive in una sorgente di
dati viene in genere definito sentiment analysis. Nellesempio precedente
abbiamo visto come individuare gli argomenti di tendenza in un social network;
ora analizzeremo il testo condiviso attorno a questi topic per determinare se
esprimono un sentimento pi positivo o pi negativo.
Un elenco di parole positive e negative per la lingua inglese il cosiddetto
lessico dopinione disponibile alla pagina http://www.cs.uic.edu/~liub/FBS/opinion-
lexicon-English.rar.

NOTA
Queste risorse e molte altre sono state raccolte dal gruppo del professor Bing Liu presso
lUniversit dellIllinois a Chicago, e sono state utilizzate, tra gli altri, in Bing Liu, Minqing Hu e
Junsheng Cheng, Opinion Observer: Analyzing and Comparing Opinions on the Web, Atti della
14ma International World Wide Web Conference (WWW-2005), 10-14 maggio 2005, Chiba,
Giappone.

In questo esempio presenteremo un metodo bag of words che, per quanto


semplicistico, pu essere utilizzato come riferimento per lanalisi delle opinioni
nel testo. Per ogni tweet e hashtag, conteremo il numero di volte in cui una parola
positiva o negativa appare, e normalizzeremo il conteggio in base alla lunghezza
del testo.
NOTA
Il modello bag of words un approccio utilizzato nellelaborazione in linguaggio naturale e nel
recupero delle informazioni per rappresentare i documenti di testo. In questo modello, il testo
rappresentato come il set (o bag) con la molteplicit delle sue parole, a prescindere dalle
propriet grammaticali o morfologiche e dallordine delle parole.
Decomprimete larchivio e collocate le liste di parole in HDFS con questa riga
di comando:
$ hdfs dfs put positive-words.txt <destination>
$ hdfs dfs put negative-words.txt <destination>

Nella classe Mapper, definiamo due oggetti che conterranno le liste di positiveWords
e negativeWords come Set<String>:
private Set<String> positiveWords = null;
private Set<String> negativeWords = null;

Eseguiamo loverride del metodo setup() di default del mapper in modo che una
lista di parole positive e negative specificate dalle due propriet di
configurazione job.positivewords.path e job.negativewords.path sia letta da HDFS
usando lAPI del file system come abbiamo visto nel capitolo precedente.
Avremmo anche potuto utilizzare DistributedCache per condividere questi dati nel
cluster. Il metodo helper parseWordsList legge un elenco di liste di parole, rimuove i
commenti e carica le parole in HashSet<String>:
private HashSet<String> parseWordsList(FileSystem fs, Path wordsListPath)
{
HashSet<String> words = new HashSet<String>();
try {

if (fs.exists(wordsListPath)) {
FSDataInputStream fi = fs.open(wordsListPath);

BufferedReader br =
new BufferedReader(new InputStreamReader(fi));
String line = null;
while ((line = br.readLine()) != null) {
if (line.length() > 0 && !line.startsWith(BEGIN_COMMENT)) {
words.add(line);
}
}

fi.close();
}
}
catch (IOException e) {
e.printStackTrace();
}

return words;
}

Nella fase del mapper, emettiamo per ogni hashtag nel tweet il sentiment
complessivo del tweet (semplicemente il conteggio delle parole positive meno
quello delle parole negative) e la lunghezza del tweet.
Utilizzeremo queste informazioni nel reducer per calcolare un rapporto di
sentiment complessivo valutato in base alla lunghezza dei tweet per stimare il
sentiment espresso da un tweet su un hashtag, cos:
public void map(Object key, Text value, Context context)
throws IOException, InterruptedException {
String[] words = value.toString().split( ) ;
Integer positiveCount = new Integer(0);
Integer negativeCount = new Integer(0);

Integer wordsCount = new Integer(0);

for (String str: words)


{
if (str.matches(HASHTAG_PATTERN)) {
hashtags.add(str);
}

if (positiveWords.contains(str)) {
positiveCount += 1;
} else if (negativeWords.contains(str)) {
negativeCount += 1;
}

wordsCount += 1;
}

Integer sentimentDifference = 0;
if (wordsCount > 0) {
sentimentDifference = positiveCount - negativeCount;
}

String stats ;
for (String hashtag : hashtags) {
word.set(hashtag);
stats = String.format(%d %d, sentimentDifference,
wordsCount);
context.write(word, new Text(stats));
}
}
}

Nella fase del reducer, sommiamo i punteggi del sentiment dati a ciascuna
istanza dellhashtag e dividiamo il risultato per la dimensione totale di tutti i tweet
in cui ricorso:
public static class HashTagSentimentReducer
extends Reducer<Text,Text,Text,DoubleWritable> {
public void reduce(Text key, Iterable<Text> values,
Context context
) throws IOException, InterruptedException {
double totalDifference = 0;
double totalWords = 0;
for (Text val : values) {
String[] parts = val.toString().split( ) ;
totalDifference += Double.parseDouble(parts[0]) ;
totalWords += Double.parseDouble(parts[1]) ;
}
context.write(key,
new DoubleWritable(totalDifference/totalWords));
}
}

Il codice sorgente di questo esempio si trova alla pagina http://bit.ly/1KwDad3.


Dopo aver eseguito il codice precedente, eseguite HashTagSentiment in questo
modo:
$ hadoop jar build/libs/mapreduce-example.jar com.learninghadoop2.mapreduce.HashTagSentiment
twitter.txt output-sentiment <positive words> <negative words>

Esaminate loutput con questo comando:


$ hdfs dfs -cat output-sentiment/part-r-00000

Dovreste vedere un output simile al seguente:


#1068 0.011861271213042056
#10YearsOfLove 0.012285135487494233
#11 0.011941109121333999
#12 0.011938693593171155
#12F 0.012339242266249566
#12M 0.011864286953783268
#12MCalleEnPazYaTeVasNicolas

In questo output, ogni riga costituita da un hashtag e dalla polarit del


sentiment a esso associata. Questo numero uneuristica che ci dice se un hashtag
associato pi a un sentiment positivo (polarit > 0) o pi a uno negativo (polarit
< 0) e ci informa sulla grandezza di un sentiment: pi il numero alto o basso, pi
il sentiment sar forte.

Pulizia del testo con ChainMapper


Negli esempi presentati finora, abbiamo ignorato un passaggio chiave
fondamentale per ogni applicazione costruita attorno allelaborazione del testo,
ovvero la normalizzazione e la pulizia dei dati di input. Le tre fasi pi comuni
della normalizzazione sono le seguenti.
Modifica della capitalizzazione in maiuscolo o minuscolo.
Eliminazione delle stop word.
Stemming.
In questo paragrafo vedremo come la classe ChainMapper (che si trova in
org.apache.hadoop.mapreduce.lib.chain.ChainMapper) permette di combinare

sequenzialmente una serie di mapper come primo passo di una pipeline di cleanup
dei dati. I mapper sono aggiunti al job configurato come segue:
ChainMapper.addMapper(
JobConf job,
Class<? extends Mapper<K1,V1,K2,V2>> klass,
Class<? extends K1> inputKeyClass,
Class<? extends V1> inputValueClass,
Class<? extends K2> outputKeyClass,
Class<? extends V2> outputValueClass, JobConf mapperConf)
Il metodo statico, addMapper, richiede il passaggio dei seguenti argomenti.
:
job JobConf per aggiungere la classe Mapper;
class: classe Mapper da aggiungere;

inputKeyClass: classe della chiave di input del mapper;

inputValueClass: classe del valore di input del mapper;

outputKeyClass: classe della chiave di output del mapper;

outputValueClass: classe del valore di output del mapper;

mapperConf: un JobConf con la configurazione per la classe Mapper.

In questo esempio, ci occuperemo della prima voce dellelenco appena


riportato: prima di calcolare il sentiment di ciascun tweet, convertiremo in
minuscolo ogni parola presente nel suo testo. In questo modo potremo accertarci
con pi precisione del sentiment degli hashtag ignorando le differenze nella
capitalizzazione tra i tweet.
Prima di tutto, definiamo una nuova classe Mapper LowerCaseMapper la cui
funzione map() chiama il metodo Java toLowerCase() di String sul valore di input ed
emette il testo in minuscolo:
public class LowerCaseMapper extends Mapper<LongWritable, Text, IntWritable, Text> {
private Text lowercased = new Text();
public void map(LongWritable key, Text value, Context context)
throws IOException, InterruptedException {
lowercased.set(value.toString().toLowerCase());
context.write(new IntWritable(1), lowercased);
}
}

Nel driver HashTagSentimentChain, possiamo configurare loggetto Job in modo che


le due classi Mapper siano concatenate ed eseguite:
public class HashTagSentimentChain
extends Configured implements Tool
{

public int run(String[] args) throws Exception {


Configuration conf = getConf();
args = new GenericOptionsParser(conf,args).
getRemainingArgs();

// posizione (su HDFS) dellelenco delle parole positive


conf.set(job.positivewords.path, args[2]);
conf.set(job.negativewords.path, args[3]);

Job job = Job.getInstance(conf);


job.setJarByClass(HashTagSentimentChain.class);

Configuration lowerCaseMapperConf = new


Configuration(false);
ChainMapper.addMapper(job,
LowerCaseMapper.class,
LongWritable.class, Text.class,
IntWritable.class, Text.class,
lowerCaseMapperConf);

Configuration hashTagSentimentConf = new


Configuration(false);
ChainMapper.addMapper(job,
HashTagSentiment.HashTagSentimentMapper.class,
IntWritable.class,
Text.class, Text.class,
Text.class,
hashTagSentimentConf);
job.setReducerClass(HashTagSentiment.
HashTagSentimentReducer.class);

job.setInputFormatClass(TextInputFormat.class);
FileInputFormat.addInputPath(job, new Path(args[0]));

job.setOutputFormatClass(TextOutputFormat.class);
FileOutputFormat.setOutputPath(job, new Path(args[1]));

return (job.waitForCompletion(true) ? 0 : 1);


}

public static void main (String[] args) throws Exception {


int exitCode = ToolRunner.run(
new HashTagSentimentChain(), args);
System.exit(exitCode);
}
}

Le classi LowerCaseMapper e HashTagSentimentMapper sono invocate in una pipeline, in


cui loutput della prima linput della seconda. Loutput dellultima Mapper sar
scritto nelloutput dellattivit. Un primo vantaggio immediato di questa struttura
la riduzione delle operazioni di I/O su disco. Le classi Mapper non devono sapere di
essere concatenate: quindi possibile riutilizzare delle classi Mapper specifiche che
possono essere combinate allinterno di ununica operazione. Questo pattern
presume che tutti i mapper (e i reducer) utilizzino coppie di output e input (chiave,
valore) che coincidono. Nessun casting n conversione vengono eseguiti da
ChainMapper di per s.

Infine, notate che la chiamata di addMapper per lultimo mapper nella catena
specifica le classi di output chiave/valore applicabili allintera pipeline del
mapper quando questo usato come composto.
Trovate il codice sorgente completo dellesempio alla pagina
http://bit.ly/17XEvIa.

Eseguite HashTagSentimentChain con il comando:


$ hadoop jar build/libs/mapreduce-example.jar
com.learninghadoop2.mapreduce.HashTagSentimentChain twitter.txt output <positive words>
<negative words>

Doveste vedere un output simile a quello dellesempio precedente. Osservate


che questa volta lhashtag in ogni riga in minuscolo.
Panoramica sullesecuzione di un job di
MapReduce
Per esplorare pi in dettaglio la relazione tra mapper e reducer, e per
dimostrare alcuni dei meccanismi di funzionamento pi interni di Hadoop,
analizzeremo come viene eseguito un job di MapReduce. La procedura si applica a
MapReduce sia in Hadoop 1 sia in Hadoop 2, per quanto nel secondo caso sia
implementata in modo molto diverso utilizzando YARN, di cui ci occuperemo pi
avanti nel capitolo. Ulteriori informazioni sui servizi descritti in questo paragrafo,
oltre ad alcuni suggerimenti per la risoluzione dei problemi nelle applicazioni
MapReduce, si trovano nel Capitolo 10.

Avvio
Il driver lunico elemento del codice che viene eseguito sulla nostra macchina
locale, e la chiamata a Job.waitForCompletion() avvia la comunicazione con il
JobTracker, che il nodo master nel sistema di MapReduce. Il JobTracker
responsabile di tutti gli aspetti della programmazione e dellesecuzione del job,
quindi diventer la nostra interfaccia principale quando eseguiremo le attivit
correlate alla gestione del job.
Per condividere le risorse sul cluster, il JobTracker pu utilizzare uno dei
numerosi approcci di programmazione per gestire i job in arrivo. Il modello
generale quello con alcune code su cui possono essere inviati i job insieme ai
criteri per assegnare le risorse alle varie code. Le implementazioni pi diffuse per
queste policy sono Capacity e Fair Scheduler.
Il JobTracker comunica con il NameNode per nostro conto e gestisce tutte le
interazioni legate ai dati salvati in HDFS.

Suddividere linput
La prima di queste interazioni si ha quando il JobTracker osserva i dati di input
e determina come assegnarli alle attivit di map. Ricordate che i file di HDFS
vengono solitamente suddivisi in blocchi di almeno 64 MB e che il JobTracker
assegna ciascun blocco a unattivit di map. Nel nostro esempio di WordCount
stata utilizzata una quantit di dati limitata che ben si adatta a un unico blocco. Se
immaginate un file di input molto pi grande, nellordine di qualche terabyte, il
modello di suddivisione avr pi senso. Ogni segmento del file split nella
terminologia di MapReduce viene elaborato da unattivit di map. Una volta che
ha calcolato gli split, il JobTracker li posiziona in HDFS insieme al file JAR che
contiene le classi Mapper e Reducer in una directory specifica per il job, il cui
percorso verr passato a ciascuna attivit allavvio.

Assegnazione delle attivit


Il servizio TaskTracker responsabile dellallocazione delle risorse,
dellesecuzione e della traccia dello stato delle attivit di map e reduce in esecuzione
su un nodo. Una volta che ha determinato quante operazioni map saranno necessarie,
controller il numero degli host nel cluster, quanti TaskTracker sono in funzione e
quante attivit map possono essere eseguite in contemporanea (una variabile di
configurazione definita dallutente). Il JobTracker verifica anche dove si trovano i
vari blocchi di dati di input data nel cluster e cerca di definire un piano di
esecuzione che riduca al minimo i casi in cui il TaskTracker elabora uno
split/blocco che si trova sullo stesso host fisico o, in caso di guasto, in cui ne
elabora almeno uno nello stesso rack hardware.
Questa ottimizzazione della posizione dei dati una delle ragioni principali che
permette ad Hadoop di elaborare in modo efficiente dataset cos corposi.
Ricordate anche che, di default, ogni blocco viene replicato su tre host diversi,
quindi la probabilit di produrre un piano di attivit/host in cui viene elaborata
localmente la maggior parte dei blocchi molto pi elevata di quanto possa
apparire in un primo tempo.

Avvio dellattivit
Ogni TaskTracker avvia un JVM separata per eseguire le attivit. Questo
aggiunge una penalit di tempo allavvio, ma isola il TaskTracker dai problemi
causati da un comportamento errato delle attivit di map o reduce, e permette di
configurare il job per la condivisione tra attivit eseguite in modo consequenziale.
Se un cluster abbastanza capace da poter eseguire tutte le attivit di map
contemporaneamente, queste verranno avviate e verr dato loro un riferimento per
lo split che devono elaborare e per il file JAR del job. Se il numero delle attivit
supera la capacit del cluster, il JobTracker manterr una coda di attivit in attesa
e le assegner ai nodi man mano che completano le attivit di map inizialmente
assegnate. Ora siamo pronti per vedere i dati eseguiti delle attivit di map. Vi
sembra che il lavoro da fare sia molto? Avete ragione; quando si esegue un
qualsiasi job di MapReduce ci vuole sempre un tempo non indifferente prima che
il sistema parta ed effettui tutta la procedura.

Monitorare il lavoro del JobTracker


Il JobTracker non smette di lavorare per aspettare che i TaskTracker eseguano
tutti i mapper e i reducer. Invece, continua a scambiare heartbeat e messaggi di
stato con i TaskTracker, cercando prove dei progressi o dei problemi. Inoltre
raccoglie le metriche dalle attivit durante lesecuzione del job, alcune fornite da
Hadoop e altre specificate dallo sviluppatore delle attivit di map e reduce, anche se
non le utilizzeremo in questi esempi.

Input della classe Mapper


La classe driver specifica il formato e la struttura del file di input usando
TextInputFormat; da questo Hadoop capisce di doverlo trattare come testo usando

loffset di byte come chiave e il contenuto della chiave come valore. Supponiamo
che il nostro dataset contenga questo testo:
This is a test
Yes it is

Alle due invocazioni del mapper verr fornito il seguente output:


1 This is a test
2 Yes it is

Esecuzione del mapper


Le coppie chiave/valore ricevute dal mapper sono rispettivamente loffset nel
file della riga e il contenuto di questa, a ragione di come il job stato configurato.
La nostra implementazione del metodo map in WordCountMapper ignora la chiave, perch
non ci interessa dove si trova ogni riga nel file, e suddivide il valore fornito in
parole usando il metodo split sulla classe String standard di Java. Una migliore
suddivisione in token si potrebbe ottenere tramite luso delle espressioni regolari
o della classe StringTokenizer, ma per i nostri scopi baster questo approccio pi
semplice. Per ogni singola parola, il mapper emette una chiave comprensiva della
parola effettiva e un valore 1.

Output del mapper e input del reducer


Loutput del mapper costituito da una serie di coppie nella forma (parola, 1):
(This,1), (is, 1), (a, 1), (test, 1), (Yes, 1), (it, 1), (is, 1)

Queste coppie di output del mapper non vengono passate direttamente al


reducer. Tra il mapping e il reducing c la fase di shuffle, dove avviene la
maggior parte della magia di MapReduce.

Input del reducer


Il reducer ricevuto dal TaskTracker si aggiorna tramite il JobTracker che gli
dice quali nodi nel cluster hanno partizioni map di output che devono essere
elaborate dalla sua attivit reduce locale. Quindi le recupera dai vari nodi e le
unisce in un unico file che verr convogliato nellattivit di reduce.

Esecuzione del reducer


La nostra classe WordCountReducer molto semplice; per ogni parola, conta il
numero di elementi nellarray ed emette loutput finale (parola, conteggio) per
ciascuna parola. Nel caso della nostra invocazione di WordCount sullinput di
esempio, tutte le parole tranne una hanno un unico valore nellelenco dei valori; is
ne ha due.

Output del reducer


Il set finale delloutput del reducer nel nostro esempio sar:
(This, 1), (is, 2), (a, 1), (test, 1), (Yes, 1), (it, 1)

Questi dati verranno generati nei file di partizione allinterno della directory di
output specificata nel driver che verr formattato usando limplementazione
OutputFormat specificata. Ogni attivit di reduce scrive su un unico file con il nome
di file part-r-nnnnn, dove nnnnn inizia a 00000 e viene incrementato.

Chiusura
Una volta che tutte le attivit sono state completate con successo, il JobTracker
genera lo stato finale del job sul client, insieme agli aggregati finali di alcuni dei
contatori pi importanti che ha raggruppato lungo il percorso. Il job completo e la
cronologia delle attivit sono disponibili nella directory di log su ciascun nodo o,
in modo pi accessibile, attraverso linterfaccia web del JobTracker; puntate il
browser alla porta 50030 sul nodo del JobTracker.

Input/Output
Abbiamo parlato della suddivisione dei file in split come parte della fase
iniziale del job e di come i dati in uno split vengano inviati allimplementazione
del mapper. Non abbiamo per considerato due aspetti: come i dati vengono
memorizzati nel file e come le singole chiavi e i valori vengono passati alla
struttura del mapper.

InputFormat e RecordReader
Per il primo di questi compiti Hadoop si serve di InputFormat. La classe astratta
InputFormat nel package org.apache.hadoop.mapreduce fornisce due metodi, come mostrato

nel codice seguente:


public abstract class InputFormat<K, V>
{
public abstract List<InputSplit> getSplits( JobContext context);
RecordReader<K, V> createRecordReader(InputSplit split,
TaskAttemptContext context) ;
}

Queste metodi visualizzano i due compiti della classe InputFormat:


fornire i dettagli su come suddividere un file di input negli split necessari per
lelaborazione di map;
creare un RecordReader che generi le serie di coppie chiave/valore da uno split.

RecordReader anche una classe astratta nel package org.apache.hadoop.mapreduce:


public abstract class RecordReader<Key, Value> implements Closeable
{
public abstract void initialize(InputSplit split,
TaskAttemptContext context);
public abstract boolean nextKeyValue()
throws IOException, InterruptedException;
public abstract Key getCurrentKey()
throws IOException, InterruptedException;
public abstract Value getCurrentValue()
throws IOException, InterruptedException;
public abstract float getProgress()
throws IOException, InterruptedException;
public abstract close() throws IOException;
}

Unistanza di RecordReader viene creata per ciascuno split e chiama getNextKeyValue


a restituire un valore Booleano che indica se disponibile unaltra coppia
chiave/valore; in caso affermativo, vengono utilizzati i metodi getKey e getValue per
accedere rispettivamente alla chiave e al valore.
La combinazione delle classi InputFormat e RecordReader quindi tutto ci che
occorre per creare un ponte tra qualsiasi tipo di dato e le coppie chiave/valore
richieste da MapReduce.

La classe InputFormat fornita da Hadoop


Hadoop fornisce alcune implementazioni di InputFormat nel package
org.apache.hadoop.mapreduce.lib.input.

FileInputFormat: una classe astratta di base che pu essere genitore di


qualsiasi input basato su file.
SequenceFileInputFormat: un efficiente formato di file binario. Lo vedremo nel

prossimo paragrafo.
TextInputFormat: viene utilizzato per i file di solo testo.

KeyValueTextInputFormat: viene utilizzato per i file di solo testo. Ogni riga

divisa in coppie chiave/valore da un byte separatore.


Notate che i formati di input non si limitano alla lettura dai file. Lo stesso
una sottoclasse di InputFormat. possibile fare in modo che Hadoop
FileInputFormat

usi dati non basati su file come input per i job di MapReduce; le sorgenti pi
comuni sono i database relazionali o orientati alle colonne, come Amazon
DynamoDB o HBase.

La classe RecordReader fornita da Hadoop


Hadoop fornisce alcune implementazioni comuni di RecordReader, presenti nel
package org.apache.hadoop.mapreduce.lib.input.

LineRecordReader: limplementazione la classe predefinita RecordReader per i file


di testo con loffset di byte nel file come chiave e il contenuto della riga come
valore.
SequenceFileRecordReader: limplementazione legge la coppia chiave/valore dal

contenitore SequenceFile.

OutputFormat e RecordWriter
Esiste un pattern analogo per scrivere un output di job coordinato da sottoclassi
di OutputFormat e RecordWriter dal package org.apache.hadoop.mapreduce. Non lo tratteremo
nel dettaglio, ma lapproccio simile, sebbene in OutputFormat le API siano pi
coinvolte, poich il pattern ha i suoi metodi per attivit come la validazione della
specifica delloutput.
questo il passaggio che comporta il fallimento di un job se esiste gi una
directory di output specificata. Per ottenere un comportamento diverso,
occorrerebbe una sottoclasse di OutputFormat che escluda questo metodo.

La classe OutputFormat fornita da Hadoop


I seguenti formati di output sono forniti dal package
org.apache.hadoop.mapreduce.output.

FileOutputFormat: la classe di base per tutti gli OutputFormat basati su file.


NullOutputFormat: unimplementazione fittizia che elimina loutput e non scrive
niente sul file.
SequenceFileOutputFormat: scrive sul formato binario SequenceFile.
TextOutputFormat: scrive un file di solo testo.

Notate che queste classi definiscono le implementazioni di RecordWriter richieste


come classi statiche annidate, quindi non ci sono implementazioni di RecordWriter
fornite separatamente.

SequenceFile
La classe SequenceFile nel package org.apache.hadoop.io fornisce un efficace formato
di file binario che spesso utile per loutput di un job di MapReduce. Vale
soprattutto se loutput del job viene elaborato come input di un altro job. I file
sequenza hanno numerosi vantaggi.
In quanto file binari, sono intrinsecamente pi compatti dei file di testo.
Supportano una compressione facoltativa supplementare che pu essere
applicata a diversi livelli, ossia su ciascun record o sullintero split.
Possono essere suddivisi ed elaborati in parallelo.
Questultima caratteristica particolarmente importante perch la maggior parte
dei formati binari, e soprattutto quelli che sono compressi o criptati, non pu
essere suddivisa e deve essere letta come un unico flusso lineare di dati. Se si
usano questi file come input di un job di MapReduce, verr impiegato un unico
mapper per elaborare il file, il che influir sulle prestazioni. In questi casi
preferibile utilizzare un formato suddivisibile, come SequenceFile, oppure
effettuare una pre-elaborazione che lo converta in questo senso. Pu essere un
inconveniente, perch la conversione occupa del tempo, ma in molti casi,
soprattutto nelle attivit di map pi complesse, questo verr controbilanciato dal
tempo risparmiato attraverso laumento del parallelismo.
YARN
YARN nato come parte delliniziativa MapReduce v2 (MRv2), ma ora un
sottoprogetto indipendente allinterno di Hadoop (quindi allo stesso livello di
MapReduce). Lo spunto da cui sorto il rendersi conto che MapReduce in
Hadoop 1 fondeva due responsabilit legate ma distinte: la gestione delle risorse e
lesecuzione dellapplicazione.
Per quanto abbia reso possibile lelaborazione prima inimmaginabile di dataset
enormi, a livello concettuale il modello di MapReduce ha un certo impatto sulle
prestazioni e sulla scalabilit. Implicito in esso che qualsiasi applicazione pu
essere costituita solo da una serie di job perlopi lineari, ognuno dei quali segue
un modello di una o pi azioni di map seguita da una o pi azioni di reduce. Questo
pattern ben si adatta ad alcune applicazioni, ma non a tutte. In particolare, mal si
presta ai carichi di lavoro che richiedono tempi di risposta a bassa latenza; i tempi
di avvio di MapReduce e le catene dei job spesso piuttosto lunghe superano di
molto la tolleranza per un processo che palese allutente. inoltre un modello
poco efficiente per i job che potrebbero essere rappresentati meglio come un DAG
(directed acyclic graph, grafico aciclico diretto) di attivit, i cui i nodi nel grafico
sono i passi dellelaborazione e gli archi sono i flussi di dati. Se analizzata ed
eseguita come DAG, allora lapplicazione pu essere eseguita in un solo passo
con un parallelismo elevato tra le varie fasi; se invece viene vista attraverso la
lente di MapReduce, il risultato solitamente una serie non efficiente di job
interdipendenti.
Diversi progetti hanno costruito numerosi tipi di elaborazione su MapReduce, e
sebbene molti abbiano avuto successo (Apache Hive e Pig sono due esempi
clamorosi), laccoppiamento stretto tra MapReduce come paradigma di
elaborazione e il meccanismo di programmazione dei job in Hadoop 1 ha reso
molto difficile per qualsiasi nuovo progetto ladattamento di questi ambiti alle
proprie necessit specifiche.
La soluzione YARN (Yet Another Resource Negotiator), che offre un
meccanismo di programmazione dei job di Hadoop molto potente e alcune
interfacce definite per i vari modelli di elaborazione da implementare al suo
interno.
Larchitettura di YARN
Per capire il funzionamento di YARN, importante smettere di pensare a
MapReduce e al modo in cui elabora i dati. YARN stesso non ci dice niente sulla
natura delle applicazioni costruite su di esso, mentre focalizzato sul fornire i
meccanismi per programmare ed eseguire i job. Come vedremo, YARN in grado
di ospitare tanto lelaborazione prolungata di stream di dati o di carichi di lavoro
a bassa latenza visibili allutente quanto di carichi di lavoro eseguiti in batch,
come MapReduce.

I componenti di YARN
YARN costituito da due componenti principali: il ResourceManager (RM),
che gestisce le risorse nel cluster, e il NodeManager (NM), che gira su ciascun
host e gestisce le risorse sulla singola macchina. Il ResourceManager e i
NodeManager si occupano della programmazione e della gestione dei contenitori,
una nozione astratta della memoria, della CPU e dellI/O che verranno dedicati a
una parte particolare del codice dellapplicazione. Prendendo MapReduce come
esempio, quando viene eseguito su YARN, il JobTracker e i TaskTracker girano
tutti nei loro contenitori specifici. Tuttavia, in YARN, ogni job di MapReduce ha il
proprio JobTracker dedicato; non c ununica istanza che gestisce tutti i job, come
in Hadoop 1.
YARN responsabile solo della programmazione delle attivit nel cluster; il
progresso a livello dellapplicazione, il monitoraggio e la tolleranza ai guasti sono
gestiti nel codice dellapplicazione. Si tratta di una decisione di design molto
chiara: rendendo YARN il pi indipendente possibile, i suoi compiti sono ben
definiti, e non vincola in modo innaturale i tipi di applicazione che possono essere
eseguiti su di esso.
Da arbitro di tutte le risorse del cluster, ha la capacit di gestirlo come intero
senza focalizzarsi sulle necessit delle risorse a livello di applicazione. Offre una
policy di programmazione nelle implementazioni fornite simile a quella di Hadoop
Capacity e Fair Scheduler; inoltre tratta il codice di tutte le applicazioni come
untrusted a prescindere, e tutte le attivit di gestione e controllo vengono eseguite
nello spazio utente.
Anatomia di unapplicazione YARN
Unapplicazione inviata a YARN ha due componenti: lApplicationMaster
(AM), che coordina il flusso generale dellapplicazione, e la specifica del codice
che verr eseguita sui nodi worker. Per MapReduce su YARN, il JobTracker
implementa la funzionalit ApplicationMaster, e i TaskTracker costituiscono il
codice personalizzato dellapplicazione distribuito sui nodi worker.
Come detto nel paragrafo precedente, in YARN, le responsabilit della gestione
dellapplicazione, del monitoraggio e della tolleranza ai guasti sono portate sul
livello dellapplicazione. Queste attivit vengono svolte dallApplicationMaster;
YARN non dice niente sui meccanismi di comunicazione tra lApplicationMaster e
il codice eseguito nei contenitori worker, per esempio.
Questa genericit consente alle applicazioni YARN di essere svincolate dalle
classi Java. LApplicationManager pu invece richiedere un NodeManager per
eseguire gli script della shell, le applicazioni native o qualsiasi altro tipo di
elaborazione resa disponibile su ciascun nodo.

Ciclo di vita di unapplicazione YARN


Come con i job di MapReduce in Hadoop 1, le applicazioni YARN vengono
inviate al cluster da un client. Quando unapplicazione YARN viene avviata, il
client prima chiama il ResourceManager (nello specifico la sua parte
ApplicationManager) e richiede il contenitore iniziale dentro il quale eseguire
lApplicationMaster. Nella maggior parte dei casi lApplicationMaster verr
eseguito da un contenitore nel cluster, cos come il resto del codice
dellapplicazione.
LApplicationManager comunica con laltro componente principale del
ResourceManager, lo scheduler vero e proprio, che ha la responsabilit finale di
gestire tutte le risorse nel cluster.
LApplicationMaster si avvia nel contenitore fornito, si coordina con il
ResourceManager e inizia il processo di negoziazione delle risorse necessarie.
Comunica con il ResourceManager e richiede i contenitori di cui ha bisogno. La
specifica di questi contenitori pu includere anche altre informazioni, come la
posizione desiderata nel cluster e le esigenze concrete delle risorse, come una
particolare quantit di memoria o di CPU.
Il ResourceManager passa allApplicationMaster i dettagli dei contenitori che
sono stati allocati, e lApplicationMaster comunica ai NodeManager di avviare
lattivit specifica dellapplicazione per ciascun contenitore. Per farlo, fornisce al
NodeManager la specifica dellapplicazione da eseguire, che, come detto, pu
essere un file JAR, uno script, un percorso per file locali eseguibili o qualunque
altra cosa un NodeManager pu invocare. Ogni NodeManager istanzia il
contenitore per il codice dellapplicazione e avvia questultima in base alla
specifica fornita.

Tolleranza ai guasti e monitoraggio


Da questo punto in avanti, il comportamento dipende molto dallapplicazione.
YARN non ne gestir il progresso ma eseguir una serie di attivit lungo il
percorso. LAMLivelinessMonitor nel ResourceManager riceve gli heartbeat da
tutti gli ApplicationMaster, e se determina che uno di questi ha fallito o ha smesso
di funzionare, lo de-registra e rilascia tutti i contenitori in esso allocati.
Dopodich il ResourceManager riprogramma lapplicazione per un numero di
volte configurabile.
Lungo questo processo, lNMLivelinessMonitor nel ResourceManager riceve
gli heartbeat dai NodeManager e tiene traccia della salute di ciascun
NodeManager nel cluster. Come per il monitoraggio della salute
dellApplicationMaster, un NodeManager verr considerato morto se non riceve
heartbeat per un tempo predefinito di 10 minuti, passati i quali anche tutti i
contenitori allocati verranno contrassegnati come morti e il nodo verr escluso da
una futura allocazione delle risorse.
Contemporaneamente, il NodeManager monitorer attivamente luso delle
risorse di ciascun contenitore allocato e, per quelle risorse non vincolate da limiti
rigidi, sopprimer i contenitori che superano la loro allocazione di risorse.
A un livello pi elevato, lo scheduler di YARN cercher sempre di
massimizzare lutilizzo del cluster entro i limiti della policy di condivisione
utilizzata. Come con Hadoop 1, questa consentir alle applicazioni a bassa priorit
di usare pi risorse del cluster se la concorrenza bassa, ma lo scheduler
ostacoler questi contenitori supplementari (richiedendo che vengano terminati) se
vengono inviate applicazioni a priorit pi elevata.
Il resto della responsabilit per la tolleranza ai guasti e il monitoraggio del
progresso a livello di applicazione deve essere implementato nel codice
dellapplicazione. Per MapReduce su YARN, per esempio, la gestione della
programmazione delle attivit e dei nuovi tentativi viene fornita a livello di
applicazione e non pi da YARN.

Pensare a livelli
Queste ultime affermazioni potrebbero suggerire che scrivere applicazioni da
eseguire su YARN sia un lavoro impegnativo, ed cos. LAPI YARN di livello
piuttosto basso e spaventa abbastanza la maggior parte degli sviluppatori che
vogliono semplicemente eseguire alcune attivit di elaborazione sui loro dati. Se
avessimo solo YARN e in ogni nuova applicazione Hadoop dovesse essere
implementato un ApplicationMaster, YARN non sarebbe cos interessante.
La cosa che migliora la situazione che, in generale, la richiesta non quella di
implementare ogni singola applicazione su YARN, bens di utilizzarlo per un
numero di framework di elaborazione pi piccoli che forniscono interfacce pi
friendly da implementare. La prima MapReduce; con questa ospitata su YARN,
lo sviluppatore scrive le consuete interfacce map e reduce, ed quasi totalmente
ignaro dei meccanismi di YARN.
Tuttavia, pu accadere che un altro sviluppatore esegua sullo stesso cluster un
job che utilizza un framework diverso con altre caratteristiche di elaborazione, e
che YARN li gestisca entrambi nello stesso tempo.
Forniremo i dettagli sui vari modelli di elaborazione di YARN attualmente
disponibili, che vanno dallelaborazione in batch alle query a bassa latenza fino
allelaborazione degli stream e dei grafici e oltre.
Con il crescere dellesperienza di YARN, tuttavia, sono nate alcune iniziative
che facilitano lo sviluppo dei framework di elaborazione. Da una parte abbiamo
interfacce di livello pi alto, come Kitten (https://github.com/cloudera/kitten) o
Apache Twill (http://twill.incubator.apache.org/), che forniscono astrazioni pi
accessibili sulle API YARN. forse per pi significativo lemergere di
framework che forniscono strumenti pi ricchi che facilitano la costruzione delle
applicazioni attraverso una classe generica comune di caratteristiche prestazionali.

Modelli di esecuzione
Abbiamo citato diverse applicazioni YARN che hanno caratteristiche di
elaborazione distinte, ma non abbiamo ancora parlato di come viene gestito il
ciclo di vita di unapplicazione YARN, distinguendo tra tre tipi principali:
applicazione per job, per sessione e always on.
Nellelaborazione in batch, come MapReduce su YARN, il ciclo di vita del
framework MapReduce legato a quello dellapplicazione. Se inviamo un job di
MapReduce, il JobTracker e i TaskTracker che lo eseguono vengono creati
specificatamente per quel job e vengono terminati quando questo completato.
Questo funziona bene in batch, ma se vogliamo fornire un modello pi interattivo,
loverhead di avvio per determinare lapplicazione YARN e le allocazioni di tutte
le sue risorse avr un impatto enorme sullesperienza dellutente se ogni comando
eseguito soffre di questa penalizzazione. Un ciclo di vita pi interattivo o basato
sulla sessione vedr lapplicazione YARN partire ed essere poi disponibile a
servire un certo numero di richieste/comandi inviati. Lapplicazione YARN
termina solo con luscita dalla sessione.
Rimane poi il concetto di applicazione a esecuzione prolungata, che elabora
stream di dati continui indipendenti da qualsiasi input interattivo. Per questi casi
ha pi senso che lapplicazione YARN parta ed elabori in continuazione i dati
recuperati attraverso qualche meccanismo esterno, Lapplicazione uscir solo
quando viene chiusa esplicitamente o in caso si verifichi una situazione non
ordinaria.
YARN nel mondo reale: il calcolo oltre
MapReduce
La trattazione precedente rimasta un po astratta, quindi in questo paragrafo
esploreremo alcune applicazioni YARN per vedere come utilizzano il framework e
come ampliano la capacit di elaborazione. Di particolare interesse sono i
framework YARN che adottano approcci diversi per gestire le risorse, la pipeline
di I/O e la tolleranza ai guasti.

Il problema con MapReduce


Finora, abbiamo considerato MapReduce in termini di API. MapReduce in
Hadoop pi di questo; fino ad Hadoop 2, era il motore di esecuzione predefinito
di diversi strumenti, tra cui Hive e Pig, che tratteremo nel dettaglio nei prossimi
capitoli. Abbiamo visto come le applicazioni MapReduce siano in realt delle
catene di job. Questo aspetto uno dei maggiori punti dolenti e fattori vincolanti
dei framework. MapReduce controlla i punti dei dati in HDFS per la
comunicazione tra processi.
Alla fine di ogni fase di reduce, loutput viene scritto su disco cos che possa
essere caricato dai mapper del job successivo e usato come suo input. Loverhead
di I/O introduce la latenza, specialmente quando abbiamo applicazioni che
richiedono pi passaggi su un dataset (da cui le scritture multiple).
Sfortunatamente, questo tipo di calcolo iterativo al centro di molte applicazioni
di analisi.
Apache Tez e Apache Spark sono due framework che affrontano questo
problema generalizzando il paradigma di MapReduce. Ne parleremo brevemente
nel resto di questo paragrafo, accanto ad Apache Samza, un framework che adotta
un approccio del tutto diverso allelaborazione in tempo reale.

Tez
Tez (http://tez.apache.org) unAPI a basso livello e un motore di esecuzione
concentrato sullelaborazione a bassa latenza, ed stata utilizzata come base
dellevoluzione pi recente di Hive, Pig e molti altri framework che implementano
operazioni standard di join, filtro, unione e raggruppamento.

Figura 3.2 Una catena di job di MapReduce.

Tez unimplementazione e unevoluzione di un modello di programmazione


presentato da Microsoft nel documento Dryad del 2009
(http://research.microsoft.com/en-us/projects/dryad/). Si tratta di una generalizzazione di
MapReduce come flusso di dati che cerca di realizzare una computazione rapida e
interattiva convogliando le operazioni di I/O in una coda per la comunicazione tra
processi. Questo impedisce le scritture costose su disco che interessano
MapReduce. LAPI fornisce primitive che esprimono dipendenze tra job come un
DAG. Il DAG completo viene poi inviato a un planner che pu ottimizzare il flusso
di esecuzione. La stessa applicazione rappresentata nel diagramma precedente
verrebbe eseguita in Tez come singolo job, con lI/O trasmesso da reducer a
reducer senza scritture e letture successive di HDFS da parte dei mapper (Figura
3.3).
Lesempio canonico di WordCount si trova allindirizzo http://bit.ly/1G5t8sO:
DAG dag = new DAG(WordCount);
dag.addVertex(tokenizerVertex)
.addVertex(summerVertex)
.addEdge(new Edge(tokenizerVertex, summerVertex,
edgeConf.createDefaultEdgeProperty()));

Sebbene la topologia dag del grafico possa essere espressa con poche righe di
codice, il boilerplate richiesto per eseguire il job notevole. Questo codice
gestisce molte delle responsabilit di programmazione ed esecuzione di basso
livello, compresa la tolleranza ai guasti. Quando Tez individua unattivit fallita,
ripercorre il grafico di elaborazione per trovare il punto da cui rieseguire le
attivit fallite.

Hive su Tez
Hive 0.13 il primo progetto di alto profilo per utilizzare Tez come motore di
esecuzione. Parleremo di Hive nel dettaglio nel Capitolo 7; per ora considereremo
solo come viene implementato su YARN.
Figura 3.3 Un DAG di Tez una generalizzazione di MapReduce.

Hive (http://hive.apache.org) un motore per linterrogazione dei dati


memorizzati in HDFS attraverso la sintassi SQL standard. Ha avuto un successo
enorme, poich questo tipo di capacit riduce di parecchio le barriere allavvio
dellesplorazione analitica dei dati in Hadoop.
In Hadoop 1, Hive non aveva altra scelta che implementare le sue istruzioni
SQL come serie di job di MapReduce. Quando SQL viene inviato ad Hive, genera
dietro le quinte i job di MapReduce richiesti e li esegue sul cluster. Questo
approccio ha due svantaggi principali: ogni volta c una penalit di tempo
tuttaltro che banale, e il modello vincolato di MapReduce fa s che le istruzioni
SQL apparentemente semplici vengano spesso tradotte in lunghe serie di pi job di
MapReduce dipendenti. Questo un esempio del tipo di elaborazione
concettualizzata naturalmente come un DAG di attivit, come descritto in
precedenza nel capitolo.
Sebbene quando Hive viene eseguito in MapReduce si ottengano alcuni
benefici, in YARN, i vantaggi principali si hanno in Hive 0.13, dove il progetto
viene reimplementato per intero usando Tez. Sfruttando le API di Tez, che sono
focalizzate sullelaborazione a bassa latenza, Hive raggiunge prestazioni ancora
migliori rendendo pi semplice la base di codice.
Poich Tez tratta i suoi carichi di lavoro come i DAG che forniscono un
adattamento migliore alla query SQL tradotte, Hive su Tez pu eseguire qualsiasi
istruzione SQL come un singolo job con il massimo del parallelismo.
Tez favorisce il supporto alle query interattive di Hive tramite un servizio
sempre attivo invece di richiedere che lapplicazione venga istanziata da zero a
ogni invio SQL. La cosa importante perch, per quanto le query che elaborano
grandi quantit di dati impieghino un certo tempo, lo scopo di Hive quello di
passare da semplice strumento di batch a strumento il pi possibile interattivo.

Apache Spark
Spark (http://spark.apache.org) un framework che eccelle nellelaborazione
interattiva e quasi in tempo reale. Creato presso lUC a Berkeley, stato donato
come progetto Apache. Spark fornisce unastrazione che consente ai dati in
Hadoop di essere visualizzati come struttura distribuita su cui possono essere
eseguite alcune serie di operazioni.
Il framework si basa sugli stessi concetti da cui Tez trae ispirazione (Dryad),
ma primeggia con i job che consentono ai dati di essere contenuti ed elaborati in
memoria, e pu programmare in modo efficiente lelaborazione sui dataset in
memoria nel cluster.
Spark controlla automaticamente la replica dei dati nel cluster, garantendo che
ciascun elemento del dataset distribuito sia contenuto in memoria su almeno due
macchine, e offre una tolleranza ai guasti basata sulla replica in qualche modo
simile ad HDFS.
Spark nato come sistema standalone, ma nella release 0.8 stato configurato
anche per girare su YARN. interessante perch, nonostante il suo modello di
elaborazione classico sia orientato al batch, fornisce con la shell un frontend
interattivo, e con il sottoprogretto Spark offre anche unelaborazione quasi in
tempo reale degli stream di dati. Spark qualcosa di diverso a seconda di chi lo
usa; tanto unAPI ad alto livello quanto un motore di esecuzione. Al momento
della stesura di questo libro, in via di sviluppo il porting a Spark di Hive e Pig.

Apache Samza
Samza (http://samza.apache.org) un framework di elaborazione degli stream
sviluppato in LinkedIn e donato allApache Software Foundation. Samza elabora
concettualmente stream di dati infiniti che vengono visti dallapplicazione come
una serie di messaggi.
Samza si integra attualmente in modo stretto con Apache Kafka
(http://kafka.apache.org), sebbene abbia unarchitettura pluggable. Kafka stesso un
sistema di messaging che eccelle con i grandi volumi di dati e fornisce
unastrazione basata sui topic simile a quella della maggior parte delle altre
piattaforme di messaging, come RabbitMQ. I publisher inviano messaggi ai topic,
e i clienti interessati utilizzano i messaggi dai topic man mano che arrivano. Kafka
ha alcune caratteristiche che lo distinguono da altre piattaforme di messaging; per
questa trattazione, quella che pi ci interessa la sua capacit di memorizzare i
messaggi per un dato periodo di tempo, il che permette la loro riproduzione nei
topic. I topic vengono suddivisi su pi host e le partizioni possono essere replicate
tra gli host per proteggere i nodi dal fallimento.
Samza costruisce il suo flusso di elaborazione in base al concetto di stream, che
quando si usa Kafka vengono mappati direttamente sulle partizioni di questo. Un
tipico job di Samza potrebbe rilevare un topic per quanto riguarda i messaggi in
arrivo, eseguire alcune trasformazioni e poi scrivere loutput su un topic diverso.
Pi job di Samza possono poi essere uniti per fornire strutture di elaborazione pi
complesse.
In quanto applicazione di YARN, Samza ApplicationMaster monitora la salute
di tute le attivit di Samza. Se una di queste fallisce, viene istanziata unattivit
sostitutiva in un nuovo contenitore.
Samza raggiunge la tolleranza ai guasti facendo scrivere a ogni attivit il suo
progresso in un nuovo stream (modellato nuovamente come un topic Kafka); in
questo modo qualsiasi attivit sostituiva dovr semplicemente leggere lo stato pi
recente di unattivit da questo topic di checkpoint e poi riprodurre il topic del
messaggio principale dallultima posizione elaborata. Samza offre inoltre un
supporto per lo stato locale dellattivit, utile per i carichi di lavoro di tipo join o
di aggregazione. Lo stato locale viene costruito sullastrazione dello stream, e
quindi intrinsecamente resiliente ai fallimenti dellhost.

Framework indipendenti da YARN


interessante notare che due dei progetti precedenti (Samza e Spark) girano su
YARN ma non sono specifici per esso. Spark nato come servizio indipendente e
prevede alcune implementazioni per altri scheduler, come Apache Mesos, o per
essere eseguito su Amazon EC2. Sebbene Samza oggi giri su YARN, la sua
architettura non specifica di YARN, e si sta discutendo se fornire delle release
per altre piattaforme.
Lindipendenza di YARN uno dei suoi vantaggi principali. Unapplicazione
scritta per usare YARN non devessere legato a essa; per definizione, tutte le
funzionalit della logica e della gestione dellapplicazione vera e propria sono
incapsulate nel codice della stessa, e sono indipendenti tanto da YARN quanto da
altre piattaforme. Questo non significa che progettare unapplicazione indipendente
dallo scheduler sia unoperazione semplice, ma almeno unoperazione
accessibile. Prima di YARN non era cos.

YARN oggi e oltre


Sebbene YARN sia stato usato in produzione per un certo tempo (in particolare
da Yahoo!), la versione GA finale non fu rilasciata prima della fine del 2012.
Anche le interfacce per YARN sono state piuttosto fluide fin verso la fine del
periodo di sviluppo. Possiamo quindi dire che la piattaforma YARN
completamente compatibile da Hadoop 2.2 relativamente recente.
YARN oggi pienamente funzionale, e per il futuro si prevedono estensioni alle
sue capacit. Tra le pi rimarchevoli segnaliamo la possibilit di specificare e
controllare le risorse del contenitore su pi dimensioni. Attualmente sono possibili
specifiche solo per la posizione, la memoria e la CPU, ma si prevedono sviluppi
in aree come quello dello storage e dellI/O di rete.
LApplicationMaster ha attualmente scarso controllo sulla gestione della
posizione condivisa dei contenitori. Un controllo pi raffinato gli consentir di
specificare le policy in base alle quali i contenitori possono o meno essere
programmati sullo stesso nodo. Il modello di allocazione delle risorse attuale
inoltre piuttosto statico, quindi sar utile consentire a unapplicazione di
modificare dinamicamente le risorse allocate a un contenitore in esecuzione.
Riepilogo
In questo capitolo abbiamo visto come elaborare quei grossi volumi di dati di
cui abbiamo parlato nel capitolo precedente. In particolare abbiamo trattato quanto
segue.
MapReduce come unico modello di elaborazione disponibile in Hadoop 1 e
il suo modello concettuale.
LAPI Java per MapReduce e come usarla per costruire alcuni esempi, dal
conteggio della parole alla sentiment analysis degli hashtag di Twitter.
I dettagli dellimplementazione pratica di MapReduce e lesecuzione di un
suo job.
Come Hadoop memorizza i dati e le classi coinvolti nella rappresentazione
dei formati di input e output e i reader e writer di valori.
I limiti di MapReduce che hanno portato allo sviluppo di YARN, aprendo la
strada ad altri modelli di calcolo sulla piattaforma Hadoop.
Larchitettura di YARN e come sono costruite le applicazioni su di esso.
Nei prossimi due capitoli ci allontaneremo dallelaborazione in batch in senso
stretto e ci tufferemo nel mondo dellelaborazione iterativa quasi in tempo reale
usando due dei framework ospitati da YARN che abbiamo introdotto in questo
capitolo, Samza e Spark.
Capitolo 4
Computazione in tempo reale con
Samza

Nel capitolo precedente abbiamo parlato di YARN e della portata che hanno i
modelli di calcolo e i framework di elaborazione allesterno del tradizionale
MapReduce basato sul batch abilitato sulla piattaforma Hadoop. In questo capitolo
e nel prossimo esploreremo due di questi progetti in profondit: Apache Samza e
Apache Spark. Abbiamo scelto questi framework perch dimostrano luso
dellelaborazione degli stream e iterativa e perch forniscono meccanismi
interessanti per combinare i vari paradigmi di elaborazione. In questo capitolo
studieremo Samza e tratteremo i seguenti argomenti.
Cos Samza e come si integra con YARN e con altri progetti come Apache
Kafka.
Come Samza fornisce una semplice interfaccia di basata sul callback per
lelaborazione degli stream.
Come Samza unisce pi job di elaborazione degli stream in flussi di lavoro
complessi.
Come Samza supporta lo stato locale persistente allinterno delle attivit per
arricchire tutto quanto pu abilitare.
Elaborazione degli stream con Samza
Per analizzare una piattaforma pura di elaborazione degli stream, utilizzeremo
Samza, disponibile allindirizzo https://samza.apache.org. Il codice l mostrato stato
testato con lattuale release 0.8, e manterremo il repository di GitHub aggiornato
man mano che il progetto evolve.
Samza stato realizzato in LinkedIn e poi donato allApache Software
Foundation nel settembre 2013. Negli anni, LinkedIn ha costruito un modello che
concettualizza gran parte dei suoi stream di dati; da questo si riscontrata la
necessit di un framework che fornisse un meccanismo friendly per lo sviluppatore
per elaborare questi stream ormai diffusi un po ovunque.
Il team di LinkedIn aveva rilevato che quando si tratta di elaborazione dei dati,
gran parte dellattenzione si concentrava agli estremi dello spettro; per esempio, i
carichi di lavoro RPC sono solitamente implementati come sistemi sincroni con
requisiti di latenza molto bassi o come sistemi batch in cui la periodicit dei job
spesso misurata in ore. Larea intermedia stato poco supportata, e questo
lambito a cui si punta Samza; la maggior parte dei suoi job prevede tempi di
risposta che vanno da millisecondi a minuti, presumendo inoltre che i dati arrivino
in uno stream teoricamente infinito di messaggi continuativi.

Come funziona Samza


Ci sono numerosi sistemi nel mondo dellopen source, come Storm
(http://storm.apache.org), e molti altri strumenti perlopi commerciali, come i sistemi

CEP (complex event processing), preposti anchessi allelaborazione di stream di


messaggi continui. Questi sistemi condividono alcune analogie ma hanno anche
alcune differenze importanti.
Nel caso di Samza, la differenza forse pi significativa riguarda la consegna dei
messaggi. Molti sistemi lavorano in maniera intensa per ridurre la latenza di
ciascun messaggio, partendo dal presupposto che lobiettivo sia quello di farlo
entrare e uscire dal sistema il pi rapidamente possibile.
Samza presume pi o meno lopposto; i suoi stream sono persistenti e resilienti,
e ogni messaggio scritto in uno stream pu essere letto nuovamente per un certo
periodo di tempo dopo che arrivato la prima volta. Come vedremo, un aiuto
non da poco relativamente alla tolleranza ai guasti. Samza si basa su questo
modello anche per consentire a ciascuna delle sue attivit di mantenere uno stato
locale resiliente.
Samza quasi sempre implementato in Scala, sebbene le sue API pubbliche
siano scritte in Java. In questo capitolo mostreremo alcuni esempi Java, ma per
implementare le applicazioni Samza pu essere utilizzato qualsiasi linguaggio per
JVM. Affronteremo Scala quando parleremo di Spark nel prossimo capitolo.

Larchitettura ad alto livello di Samza


Samza vede il mondo come fatto a tre livelli o componenti principali: lo
streaming, lesecuzione e lelaborazione.

Figura 4.1 Larchitettura di Samza.

Il livello dello streaming fornisce laccesso agli stream di dati, sia per il
consumo sia per la pubblicazione. Il livello di esecuzione offre il mezzo tramite
cui le applicazioni Samza possono essere eseguite, ottenere risorse di CPU e
memoria ed essere gestite nei propri cicli di vita. Il livello di elaborazione il
framework di Samza vero e proprio, e le sue interfacce permettono la funzionalit
basata sui messaggi.
Samza fornisce delle interfacce specifiche per supportare i primi due livelli,
sebbene le principali implementazioni correnti utilizzino Kafka per lo streaming e
YARN per lesecuzione. Approfondiremo largomento nei prossimi paragrafi.

Il miglior amico di Samza: Apache Kafka


Samza di per s non implementa lo stream di messaggi vero e proprio. Fornisce
piuttosto uninterfaccia per un sistema di messaggi con cui poi si integra.
Limplementazione predefinita dello stream costruita su Apache Kafka
(http://kafka.apache.org), un sistema di messaggi costruito anche in LinkedIn ma ora
un progetto open source di successo e ampiamente adottato.
Kafka pu essere considerato con un broker di messaggi simile a RabbitMQ o
ad ActiveMQ ma, come detto in precedenza, scrive tutti i messaggi su disco e
scala su pi host come caratteristica chiave del suo design. Kafka utilizza un
modello pubblica/sottoscrivi tramite dei topic con nome su cui i produttori
scrivono messaggi e da cui i consumatori li leggono. lo stesso funzionamento di
qualsiasi altro sistema di messaging.
Poich Kafka scrive tutti i messaggi su disco, potrebbe non avere lo stesso
throughput di messaggi a latenza superbassa di altri sistemi analoghi, che si
concentrano sullelaborazione il pi rapida possibile dei messaggi e non hanno
come obiettivo la loro memorizzazione sul lungo termine. Tuttavia, Kafka ha
abilit di scala eccezionali, e la sua capacit di riprodurre uno stream di messaggi
pu rivelarsi davvero utile. Per esempio, se un client consumatore fallisce, pu
leggere i nuovamente i messaggi da un punto valido nel tempo; se un algoritmo a
valle cambia, il traffico pu essere riprodotto per utilizzare la nuova funzionalit.
Quando scala tra gli host, Kafka partiziona i topic e supporta la replica delle
partizioni per la tolleranza ai guasti. Ciascun messaggio di Kafka associato a una
chiave, e questa viene usata per decidere a quale partizione il messaggio viene
inviato. Questo consente un partizionamento semanticamente utile, per esempio,
quando la chiave un ID utente nel sistema, allorch tutti i messaggi per un dato
utente verranno inviati alla stessa partizione. Kafka garantisce una consegna
ordinata in ogni partizione, cos che qualsiasi client che la legge possa sapere di
ricevere tutti i messaggi relativi a ciascuna chiave in quella partizione nellordine
in cui sono stati scritti dal produttore.
Samza trascrive periodicamente dei checkpoint della posizione in cui ha letto
tutti gli stream che sta consumando. Anche questi messaggi di checkpoint sono
scritti su un topic di Kafka; in questo modo, quando viene avviato un job di Samza,
ogni attivit pu leggere nuovamente il suo stream di checkpoint per sapere in
quale posizione nello stream avviare lelaborazione dei messaggi. Questo vuol
dire che Kafka agisce anche come un buffer; se un job di Samza fallisce o viene
modificato per laggiornamento, nessun messaggio andr perduto, e il job ripartir
dallultimo checkpoint. Questa funzionalit di buffer importante perch facilita
lesecuzione di pi job di Samza come parte di un flusso di lavoro complesso.
Quando i topic di Kafka sono i punti di coordinamento tra i job, un job potrebbe
consumare un topic che scritto da un altro; in casi del genere, Kafka pu ridurre i
problemi dovuti al fatto che un job viene eseguito pi lentamente degli altri.
Solitamente questo un inconveniente tuttaltro che trascurabile nei sistemi
costituiti da job in pi fasi, ma Kafka, in quanto buffer resiliente, permette a ogni
job di leggere e scrivere seguendo il proprio ritmo. un meccanismo non
dissimile dal coordinamento di pi job di MapReduce, che usa HDFS per scopi
analoghi.
Kafka fornisce una semantica di consegna dei messaggi in cui qualsiasi
messaggio scritto in Kafka garantito come disponibile a un client di quella
partizione specifica. I messaggi possono per essere elaborati tra i checkpoint, ed
possibile che il client riceva messaggi duplicati. Per limitare il problema
esistono meccanismi specifici per lapplicazione, e sia Kafka sia Samza hanno
questo tipo di semantica nelle loro roadmap; tenetelo presente quando pianificate i
vostri job.
Non approfondiremo Kafka oltre quello che ci serve per illustrare Samza. Se vi
interessa largomento, visitate il relativo sito web e il wiki; contengono
informazioni interessanti e documenti e presentazioni eccellenti.

Integrazione con YARN


Laddove Samza utilizza Kafka per limplementazione del suo livello di
streaming, usa YARN per il livello di esecuzione. Proprio come qualsiasi
applicazione YARN descritta nel Capitolo 3, Samza fornisce unimplementazione
sia di un ApplicationMaster, che controlla il ciclo di vita del job nel suo
complesso, sia implementazioni di funzionalit specifiche di Samza (chiamate
attivit, dallinglese task) che vengono eseguite in ciascun contenitore. Se Kafka
partiziona i suoi topic, le attivit sono il meccanismo tramite cui Samza partiziona
lelaborazione. Ogni partizione di Kafka verr letta da ununica attivit di Samza.
Se un job di Samza consuma pi stream, allora una data attivit sar lunica
consumatrice allinterno del job di ogni partizione dello stream a essa assegnata.
Ogni configurazione del job comunica al framework Samza quanto di Kafka pu
interessare al job a proposito degli stream, e Samza sonda continuamente questi
stream per determinare se arrivato qualche nuovo messaggio. Quando un nuovo
messaggio disponibile, lattivit di Samza invoca un callback definito dallutente
per elaborare il messaggio, un modello che dovrebbe essere familiare agli
sviluppatori di MapReduce. Questo metodo definito in uninterfaccia chiamata
StreamTask e ha la seguente segnatura:

public void process(IncomingMessageEnvelope envelope,


MessageCollector collector,
TaskCoordinator coordinator)

Questo il cuore di tutte le attivit di Samza, e definisce la funzionalit da


applicare ai messaggi ricevuti. Il messaggio ricevuto da elaborare viene incluso
nellIncomingMessageEnvelope; i messaggi di output possono essere scritti nel
MessageCollector, mentre la gestione dellattivit (come Shutdown) pu essere

eseguita tramite il TaskCoordinator.


Come gi detto, Samza crea unistanza di attivit per ogni partizione nel topic di
Kafka sottostante. Ogni contenitore YARN gestir una o pi di queste attivit. Il
modello generale quello del Samza Application Master che coordina pi
contenitori, ciascuno dei quali responsabile di una o pi istanze di StreamTask.

Un modello indipendente
Sebbene in questo capitolo parleremo esclusivamente di Kafka e YARN come
strumenti che forniscono i livelli di streaming e di esecuzione di Samza,
importante ricordare che il core di Samza utilizza interfacce ben definite per i due
sistemi. Esistono implementazioni di sorgenti multiple di stream (ne vedremo una
nel prossimo paragrafo), e insieme al supporto di YARN, Samza offre una classe
LocalJobRunner. Questo metodo alternativo pu eseguire istanze di StreamTask durante

lelaborazione sulla JVM invece di richiedere un intero cluster YARN, il che a


volte pu essere un utile strumento di test e debug. Sono inoltre in corso sviluppi
delle implementazioni di Samza su altri manager di cluster o su framework di
virtualizzazione.
Hello Samza!
Poich non tutti potrebbero avere cluster ZooKeeper, Kafka e YARN gi pronti
alluso, il team di Samza ci regala un modo magnifico per iniziare a lavorare con
il prodotto. Invece di avere un semplice programma Hello world!, esiste un
repository chiamato Hello Samza, disponibile alla pagina git://git.apache.org/samza-
hello-samza.git, che pu essere clonato.

Potrete cos scaricare e installare delle istanze dedicate di ZooKeeper, Kafka e


YARN (i tre prerequisiti fondamentali per Samza), creando unarchitettura
completa a cui poter inviare i job di Samza.
Vi sono anche numerosi esempi di job di Samza che elaborano i dati dalle
notifiche delle modifiche di Wikipedia. Date unocchiata alla pagina
http://samza.apache.org/startup/hello-samza/0.8/ e seguite le istruzioni. (Al momento

della stesura di questo libro, Samza era ancora un progetto relativamente recente,
per cui non abbiamo incluso informazioni dirette sugli esempi, che nel frattempo
potrebbero essere cambiati.)
Per il resto dei casi di Samza di questo capitolo, presupporremo che utilizziate
il package Hello Samza per quanto riguarda i componenti richiesti
(ZooKeeper/Kafka/YARN) o che abbiate implementato alcune integrazioni con
altre istanze di ciascuno.
In questo esempio abbiamo tre diversi job di Samza costruiti uno sullaltro. Il
primo legge le modifiche di Wikipedia, il secondo analizza questi record e il terzo
produce alcune statistiche in base ai record elaborati. Costruiremo il nostro flusso
di lavoro a pi stream tra breve.
Una delle cose pi interessanti qui lesempio di WikipediaFeed, che utilizza
Wikipedia invece di Kafka come propria sorgente dei messaggi. Nello specifico,
fornisce unaltra implementazione dellinterfaccia SystemConsumer di Samza per
consentirgli di leggere i messaggi provenienti da un sistema esterno. Come gi
visto, Samza non vincolato a Kafka e, come mostra lesempio, la costruzione di
una nuova implementazione dello stream non deve avvenire su un componente di
uninfrastruttura generica; devessere specifica per quel job, considerato che il
lavoro richiesto non enorme.
NOTA
La configurazione di default per ZooKeeper e Kafka scrive i dati di sistema nelle directory sotto
a /tmp, che sar la directory impostata se usate Hello Samza. Fate attenzione se utilizzate una
distribuzione Linux che cancella il contenuto di questa directory al reboot. Se intendete eseguire
dei test, preferibile che riconfiguriate questi componenti in modo che utilizzino meno posizioni
effimere. Modificate i file di configurazione pi importanti; si trovano nella directory dei servizi
sotto a hello-samza/deploy.

Creare un job di parsing di un tweet


Realizziamo allora la nostra implementazione di un job per mostrare il codice
completo richiesto. Eseguiremo il parsing dello stream Twitter per gli esempi di
questo capitolo e in seguito imposteremo un pipe dai messaggi del client
provenienti dallAPI Twitter fino a un topic di Kafka. Quello che ci serve
unattivit di Samza che legga lo stream di messaggi JSON, estragga il testo del
tweet vero e proprio e poi scriva il tutto in un topic di tweet.
Ecco il codice principale da TwitterParseStreamTask.java, disponibile alla pagina
http://bit.ly/1NJsB5r:

package com.learninghadoop2.samza.tasks;
public class TwitterParseStreamTask implements StreamTask {
@Override
public void process(IncomingMessageEnvelope envelope,
MessageCollector collector, TaskCoordinator coordinator) {
String msg = ((String) envelope.getMessage());

try {
JSONParser parser = new JSONParser();
Object obj = parser.parse(msg);
JSONObject jsonObj = (JSONObject) obj;
String text = (String) jsonObj.get(text);

collector.send(new OutgoingMessageEnvelope(new
SystemStream(kafka, tweets-parsed), text));
} catch (ParseException pe) {}
}
}
}

Il codice piuttosto auto-esplicativo, ma vale la pena sottolineare alcuni punti.


Utilizziamo JSON Simple (http://code.google.com/p/json-simple/) per i requisiti di
parsing del nostro JSON (relativamente semplice); lo utilizzeremo ancora pi
avanti nel libro.
LIncomingMessageEnvelope e il suo OutputMessageEnvelope corrispondente sono le
strutture principali che hanno a che fare con i dati del messaggio. Insieme al
messaggio, linvolucro (envelope) conterr anche dei dati relativi al sistema, al
nome del topic ed eventualmente al numero della partizione, insieme ad altri
metadati. Per i nostri scopi, estraiamo semplicemente il corpo del messaggio da
quello in arrivo e inviamo il testo del tweet tramite un nuovo OutgoingMessageEnvelope
a un topic tweets-parsed allinterno di un sistema chiamato kafka. (Notate il nome in
minuscolo; lo spiegheremo tra poco.)
Il tipo di messaggio nellIncomingMessageEnvelope java.lang.Object. Attualmente
Samza non applica un modello di dati, e quindi non prevede uno corpo del
messaggio fortemente tipizzato. Quando si estraggono i contenuti di un messaggio,
richiesto un cast esplicito. Poich ogni attivit deve conoscere il formato di
messaggio previsto degli stream che elabora, non poi quella stranezza che
potrebbe sembrare.

Il file di configurazione
Nel codice precedente non c nulla che dica da dove provengono i messaggi; il
framework li presenta semplicemente allimplementazione di StreamTask, ma
ovviamente Samza deve sapere dove recuperarli. Per ogni job esiste un file di
configurazione che definisce questo e altro. Quello che segue si pu trovare come
twitter-parse.properties alla pagina http://bit.ly/1MrpzPZ:

# Job
job.factory.class=org.apache.samza.job.yarn.YarnJobFactory
job.name=twitter-parser

# YARN
yarn.package.path=file:///home/gturkington/samza/build/distributions/learninghadoop2-
0.1.tar.gz

# Attivit
task.class=com.learninghadoop2.samza.tasks.TwitterParseStreamTask
task.inputs=kafka.tweets
task.checkpoint.factory=org.apache.samza.checkpoint.kafka.KafkaCheckpointManagerFactory
task.checkpoint.system=kafka

# Di solito dovrebbe essere 3, ma qui abbiamo un solo broker.


task.checkpoint.replication.factor=1

# Serializer
serializers.registry.string.class=org.apache.samza.serializers.StringSerdeFactory

# Sistemi
systems.kafka.samza.factory=org.apache.samza.system.kafka.KafkaSystemFactory
systems.kafka.streams.tweets.samza.msg.serde=string
systems.kafka.streams.tweets-parsed.samza.msg.serde=string
systems.kafka.consumer.zookeeper.connect=localhost:2181/
systems.kafka.consumer.auto.offset.reset=largest
systems.kafka.producer.metadata.broker.list=localhost:9092
systems.kafka.producer.producer.type=sync
systems.kafka.producer.batch.num.messages=1

Pu sembrare molto codice, ma per ora limitiamoci alla struttura di alto livello
e ad alcune impostazioni chiave. La sezione Job imposta YARN come framework di
esecuzione e assegna un nome al job. Se dovessimo eseguire pi copie dello
stesso job, dovremmo anche assegnare a ogni copia un ID univoco. La sezione
delle attivit specifica la classe di implementazione della nostra attivit e anche il
nome degli stream per i quali dovrebbe ricevere dei messaggi. I Serializer dicono a
Samza come leggere e scrivere i messaggi da e sullo stream, mentre lultima
sezione definisce i sistemi per nome e associa loro le classi di implementazione.
Nel nostro caso, definiamo solo un sistema chiamato kafka e vi facciamo
riferimento quando inviamo il nostro messaggio nellattivit precedente. Notate
che questo nome arbitrario, e potremmo darne uno qualsiasi. Per chiarezza ha
senso chiamare il sistema Kafka con lo stesso nome, ma solo una convenzione. A
volte dovrete assegnare nomi diversi quando avete a che fare con pi sistemi
simili, o magari quando trattate lo stesso sistema in modo diverso a seconda delle
parti del file di configurazione.
In questa sezione, specifichiamo anche il serde da associare agli stream usati
dallattivit. Ricordate che i messaggi di Kafka hanno un corpo e una chiave
facoltativa che viene utilizzata per determinare a quale partizione viene inviato il
messaggio. Samza deve sapere come trattare il contenuto delle chiavi e dei
messaggi per questi stream, e ha il supporto per gestirli come byte grezzi o come
tipi specifici, come stringhe, interi e JSON, come spiegato in precedenza.
Il resto della configurazione rimarr pressoch identico da job a job, poich
include cose come la posizione dellensemble dei ZooKeeper e dei cluster di
Kafka, e specifica come realizzare i checkpoint degli stream. Samza consente
unampia gamma di personalizzazioni; le varie opzioni sono riportate nel dettaglio
alla pagina http://bit.ly/1B4dhGo.

Portare i dati di Twitter in Kafka


Prima di eseguire il job, dobbiamo portare alcuni tweet in Kafka. Creiamo
allora un nuovo topic di Kafka chiamato tweets su cui li scriveremo.
Per eseguire questa e altre operazioni relative a Kafka, utilizzeremo gli
strumenti a riga di comando che si trovano nella directory bin della distribuzione
di Kafka. Se state eseguendo un job dallinterno dello stack creato come parte
dellapplicazione Hello Samza, la directory sar deploy/kafka/bin.
kafka-topics.sh uno strumento general purpose che pu essere impiegato per

creare, aggiornare e descrivere i topic. Per essere utilizzato, richiede alcuni


argomenti che specifichino la posizione del cluster locale di ZooKeeper, dove i
broker di Kafka immagazzinano i dettagli, e il nome del topic su cui lavorare. Per
creare un nuovo topic, eseguite questo comando:
$ kafka-topics.sh --zookeeper localhost:2181 --create topic tweets --partitions 1 --
replication-factor 1

Questa riga crea un topic chiamato tweets e imposta esplicitamente il numero di


partizioni e il fattore di replica a 1. lideale se eseguite Kafka in una VM locale
di test, ma ovviamente le distribuzioni di produzione avranno pi partizioni per
scalare il carico tra pi broker e un fattore di replica di almeno 2 per garantire la
tolleranza ai guasti.
Usate lopzione di lista dello strumento kafka-topics.sh per vedere un elenco dei
topic nel sistema, oppure ricorrete a describe per ottenere informazioni pi
dettagliate sui singoli topic:
$ kafka-topics.sh --zookeeper localhost:2181 --describe --topic tweets
Topic:tweets PartitionCount:1 ReplicationFactor:1 Configs:
Topic: tweets Partition: 0 Leader: 0 Replicas: 0 Isr: 0

Gli 0 multipli potrebbero confondere, perch sono etichette e non conteggi. Ogni
broker nel sistema ha un ID che solitamente parte da 0, come accade con le
partizioni in ciascun topic. Loutput precedente ci dice che il topic chiamato tweets
ha ununica partizione con ID 0, che il broker che agisce da leader in quella
partizione il broker 0 e che il set delle ISR (in-sync replicas) per questa
partizione solo il broker 0. Questultimo valore particolarmente importante
quando si ha a che fare con la replica.
Utilizzeremo ora lutility Python dei capitoli precedenti per trarre i tweet JSON
dal flusso di Twitter, e poi ricorreremo a un produttore di messaggi della CLI di
Kafka per scrivere i messaggi in un topic di Kafka. Non un modo esattamente
efficiente di fare le cose, ma si adatta alla spiegazione. Supponendo che lo script
di Python si trovi nella nostra directory home, eseguiamo il prossimo comando
dallinterno della directory bin di Kafka:
$ python ~/stream.py j | ./kafka-console-producer.sh --broker-list localhost:9092 --topic
tweets

Il comando viene eseguito a tempo indeterminato, quindi fate attenzione a non


lasciarlo correre per tutta la notte su una VM di test con poco spazio su disco.

Eseguire un job di Samza


Per eseguire un job di Samza, il codice devessere combinato con i componenti
di Samza richiesti per lesecuzione in un archivio .tar.gz che sar letto dal
NodeManager di YARN. Questo il file a cui si fa riferimento tramite la propriet
yarn.file.package nel file di configurazione dellattivit di Samza.

Quando si usa il singolo nodo Hello Samza possiamo utilizzare un percorso


assoluto sul file system, come abbiamo visto nel precedente esempio di
configurazione. Per i job su griglie YARN pi grandi, il modo pi semplice
collocare il package su HDFS e farvi riferimento tramite un URI hdfs:// o su un
server web (Samza fornisce un meccanismo per consentire a YARN di leggere il
file via HTTP).
Poich Samza ha pi sottocomponenti, e ciascun sottocomponente ha le proprie
dipendenze, il package YARN completo potrebbe finire con il contenere
moltissimi file JAR (pi di 100!). Inoltre, dovrete includere il vostro codice
personalizzato per lattivit e alcuni script dalla distribuzione Samza. Non
qualcosa che si pu fare a mano. Nel codice di esempio alla pagina
https://github.com/learninghadoop2/book-examples/tree/master/ch4, abbiamo impostato una

struttura campione che contiene il codice e i file di configurazione e fornito un po


di automazione tramite Gradle per costruire larchivio necessario e per avviare le
attivit.
Una volta entrati nella root della directory del codice di esempio di Samza per
questo libro, eseguite il prossimo comando per costruire un unico file di archivio
che contiene tutte le classi di questo capitolo compilate e assemblate con tutti gli
altri file richiesti:
$ ./gradlew targz

Questa attivit di Gradle non solo creer larchivio .tar.gz necessario nella
directory build/distributions, ma ne memorizzer anche una versione espansa sotto
a build/samza-package. Sar utile, perch Samza utilizzer gli script contenuti
nella directory bin dellarchivio per inviare effettivamente lattivit a YARN.
Eseguiamo il nostro job. Dobbiamo avere i percorsi di file per due elementi: lo
script run-job.sh di Samza per inviare un job a YARN e il file di configurazione per
il nostro job. Poich nel package del job creato sono riunite tutte le attivit
compilate, diremo a Samza quale attivit eseguire tramite un file di configurazione
diverso che specifica una classe di implementazione dellattivit nella propriet
task.class. Per eseguirla nel concreto, possiamo lanciare il seguente comando

dallinterno dellarchivio espanso del progetto in build/samza-archives:


$ bin/run-job.sh --config-factory=org.apache.samza.config.factories.PropertiesConfigFactory -
-config-path=]config/twitter-parser.properties

Per comodit, abbiamo aggiunto unattivit di Gradle per eseguire questo job:
$ ./gradlew runTwitterParser

Per vedere loutput del job, utilizzeremo la CLI di Kafka per consumare i
messaggi:
$ ./kafka-console-consumer.sh zookeeper localhost:2181 topic tweets-parsed

Dovreste veder apparire sul client uno stream continuo di tweet.


NOTA
Non abbiamo creato esplicitamente il topic chiamato tweets-parsed. Kafka consente che i topic
vengano creati dinamicamente quando un produttore o un consumatore prova a usare il topic.
In molte situazioni, sebbene i valori predefiniti di partizionamento e di replica potrebbero essere
adatti, sar richiesta una creazione esplicita del topic per garantire che questi attributi
fondamentali vengano definiti correttamente.

Samza e HDFS
Potreste aver notato che abbiamo citato HDFS solo ora per la prima volta nella
nostra discussione su Samza. Se vero che Samza si integra strettamente con
YARN, non ha unintegrazione diretta con HDFS. A un livello logico, i sistemi di
implementazione degli stream di Samza (come Kafka) forniscono quel livello di
storage che viene solitamente fornito da HDFS per i tradizionali carichi di lavoro
di Hadoop. Nella terminologia dellarchitettura di Samza, come descritto in
precedenza, YARN il livello di esecuzione in entrambi i modelli, Samza usa un
livello di streaming per i suoi dati di origine e di destinazione mentre framework
come MapReduce utilizzano HDFS. Questo un buon esempio di come YARN non
solo abiliti modelli di calcolo che elaborano i dati in modo molto diverso dal
MapReduce orientato al batch, ma pu anche utilizzare sistemi di storage del tutto
differenti per i dati dorigine.

Windowing
Spesso torna utile generare alcuni dati in base ai messaggi ricevuti su uno
stream in una specifica finestra temporale. Per esempio, si possono registrare i
valori dellattributo top N misurati minuto per minuto. Samza supporta la
funzionalit di windowing attraverso linterfaccia WindowableTask, per la quale
devessere implementato questunico metodo:
public void window(MessageCollector collector, TaskCoordinator
coordinator);

simile al metodo process nellinterfaccia StreamTask. Tuttavia, poich il metodo


viene chiamato secondo una programmazione temporale, la sua invocazione non
associata a un messaggio ricevuto. Rimangono comunque i parametri
MessageCollector e TaskCoordinator, poich molte delle attivit di windowing

produrranno messaggi di output e potrebbero anche dover eseguire alcune azioni di


gestione delle attivit.
Prendiamo la nostra attivit precedente e aggiungiamo una funzione di
windowing che generer il numero di tweet ricevuti in ciascun periodo impostato.
Questa limplementazione della classe principale di TwitterStatisticsStreamTask.java
che si trova alla pagina http://bit.ly/18Aesqz:
public class TwitterStatisticsStreamTask implements StreamTask, WindowableTask {
private int tweets = 0;

@Override
public void process(IncomingMessageEnvelope envelope, MessageCollector collector,
TaskCoordinator coordinator) {
tweets++;
}

@Override
public void window(MessageCollector collector, TaskCoordinator
coordinator) {
collector.send(new OutgoingMessageEnvelope(new
SystemStream(kafka, tweet-stats), + tweets));

// Reimposta i conteggi dopo il windowing.


tweets = 0;
}
}

La classe TwitterStatisticsStreamTask ha una variabile membro privata chiamata


tweets che inizializzata a 0 ed incrementata a ogni chiamata del metodo process.
Sappiamo quindi che questa variabile verr incrementata per ciascun messaggio
passato allattivit dallimplementazione sottostante dello stream. Ogni contenitore
Samza ha un singolo thread che gira in un ciclo che esegue i metodi process e window
su tutte le attivit al suo interno. Questo significa che non dobbiamo preoccuparci
delle variabili distanza rispetto alle modifiche simultanee: in contemporanea,
verr infatti eseguito solo un metodo su ciascuna attivit allinterno di un
contenitore.
Nel nostro metodo window, inviamo un messaggio a un nuovo topic chiamato tweet-
stats e poi reimpostiamo la variabile tweets. una procedura piuttosto diretta;

lunico pezzo mancante come Samza fa a capire quando chiamare il metodo


window. Specifichiamolo nel file di configurazione:

task.window.ms=5000

Questa riga dice a Samza di chiamare il metodo window su ciascuna istanza


dellattivit ogni 5 secondi. Per eseguire lattivit window, ricorriamo a Gradle:
$ ./gradlew runTwitterStatistics

Se adesso usiamo kafka-console-consumer.sh per rilevare lo stream tweet-stats,


vedremo il seguente output:
Number of tweets: 5012
Number of tweets: 5398

NOTA
In questo contesto, il termine windowing fa riferimento alla suddivisione concettuale di Samza
dello stream dei messaggi in intervalli di tempo e al meccanismo per eseguire lelaborazione
nel punto in cui si passa da un intervallo a un altro. Samza non offre direttamente
unimplementazione dellaltro uso del termine, cio la serie di finestre a scorrimento in cui una
serie di valori viene conservata ed elaborata nel tempo. Tuttavia, linterfaccia dellattivit di
windowing fornisce limpianto per implementare anche questo tipo di finestre.

Flussi di lavoro con pi job


Come abbiamo visto negli esempi di Hello Samza, parte del vero potere di
Samza deriva dalla combinazione di pi job; per dimostrare questa capacit,
ricorreremo a un job di pulizia (cleanup) del testo.
Nel prossimo paragrafo effettueremo la sentiment analysis di alcuni tweet
confrontandoli con una serie di parole inglesi positive e negative. La sua
applicazione al flusso grezzo di Twitter, tuttavia, dar dei risultati irregolari,
considerata la ricchezza linguistica dello stream. Dovremo anche considerare cose
come la pulizia del testo, la capitalizzazione, le contrazioni frequenti e cos via.
Come sa chiunque ha lavorato con dataset complessi, rendere i dati adatti
allelaborazione richiede un notevole impegno.
Prima di procedere, allora, puliamo un po il testo; in particolare,
selezioneremo solo i tweet in inglese e porteremo il loro testo in minuscolo prima
di inviarli a un nuovo stream di output.
Lindividuazione della lingua un problema difficile; per questo utilizzeremo
una risorsa della libreria Apache Tika (http://tika.apache.org). Tika fornisce una
serie articolata di funzionalit per estrarre il testo da varie fonti e per derivare
altre informazioni da quel testo. Se usiamo i nostri script Gradle, la dipendenza di
Tika gi specificata e verr inclusa automaticamente nel package di job generato.
Se invece si procede tramite un altro meccanismo, si dovr scaricare il file JAR
dalla home page e aggiungerlo al package di job YARN. Il codice che segue si
trova come TextCleanupStreamTask.java alla pagina http://bit.ly/1C4TY5o:
public class TextCleanupStreamTask implements StreamTask {
@Override
public void process(IncomingMessageEnvelope envelope, MessageCollector collector,
TaskCoordinator coordinator) {
String rawtext = ((String) envelope.getMessage());

if (en.equals(detectLanguage(rawtext))) {
collector.send(new OutgoingMessageEnvelope(new SystemStream(kafka, english-
tweets),
rawtext.toLowerCase()));
}
}

private String detectLanguage(String text) {


LanguageIdentifier li = new LanguageIdentifier(text);

return li.getLanguage();
}
}

Questa attivit piuttosto immediata grazie al cospicuo intervento di Tika.


Creiamo un metodo di utilit che racchiude la creazione e luso di unoperazione
di Tika, LanguageDetector, quindi chiamiamo questo metodo sul corpo di ciascun
messaggio in arrivo nel metodo process. Scriveremo sullo stream di output solo se
il risultato dellapplicazione del metodo en, il codice di due lettere che indica
linglese (English).
Il file di configurazione di questa attivit simile a quello dellattivit
precedente, con valori specifici per il nome e la classe di implementazione. il
repository textcleanup.properties alla pagina http://bit.ly/1E8h1Je. Dovremo anche
specificare lo stream di input:
task.inputs=kafka.tweets-parsed

importante perch questa attivit necessaria per analizzare il testo dei tweet
estratto nellattivit precedente e per evitare la duplicazione della logica di
parsing JSON che al suo meglio incapsulata in un unico punto. Possiamo
eseguire questa attivit con il seguente comando:
$ ./gradlew runTextCleanup

Ora possiamo eseguire tutte e tre le attivit insieme; TwitterParseStreamTask e


TwitterStatisticsStreamTask consumeranno lo stream dei tweet grezzi, mentre

TextCleanupStreamTask consumer loutput da TwitterParseStreamTask.

Sentiment analysis dei tweet


Implementeremo ora unattivit che esegue una sentiment analysis dei tweet
simile a quella che abbiamo svolto con MapReduce nel capitolo precedente. Sar
loccasione per mostrarvi anche un utile meccanismo offerto da Samza: gli stream
di bootstrap.

Figura 4.2 Elaborazione dei dati sugli stream.

Stream di bootstrap
In linea generale, la maggior parte dei job di elaborazione degli stream parte
dallelaborazione dei messaggi che arrivano dopo lavvio, ignorando solitamente
quelli precedenti. Considerata la sua riproducibilit degli stream, Samza non ha
questo vincolo.
Nel nostro job di sentiment analysis, avevamo due insiemi di termini di
riferimento: parole positive e negative. Anche se finora non labbiamo mostrato,
Samza pu consumare messaggi da pi stream; i meccanismi sottostanti
sonderanno tutti gli stream con nome e ne forniranno i messaggi, uno alla volta, al
metodo process. Potremo quindi creare stream per le parole positive e negative e
inviare i dataset su questi stream. Di primo acchito, potremmo programmare di
riavvolgere i due stream fino al punto pi indietro possibile e poi leggere i
tweet man mano che arrivano. Il problema che Samza non garantisce
lordinamento dei messaggi da pi stream, e sebbene esista un meccanismo che
assegna agli stream la priorit pi elevata, non possiamo dare per scontato che
tutte le parole negative e positive vengano elaborate prima dellarrivo del primo
tweet.
Per questi tipi di scenari, Samza prevede gli stream di bootstrap, cio
intermedi. Se per unattivit sono definiti degli stream di bootstrap, Samza li
legger dal primissimo offset finch non vengono elaborati per intero
(tecnicamente, legger gli stream finch non vengono raggiunti, cos che qualsiasi
parola nuova inviata a uno stream verr trattata senza priorit e arriver interposta
tra i tweet).
Creeremo ora un nuovo job chiamato TweetSentimentStreamTask che legge due stream
di bootstrap, ne riunisce il contenuto in alcune HashMap, raccoglie i conteggi in
corso per le tendenze del sentiment e utilizza una funzione window per generare
questi dati a intervalli. Potete trovare questo codice alla pagina
http://bit.ly/1EzCvUH:

public class TwitterSentimentStreamTask implements StreamTask,


WindowableTask {
private Set<String> positiveWords = new
HashSet<String>();
private Set<String> negativeWords = new
HashSet<String>();
private int tweets = 0;
private int positiveTweets = 0;
private int negativeTweets = 0;
private int maxPositive = 0;
private int maxNegative = 0;

@Override
public void process(IncomingMessageEnvelope envelope,
MessageCollector collector, TaskCoordinator coordinator) {
if (positive-words.equals(envelope.getSystemStreamPartition().
getStream())) {
positiveWords.add(((String) envelope.getMessage()));
} else if (negative-words.equals(envelope.getSystemStreamPartition().getStream())) {
negativeWords.add(((String) envelope.getMessage()));
} else if (english-tweets.equals(envelope.getSystemStreamPartition().getStream())) {
tweets++;

int positive = 0;
int negative = 0;
String words = ((String) envelope.getMessage());

for (String word : words.split( )) {


if (positiveWords.contains(word)) {
positive++;
} else if (negativeWords.contains(word)) {
negative++;
}
}

if (positive > negative) {


positiveTweets++;
}

if (negative > positive) {


negativeTweets++;
}

if (positive > maxPositive) {


maxPositive = positive;
}

if (negative > maxNegative) {


maxNegative = negative;
}
}
}

@Override
public void window(MessageCollector collector, TaskCoordinator
coordinator) {
String msg = String.format(Tweets: %d Positive: %d Negative:
%d MaxPositive: %d MinPositive: %d, tweets, positiveTweets,
negativeTweets, maxPositive, maxNegative);

collector.send(new OutgoingMessageEnvelope(new
SystemStream(kafka, tweet-sentiment-stats), msg));

// Reimposta i conteggi dopo il windowing.


tweets = 0;
positiveTweets = 0;
negativeTweets = 0;
maxPositive = 0;
maxNegative = 0;
}

In questa attivit, aggiungiamo un numero di variabili membro private che


utilizzeremo per mantenere un conteggio continuo del numero complessivo dei
tweet, di quanti sono positivi e negativi e dei conteggi positivi e negativi massimi
in un singolo tweet.
Questa attivit lavora su tre topic Kafka. Anche se ne configureremo due da
usare come stream di bootstrap, sono esattamente lo stesso tipo di topic Kafka da
cui vengono ricevuti i messaggi; lunica differenza con gli stream di bootstrap
che diciamo a Samza di utilizzare le capacit di riavvolgimento di Kafka per
rileggere per intero ogni messaggio nello stream. Per laltro stream di tweet,
iniziamo semplicemente a leggere i nuovi messaggi man mano che arrivano.
Come gi suggerito, se unattivit opera su pi stream, lo stesso metodo process
ricever messaggi da ogni stream. Ecco perch usiamo
envelope.getSystemStreamPartition().getStream() per estrarre il nome dello stream per

ogni messaggio dato e agire di conseguenza. Se il messaggio proviene da uno


stream di bootstrap, aggiungiamone il contenuto allhashmap opportuna.
Suddividiamo un messaggio di tweet nelle sue parole costitutive, testiamo ogni
parola in base al sentiment positivo o negativo e aggiorniamo i conteggi di
conseguenza. Come potete vedere, questa attivit non genera i tweet ricevuti in un
altro topic.
Poich non eseguiamo alcuna elaborazione diretta, non ha senso procedere cos;
qualsiasi altra attivit che desidera consumare messaggi pu aderire direttamente
allo stream dei tweet in arrivo. Tuttavia, una possibile modifica potrebbe essere
quella di scrivere dei tweet con un sentiment positivo e negativo su stream
dedicati.
Il metodo window genera una serie di conteggi e poi reimposta le variabili (come
ha fatto prima). Notate che Samza supporta lesposizione diretta delle metriche
attraverso JMX, il che potrebbe essere perfetto per i semplici esempi qui mostrati.
(Qui non abbiamo per spazio per approfondire largomento.)
Per eseguire questo job, dobbiamo modificare il file di configurazione
impostando i nomi del job e dellattivit come di consueto, ma dobbiamo anche
specificare i vari stream di input:
task.inputs=kafka.english-tweets,kafka.positive-words,kafka.negative-words

Dobbiamo poi indicare che i nostri sono due stream di bootstrap che devono
essere letti dal primo offset in ordine. In particolare, impostiamo tre propriet per
gli stream. Si dice che sono bootstrapped, ossia pronti prima di altri stream; per
raggiungere questo obiettivo, specifichiamo che loffset su ciascuno stream deve
essere reimpostato alla posizione pi vecchia (la prima):
systems.kafka.streams.positive-words.samza.bootstrap=true
systems.kafka.streams.positive-words.samza.reset.offset=true
systems.kafka.streams.positive-words.samza.offset.default=oldest

systems.kafka.streams.negative-words.samza.bootstrap=true
systems.kafka.streams.negative-words.samza.reset.offset=true
systems.kafka.streams.negative-words.samza.offset.default=oldest

Possiamo avviare il job con questo comando:


$ ./gradlew runTwitterSentiment

Dopo lavvio del job, osservate loutput dei messaggi sullargomento tweet-
sentiment-stats.

Lindividuazione del job relativo al sentiment creer degli stream intermedi


delle parole positive e negative prima di leggere uno qualsiasi degli altri tweet in
inglese e in maiuscolo appena rilevati.
Possiamo ora visualizzare i nostri quattro job coordinati come mostrato nel
diagramma della Figura 4.3.

Figura 4.3 Stream di bootstrap e attivit correlate.

NOTA
Per eseguire correttamente i job, si potrebbe pensare di dover avviare il job di parsing JSON
seguito da quello di cleanup prima di avviare il job del sentiment, ma qui non cos. I messaggi
non letti rimangono salvati in Kafka, quindi non importa lordine in cui vengono avviati i job in un
flusso di lavoro multiplo. Ovviamente il job del sentiment produrr conteggi di 0 tweet finch non
inizia a ricevere dati, ma se un job dovesse iniziare prima di un altro da cui dipende non
succeder niente.

Attivit stateful
Lultimo aspetto di Samza che esploreremo riguarda il modo in cui consente che
le partizioni degli stream di elaborazione delle attivit mantengano uno stato
locale persistente. Nel caso precedente abbiamo utilizzato delle variabili private
per tenere traccia dei totali intermedi, ma a volte utile che unattivit abbia uno
stato locale pi ricco. Un esempio potrebbe essere lesecuzione di un join logico
su due stream, dove pu essere comodo trarre un modello di stato da uno stream
per poi confrontarlo con laltro.
NOTA
Samza pu utilizzare gli stream partizionati per ottimizzare il join tra gli stream. Se ciascuno
stream da unire usa la stessa chiave di partizione (per esempio un ID utente), allora ogni
attivit che consuma quello stream ricever tutti i messaggi associati a ciascun ID tra tutti gli
stream.

Samza fornisce unaltra astrazione simile alla sua nozione di framework per
gestire i suoi lavori e che implementa le attivit. Definisce infatti uno store astratto
chiave-valore che pu avere pi implementazioni concrete, servendosi di alcuni
progetti open source esistenti per le implementazioni su disco (utilizzando
LevelDB dalla v0.7 e aggiungendo RocksDB dalla v0.8). Esiste anche uno store in
memoria che non conserva in modo persistente i dati chiave-valore ma che pu
essere utile durante i test o con carichi di lavoro di produzione potenzialmente
molto specifici.
Ogni attivit pu scrivere in questo store chiave-valore, e Samza ne gestisce la
persistenza sullimplementazione locale. Per supportare gli stati persistenti, lo
store modellato come uno stream, e anche tutto quanto viene scritto in esso viene
inserito in uno stream. Se unattivit fallisce, al riavvio pu recuperare lo stato del
suo store chiave-valore riproducendo i messaggi nel topic di backup. La
preoccupazione riguarda ovviamente il numero di messaggi che devono essere
riprodotti; tuttavia, Kafka, per esempio, comprime i messaggi con la stessa chiave,
cos che nel topic rimanga solo laggiornamento pi recente.
Ora modificheremo lesempio precedente del sentiment dei tweet per
aggiungere un conteggio sul sentiment positivo e negativo massimo dei vari tweet.
Il codice che segue si trova come TwitterStatefulSentimentStateTask.java alla pagina
http://bit.ly/1AiGBZT. Il metodo process lo stesso di TwitterSentimentStateTask.java,

quindi non lo ripeteremo:


public class TwitterStatefulSentimentStreamTask implements StreamTask, WindowableTask,
InitableTask {
private Set<String> positiveWords = new HashSet<String>();
private Set<String> negativeWords = new HashSet<String>();
private int tweets = 0;
private int positiveTweets = 0;
private int negativeTweets = 0;
private int maxPositive = 0;
private int maxNegative = 0;
private KeyValueStore<String, Integer> store;

@SuppressWarnings(unchecked)
@Override
public void init(Config config, TaskContext context) {
this.store = (KeyValueStore<String, Integer>) context.getStore(tweet-store);
}

@Override
public void process(IncomingMessageEnvelope envelope, MessageCollector collector,
TaskCoordinator coordinator) {
...
}

@Override
public void window(MessageCollector collector, TaskCoordinator coordinator) {
Integer lifetimeMaxPositive = store.get(lifetimeMaxPositive);
Integer lifetimeMaxNegative = store.get(lifetimeMaxNegative);

if ((lifetimeMaxPositive == null) || (maxPositive > lifetimeMaxPositive)) {


lifetimeMaxPositive = maxPositive;
store.put(lifetimeMaxPositive, lifetimeMaxPositive);
}

if ((lifetimeMaxNegative == null) || (maxNegative > lifetimeMaxNegative)) {


lifetimeMaxNegative = maxNegative;
store.put(lifetimeMaxNegative, lifetimeMaxNegative);
}

String msg =
String.format(
Tweets: %d Positive: %d Negative: %d MaxPositive: %d MaxNegative: %d
LifetimeMaxPositive: %d LifetimeMaxNegative: %d,
tweets, positiveTweets, negativeTweets, maxPositive, maxNegative,
lifetimeMaxPositive,
lifetimeMaxNegative);

collector.send(new OutgoingMessageEnvelope(new SystemStream(kafka, tweet-stateful-


sentiment-stats), msg));

// Reimposta i conteggi dopo il windowing.


tweets = 0;
positiveTweets = 0;
negativeTweets = 0;
maxPositive = 0;
maxNegative = 0;
}
}
Questa classe implementa una nuova interfaccia chiamata InitableTask.
Linterfaccia ha un unico metodo, init, che viene usato quando unattivit deve
impostare alcuni aspetti della sua configurazione prima dellesecuzione. Qui
useremo il metodo init() per creare unistanza della classe KeyValueStore e salvarla
in una variabile membro privata.
KeyValueStore fornisce uninterfaccia familiare del tipo put/ get. In questo caso

specifichiamo che le chiavi sono del tipo String e i valori degli Integer. Nel metodo
window, recuperiamo altri valori eventualmente salvati in precedenza per il

sentiment positivo e negativo massimo, e se il conteggio nella finestra corrente


pi alto, aggiorniamo lo store di conseguenza. Per finire, produciamo in output i
risultati del metodo window come fatto prima.
Come potete vedere, lutente non deve badare ai dettagli della persistenza
locale o remota dellistanza di KeyValueStore, perch Samza a occuparsene.
Lefficienza di questo meccanismo permette anche alle attivit di controllare
agevolmente lentit dimensionabile dello stato locale, che pu avere un certo
peso in casi come le aggregazioni a esecuzione prolungata o i join di stream.
Il file di configurazione per il job si trova alla pagina http://bit.ly/1D57HuC.
necessario aggiungere alcune voci, come segue:
stores.tweet-store.factory=org.apache.samza.storage.kv.KeyValueStorageEngineFactory
stores.tweet-store.changelog=kafka.twitter-stats-state
stores.tweet-store.key.serde=string
stores.tweet-store.msg.serde=integer

La prima riga specifica la classe di implementazione per lo store, la seconda


indica il topic di Kafka da usare per lo stato persistente e le ultime due righe
specificano il tipo di chiave-valore.
Per eseguire questo job, avviate il comando
$ ./gradlew runTwitterStatefulSentiment

Per comodit, il prossimo comando avvier quatto parser JSON: il cleanup del
testo, il job delle statistiche e i job stateful del sentiment:
$ ./gradlew runTasks

Samza un sistema di elaborazione degli stream puro che fornisce


implementazioni portabili dei suoi livelli di storage e di esecuzione. I plug-in pi
usati sono YARN e Kafka, e questo dimostra come possa integrarsi strettamente
con Hadoop su YARN mentre utilizza un layer di storage completamente diverso.
Samza ancora un progetto relativamente nuovo, e le caratteristiche attuali sono un
primo nucleo di quanto sar possibile in futuro. Consultate la pagina web per
ottenere le informazioni pi recenti sullo stato dellarte.
Riepilogo
In questo capitolo ci siamo concentrati maggiormente su quanto pu essere fatto
con Hadoop 2, in particolare con YARN, che non sui dettagli interni di Hadoop.
Abbiamo dimostrato che Hadoop sta realizzando il suo obiettivo di diventare una
piattaforma di elaborazione dei dati pi flessibile e generica, non pi legata
allelaborazione in batch. Abbiamo imparato come, grazie a Samza, i framework
di elaborazione implementabili su YARN possono rinnovare e abilitare un tipo di
funzionalit decisamente diversa da quella che era disponibile con Hadoop 1.
In particolare, abbiamo visto come Samza si muove allestremo opposto dello
spettro della latenza dallelaborazione in batch, consentendo lelaborazione dei
singoli messaggi man mano che arrivano.
Abbiamo anche visto che fornisce un meccanismo di callback che familiare
agli sviluppatori di MapReduce, ma che lo utilizza per un modello di elaborazione
molto diverso. Abbiamo illustrato le varie modalit in cui Samza utilizza YARN
come suo framework di esecuzione principale e come implementa il modello
descritto nel Capitolo 3.
Nel prossimo capitolo cambieremo marcia ed esploreremo Apache Spark. Per
quanto sia un modello di dati diverso da Samza, scopriremo che ha unestensione
che supporta lelaborazione degli stream in tempo reale, compresa la possibilit di
integrarsi con Kafka. Nondimeno, i due progetti sono cos diversi da essere pi
complementari che alternativi.
Capitolo 5
Computazione iterativa con Spark

Nel capitolo precedente abbiamo visto come Samza pu abilitare lelaborazione


di stream di dati in tempo reale allinterno di Hadoop. Si allontana un po dal
modello di elaborazione in batch di MapReduce, ma si avvicina a quello di
uninterfaccia ben definita su cui possono essere implementate attivit di logica
business. In questo capitolo tratteremo Apache Spark, che pu essere considerato
tanto un framework su cui si possono costruire delle applicazioni quanto un
framework di elaborazione fatto e finito. inoltre possibile reimplementare i
componenti nellecosistema di Hadoop in modo che utilizzino Spark come
framework di elaborazione sottostante. In particolare, vedremo quanto segue.
Cos Spark e come il suo sistema core gira su YARN.
Il modello di dati fornito da Spark, che consente unelaborazione dei dati
ampiamente scalabile ed efficiente.
I componenti aggiuntivi di Spark e i progetti correlati.
Sebbene Spark abbia un proprio meccanismo per elaborare gli stream di dati,
questa sola una parte di quanto ha da offrire; va considerato come uniniziativa
molto pi ampia.
Apache Spark
Apache Spark (https://spark.apache.org/) un framework di elaborazione dei dati
basato su una generalizzazione di MapReduce sviluppato originariamente da
AMPLab a Berkeley (https://amplab.cs.berkeley.edu/). Come Tez, agisce da motore di
esecuzione che modella le trasformazioni dei dati in DAG, puntando a eliminare
loverhead di I/O di MapReduce per eseguire una computazione iterativa in scala.
Laddove lo scopo principale di Tez quello di fornire un motore di esecuzione
pi rapido per MapReduce su Hadoop, Spark stato progettato per essere tanto un
framework indipendente quanto unAPI per lo sviluppo di applicazioni. Il sistema
studiato per effettuare unelaborazione dei dati general purpose in memoria e per
eseguire dei flussi di lavoro di stream e una computazione interattiva e iterativa.
Spark implementato in Scala, un linguaggio di programmazione statisticamente
tipizzato per la JVM che espone interfacce di programmazione native per Java e
Python, oltre che per lo stesso Scala. Sebbene il codice Java possa chiamare
direttamente linterfaccia di Scala, ci sono alcuni aspetti del sistema a tipi che
rende questo codice piuttosto difficile da manipolare; da qui luso dellAPI Java
nativa.
Scala dotato di una shell interattiva simile a quella di Ruby e Python; questo
consente agli utenti di eseguire Spark interattivamente dallinterprete per
interrogare i dataset. Linterprete di Scala opera compilando una classe per ogni
riga digitata dallutente, caricandola nella JVM e invocando una funzione su di
essa. Questa classe include un oggetto singleton che contiene le variabili o le
funzioni su quella riga ed esegue il codice della stessa in un metodo di
inizializzazione. In aggiunta alle sue ricche interfacce di programmazione, Spark si
sta affermando come motore di esecuzione, con strumenti popolari dellecosistema
di Hadoop (come Pig e Hive) portati sul framework.

Computazione dei cluster con i working set


Larchitettura di Spark centrata attorno al concetto di RDD (Resilient
Distributed Dataset), una raccolta di oggetti di Scala di sola lettura suddivisi su
un set di macchine che possono persistere in memoria. Questa astrazione fu
proposta per la prima volta nel documento del 2012 Resilient Distributed
Datasets: A Fault-Tolerant Abstraction for In-Memory Cluster Computing,
disponibile allindirizzo https://www.cs.berkeley.edu/~matei/papers/2012/nsdi_spark.pdf.
Unapplicazione Spark costituita da un programma driver che esegue
operazioni parallele su un cluster di processi worker a lunga durata e che possono
conservare le partizioni dei dati in memoria passando funzioni che vengono
eseguite come attivit parallele (Figura 5.1).
I processi sono coordinati attraverso unistanza di SparkContext. Questo si
connette a un gestore delle risorse (come YARN), richiede gli esecutori sui nodi
worker e invia le attivit che devono essere eseguite. Gli esecutori sono i
responsabili dellesecuzione delle attivit e della gestione della memoria a livello
locale.

Figura 5.1 Architettura dei cluster Spark.

Spark consente di condividere le variabili tra le attivit, o tra queste e il driver,


usando lastrazione delle variabili condivise. Di queste ne supporta due tipi: le
variabili di broadcast, che possono essere usate per conservare un valore in
memoria su tutti i nodi, e gli accumulatori, variabili aggiuntive come contatori e
somme.

Resilient Distributed Dataset (RDD)


Un RDD viene salvato in memoria, condiviso tra le macchine ed usato nelle
operazioni parallele come quelle di MapReduce. La tolleranza dei guasti viene
ottenuta attraverso il lineage (la discendenza): se una partizione di un RDD va
perduta, lRDD prende dagli altri RDD le informazioni sullorigine sufficienti a
poter ricostruire esattamente quella partizione. Un RDD pu essere costruito in
quattro modi:
leggendo i dati da un file salvato in HDFS:
suddividendo una collezione di Scala in un dato numero di partizioni che
vengono inviate ai worker;
trasformando un RDD esistente utilizzando operatori paralleli;
modificando la persistenza di un RDD esistente.
Spark d il meglio di s quando gli RDD si adattano alla memoria e possono
essere conservati tra le operazioni. LAPI espone i metodi per la persistenza degli
RDD e consente numerose strategie di persistenza e livelli di storage, permettendo
lo spill su disco oltre che la serializzazione binaria per lefficienza dello spazio.

Azioni
Le operazioni vengono invocate passando le funzioni a Spark. Il sistema
gestisce le variabili e gli effetti collaterali secondo il paradigma di
programmazione funzionale. Le closure possono fare riferimento alle variabili
nellambito in cui sono state create. Esempi di azioni sono count (che restituisce il
numero di elementi nel dataset) e save (che genera il dataset nello storage). Altre
opzioni parallele sugli RDD includono le seguenti.
map: applica una funzione a ciascun elemento del dataset.
filter: seleziona gli elementi da un dataset secondo criteri forniti dallutente.

reduce: combina gli elementi di un dataset usando una funzione associativa.

collect: invia tutti gli elementi del dataset al programma driver.

foreach: passa ogni elemento attraverso una funzione fornita dallutente.

groupByKey: raggruppa le voci in base a una chiave.

sortByKey: ordina le voci per chiave.

Distribuzione
Spark pu essere eseguito sia in modalit locale, analogamente al setup a nodo
singolo di Hadoop, sia come gestore di risorse (resource mananger). Tra i gestori
di risorse attualmente supportati troviamo i seguenti:
Spark Standalone Cluster Mode;
YARN;
Apache Mesos.

Spark su YARN
Per distribuire Spark su YARN occorre un file JAR consolidato ad hoc. Spark
avvia unistanza del cluster distribuito standalone dentro il ResourceManager.
Cloudera e MapR forniscono entrambi Spark su YARN come parte della loro
distribuzione software. Al momento della stesura di questo libro, Spark
disponibile per HDP di Hortonworks come anteprima della tecnologia
(http://hortonworks.com/hadoop/spark/).

Spark su EC2
Spark fornisce uno script di distribuzione, spark-ec2, collocato nella directory
ec2. Questo script imposta automaticamente Spark e HDFS su un cluster di istanze
di EC2. Per poter avviare un cluster di Spark sul cloud di Amazon, accedete alla
directory ec2 ed eseguite questo comando:
./spark-ec2 -k <keypair> -i <key-file> -s <num-slaves> launch <cluster-name>

<keypair> il nome della vostra coppia EC2, <key-file> il file della chiave
privata per la coppia di chiavi, <num-slaves> il numero dei nodi slave da avviare e
<cluster-name> il nome da assegnare al cluster. Consultate il Capitolo 1 per

ulteriori dettagli riguardanti limpostazione delle coppie di chiavi e verificate che


lo scheduler del cluster sia attivo e veda tutti i nodi slave accedendo alla sua
interfaccia utente web, il cui indirizzo verr stampato una volta che lo script
completato.
Potete specificare un percorso in S3 come input attraverso un URI nella forma
s3n://<bucket>/path. Dovrete anche impostare le vostre credenziali di sicurezza di

Amazon, impostando le variabili dambiente AWS_ACCESS_KEY_ID e AWS_SECRET_ACCESS_KEY


prima che il programma venga eseguito oppure attraverso
SparkContext.hadoopConfiguration.

Iniziare con Spark


I binari di Spark e il codice sorgente sono disponibili sul sito web del progetto
allindirizzo http://spark.apache.org/. Gli esempi nel prossimo paragrafo sono stati
testati usando Spark 1.1.0 costruito dal sorgente sulla macchina virtuale Cloudera
CDH 5.0 QuickStart.
Scaricate e decomprimete larchivio gzip con i seguenti comandi:
$ wget http://d3kbcqa49mib13.cloudfront.net/spark-1.1.0.tgz
$ tar xvzf spark-1.1.0.tgz
$ cd spark-1.1.0

Spark costruito su Scala 2.10 e utilizza sbt (https://github.com/sbt/sbt) per creare


il core del sorgente e i relativi esempi:
$ ./sbt/sbt -Dhadoop.version=2.2.0 -Pyarn assembly

Con le opzioni -Dhadoop.version=2.2.0 e -Pyarn, diciamo a sbt di costruire su Hadoop


versione 2.2.0 o successive e di abilitare il supporto per YARN.
Avviate Spark in modalit standalone con questo comando:
$ ./sbin/start-all.sh

Il comando lancer unistanza master locale a spark://localhost:7077 insieme a un


nodo worker.
Si pu accedere a uninterfaccia web del nodo master a http://localhost:8080/
(Figura 5.2).

Figura 5.2 Interfaccia web del nodo master.


Spark pu essere eseguito in modo interattivo attraverso spark-shell, che una
versione modificata della shell di Scala. Come primo esempio, implementeremo
un conteggio di parole del dataset di Twitter che abbiamo illustrato nel Capitolo 3
usando lAPI Scala.
Avviate una sessione interattiva di spark-shell eseguendo questo comando:
$ ./bin/spark-shell

La shell istanzia un oggetto SparkContext, sc, che responsabile della gestione


delle connessioni del driver ai nodi worker. Ne descriveremo la semantica
successivamente nel capitolo.
Per facilitare un po le cose, creiamo un dataset di testo campione che contiene
un aggiornamento dello stato per riga:
$ stream.py -t -n 1000 > sample.txt

Copiatelo in HDFS:
$ hdfs dfs -put sample.txt /tmp

In spark-shell, creiamo prima un RDD file dai dati di esempio:


val file = sc.textFile(/tmp/sample.txt)

Applichiamo poi una serie di trasformazioni per contare le occorrenze delle


parole in questo file. Notate che loutput della catena di trasformazione counts
ancora un RDD:
val counts = file.flatMap(line => line.split( ))
.map(word => (word, 1))
.reduceByKey((m, n) => m + n)

La catena corrisponde alle fasi di map e reduce con cui abbiamo gi una certa
familiarit. Nella fase di map, carichiamo ogni riga del dataset (flatMap),
scomponiamo ciascun tweet in una sequenza di parole, contiamo le occorrenze di
ogni parola (map) ed emettiamo le coppie (key, value). Nella fase di reduce,
raggruppiamo per chiave (word) e sommiamo i valori (m, n) per ottenere il conteggio
delle parole.
Infine, visualizziamo nella console i primi 10 elementi, counts.take(10):
counts.take(10).foreach(println)

Scrivere ed eseguire applicazioni standalone


Spark consente la scrittura di applicazioni standalone attraverso luso di tre
API: Scala, Java e Python.

API Scala
La prima cosa che un driver di Spark deve fare creare un oggetto SparkContext
che dica a Spark come accedere a una cluster. Questo avviene dopo aver importato
le classi e aver effettuato le conversioni implicite in un programma, cos:
import org.apache.spark.SparkContext
import org.apache.spark.SparkContext._

Loggetto SparkContext pu essere creato con il seguente costruttore:


new SparkContext(master, appName, [sparkHome])

e anche attraverso SparkContext(conf), che prende un oggetto SparkConf.


Il parametro principale una stringa che specifica un URI del cluster a cui
connettersi (come spark://localhost:7077) o una stringa local da eseguire in modalit
locale. appName il nome dellapplicazione che apparir nellinterfaccia utente web
del cluster.
Non possibile loverriding della classe predefinita SparkContext, n se ne pu
creare una nuova allinterno della shell di Spark in esecuzione. Tuttavia, si pu
specificare a quale nodo master si connetter il contesto usando la variabile
dambiente MASTER. Per esempio, per eseguire spark-shell sui quattro core, usiamo
$ MASTER=local[4] ./bin/spark-shell

API Java
Il package org.apache.spark.api.java espone tutte le caratteristiche di Spark
disponibili nella versione di Scala su Java. LAPI Java ha una classe
JavaSparkContext che restituisce istanze di org.apache.spark.api.java.JavaRDD e lavora con

le collezioni Java invece che con quelle di Scala.


Tra lAPI Java e lAPI Scala ci sono alcune differenze chiave.
Java 7 non supporta le funzioni anonime o di prima classe; le funzioni devono
quindi essere implementate estendendo
org.apache.spark.api.java.function.Function, Function2 e altre classi. Come in Spark

versione 1.0, lAPI stata rifattorizzata per supportare le espressioni lambda


di Java 8. Con Java 8, le classi Function possono essere sostituite con
espressioni inline che agiscono da abbreviazione per le funzioni anonime.
I metodi RDD restituiscono collezioni Java.
Le coppie chiave-valore, che in Scala sono scritte semplicemente come (key,
value), sono rappresentate dalla classe scala.Tuple2.

Per mantenere la sicurezza del tipo, alcuni metodi RDD e di funzione, come
quelli che gestiscono le coppie di chiavi e i double, sono implementate come
classi specializzate.

WordCount in Java
Un esempio di WordCount in Java incluso nella distribuzione del codice
sorgente di Spark alla pagina
examples/src/main/java/org/apache/spark/examples/JavaWordCount.java.

Prima di tutto, creiamo un contesto usando la classe JavaSparkContext:


JavaSparkContext sc = new JavaSparkContext(master, JavaWordCount,
System.getenv(SPARK_HOME), JavaSparkContext.jarOfClass(JavaWordCount.class));

JavaRDD<String> data = sc.textFile(infile, 1);


JavaRDD<String> words = data.flatMap(new FlatMapFunction<String, String>() {
@Override
public Iterable<String> call(String s) {
return Arrays.asList(s.split( ));
}
});

JavaPairRDD<String, Integer> ones = words.map(new PairFunction<String, String, Integer>() {


@Override
public Tuple2<String, Integer> call(String s) {
return new Tuple2<String, Integer>(s, 1);
}
});

JavaPairRDD<String, Integer> counts = ones.reduceByKey(new Function2<Integer, Integer,


Integer>() {
@Override
public Integer call(Integer i1, Integer i2) {
return i1 + i2;
}
});

Costruiamo poi un RDD dallinfile HDFS. Nel primo passo della catena di
trasformazione, scomponiamo ogni tweet in un dataset e restituiamo un elenco di
parole. Utilizziamo unistanza di JavaPairRDD<String, Integer> per contare le
occorrenze di ogni parola. Per finire, riduciamo lRDD a una nuova istanza di
JavaPairRDD<String, Integer> che contiene una lista di tuple, dove ciascuna rappresenta

una parola e il numero di volte che stata trovata nel dataset.


API Python
PySpark richiede Python versione 2.6 o successiva. Gli RDD supportano gli
stessi metodi delle loro controparti di Scala, ma prendono le funzioni Python e
restituiscono tipi di collezioni Python. La sintassi lambda
(https://docs.python.org/2/reference/expressions.html) utilizzata per passare le funzioni
agli RDD.
Il conteggio delle parole in pyspark relativamente simile a quello del
corrispettivo di Scala:
tweets = sc.textFile(/tmp/sample.txt)
counts = tweets.flatMap(lambda tweet: tweet.split( )) \
.map(lambda word: (word, 1)) \
.reduceByKey(lambda m,n:m+n)

Il costrutto lambda crea funzioni anonime durante il runtime. lambda tweet:


tweet.split( ) crea una funzione che prende un tweet stringa come input e genera in

output un elenco di stringhe separate da uno spazio bianco. flatMap di Spark applica
questa funzione a ogni riga del dataset tweets.
Nella fase di map, per ogni token word, lambda word: (word, 1) restituisce tuple (word,
1) che indicano loccorrenza di una parola nel dataset. In reduceByKey, raggruppiamo

queste tuple per chiave (word) e sommiamo i valori per ottenere un conteggio delle
parole con lambda m,n:m+n.
Lecosistema di Spark
Apache Spark fornisce diversi strumenti, sia come libreria sia come motore di
esecuzione.

Spark Streaming
Spark Streaming (http://spark.apache.org/docs/latest/streaming-programming-guide.html)
unestensione dellAPI Scala che consente lelaborazione di dati da stream come
i socket Kafka, Flume, Twitter, ZeroMQ e TCP.
Spark Streaming riceve stream di dati di input live e li divide in batch (finestre
temporali dimensionate in modo arbitrario) che vengono poi elaborati dal motore
core di Spark per generare lo stream finale dei risultati. Questa astrazione di alto
livello chiamata DStream (org.apache.spark.streaming.dstream.DStreams) ed
implementata come una sequenza di RDD. DStream rende possibili due tipi di
operazioni: quelle di trasformazione e quelle di output.
Le trasformazioni lavorano su uno o pi DStream per crearne di nuovi. Come parte
della catena delle trasformazioni, i dati possono essere resi persistenti a un livello
di storage (HDFS) oppure su un canale di output. Spark Streaming consente le
trasformazioni in una finestra scorrevole di dati.
Unoperazione basata su finestre richiede la specifica di tre parametri: la
lunghezza della finestra, la sua durata e lintervallo di scorrimento in base al quale
loperazione verr eseguita.

GraphX
GraphX (https://spark.apache.org/docs/latest/graphx-programming-guide.html) unAPI
di calcolo grafico che espone un set di operatori e algoritmi per la computazione
orientata ai grafici, oltre che una variante ottimizzata di Pregel.

MLlib
MLlib (http://spark.apache.org/docs/latest/mllib-guide.html) fornisce funzionalit
Machine Learning (ML) comuni, tra cui test e generatori di dati. MLlib supporta
attualmente quattro tipi di algoritmi: classificazione binaria, regressione,
clustering e filtro collaborativo.

Spark SQL
Spark SQL deriva da Shark, unimplementazione del sistema di warehousing di
Hive che utilizza Spark come motore di esecuzione. Parleremo di Hive nel
Capitolo 7. Con Spark SQL, possibile mescolare query di tipo SQL con codice
Scala o Python. I set di risultati restituiti da una query sono degli RDD, e in quanto
tali possono essere manipolati dai metodi principali di Spark o da MLlib e
GraphX.
Elaborare i dati con Apache Spark
In questo paragrafo implementeremo gli esempi del Capitolo 3 utilizzando lAPI
Scala. Considereremo sia lo scenario dellelaborazione a batch sia quello in
tempo reale. Vedremo come Spark Streaming pu essere utilizzato per effettuare
calcoli statistici sullo stream live di Twitter.

Costruire ed eseguire gli esempi


Il codice di Scala per gli esempi si trova alla pagina
. Useremo sbt per
https://github.com/learninghadoop2/book-examples/tree/master/ch5

costruire, gestire ed eseguire il codice.


Il file build.sbt controlla i metadati della base di codice e le dipendenze
software; queste includono la versione dellinterprete di Scala a cui si collega
Spark, un link al repository di package Akka usato per risolvere le dipendenze
implicite e alcune dipendenze sulle librerie di Spark e Hadoop.
Il codice sorgente di tutti gli esempi pu essere compilato con
$ sbt compile

In alternativa, pu essere compresso in un file JAR con


$ sbt package

Con le righe seguenti si pu generare uno script helper per eseguire le classi
compilate:
$ sbt add-start-script-tasks
$ sbt start-script

Lhelper pu essere invocato come segue:


$ target/start <class name> <master> <param1> <param n>

Qui <master> lURI del nodo master. Una sessione interattiva di Scala pu
essere invocata tramite sbt con il seguente comando:
$ sbt console

Questa console non la stessa shell interattiva di Spark, ma piuttosto un modo


alternativo per eseguire il codice. Per eseguire il codice Spark al suo interno
dobbiamo importare e istanziare manualmente un oggetto SparkContext. Tutti gli
esempi presentati in questo paragrafo necessitano di un file twitter4j.properties che
contiene la chiave del consumatore e quella segreta, e richiedono che i token di
accesso si trovino nella stessa directory dove invocato sbt o spark-shell:
oauth.consumerKey=
oauth.consumerSecret=
oauth.accessToken=
oauth.accessTokenSecret=

Eseguire gli esempi su YARN


Per eseguire gli esempi su una griglia YARN, dobbiamo prima costruire un file
JAR usando
$ sbt package

Dopodich lo passiamo al gestore delle risorse tramite il comando spark-submit:


./bin/spark-submit --class application.to.execute --master yarn-cluster [options]
target/scala-2.10/chapter-4_2.10-1.0.jar [<param1> <param n>]

A differenza della modalit standalone, non serve specificare un URI <master>. In


YARN, il ResourceManager viene selezionato dalla configurazione del cluster.
Trovate ulteriori informazioni su come avviare Spark su YARN allindirizzo
http://spark.apache.org/docs/latest/running-on-yarn.html.

Trovare i topic pi popolari


A differenza degli esempi precedenti con la shell di Spark, inizializziamo uno
SparkContext come parte del programma. Passiamo tre argomenti al costruttore di

SparkContext: il tipo di scheduler che vogliamo usare, un nome per lapplicazione e

la directory in cui Spark installato:


import org.apache.spark.SparkContext._
import org.apache.spark.SparkContext
import scala.util.matching.Regex

object HashtagCount {
def main(args: Array[String]) {
[]
val sc = new SparkContext(master,
HashtagCount,
System.getenv(SPARK_HOME))

val file = sc.textFile(inputFile)


val pattern = new Regex((?:\\s|\\A|^)[##]+([A-Za-z0-9-_]+))

val counts = file.flatMap(line =>


(pattern findAllIn line).toList)
.map(word => (word, 1))
.reduceByKey((m, n) => m + n)

counts.saveAsTextFile(outputPath)
}
}
Creiamo un RDD iniziale da un dataset salvato in HDFS inputFile e
applichiamo una logica simile a quella dellesempio di WordCount.
Per ogni tweet nel dataset, estraiamo un array di stringhe che corrisponde al
pattern degli hashtag (pattern findAllIn line).toArray e contiamo unoccorrenza di
ciascuna stringa usando loperatore di mappa. In questo modo si genera un nuovo
RDD come lista di tuple nella forma
(word, 1), (word2, 1), (word, 1)

Infine, combiniamo gli elementi di questo RDD usando il metodo reduceByKey() e


risalviamo lRDD generato in questa fase in HDFS con saveAsTextFile.
Trovate il codice per il programma driver standalone alla pagina
http://bit.ly/1GcHVlY.

Assegnare un sentiment ai topic


Il codice sorgente di questo esempio si trova alla pagina http://bit.ly/1BLlRz4:
import org.apache.spark.SparkContext._
import org.apache.spark.SparkContext
import scala.util.matching.Regex
import scala.io.Source

object HashtagSentiment {
def main(args: Array[String]) {
[]
val sc = new SparkContext(master,
HashtagSentiment,
System.getenv(SPARK_HOME))

val file = sc.textFile(inputFile)

val positive = Source.fromFile(positiveWordsPath)


.getLines
.filterNot(_ startsWith ;)
.toSet
val negative = Source.fromFile(negativeWordsPath)
.getLines
.filterNot(_ startsWith ;)
.toSet

val pattern = new Regex((?:\\s|\\A|^)[##]+([A-Za-z0-9-_]+))


val counts = file.flatMap(line => (pattern findAllIn line).map({
word => (word, sentimentScore(line, positive, negative))
})).reduceByKey({ (m, n) => (m._1 + n._1, m._2 + n._2) })

val sentiment = counts.map({hashtagScore =>


val hashtag = hashtagScore._1
val score = hashtagScore._2
val normalizedScore = score._1 / score._2
(hashtag, normalizedScore)
})
sentiment.saveAsTextFile(outputPath)
}
}

Come prima cosa, leggiamo un elenco di parole positive e negative negli oggetti
Set di Scala e filtriamo i commenti (le stringhe che iniziano con ;).

Quando troviamo un hashtag, chiamiamo una funzione sentimentScore per


valutare il sentiment espresso da quel testo. Questa funzione implementa la stessa
logica che abbiamo utilizzato nel Capitolo 3 per determinare il sentiment di un
tweet. Prende come parametri di input il testo del tweet, str, e un elenco di parole
positive e negative come oggetti Set[String]. Il valore restituito la differenza tra i
punteggi positivi e negativi e il numero di parole nei tweet. In Spark,
rappresentiamo questo valore come una coppia di oggetti Double e Integer:
def sentimentScore(str: String, positive: Set[String],
negative: Set[String]): (Double, Int) = {
var positiveScore = 0; var negativeScore = 0;
str.split(\s+).foreach { w =>
if (positive.contains(w)) { positiveScore+=1; }
if (negative.contains(w)) { negativeScore+=1; }
}
((positiveScore - negativeScore).toDouble,
str.split(\s+).length)
}

Riduciamo loutput della mappa aggregando in base alla chiave (lhashtag). In


questa fase, emettiamo un output triplo costituito dallhashtag, dalla somma della
differenza tra punteggi positivi e negativi e dal numero di parole per tweet.
Compiamo poi unulteriore operazione di mappa per normalizzare il punteggio del
sentiment e salvare in HDFS lelenco risultante degli hashtag e le coppie di
sentiment.

Elaborazione dei dati sugli stream


Lesempio precedente pu essere adattato facilmente per lavorare su uno stream
di dati in tempo reale. In questo paragrafo e nel prossimo, utilizzeremo spark-
streaming-twitter per eseguire alcune semplici attivit di analisi sul firehose:

val window = 10
val ssc = new StreamingContext(master, TwitterStreamEcho,
Seconds(window), System.getenv(SPARK_HOME))

val stream = TwitterUtils.createStream(ssc, auth)

val tweets = stream.map(tweet => (tweet.getText()))


tweets.print()
ssc.start()
ssc.awaitTermination()
}

Trovate il codice sorgente di Scala per questo esempio alla pagina


http://bit.ly/1x7Qf6V.

I due package principali da importare sono:


import org.apache.spark.streaming.{Seconds, StreamingContext}
import org.apache.spark.streaming.twitter.

Inizializziamo un nuovo StreamingContext ssc su un cluster locale usando una


finestra di 10 secondi, e utilizziamo questo contesto per creare un DStream di tweet
di cui visualizzeremo il testo.
Se lesecuzione ha successo, il firehose in tempo reale di Twitter verr ripetuto
nel terminale in batch di 10 secondi. Notate che la computazione continuer
ininterrottamente, ma pu essere fermata in qualunque momento premendo Ctrl+C.
Loggetto TwitterUtils un wrapper per la libreria Twitter4j
(http://twitter4j.org/en/index.html) dotato di spark-streaming-twitter. Una chiamata
riuscita a TwitterUtils.createStream restituir un DStream di oggetti Twitter4j
(TwitterInputDStream). Nellesempio precedente abbiamo utilizzato il metodo getText()
per estrarre il testo del tweet; osservate per che loggetto twitter4j espone lintera
API Twitter. Per esempio, possiamo visualizzare uno stream di utenti con la
seguente chiamata:
val users = stream.map(tweet => (tweet.getUser().getId(),
tweet.getUser().getName()))
users.print()

Gestione dello stato


Spark Streaming fornisce un DStream ad hoc per mantenere lo stato di ogni chiave
in un RDD e il metodo updateStateByKey per trasformare lo stato.
Possiamo riutilizzare il codice dellesempio a batch per assegnare e aggiornare
i punteggi del sentiment sugli stream:
object StreamingHashTagSentiment {
[]

val counts = text.flatMap(line => (pattern findAllIn line)


.toList
.map(word => (word, sentimentScore(line, positive, negative))))
.reduceByKey({ (m, n) => (m._1 + n._1, m._2 + n._2) })

val sentiment = counts.map({hashtagScore =>


val hashtag = hashtagScore._1
val score = hashtagScore._2
val normalizedScore = score._1 / score._2
(hashtag, normalizedScore)
})

val stateDstream = sentiment


.updateStateByKey[Double](updateFunc)

stateDstream.print

ssc.checkpoint(/tmp/checkpoint)
ssc.start()
}

Un DStream di stato viene creato chiamando hashtagSentiment.updateStateByKey.


La funzione updateFunc implementa la logica di trasformazione dello stato, che
una somma complessiva dei punteggi del sentiment in un dato periodo di tempo:
val updateFunc = (values: Seq[Double], state: Option[Double]) => {
val currentScore = values.sum

val previousScore = state.getOrElse(0.0)

Some( (currentScore + previousScore) * decayFactor)


}

un valore costante, inferiore o uguale a zero, che usiamo per ridurre


decayFactor

proporzionalmente il punteggio nel tempo. Come si pu intuire, interverr sugli


hashtag quando non sono pi di tendenza. Spark Streaming scrive dei dati
intermedi per le operazioni stateful su HDFS, quindi ci occorre un checkpoint per
il contesto di Streaming, che possiamo creare con ssc.checkpoint. Trovate il codice
sorgente di questo esempio alla pagina http://bit.ly/1wPzqxa.

Analisi dei dati con Spark SQL


Spark SQL pu semplificare le attivit di rappresentazione e manipolazione dei
dati strutturati. Caricheremo un file JSON in una tabella temporanea e calcoleremo
alcune semplici statistiche fondendo istruzioni SQL e codice Scala:
object SparkJson {
[]
val file = sc.textFile(inputFile)

val sqlContext = new org.apache.spark.sql.SQLContext(sc)


import sqlContext._

val tweets = sqlContext.jsonFile(inFile)


tweets.printSchema()

// Registra lo SchemaRDD come tabella


tweets.registerTempTable(tweets)
val text = sqlContext.sql(SELECT text, user.id FROM tweets)
// Trova i 10 hastag pi popolari
val pattern = new Regex((?:\\s|\\A|^)[##]+([A-Za-z0-9-_]+))

val counts = text.flatMap(sqlRow => (pattern findAllIn


sqlRow(0).toString).toList)
.map(word => (word, 1))
.reduceByKey( (m, n) => m+n)
counts.registerTempTable(hashtag_frequency)

counts.printSchema

val top10 = sqlContext.sql(SELECT _1 as hashtag, _2 as frequency


FROM hashtag_frequency order by frequency desc limit 10)

top10.foreach(println)
}

Come negli esempi precedenti, istanziamo uno SparkContext sc e carichiamo il


dataset di tweet JSON. Creiamo quindi unistanza di org.apache.spark.sql.SQLContext
basata sullsc esistente. import sqlContext._ d laccesso a tutte le funzioni e alle
convenzioni implicite per sqlContext. Carichiamo il dataset di tweet JSON usando
sqlContext.jsonFile. Loggetto tweets risultante unistanza di SchemaRDD, un nuovo tipo

di RDD introdotto da Spark SQL. La classe SchemaRDD concettualmente simile a


una tabella di un database relazionale; costituita da oggetti Row e da uno schema
che descrive il contenuto di ogni Row. Possiamo vedere lo schema per un tweet
chiamando tweets.printSchema(). Prima di poter manipolare i tweet con le istruzioni
SQL, dovremo registrare SchemaRDD come una tabella nellSQLContext. Estraiamo
quindi il campo di testo di un tweet JSON con una query SQL. Notate che loutput
di sqlContext.sql di nuovo un RDD; in quanto tale, possiamo manipolarlo usando i
metodi principali di Spark. Nel nostro caso riutilizziamo la logica che abbiamo
impiegato negli esempi precedenti per estrarre gli hashtag e contiamo le loro
occorrenze. Infine, registriamo lRDD risultante come una tabella, hashtag_frequency,
e ordiniamo gli hashtag secondo la frequenza con una query SQL.
Trovate il codice sorgente di questo esempio alla pagina http://bit.ly/1GL7qdO.

SQL sugli stream di dati


Al momento della stesura di questo libro, un SQLContext non poteva essere
istanziato direttamente da un oggetto StreamingContext. tuttavia possibile
interrogare un DStream registrando uno SchemaRDD per ciascun RDD di uno stream:
object SqlOnStream {
[]
val ssc = new StreamingContext(sc, Seconds(window))

val gson = new Gson()

val dstream = TwitterUtils


.createStream(ssc, auth)
.map(gson.toJson(_))

val sqlContext = new org.apache.spark.sql.SQLContext(sc)


import sqlContext._

dstream.foreachRDD( rdd => {


rdd.foreach(println)
val jsonRDD = sqlContext.jsonRDD(rdd)
jsonRDD.registerTempTable(tweets)
jsonRDD.printSchema

sqlContext.sql(query)
})

ssc.checkpoint(/tmp/checkpoint)
ssc.start()
ssc.awaitTermination()
}

Perch i due lavorino insieme, creiamo prima uno SparkContext sc che utilizziamo
per istanziare sia uno StreamingContext ssc sia un sqlContext. Come prima, usiamo
TwitterUtils.createStream per creare un RDD DStream dstream. In questo esempio

utilizzeremo il parser JSON Gson di Google per serializzare ciascun oggetto


twitter4j in una stringa JSON. Per eseguire query SQL Spark sullo stream,

registriamo uno SchemaRDD jsonRDD allinterno di un ciclo dstream.foreachRDD. Utilizziamo


il metodo sqlContext.jsonRDD per creare un RDD da un batch di tweet JSON,
dopodich potremo interrogare lo SchemaRDD attraverso il metodo sqlContext.sql.
Trovate il codice sorgente di questo esempio alla pagina http://bit.ly/1AM8QQL.
Spark e Samza Streaming a confronto
utile mettere a confronto Samza e Spark Streaming per identificare le aree in
cui possono essere applicati al meglio. Come dovrebbe essere chiaro da quanto
detto finora, queste tecnologie sono profondamente complementari. Anche se Spark
Streaming potrebbe apparire un concorrente di Samza, entrambi i prodotti offrono
notevoli vantaggi specifici in determinate aree.
Samza eccelle quando i dati di input sono effettivamente uno stream di eventi
distinti e voi desiderate impostare un tipo di elaborazione che opera su questo tipo
di input. I job di Samza eseguiti su Kafka possono mostrare latenze nellordine di
millisecondi. Questo fornisce un modello di programmazione focalizzato sui
singoli messaggi, pi adatto alle applicazioni di elaborazione quasi in tempo reale.
Sebbene non supporti le topologie di costruzione di job collaborativi, il suo
modello semplice permette la realizzazione e lo sviluppo potenziale di costrutti
del genere. Anche il modello di partizionamento e scala centrato sulla
semplicit, e questo rende Samza unapplicazione facile da capire e la scelta
privilegiata quando si devono gestire cose intrinsecamente complesse come i dati
in tempo reale.
Spark molto pi che un prodotto di streaming. Il supporto per la creazione di
strutture di dati distribuite da dataset esistenti e luso di potenti primitive per
manipolarli gli consentono di elaborare dataset molto grandi a un livello elevato
di granularit. Nellecosistema di Spark sono poi presenti altri prodotti che
costruiscono ulteriori interfacce o astrazioni partendo dal core di elaborazione a
batch.
Il modello a batch si ritrova anche in Spark Streaming; a differenza di un
modello di elaborazione per messaggio, suddivide lo stream di messaggi in una
serie di RDD. Avendo un motore di esecuzione veloce, le latenze scendono a 1
secondo (http://www.cs.berkeley.edu/~matei/papers/2012/hotcloud_spark_streaming.pdf). Nel
caso di carichi di lavoro che devono analizzare lo stream in questo modo, Spark si
adatta meglio del modello per messaggio di Samza, che richiede una logica
supplementare per fornire questo tipo di windowing.
Riepilogo
In questo capitolo abbiamo esaminato Spark e abbiamo mostrato la sua
elaborazione iterativa come nuovo framework sul quale si possono costruire
applicazioni su YARN. In particolare, abbiamo evidenziato:
il modello di elaborazione basato su una struttura di dati distribuita di Spark
e come questo consente unelaborazione efficiente dei dati in memoria;
il pi vasto ecosistema di Spark e come vengono costruiti su di esso i vari
progetti aggiuntivi per rendere ancora pi specializzato il modello di calcolo.
Nel prossimo capitolo esploreremo Apache Pig e il suo linguaggio di
programmazione, Pig Latin. Vedremo come questo strumento pu semplificare
notevolmente lo sviluppo software per Hadoop attraverso lastrazione di parte
della complessit di MapReduce e Spark.
Capitolo 6
Analisi dei dati con Apache Pig

Nei capitoli precedenti abbiamo esplorato alcune API per lelaborazione dei
dati. MapReduce, Spark, Tez e Samza sono di livello piuttosto basso, e scrivere
una logica business complessa con esse richiede spesso uno sviluppo Java
significativo. Inoltre, utenti diversi hanno esigenze diverse. Per un analista
potrebbe essere poco pratico scrivere codice di MapReduce o costruire DAG di
input e output per rispondere a una manciata di query semplici. Allo stesso tempo,
un ingegnere software o un ricercatore potrebbero voler creare dei prototipi delle
loro idee e dei loro algoritmi usando astrazioni di alto livello prima di passare ai
dettagli di implementazione di basso livello.
In questo capitolo e nel prossimo considereremo alcuni strumenti che forniscono
un modo per elaborare i dati in HDFS attraverso astrazioni di livello pi elevato.
Qui illustreremo Apache Pig, e in particolare affronteremo i seguenti argomenti.
Cos Apache Pig e quale modello di flusso dati fornisce.
I tipi di dati e le funzioni di Pig Latin.
Come migliorare Pig tramite del codice personalizzato dellutente.
Come utilizzare Pig per analizzare lo stream di Twitter.
Panoramica su Pig
In origine, il toolkit di Pig era costituito da un compilatore che generava
programmi MapReduce, accorpava le loro dipendenze e le eseguiva su Hadoop. I
job di Pig sono scritti in un linguaggio chiamato Pig Latin e possono essere
eseguiti sia in modalit interattiva sia in batch. Inoltre, Pig Latin pu essere esteso
usando le User Defined Function (UDF) scritte in Java, Python, Ruby, Groovy o
JavaScript.
Gli ambiti in cui si pu utilizzare Pig sono i seguenti:
elaborazione dei dati;
query di analisi ad hoc;
prototipazione rapida degli algoritmi;
pipeline estrazione-trasformazione-caricamento.
Seguendo una tendenza gi rilevata nei capitoli precedenti, Pig si sta muovendo
verso unarchitettura di calcolo general purpose. Dalla versione 0.13 linterfaccia
ExecutionEngine (org.apache.pig.backend.executionengine) agisce da ponte tra il
frontend e il backend di Pig, consentendo agli script Pig Latin di essere compilati
ed eseguiti su framework diversi da MapReduce.
Al momento della stesura di queste righe, la versione 0.13 era dotata di
MRExecutionEngine
(org.apache.pig.backend.hadoop.executionengine.mapReduceLayer.MRExecutionEngine); si ipotizza
che nella versione 0.14 (https://issues.apache.org/jira/browse/PIG-3446) verr incluso il
lavoro su un backend a bassa latenza basato su Tez
(http://pig.apache.org/docs/r0.14.0/api/org/apache/pig/backend/hadoop/executionengine/tez/plan/operator/pa
tree.html). Attualmente, inoltre, in corso lo sviluppo dellintegrazione di Spark

(https://issues.apache.org/jira/browse/PIG-4059).
Pig 0.13 fornisce alcuni miglioramenti nelle prestazioni per il backend di
MapReduce, in particolare con due funzionalit che riducono la latenza dei job pi
piccoli: laccesso diretto a HDFS (https://issues.apache.org/jira/browse/PIG-3642) e la
modalit locale automatica (https://issues.apache.org/jira/browse/PIG-3463). La prima
caratteristica (propriet opt.fetch) abilitata di default.
Quando si esegue un DUMP in uno script semplice (solo map) che contiene
esclusivamente gli operatori LIMIT, FILTER, UNION, STREAM o FOREACH, i dati di input
vengono raccolti da HDFS, e la query viene eseguita direttamente in Pig,
aggirando MapReduce. Con la modalit locale automatica (propriet
pig.auto.local.enabled) Pig esegue una query nella modalit locale di Hadoop quando

le dimensioni dei dati sono inferiori a pig.auto.local.input.maxbytes. Questa propriet


disabilitata di default.
Pig lancer i job di MapReduce se entrambe le modalit sono disattivate o se la
query non ha i requisiti per esse. Se tali modalit sono attive, Pig controller se la
query ha i requisiti per laccesso diretto, e se cos fosse passer alla modalit
locale automatica. Se anche questa fallisce, eseguir la query su MapReduce.
Per iniziare
Utilizzeremo le opzioni dello script stream.py per estrarre i dati JSON e
recuperare un numero specifico di tweet. Eseguiamo un comando come questo:
$ python stream.py -j -n 10000 > tweets.json

Il tweets.json conterr una stringa JSON su ciascuna riga che rappresenta un


tweet.
Ricordate che necessario rendere disponibili le credenziali dellAPI Twitter
come variabili dambiente o immetterle direttamente nello script.
Eseguire Pig
Pig uno strumento che traduce le istruzioni scritte in Pig Latin e le esegue su
una singola macchina in modalit standalone o su un intero cluster Hadoop in
modalit distribuita. Anche nel secondo caso, il ruolo di Pig quello di tradurre le
istruzioni Pig Latin in job di MapReduce, e quindi non richiesta linstallazione di
servizi o daemon supplementari. Viene utilizzato come strumento a riga di
comando con le librerie associate.
Cloudera CDH ha Apache Pig versione 0.12. In alternativa, trovate il codice
sorgente di Pig e le distribuzioni binarie alla pagina
https://pig.apache.org/releases.html.

Come prevedibile, la modalit MapReduce richiede laccesso a un cluster


Hadoop e linstallazione di HDFS; questa la modalit predefinita quando si
esegue il comando pig dal prompt. Gli script possono essere eseguiti con questo
comando:
$ pig -f <script>

I parametri possono essere passati attraverso la riga di comando usando -param


<param>=<val>, come segue:

$ pig param input=tweets.txt

Possono inoltre essere specificati in un file param passabile a Pig attraverso


lopzione -param_file <file>. possibile specificare pi file. Se un parametro
presente pi volte nel file, sar utilizzato lultimo valore e verr visualizzato un
avvertimento. Un file param contiene un parametro per riga. Sono ammesse righe
vuote e anche i commenti (segnalati con #). In uno script Pig, i parametri sono nella
forma $<parameter>. Il valore di default pu essere assegnato usando listruzione
default: %default input tweets.json. Il comando default non funzioner in una sessione

di Grunt (parleremo di Grunt nel prossimo paragrafo).


In modalit local, tutti i file sono installati ed eseguiti usando lhost e il file
system locali. Specificatela utilizzando il flag -x:
$ pig -x local

In entrambe le modalit, i programmi Pig possono essere eseguiti nella shell


interattiva o in batch.
Grunt, la shell interattiva di Pig
Pig pu girare in una modalit interattiva utilizzando la shell di Grunt, che
invocata quando usiamo il comando pig al prompt del terminale. Nel resto del
capitolo presupporremo che gli esempi vengano eseguiti in una sessione di Grunt.
Diversamente da quando si lavora con le istruzioni Pig Latin, Grunt offre alcune
utility e un accesso ai comandi della shell.
fs: consente agli utenti di manipolare gli oggetti del file system di Hadoop e
ha la stessa semantica della CLI di Hadoop.
sh: esegue i comandi tramite la shell del sistema operativo.

exec: avvia uno script Pig allinterno di una sessione interattiva di Grunt.

kill: sopprime un job di MapReduce.

help: visualizza un elenco di tutti i comandi disponibili.

Elastic MapReduce
Gli script di Pig possono essere eseguiti su EMR creando un cluster con --
applications Name=Pig,Args=--version,<version>, cos:

$ aws emr create-cluster \


--name Pig cluster \
--ami-version <ami version> \
--instance-type <EC2 instance> \
--instance-count <number of nodes> \
--applications Name=Pig,Args=--version,<version>\
--log-uri <S3 bucket> \
--steps Type=PIG,\
Name=Pig script,\
Args=[-f,s3://<script location>,\
-p,input=<input param>,\
-p,output=<output param>]

Questo comando fornisce un nuovo cluster EMR ed esegue s3://<script location>.


Notate che previsto che gli script da eseguire e i percorsi di input (-p input) e
output (-p output) si trovino su S3.
Come alternativa alla creazione di un nuovo cluster EMR, possibile
aggiungere dei passi in Pig a un cluster EMR gi istanziato usando questo
comando:
$ aws emr add-steps \
--cluster-id <cluster id>\
--steps Type=PIG,\
Name= Other Pig script,\
Args=[-f,s3://<script location>,\
-p,input=<input param>,\
-p,output=<output param>]

lID del cluster istanziato.


<cluster id>

Si pu anche eseguire ssh nel nodo master e poi passare alle istruzioni Pig Latin
in una sessione di Grunt con il seguente comando:
$ aws emr ssh --cluster-id <cluster id> --key-pair-file <key pair>
Fondamenti di Apache Pig
Linterfaccia principale per programmare Apache Pig Pig Latin, un linguaggio
procedurale che implementa i concetti del paradigma del flusso di dati.
I programmi Pig Latin sono solitamente organizzati come segue.
Unistruzione LOAD legge i dati da HDFS.
Alcune istruzioni aggregano e manipolano i dati.
Unistruzione STORE scrive loutput sul file system.
In alternativa, unistruzione DUMP visualizza loutput sul terminale.

Lesempio che segue mostra una sequenza di istruzioni che genera in output i
primi 10 hashtag ordinati per frequenza, estratti dal dataset di tweet:
tweets = LOAD tweets.json
USING JsonLoader(created_at:chararray,
id:long,
id_str:chararray,
text:chararray);

hashtags = FOREACH tweets {


GENERATE FLATTEN(
REGEX_EXTRACT(
text,
(?:\\s|\\A|^)[##]+([A-Za-z0-9-_]+), 1)
) as tag;
}

hashtags_grpd = GROUP hashtags BY tag;


hashtags_count = FOREACH hashtags_grpd {
GENERATE
group,
COUNT(hashtags) as occurrencies;
}
hashtags_count_sorted = ORDER hashtags_count BY occurrencies DESC;
top_10_hashtags = LIMIT hashtags_count_sorted 10;
DUMP top_10_hashtags;

Per prima cosa, carichiamo il dataset tweets.json da HDFS, deserializziamo il


file JSON e mappiamolo in uno schema a quattro colonne che contiene lora di
creazione del tweet, il suo ID nella forma numerica e di stringa e il testo. Per ogni
tweet, estraiamo gli hashtag dal testo usando unespressione regolare. Eseguiamo
unaggregazione sullhashtag, contiamo il numero di occorrenze e ordiniamo per
frequenza. Infine, limitiamo i record ordinati ai primi 10 hashtag pi frequenti.
Una serie di istruzioni come questa viene presa dal compilatore di Pig,
trasformata in job di MapReduce ed eseguita su un cluster Hadoop. Il planner e
loptimizer risolveranno le dipendenze sulle relazioni di input e output ed
eseguiranno le istruzioni in parallelo ogni volta che possibile.
Le istruzioni sono i mattoni dellelaborazione dei dati con Pig. Prendono una
relazione come input e ne producono unaltra come output. Nella terminologia di
Pig Latin, una relazione pu essere definita coma una bag di tuple, due tipi di dati
che utilizzeremo nel resto del capitolo.
Gli utenti che gi conoscono SQL e il modello di dati relazionale potrebbero
trovare familiare la sintassi di Pig Latin. In realt, al di l delle sicure
somiglianze, Pig Latin implementa un modello di calcolo completamente diverso.
Pig Latin procedurale, e specifica le trasformazioni effettive dei dati da eseguire,
mentre SQL dichiarativo, e descrive la natura del problema senza specificare la
reale elaborazione in runtime. In termini di organizzazione dei dati, una relazione
paragonabile a una tabella in un database relazionale, mentre le tuple in una bag
corrispondono alle righe in una tabella. Le relazioni non sono ordinate, per cui
facile elaborarle in parallelo, e sono meno vincolate delle tabelle relazionali. Le
relazioni di Pig possono contenere tuple con numeri di campi diversi, e quelle con
lo stesso conteggio dei campi possono avere campi di tipo differente nelle
posizioni corrispondenti.
Una difformit fondamentale tra SQL e il modello del flusso di dati adottato da
Pig Latin sta nel modo in cui questo gestisce gli split della pipeline di dati. Nel
mondo relazionale, un linguaggio dichiarativo come SQL implementa ed esegue
query che generano un singolo risultato. Il modello del flusso di dati vede le
trasformazioni dei dati come un grafico in cui linput e loutput sono nodi collegati
da un operatore. Per esempio, i passi intermedi di una query potrebbero richiedere
che linput sia raggruppato secondo un certo numero di chiavi e che dia pi output
(GROUP BY). Pig ha in s alcuni meccanismi per gestire pi flussi di dati in un grafico
di questo tipo non appena gli input sono resi disponibili, e applica potenzialmente
degli operatori diversi a ciascun flusso. Per esempio, limplementazione di Pig
delloperatore GROUP BY usa la funzione di parallelismo
(http://pig.apache.org/docs/r0.12.0/perf.html#parallel) per consentire allutente di
aumentare il numero delle attivit di reduce per i job di MapReduce generati,
accrescendo cos la simultaneit. Un ulteriore effetto collaterale di questa
propriet che se pi operatori possono essere eseguiti in parallelo nello stesso
programma, Pig lo far (trovate altri dettagli sullimplementazione di pi query in
Pig alla pagina http://pig.apache.org/docs/r0.12.0/perf.html#multi-query-execution).
Unaltra conseguenza dellapproccio di Pig Latin al calcolo che consente la
persistenza dei dati in qualsiasi punto della pipeline. Permette allo sviluppatore di
selezionare implementazioni per operatori specifici e piani di esecuzione quando
necessario, aggirando di fatto loptimizer.
Pig Latin incoraggia gli sviluppatori a inserire il proprio codice pressoch
ovunque in una pipeline attraverso le User Defined Function (UDF), oltre a usare
lo streaming di Hadoop. Le UDF permettono agli utenti di specificare una logica
business personalizzata su come i dati vanno caricati, memorizzati ed elaborati,
mentre lo streaming premette agli utenti di lanciare degli eseguibili in qualsiasi
punto del flusso di dati.
Programmare Pig
Pig Latin ha alcune funzioni interne e alcuni tipi di dati scalari e complessi.
Inoltre, consente lestensione delle funzioni e dei tipi di dati per mezzo di UDF e
dellinvocazione dinamica dei metodi Java.

Tipi di dati di Pig


Pig supporta i seguenti tipi di dati scalari.
int: un integer a 32 bit con segno.
long: un integer a 64 bit con segno.

float: un floating point a 32 bit.

double: un floating point a 64 bit.

chararray: un array di caratteri (stringa) nel formato Unicode UTF-8.

bytearray: un array di byte (blob).

boolean: un booleano.

datetime: un datetime.

biginteger: un BigInteger Java.

bigdecimal: un BigDecimal Java.

Supporta poi i seguenti tipi di dati complessi.


map: un array associativo racchiuso tra [], con la chiave e il valore separati da
# e le varie voci separate da ,.

tuple: una lista ordinata di dati in cui gli elementi possono essere di tipo

scalare o complesso racchiusa tra (), con le varie voci separate da ,.


bag: una collezione non ordinata di tuple racchiusa tra {} con le varie voci

separate da ,.
Di default, Pig tratta i dati come non tipizzati. Lutente pu dichiarare il tipo di
dato al momento del caricamento o eseguire il cast manuale se necessario. Se non
dichiarato alcun tipo di dato, ma se uno script tratta implicitamente un valore
come di un certo tipo, Pig lo dar per buono ed eseguir il cast di conseguenza. Si
pu fare riferimento ai campi di una bag di tuple in base al nome tuple.field o alla
posizione $<index>. Pig conta partendo da 0, quindi il primo elemento sar indicato
come $0.

Funzioni di Pig
Le funzioni interne sono implementate in Java, e cercano di seguire le
convenzioni standard di questo linguaggio. Vanno tuttavia considerate alcune
differenze.
I nomi delle funzioni sono case sensitive e in maiuscolo.
Se il valore risultate null, vuoto o not a number (NaN), Pig restituisce null.
Se Pig non riesce a elaborare lespressione, restituisce uneccezione.
Trovate un elenco di tutte le funzioni interne alla pagina
http://pig.apache.org/docs/r0.12.0/func.html.

Funzioni di caricamento/storage (load/store)


Le funzioni di caricamento/storage determinano come i dati entrano ed escono
da Pig. Le funzioni PigStorage, TextLoader e BinStorage possono essere utilizzate per
leggere e scrivere rispettivamente un testo non strutturato delimitato da UTF-8 e
dei dati binari. Il supporto della compressione determinato dalla funzione di
caricamento/storage. Le funzioni PigStorage e TextLoader supportano le compressioni
gzip e bzip2 sia per la lettura (caricamento) sia per la scrittura (storage). La
funzione BinStorage non supporta la compressione.
Dalla versione 0.12, Pig include un supporto per caricare e salvare i dati Avro e
JSON attraverso AvroStorage (caricamento/storage), JsonStorage (storage) e JsonLoader
(caricamento). Al momento della stesura di questo libro, il supporto di JSON era
ancora limitato. In particolare, Pig si aspetta, per i dati da fornire, uno schema
sotto forma di argomento per JsonLoader/JsonStorage, altrimenti presume che nella
directory che contiene i dati di input sia presente .pig_schema (generato da
JsonStorage). Nella pratica, diventa difficile lavorare con i dump JSON non generati

dallo stesso Pig.


Come mostrato nel prossimo esempio, possiamo caricare il dataset JSON con
JsonLoader:
tweets = LOAD tweets.json USING JsonLoader(
created_at:chararray,
id:long,
id_str:chararray,
text:chararray,
source:chararray);

Forniamo uno schema in modo tale che vengano mappati i primi cinque elementi
di un oggetto JSON, cio created_id, id, id_str, text e source. Guarderemo lo schema
di tweet utilizzando describe tweets, che restituisce quanto segue:
tweets: {created_at: chararray,id: long,id_str: chararray,text: chararray,source: chararray}

Valutazione (eval)
Le funzioni di valutazione implementano una serie di operazioni da applicare su
unespressione che restituisce una bag o una mappa dei tipi di dati. Lespressione
risultante viene valutata entro il contesto della funzione.
AVG(expression): calcola la media dei valori numerici in una bag a colonna
singola.
COUNT(expression) : conta tutti gli elementi con valori non null in prima posizione
in una bag.
COUNT_STAR(expression) : conta tutti gli elementi in una bag.
IsEmpty(expression): controlla se una bag o una mappa vuota.

MAX(expression), MIN(expression) e SUM(expression): restituiscono il valore massimo,

minimo o la somma degli elementi in una bag.


TOKENIZE(expression): scompone una stringa e genera in output una bag of words.

Le funzioni di tupla, bag e mappa


Queste funzioni consentono la conversione da/a tipi bag, tuple e map. Includono
le seguenti.
,
TOTUPLE(expression) TOMAP(expression) e TOBAG(expression): convertono expression in
una tupla, una mappa o una bag.
TOP(n, column, relation): restituisce le tuple top n da una bag di tuple.

Le funzioni math, string e datetime


Pig espone le funzioni java.lang.String, java.util.Date e Joda-Time DateTime

(http://www.joda.org/joda-time/) fornite dalla classe java.lang.Math.

Invocatori dinamici
Gli invocatori dinamici consentono lesecuzione di funzioni Java senza doverle
racchiudere in una UDF. Possono essere utilizzati per qualsiasi funzione statica
che:
non accetta argomenti o accetta una combinazione di string, int, long, double,
float o array con questi stessi tipi;

restituisce un valore string, int, long, double o float.


Per i numeri possono essere utilizzate solo le primitive, e le classi Java boxed
(come Integer) non possono essere impiegate come argomenti. A seconda del tipo
restituito, devessere usato un tipo di invocatore specifico: InvokeForString,
InvokeForInt, InvokeForLong, InvokeForDouble o InvokeForFloat. Ulteriori dettagli sugli

invocatori dinamici si trovano alla pagina


http://pig.apache.org/docs/r0.12.0/func.html#dynamic-invokers.

Macro
Dalla versione 0.9, il preprocessore di Pig Latin supporta lespansione delle
macro. Queste sono definite usando listruzione DEFINE:
DEFINE macro_name(param1, ..., paramN) RETURNS output_bag {
pig_latin_statements
};

La macro viene espansa inline, e i suoi parametri vengono referenziati nel


blocco di Pig Latin racchiuso tra { }.
La relazione in output della macro data nelle istruzioni RETURNS (output_bag).
RETURNS void usata per una macro senza relazione in output.

Possiamo definire un macro perch conti il numero di righe in una relazione,


cos:
DEFINE count_rows(X) RETURNS cnt {
grpd = group $X all;
$cnt = foreach grpd generate COUNT($X);
};

Possiamo utilizzarla in uno script Pig o in una sessione di Grunt per contare il
numero di tweet:
tweets_count = count_rows(tweets);
DUMP tweets_count;

Le macro ci permettono di creare script modulari incorporando il codice in file


separati, importandoli quando necessario. Per esempio, possiamo salvare
count_rows in un file chiamato count_rows.macro e in seguito importarlo con il comando

import count_rows.macro.

Le macro hanno alcuni limiti; in particolare, al loro interno sono ammesse solo
istruzioni Pig Latin. Non possibile usare istruzioni REGISTER e comandi della shell,
non sono ammesse le UDF e non supportata la sostituzione allinterno della
macro.

Lavorare con i dati


Pig Latin fornisce alcuni operatori relazionali per combinare le funzioni e
applicare le trasformazioni ai dati. Operazioni tipiche in una pipeline di dati sono
il filtro delle relazioni (FILTER), laggregazione degli input in base a delle chiavi
(GROUP), la generazione di trasformazioni basate su colonne di dati (FOREACH) e il join
delle relazioni (JOIN) secondo delle chiavi condivise.
Nei prossimi capitoli, illustreremo lazione di questi operatori su un dataset di
tweet generato caricando dati JSON.

Filtro
Loperatore FILTER seleziona le tuple da una relazione basata su unespressione,
come segue:
relation = FILTER relation BY expression;

Possiamo utilizzarlo per filtrare i tweet il cui testo coincide con lespressione
regolare dellhashtag, cos:
tweets_with_tag = FILTER tweets BY
(text
MATCHES (?:\\s|\\A|^)[##]+([A-Za-z0-9-_]+)
);

Aggregazione
Loperatore GROUP raggruppa i dati in una o pi relazioni basate su unespressione
o una chiave:
relation = GROUP relation BY expression;

Possiamo raggruppare i tweet in base al campo source in una nuova relazione


grpd, cos:

grpd = GROUP tweets BY source;

possibile aggregare secondo pi dimensioni specificando una tupla come


chiave:
grpd = GROUP tweets BY (created_at, source);

Il risultato di unoperazione GROUP una relazione che include una tupla per ogni
valore univoco dellespressione di gruppo. Questa tupla contiene due campi: il
primo si chiama group ed dello stesso tipo della chiave del gruppo; il secondo
campo prende il nome della relazione originale ed di tipo bag. I nomi di
entrambi i campi sono generati dal sistema.
Utilizzando la parola chiave ALL, Pig eseguir laggregazione sullintera
relazione. Lo schema GROUP tweets ALL aggregher tutte le tuple nello stesso gruppo.
Come gi detto, Pig consente la gestione esplicita del livello di simultaneit
delloperatore GROUP usando loperatore PARALLEL:
grpd = GROUP tweets BY (created_at, id) PARALLEL 10;

Nellesempio precedente, il job di MapReduce generato dal compilatore


eseguir contemporaneamente 10 attivit di reduce; Pig compie una stima euristica
di quanti reducer utilizzare. Un altro modo per applicare globalmente il numero di
attivit reduce quello di ricorrere al comando set default_parallel <n>.

Foreach
Loperatore FOREACH applica le funzioni alle colonne, come segue:
relation = FOREACH relation GENERATE transformation;

Loutput di FOREACH dipende dalla trasformazione applicata. Possiamo utilizzarlo


per mostrare il testo di tutti i tweet che contengono un hashtag:
t = FOREACH tweets_with_tag GENERATE text;

anche possibile applicare una funzione alle colonne mostrate. Per esempio,
possiamo usare la funzione REGEX_TOKENIZE per scomporre ogni tweet in parole, cos:
t = FOREACH tweets_with_tag GENERATE FLATTEN(TOKENIZE(text)) as word;
Il modificatore FLATTEN scompone ulteriormente la bag generata da TOKENIZE in una
tupla di parole.

Join
Loperatore JOIN esegue un inner join di due o pi relazioni in base a dei valori
di campo comuni. La sua sintassi la seguente:
relation = JOIN relation1 BY expression1, relation2 BY expression2;

Possiamo utilizzarlo per individuare i tweet che contengono parole positive,


come segue:
positive = LOAD positive-words.txt USING PigStorage() as (w:chararray);

Filtriamo i commenti:
positive_words = FILTER positive BY NOT w MATCHES ^;.*;

positive_words una bag di tuple, ciascuna delle quali contiene una parola.
Possiamo scomporre il testo dei tweet in token e creare una nuova bag di tuple
(id_str, word) cos:
id_words = FOREACH tweets {
GENERATE
id_str,
FLATTEN(TOKENIZE(text)) as word;
}

Uniamo le due relazioni sul campo word e otterremo una relazione di tutti i tweet
che contengono una o pi parole positive:
positive_tweets = JOIN positive_words BY w, id_words BY word;

In questa istruzione, uniamo positive_words e id_words in base alla condizione


secondo cui id_words.word una parola positiva. Loperatore positive_tweets una bag
nella forma {w:chararray,id_str:chararray, word:chararray} che racchiude tutti gli
elementi positive_words e id_words che soddisfano la condizione di join.
Possiamo combinare gli operatori GROUP e FOREACH per calcolare il numero di
parole positive per tweet (con almeno una parola positiva). Come prima cosa,
raggruppiamo la relazione dei tweet positivi secondo lID del tweet, quindi
contiamo il numero delle occorrenze di ciascun ID nella relazione:
grpd = GROUP positive_tweets BY id_str;
score = FOREACH grpd GENERATE FLATTEN(group), COUNT(positive_tweets);

Loperatore JOIN pu anche sfruttare il parallelismo:


positive_tweets = JOIN positive_words BY w, id_words BY word PARALLEL 10
Questo comando esegue il join con 10 attivit del reducer.
possibile specificare il comportamento delloperatore usando la parola
chiave USING seguita dallID di un join specializzato. Trovate altri dettagli alla
pagina http://pig.apache.org/docs/r0.12.0/perf.html#specialized-joins.
Estendere Pig (UDF)
Le funzioni possono essere parte di qualsiasi operatore in Pig. Ci sono due
differenze principali tra le UDF e le funzioni interne. Come prima cosa, le UDF
vanno registrate usando la parola chiave REGISTER per poter essere disponibili per
Pig. In secondo luogo, devono essere qualificate quando vengono usate.
Attualmente le UDF di Pig possono essere implementate in Java, Python, Ruby,
JavaScript e Groovy. Il supporto pi esteso si ha per le funzioni Java, che
consentono di personalizzare tutte le parti del processo, compresi
caricamento/storage dei dati, trasformazione e aggregazione. Inoltre, le funzioni
Java sono pi efficienti, perch vengono implementate nello stesso linguaggio di
Pig e perch sono supportate alcune interfacce aggiuntive (per esempio Algebraic
e Accumulator). vero daltra parte che le API Ruby e Python consentono una
prototipazione pi rapida.
Lintegrazione delle UDF con lambiente Pig gestita in gran parte dalle
istruzioni REGISTER e DEFINE.
REGISTER registra un file JAR in modo che possano essere utilizzate le UDF nel
file, cos:
REGISTER piggybank.jar

DEFINE crea un alias a una funzione o a un comando di streaming:


DEFINE MyFunction my.package.uri.MyFunction

La versione 0.12 di Pig ha introdotto lo streaming delle UDF come meccanismo


per scrivere funzioni utilizzando linguaggi senza implementazione di una JVM.

Repository di UDF
La base di codice di Pig ospita un repository di UDF chiamato Piggybank. Altri
repository popolari sono Elephant Bird di Twitter
(https://github.com/kevinweil/elephant-bird/) e Apache DataFu
(http://datafu.incubator.apache.org/).

Piggybank
Piggybank un luogo dove gli utenti di Pig condividono le loro funzioni. Il
codice condiviso si trova nel repository ufficiale Subversion alla pagina
http://bit.ly/1AA0QTV. La documentazione delle API recuperabile allindirizzo

http://pig.apache.org/docs/r0.12.0/api/ nella sezione contrib. Le UDF di Piggybank

possono essere ottenute verificando e compilando i sorgenti dal repository


Subversion o utilizzando il file JAR contenuto nelle release binarie di Pig. In
Cloudera CDH, piggybank.jar disponibile alla pagina
/opt/cloudera/parcels/CDH/lib/pig/piggybank.jar.

Elephant Bird
Elephant Bird una libreria open source di Twitter di tutto quanto ha usato
Hadoop in produzione. Contiene diversi strumenti di serializzazione, formati
personalizzati di input e output, scrivibili, funzioni di caricamento/storage di Pig e
varie.
Elephant Bird dotata di una funzione loader JSON estremamente flessibile,
che al momento della stesura di queste righe era la risorsa privilegiata per la
manipolazione dei dati JSON in Pig.

Apache DataFu
Apache DataFu Pig raccoglie alcune funzioni di analisi sviluppate da LinkedIn.
Include funzioni statistiche e di stima, operazioni di bag e set, funzioni di
campionamento, hash e analisi dei link.
Analizzare lo stream di Twitter
Nei prossimi esempi utilizzeremo limplementazione del JsonLoader fornita da
Elephant Bird per caricare e manipolare dati JSON. Ci serviremo di Pig per
esplorare i metadati dei tweet e per analizzare le tendenze nel dataset. Infine,
modelleremo linterazione tra gli utenti attraverso un grafico e useremo Apache
DataFu per analizzare questo social network.

Prerequisiti
Scaricate i file JAR elephant-bird-pig (http://bit.ly/1CoSAL5), elephant-bird-hadoop-
compat (http://bit.ly/1NzA5VZ) ed elephant-bird-core (http://bit.ly/1Dniml6) dal repository

centrale Maven e copiateli in HDFS usando il seguente comando:


$ hdfs dfs -put target/elephant-bird-pig-4.5.jar hdfs:///jar/
$ hdfs dfs put target/elephant-bird-hadoop-compat-4.5.jar hdfs:///jar/
$ hdfs dfs put elephant-bird-core-4.5.jar hdfs:///jar/

Esplorazione del dataset


Prima di tuffarci nel dataset, dobbiamo registrare le dipendenze in Elephant
Bird e DataFu:
REGISTER /opt/cloudera/parcels/CDH/lib/pig/datafu-1.1.0-cdh5.0.0.jar
REGISTER /opt/cloudera/parcels/CDH/lib/pig/lib/json-simple-1.1.jar
REGISTER hdfs:///jar/elephant-bird-pig-4.5.jar
REGISTER hdfs:///jar/elephant-bird-hadoop-compat-4.5.jar
REGISTER hdfs:///jar/elephant-bird-core-4.5.jar

Carichiamo poi il dataset JSON di tweet con


com.twitter.elephantbird.pig.load.JsonLoader:

tweets = LOAD tweets.json using com.twitter.elephantbird.pig.load.


JsonLoader(-nestedLoad);

decodifica ogni riga del file di input in


com.twitter.elephantbird.pig.load.JsonLoader

JSON e passa a Pig la mappa di valori risultante come una tupla costituita da un
unico elemento. Questo consente laccesso agli elementi delloggetto JSON senza
dover specificare preventivamente uno schema. Largomento nestedLoad istruisce la
classe affinch carichi strutture di dati annidate.

Metadati dei tweet


Nel resto del capitolo utilizzeremo i metadati dal dataset JSON per modellare
lo stream dei tweet. Un esempio di metadato collegato a un tweet loggetto Place,
che contiene le informazioni geografiche riguardo la posizione dellutente. Place
contiene dei campi che descrivono il nome dellutente, il suo ID, la nazione e altro
ancora. Trovate una descrizione completa alla pagina
https://dev.twitter.com/docs/platform-objects/places:

place = FOREACH tweets GENERATE (chararray)$0#place as place;

Le entit danno informazioni quali i tweet dai dati strutturati, gli URL, gli
hashtag e le menzioni senza doverle estrarre dal testo. Allindirizzo
https://dev.twitter.com/docs/entities trovate una loro descrizione. Lentit hashtag un

array di tag estratti da un tweet. Ogni entit ha due attributi:


Text: il testo dellhashtag.
Indices: la posizione del carattere da cui lhashtag stato estratto.
Il codice seguente usa le entit:
hashtags_bag = FOREACH tweets {
GENERATE
FLATTEN($0#entities#hashtags) as tag;
}

Usiamo poi hashtags_bag per estrarre il testo di ciascun hashtag:


hashtags = FOREACH hashtags_bag GENERATE tag#text as topic;

Le entit per gli oggetti user contengono le informazioni che appaiono nel profilo
dellutente e nei campi della descrizione. Possiamo estrarre gli ID dellautore del
tweet attraverso il campo user nella mappa del tweet:
users = FOREACH tweets GENERATE $0#user#id as id;

Preparazione dei dati


Loperatore interno SAMPLE seleziona dal dataset un insieme di n tuple con
probabilit p, come segue:
sampled = SAMPLE tweets 0.01;

Questo comando seleziona approssimativamente l1 percento del dataset.


Considerato che SAMPLE un operatore di probabilit
(http://en.wikipedia.org/wiki/Bernoulli_sampling), non c la certezza che le dimensioni
del campione siano esatte. Inoltre la funzione campiona utilizzando la sostituzione,
e quindi ogni elemento potrebbe apparire pi di una volta.
Apache DataFu implementa alcuni metodi di campionamento per i casi in cui si
dispone della dimensione esatta del campione e non si desidera la sostituzione
(SimpleRandomSampling), per quando si vuole campionare tramite sostituzione
(SimpleRandomSampleWithReplacementVote e SimpleRandomSampleWithReplacementElect) e per
quando si desidera eseguire il campionamento su base casuale
(WeightedRandomSampling) o tra pi relazioni (SampleByKey).
Possiamo creare un campione dell1 percento esatto del dataset, in cui ogni
elemento ha la stessa probabilit di essere selezionato, usando SimpleRandomSample.
NOTA
La garanzia effettiva data un campione di dimensione ceil (p*n) con una probabilit di almeno
il 99 percento.

Passiamo innanzitutto una probabilit di 0.01 al costruttore dellUDF:


DEFINE SRS datafu.pig.sampling.SimpleRandomSample(0.01);

e poi la bag da campionare creata con (GROUP tweets ALL) :


sampled = FOREACH (GROUP tweets ALL) GENERATE FLATTEN(SRS(tweets));

LUDF SimpleRandomSample seleziona senza sostituire, quindi ogni elemento


apparir solo una volta.
NOTA
Il metodo di campionamento da utilizzare dipende dai dati su cui si lavora, dai presupposti su
come gli elementi sono distribuiti, dalla dimensione del dataset e da quello che vogliamo
effettivamente ottenere. In generale, se intendiamo esplorare un dataset per formulare delle
ipotesi, SimpleRandomSample pu essere una buona scelta. Tuttavia, in molte applicazioni di
analisi pi comune impiegare metodi che presumono la sostituzione (per esempio il
bootstrapping). Quando si lavora con dataset molto grandi, i campionamenti con sostituzione e
senza sostituzione tendono a comportarsi in modo simile. La probabilit che un elemento
venga selezionato due volte su una base di miliardi di elementi molto bassa.

Statistiche top n
Una delle prime domande che potremmo porre riguarda la frequenza di
determinati elementi. Per esempio, potremmo voler creare un istogramma dei
primi 10 topic in base al numero di menzioni. Oppure potremmo voler trovare le
prime 50 nazioni o i primi 10 utenti. Prima di considerare i dati dei tweet,
definiremo una macro che ci permetter di applicare la stessa logica di selezione a
collezioni di elementi diverse:
DEFINE top_n(rel, col, n)
RETURNS top_n_items {
grpd = GROUP $rel BY $col;
cnt_items = FOREACH grpd
GENERATE FLATTEN(group), COUNT($rel) AS cnt;
cnt_items_sorted = ORDER cnt_items BY cnt DESC;
$top_n_items = LIMIT cnt_items_sorted $n;
}

Il metodo top_n prende una relazione rel, la colonna col su cui vogliamo
effettuare il conteggio e il numero di elementi n da restituire come parametri. Nel
blocco di Pig Latin, raggruppiamo prima rel per gli elementi in col, contiamo il
numero delle occorrenze di ciascun elemento, ordiniamole e selezioniamo ln pi
frequente.
Per trovare i primi 10 hashtag in inglese, filtriamo per lingua ed estraiamo il
testo:
tweets_en = FILTER tweets by $0#lang == en;
hashtags_bag = FOREACH tweets {
GENERATE
FLATTEN($0#entities#hashtags) AS tag;
}
hashtags = FOREACH hashtags_bag GENERATE tag#text AS tag;

Applichiamo poi la macro top_n:


top_10_hashtags = top_n(hashtags, tag, 10);

Per meglio caratterizzare cosa fa tendenza e rendere questa informazione pi


significativa per gli utenti, possiamo scavare nel dataset e cercare gli hashtag per
posizione geografica.
Generiamo innanzitutto una bag di tuple (place, hashtag):
hashtags_country_bag = FOREACH tweets generate {
0#place as place,
FLATTEN($0#entities#hashtags) as tag;
}

Estraiamo quindi il codice della nazione e il testo dellhashtag:


hashtags_country = FOREACH hashtags_country_bag {
GENERATE
place#country_code as co,
tag#text as tag;
}

Contiamo ora quante volte ogni codice della nazione e hashtag appaiono
insieme:
hashtags_country_frequency = FOREACH (GROUP hashtags_country ALL) {
GENERATE
FLATTEN(group),
COUNT(hashtags_country) as count;
}
Infine, contiamo le prime 10 nazioni in base allhashtag con la funzione TOP,
come segue:
hashtags_country_regrouped= GROUP hashtags_country_frequency BY cnt;
top_results = FOREACH hashtags_country_regrouped {
result = TOP(10, 1, hashtags_country_frequency);
GENERATE FLATTEN(result);
}

I parametri di TOP sono il numero di tuple da restituire, la colonna da confrontare


e la relazione che contiene tale colonna:
top_results = FOREACH D {
result = TOP(10, 1, C);
GENERATE FLATTEN(result);
}

Trovate il codice sorgente per questo esempio alla pagina http://bit.ly/1HZhy3X.

Manipolazione di datetime
Il campo created_at nei tweet JSON ci fornisce alcune informazioni temporali su
quando il tweet stato postato. Sfortunatamente, il suo formato non compatibile
con il tipo datetime interno di Pig.
Piggybank viene in nostro aiuto con alcune UDF per la manipolazione del
tempo, contenute in org.apache.pig.piggybank.evaluation.datetime.convert. Una di queste
CustomFormatToISO, che converte un timestamp formattato in modo arbitrario in una

stringa datetime ISO 8601.


Per poter accedere a queste UDF, dobbiamo prima registrare il file piggybank.jar,
come segue:
REGISTER /opt/cloudera/parcels/CDH/lib/pig/piggybank.jar

Per semplificare un po il codice, possiamo creare un alias per il nome


pienamente qualificato della classe CustomFormatToISO:
DEFINE CustomFormatToISO
org.apache.pig.piggybank.evaluation.datetime.convert.CustomFormatToISO();

Sapendo come manipolare i timestamp, possiamo calcolare le statistiche in


determinati intervalli di tempo. Per esempio, possiamo vedere quanti tweet
vengono creati ogni ora. Pig prevede una funzione GetHour che estrae lora da un
tipo datetime. Per utilizzarla, convertiamo prima la stringa del timestamp in ISO
8601 con CustomFormatToISO e poi convertiamo il risultante chararray in datetime
attraverso la funzione interna ToDate:
hourly_tweets = FOREACH tweets {
GENERATE
GetHour(
ToDate(
CustomFormatToISO(
$0#created_at, EEE MMMM d HH:mm:ss Z y)
)
) as hour;
}

Adesso non resta che raggruppare gli hourly_tweets per ora e generare un
conteggio dei tweet per gruppo, in questo modo:
hourly_tweets_count = FOREACH (GROUP hourly_tweets BY hour) {
GENERATE FLATTEN(group), COUNT(hourly_tweets);
}

Sessioni
La classe Sessionize di DataFu pu aiutarci a meglio catturare lattivit di un
utente nel tempo. Una sessione rappresenta lattivit di un utente in un dato
periodo. Per esempio, possiamo guardare lo stream di tweet di ogni utente a
intervalli di 15 minuti e misurare queste sessioni per determinare sia il volume di
rete sia lattivit dellutente:
DEFINE Sessionize datafu.pig.sessions.Sessionize(15m);
users_activity = FOREACH tweets {
GENERATE
CustomFormatToISO($0#created_at,
EEE MMMM d HH:mm:ss Z y) AS dt,
(chararray)$0#user#id as user_id;
}
users_activity_sessionized = FOREACH
(GROUP users_activity BY user_id) {
ordered = ORDER users_activity BY dt;
GENERATE FLATTEN(Sessionize(ordered))
AS (dt, user_id, session_id);
}

user_activityregistra semplicemente lora dt in cui un dato user_id ha postato un


aggiornamento dello stato.
Sessionize prende la sospensione (timeout) della sessione e una bag come input.

Il primo elemento della bag di input un timestamp ISO 8601, e la bag deve essere
ordinata secondo questo timestamp. Gli eventi che avvengono entro 15 minuti
luno dallaltro rientreranno nella stessa sessione.
La bag di input viene restituita con un nuovo campo, session_id, che identifica in
modo univoco una sessione. Con questi dati, possiamo calcolare la lunghezza della
sessione e altre statistiche. Trovate altri esempi delluso di Sessionize alla pagina
.
http://datafu.incubator.apache.org/docs/datafu/guide/sessions.html

Catturare le interazioni dellutente


Nel resto del capitolo vedremo come identificare alcuni pattern dalle interazioni
degli utenti. Come primo passo in questa direzione, creeremo un dataset adatto per
modellare un social network. Il dataset conterr un timestamp, lID del tweet,
lutente che ha postato il tweet, lutente e il tweet a cui risponde e lhashtag nel
tweet.
Twitter considera come una risposta a quella data persona
(in_reply_to_status_id_str) qualsiasi messaggio che inizia con il carattere @.
Collocando un carattere @ altrove nel tweet, questo viene interpretato come
menzione (entities#user_mentions) e non come risposta. La differenza che le
menzioni sono trasmesse immediatamente ai follower di una persona, mentre le
risposte no. Tuttavia, le risposte sono considerate come menzioni.
Quando si lavora con informazioni legate a una persona identificabile, buona
norma renderle anonime, se non addirittura rimuovere del tutto dati sensibili come
indirizzi IP, nomi e ID utenti. Una tecnica usata comunemente implica luso di una
funzione hash che prende come input i dati che vogliamo rendere anonimi,
concatenati con dati aggiuntivi casuali chiamati salt. Il codice seguente mostra un
esempio:
DEFINE SHA datafu.pig.hash.SHA();
from_to_bag = FOREACH tweets {
dt = $0#created_at;
user_id = (chararray)$0#user#id;
tweet_id = (chararray)$0#id_str;
reply_to_tweet = (chararray)$0#in_reply_to_status_id_str;
reply_to = (chararray)$0#in_reply_to_user_id_str;
place = $0#place;
topics = $0#entities#hashtags;

GENERATE
CustomFormatToISO(dt, EEE MMMM d HH:mm:ss Z y) AS dt,
SHA((chararray)CONCAT(SALT, user_id)) AS source,
SHA(((chararray)CONCAT(SALT, tweet_id))) AS tweet_id,
((reply_to_tweet IS NULL)
? NULL
: SHA((chararray)CONCAT(SALT, reply_to_tweet)))
AS reply_to_tweet_id,
((reply_to IS NULL)
? NULL
: SHA((chararray)CONCAT(SALT, reply_to)))
AS destination,
(chararray)place#country_code as country,
FLATTEN(topics) AS topic;
}

-- extract the hashtag text


from_to = FOREACH from_to_bag {
GENERATE
dt,
tweet_id,
reply_to_tweet_id,
source,
destination,
country,
(chararray)topic#text AS topic;
}

Qui usiamo CONCAT per associare una stringa salt (non cos casuale) ai dati
personali. A seguire, generiamo lhash degli ID con salt con la funzione SHA di
DataFu. Questa richiede che i suoi parametri di input non siano null. Applichiamo
questa condizione utilizzando delle istruzioni if-then-else. In Pig Latin, lo
esprimiamo come <condition is true> ? <true branch> : <false branch> . Se la stringa
null, restituiamo NULL, altrimenti restituiamo un hash con salt. Per rendere il codice
pi leggibile, utilizziamo degli alias per i campi JSON del tweet e referenziamoli
nel blocco GENERATE.

Analisi dei link


Possiamo ridefinire il nostro approccio alla determinazione dei trending topic in
modo da includere le reazioni degli utenti. Un primo sistema, un po naif, potrebbe
essere quello di considerare un topic importante se ha comportato un numero di
risposte superiore a quello di un valore soglia.
Un problema con questo approccio che i tweet generano solitamente poche
risposte, quindi il volume del dataset risultante sar basso. Serve allora un grossa
quantit di dati per contenere i tweet a cui viene data risposta e per produrre un
risultato. Nella pratica, potremmo voler combinare questa metrica con altre (per
esempio con le menzioni) per poter eseguire analisi pi significative.
Per soddisfare questa query, creeremo un nuovo dataset che include gli hashtag
estratti sia dal tweet sia da quello a cui sta rispondendo un utente:
tweet_hashtag = FOREACH from_to GENERATE tweet_id, topic;
from_to_self_joined = JOIN from_to BY reply_to_tweet_id LEFT,
tweet_hashtag BY tweet_id;

twitter_graph = FOREACH from_to_self_joined {


GENERATE
from_to::dt AS dt,
from_to::tweet_id AS tweet_id,
from_to::reply_to_tweet_id AS reply_to_tweet_id,
from_to::source AS source,
from_to::destination AS destination,
from_to::topic AS topic,
from_to::country AS country,
tweet_hashtag::topic AS topic_replied;
}

Notate che Pig non ammette un cross join sulla stessa relazione, quindi
dobbiamo creare un tweet_hashtag per il lato destro del join. Qui usiamo loperatore
:: per evitare equivoci sulla relazione e la colonna di cui vogliamo selezionare i

record.
Ancora una volta possiamo cercare i primi 10 topic in base al numero delle
risposte usando la macro top_n:
top_10_topics = top_n(twitter_graph, topic_replied, 10);

Con DataFu possiamo calcolare statistiche pi descrittive su questo dataset.


Usando la funzione Quantile, possiamo calcolare la mediana, il 90mo, 95mo e il
99mo percentile del numero di reazioni degli hashtag, cos:
DEFINE Quantile datafu.pig.stats.Quantile(0.5,0.90,0.95,0.99);

Poich lUDF si aspetta come input una bag ordinata di valori integer, contiamo
prima la frequenza di ogni elemento topic_replied:
topics_with_replies_grpd = GROUP twitter_graph BY topic_replied;
topics_with_replies_cnt = FOREACH topics_with_replies_grpd {
GENERATE
COUNT(twitter_graph) as cnt;
}

Applichiamo poi Quantile sulla bag di frequenze, come segue:


quantiles = FOREACH (GROUP topics_with_replies_cnt ALL) {
sorted = ORDER topics_with_replies_cnt BY cnt;
GENERATE Quantile(sorted);
}

Trovate il codice sorgente di questo esempio alla pagina http://bit.ly/19F8uoN.

Utenti influenti
Utilizzeremo ora PageRank un algoritmo sviluppato da Google per
classificare le pagine web (http://ilpubs.stanford.edu:8090/422/1/1999-66.pdf) per
identificare gli utenti influenti nel grafico di Twitter che abbiamo generato nel
paragrafo precedente.
Questo tipo di analisi meglio si adatta ad alcuni casi, come il targeted e il
contextual advertisement, i sistemi di raccomandazione, lindividuazione dello
spam e nella misurazione dellimportanza delle pagine web. Un approccio
analogo, utilizzato da Twitter per implementare la funzione Chi seguire, descritto
nel documento WTF: The Who to Follow service at Twitter found at
(http://stanford.edu/~rezab/papers/wtf_overview.pdf).
PageRank determina il valore di una pagina in base allimportanza di altre
pagine collegate a essa e le assegna un punteggio tra 0 e 1. Un punteggio PageRank
elevato indica che molte pagine puntano a questa. Essere collegati a pagine con un
PageRank alto ovviamente unattestazione di qualit. Nei termini del grafico di
Twitter, si presume che gli utenti che ricevono molte risposte siano importanti o
influenti nel social network. Nel caso di Twitter, consideriamo una definizione
estesa di PageRank, in cui il link tra due utenti dato da una risposta diretta ed
etichettata da un hashtag presente nel messaggio. Qui vogliamo identificare gli
utenti influenti rispetto a un dato topic.
Nellimplementazione di DataFu, ogni grafico rappresentato come una bag di
tuple (source, edges). La tupla source un ID integer che rappresenta il nodo
sorgente. Gli archi sono una bag di tuple (destination, weight). destination un ID
integer che rappresenta il nodo di destinazione e weight un double che rappresenta
il peso che deve avere larco. Loutput dellUDF una bag di coppie (source, rank),
dove rank il valore PageRank dellutente sorgente nel grafico. Qui abbiamo
parlato di nodi, archi e grafici in termini astratti. Nel caso di Google i nodi sono
pagine web, gli archi sono link da una pagina allaltra e i grafici sono gruppi di
pagine connesse in modo diretto e indiretto.
Nel nostro caso, i nodi rappresentano gli utenti e gli archi le menzioni
in_reply_to_user_id_str; gli archi sono etichettati dagli hashtag nei tweet. Loutput di

PageRank dovrebbe suggerirci quali utenti sono influenti rispetto a un dato topic
considerati i loro pattern di interazione.
In questo paragrafo scriveremo una pipeline per:
rappresentare i dati come un grafico in cui ogni nodo un utente e gli archi
sono etichettati da hashtag;
mappare gli ID e gli hashtag in integer cos che possano essere elaborati da
PageRank;
applicare PageRank;
conservare i risultati in HDFS in un formato interoperabile (Avro).
Rappresentiamo il grafico come bag di tuple nella forma (source, destination,
topic), dove ogni tupla rappresenta linterazione tra i nodi. Trovate il codice

sorgente di questo esempio alla pagina http://bit.ly/19z3hQd.


Mapperemo il testo degli utenti e degli hashtag in ID numerici. Utilizziamo il
metodo Java String hashCode() per effettuare questa conversione e incorporare la
logica in unUDF Eval.
NOTA
La dimensione di un integer costituisce il limite superiore per il numero di nodi e di archi nel
grafico. Nel codice di produzione, si consiglia di usare una funzione hash pi robusta.

La classe StringToInt prende una stringa come input, chiama il metodo hashCode() e
restituisce loutput del metodo a Pig. Il codice dellUDF si trova alla pagina
http://bit.ly/1O5CaMm:

package com.learninghadoop2.pig.udf;
import java.io.IOException;
import org.apache.pig.EvalFunc;
import org.apache.pig.data.Tuple;

public class StringToInt extends EvalFunc<Integer> {


public Integer exec(Tuple input) throws IOException {
if (input == null || input.size() == 0)
return null;
try {
String str = (String) input.get(0);
return str.hashCode();
} catch(Exception e) {
throw
new IOException(Cannot convert String to Int, e);
}
}
}

Estendiamo org.apache.pig.EvalFunc ed effettuiamo loverride del metodo exec per


restituire str.hashCode() sullinput della funzione. La classe EvalFunc<Integer> viene
parametrizzata con il tipo di ritorno dellUDF (Integer).
A seguire, compiliamo la classe e archiviamola in un JAR:
$ javac -classpath /opt/cloudera/parcels/CDH/lib/pig/pig.jar:$(hadoop classpath)
com/learninghadoop2/pig/udf/StringToInt.java
$ jar cvf myudfs-pig.jar com/learninghadoop2/pig/udf/StringToInt.class

Ora possiamo registrare lUDF in Pig e creare un alias per StringToInt, come
segue:
REGISTER myudfs-pig.jar
DEFINE StringToInt com.learninghadoop2.pig.udf.StringToInt();
Filtriamo poi i tweet senza destination n topic:
tweets_graph_filtered = FILTER twitter_graph by
(destination IS NOT NULL) AND
(topic IS NOT null);

Convertiamo quindi source, destination e topic in ID integer:


from_to = foreach tweets_graph_filtered {
GENERATE
StringToInt(source) as source_id,
StringToInt(destination) as destination_id,
StringToInt(topic) as topic_id;
}

Una volta che i dati sono nel formato opportuno, possiamo riutilizzare
limplementazione di PageRank e il codice di esempio (che trovate alle pagina
http://bit.ly/1CskfuG) fornito da DataFu, come mostrato di seguito:

DEFINE PageRank datafu.pig.linkanalysis.PageRank(dangling_nodes,true);

Iniziamo creando una bag di tuple (source_id, :


destination_id, topic_id)

reply_to = group from_to by (source_id, destination_id, topic_id);

Contiamo le occorrenze di ogni tupla, ossia quante volte due persone hanno
discusso di un certo topic:
topic_edges = foreach reply_to {
GENERATE flatten(group), ((double)COUNT(from_to.topic_id)) as w;
}

Ricordate che il topic larco del grafico; partiamo creando unassociazione tra
il nodo sorgente e il topic sorgente, cos:
topic_edges_grouped = GROUP topic_edges by (topic_id, source_id);

Poi raggruppiamolo allo scopo di aggiungere un nodo di destinazione e il peso


dellarco:
topic_edges_grouped = FOREACH topic_edges_grouped {
GENERATE
group.topic_id as topic,
group.source_id as source,
topic_edges.(destination_id,w) as edges;
}

Una volta creato il grafico di Twitter, calcoliamo il PageRank di tutti gli utenti
(source_id):
topic_rank = FOREACH (GROUP topic_edges_grouped BY topic) {
GENERATE
group as topic,
FLATTEN(PageRank(topic_edges_grouped.(source,edges))) as (source,rank);
}
topic_rank = FOREACH topic_rank GENERATE topic, source, rank;

Salviamo il risultato in HDFS nel formato Avro. Se nel percorso delle classi
non ci sono dipendenze Avro, dovremo aggiungere il file JAR MapReduce di Avro
al nostro ambiente prima di accedere ai singoli campi. In Pig, per esempio,
aggiungiamo quanto segue sulla Cloudera CDH5 VM:
REGISTER /opt/cloudera/parcels/CDH/lib/avro/avro.jar
REGISTER /opt/cloudera/parcels/CDH/lib/avro/avro-mapred-hadoop2.jar
STORE topic_rank INTO replies-pagerank using AvroStorage();

NOTA
Negli ultimi paragrafi abbiamo dato per scontate un paio di cose sullaspetto che potrebbe avere
un grafico di Twitter e sul significato dei concetti di topic e interazione dellutente. Considerati i
vincoli posti, il social network risultante che abbiamo analizzato sar relativamente piccolo e
non necessariamente rappresentativo dellintero Twitter. Lestrapolazione dei risultati da questo
dataset scoraggiata. Nella pratica, ci sono molti altri fattori di cui tener conto quando si deve
generare un modello robusto di interazione social.
Riepilogo
In questo capitolo abbiamo introdotto Apache Pig, una piattaforma per lanalisi
su larga scala su Hadoop. In particolare, abbiamo trattato quanto segue.
Gli obiettivi di Pig come sistema per fornire unastrazione a flusso di dati che
non richiede uno sviluppo effettivo su MapReduce.
Un confronto tra lapproccio di Pig e di SQL allelaborazione dei dati, dove
Pig un linguaggio procedurale e SQL dichiarativo.
Iniziare a lavorare con Pig: unattivit semplice come una libreria che genera
del codice personalizzato e non richiede servizi aggiuntivi.
Una panoramica dei tipi di dati, delle funzioni principali e dei meccanismi di
estensione forniti da Pig.
Alcuni esempi di applicazione di Pig allanalisi dettagliata del dataset di
Twitter, che dimostrano la sua capacit di esprimere concetti complessi in
una modalit concisa.
Le librerie quali Piggybank, Elephant Bird e DataFu che forniscono dei
repository per numerose funzioni utili e gi pronte di Pig.
Nel prossimo capitolo torneremo sul confronto con SQL esplorando alcuni
strumenti che espongono unastrazione di tipo SQL sui dati conservati in HDFS.
Capitolo 7
Hadoop e SQL

MapReduce un paradigma potente che consente quellelaborazione dei dati


complessi che pu svelare una serie di indizi importanti. Tuttavia, come visto nei
capitoli precedenti, richiede un approccio mentale diverso e un po di formazione
ed esperienza nella scomposizione analitica del processo in una serie di passi di
map e reduce. Esistono diversi prodotti costruiti su Hadoop che forniscono viste di

livello pi elevato (o pi familiari) dei dati conservati in HDFS, e Pig uno dei
pi diffusi. In questo capitolo esploreremo laltra astrazione pi comune
implementata su Hadoop: SQL. In particolare, vedremo quanto segue.
In quali casi usare SQL su Hadoop e perch cos popolare.
HiveQL, il dialetto SQL introdotto da Apache Hive.
Utilizzare HiveQL per eseguire analisi di tipo SQL del dataset di Twitter.
Come HiveQL ricalca le funzioni pi comuni dei database relazionali come le
viste e i join.
Il modo in cui HiveQL consente di incorporare funzioni definite dallutente
nelle sue query.
Come SQL su Hadoop completa Pig.
Altri prodotti SQL su Hadoop come Impala e in cosa sono diversi da Hive.
Perch SQL su Hadoop
Finora abbiamo visto come scrivere programmi Hadoop usando le API
MapReduce e come Pig Latin fornisce unastrazione di scripting e un wrapper per
la logica business personalizzata per mezzo delle UDF. Pig uno strumento molto
potente, ma il suo modello di programmazione a flusso di dati non familiare alla
maggior parte degli sviluppatori o analisti. Lo strumento tradizionale privilegiato
da questi professionisti per esplorare i dati SQL.
Nel 2008 Facebook ha rilasciato Hive, la prima implementazione di SQL su
Hadoop utilizzata su vasta scala. Invece di fornire un modo per sviluppare pi
rapidamente le attivit di map e reduce, Hive offre unimplementazione di HiveQL,
un linguaggio di interrogazione basato su SQL. Hive prende le istruzioni HiveQL e
traduce immediatamente e automaticamente le query in uno o pi job di
MapReduce. Esegue quindi il programma MapReduce nel suo complesso e
restituisce i risultati allutente. Linterfaccia per Hadoop non solo riduce il tempo
necessario a produrre i risultati dallanalisi dei dati, ma estende anche
significativamente la rete di chi pu usare Hadoop. Non occorrono competenze di
sviluppo software: chiunque ha familiarit con SQL pu usare Hive.
La combinazione di questi attributi fa s che HiveQL venga spesso impiegato
come strumento di analisi dei dati e aziendale per eseguire query ad hoc sui dati
conservati in HDFS. Con Hive, lanalista pu lavorare sul perfezionamento delle
query senza coinvolgere lo sviluppatore software. Come Pig, consente lestensione
di HiveQL per mezzo di UDF (User Defined Function), permettendo la
personalizzazione del dialetto SQL di base con funzionalit specifiche per il
business.

Altre soluzioni SQL su Hadoop


Hive stato il primo prodotto a introdurre e supportare HiveQL, ma oggi non
pi lunico. Nel resto del capitolo scopriremo Impala, uno strumento rilasciato nel
2013 gi molto popolare, soprattutto per le query a bassa latenza. Ce ne sono
altri, ma noi ci occuperemo soprattutto di Hive e Impala perch sono quelli che
hanno avuto pi successo.
Presentando le funzioni e le capacit principali di SQL su Hadoop, forniremo
anche degli esempi duso di Hive; sebbene Hive e Impala condividano numerose
funzionalit SQL, hanno anche molte differenze. Non approfondiremo ogni nuova
caratteristica studiando esattamente com supportata in Hive rispetto a Impala.
Vedremo piuttosto gli aspetti che sono comuni a entrambi, ma se li utilizzate tutti e
due, leggete le note del rilascio pi recente per individuare le diversit.
Prerequisiti
Prima di affrontare le tecnologie specifiche, generiamo alcuni dati che
utilizzeremo negli esempi di questo capitolo. Nello specifico, creeremo una
versione modificata dello script di Pig visto nel Capitolo 6. Lo script in questo
capitolo presume che nella directory /jar in HDFS siano disponibili i JAR di
Elephant Bird usati in precedenza. Trovate il codice sorgente completo alla pagina
http://bit.ly/1LuwRr5; il cuore di extract_for_hive.pig il seguente:

-- load JSON data


tweets = load $inputDir using com.twitter.elephantbird.pig.load.JsonLoader(-nestedLoad);
-- Tweets
tweets_tsv = foreach tweets {
generate
(chararray)CustomFormatToISO($0#created_at,
EEE MMMM d HH:mm:ss Z y) as dt,
(chararray)$0#id_str,
(chararray)$0#text as text,
(chararray)$0#in_reply_to,
(boolean)$0#retweeted as is_retweeted,
(chararray)$0#user#id_str as user_id, (chararray)$0#place#id as place_id;
}
store tweets_tsv into $outputDir/tweets
using PigStorage(\u0001);
-- Places
needed_fields = foreach tweets {
generate
(chararray)CustomFormatToISO($0#created_at,
EEE MMMM d HH:mm:ss Z y) as dt,
(chararray)$0#id_str as id_str,
$0#place as place;
}
place_fields = foreach needed_fields {
generate
(chararray)place#id as place_id,
(chararray)place#country_code as co,
(chararray)place#country as country,
(chararray)place#name as place_name,
(chararray)place#full_name as place_full_name,
(chararray)place#place_type as place_type;
}
filtered_places = filter place_fields by co != ;
unique_places = distinct filtered_places;
store unique_places into $outputDir/places
using PigStorage(\u0001);

-- Users
users = foreach tweets {
generate
(chararray)CustomFormatToISO($0#created_at,
EEE MMMM d HH:mm:ss Z y) as dt,
(chararray)$0#id_str as id_str,
$0#user as user;
}
user_fields = foreach users {
generate
(chararray)CustomFormatToISO(user#created_at,
EEE MMMM d HH:mm:ss Z y) as dt,
(chararray)user#id_str as user_id,
(chararray)user#location as user_location,
(chararray)user#name as user_name,
(chararray)user#description as user_description,
(int)user#followers_count as followers_count,
(int)user#friends_count as friends_count,
(int)user#favourites_count as favourites_count,
(chararray)user#screen_name as screen_name,
(int)user#listed_count as listed_count;

}
unique_users = distinct user_fields;
store unique_users into $outputDir/users
using PigStorage(\u0001);

Eseguite lo script come segue:


$ pig f extract_for_hive.pig param inputDir=<json input> -param outputDir=<output path>

Questo codice scrive i dati in tre file TSV separati per il tweet, lutente e le
informazioni sulla posizione. Notate che nel comando store passiamo un argomento
quando chiamiamo PigStorage. Questo singolo argomento modifica il separatore
predefinito da un carattere di tabulazione al valore Unicode U0001; in alternativa
potete anche premere Ctrl+C+A. Questo il separatore tipico delle tabelle di
Hive e ci torner particolarmente utile perch i dati dei tweet potrebbero
contenere delle tabulazioni in altri campi.

Panoramica su Hive
Illustreremo ora come importare i dati in Hive e come eseguire una query
sullastrazione della tabella Hive che ci procurer i dati. In questo esempio e nel
resto del capitolo presupporremo che le query vengano digitate nella shell che pu
essere invocata tramite il comando hive.
Recentemente stato reso disponibile un client chiamato Beeline, che potrebbe
diventare il client CLI privilegiato nel prossimo futuro.
Quando si importano nuovi dati in Hive, si segue solitamente un processo in tre
passi.
1. Creiamo la specifica della tabella in cui devono essere importati i dati.
2. Importiamo i dati nella tabella creata.
3. Eseguiamo le query HiveQL sulla tabella.
La maggior parte delle istruzioni HiveQL ha un corrispettivo diretto nelle
istruzioni con nome simili di SQL standard. (Se vi serve un ripasso su SQL,
trovate numerose risorse online.)
Hive fornisce query con una vista strutturata dei dati, ma perch ci sia
possibile, dobbiamo prima definire la specifica delle colonne della tabella e
importare i dati nella tabella prima di poter eseguire qualsiasi query. Tale
specifica viene generata con unistruzione CREATE che indica il nome della tabella, il
nome e i tipi delle sue colonne e alcuni metadati riguardanti la sua
memorizzazione:
CREATE table tweets (
created_at string,
tweet_id string,
text string,
in_reply_to string,
retweeted boolean,
user_id string,
place_id string
) ROW FORMAT DELIMITED
FIELDS TERMINATED BY \u0001
STORED AS TEXTFILE;

Listruzione crea una nuova tabella tweets definita da un elenco di nomi per le
colonne nel dataset e dal rispettivo tipo di dati. Specifichiamo che i dati sono
delimitati dal carattere Unicode U0001 e che il formato usato per salvare i dati
TEXTFILE.

I dati possono essere importati da una posizione nella directory tweets/ di


HDFS usando listruzione LOAD DATA:
LOAD DATA INPATH tweets OVERWRITE INTO TABLE tweets;

Di default, i dati per le tabelle di Hive sono conservati in HDFS sotto a


/user/hive/warehouse. Se si fornisce allistruzione LOAD un percorso per i dati in
HDFS, non copier semplicemente i dati in /user/hive/warehouse, ma ve li
sposter proprio. Se volete analizzare quei dati in HDFS che sono usati da altre
applicazioni, allora createne una copia o ricorrete alle tabelle EXTERNAL, che
descriveremo pi avanti.
Una volta che i dati sono stati importati in Hive, possiamo interrogarli. Per
esempio:
SELECT COUNT(*) FROM tweets;

Questo codice restituisce il numero totale di tweet presenti nel dataset. HiveQL,
come SQL, non case sensitive per quanto riguarda parole chiave e nomi di
colonne e tabelle. Per convenzione, le istruzioni SQL usano le maiuscole per le
parole chiave del linguaggio SQL, e lo stesso faremo noi quando utilizzeremo
HiveQL nei file, come vedremo tra poco. Tuttavia, quando digiteremo dei comandi
interattivi, prenderemo la via pi facile e ci atterremo alle minuscole.
Se considerate attentamente il tempo impiegato dai vari comandi nellesempio
precedente, noterete che caricare dei dati in una tabella dura quanto crearne la
specifica, mentre il semplice conteggio di tutte le righe occupa un tempo pi lungo.
Anche loutput mostra che la creazione della tabella e il caricamento dei dati non
comportano lesecuzione di job di MapReduce, e questo spiega i tempi di
esecuzione cos brevi.

La natura delle tabelle di Hive


Sebbene Hive copi i file dei dati nella sua directory di lavoro, in realt in
questo momento non elabora i dati di input in righe.
Le istruzioni CREATE TABLE e LOAD DATA non creano effettivamente delle tabelle reali,
ma producono i metadati che verranno utilizzati quando Hive generer i job di
MapReduce per accedere ai dati salvati concettualmente nella tabella ma che
risiedono effettivamente in HDFS. Per quanto le istruzioni HiveQL facciano
riferimento alla struttura di una tabella specifica, compito di Hive generare il
codice che la mappa nel formato effettivo su disco in cui sono memorizzati i file
dei dati.
Questo potrebbe suggerire che Hive non sia un vero database, ed cos.
Laddove un database relazionale richiede di definire uno schema della tabella
prima dellinserimento dei dati e accetta solo quei dati che sono conformi a tale
specifica, Hive molto pi flessibile. La natura meno concreta delle sue tabelle
implica che gli schemi possano essere definiti in base ai dati man mano che
arrivano, senza presupporre come dovrebbero essere. Per quanto i formati di dati
mutevoli siano sempre problematici a prescindere dalla tecnologia, il modello di
Hive fornisce una grado di libert maggiore nella gestione dellinconveniente
quando sorge (e non se dovesse sorgere).
Larchitettura di Hive
Fino alla versione 2, Hadoop era soprattutto un sistema batch. Come abbiamo
visto nei capitoli precedenti, i job di MapReduce tendono ad avere una latenza
elevata e un overhead derivante dallinvio e dalla programmazione temporale.
Internamente, Hive compila le istruzioni HiveQL in job di MapReduce. Le sue
query sono sempre state caratterizzate dallalta latenza, ma questa situazione
cambiata con liniziativa Stinger e con i miglioramenti introdotti in Hive 0.13, che
discuteremo successivamente.
Hive esegue unapplicazione client che elabora le query di HiveQL, le converte
in job di MapReduce e le invia a un cluster Hadoop, sul MapReduce nativo di
Hadoop 1 o sul MapReduce Application Master che gira su YARN in Hadoop 2.
A prescindere dal modello, Hive utilizza sempre un componente chiamato
metastore, in cui conserva tutti i metadati riguardanti le tabelle definite nel
sistema. Ironicamente, questo salvato in un database relazionale dedicato alluso
di Hive. Nelle prime versioni di Hive, tutti i client comunicavano direttamente con
il metastore, ma questo voleva dire che ogni utente della CLI di Hive doveva
conoscere il nome utente e la password per il metastore.
HiveServer era stato creato per funzionare come punto di immissione per i
client remoti, che poteva anche agire come singolo punto di accesso-controllo e
che controllava tutti gli accessi al metastore sottostante. A causa delle limitazioni
in HiveServer, il sistema pi recente per accadere ad Hive attraverso
HiveServer2, che multi-client.
HiveServer2 introduce alcuni miglioramenti rispetto al suo predecessore, come
lautenticazione dellutente e il supporto per connessioni multiple dallo stesso
client. Trovate ulteriori informazioni allindirizzo
https://cwiki.apache.org/confluence/display/Hive/Setting+Up+HiveServer2.

Le istanze di HiveServer e HiveServer2 possono essere eseguite manualmente


attraverso i comandi hive --service hiveserver e hive --service hiveserver2.
Negli esempi visti in precedenza e nel resto del capitolo usiamo implicitamente
HiveServer per inviare le query attraverso lo strumento a riga di comandi di Hive.
HiveServer2 offre Beeline.
Per ragioni di compatibilit e maturit, essendo Beeline relativamente nuovo,
entrambi gli strumenti sono resi disponibili su Cloudera e sulle altre distribuzioni
principali. Il client di Beeline parte integrante della distribuzione principale di
Apache Hive ed quindi completamente open source. Pu essere eseguito nella
versione incorporata con il seguente comando:
$ beeline -u jdbc:hive2://

Tipi di dati
HiveQL supporta molti dei tipi di dati comuni forniti dai sistemi di database
standard. Questi includono i tipi primitivi, come float, double, int e string, attraverso
tipi di collezioni strutturate che forniscono un corrispettivo SQL a tipi come arrays,
structs e unions (structs con opzioni per alcuni campi). Poich Hive implementato

in Java, i tipi primitivi si comporteranno come le loro controparti Java. I tipi di


dati di Hive possono essere raggruppati in cinque categorie.
Numerici: tinyint, smallint, int, bigint, float, double e decimal.
Di data e ora: timestamp e date.
Stringa: string, varchar e char.
Collezioni: array, map, struct e uniontype.
Varie: boolean, binary e NULL

Istruzioni DDL
HiveQL offre alcune istruzioni per creare, eliminare e modificare i database, le
tabelle e le viste. Listruzione CREATE DATABASE <name> crea un nuovo database con il
nome dato. Un database rappresenta un namespace dove sono contenuti i metadati
della tabella e della vista. Se sono presenti pi database, listruzione USE <database
name> specifica quale usare per interrogare le tabelle o per creare nuovi metadati.

Se non specificato esplicitamente un database, Hive eseguir tutte le istruzioni


sul database default. SHOW [DATABASES, TABLES, VIEWS] visualizza i database attualmente
disponibili allinterno di un data warehouse e quali metadati di tabella e vista
sono presenti nel database in uso in quel momento:
CREATE DATABASE twitter;
SHOW databases;
USE twitter;
SHOW TABLES;

Listruzione CREATE TABLE [IF NOT EXISTS] <name> crea una tabelle con il nome dato.
Come gi accennato, quello che viene effettivamente creato sono i metadati che
rappresentano la tabella e la relativa mappatura sui file in HDFS, oltre che una
directory in cui salvare i file dei dati. Se esiste gi una tabella o una vista con lo
stesso nome, Hive sollever uneccezione.
I nomi delle tabelle e delle colonne non sono case sensitive. Nelle versioni pi
vecchie di Hive (0.12 e precedenti), in questi nomi erano ammessi solo caratteri
alfanumerici e underscore. Da Hive 0.13, il sistema supporta i caratteri Unicode
nei nomi delle colonne. Le parole riservate, come load e create, devono prevedere
dei backtick (il carattere `) per poter essere trattate in modo letterale.
La parola chiave EXTERNAL specifica che la tabella si trova in risorse fuori dal
controllo di Hive, un meccanismo che pu essere utile per estrarre dati da unaltra
sorgente allinizio della pipeline ETL (Extract-Transform-Load) basata su
Hadoop. La clausola LOCATION specifica dove trovare il file (o la directory)
sorgente. La parola chiave EXTERNAL e la clausola LOCATION sono state utilizzate nel
codice seguente:
CREATE EXTERNAL TABLE tweets (
created_at string,
tweet_id string,
text string,
in_reply_to string,
retweeted boolean,
user_id string,
place_id string
) ROW FORMAT DELIMITED
FIELDS TERMINATED BY \u0001
STORED AS TEXTFILE
LOCATION ${input}/tweets;

Questa tabella verr creata nel metastore, ma i dati non verranno copiati nella
directory /user/hive/warehouse.
NOTA
Hive non contempla i concetti di chiave primaria e di identificatore univoco. Lunicit e la
normalizzazione dei dati sono aspetti da affrontare prima di caricare i dati nel data warehouse.

Listruzione CREATE VIEW <view name> AS SELECT crea una vista con il nome dato. Per
esempio, possiamo creare una vista per isolare i retweet dagli altri messaggi, cos:
CREATE VIEW retweets
COMMENT Tweets that have been retweeted
AS SELECT * FROM tweets WHERE retweeted = true;
A meno che non sia diversamente specificato, i nomi delle colonne si ottengono
tramite listruzione SELECT. Attualmente Hive non supporta le viste materializzate.
Le istruzioni DROP TABLE e DROP VIEW rimuovono i metadati e i dati da una tabella o
vista. Quando si elimina una tabella o una vista EXTERNAL, vengono cancellati solo i
metadati; i file dei dati effettivi non vengono toccati.
Hive consente la modifica dei metadati di una tabella attraverso listruzione
ALTER TABLE, che pu essere utilizzata per modificare il tipo di una colonna, il nome,

la posizione e il commento o per aggiungere e sostituire le colonne.


Quando si aggiungono colonne, importante ricordare che verranno modificati
solo i metadati e non il dataset. Per esempio, se volessimo aggiungere al centro di
una tabella una colonna che non esisteva nei file precedenti, al momento di
selezionare i dati pi vecchi potremmo ottenere valori sbagliati nelle colonne
sbagliate. Questo perch staremmo guardando i file vecchi secondo un formato
nuovo. Illustreremo la migrazione dei dati e degli schemi nel Capitolo 8, quando
parleremo di Avro.
Analogamente, ALTER VIEW <view name> AS <select statement> modifica la definizione
di una vista esistente.

Formati di file e storage


I file dei dati che soggiacciono a una tabella di Hive non sono diversi dagli altri
file in HDFS. Gli utenti possono leggere direttamente i file in HDFS nelle tabelle
di Hive usando altri strumenti. Lo stesso vale se vogliono scrivere sui file in
HDFS che possono essere caricati in Hive attraverso CREATE EXTERNAL TABLE o LOAD DATA
INPATH.

Hive utilizza le classi Serializer e Deserializer (SerDe), oltre che FileFormat per
leggere e scrivere le righe delle tabelle. Si usa una SerDe nativa se il ROW FORMAT non
specificato o se il ROW FORMAT DELIMITED specificato in unistruzione CREATE TABLE. La
clausola DELIMITED dice al sistema di leggere i file delimitati. Per lescape dei
caratteri di delimitazione si pu utilizzare la clausola ESCAPED BY.
Hive impiega attualmente le seguenti classi FileFormat per leggere e scrivere i
file in HDFS.
TextInputFormat e HiveIgnoreKeyTextOutputFormat: leggono/scrivono i dati nel formato
di solo testo.
e SequenceFileOutputFormat: leggono/scrivono i dati nel
SequenceFileInputFormat

formato SequenceFile di Hadoop.


Le seguenti classi SerDe possono invece essere utilizzate per serializzare e
deserializzare i dati.
MetadataTypedColumnsetSerDe : legge/scrive record delimitati, come i record CSV o
quelli separati da tabulazione.
ThriftSerDe e DynamicSerDe: leggono/scrivono gli oggetti Thrift.

JSON
Dalla versione 0.13, Hive dotato dellorg.apache.hive.hcatalog.data.JsonSerDe
nativo. Per le versioni precedenti, uno dei moduli di
serializzazione/deserializzazione JSON pi ricchi probabilmente Hive-JSON-
Serde (https://github.com/rcongiu/Hive-JSON-Serde).
Possiamo usare un modulo o laltro per caricare i tweet JSON senza la
necessit di una pre-elaborazione e definire semplicemente uno schema di Hive
che corrisponda al contenuto di un documento JSON. Nel prossimo esempio,
utilizzeremo Hive-JSON-SerDe. Come facciamo per qualsiasi modulo di terze
parti, carichiamo i suoi JAR con il codice seguente:
ADD JAR JAR json-serde-1.3-jar-with-dependencies.jar;

Eseguiamo poi la consueta istruzione CREATE:


CREATE EXTERNAL TABLE tweets (
contributors string,
coordinates struct <
coordinates: array <float>,
type: string>,
created_at string,
entities struct <
hashtags: array <struct <
indices: array <tinyint>,
text: string>>,

)
ROW FORMAT SERDE org.openx.data.jsonserde.JsonSerDe
STORED AS TEXTFILE
LOCATION tweets;

Con questa SerDe, possiamo mappare i documenti annidati (come entit o


utenti) sui tipi struct o map. Diciamo ad Hive che i dati salvati in LOCATION tweets
sono di testo (STORED AS TEXTFILE) e che ogni riga un oggetto JSON (ROW FORMAT SERDE
org.openx.data.jsonserde.JsonSerDe). In Hive 0.13 e versioni successive, possiamo

esprimere questa propriet come ROW FORMAT SERDE


org.apache.hive.hcatalog.data.JsonSerDe.

Specificare a mano lo schema per i documenti pi complessi pu essere un


processo noioso e a rischio di errori. Il modulo hive-json
(https://github.com/hortonworks/hive-json) una pratica utility che permette di
analizzare i documenti estesi e di generare uno schema Hive adatto. A seconda
della collezione del documento, potrebbe servire un intervento di raffinamento.
Nel nostro esempio abbiamo utilizzato uno schema generato con hive-json che
mappa i tweet JSON su un certo numero di tipi di dati struct. Questo ci permette di
interrogare i dati utilizzando una comoda notazione puntata. Per esempio,
possiamo estrarre il nome dello schermo e i campi descrittivi di un oggetto utente
con il seguente codice:
SELECT user.screen_name, user.description FROM tweets_json LIMIT 10;

Avro
AvroSerde (https://cwiki.apache.org/confluence/display/Hive/AvroSerDe) permette di
leggere e scrivere i dati nel formato Avro. Dalla versione 0.14, si possono creare
tabelle Avro usando listruzione STORED AS AVRO, e Hive si occuper di creare lo
schema Avro opportuno per la tabella. Le versioni precedenti di Hive sono un po
pi prolisse.
Come esempio, carichiamo il dataset di PageRank che abbiamo generato nel
Capitolo 6. Il dataset era stato creato usando la classe AvroStorage di Pig, e ha
questo schema:
{
type:record,
name:record,
fields: [
{name:topic,type:[null,int]},
{name:source,type:[null,int]},
{name:rank,type:[null,float]}
]
}

La struttura della tabella viene catturata in un record Avro, che contiene le


informazioni dellheader (un nome e un namespace facoltativo per qualificare il
nome) e un array dei campi. Ogni campo specificato con il suo nome e tipo, oltre
che con una stringa di documentazione (facoltativa).
Per alcuni campi, il tipo non un valore singolo, ma una coppia di valori, uno
dei quali null. Questa una unione Avro, ed il modo caratteristico di gestire le
colonne che potrebbero contenere un valore null. Avro specifica null come un tipo
reale, e qualsiasi posizione in cui un altro tipo potrebbe avere un valore null dovr
essere specificata in questo modo. Il tutto sar gestito in modo a noi trasparente
quando usiamo lo schema.
Con questa definizione, possiamo creare una tabella di Hive che utilizza questo
schema per la specifica:
CREATE EXTERNAL TABLE tweets_pagerank
ROW FORMAT SERDE
org.apache.hadoop.hive.serde2.avro.AvroSerDe
WITH SERDEPROPERTIES (avro.schema.literal={
type:record,
name:record,
fields: [
{name:topic,type:[null,int]},
{name:source,type:[null,int]},
{name:rank,type:[null,float]}
]
})
STORED AS INPUTFORMAT
org.apache.hadoop.hive.ql.io.avro.AvroContainerInputFormat
OUTPUTFORMAT
org.apache.hadoop.hive.ql.io.avro.AvroContainerOutputFormat
LOCATION ${data}/ch5-pagerank;

Osserviamo poi la seguente definizione della tabella dallinterno di Hive (anche


HCatalog, che presenteremo nel Capitolo 8, supporta tali definizioni):
DESCRIBE tweets_pagerank;
OK
topic int from deserializer
source int from deserializer
rank float from deserializer

Nel DDL, diciamo ad Hive che i dati sono salvati nel formato Avro usando
AvroContainerInputFormat e AvroContainerOutputFormat. Ogni riga deve essere serializzata e

deserializzata tramite org.apache.hadoop.hive.serde2.avro.AvroSerDe. Hive deduce lo


schema della tabella dallo schema di Avro incorporato in avro.schema.literal.
In alternativa, possiamo salvare uno schema in HDFS e fare in modo che Hive
lo legga per determinare la struttura della tabella. Creiamo lo schema precedente
in un file chiamato pagerank.avsc (.avsc lestensione di file standard per gli schemi
di Avro), quindi collochiamolo in HDFS; preferibile avere una posizione unica
per i file degli schemi, per esempio /schema/avro. Infine, definiamo la tabella
usando la propriet WITH SERDEPROPERTIES
(avro.schema.url=hdfs://<namenode>/schema/avro/pagerank.avsc) della SerDe

avro.schema.url.

Se nel percorso della classe non sono presenti dipendenze Avro, dovremo
aggiungere il JAR Avro MapReduce al nostro ambiente prima di accedere ai singoli
campi. In Hive, sulla CDH5 VM:
ADD JAR /opt/cloudera/parcels/CDH/lib/avro/avro-mapred-hadoop2.jar;

Possiamo anche usare questa tabella come qualsiasi altra. Per esempio,
possiamo interrogare i dati per selezionare le coppie utente-topic che hanno un
PageRank elevato:
SELECT source, topic from tweets_pagerank WHERE rank >= 0.9;

Nel Capitolo 8, vedremo il ruolo svolto da Avro e da avro.schema.url

nellabilitare le migrazioni degli schemi.

Storage a colonne
Hive sfrutta anche lo storage in colonne attraverso i formati ORC
(https://cwiki.apache.org/confluence/display/Hive/LanguageManual+ORC) e Parquet
(https://cwiki.apache.org/confluence/display/Hive/Parquet).
Se una tabella contiene molte colonne, non insolito che una query ne elabori
solo un piccolo sottoinsieme. Tuttavia, anche in SequenceFile ogni riga completa e
tutte le sue colonne verranno lette dal disco, decompresse ed elaborate.
Loperazione consuma parecchie risorse di sistema anche per quei dati che
sappiamo fin dallinizio non essere interessanti.
Anche i database relazionali tradizionali salvano i dati per riga, e un tipo di
database colonnare sposta il focus sulle colonne. Nel modello pi semplice,
invece di un file per tabella, ci sar un file per ciascuna colonna della tabella. Se
una query deve accedere solo a cinque colonne in una tabella che ne contiene 100,
allora verranno letti solo i file di quelle cinque colonne. Sia ORC sia Parquet
usano questo principio e altre ottimizzazioni per velocizzare le interrogazioni.

Query
Le tabelle possono essere interrogate usando la consueta istruzione SELECT FROM.
Listruzione WHERE permette di specificare le condizioni di filtro, GROUP BY aggrega i
record, ORDER BY specifica i criteri di ordinamento e LIMIT determina il numero di
record da recuperare. Ai record raggruppati possono essere applicate funzioni
aggregate, come count e sum. Per esempio, il prossimo codice restituisce i primi 10
utenti pi prolifici nel dataset:
SELECT user_id, COUNT(*) AS cnt FROM tweets GROUP BY user_id ORDER BY cnt DESC LIMIT 10

Eccoli:
2263949659 4
1332188053 4
959468857 3
1367752118 3
362562944 3
58646041 3
2375296688 3
1468188529 3
37114209 3
2385040940 3

Possiamo migliorare la leggibilit delloutput di hive impostando


SET hive.cli.print.header=true;

Questa riga dir ad hive, ma non a beeline, di visualizzare i nomi delle colonne
come parte delloutput.
SUGGERIM ENT O
Potete aggiungere il comando al file .hiverc che si trova solitamente nella root della directory
home dellutente per applicarlo a tutte le sessioni della CLI di hive.

HiveQL implementa un operatore JOIN che permette di combinare le tabelle.


Nella sezione Prerequisites, generiamo dei dataset separati per lutente e per le
informazioni sulla posizione. Carichiamoli in Hive usando delle tabelle esterne.
Come prima cosa creiamo una tabella user per contenere i dati dellutente, cos:
CREATE EXTERNAL TABLE user (
created_at string,
user_id string,
location string,
name string,
description string,
followers_count bigint,
friends_count bigint,
favourites_count bigint,
screen_name string,
listed_count bigint
) ROW FORMAT DELIMITED
FIELDS TERMINATED BY \u0001
STORED AS TEXTFILE
LOCATION ${input}/users;
Poi creiamo una tabella place per i dati riguardanti la posizione:
CREATE EXTERNAL TABLE place (
place_id string,
country_code string,
country string,
name string,
full_name string,
place_type string
) ROW FORMAT DELIMITED
FIELDS TERMINATED BY \u0001
STORED AS TEXTFILE
LOCATION ${input}/places;

Possiamo utilizzare loperatore JOIN per visualizzare i nomi dei 10 utenti pi


prolifici, come segue:
SELECT tweets.user_id, user.name, COUNT(tweets.user_id) AS cnt
FROM tweets
JOIN user ON user.user_id = tweets.user_id
GROUP BY tweets.user_id, user.user_id, user.name
ORDER BY cnt DESC LIMIT 10;

AT T ENZIONE
Hive supporta solo i (semi) join di equality, outer e left.

Notate che ci potrebbero essere pi immissioni con un dato ID utente ma valori


diversi per le colonne followers_count, friends_count e favourites_count. Per evitare i
duplicati, contiamo solo lo user_id dalla tabella tweets.
Possiamo riscrivere la query precedente come segue:
SELECT tweets.user_id, u.name, COUNT(*) AS cnt
FROM tweets
join (SELECT user_id, name FROM user GROUP BY user_id, name) u
ON u.user_id = tweets.user_id
GROUP BY tweets.user_id, u.name
ORDER BY cnt DESC LIMIT 10;

Invece di unire direttamente la tabella user, eseguiamo una sottoquery:


SELECT user_id, name FROM user GROUP BY user_id, name;

La sottoquery estrae gli ID utente univoci e i nomi. Hive ha un supporto limitato


per le sottoquery, ammettendone da sempre una solo nella clausola FROM di
unistruzione SELECT. Hive 0.13 ha aggiunto un ulteriore supporto limitato per le
sottoquery anche nella clausola WHERE.
HiveQL un linguaggio sempre in evoluzione, e la sua trattazione va oltre gli
scopi di questo libro. Trovate una descrizione delle sue query e delle capacit di
DDL alla pagina https://cwiki.apache.org/confluence/display/Hive/LanguageManual.
Strutturare le tabelle di Hive per i vari carichi di
lavoro
Spesso Hive non utilizzato da solo; le tabelle, infatti, vengono create avendo
in mente dei carichi di lavoro specifici o devono essere invocate in modo da poter
essere incluse in processi automatizzati. Esploriamo alcuni di questi scenari.

Partizionare una tabella


Quando abbiamo parlato dei formati di file colonnari, abbiamo spiegato i
vantaggi dellescludere il prima possibile i dati non necessari quando si elabora
una query. Un concetto analogo utilizzato da qualche tempo anche in SQL: il
partizionamento delle tabelle.
Quando si crea una tabella partizionata, si indica una colonna come chiave di
partizione. Tutti i valori che hanno quella chiave vengono immagazzinati insieme.
Nel caso di Hive, vengono create diverse sottodirectory per ciascuna chiave di
partizione sotto alla directory della tabella nella posizione in cui si trova il
warehouse in HDFS.
importante cogliere la cardinalit della colonna di partizione. Se ci sono
pochi valori distinti, i vantaggi sono pochi, perch i file rimangono comunque
grandi. Se ce ne sono troppi, allora le query devono analizzare molti file per poter
accedere a tutti i dati richiesti. La chiave di partizione pi comune forse quella
che si basa sulla data. Per esempio, potremmo partizionare fin dallinizio la
tabella user in base alla colonna created_at, cio in base alla data in cui lutente
stato registrato per la prima volta. Poich il partizionamento per definizione
influisce sulla struttura dei file della tabella, creiamo la tabella adesso come non
esterna:
CREATE TABLE partitioned_user (
created_at string,
user_id string,
location string,
name string,
description string,
followers_count bigint,
friends_count bigint,
favourites_count bigint,
screen_name string,
listed_count bigint
) PARTITIONED BY (created_at_date string)
ROW FORMAT DELIMITED
FIELDS TERMINATED BY \u0001
STORED AS TEXTFILE;

Per caricare i dati in una partizione, possiamo assegnare specificatamente un


valore per la partizione in cui inserire i dati, come segue:
INSERT INTO TABLE partitioned_user
PARTITION( created_at_date = 2014-01-01)
SELECT
created_at,
user_id,
location,
name,
description,
followers_count,
friends_count,
favourites_count,
screen_name,
listed_count
FROM user;

Il codice risultante un po prolisso, perch serve unistruzione per ogni valore


di chiave di partizione; se una singola istruzione LOAD o INSERT contiene dei dati per
pi partizioni, non funzioner. Hive offre una funzione di partizionamento
dinamico che pu venirci in aiuto. Impostiamo le tre variabili seguenti:
SET hive.exec.dynamic.partition = true;
SET hive.exec.dynamic.partition.mode = nonstrict;
SET hive.exec.max.dynamic.partitions.pernode=5000;

Le prime due istruzioni permettono a tutte le partizioni (opzione nonstrict) di


essere dinamiche. La terza permette la creazione di 5000 partizioni diverse su
ciascun nodo mapper e reducer.
Utilizziamo semplicemente il nome della colonna da usare come chiave di
partizione, e Hive inserir i dati nelle partizioni a seconda del valore della chiave
per una data riga:
INSERT INTO TABLE partitioned_user
PARTITION( created_at_date )
SELECT
created_at,
user_id,
location,
name,
description,
followers_count,
friends_count,
favourites_count,
screen_name,
listed_count,
to_date(created_at) as created_at_date
FROM user;

Anche se qui usiamo una sola colonna di partizione, possiamo suddividere una
tabella in base a pi chiavi di colonna; sufficiente disporle in una lista con le
voci separate da virgole nella clausola PARTITIONED BY.
Notate che le colonne della chiave di partizione devono essere incluse come
ultime colonne in qualsiasi istruzione si sta utilizzando per linserimento in una
tabella partizionata. Nel codice precedente usiamo la funzione to_date di Hive per
convertire il timestamp created_at in una stringa formattata come YYYY-MM-DD.
I dati partizionati sono salvati in HDFS come
/path/to/warehouse/<database>/<table>/key=<value>. Nel nostro esempio, la struttura della

tabella partitioned_user apparir come


/user/hive/warehouse/default/partitioned_user/created_at=2014-04-01.

Se i dati vengono aggiunti direttamente nel file system, magari da qualche


strumento di elaborazione di terze parti o da hadoop fs -put, il metastore non rilever
automaticamente le nuove partizioni. Lutente dovr eseguire manualmente
unistruzione ALTER TABLE come la seguente per ciascuna partizione appena inserita:
ALTER TABLE <table_name> ADD PARTITION <location>;

Per aggiungere i metadati per tutte le partizioni attualmente non presenti nel
metastore possiamo utilizzare listruzione MSCK REPAIR TABLE <table_name>;. Su EMR,
equivale a eseguire la seguente istruzione:
ALTER TABLE <table_name> RECOVER PARTITIONS;

Notate che entrambe le istruzioni funzioneranno anche con le tabelle esterne.


Nel prossimo capitolo vedremo come sfruttare questo pattern per creare pipeline
flessibili e interoperabili.

Sovrascrivere e aggiornare i dati


Il partizionamento si rivela utile anche quando dobbiamo aggiornare una parte
di una tabella. Solitamente unistruzione nella forma che segue sostituir tutti i dati
per la tabella di destinazione:
INSERT OVERWRITE INTO <table>

Se si omette OVERWRITE, ogni istruzione INSERT aggiunger dei dati alla tabella. A
volte pu andar bene, ma spesso i dati sorgente da immettere in una tabella di Hive
hanno lo scopo di aggiornare completamente un subset dei dati lasciando inalterato
il resto.
Se eseguiamo unistruzione INSERT OVERWRITE (o LOAD OVERWRITE) in una partizione,
solo questa sar interessata dallintervento. Per esempio, se dovessimo inserire
dei dati utente volendo intervenire solo sulle partizioni che contengono dati nel file
sorgente, dovremmo aggiungere la parola chiave OVERWRITE allistruzione INSERT
precedente.
Possiamo anche aggiungere delle condizioni allistruzione SELECT. Per esempio,
immaginiamo di voler aggiornare i dati per un certo mese:
INSERT INTO TABLE partitioned_user
PARTITION (created_at_date)
SELECT created_at ,
user_id,
location,
name,
description,
followers_count,
friends_count,
favourites_count,
screen_name,
listed_count,
to_date(created_at) as created_at_date
FROM user
WHERE to_date(created_at) BETWEEN 2014-03-01 and 2014-03-31;

Bucketing e ordinamento
Il partizionamento un costrutto che potete sfruttare utilizzando la colonna (o le
colonne) di partizione nella clausola WHERE delle interrogazioni sulle tabelle. Esiste
un altro meccanismo chiamato bucketing che pu segmentare ulteriormente il
modo in cui una tabella memorizzata, e lo fa in un modo che permette allo stesso
Hive di ottimizzare i propri piani di interrogazione per trarre vantaggio dalla
struttura.
Creiamo versioni a bucket dei nostri tweet e delle tabelle utente; osservate le
istruzioni supplementari CLUSTER BY e SORT BY nellistruzione CREATE TABLE:
CREATE table bucketed_tweets (
tweet_id string,
text string,
in_reply_to string,
retweeted boolean,
user_id string,
place_id string
) PARTITIONED BY (created_at string)
CLUSTERED BY(user_ID) into 64 BUCKETS
ROW FORMAT DELIMITED
FIELDS TERMINATED BY \u0001
STORED AS TEXTFILE;

CREATE TABLE bucketed_user (


user_id string,
location string,
name string,
description string,
followers_count bigint,
friends_count bigint,
favourites_count bigint,
screen_name string,
listed_count bigint
) PARTITIONED BY (created_at string)
CLUSTERED BY(user_ID) SORTED BY(name) into 64 BUCKETS
ROW FORMAT DELIMITED
FIELDS TERMINATED BY \u0001
STORED AS TEXTFILE;

Notate che abbiamo modificato la tabella tweets in modo che venga partizionata;
il bucketing pu essere applicato solo a una tabella gi segmentata.
Cos come dobbiamo specificare una colonna di partizione quando aggiungiamo
dati a una tabella partizionata, dovremo anche assicurarci che i dati inseriti in una
tabella a bucket siano raggruppati correttamente. Per farlo, impostiamo il flag
seguente prima di immettere i dati nella tabella:
SET hive.enforce.bucketing=true;

Come accade con le tabelle partizionate, non possibile applicare la funzione


di bucketing quando si utilizza listruzione LOAD DATA; se volete caricare dati esterni
in una tabella a bucket, inseritela prima in una tabella temporanea e poi usate la
sintassi INSERTSELECT per popolarla.
Fatto questo, le righe vengono allocate a un bucket in base al risultato della
funzione hash applicata alla colonna specificata nella clausola CLUSTERED BY.
Il bucketing particolarmente vantaggioso quando dobbiamo unire in modo
simile due tabelle segmentate in bucket, come nel caso precedente. Per esempio,
una query nella forma seguente migliorerebbe parecchio:
SET hive.optimize.bucketmapjoin=true;
SELECT
FROM bucketed_user u JOIN bucketed_tweet t
ON u.user_id = t.user_id;

Con il join eseguito sulla colonna usata per il bucketing, Hive pu ottimizzare
lelaborazione, poich sa che ogni bucket contiene lo stesso insieme di colonne
user_id in entrambe le tabelle. Quando si deve decidere su quali righe eseguire

linterrogazione, basteranno quelle contenute nel bucket. Questo richiede che


entrambe le tabelle siano raggruppate sulla stessa colonna e che i numeri dei
bucket siano identici o che uno sia un multiplo dellaltro. Nel secondo caso, con
una tabella raggruppata in 32 bucket e unaltra in 64, la funzione hash predefinita
usata per allocare i dati su un bucket far s che gli ID nel bucket 3 nella prima
tabella coprano quelli nei bucket 3 e 35 nella seconda.

Campionamento dei dati


Il bucketing di una tabella si rivela utile anche quando si sfrutta la capacit di
Hive di campionare i dati in una tabella. Il campionamento permette a una query di
raccogliere solo un sottoinsieme specificato delle righe complessive di una
tabella. Pu essere comodo quando si ha una tabella molto grande con un pattern di
dati relativamente coerenti. In questo caso, lapplicazione di una query a una
piccola parte dei dati pi veloce, e fornisce un risultato ampiamente
rappresentativo. Naturalmente questo vale solo per le query con le quali cercate di
determinare le caratteristiche della tabella, come gli intervalli dei pattern nei dati;
se state cercando di contare qualcosa, allora il risultato dovr essere scalato sulla
tabella a dimensioni intere.
Per una tabella non segmentata in bucket, potete eseguire il campionamento con
un meccanismo simile a quello che abbiamo visto in precedenza, specificando che
la query dovr essere applicata solo a un dato sottoinsieme della tabella:
SELECT max(friends_count)
FROM user TABLESAMPLE(BUCKET 2 OUT OF 64 ON name);

In questa query, Hive effettuer lhash delle righe della tabella in 64 bucket in
base al nome della colonna; utilizzer quindi solo il secondo bucket per la query.
possibile specificare pi bucket, e se RAND() fornito come clausola ON, lintera riga
viene usata dalla funzione di bucketing.
Anche se funziona, non un sistema particolarmente efficiente, poich si dovr
analizzare lintera tabella per generare il sottoinsieme di dati. Se campioniamo una
tabella in bucket e ci assicuriamo che il numero di bucket campionati uguale o
un multiplo dei bucket nella tabella, allora Hive legger solo i bucket in questione.
Per esempio:
SELECT MAX(friends_count)
FROM bucketed_user TABLESAMPLE(BUCKET 2 OUT OF 32 on user_id);

Nella query precedente sulla tabella bucketed_user, che creata con 64 bucket
sulla colonna user_id, il campionamento legger solo i bucket richiesti, poich usa
la stessa colonna. In questo caso, si tratter dei bucket 2 e 34 di ciascuna
partizione.
Unultima forma di campionamento quella dei blocchi. In questo caso,
possiamo specificare la parte di tabella da campionare, e Hive ne utilizzer
unapprossimazione leggendo solo quei blocchi di dati sorgente in HDFS che sono
sufficienti per assecondare tale dimensione.
La dimensione dei dati pu essere indicata come percentuale della tabella, come
assoluta o come numero di righe in ciascun blocco. La sintassi seguente di
TABLESAMPLE campiona rispettivamente lo 0,5 percento della tabella, 1 GB di dati o

100 righe per split:


TABLESAMPLE(0.5 PERCENT)
TABLESAMPLE(1G)
TABLESAMPLE(100 ROWS)

Se vi interessano queste ultime forme di campionamento, consultate la


documentazione, perch ci sono alcune limitazioni specifiche ai formati di input e
di file che sono supportati.

Scrivere degli script


Possiamo collocare i comandi di Hive in un file ed eseguirli con lopzione -f
nella CLI di hive:
$ cat show_tables.hql
show tables;
$ hive -f show_tables.hql

possibile impostare dei parametri per le istruzioni HiveQL attraverso hiveconf.


Questo ci consente di specificare il nome di una variabile dambiente nel punto in
cui viene usata invece che in quello in cui viene invocata. Per esempio:
$ cat show_tables2.hql
show tables like ${hiveconf:TABLENAME};

$ hive -hiveconf TABLENAME=user -f show_tables2.hql

La variabile pu essere impostata anche nello script di Hive o in una sessione


interattiva:
SET TABLE_NAME=user;

Largomento hiveconf precedente aggiunge le eventuali nuove variabili nello


stesso namespace come opzioni di configurazione di Hive. Dalla versione 0.8,
offerta unopzione analoga chiamata hivevar che aggiunge le variabili utente in un
namespace distinto.
Usando hivevar, il comando precedente sarebbe il seguente:
$ cat show_tables3.hql
show tables like ${hivevar:TABLENAME};
$ hive -hivevar TABLENAME=user f show_tables3.hql

In alternativa possiamo scrivere il comando in modo interattivo:


SET hivevar:TABLE_NAME=user;
Hive e Amazon Web Services
Con Elastic MapReduce come servizio AWS on demand di Hadoop, possibile
eseguire Hive su un cluster EMR. Inoltre si possono utilizzare i servizi di storage
di Amazon, soprattutto S3, da qualsiasi cluster Hadoop, che sia in EMR o sul
cluster locale.

Hive e S3
Come detto nel Capitolo 2, si pu specificare un file system predefinito diverso
da HDFS per Hadoop, e S3 una possibilit. Al suo interno si possono salvare
tabelle specifiche, i cui dati verranno raccolti in un cluster da elaborare; i dati
risultanti possono essere scritti in una posizione diversa in S3 (la stessa tabella
non pu essere la sorgente e la destinazione di una singola query) oppure in HDFS.
Possiamo prendere un file dei nostri dati dei tweet e collocarlo in una posizione
in S3 con un comando come il seguente:
$ aws s3 put tweets.tsv s3://<bucket-name>/tweets/

Come prima cosa dobbiamo specificare la chiave daccesso e quella segreta per
entrare nel bucket. Si pu procedere in tre modi:
impostando fs.s3n.awsAccessKeyId e fs.s3n.awsSecretAccessKey con i valori opportuni
nella CLI di Hive;
impostando gli stessi valori in hive-site.xml, tenendo per presente che questo
limita luso di S3 a un unico set di credenziali;
specificando esplicitamente la posizione della tabella nellURL della stessa,
cio s3n://<access key>:<secret access key>@<bucket>/<path>.
Dopodich possiamo creare una tabella facendo riferimento a questi dati:
CREATE table remote_tweets (
created_at string,
tweet_id string,
text string,
in_reply_to string,
retweeted boolean,
user_id string,
place_id string
) CLUSTERED BY(user_ID) into 64 BUCKETS
ROW FORMAT DELIMITED
FIELDS TERMINATED BY \t
LOCATION s3n://<bucket-name>/tweets
Pu essere un modo eccezionalmente efficace di raccogliere i dati di S3 in un
cluster locale di Hadoop per lelaborazione.
NOTA
Per poter utilizzare le credenziali AWS nellURI di una posizione in S3 a prescindere da come
vengono passati i parametri, le due chiavi di accesso non devono contenere i caratteri /, +, = o
\. Se necessario, si pu generare un nuovo set di credenziali dalla console IAM allindirizzo
https://console.aws.amazon.com/iam/.

In teoria, potreste lasciare i dati nella tabella esterna e farvi riferimento quando
necessario per evitare le latenze e i costi di trasferimento dei dati sulla WAN,
anche se spesso ha pi senso raccogliere i dati in una tabella locale e partire da
qui per lelaborazione. Se la tabella partizionata, potreste per esempio ritrovarvi
a dover recuperare una partizione nuova ogni giorno.

Hive su Elastic MapReduce


Da un certo punto di vista, luso di Hive su Amazon Elastic MapReduce non
difforme da quanto visto finora nel capitolo. Potete creare un cluster persistente,
accedere al nodo master e usare la CLI di Hive per creare tabelle e sottoporre
query. Tutte queste operazioni utilizzano lo storage locale sulle istanze di EC2 per
i dati delle tabelle.
Come prevedibile, i job sui cluster EMR possono anche fare riferimento a
tabelle i cui dati sono salvati in S3 (o in DynamoDB). Altrettanto prevedibile
che Amazon abbia rilasciato delle estensioni alle sue versioni di Hive per rendere
il tutto pi fluido. Dallinterno di un job di EMR piuttosto semplice recuperare i
dati da una tabella che si trova in S3, elaborarli, scrivere gli eventuali dati
intermedi sullo store EMR locale e poi scrivere i risultati di output in S3,
DynamoDB o in uno dei sempre pi numerosi servizi AWS.
Il pattern prima citato in cui i dati nuovi vengono aggiunti ogni giorno in una
nuova directory di partizione per una tabella si dimostrato molto efficiente in S3;
questa spesso la posizione di storage privilegiata per i dataset che continuano a
crescere. Quando si usa EMR si incontra per una differenza nella sintassi; invece
del comando MSCK citato, il comando per aggiornare una tabella di Hive con i
nuovi dati aggiunti alla directory di una partizione il seguente:
ALTER TABLE <table-name> RECOVER PARTITIONS;
Consultate la documentazione di EMR allindirizzo http://amzn.to/1Ct2gnW per
scoprire le ultime migliorie. Leggete anche la documentazione pi ampia, in
particolare per quanto riguarda lintegrazione con altri servizi AWS in un ambito
in costante sviluppo.
Estendere HiveQL
Il linguaggio HiveQL pu essere esteso attraverso alcuni plug-in e funzioni di
terze parti. In Hive, ci sono tre tipi di funzioni caratterizzate dal numero di righe
che prendono come input e che producono in output.
User Defined Function (UDF): sono funzioni pi semplici che agiscono su
una riga alla volta.
User Defined Aggregate Function (UDAF): prendono pi righe come input e
generano pi righe come output. Si tratta di funzioni aggregate da usare in
combinazione con unistruzione GROUP BY (simile a COUNT(), AVG(), MIN(), MAX() e
cos via).
User Defined Table Function (UDTF): prendono pi righe come input e
generano una tabella logica costituita da pi righe che possono essere
utilizzate nelle espressioni di join.
NOTA
Queste API sono fornite solo in Java. Per gli altri linguaggi, possibile far fluire i dati attraverso
uno script definito dallutente usando le clausole TRANSFORM, MAP e REDUCE, che agiscono da
frontend per le capacit di streaming di Hadoop.

Per scrivere le UDF sono disponibili due API. LAPI pi semplice


org.apache.hadoop.hive.ql.exec.UDF pu essere utilizzata per le funzioni che prendono e

restituiscono tipi scrivibili di base. UnAPI pi ricca che fornisce il supporto per i
tipi di dati diversi da quelli scrivibili si trova nel package
org.apache.hadoop.hive.ql.udf.generic.GenericUDF. Vedremo ora come si pu utilizzare la

prima API per implementare una stringa in una funzione ID simile a quella usata
nel Capitolo 5 per mappare gli hashtag in integer in Pig. Per creare una UDF con
questa API sufficiente estendere la classe UDF e scrivere un metodo evaluate():
public class StringToInt extends UDF {
public Integer evaluate(Text input) {
if (input == null)
return null;

String str = input.toString();


return str.hashCode();
}
}

La funzione prende un oggetto Text come input e lo mappa su un valore integer


con il metodo hashCode(). Il codice sorgente di questa funzione si trova alla pagina
http://bit.ly/1BygFtw .
NOTA
Come gi visto nel Capitolo 6, in produzione dovrebbe essere utilizzata una funzione hash pi
robusta.

Compiliamo la classe e archiviamola in un file JAR, cos:


$ javac -classpath $(hadoop classpath):/opt/cloudera/parcels/CDH/lib/hive/lib/*
com/learninghadoop2/hive/udf/StringToInt.java
$ jar cvf myudfs-hive.jar com/learninghadoop2/hive/udf/StringToInt.class

Prima di poter utilizzare una UDF, la si deve registrare in Hive con i seguenti
comandi:
ADD JAR myudfs-hive.jar;
CREATE TEMPORARY FUNCTION string_to_int AS com.learninghadoop2.hive.udf.StringToInt;

Listruzione ADD JARaggiunge un file JAR alla cache distribuita. Listruzione


CREATE TEMPORARY FUNCTION <function> AS <class> registra una funzione in Hive che

implementa una classe Java. La funzione verr eliminata alla chiusura della
sessione di Hive. Dalla versione 0.13, possibile creare funzioni permanenti la
cui definizione conservata nel metastore usando CREATE FUNCTION .
Una volta registrata, StringToInt pu essere impiegata in una query come
qualsiasi altra funzione. Nel prossimo esempio estrarremo prima un elenco di
hashtag dal testo dei tweet applicando regexp_extract, dopodich utilizzeremo
string_to_int per mappare ogni tag su un ID numerico:

SELECT unique_hashtags.hashtag, string_to_int(unique_hashtags.hashtag) AS tag_id FROM


(
SELECT regexp_extract(text,
(?:\\s|\\A|^)[##]+([A-Za-z0-9-_]+)) as hashtag
FROM tweets
GROUP BY regexp_extract(text,
(?:\\s|\\A|^)[##]+([A-Za-z0-9-_]+))
) unique_hashtags GROUP BY unique_hashtags.hashtag, string_to_int(unique_hashtags.hashtag);

Come abbiamo fatto nel Capitolo 6, possiamo usare la query precedente per
creare una tabella di lookup:
CREATE TABLE lookuptable (tag string, tag_id bigint);
INSERT OVERWRITE TABLE lookuptable
SELECT unique_hashtags.hashtag,
string_to_int(unique_hashtags.hashtag) as tag_id
FROM
(
SELECT regexp_extract(text,
(?:\\s|\\A|^)[##]+([A-Za-z0-9-_]+)) AS hashtag
FROM tweets
GROUP BY regexp_extract(text,
(?:\\s|\\A|^)[##]+([A-Za-z0-9-_]+))
) unique_hashtags
GROUP BY unique_hashtags.hashtag, string_to_int(unique_hashtags.hashtag);
Interfacce programmatiche
Oltre che con gli strumenti a riga di comando hive e beeline, possibile inviare
query HiveQL al sistema attraverso le interfacce programmatiche JDBC e Thrift. Il
supporto per ODBC era incluso nelle versioni pi vecchie di Hive, ma dalla
versione 0.12 devessere costruito da zero. Trovate ulteriori informazioni sulla
procedura alla pagina https://cwiki.apache.org/confluence/display/Hive/HiveODBC.

JDBC
Un client Hive scritto usando le API JDBC assomiglia in tutto e per tutto a un
programma client scritto per altri sistemi di database (per esempio MySQL).
Quello che segue un esempio. Trovate il codice sorgente alla pagina
http://bit.ly/1AFnlqu:

public class HiveJdbcClient {


private static String driverName = org.apache.hive.jdbc.HiveDriver;

// stringa di connessione
public static String URL = jdbc:hive2://localhost:10000;

// Mostra tutte le tabelle nel database di default


public static String QUERY = show tables;

public static void main(String[] args) throws SQLException {


try {
Class.forName (driverName);
}
catch (ClassNotFoundException e) {
e.printStackTrace();
System.exit(1);
}
Connection con = DriverManager.getConnection (URL);
Statement stmt = con.createStatement();

ResultSet resultSet = stmt.executeQuery(QUERY);


while (resultSet.next()) {
System.out.println(resultSet.getString(1));
}
}
}

La sezione URL lURI JDBC che descrive il punto finale della connessione. La
sintassi per stabilire una connessione remota jdbc:hive2:<host>:<port>/<database>. Le
connessioni in modalit incorporata possono essere stabilite senza specificare un
host o una porta, come jdbc:hive2://.
hive e hive2 sono i driver da usare quando ci si connette ad HiveServer e HiveServer2.

QUERY contiene la query HiveQL da eseguire.


NOTA
Linterfaccia di JDBC di Hive visualizza solo il database predefinito. Per poter accedere ad altri
database, dovete fare esplicitamente riferimento a essi nelle query sottostanti utilizzando la
notazione <database>.<table>.

Carichiamo prima il driver HiveServer2 JDBC org.apache.hive.jdbc.HiveDriver.


NOTA
Utilizzate org.apache.hadoop.hive.jdbc.HiveDriver per connettervi ad HiveServer.

Poi, come con qualsiasi altro programma JDBC, stabiliamo una connessione
allURL e usiamola per istanziare una classe Statement. Eseguiamo QUERY, senza
autenticarci, e salviamo il dataset di output nelloggetto ResultSet. Infine, scorriamo
il resultSet e visualizziamone il contenuto nella riga di comando.
Compiliamo ed eseguiamo lesempio con i comandi seguenti:
$ javac HiveJdbcClient.java
$ java -cp $(hadoop
classpath):/opt/cloudera/parcels/CDH/lib/hive/lib/*:/opt/cloudera/parcels/CDH/lib/hive/lib/hive-
jdbc.jar: com.learninghadoop2.hive.client.HiveJdbcClient

Thrift
Thrift fornisce un accesso di basso livello ad Hive e offre alcuni vantaggi
rispetto allimplementazione JDBC di HiveServer. Principalmente, consente pi
connessioni dallo stesso client e luso agevole di linguaggi di programmazione
diversi da Java. Con HiveServer2, non lopzione pi frequente, ma vale la pena
citarlo per quanto riguarda la compatibilit. Alla pagina http://bit.ly/1O6nZ9V trovate
un esempio di client di Thrift implementato usando lAPI Java. Questo client pu
essere utilizzato per connettersi ad HiveServer, ma a causa di differenze di
protocollo, il client non funzioner con HiveServer2.
Nellesempio definiamo un metodo getClient() che prende come input lhost e la
porta di un servizio HiveServer e restituisce unistanza di
org.apache.hadoop.hive.service.ThriftHive.Client.

Un client si ottiene istanziando come prima cosa una connessione socket


(org.apache.thrift.transport.TSocket) sul servizio HiveServer, e poi specificando un
protocollo (org.apache.thrift.protocol.TBinaryProtocol) per serializzare e trasmettere i
dati, come segue:
TSocket transport = new TSocket(host, port);
transport.setTimeout(TIMEOUT);
transport.open();
TBinaryProtocol protocol = new TBinaryProtocol(transport);
client = new ThriftHive.Client(protocol);

Chiamiamo getClient() dal metodo main e usiamo il client per eseguire una query
su unistanza di HiveServer in esecuzione sul localhost sulla porta 11111:
public static void main(String[] args) throws Exception {
Client client = getClient(localhost, 11111);
client.execute(show tables);
List<String> results = client.fetchAll();
for (String result : results) {
System.out.println(result);
}
}

Assicuratevi che giri proprio su questa porta; se cos non fosse, avviate
unistanza con questo comando:
$ sudo hive --service hiveserver -p 11111

Compilate ed eseguite lesempio di HiveThriftClient.java con


$ javac $(hadoop classpath):/opt/cloudera/parcels/CDH/lib/hive/lib/*
com/learninghadoop2/hive/client/HiveThriftClient.java
$ java -cp $(hadoop classpath):/opt/cloudera/parcels/CDH/lib/hive/lib/*:
com.learninghadoop2.hive.client.HiveThriftClient
Liniziativa Stinger
Hive continua ad avere successo fin dalle prime release, in particolare per
quanto riguarda la sua capacit di fornire unelaborazione di tipo SQL su dataset
molto grandi. Ma le altre applicazioni non dormono sugli allori, e nel tempo Hive
si fatto la reputazione di essere piuttosto lento, soprattutto rispetto ai tempi di
avvio di job estesi e alla sua inadeguatezza quando si tratta di fornire risposte
veloci a query concettualmente semplici.
Queste limitazioni sono dovute meno ad Hive in s e pi a una conseguenza
dellinefficienza insita nella traduzione delle query SQL nel modello di
MapReduce rispetto ad altri modi per implementare una query SQL. In particolare
per quanto riguarda dataset molto grandi, MapReduce richiede molte operazioni di
I/O (e quindi molto tempo) per scrivere i risultati di un job che vengono poi letti
da un altro. Come visto nel Capitolo 3, questa una delle caratteristiche principali
del design di Tez, che pu programmare i job su un cluster Hadoop come un
grafico che non richiede attivit di scrittura e lettura poco efficienti.
Quella che segue una query sul framework MapReduce rispetto a Tez:
SELECT a.country, COUNT(b.place_id) FROM place a JOIN tweets b ON (a. place_id = b.place_id)
GROUP BY a.country;

La Figura 7.1 ne d una rappresentazione grafica.

Figura 7.1 Hive su MapReduce e Tez a confronto.


In MapReduce, vengono creati due job per le clausole GROUP BY e JOIN. Il primo
costituito da un insieme di attivit di MapReduce che legge i dati dal disco per
realizzare il raggruppamento. I reducer scrivono dei risultati intermedi su disco in
modo che loutput possa essere sincronizzato. I mapper nel secondo job leggono i
risultati intermedi dal disco e i dati dalla tabella b. Il dataset combinato viene
quindi passato al reducer dove le chiavi condivise vengono unite. Se avessimo
dovuto eseguire unistruzione ORDER BY, questa avrebbe generato un terzo job e altri
passaggi di MapReduce. La stessa query viene eseguita su Tez come job singolo
da un unico set di attivit di map che leggono i dati dal disco. Il raggruppamento e
lunione dellI/O vengono convogliati tra i reducer.
Al di l di questi limiti a livello di architettura, attorno al supporto del
linguaggio SQL cerano alcuni ambiti in cui si poteva fornire una migliore
efficienza; nel 2013 fu lanciata liniziativa Stinger con lobiettivo dichiarato di
rendere Hive pi veloce di oltre 100 volte e con un supporto pi ricco a SQL.
Hive 0.13 ha tutte le caratteristiche delle tre fasi di Stinger, e produce un dialetto
SQL molto pi completo. In aggiunta allimplementazione basata su MapReduce in
YARN, viene anche offerto Tez come framework di esecuzione pi efficiente
rispetto alle implementazioni precedenti di MapReduce in Hadoop 1.
Con Tez come motore di esecuzione, Hive non pi vincolato a una serie job di
MapReduce lineari, e pu costruire un grafico di processo in cui ogni passo pu,
per esempio, far fluire i risultati in alcuni passi secondari.
Per trarre il massimo dal framework Tez, approfittiamo della nuova
impostazione della variabile hive:
set hive.execution.engine=tez;

Questa impostazione presuppone che Tez sia installato sul cluster; disponibile
nella forma sorgente da http://tez.apache.org e in molte altre distribuzioni (ma non in
Cloudera al momento della stesura di queste righe).
Il valore alternativo mr, che usa il classico modello di MapReduce (su YARN);
quindi possibile, in ununica installazione, fare un confronto con le prestazioni di
Hive usando Tez.
Impala
Hive non lunico prodotto che fornisce la capacit di SQL su Hadoop. Il
secondo pi utilizzato probabilmente Impala, annunciato a fine 2012 e rilasciato
nella primavera del 2013. Per quanto sviluppato inizialmente in Cloudera, il suo
codice sorgente viene caricato periodicamente su un repository Git open source
(https://github.com/cloudera/impala).
Impala nato partendo dalla stessa percezione della debolezza di Hive che ha
portato alliniziativa Stinger. Inoltre trae parte della sua ispirazione da Google
Dremel
(http://static.googleusercontent.com/media/research.google.com/en//pubs/archive/36632.pdf),
descritto pubblicamente per la prima volta in un documento del 2009. Dremel fu
costruito in Google per colmare il divario tra la necessit di query molto veloci su
dataset molto grandi e la latenza elevata insita nel modello di MapReduce allora
esistente alla base di Hive. Dremel costituiva un approccio sofisticato al
problema: invece di attenuare linconveniente su MapReduce cos come
implementato da Hive, creava un nuovo servizio che accedeva agli stessi dati
conservati in HDFS. Dremel beneficiava anche del grande lavoro svolto per
ottimizzare il formato di storage dei dati allo scopo di velocizzare le query
analitiche.

Larchitettura di Impala
Larchitettura di base di Impala costituita tre componenti principali: i daemon
di Impala, lo store dello stato e i client. Versioni recenti hanno aggiunto altri
componenti che migliorano il servizio, ma noi ci concentreremo sullarchitettura di
alto livello.
Il daemon di Impala (impalad) dovrebbe essere eseguito su ogni host sul quale un
processo di DataNode gestisce i dati di HDFS. Notate che impalad non accede ai
blocchi del file system attraverso lAPI FileSystem di HDFS, ma utilizza una
funzione di letture a cortocircuito che rendono laccesso pi efficiente.
Quando un client invia una query, pu farlo su qualsiasi processo impalad in
esecuzione, che diventer il coordinatore delloperazione. Laspetto chiave delle
prestazioni di Impala che per ogni query genera del codice nativo personalizzato,
che viene poi inserito ed eseguito da tutti i processi impalad sul sistema. Il codice
cos ottimizzato esegue la query sui dati locali, e ogni impalad restituisce un
sottoinsieme dei risultati al nodo coordinatore, che effettua laccorpamento finale
dei dati per produrre il risultato definitivo. Questo tipo di architettura dovrebbe
essere familiare a chiunque ha lavorato con una delle soluzioni di data warehouse
MPP (Massively Parallel Processing) oggi disponibili, solitamente commerciali e
costose. Con il cluster in esecuzione, il daemon dello store dello stato garantisce
che ciascun processo impalad sia consapevole di tutti gli altri e fornisce una vista
della salute complessiva del cluster.

Coesistenza con Hive


Impala nuovo, quindi tende ad avere un set pi limitato di tipi di dati SQL,
oltre a supportare un dialetto pi vincolato di SQL che non Hive. Sta tuttavia
espandendo il suo supporto a ogni nuova release. Consultate la sua
documentazione (http://www.cloudera.com/content/cloudera-content/cloudera-
docs/CDH5/latest/Impala/impala.html) per una panoramica sullo stato dellarte

Impala supporta il meccanismo dei metastore usato da Hive per conservare in


modo persistente i metadati che circondano la struttura della tabella e lo storage.
Questo significa che su un cluster con unimpostazione di Hive gi esistente,
dovrebbe essere possibile utilizzare subito Impala in quanto accede allo stesso
metastore, garantendo quindi laccesso alle stesse tabelle disponibili in Hive.
Attenzione per: le differenze nel dialetto SQL e nei tipi di dati potrebbero
causare risultati imprevisti quando si lavora in un ambiente combinato Hive-
Impala. Alcune query potrebbero funzionare su uno ma non sullaltro, potrebbero
avere delle caratteristiche prestazionali molto diverse (come vedremo in seguito)
o potrebbero dare effettivamente dei risultati sfasati. Questultimo inconveniente
diventa palese quando si usano tipi di dati come float e double che vengono trattati
in modo diverso dai sistemi sottostanti (Hive implementato su Java, mentre
Impala scritto in C++).
Dalla versione 1.2, supporta le UDF scritte sia in C++ sia in Java, sebbene C++
sia raccomandato perch molto pi veloce. Ricordatelo quando intendete
condividere delle funzioni personalizzate tra Hive e Impala.

Una filosofia diversa


Al suo primo rilascio, il vantaggio principale di Impala consisteva
nellattuazione della cosiddetta analisi alla velocit del pensiero. Le query
venivano restituite in tempi sufficientemente rapidi da permettere di esplorare un
thread di analisi in modo del tutto interattivo senza dover attendere minuti perch
venisse completata una query alla volta. Le prestazioni di Impala erano
considerate stupefacenti, soprattutto se paragonate alla versione di Hive allora in
dotazione.
Impala si focalizza ancora oggi sulle query pi brevi, e questo senza imporre
alcun limite al sistema. Le sue performance tendono a richiedere molta memoria
durante lelaborazione; se una query necessita di un dataset per essere tenuta in
memoria invece che essere disponibile sul nodo in esecuzione, fallir nelle
versioni di Impala precedenti la 2.0.
Se confrontiamo Stinger con Impala, vediamo che questultimo punta
maggiormente alleccellenza nelle interrogazioni pi brevi (e presumibilmente pi
comuni) che supportano lanalisi dei dati interattiva. Molti strumenti e servizi di
business intelligence oggi sono certificati per girare su Impala. Liniziativa Stinger
si concentrata di meno sul rendere Hive veloce nellambito in cui invece Impala
ha pi successo, ma ha migliorato Hive a vari livelli per tutti i carichi di lavoro.
Impala continua a evolvere a grandi passi, e Stinger ha dato pi slancio ad Hive,
quindi vale la pena considerare entrambi i prodotti per determinare quale meglio
si adatta ai requisiti di prestazioni e funzionalit dei propri progetti e flussi di
lavoro.
Ricordate poi che anche il mercato influisce sullindirizzo dello sviluppo di
Impala e Hive. Impala stato creato ed ancora seguito da Cloudera, il produttore
pi famoso delle distribuzioni Hadoop. Liniziativa Stinger, per quanto abbia dato
il suo contributo in molte compagnie come Microsoft e Intel, era guidata da
Hortonworks, probabilmente il secondo produttore pi grande delle distribuzioni
Hadoop. Quindi, se usate la distribuzione di Hadoop di Cloudera, le funzioni di
Hive potrebbero arrivare con il contagocce, mentre Impala sar sempre
aggiornato; per contro, se usate unaltra distribuzione, potreste ottenere la release
di Hive pi recente ma che potrebbe avere una versione di Impala pi vecchia o,
come accade spesso, potreste doverla scaricare e installare da voi.
Una situazione analoga si avuta con Parquet e i formati di file ORC prima
citati. Parquet preferito da Impala e sviluppato da un gruppo di compagnie
guidate da Cloudera, mentre ORC preferito da Hive ed sostenuto da
Hortonworks.
Sfortunatamente, nella realt il supporto di Parquet arriva molto in fretta nella
distribuzione di Cloudera ma molto meno in quella di Hortonworks, dove si
preferisce il formato di file ORC.
Tutto questo un po preoccupante; per quanto la concorrenza in questo ambito
sia buona cosa e nonostante lannuncio di Impala abbia elettrizzato la community
di Hive, diversamente dal passato, cresce il rischio che la distribuzione abbia un
impatto maggiore sugli strumenti e sui formati di file che verranno supportati in
futuro. Ponderate quindi al meglio le vostre scelte sulla distribuzione, valutando
con attenzione le vostre esigenze rispetto a SQL su Hadoop.

Drill, Tajo e oltre


SQL su Hadoop non fa pi riferimento esclusivamente ad Hive o a Impala.
Apache Drill (http://drill.apache.org) unimplementazione completa del modello
Dremel descritto per la prima volta da Google. Laddove Impala implementa
larchitettura di Dremel sui dati in HDFS, Drill cerca di fornire funzionalit
analoghe tra pi sorgenti di dati. ancora agli albori, ma se avete esigenze pi
ampie di quelle che possono essere soddisfatte da Hive o Impala, potete dargli
unoccasione.
Tajo (http://tajo.apache.org) un altro progetto Apache che punta a diventare un
sistema completo di data warehouse sui dati di Hadoop. Ha unarchitettura simile
a quella di Impala, ma offre un sistema molto pi ricco con componenti quali gli
optimizer multipli e gli strumenti ETL caratteristici dei data warehouse
tradizionali ma che si trovano pi raramente nel mondo Hadoop. Ha una base
utente pi piccola ma stato utilizzato con successo da molte compagnie per tanto
tempo, e potrebbe essere la vostra scelta privilegiata se vi serve una soluzione pi
completa di data warehouse.
In questo campo stanno nascendo molti altri prodotti, ed una buona idea fare
qualche ricerca. Hive e Impala sono fantastici, ma se non rispondono alle vostre
esigenze, date unocchiata in giro: qualche altro strumento potrebbe fare al caso
vostro.
Riepilogo
Alle origini, Hadoop era visto in qualche modo come il killer dei database
relazionali. Nel tempo diventato chiaro che va considerato come complementare
alle tecnologie RDBMS, tanto che la community ha sviluppato strumenti come SQL
altrettanto validi nel mondo di Hadoop.
HiveQL unimplementazione di SQL su Hadoop e ha costituito il focus
principale di questo capitolo. Per quanto riguarda HiveQL e le sue
implementazioni, abbiamo trattato i seguenti argomenti.
Il modello logico fornito da HiveQL sui dati conservati in HDFS, che
diverso dai database relazionali in cui la struttura della tabella applicata
preventivamente.
Il supporto di HiveQL a molti dei tipi di dati e dei comandi standard SQL,
compresi i join e le viste.
Le funzioni di tipo ETL offerte da HiveQL, compresa la capacit di importare
i dati in tabelle e di ottimizzare la struttura della tabella attraverso il
partizionamento e altri meccanismi simili.
La capacit di HiveQL di estendere il suo set di operatori principale con un
codice definito dallutente, e come si differenzia dal meccanismo delle UDF
di Pig.
La storia recente degli sviluppi di Hive, come liniziativa Stinger, che ha
visto il passaggio di Hive a unimplementazione aggiornata che usa Tez.
Lecosistema pi ampio di HiveQL, che include oggi prodotti come Impala,
Tajo e Drill, e come ogni strumento si focalizza ed eccelle in aree specifiche.
Con Pig e Hive, abbiamo introdotto modelli alternativi per elaborare i dati di
MapReduce, ma finora abbiamo trascurato una domanda: quali approcci e
strumenti sono necessari per far s che questi grossi dataset vengano raccolti in
Hadoop restando utili e gestibili nel tempo? Nel prossimo capitolo saliremo di un
livello nella gerarchia delle astrazioni, e vedremo come gestire il ciclo di vita di
queste incomparabili risorse che sono i dati.
Capitolo 8
Gestione del ciclo di vita dei dati

Nei capitoli precedenti ci siamo concentrati sulla tecnologia, illustrando la


natura e luso di alcuni strumenti e tecniche. In questo capitolo e nel prossimo
adotteremo un approccio pi dallalto verso il basso; descriveremo un ambito
problematico che vi troverete sicuramente ad affrontare, e vedremo come gestirlo.
In particolare, tratteremo quanto segue.
Cosa sintende con lespressione gestione del ciclo di vita dei dati.
Perch bisogna occuparsene.
Le categorie di strumenti che possono essere utilizzati per risolvere il
problema.
Come utilizzare questi strumenti per costruire la prima met di una pipeline di
sentiment analysis per Twitter.
Cos la gestione del ciclo di vita dei dati
I dati non esistono solo in un punto nel tempo. Soprattutto nei workflow di
produzione a esecuzione prolungata, probabile che acquisirete man mano una
quantit significativa di dati nei cluster Hadoop. Le vostre esigenze saranno
raramente sempre le stesse, quindi accanto a una nuova logica potreste veder
cambiare anche il formato di quei dati o potrebbe essere necessario ricorrere a
sorgenti diverse per il dataset elaborato nellapplicazione. Utilizziamo
lespressione gestione del ciclo di vita dei dati per descrivere un approccio alla
raccolta, allo storage e alla trasformazione dei dati che garantisce che i dati si
trovino dove devono, che siano nel formato corretto e che possano evolvere nel
tempo insieme al sistema.

Importanza della gestione del ciclo di vita dei


dati
Se costruite applicazioni che elaborano dati, dipendete per definizione dai dati
che devono essere elaborati. Cos come teniamo in considerazione laffidabilit
delle applicazioni e dei sistemi, diventa necessario assicurarsi che anche i dati
siano pronti per la produzione. Sono parte dellimpresa e spesso hanno diversi
punti di integrazione con dei sistemi esterni. Se i dati che arrivano da questi
sistemi non sono affidabili, allora limpatto sui job di elaborazione sar
devastante come il fallimento del sistema. Limportazione dei dati diventa un
componente cruciale di per s. E quando diciamo che devono essere affidabili,
non intendiamo solo che arrivano per certo, ma che lo fanno in un formato
utilizzabile e attraverso un meccanismo che pu gestirne levoluzione nel tempo.
Il problema che queste criticit non emergono chiaramente finch i flussi non
iniziano a diventare grandi, quando il sistema diventa fondamentale e limpatto sul
business rilevante. Quegli approcci ad hoc che funzionavano su flussi di dati meno
problematici non verranno scalati, e sostituirli su un sistema live pu diventare una
sfida.

Strumenti di supporto
Niente panico! Esistono alcune categorie di strumenti che possono darci una
mano. In questo capitolo illustreremo alcuni esempi di tre categorie.
Servizi di orchestrazione: la creazione di una pipeline di importazione
costituita in genere da alcune fasi distinte; ci serviremo di uno strumento di
orchestrazione per consentirne la descrizione, lesecuzione e la gestione.
Connettori: data limportanza dellintegrazione con i sistemi esterni, vedremo
come utilizzare i connettori per semplificare le astrazioni fornite dallo
storage di Hadoop.
Formati di file: il modo in cui salviamo i dati ha un impatto sul modo in cui
gestiamo levoluzione del formato nel tempo; in questo veniamo aiutati da
alcuni formati di supporto ricchi.
Costruire la capacit per lanalisi dei
tweet
Nei capitoli precedenti abbiamo utilizzato diverse implementazioni dellanalisi
dei dati di Twitter per descrivere vari concetti. Scenderemo ora pi in profondit
seguendo un ipotetico case study. Creeremo una pipeline di importazione dei dati,
costruendo un workflow (un flusso di lavoro) pronto per la produzione tenendo
conto dellaffidabilit e dellevoluzione futura.
Procederemo in modo progressivo lungo tutto il capitolo. In ogni fase,
evidenzieremo cosa cambiato, ma per ragioni di spazio non potremo includere
tutti i listati per intero; trovate comunque tutte le iterazioni nel codice sorgente.

Ottenere i dati dei tweet


La prima cosa da fare ottenere i dati dei tweet. Come negli esempi precedenti,
possiamo passare gli argomenti -j e -n a stream.py per scaricare i tweet JSON in
stdout:

$ stream.py -j -n 10000 > tweets.json

Avendo a disposizione questo strumento per creare un batch di tweet campione


su richiesta, possiamo avviare la pipeline eseguendo il job su base periodica. Ma
come?

Oozie
Volendo potremmo usare qualcosa come cron per una programmazione semplice
dei job, ma ricordate che dobbiamo alimentare una pipeline costruita tenendo
presente laffidabilit. Ci serve allora uno strumento che ci permetta anche di
individuare gli errori e di reagire di fonte a situazioni eccezionali. Lo strumento
che useremo qui Oozie (http://oozie.apache.org), un motore di workflow e uno
scheduler concepito con un focus sullecosistema di Hadoop.
Oozie fornisce un mezzo per definire un workflow come una serie di nodi con
parametri configurabili e una transizione controllata da un nodo allaltro.
installato come parte della Cloudera QuickStart VM; il client principale a riga di
comando si chiama oozie, appunto.
NOTA
Abbiamo testato i workflow di questo capitolo sulla versione 5.0 della Cloudera QuickStart VM,
e al momento della stesura di queste righe Oozie nella versione 5.1 (la pi recente) mostrava
qualche inconveniente. Nei nostri workflow non c tuttavia nulla di specifico per una data
versione, quindi dovrebbero essere compatibili con qualsiasi implementazione di Oozie v4
correttamente funzionante.

Oozie potente e flessibile, ma potrebbe servire un po di tempo prima di


imparare a utilizzarlo; per questo vi forniremo alcuni esempi e descriveremo man
mano quello che accade.
Il nodo del workflow di Oozie pi comune action. dentro a questi nodi che
vengono eseguiti i passi del workflow; gli altri tipi di nodi gestiscono il workflow
per quanto riguarda le decisioni, il parallelismo e la rilevazione degli errori.
Oozie pu effettuare pi tipi di azioni. Una di queste lazione della shell, che pu
essere utilizzata per eseguire qualsiasi comando sul sistema, come i binari nativi,
gli script della shell o qualunque altra utility a riga di comando. Creiamo uno
script per generare un file di tweet e copiarlo in HDFS:
set -e
source twitter.keys
python stream.py -j -n 500 > /tmp/tweets.out
hdfs dfs -put /tmp/tweets.out /tmp/tweets/tweets.out
rm -f /tmp/tweets.out

Notate che la prima riga comporter il fallimento dellintero script qualora


dovesse fallire uno qualsiasi dei comandi inclusi. Utilizziamo un file dambiente
per fornire le chiavi di Twitter allo script in twitter.keys, nella forma seguente:
export TWITTER_CONSUMER_KEY=<value>
export TWITTER_CONSUMER_SECRET=<value>
export TWITTER_ACCESS_KEY=<value>
export TWITTER_ACCESS_SECRET=<value>

Oozie usa XML per descrivere i suoi workflow, salvati solitamente in un file
chiamato workflow.xml. Percorriamo la definizione di un workflow di Oozie che
chiama un comando di shell.
Lo schema per un workflow di Oozie detto workflow-app, e possiamo dare al
flusso un nome specifico. Pu essere utile quando si visualizza la cronologia di un
job nella CLI o in uninterfaccia web di Oozie. Negli esempi di questo libro
utilizzeremo un numero di versione progressivo che ci permetter di distinguere
pi facilmente le varie iterazioni nel repository sorgente. Ecco come assegnare un
nome al workflow-app:
<workflow-app xmlns=uri:oozie:workflow:0.4 name=v1>

I workflow di Oozie sono costituiti da una serie di nodi connessi, ognuno dei
quali indica un passo nel processo e che sono rappresentati da nodi XML nella
definizione del flusso. In Oozie ci sono alcuni nodi che si occupano della
transizione del workflow da un passo allaltro. Il primo di essi il nodo di inizio
(start), che dichiara il nome del primo nodo da eseguire nel flusso:
<start to=fs-node/>

Abbiamo poi la definizione del nodo start con nome. In questo caso un nodo
action, che il tipo generico della maggior parte dei nodi di Oozie che svolgono

effettivamente una qualche elaborazione:


<action name=fs-node>

La categoria dei nodi action piuttosto ampia, e la specificheremo meglio in


base allelaborazione particolare che svolge su quel nodo. Qui usiamo il tipo di
nodo fs, che ci permette di eseguire operazioni sul file system:
<fs>

Dobbiamo assicurarci che la directory su HDFS in cui vogliamo copiare il file


dei dati dei esista, che sia vuota e che abbia i permessi opportuni. Per farlo,
proviamo a eliminarla se presente, quindi creiamola e infine applichiamo i
permessi necessari, come segue:
<delete path=${nameNode}/tmp/tweets/>
<mkdir path=${nameNode}/tmp/tweets/>
<chmod path=${nameNode}/tmp/tweets permissions=777/>
</fs>

Vedremo in seguito un modo alternativo per impostare le directory. Una volta


eseguita la funzionalit del nodo, Oozie deve sapere come procedere nel
workflow. La maggior parte delle volte il passo successivo contempla lo
spostamento su un altro nodo action se questo ha avuto successo, altrimenti si
interrompe il flusso. Il tutto viene specificato dagli elementi successivi. Il nodo ok
fornisce il nome del nodo a cui passare se lesecuzione ha successo; il nodo error
indica invece il nodo di destinazione in caso di fallimento (fail). Eccome vengono
utilizzati i nodi ok e fail:
<ok to=shell-node/>
<error to=fail/>
</action>
<action name=shell-node>
Anche il secondo nodo action specializzato in base al tipo di elaborazione; in
questo caso abbiamo un nodo shell:
<shell xmlns=uri:oozie:shell-action:0.2>

Nellazione shell sono specificati il JobTracker e il NameNode di Hadoop.


Notate che i valori effettivi sono forniti da variabili; spiegheremo da dove
arrivano tra breve. Il JobTracker e il NameNode sono specificati come segue:
<job-tracker>${jobTracker}</job-tracker>
<name-node>${nameNode}</name-node>

Come illustrato nel Capitolo 3, MapReduce utilizza pi code per fornire un


supporto ad approcci diversi di programmazione delle risorse. Il prossimo
elemento specifica la coda di MapReduce a cui dovrebbe essere inviato il
workflow:
<configuration>
<property>
<name>mapred.job.queue.name</name>
<value>${queueName}</value>
</property>
</configuration>

Ora che il nodo shell configurato, possiamo indicare il comando da invocare,


nuovamente attraverso una variabile:
<exec>${EXEC}</exec>

I vari passi del workflow di Oozie vengono eseguiti come job di MapReduce.
Questa azione della shell verr quindi eseguita come istanza di unattivit
specifica su un dato TaskTracker. Dovremo perci indicare quali file devono
essere copiati sulla directory di lavoro locale sulla macchina del TaskTracker
prima che lazione venga eseguita. Nel nostro caso copieremo lo script della shell
principale, il generatore di tweet di Python e il file di configurazione di Twitter:
<file>${workflowRoot}/${EXEC}</file>
<file>${workflowRoot}/twitter.keys</file>
<file>${workflowRoot}/stream.py</file>

Una volta chiuso lelemento della shell, specifichiamo nuovamente cosa fare a
seconda che lazione sia stata completata con successo o meno. Poich
MapReduce usato per lesecuzione dei job, la maggioranza dei tipi di nodi ha
per definizione una logica interna di riprova e ripristina, anche se questo non
vale per i nodi shell:
</shell>
<ok to=end/>
<error to=fail/>
</action>
Se il flusso fallisce, va eliminato. Il tipo di nodo kill fa proprio questo:
impedisce che il workflow passi alla fase successiva, registrando man mano dei
messaggi derrore. Ecco luso tipico di questo nodo:
<kill name=fail>
<message>Shell action failed, error
message[${wf:errorMessage(wf:lastErrorNode())}]</message>
</kill>

Il nodo end, invece, ferma semplicemente il flusso e lo registra come completato


con successo in Oozie:
<end name=end/>
</workflow-app>

Ci si potrebbe chiedere cosa rappresentano le variabili precedenti e da dove


traggono i loro valori. Queste variabili sono esempi dellExpression Language
(EL) di Oozie.
Insieme al file di definizione del workflow (workflow.xml), che descrive i vari
passaggi, dobbiamo creare anche un file di configurazione che fornisca valori
specifici per una data esecuzione nel flusso. Tale separazione tra funzionalit e
configurazione ci permette di scrivere dei workflow che possano essere utilizzati
su cluster diversi, in varie posizioni di file o con valori di variabili differenti
senza dover ricreare il flusso. Per convenzione, questo file si chiama solitamente
job.properties. Vediamone un esempio per il workflow precedente.

Come prima cosa, specifichiamo la posizione del JobTracker, del NameNode e


della coda di MapReduce a cui inviare il flusso. Quanto segue funziona sulla
Cloudera 5.0 QuickStart VM, anche se nella versione 5.1 il nome dellhost stato
modificato in quickstart.cloudera. La cosa importante che gli indirizzi specificati
del NameNode e del JobTracker devono trovarsi nella whitelist di Oozie; i servizi
locali sulla VM vengono aggiunti in automatico:
jobTracker=localhost.localdomain:8032
nameNode=hdfs://localhost.localdomain:8020
queueName=default

Impostiamo poi alcuni valori relativi a dove si possono trovare le definizioni


del flusso e i file associati allinterno del file system HDFS. Notate luso di una
variabile che rappresenta il nome utente che esegue il job. Questo permette che un
unico workflow possa essere applicato a pi percorsi diversi a seconda
dellutente che lo invia:
tasksRoot=book
workflowRoot=${nameNode}/user/${user.name}/${tasksRoot}/v1
oozie.wf.application.path=${nameNode}/user/${user.name}/${tasksRoot}/v1

Ora denominiamo il comando da eseguire nel flusso come ${EXEC}:


EXEC=gettweets.sh

Flussi pi complessi richiedono altri elementi nel file job.properties; il flusso


precedente semplice, quindi va bene cos.
Lo strumento a riga di comando oozie deve sapere dov in esecuzione il server
di Oozie. Linformazione potrebbe essere aggiunta come un argomento a ogni
comando della shell di Oozie, ma la situazione diventerebbe presto ingestibile. In
alternativa potete impostare la variabile dambiente della shell:
$ export OOZIE_URL=http://localhost:11000/oozie

Dopo tutto questo lavoro possiamo finalmente eseguire un workflow di Oozie.


Creiamo una directory in HDFS come specificato nei valori nel file job.properties.
Nel comando precedente, lo abbiamo creato come book/v1 sotto alla nostra
directory home in HDFS. Copiamo i file stream.py, gettweets.sh e twitter.properties in
quella directory; questi sono i file richiesti per avviare lesecuzione vera e propria
del comando della shell. Aggiungiamo poi il file workflow.xml nella stessa directory.
Per avviare il workflow, procediamo come segue:
$ oozie job -run -config <path-to-job.properties>

Se loperazione ha successo, Oozie mostrer a video il nome del job. Potete


impostare lo stato attuale di questo workflow con
$ oozie job -info <job-id>

Potete anche controllare i log per il job:


$ oozie job -log <job-id>

Inoltre, tutti i job correnti e quelli recenti possono essere visualizzati con
$ oozie jobs

Una nota sui permessi dei file di HDFS


Nel comando della shell va tenuto conto di un aspetto insidioso che pu cogliere
di sorpresa i meno esperti. Invece di avere il nodo fs, potremmo includere un
elemento prepare nel nodo shell per creare la directory che ci serve nel file system,
e che ha questo aspetto:
<prepare>
<mkdir path=${nameNode}/tmp/tweets/>
</prepare>

La fase prepare viene svolta dallutente che ha inviato il workflow, ma poich


lesecuzione dello script avviene su YARN, solitamente portata avanti da un
utente di YARN. Potreste avere dei problemi l dove lo script genera i tweet,
viene creata la directory /tmp/tweets in HDFS ma lo script non riesce a ottenere il
permesso per scrivere in essa. Potete risolvere linconveniente assegnando i
permessi con pi precisione o, come mostrato in precedenza, aggiungendo un nodo
al file system per incapsulare le operazioni necessarie. In questo capitolo
utilizzeremo un misto di entrambe le tecniche; per i nodi non shell useremo degli
elementi prepare, soprattutto se la directory richiesta viene manipolata solo da quel
nodo. Quando invece implicato un nodo della shell o quando le directory create
verranno utilizzate su pi nodi, andremo sul sicuro e useremo il nodo pi esplicito
fs.

Facilitare lo sviluppo
A volte pu essere complicato gestire i file e le risorse di un job di Oozie
durante lo sviluppo. Parte di questi elementi deve trovarsi in HDFS mentre altri
devono essere a livello locale, e modificare alcuni file significa doverne
modificare altri. Lapproccio pi semplice consiste nello sviluppare o
nellapportare le modifiche in un clone della directory del workflow sul file
system locale, passandole poi da qui alla directory con nome simile in HDFS,
ovviamente senza dimenticare di sottoporle tutte a un controllo di revisione. In
termini di esecuzione operativa del workflow il file job.properties lunico
elemento che deve essere nel file system locale, mentre tutti gli altri file devono
trovarsi in HDFS. Ricordate che facilissimo modificare la copia locale del
workflow, dimenticarsi di riportare i cambiamenti in HDFS e poi ritrovarsi con un
flusso che non rispecchia le modifiche.

Estrarre i dati e importarli in Hive


Con i dati in HDFS, possiamo estrarre i singoli dataset per i tweet e gli utenti e
posizionarli come fatto nei capitoli precedenti. Possiamo riutilizzare
extract_for_hive.pig per analizzare i tweet JSON grezzi in file separati, salvarli
nuovamente in HDFS e poi procedere importandoli in tabelle di Hive diverse per i
tweet, gli utenti e le informazioni sulla posizione.
Per farlo allinterno di Oozie, dovremo aggiungere due nuovi nodi al workflow,
unazione di Pig per il primo passo e una di Hive per il secondo.
Per lazione di Hive, creeremo tre tabelle esterne che puntano ai file generati da
Pig. In questo modo potremo seguire il modello descritto di importazione dei dati
in tabelle temporanee o esterne, oltre che utilizzare da qui istruzioni INSERT HiveQL
per linserimento in tabelle operazionali e spesso partizionate. Trovate lo script
create.hql alla pagina http://bit.ly/1FK1PXa; qui nella forma semplice:

CREATE DATABASE IF NOT EXISTS twttr ;


USE twttr;
DROP TABLE IF EXISTS tweets;
CREATE EXTERNAL TABLE tweets (
...
) ROW FORMAT DELIMITED
FIELDS TERMINATED BY \u0001
STORED AS TEXTFILE
LOCATION ${ingestDir}/tweets;

DROP TABLE IF EXISTS user;


CREATE EXTERNAL TABLE user (
...
) ROW FORMAT DELIMITED
FIELDS TERMINATED BY \u0001
STORED AS TEXTFILE
LOCATION ${ingestDir}/users;

DROP TABLE IF EXISTS place;


CREATE EXTERNAL TABLE place (
...
) ROW FORMAT DELIMITED
FIELDS TERMINATED BY \u0001
STORED AS TEXTFILE
LOCATION ${ingestDir}/places;

Notate che il separatore di file in ogni tabella impostato esplicitamente per


coincidere con quanto generato in output da Pig. Inoltre, in entrambi i casi le
posizioni (location) sono specificate da variabili per le quali forniremo dei valori
reali nel file job.properties.
Con le istruzioni precedenti, possiamo creare il nodo di Pig per il nostro
workflow che si trova nel codice sorgente come v2 (versione 2) della pipeline. La
definizione del nodo molto simile a quella del nodo shell che abbiamo visto in
precedenza, poich abbiamo impostato gli stessi elementi di configurazione; notate
anche come stato usato prepare per creare la directory di output. Possiamo creare
il nodo di Pig per il workflow come mostrato nellaction seguente:
<action name=pig-node>
<pig>
<job-tracker>${jobTracker}</job-tracker>
<name-node>${nameNode}</name-node>
<prepare>
<delete path=${nameNode}/${outputDir}/>
<mkdir path=${nameNode}/${outputDir}/>
</prepare>
<configuration>
<property>
<name>mapred.job.queue.name</name>
<value>${queueName}</value>
</property>
</configuration>

Analogamente al comando della shell, dobbiamo comunicare allazione di Pig


la posizione dello script di Pig corrente. Questa specificata nellelemento script
seguente:
<script>${workflowRoot}/pig/extract_for_hive.pig</script>

Dobbiamo anche modificare la riga di comando usata per invocare lo script di


Pig per aggiungere i vari parametri. Lo faranno gli elementi che seguono; notate il
pattern di costruzione, in cui un elemento aggiunge il nome del parametro vero e
proprio e quello successivo il suo valore (nel prossimo paragrafo vedremo un
meccanismo alternativo per passare gli argomenti):
<argument>-param</argument>
<argument>inputDir=${inputDir}</argument>
<argument>-param</argument>
<argument>outputDir=${outputDir}</argument>
</pig>

Visto che vogliamo passare da questa fase al nodo di Hive, dovremo impostare i
seguenti elementi nel modo opportuno:
<ok to=hive-node/>
<error to=fail/>
</action>

Lazione di Hive leggermente diversa dai nodi precedenti; inizia nella stessa
maniera, ma poi specifica il suo namespace, come segue:
<action name=hive-node>
<hive xmlns=uri:oozie:hive-action:0.2>
<job-tracker>${jobTracker}</job-tracker>
<name-node>${nameNode}</name-node>

Lazione necessita di molti degli elementi di configurazione usati da Hive


stesso; nella maggior parte dei casi, copiamo il file hive-site.xml nella directory del
workflow e specifichiamo la sua posizione, come nel prossimo xml. Notate che
questo meccanismo non specifico di Hive, e pu essere utilizzato anche per le
azioni personalizzate:
<job-xml>${workflowRoot}/hive-site.xml</job-xml>
Potrebbe anche essere necessario loverriding di alcune propriet predefinite di
MapReduce, come mostrato di seguito, dove specifichiamo che per il nostro job
deve essere applicata una compressione intermedia:
<configuration>
<property>
<name>mapred.compress.map.output</name>
<value>true</value>
</property>
</configuration>

Una volta configurato lambiente di Hive, specifichiamo la posizione dello


script di Hive:
<script>${workflowRoot}/hive/create.hql</script>

Dobbiamo anche fornire il meccanismo per passare gli argomenti allo script di
Hive. Tuttavia, invece di costruire la riga di comando un componente alla volta,
aggiungeremo alcuni elementi param che mapperanno il nome di un elemento di
configurazione nel file job.properties sulle variabili indicate nello script; questo
meccanismo supportato anche con le azioni di Pig:
<param>dbName=${dbName}</param>
<param>ingestDir=${ingestDir}</param>
</hive>

Il nodo di Hive si chiude poi come gli altri:


<ok to=end/>
<error to=fail/>
</action>

Ora dobbiamo assemblare il tutto per eseguire il workflow in pi fasi in Oozie.


Il file workflow.xml completo si trova alla pagina http://bit.ly/1LzP6v8. La Figura 8.1
mostra una rappresentazione grafica del workflow.
Figura 8.1 Workflow di importazione dei dati (v2).

Questo workflow esegue tutti i passi descritti; genera i dati dei tweet, estrae dei
sottoinsiemi di dati attraverso Pig e poi li porta in Hive.

Una nota sulla struttura delle directory del workflow


Ora la nostra directory contiene diversi file; quindi preferibile adottare alcune
convenzioni per la struttura e la denominazione. Per il workflow corrente, la
nostra directory in HDFS avr questo aspetto:
/hive/
/hive/create.hql
/lib/
/pig/
/pig/extract_for_hive.pig
/scripts/
/scripts/gettweets.sh
/scripts/stream-json-batch.py
/scripts/twitter-keys
/hive-site.xml
/job.properties
/workflow.xml

Il criterio che seguiamo quello di mantenere i file di configurazione nella


directory di alto livello e i file relativi a un certo tipo di azioni in alcune
sottodirectory dedicate. Osservate che utile avere una lib anche se vuota,
perch alcuni tipi di nodi la ricercheranno.
Nella struttura precedente, il file job.properties del nostro job assemblato
risulter come segue:
jobTracker=localhost.localdomain:8032
nameNode=hdfs://localhost.localdomain:8020
queueName=default
tasksRoot=book

workflowRoot=${nameNode}/user/${user.name}/${tasksRoot}/v2
oozie.wf.application.path=${nameNode}/user/${user.name}/${tasksRoot}/v2
oozie.use.system.libpath=true
EXEC=gettweets.sh
inputDir=/tmp/tweets
outputDir=/tmp/tweetdata
ingestDir=/tmp/tweetdata
dbName=twttr

In questo codice abbiamo aggiornato per intero la definizione di workflow.xml per


riportare tutti i passi descritti finora, includendo un nodo fs iniziale per creare la
directory necessaria senza doverci preoccupare dei permessi dellutente.

Introduzione a HCatalog
Se analizziamo bene il nostro workflow, possiamo rilevare uninefficienza nel
modo in cui usa DFS come interfaccia tra Pig e Hive. Dobbiamo produrre in output
il risultato dello script di Pig in HDFS, dove lo script di Hive potr poi utilizzarlo
come posizione delle nuove tabelle. Emerge cos che spesso molto utile che i
dati siano salvati in Hive, ma che questa anche una limitazione, perch sono
pochi gli strumenti che possono accedere al suo metastore e da qui leggere e
scrivere i dati. Se ci pensiamo, Hive ha due livelli principali: i suoi strumenti per
accedere ai dati e manipolarli e il framework per eseguire delle query su di essi.
Il sottoprogetto HCatalog di Hive fornisce unimplementazione indipendente del
primo di questi livelli, cio il mezzo per accedere e manipolare i dati nel
metastore di Hive. HCatalog offre dei meccanismi perch altri strumenti, come Pig
e MapReduce, leggano e scrivano nativamente dati strutturati in tabelle che sono
salvate in HDFS.
Ricordate per che i dati in HDFS sono salvati nei formati pi diversi. Il
metastore di Hive fornisce i modelli per astrarre questi file da Hive nella struttura
della tabella relazionale che ci familiare. Quando salviamo i dati in HCatalog,
quindi, in realt li salviamo in HDFS in modo tale che possano essere esposti
dalle strutture tabellari specificate nel metastore di Hive. Per contro, quando
facciamo riferimento ai dati di Hive, parliamo di dati i cui metadati si trovano nel
metastore di Hive, e che sono accessibili attraverso uno strumento che riconosce i
metastore, come HCatalog.

Utilizzare HCatalog
Lo strumento a riga di comando HCatalog viene chiamato hcat ed preinstallato
sulla Cloudera QuickStart VM, oltre che con qualsiasi versione di Hive successiva
alla 0.11 compresa.
Lutility hcat non prevede una modalit interattiva, quindi in genere si utilizza
con argomenti espliciti a riga di comando o puntandola a un file di comandi, come
segue:
$ hcat e use default; show tables
$ hcat f commands.hql

Sebbene HCatalog sia utile e possa essere incorporato negli script, quello per
cui ci interessa di pi in questo contesto la sua integrazione con Pig. HCatalog
definisce un nuovo loader Pig chiamato HCatLoader e uno storer chiamato HCatStorer.
Come si pu intuire dai loro nomi, questi due strumenti permettono che gli script di
Pig vengano letti e scritti direttamente in tabelle di Hive. Possiamo sfruttare questo
meccanismo per sostituire le azioni di Pig e Hive viste in precedenza nel
workflow di Oozie con ununica azione di Pig basata su HCatalog che scrive
loutput del job di Pig direttamente nelle tabelle di Hive.
Per chiarezza, creeremo tre nuove tabelle chiamate tweets_hcat, places_hcat e
users_hcat in cui inseriremo questi dati (notate che non sono pi tabelle esterne):

CREATE TABLE tweets_hcat


CREATE TABLE places_hcat
CREATE TABLE users_hcat

Se questi comandi fossero in un file di script, potremmo usare la CLI di HCat


per eseguirle, cos:
$ hcat f create.hql

La CLI di HCat tuttavia, non offre una shell interattiva simile alla CLI di Hive.
Ora possiamo usare il nostro script di Pig precedente, e ci baster modificare i
comandi di storage, sostituendo luso di PigStorage con HCatStorer. Lo script
aggiornato, extract_to_hcat.pig, includer quindi comandi store come questo:
store tweets_tsv into twttr.tweets_hcat using org.apache.hive.hcatalog.pig.HCatStorer();

Osservate che il nome del package per la classe HCatStorer ha il prefisso


org.apache.hive.hcatalog; quando HCatalog era ancora nellincubatore di Apache,
usava come prefisso org.apache.hcatalog; questa vecchia forma ora deprecata,
mentre andrebbe utilizzata una nuova forma che indica esplicitamente che
HCatalog un sottoprogetto di Hive.
Con questo nuovo script, posiamo sostituire le azioni precedenti di Pig e Hive
con unazione di Pig aggiornata usando HCatalog. In questa occasione utilizzeremo
per la prima volta la sharelib di Oozie, di cui parleremo nel prossimo paragrafo.
Nella definizione del workflow, lelemento pig di questa azione verr definito
come mostrato nel prossimo listato e appare come v3 della pipeline nel bundle
sorgente; nella versione 3, abbiamo anche aggiunto un nodo di utility di Hive da
eseguire prima del nodo di Pig per garantire che ci siano tutte le tabelle necessarie
prima che venga eseguito lo script di Pig che le richiede:
<pig>
<job-tracker>${jobTracker}</job-tracker>
<name-node>${nameNode}</name-node>
<job-xml>${workflowRoot}/hive-site.xml</job-xml>
<configuration>
<property>
<name>mapred.job.queue.name</name>
<value>${queueName}</value>
</property>
<property>
<name>oozie.action.sharelib.for.pig</name>
<value>pig,hcatalog</value>
</property>
</configuration>
<script>${workflowRoot}/pig/extract_to_hcat.pig
</script>
<argument>-param</argument>
<argument>inputDir=${inputDir}</argument>
</pig>

Le due modifiche pi importanti sono laggiunta del riferimento esplicito al file


hive-site.xml (come richiesto da HCatalog) e il nuovo elemento di configurazione

che dice a Oozie di includere i JAR di HCatalog necessari.

La sharelib di Oozie
Lultima aggiunta apre le porte a un aspetto importante di Oozie di cui finora non
abbiamo parlato: la sua sharelib. Quando Oozie esegue i vari tipi di azione,
necessita di diversi JAR per poter accedere ad Hadoop e invocare vari strumenti,
tra cui Hive e Pig. Come parte dellinstallazione di Oozie, in HDFS si trova un
gran numero di JAR dipendenti che verranno utilizzati da Oozie e dai vari tipi di
azione, e che costituiscono la sharelib.
Per la maggior parte dei casi sufficiente sapere che la sharelib esiste,
solitamente nella directory /user/oozie/share/lib in HDFS, e conoscere quando
devono essere aggiunti dei valori di configurazione espliciti (come nellesempio
precedente). Quando si usa unazione di Pig, verranno prelevati automaticamente i
JAR di Pig, ma quando lo script di Pig usa qualcosa come HCatalog, la
dipendenza non risulter nota a Oozie.
La CLI di Oozie consente la manipolazione della sharelib (ma questo va oltre
lambito del libro). Il comando che segue, per esempio, pu rivelarsi utile per
vedere quali sono i componenti inclusi nella sharelib di Oozie:
$ oozie admin -shareliblist

Il prossimo comando serve invece per vedere i singoli JAR che comprendono
un determinato componente nella sharelib, in questo caso HCatalog:
$ oozie admin -shareliblist hcat

Questi comandi tornano comodi per verificare che i JAR richiesti siano stati
inclusi e per vedere quali versioni specifiche sono utilizzate.

HCatalog e le tabelle partizionate


Se eseguite il workflow precedente una seconda volta, fallir; se controllate i
log, vedrete che HCatalog obietta di non riuscire a scrivere in una tabella che
contiene gi dei dati. Questo uno dei limiti pi comuni di HCatalog, che vede di
default le tabelle e le partizioni come immutabili. Hive, invece, aggiunger i nuovi
dati a una tabella o partizione, quindi di default considera una tabella come
passibile di mutamenti.
Sviluppi futuri di Hive e HCatalog prevedranno il supporto di una nuova
propriet di tabella che controller tale comportamento nei due strumenti; per
esempio, la prossima riga inserita in una definizione di tabella consente delle
aggiunte alla tabella come avviene oggi in Hive:
TBLPROPERTIES(immutable=false)

Questo meccanismo non per attualmente disponibile nella versione di Hive e


HCatalog. Per far s che un workflow aggiunga sempre pi dati nelle nostre
tabelle, dovremo creare una nuova partizione per ogni esecuzione sul flusso.
Abbiamo apportato questa modifica nella v4, dove come prima cosa ricreiamo le
tabelle con una chiave di partizione di integer:
CREATE TABLE tweets_hcat (
)
PARTITIONED BY (partition_key int)
ROW FORMAT DELIMITED
FIELDS TERMINATED BY \u0001
STORED AS SEQUENCEFILE;

CREATE TABLE places_hcat(


)
partitioned by(partition_key int)
ROW FORMAT DELIMITED
FIELDS TERMINATED BY \u0001
STORED AS SEQUENCEFILE
TBLPROPERTIES(immutable=false) ;

CREATE TABLE users_hcat(


)
partitioned by(partition_key int)
ROW FORMAT DELIMITED
FIELDS TERMINATED BY \u0001
STORED AS SEQUENCEFILE
TBLPROPERTIES(immutable=false) ;

di Pig prende una definizione di partizione facoltativa, e noi


HCatStorer

modifichiamo di conseguenza le istruzioni store nello script di Pig script; per


esempio:
store tweets_tsv into twttr.tweets_hcat
using org.apache.hive.hcatalog.pig.HCatStorer(
partition_key=$partitionKey);

Modifichiamo quindi la nostra azione di Pig nel file workflow.xml per includere
questaltro parametro:
<script>${workflowRoot}/pig/extract_to_hcat.pig</script>
<param>inputDir=${inputDir}</param>
<param>partitionKey=${partitionKey}</param>

La domanda ora come passare la chiave di partizione al workflow. Potremmo


specificarla nel file job.properties, ma cos facendo incontreremmo lo stesso
problema di quando proviamo a scrivere una partizione esistente allesecuzione
successiva.
Passiamo questa riga come argomento esplicito allinvocazione della CLI di
Oozie (vedremo delle alternative migliori successivamente):
$ oozie job run config v4/job.properties DpartitionKey=12345

AT T ENZIONE
Una conseguenza di questo comportamento che la nuova esecuzione di un workflow di HCat
con gli stessi argomenti fallir. Tenetene conto quando testate i workflow o sperimentate con il
codice di esempio del libro.
Produrre dati derivati
Ora che la pipeline principale definita, ci sono alcune azioni che necessario
intraprendere dopo aver aggiunto ogni nuovo dataset. Nel meccanismo precedente
in cui abbiamo aggiunto ciascun set di dati degli utenti in una partizione separata,
la tabella users_hcat contiene gli utenti pi volte. Creiamo allora una tabella per gli
utenti univoci e rigeneriamola ogni volta che inseriamo nuovi dati.

Figura 8.2 Workflow di importazione dei dati (v4).

Considerati i limiti citati di HCatalog, utilizzeremo unazione di Hive per


raggiungere lobiettivo, poich ci occorre sostituire i dati in un tabella.
Per iniziare, creiamo una nuova tabella per le informazioni sugli utenti univoci
come segue:
CREATE TABLE IF NOT EXISTS unique_users(
user_id string ,
name string ,
description string ,
screen_name string )
ROW FORMAT DELIMITED
FIELDS TERMINATED BY \t
STORED AS sequencefile ;

In questa tabella riuniremo solo gli attributi di un utente che non cambia mai
(ID) o che lo fa raramente (il nome dello schermo e cos via). Possiamo scrivere
una semplice istruzione Hive per popolare la tabella partendo dalla tabella
completa users_hcat:
USE twttr;
INSERT OVERWRITE TABLE unique_users
SELECT DISTINCT user_id, name, description, screen_name
FROM users_hcat;

Possiamo poi aggiungere un nodo action di Hive supplementare proveniente dal


precedente nodo di Pig nel workflow. Nel farlo, scopriremo che procedere
denominando i nodi come nodi di Hive una pessima idea, perch adesso ci
ritroviamo con due nodi basati su Hive. Nella v5 del workflow, aggiungiamo
questo nuovo nodo e modifichiamo gli altri perch abbiano dei nomi pi
descrittivi.

Figura 8.3 Workflow di importazione dei dati (v5).

Eseguire pi azioni in parallelo


Il nostro workflow ha due tipi di attivit: limpostazione iniziale con i nodi che
inizializzano il file system e le tabelle di Hive, e i suoi nodi funzionali che
eseguono lelaborazione vera e propria. Se osserviamo i due dati di impostazione
in uso, appare chiaro che sono piuttosto diversi e non interdipendenti. In questo
caso possiamo approfittare della funzione di Oozie dei nodi fork e join per eseguire
queste azioni in parallelo. Linizio del file workflow.xml diventa questo:
<start to=setup-fork-node/>
Il nodo fork di Oozie contiene alcuni elementi di path che specificano ciascuno
un nodo di partenza. Verranno tutti avviati in parallelo:
<fork name=setup-fork-node>
<path start=setup-filesystem-node />
<path start=create-tables-node />
</fork>

Ogni singolo nodo di azione specificato non diverso da quelli che abbiamo gi
usato. Un nodo action porta a una serie di altri nodi; lunico requisito che ogni
serie parallela di azioni termini con una transizione al nodo join associato al nodo
fork, come segue:

<action name=setup-filesystem-node>

<ok to=setup-join-node/>
<error to=fail/>
</action>
<action name=create-tables-node>

<ok to=setup-join-node/>
<error to=fail/>
</action>

Il nodo join stesso agisce da punto di coordinamento; qualsiasi workflow


completo attender finch tutti i percorsi specificati nel nodo fork raggiungono
questo punto. In quel momento, il workflow proseguir verso il nodo indicato
allinterno del nodo join, che viene utilizzato come segue:
<join name=create-join-node to=gettweets-node/>

In questo codice abbiamo omesso le definizioni dellazione per ragioni di


spazio; il workflow completo nella v6 illustrato nella Figura 8.4.
Figura 8.4 Workflow di importazione dei dati (v6).

Chiamare un workflow secondario


Sebbene il meccanismo fork/join renda molto pi efficiente il meccanismo delle
azioni parallele, comporta una certa prolissit quando lo includiamo nella
definizione del workflow.xml principale. Da un punto di vista concettuale, abbiamo
una serie di azioni che eseguono alcune attivit correlate richieste dal workflow
senza necessariamente farne parte. Per casi come questi, Oozie offre la possibilit
di invocare un workflow secondario. Il flusso genitore eseguir il flusso figlio e
aspetter che sia stato completato, potendo passare gli elementi di configurazione
da un flusso allaltro.
Il flusso figlio un workflow a tutti gli effetti, contenuto solitamente in una
directory in HDFS con la struttura tipica di questo elemento, e cio il file
workflow.xml principale, tutti i file di Hive Pig e altri file eventualmente richiesti.

Possiamo creare una nuova directory in HDFS chiamata setup-workflow, e al


suo interno creare i file necessari solo per il nostro file system e per le azioni di
creazione di Hive. Il file di configurazione del flusso secondario avr questo
aspetto:
<workflow-app xmlns=uri:oozie:workflow:0.4 name=create-workflow>
<start to=setup-fork-node/>
<fork name=setup-fork-node>
<path start=setup-filesystem-node />
<path start=create-tables-node />
</fork>
<action name=setup-filesystem-node>

</action>
<action name=create-tables-node>

</action>
<join name=create-join-node to=end/>
<kill name=fail>
<message>Action failed, error
message[${wf:errorMessage(wf:lastErrorNode())}]</message>
</kill>
<end name=end/>
</workflow-app>

Modifichiamo poi i primi nodi del workflow principale in modo da utilizzare un


workflow secondario:
<start to=create-subworkflow-node/>
<action name=create-subworkflow-node>
<sub-workflow>
<app-path>${subWorkflowRoot}</app-path>
<propagate-configuration/>
</sub-workflow>
<ok to=gettweets-node/>
<error to=fail/>
</action>

Specifichiamo il subWorkflowPath nelle job.properties del flusso genitore, e


lelemento propagate-configuration passer la configurazione del flusso genitore al
figlio.

Aggiungere impostazioni globali


Estraendo i nodi di utility in workflow secondari, possiamo ridurre
notevolmente la complessit e la confusione nella definizione del workflow
principale. Nella v7 della nostra pipeline, effettueremo unulteriore
semplificazione e aggiungeremo una sezione di configurazione global:
<workflow-app xmlns=uri:oozie:workflow:0.4 name=v7>
<global>
<job-tracker>${jobTracker}</job-tracker>
<name-node>${nameNode}</name-node>
<job-xml>${workflowRoot}/hive-site.xml</job-xml>
<configuration>
<property>
<name>mapred.job.queue.name</name>
<value>${queueName}</value>
</property>
</configuration>
</global>
<start to=create-subworkflow-node/>

Grazie a questa sezione, non dovremo pi specificare i valori nei nodi di Hive e
Pig nel resto del workflow (notate che in questo momento il nodo shell non
supporta il meccanismo della configurazione globale). Questo permette di
semplificare notevolmente alcuni dei nostri nodi. Per esempio, il nodo di Pig si
presenta cos:
<action name=hcat-ingest-node>
<pig>
<configuration>
<property>
<name>oozie.action.sharelib.for.pig</name>
<value>pig,hcatalog</value>
</property>
</configuration>
<script>${workflowRoot}/pig/extract_to_hcat.pig</script>
<param>inputDir=${inputDir}</param>
<param>dbName=${dbName}</param>
<param>partitionKey=${partitionKey}</param>
</pig>
<ok to=derived-data-node/>
<error to=fail/>
</action>
Possiamo aggiungere altri elementi di configurazione o effettuare loverriding di
quelli specificati nella sezione global, ottenendo una definizione molto pi chiara
che si concentra solo sulle informazioni specifiche per lazione in questione. Nel
v7 del workflow sono stati inseriti sia la sezione global sia un workflow
secondario, e la leggibilit ne guadagna.
Le sfide dei dati esterni
Quando ci affidiamo a dati esterni per la nostra applicazione, dipendiamo
implicitamente dalla loro qualit e stabilit. Ovviamente questo vale sempre, ma
nel caso di dati generati da fonti terze su cui non abbiamo alcun controllo, i rischi
sono pi elevati.
Dobbiamo quindi pensare a come limitarli quando costruiamo applicazioni che
dovrebbero essere affidabili basandoci su quei dati, e soprattutto quando il volume
di questi cresce.

Figura 8.5 Workflow di importazione dei dati (v7).

Validazione dei dati


Lespressione generica validazione dei dati si riferisce allassicurare che i dati
in entrata assecondino le nostre aspettative e alleventuale loro normalizzazione
per modificare o eliminare linput corrotto o mal formattato. Ci che questo
significa dipende molto dallapplicazione. A volte, per esempio, la cosa
importante garantire che il sistema importi solo quei dati conformi a un dato
criterio di precisione e pulizia. Nel caso dei dati dei tweet, non siamo interessati a
ogni singolo record, e potremmo adottare come parametro quello di rifiutare i
record che non contengono valori in alcuni campi che ci interessano. In altre
applicazioni, invece, potrebbe essere fondamentale catturare ogni record immesso,
e questo porta allimplementazione di una logica che riformatta ciascun record in
modo che soddisfi i requisiti. In altri casi ancora, verranno importati solo i record
corretti, ma quelli rimanenti, invece di essere eliminati, potranno essere salvati da
qualche parte per unanalisi successiva.
Cercare di definire un approccio generico alla validazione dei dati va oltre gli
scopi di questo libro. Possiamo per indicarvi dove incorporare i vari tipi di
logica di validazione nella pipeline.

Azione di validazione
La logica per effettuare la validazione o il cleanup necessari pu essere
incorporata direttamente in altre azioni. A un nodo shell che esegue uno script di
raccolta dati si possono aggiungere dei comandi specifici per gestire in modo
diverso i record mal formattati. Le azioni di Pig e Hive che caricano i dati nelle
tabelle possono applicare un filtro durante limportazione (riesce meglio in Pig) o
aggiungere delle precisazioni quando si copiano i dati da una tabella di
importazione allo store operativo.
Esiste anche un argomento per aggiungere un nodo di validazione nel workflow,
anche se inizialmente non esegue una logica vera e propria. Potrebbe essere per
esempio unazione di Pig che legge i dati, applica la validazione e scrive i dati
validati in una nuova posizione dove possa essere letta dai nodi successivi. Il
vantaggio che in seguito si potr aggiornare la logica di validazione senza
modificare le altre azioni, riducendo cos il rischio di interrompere
accidentalmente la pipeline e rendendo i nodi meglio definiti per quanto riguarda
le responsabilit. Ne consegue che anche un workflow secondario di validazione
un modello, poich non solo fornisce una separazione tra le responsabilit, ma
rende la logica di validazione pi facile da testare e aggiornare.
Lovvio svantaggio di questo approccio che comporta unulteriore
elaborazione e un altro ciclo di lettura dei dati e di loro riscrittura, il che contrasta
con uno dei vantaggi che abbiamo evidenziato quando abbiamo parlato delluso di
HCatalog da Pig.
una questione di compromesso fra prestazioni e complessit e manutenzione
del workflow. Quando prendete in considerazione come effettuare la validazione e
cosa questa significa per il vostro workflow, valutate tutti questi elementi prima di
scegliere unimplementazione invece di unaltra.

Gestire le modifiche al formato


Il fatto di avere i dati che fluiscono nel nostro sistema e che siano validati non
sufficiente per cantare vittoria. Soprattutto se provengono da una fonte esterna,
dovremo pensare a come la loro struttura potrebbe cambiare nel tempo.
Ricordate che sistemi come Hive applicano lo schema della tabella solo quando
i dati vengono letti. C un notevole vantaggio nel consentire uno storage e
unimportazione dei dati flessibili, ma questo pu anche portare a query o a carichi
di lavoro che falliscono improvvisamente se i dati importati non soddisfano pi le
interrogazioni a cui sono stati sottoposti. Un database relazionale, che applica gli
schemi in scrittura, non permette che tali dati vengano importati nel sistema.
Per gestire le modifiche subite dal formato dei dati occorre rielaborare i dati
esistenti nel nuovo formato, unoperazione pi che fattibile sui dataset pi piccoli
ma pressoch impossibile con i volumi tipici dei grandi cluster Hadoop.

Gestire levoluzione dello schema con Avro


Nella sua integrazione con Hive, Avro ha alcune funzioni che ci aiutano a
risolvere il problema. Nella tabella per i dati dei tweet, possiamo rappresentare la
struttura del record di un tweet con il seguente schema di Avro:
{
namespace: com.learninghadoop2.avrotables,
type:record,
name:tweets_avro,
fields:[
{name: created_at, type: [null ,string]},
{name: tweet_id_str, type: [null,string]},
{name: text,type:[null,string]},
{name: in_reply_to, type: [null,string]},
{name: is_retweeted, type: [null,string]},
{name: user_id, type: [null,string]},
{name: place_id, type: [null,string]}
]
}

Creiamo lo schema precedente in un file chiamato tweets_avro.avsc (.avsc


lestensione di file standard per gli schemi di Avro), quindi collochiamolo in
HDFS; potrebbe essere una buona idea destinare ununica posizione ai file degli
schemi, per esempio in /schema/avro.
Con questa definizione, possiamo creare una tabella di Hive che utilizza tale
schema per la sua specifica:
CREATE TABLE tweets_avro
PARTITIONED BY ( partition_key int)
ROW FORMAT SERDE
org.apache.hadoop.hive.serde2.avro.AvroSerDe
WITH SERDEPROPERTIES (
avro.schema.url=hdfs://localhost.localdomain:8020/schema/avro/tweets_avro.avsc
)
STORED AS INPUTFORMAT
org.apache.hadoop.hive.ql.io.avro.AvroContainerInputFormat
OUTPUTFORMAT
org.apache.hadoop.hive.ql.io.avro.AvroContainerOutputFormat;

Osserviamola dallinterno di Hive (o di HCatalog, che supporta anchesso tali


definizioni):
describe tweets_avro
OK
created_at string from deserializer
tweet_id_str string from deserializer
text string from deserializer
in_reply_to string from deserializer
is_retweeted string from deserializer
user_id string from deserializer
place_id string from deserializer
partition_key int None

Possiamo utilizzare questa tabella come qualsiasi altra, per esempio per copiare
nella tabella di Avro i dati provenienti dalla partizione 3 della tabella non di Avro:
SET hive.exec.dynamic.partition.mode=nonstrict
INSERT INTO TABLE tweets_avro
PARTITION (partition_key)
SELECT FROM tweets_hcat

NOTA
Come negli esempi precedenti, se nel percorso della classe non sono presenti delle
dipendenze Avro, dovremo aggiungere il JAR di MapReduce di Avro al nostro ambiente prima di
poter compiere qualsiasi selezione dalla tabella.

Abbiamo cos una nuova tabella di tweet specificata da uno schema di Avro, che
ora come ora assomiglia a una tabella qualsiasi. Quello che ci interessa qui, per,
come sfruttare il meccanismo di Avro per gestire levoluzione dello schema.
Aggiungiamo un nuovo campo:
{
namespace: com.learninghadoop2.avrotables,
type:record,
name:tweets_avro,
fields:[
{name: created_at, type: [null ,string]},
{name: tweet_id_str, type: [null,string]},
{name: text,type:[null,string]},
{name: in_reply_to, type: [null,string]},
{name: is_retweeted, type: [null,string]},
{name: user_id, type: [null,string]},
{name: place_id, type: [null,string]},
{name: new_feature, type: string, default: wow!}
]
}

Grazie a questo nuovo schema, possiamo confermare che stata aggiornata


anche la definizione della tabella:
describe tweets_avro;
OK
created_at string from deserializer
tweet_id_str string from deserializer
text string from deserializer
in_reply_to string from deserializer
is_retweeted string from deserializer
user_id string from deserializer
place_id string from deserializer
new_feature string from deserializer
partition_key int None

Senza aggiungere dati, possiamo eseguire su questo nuovo campo delle query
che restituiranno il valore predefinito per i dati esistenti:
SELECT new_feature FROM tweets_avro LIMIT 5;
...
OK
wow!
wow!
wow!
wow!
wow!

Ancora pi rimarchevole il fatto che la nuova colonna non devessere aggiunta


per forza alla fine, ma pu trovarsi in un punto qualsiasi del record. Grazie a
questo meccanismo, possiamo aggiornare gli schemi di Avro in modo che
rappresentino la nuova struttura dei dati e riportare automaticamente queste stesse
modifiche nelle definizioni delle tabelle di Hive. Qualsiasi query che far
riferimento alla nuova colonna recuperer il valore di default per tutti i dati
esistenti in cui non presente quel campo.
Notate che il meccanismo di default che stiamo usando qui centrale in Avro e
non specifico in Hive. Avro un formato molto potente e flessibile che ha
numerose applicazioni in varie aree, e merita un ulteriore approfondimento che
non possiamo darvi qui.
Tecnicamente, viene fornita la cosiddetta compatibilit retroattiva. Se vero
che possiamo modificare il nostro schema e far s che i dati esistenti siano
comunque compatibili con la nuova struttura, non possiamo tuttavia continuare a
importare i date nel vecchio formato allinterno di tabelle aggiornate:
INSERT INTO TABLE tweets_avro
PARTITION (partition_key)
SELECT * FROM tweets_hcat;
FAILED: SemanticException [Error 10044]: Line 1:18 Cannot insert into target table because
column number/types are different tweets_avro: Table insclause-0 has 8 columns, but query
has 7 columns.

Il supporto dellevoluzione dello schema con Avro premette che le modifiche ai


dati siano affrontabili come parte della normale attivit invece che come
unemergenza da affrontare frequentemente. Ma tutto questo ha un prezzo: le
modifiche devono comunque avvenire nella pipeline ed essere poi convogliate in
produzione. Le tabelle di Hive che forniscono la compatibilit retroattiva
permettono tuttavia una procedura in passi pi gestibili. Se cos non fosse,
dovreste sincronizzare ogni cambiamento in ciascuna fase della pipeline.
Se le modifiche avvengono dal momento dellimportazione fino al punto in cui
vengono inserite in tabelle di Hive supportate da Avro, allora tutti gli utenti di
queste tabelle potranno rimanere inalterati e continuare a eseguire query sui nuovi
dati (questo a meno che non compiano operazioni come select *, cosa che
comunque sempre una pessima idea). Queste applicazioni possono essere
modificate secondo tempi diversi per quanto riguarda il meccanismo di
importazione. Nella v8 della nostra pipeline, mostriamo come utilizzare le tabelle
Avro allinterno della funzionalit esistente.
NOTA
Hive 0.14 ha appena rilasciato un supporto interno pi ricco ad Avro che potrebbe semplificare
ulteriormente il processo evolutivo degli schemi. Verificate la sua implementazione definitiva
alla pagina https://hive.apache.org/downloads.html.

Ultime considerazioni sulluso dellevoluzione dello schema di Avro


Con questa trattazione di Avro, abbiamo toccato alcuni aspetti di temi molto pi
ampi, in particolare la gestione dei dati su vasta scala e i criteri riguardanti le
versioni e il mantenimento dei dati. Gli scenari variano a seconda
dellorganizzazione, ma esistono alcuni principi generali che possono essere
applicati a tutti i livelli. Vediamoli.

Fate solo modifiche additive


Nellesempio precedente abbiamo parlato dellaggiunta di colonne. Pu
accadere, anche se raramente, che nei vostri dati sorgente non ci siano colonne o
magari che una colonna o pi colonne non vi servano pi. Avro non fornisce
strumenti specifici per queste esigenze. Invece di eliminare le colonne non pi
necessarie, si tende a mantenere i vecchi dati senza utilizzare le colonne vuote nei
nuovi dati. La cosa pi gestibile se si controlla il formato dei dati; se state
importando delle fonti esterne, per poter seguire questo approccio dovrete
rielaborare i dati per rimuovere la colonna vecchia o modificare il meccanismo di
importazione in modo da aggiungere un valore di default per tutti i dati nuovi.

Gestite esplicitamente le versioni dello schema


Negli esempi avevamo un unico file dello schema che abbiamo modificato
direttamente. Lidea non poi cos brillante, perch in questo modo non abbiamo
pi la possibilit di tenere traccia delle modifiche allo schema nel tempo. Oltre a
considerare gli schemi come prodotti di cui controllare la versione (i vostri
schemi non sono anche in Git, giusto?), utile identificare ciascuno di essi con una
versione esplicita, soprattutto se anche i dati in arrivo hanno una versione. Cos,
invece di sovrascrivere il file dello schema esistente, potete aggiungere il nuovo
file e utilizzare unistruzione ALTER TABLE per puntare la definizione della tabella di
Hive al nuovo schema. Ovviamente qui si presuppone che non abbiate la
possibilit di usare una query diversa per i dati vecchi con il formato differente.
Per quanto Hive non disponga di un meccanismo automatico per selezionare uno
schema, possono verificarsi situazioni in cui potete controllare manualmente
questa operazione e aggirare la questione dellevoluzione.

Pensate alla distribuzione dello schema


Quando utilizzate un file di schema, pensate a come verr distribuito ai client.
Se, come nellesempio precedente, il file in HDFS, probabilmente ha pi senso
assegnargli un fattore di replica elevato: il file verr recuperato da ciascun mapper
in ogni job di MapReduce che interroga la tabella.
LURL di Avro pu essere specificato come posizione sul file system locale
(file://), il che pu servire nello sviluppo, e anche come risorsa web (http://). Per
quanto la seconda opzione sia un metodo immediato per distribuire lo schema su
client non Hadoop, ricordate che il carico sul server web potrebbe essere elevato.
Se lhardware moderno e i server web efficienti non ci saranno problemi, ma se
avete un cluster di migliaia di macchine che eseguono numerosi job in parallelo
nei quali ogni mapper deve toccare il server web, pensateci due volte.
Raccogliere dati supplementari
Molti sistemi di elaborazione non attingono a ununica fonte di dati; spesso la
sorgente principale affiancata da alcune fonti secondarie. Vedremo ora come
inglobare il recupero di questi dati di riferimento nel nostro warehouse.
A un livello elevato, il problema non molto diverso dal recuperare i dati
grezzi dei tweet, poich importeremo dei dati da una sorgente esterna, li
elaboreremo in qualche modo e poi li salveremo da qualche parte per poterli
riutilizzare successivamente. Ma questo ci porta a una domanda fondamentale:
dobbiamo recuperare questi dati ogni volta che importiamo dei nuovi tweet?
Assolutamente no. I dati di riferimento cambiano molto raramente, e possiamo
raccoglierli molto meno spesso che non i dati dei nuovi tweet. Ma allora sorge
unaltra domanda: come programmare i workflow di Oozie?

Programmare i workflow
Finora abbiamo eseguito tutti i nostri workflow di Oozie su richiesta dalla CLI.
Oozie dotato di uno scheduler che consente lavvio dei job su una base
temporale oppure quando vengono soddisfatti alcuni criteri esterni come la
comparsa dei dati in HDFS. Una buona soluzione per i nostri workflow potrebbe
essere eseguire la pipeline dei tweet ogni 10 minuti, per esempio, e aggiornare i
dati di riferimento una sola volta al giorno.
AT T ENZIONE
A prescindere da dove vengono recuperati i dati, pensate con attenzione a come gestire i
dataset che eseguono unoperazione di eliminazione/sostituzione. In particolare, non eliminate
prima di recuperare e validare i nuovi dati; se lo fate, i job che richiedono i dati di riferimento
falliranno fino al prossimo recupero che avr successo. Pu essere una buona idea includere
delle operazioni distruttive in un workflow secondario che viene avviato solo a completamento
avvenuto della procedura di recupero.

Oozie definisce attualmente due tipi di applicazioni eseguibili: dei workflow


come quelli che abbiamo utilizzato finora e dei coordinatori, che pianificano i
workflow da eseguire in base a criteri diversi. Un job coordinatore
concettualmente simile agli altri workflow; collochiamo un file di configurazione
XML in HDFS e utilizziamo un file di propriet con parametri per configurarlo
durante il runtime. Inoltre, i job coordinatori possono ricevere una
parametrizzazione supplementare dagli eventi che ne innescano lesecuzione.
Chiariamo con un esempio. Immaginiamo di voler creare un coordinatore che
esegue la v7 del nostro workflow di importazione ogni 10 minuti. Ecco il file
coordinator.xml (il nome standard per la definizione XML del coordinatore):

<coordinator-app name=tweets-10min-coordinator frequency=${freq} start=${startTime}


end=${endTime} timezone=UTC xmlns=uri:oozie:coordinator:0.2>

Il nodo action principale in un coordinatore il workflow, di cui dobbiamo


specificare la posizione nella root in HDFS e tutte le propriet necessarie, cos:
<action>
<workflow>
<app-path>${workflowPath}</app-path>
<configuration>
<property>
<name>workflowRoot</name>
<value>${workflowRoot}</value>
</property>

Dobbiamo anche includere le propriet richieste dalle azioni nel workflow o da


qualsiasi workflow secondario che avvia; questo significa che qui andranno
incluse tutte le variabili definite dallutente presenti in uno qualsiasi dei workflow
da avviare:
<property>
<name>dbName</name>
<value>${dbName}</value>
</property>
<property>
<name>partitionKey</name>
<value>${coord:formatTime(coord:nominalTime(),
yyyyMMddhhmm)}
</value>
</property>
<property>
<name>exec</name>
<value>gettweets.sh</value>
</property>
<property>
<name>inputDir</name>
<value>/tmp/tweets</value>
</property>
<property>
<name>subWorkflowRoot</name>
<value>${subWorkflowRoot}</value>
</property>
</configuration>
</workflow>
</action>
</coordinator-app>

In questo XML abbiamo usato alcune funzioni specifiche del coordinatore.


Notate lindicazione dellora di inizio e di fine del coordinatore e la sua frequenza
(in minuti). Qui stiamo usando la forma pi semplice, ma Oozie dispone di un set
di funzioni che consentono ulteriori precisazioni sulla frequenza.
Usiamo le funzioni EL del coordinatore nella definizione della variabile
partitionKey. In precedenza, quando abbiamo eseguito i workflow dalla CLI, le

abbiamo specificate esplicitamente, ma avevamo detto che cera un modo migliore


per farlo: questo. La prossima espressione genera un output formattato che
contiene lanno, il mese, il giorno, lora e il minuto:
${coord:formatTime(coord:nominalTime(), yyyyMMddhhmm)}

Se utilizziamo queste informazioni come valore per la chiave di partizione,


possiamo essere certi che ogni invocazione del workflow crei in modo corretto
una partizione univoca nelle tabelle di HCatalog.
Le job.properties corrispondenti per il job coordinatore assomigliano ai file di
configurazione precedenti con le solite immissioni per il NameNode e variabili
analoghe, oltre ad avere valori per variabili specifiche per lapplicazione, come
dbName. Inoltre, dovremo indicare la root della posizione del coordinatore in HDFS,

come segue:
oozie.coord.application.path=${nameNode}/user/${user.name}/${tasksRoot}/tweets_10min

Notate che il prefisso del namespace oozie.coord invece delloozie.wf usato in


precedenza. Con la definizione del coordinatore in HDFS, possiamo inviare il file
a Oozie come con i job gi visti. In questo caso, per, il job verr eseguito solo
per un dato periodo di tempo, in particolare ogni cinque minuti (la frequenza
variabile) quando il clock di sistema tra la startTime e lendTime.
Trovate la configurazione completa nella directory tweets_10min nel codice
sorgente di questo capitolo.

Altri trigger di Oozie


Il coordinatore precedente ha un trigger molto semplice: parte periodicamente
entro un intervallo temporale specificato. Oozie ha lulteriore caratteristica dei
dataset, grazie alla quale pu essere avviato in base alla disponibilit dei nuovi
dati.
Non si adatta particolarmente al modo in cui abbiamo definito la nostra pipeline
finora, ma immaginiamo che, invece di avere il workflow che raccoglie i tweet
come prima cosa, un sistema esterno collochi regolarmente i nuovi file di tweet in
HDFS. Oozie pu essere configurato in modo da cercare la presenza di nuovi dati
in base a un pattern di directory o per innescare un dato trigger quando un file
pronto in HDFS. Questultima configurazione fornisce un meccanismo molto
comodo con cui integrare loutput dei job di MapReduce, che di default scrivono
un file _SUCCESS nella loro directory di output.
I dataset di Oozie sono verosimilmente una delle parti pi potenti dellintero
sistema, ma per ragioni di spazio non possiamo approfondirle oltre. Vi
consigliamo quindi di consultare la home page di Oozie per ulteriori informazioni.
Assemblare il tutto
Ripercorriamo quanto discusso finora e rivediamo come possiamo utilizzare
Oozie per costruire una serie sofisticata di workflow che implementino un
approccio alla gestione del ciclo di vita dei dati assemblando tutte le tecniche
trattate.
Come prima cosa, importante definire delle responsabilit chiare e
implementare parti del sistema attraverso una buona progettazione e una
separazione dei principi. Applicando questo metodo, si ottengono diversi
workflow.
Un workflow secondario per garantire che lambiente (perlopi HDFS e
metadati di Hive) sia configurato correttamente.
Un workflow secondario per effettuare la validazione dei dati.
Il workflow principale che avvia i due workflow secondari precedenti e poi
recupera nuovi dati attraverso una pipeline di importazione in pi passi.
Un coordinatore che esegue i workflow precedenti ogni 10 minuti.
Un secondo coordinatore che importa i dati di riferimento utili per la pipeline
dellapplicazione.
Definiamo anche tutte le tabelle con gli schema di Avro e usiamole ogni volta
che possibile per favorire la gestione dellevoluzione degli schemi e il
cambiamento dei formati dei dati nel tempo.
Trovate il codice completo di questi componenti nella versione finale del
workflow nei sorgenti relativi a questo capitolo.

Altri strumenti di supporto


Sebbene Oozie sia uno strumento molto potente, a volte pu essere difficile
scrivere correttamente i file di definizione del workflow. Con il crescere delle
dimensioni delle pipeline, gestire la complessit diventa una sfida anche con una
buona suddivisione dei workflow. A un livello pi semplice, XML non mai
troppo divertente da scrivere per un umano, ma alcuni strumenti possono aiutarci.
Hue, lo strumento che si autodefinisce linterfaccia utente di Hadoop
(http://gethue.com/), fornisce alcuni elementi grafici per comporre, eseguire e gestire
i workflow di Oozie. Non per uno strumento per principianti; ne parleremo nel
Capitolo 11.
Falcon (http://falcon.incubator.apache.org) un nuovo progetto di Apache di un
certo interesse. Utilizza Oozie per costruire un intervallo di flussi di dati e azioni a
livello pi elevato, per esempio fornisce le indicazioni per abilitare e garantire la
replica cross-site tra pi cluster Hadoop. Il team di Falcon sta lavorando sul
miglioramento delle interfacce per costruire i workflow, quindi vale la pena tenere
docchio levoluzione del progetto.
Riepilogo
Lo scopo di questo capitolo era quello di presentare largomento della gestione
del ciclo di vita dei dati come qualcosa di differente da un concetto astratto. In
particolare, abbiamo trattato quanto segue.
La definizione di gestione del ciclo di vita dei dati e come questa copre
diversi aspetti e tecniche che diventano particolarmente importanti quando si
lavora con grossi volumi di dati.
Il concetto di costruzione di pipeline per limportazione dei dati insieme ai
principi di gestione del ciclo di vita dei dati, che possono poi essere utilizzati
da strumenti analitici di livello pi alto.
Oozie come manager dei workflow centrati su Hadoop e come possiamo
impiegarlo per assemblare una serie di azioni in un workflow unificato.
Alcuni strumenti di Oozie come i workflow secondari, lesecuzione
parallela delle azioni e le variabili globali che ci consentono di applicare
principi di vero design ai nostri workflow.
HCatalog e i mezzi diversi da Hive che fornisce per leggere e scrivere dati
strutturati in tabelle; abbiamo illustrato le sue potenzialit e lintegrazione
con strumenti come Pig, evidenziandone al contempo i punti deboli.
Avro come strumento privilegiato per gestire levoluzione degli schemi nel
tempo.
Luso dei coordinatori di Oozie per costruire workflow programmati in base
a intervalli di tempo o alla disponibilit dei dati per guidare lesecuzione di
pi pipeline di importazione.
Altri strumenti che possono facilitare le varie operazioni, per esempio Hue e
Falcon.
Nel prossimo capitolo compiremo una panoramica tra alcuni strumenti analitici
di livello pi elevato e framework che possono costruire una logica di
applicazione sofisticata sui dati raccolti in una pipeline di importazione.
Capitolo 9
Facilitare il lavoro di sviluppo

In questo capitolo vedremo come semplificare lo sviluppo delle applicazioni in


Hadoop, a seconda dei casi duso e degli obiettivi, utilizzando alcune astrazioni e
framework costruiti sulle API Java. In particolare, tratteremo quanto segue.
Come lAPI di streaming consente di scrivere job di MapReduce usando
linguaggi dinamici come Python e Ruby.
In che modo dei framework come Apache Crunch e Kite Morphlines
permettono di costruire delle pipeline di trasformazione dei dati attraverso
astrazioni di livello pi elevato.
Come Kite Data, un framework promettente sviluppato da Cloudera, ci d la
possibilit di applicare dei pattern di design e degli standard che agevolano
lintegrazione e linteroperabilit dei diversi componenti dellecosistema di
Hadoop.
Scegliere un framework
Nei capitoli precedenti, abbiamo utilizzato le API di programmazione di
MapReduce e Spark per scrivere applicazioni distribuite. Molto potenti e
flessibili, queste API hanno un certo livello di complessit, e in genere richiedono
un tempo di sviluppo significativo.
Per ridurre la prolissit, abbiamo introdotto i framework Pig e Hive, che
compilano dei linguaggi specifici Pig Latin e Hive QL in alcuni job di
MapReduce o in DAG di Spark, astraendo efficacemente le API. Entrambi i
linguaggi possono essere estesi con alcune UDF, un modo per mappare la logica
complessa nei modelli di dati di Pig e Hive.
Se quello che ci serve un certo grado di flessibilit e modularit, le cose si
fanno un po difficili. A seconda delle necessit concrete del business e dello
sviluppatore, lecosistema di Hadoop offre unampia gamma di API, framework e
librerie. In questo capitolo identificheremo quattro categorie di utenti e li
assoceremo ai relativi strumenti principali.
Sviluppatori che vogliono evitare Java a favore della scrittura di job di
MapReduce tramite linguaggi dinamici o linguaggi non implementati sulla
JVM. Casi duso tipici potrebbero essere lanalisi preventiva e la
prototipazione rapida. Lo strumento adatto Hadoop Streaming.
Sviluppatori Java che devono integrare componenti dellecosistema di
Hadoop e possono trarre benefici da pattern e standard di design codificati.
Lo strumento adatto Kite Data.
Sviluppatori Java che vogliono scrivere pipeline di dati modulari usando
unAPI familiare. Lo strumento adatto Apache Crunch.
Sviluppatori che devono configurare catene di trasformazioni di dati, per
esempio un ingegnere dei dati che vuole incorporare del codice esistente in
una pipeline ETL. Lo strumento adatto Kite Morphlines.
Hadoop Streaming
In precedenza abbiamo detto che i programmi di MapReduce non devono essere
scritti in Java. Ci sono molte ragioni per cui potreste o dovreste scrivere le attivit
di map e reduce in un altro linguaggio; magari dovete reimpiegare del codice
esistente oppure siete costretti a utilizzare dei binari di terze parti. I motivi sono
tanti e tutti validi.
Hadoop fornisce alcuni meccanismi per aiutarvi nello sviluppo non Java; tra i
principali ci sono i pipe di Hadoop, che forniscono uninterfaccia C++ nativa, e
Hadoop Streaming, che consente di utilizzare qualsiasi programma che impiega un
input e un output standard per le attivit di map e reduce. Con lAPI Java di
MapReduce, entrambe queste attivit forniscono delle implementazioni per i
metodi che contengono la funzionalit dellattivit. I metodi ricevono linput come
argomenti e generano loutput attraverso loggetto Context. uninterfaccia chiara e
indipendente dal tipo, ma per definizione specifica di Java.
Hadoop Streaming adotta un approccio differente. Con lo streaming, scrivete
unattivit di map che legge il suo input dallinput standard, una riga alla volta, e
fornisce loutput dei suoi risultati nelloutput standard. Lattivit di reduce fa poi lo
stesso, utilizzando anchessa solo linput e loutput standard per il suo flusso dei
dati.
Qualsiasi programma che legge scrive dallinput e output standard pu essere
utilizzato in streaming, e questo vale per i binari compilati, gli script della shell di
Unix o i programmi scritti in un linguaggio dinamico come Python o Ruby. Il
vantaggio principale dello streaming che consente di provare le idee e di
iterarle pi rapidamente che non utilizzando Java. Invece di avere un ciclo
compilazione-JAR-invio, potete scrivere semplicemente gli script e passarli come
argomenti al file JAR in streaming. un meccanismo che velocizza notevolmente
lo sviluppo, soprattutto durante lanalisi iniziale su un nuovo dataset o quando si
stanno sperimentano nuove idee.
La classica diatriba tra linguaggi dinamici e linguaggi statici si basa sul
confronto tra i vantaggi dello sviluppo rapido e le prestazioni di runtime e il
controllo del tipo. Gli inconvenienti dei linguaggi dinamici valgono anche per lo
streaming. Per tale ragione preferiamo utilizzare questultimo per lanalisi
preventiva e Java per limplementazione dei job che verranno eseguiti sul cluster
di produzione.

Conteggio delle parole in streaming in Python


Illustreremo ora Hadoop Streaming reimplementando lesempio del conteggio
delle parole usando Python. Come prima cosa, creiamo uno script che sar il
nostro mapper. Lo script prende delle righe di testo codificate in UTF-8 tratte
dallinput standard con un ciclo for, le scompone in parole e usa la funzione print
per scrivere ciascuna parola nelloutput standard:
#!/bin/env python
import sys

for line in sys.stdin:


# ignora le rige vuote
if line == \n:
continue

# mantiene la codifica UTF-8


try:
line = line.encode(utf-8)
except UnicodeDecodeError:
continue
# nel testo possono apparire i caratteri newline
line = line.replace(\n, )

# minuscole e scomposizione
line = line.lower().split()

for term in line:


if not term:
continue
try:
print(
u%s % (
term.decode(utf-8)))
except UnicodeEncodeError:
continue

Il reducer conta il numero di occorrenze di ogni parola dallinput standard e


genera loutput come valore finale nelloutput standard:
#!/bin/env python
import sys

count = 1
current = None

for word in sys.stdin:


word = word.strip()

if word == current:
count += 1
else:
if current:
print %s\t%s % (current.decode(utf-8), count)
current = word
count = 1
if current == word:
print %s\t%s % (current.decode(utf-8), count)

NOTA
In entrambi i casi utilizziamo implicitamente i formati di input e output di Hadoop presentati nei
capitoli precedenti. TextInputFormat elabora il file sorgente e fornisce una riga alla volta allo
script di map. Per contro, TextOutputFormat garantisce che loutput delle attivit di reduce sia
scritto correttamente come un testo.

Copiamo map.py e reduce.py in HDFS ed eseguiamo gli script come job in


streaming utilizzando i dati di esempio dei capitoli precedenti:
$ hadoop jar /opt/cloudera/parcels/CDH/lib/hadoop-mapreduce/hadoop-streaming.jar \
-file map.py \
-mapper python map.py \
-file reduce.py \
-reducer python reduce.py \
-input sample.txt \
-output output.txt

AT T ENZIONE
I tweet sono codificati in UTF-8. Accertatevi che PYTHONIOENCODING sia impostato di
conseguenza per poter convogliare i dati in un terminale UNIX:
$ export PYTHONIOENCODING=UTF-8

Lo stesso codice pu essere eseguito dal prompt della riga di comando:


$ cat sample.txt | python map.py| python reduce.py > out.txt

Il codice del mapper e del reducer si trova alla pagina http://bit.ly/1CmmKya.

Differenze tra i job quando si usa lo streaming


Sappiamo che in Java, il metodo map() viene invocato una volta per ciascuna
coppia chiave/valore in input, mentre il metodo reduce() viene invocato per ogni
chiave e per il suo set di valori. Nello streaming, non esiste pi il concetto di
metodi map o reduce; abbiamo invece degli script che elaborano gli stream dei dati
ricevuti. Questo comporta una modifica nel modo in cui scriveremo il reducer. In
Java, il raggruppamento dei valori per ogni chiave viene eseguito da Hadoop; ogni
invocazione del metodo reduce riceve una singola chiave separata da una
tabulazione con tutti i relativi valori. Nello streaming, a ogni istanza dellattivit
di reduce vengono forniti i singoli valori non aggregati uno alla volta.
Per esempio, Hadoop Streaming ordina le chiavi se un mapper ha emesso i
seguenti dati:
First 1
Word 1
Word 1
A 1
First 1

Il reducer in streaming li ricever in questordine:


A 1
First 1
First 1
Word 1
Word 1

Hadoop raccoglie comunque dei valori per ogni chiave e garantisce che siano
passati esclusivamente a un singolo reducer. In altre parole, un reducer ottiene tutti
i valori per un certo numero di chiavi e li raggruppa; tuttavia, a differenza di
quanto accade con lAPI Java, i valori non sono assemblati in singole esecuzioni
del reducer, uno per chiave. Poich Hadoop Streaming utilizza i canali di stdin e
stdout per lo scambio dei dati tra le attivit, i messaggi di debug e di errore non

appariranno nelloutput standard. Nel prossimo esempio utilizzeremo il package


logging di Python (https://docs.python.org/2/library/logging.html) per registrare delle

istruzioni di avvertimento in un file.

Trovare parole importanti nel testo


Implementeremo ora una metrica, la Term Frequency-Inverse Document
Frequency (TF-IDF), che ci aiuter a determinare limportanza delle parole in
base alla frequenza con cui appaiono in un insieme di documenti (di tweet, nel
nostro caso).
In linea generale, se una parola appare spesso in un documento significa che
importante, e dovrebbe avere un punteggio elevato. Se per appare in pi
documenti, dovremo penalizzarla con un punteggio pi basso, perch una parola
comune e la sua frequenza non specifica del documento considerato.
Il punteggio di parole diffuse come il e per, quindi, che appaiono in molti
documenti, andr scalato verso il basso, mentre il punteggio delle parole che
appaiono spesso in un singolo tweet verr scalato verso lalto. Tra gli usi della
TF-IDF, spesso in combinazione con altre metriche e tecniche, ci sono la
rimozione delle stop word e la classificazione del testo. Questa tecnica ha alcuni
svantaggi quando si ha a che fare con documenti brevi, come i tweet. In questi casi,
il componente della frequenza di un termine tender a diventare uno; per contro si
potrebbe sfruttare questa propriet per individuare le anomalie.
La definizione della TF-IDF che utilizzeremo nel nostro esempio la seguente:
tf = # di volte in cui il termine appare in un documento (frequenza grezza)
idf = 1+log(# di documenti / # documenti che contengono il termine)
tf-idf = tf * idf

Implementeremo lalgoritmo in Python usando tre job di MapReduce:


il primo calcola la frequenza del termine;
il secondo calcola la frequenza del documento (il denominatore dellIDF);
il terzo calcola la TF-IDF per tweet.

Calcolare la frequenza di un termine


Il calcolo della frequenza di un termine avviene in modo molto simile
allesempio del conteggio delle parole. La differenza principale che utilizzeremo
una chiave multicampo separata da tabulazione per tenere traccia delle co-
occorrenze dei termini e degli ID di documento. Per ogni tweet nel formato
JSON il mapper estrae i campi id_str e text, li scompone in text ed emette una
tupla term, doc_id:
for tweet in sys.stdin:
# ignora le righe vuote
if tweet == \n:
continue
try:
tweet = json.loads(tweet)
except:
logger.warn(Invalid input %s % tweet)
continue
# Nel nostro esempio un tweet corrisponde a un documento.
doc_id = tweet[id_str]
if not doc_id:
continue

# mantiene la codifica UTF-8


text = tweet[text].encode(utf-8)
# nel testo possono apparire i caratteri newline
text = text.replace(\n, )

# minuscole e scomposizione
text = text.lower().split()

for term in text:


try:
print(
u%s\t%s % (
term.decode(utf-8), doc_id.decode(utf-8))
)
except UnicodeEncodeError:
logger.warn(Invalid term %s % term)
Nel reducer, emettiamo la frequenza di ciascun termine in un documento come
stringa separata da tabulazione:
freq = 1
cur_term, cur_doc_id = sys.stdin.readline().split()
for line in sys.stdin:
line = line.strip()
try:
term, doc_id = line.split(\t)
except:
logger.warn(Invalid record %s % line)

# la chiave una coppia (doc_id, term)


if (doc_id == cur_doc_id) and (term == cur_term):
freq += 1

else:
print(
u%s\t%s\t%s % (
cur_term.decode(utf-8), cur_doc_id.decode(utf-8), freq))
cur_doc_id = doc_id
cur_term = term
freq = 1

print(
u%s\t%s\t%s % (
cur_term.decode(utf-8), cur_doc_id.decode(utf-8), freq))

Perch questa implementazione funzioni, fondamentale che linput del reducer


sia ordinato per parola. Possiamo testare entrambi gli script dalla riga di
comando, come segue:
$ cat tweets.json | python map-tf.py | sort -k1,2 | \
python reduce-tf.py

Laddove nella riga di comando usiamo lutility sort, in MapReduce utilizzeremo


org.apache.hadoop.mapreduce.lib.KeyFieldBasedComparator. Questo comparatore implementa

un sottoinsieme di funzioni fornite dal comando sort. In particolare, si pu


specificare lordinamento per campo con lopzione k<position>. Per filtrare
secondo la parola, che il primo campo della nostra chiave, impostiamo -D
mapreduce.text.key.comparator.options=-k1:

/usr/bin/hadoop jar /opt/cloudera/parcels/CDH/lib/hadoop-mapreduce/


hadoop-streaming.jar \
-D map.output.key.field.separator=\t \
-D stream.num.map.output.key.fields=2 \
-Dmapreduce.output.key.comparator.class=\
org.apache.hadoop.mapreduce.lib.KeyFieldBasedComparator \
-D mapreduce.text.key.comparator.options=-k1,2 \
-input tweets.json \
-output /tmp/tf-out.tsv \
-file map-tf.py \
-mapper python map-tf.py \
-file reduce-tf.py \
-reducer python reduce-tf.py

NOTA
Specifichiamo quali campi appartengono alla chiave (per lo shuffling) nelle opzioni del
comparatore.

Trovate il codice del mapper e del reducer alla pagina http://bit.ly/1ENABgR.

Calcolare la frequenza di un documento


La logica principale per calcolare la frequenza del documento si trova nel
reducer, mentre il mapper semplicemente una funzione di identit che carica e
convoglia loutput (ordinato per parola) del job della TF. Nel reducer, contiamo
quante volte ogni parola ricorre in tutti i documenti. Per ogni parola, manteniamo
un buffer key_cache di tuple (term, doc_id, tf), e quando viene trovato un nuovo
termine, mandiamo il buffer alloutput standard, insieme alla frequenza df
accumulata del documento:
# Cache della tupla (term,doc_id, tf).
key_cache = []

line = sys.stdin.readline().strip()
cur_term, cur_doc_id, cur_tf = line.split(\t)
cur_tf = int(cur_tf)
cur_df = 1

for line in sys.stdin:


line = line.strip()

try:
term, doc_id, tf = line.strip().split(\t)
tf = int(tf)
except:
logger.warn(Invalid record: %s % line)
continue

# il termine lunica chiave per questo input


if (term == cur_term):
# incrementa la frequenza del documento
cur_df += 1

key_cache.append(
u%s\t%s\t%s % (term.decode(utf-8), doc_id.decode(utf-8), tf))

else:
for key in key_cache:
print(%s\t%s % (key, cur_df))

print (
u%s\t%s\t%s\t%s % (
cur_term.decode(utf-8),
cur_doc_id.decode(utf-8),
cur_tf, cur_df)
)

# la cache viene passata


key_cache = []
cur_doc_id = doc_id
cur_term = term
cur_tf = tf
cur_df = 1

for key in key_cache:


print(u%s\t%s % (key.decode(utf-8), cur_df))
print(
u%s\t%s\t%s\t%s\n % (
cur_term.decode(utf-8),
cur_doc_id.decode(utf-8),
cur_tf, cur_df))

Possiamo testare gli script dalla riga di comando con


$ cat /tmp/tf-out.tsv | python map-df.py | python reduce-df.py > /tmp/df-out.tsv

oppure possiamo farlo in Hadoop Streaming:


/usr/bin/hadoop jar /opt/cloudera/parcels/CDH/lib/hadoop-mapreduce/
hadoop-streaming.jar \
-D map.output.key.field.separator=\t \
-D stream.num.map.output.key.fields=3 \
-D mapreduce.output.key.comparator.class=\
org.apache.hadoop.mapreduce.lib.KeyFieldBasedComparator \
-D mapreduce.text.key.comparator.options=-k1 \
-input /tmp/tf-out.tsv/part-00000 \
-output /tmp/df-out.tsv \
-mapper org.apache.hadoop.mapred.lib.IdentityMapper \
-file reduce-df.py \
-reducer python reduce-df.py

In Hadoop usiamo org.apache.hadoop.mapred.lib.IdentityMapper, che fornisce la stessa


logica dello script map-df.py.
Trovate il codice del mapper e del reducer alla pagina http://bit.ly/1FTklfL.

Assemblare il tutto: TF-IDF


Per calcolare la TF-IDF, ci serve semplicemente un mapper che usa loutput del
passo precedente:
num_doc = sys.argv[1]

for line in sys.stdin:


line = line.strip()

try:
term, doc_id, tf, df = line.split(\t)

tf = float(tf)
df = float(df)
num_doc = float(num_doc)
except:
logger.warn(Invalid record %s % line)

# idf = num_doc / df
tf_idf = tf * (1+math.log(num_doc / df))
print(%s\t%s\t%s % (term, doc_id, tf_idf))

Il numero dei documenti nella collezione viene passato come parametro a tf-
idf.py:
/usr/bin/hadoop jar /opt/cloudera/parcels/CDH/lib/hadoop-mapreduce/
hadoop-streaming.jar \
-D mapreduce.reduce.tasks=0 \
-input /tmp/df-out.tsv/part-00000 \
-output /tmp/tf-idf.out \
-file tf-idf.py \
-mapper python tf-idf.py 15578

Per calcolare il numero totale di tweet, possiamo usare le utility Unix cat e wc in
combinazione con Hadoop Streaming:
/usr/bin/hadoop jar /opt/cloudera/parcels/CDH/lib/hadoop-mapreduce/hadoop-streaming.jar \
-input tweets.json \
-output tweets.cnt \
-mapper /bin/cat \
-reducer /usr/bin/wc

Trovate il codice sorgente del mapper alla pagina http://bit.ly/1Bp2fw3.


Kite Data
Kite SDK (http://www.kitesdk.org) una collezione di classi, strumenti a riga di
comando ed esempi che hanno lo scopo di agevolare il processo di costruzione
delle applicazioni su Hadoop.
In questo paragrafo vedremo come Kite Data, un sottoprogetto di Kite, pu
favorire lintegrazione con numerosi componenti di un data warehouse Hadoop.
Trovate alcuni esempi di Kite alla pagina https://github.com/kite-sdk/kite-examples.
Sulla Cloudera QuickStart VM, trovate i JAR di Kite in
/opt/cloudera/parcels/CDH/lib/kite/.
Kite Data organizzato in progetti secondari, alcuni dei quali saranno descritti
nei prossimi paragrafi.

Data Core
Come suggerisce il nome, il core alla base di tutte le funzionalit fornite nel
modulo Data. Le sue astrazioni principali sono i dataset e i repository.
Linterfaccia org.kitesdk.data.Dataset viene utilizzata per rappresentare un set di
dati non mutevole:
@Immutable
public interface Dataset<E> extends RefinableView<E> {
String getName();
DatasetDescriptor getDescriptor();
Dataset<E> getPartition(PartitionKey key, boolean autoCreate);
void dropPartition(PartitionKey key);
Iterable<Dataset<E>> getPartitions();
URI getUri();
}

Ogni dataset identificato da un nome e da unistanza dellinterfaccia


org.kitesdk.data.DatasetDescriptor, cio la descrizione della struttura di un dataset, di

cui fornisce lo schema (org.apache.avro.Schema) e la strategia di partizionamento.


Le implementazioni dellinterfaccia Reader<E> sono utilizzate per leggere i dati da
un sistema di storage sottostante e producono entit deserializzate di tipo E.
Possiamo ricorrere al metodo newReader() per ottenere limplementazione opportuna
per uno specifico dataset:
public interface DatasetReader<E> extends Iterator<E>, Iterable<E>, Closeable {
void open();
boolean hasNext();

E next();
void remove();
void close();
boolean isOpen();
}

Unistanza di DatasetReader fornir i metodi per leggere e iterare sugli stream di


dati. Analogamente, org.kitesdk.data.DatasetWriter offre uninterfaccia per scrivere
stream di dati sugli oggetti Dataset:
public interface DatasetWriter<E> extends Flushable, Closeable {
void open();
void write(E entity);
void flush();
void close();
boolean isOpen();
}

Come i reader, i writer sono oggetti utilizzabili una volta sola. Serializzano
istanze delle entit di tipo E e le scrivono sul sistema di storage sottostante. In
genere i writer non sono istanziati direttamente, ma pu essere creata
unimplementazione adatta attraverso il metodo factory newWriter().
Implementazioni di DatasetWriter tratterranno le risorse finch non viene chiamato
close(), e attendono che il chiamante invochi close() in un blocco finally quando il

writer non pi in uso. Infine, ricordate che le implementazioni di DatasetWriter non


sono thread-safe: il comportamento di un writer a cui si accede da pi thread non
definito.
Un caso particolare di dataset linterfaccia View, mostrata di seguito:
public interface View<E> {
Dataset<E> getDataset();
DatasetReader<E> newReader();
DatasetWriter<E> newWriter();
boolean includes(E entity);
public boolean deleteAll();
}

Le viste (view) contengono dei subset delle chiavi e delle partizioni di un


dataset esistente; concettualmente, sono simili alle viste del modello relazionale.
Uninterfaccia View pu essere creata da una serie di dati, di chiavi o dallunione
di altre viste.

Data HCatalog
Data HCatalog un modulo che abilita laccesso ai repository di HCatalog. Le
sue astrazioni principali sono org.kitesdk.data.hcatalog.HCatalogAbstractDatasetRepository
e limplementazione concreta, org.kitesdk.data.hcatalog.HCatalogDatasetRepository.
Descrivono un DatasetRepository che utilizza HCatalog per gestire i metadati e HDFS
per lo storage:
public class HCatalogDatasetRepository extends HCatalogAbstractDatasetRepository {
HCatalogDatasetRepository(Configuration conf) {
super(conf, new HCatalogManagedMetadataProvider(conf));
}
HCatalogDatasetRepository(Configuration conf, MetadataProvider provider) {
super(conf, provider);
}
public <E> Dataset<E> create(String name, DatasetDescriptor descriptor) {
getMetadataProvider().create(name, descriptor);
return load(name);
}
public boolean delete(String name) {
return getMetadataProvider().delete(name);
}
public static class Builder {

}
}

NOTA
Come nel caso di Kite 0.17, Data HCatalog stato deprecato a favore del nuovo modulo Data
Hive.

La posizione della directory dei dati viene scelta da Hive/HCatalog (le cosiddette
tabelle gestite) o specificata quando si crea unistanza di questa classe fornendo
un file system e una directory root nel costruttore (tabelle esterne).

Data Hive
Il modulo Data Hive espone gli schemi di Hive attraverso linterfaccia Dataset.
Come nel caso di Kite 0.17, questo package sostituisce Data HCatalog.

Data MapReduce
Il package org.kitesdk.data.mapreduce fornisce interfacce per leggere e scrivere i
dati in e da un dataset con MapReduce.

Data Spark
Il package org.kitesdk.data.spark fornisce interfacce per leggere e scrivere i dati in
e da un dataset con Apache Spark.
Data Crunch
Il package org.kitesdk.data.crunch.CrunchDatasets una classe helper che espone
dataset e viste come classi ReadableSource o Target di Crunch:
public class CrunchDatasets {
public static <E> ReadableSource<E> asSource(View<E> view, Class<E> type) {
return new DatasetSourceTarget<E>(view, type);
}
public static <E> ReadableSource<E> asSource(URI uri, Class<E> type) {
return new DatasetSourceTarget<E>(uri, type);
}
public static <E> ReadableSource<E> asSource(String uri, Class<E> type) {
return asSource(URI.create(uri), type);
}

public static <E> Target asTarget(View<E> view) {


return new DatasetTarget<E>(view);
}
public static Target asTarget(String uri) {
return asTarget(URI.create(uri));
}
public static Target asTarget(URI uri) {
return new DatasetTarget<Object>(uri);
}
}
Apache Crunch
Apache Crunch (http://crunch.apache.org) una libreria Java e Scala per creare
pipeline di job di MapReduce. Si basa sul documento e sulla libreria FlumeJava
(http://dl.acm.org/citation.cfm?id=1806638) di Google. Lobiettivo del progetto quello
di rendere la scrittura dei job di MapReduce il pi immediata possibile per chi ha
familiarit con il linguaggio di programmazione Java esponendo alcuni pattern che
implementano operazioni come laggregazione, lunione, il filtro e lordinamento
dei record.
Simili a strumenti come Pig, le pipeline di Crunch vengono create assemblando
strutture di dati non mutevoli e distribuite ed eseguendo tutte le operazioni di
elaborazione su di esse; sono espresse e implementate come funzioni definite
dallutente. Le pipeline vengono compilate in un DAG di job di MapReduce la cui
gestione affidata al planner della libreria. Crunch permette di scrivere codice
iterativo e astrae la complessit legata al pensare in termini di operazioni di map e
reduce, evitando al contempo la necessit di un linguaggio di programmazione ad

hoc come PigLatin. Inoltre, offre un sistema di tipi altamente personalizzabile che
ci d la possibilit di lavorare (e di mescolare) con le interfacce Writable di
Hadoop, con HBase e con gli oggetti serializzati di Avro.
FlumeJava presume che MapReduce sia il livello di astrazione sbagliato per
molte classi di problemi, in cui i calcoli sono spesso costituiti da pi lavori
concatenati. Spesso, per ragioni di prestazioni, necessario combinare operazioni
indipendenti dal punto di vista logico (per esempio filtro, proiezione, aggregazione
e altre trasformazioni) in un unico job fisico di MapReduce. Questo aspetto
influisce anche sulla testabilit del codice. Non potendo affrontare questo
argomento nel capitolo, vi rimandiamo alla documentazione di Crunch.

Per iniziare
I JAR di Crunch sono gi installati sulla QuickStart VM; di default, si trovano in
/opt/cloudera/parcels/CDH/lib/crunch. In alternativa, possibile scaricare
alcune librerie di Crunch recenti allindirizzo https://crunch.apache.org/download.html,
dai repository specifici di Maven Central o Cloudera.
Concetti
Le pipeline di Crunch vengono create unendo due astrazioni: PCollection e PTable.
LinterfacciaPCollection<T> una collezione distribuita e non mutevole di oggetti di
tipo T. Linterfaccia PTable<Key, Value> unhashtable (una sotto-interfaccia di
PCollection) distribuita e non mutevole di chiavi del tipo Key e di valori del tipo
Value che espone metodi per lavorare con le coppie chiave-valore.

Queste due astrazioni supportano le seguenti operazioni primitive.


parallelDo: applica una funzione definita dallutente, DoFn, a una data PCollection
e restituisce una nuova PCollection.
union: unisce due o pi PCollection in ununica PCollection virtuale.

groupByKey: ordina e raggruppa gli elementi di una PTable in base alle loro

chiavi.
combineValues: aggrega i valori da una groupByKey operation.

Il codice alla pagina http://bit.ly/1GQ1UqS implementa una pipeline Crunch di


MapReduce che conta le occorrenze degli hashtag:
Pipeline pipeline = new MRPipeline(HashtagCount.class, getConf());

pipeline.enableDebug();

PCollection<String> lines = pipeline.readTextFile(args[0]);

PCollection<String> words = lines.parallelDo(new DoFn<String, String>() {


public void process(String line, Emitter<String> emitter) {
for (String word : line.split(\\s+)) {
if (word.matches((?:\\s|\\A|^)[##]+([A-Za-z0-9-_]+))) {
emitter.emit(word);
}
}
}
}, Writables.strings());

PTable<String, Long> counts = words.count();

pipeline.writeTextFile(counts, args[1]);
// Esegue la pipeline come un job di MapReduce.
pipeline.done();

In questo esempio, creiamo prima una pipeline MRPipeline e utilizziamola per


leggere il contenuto del file sample.txt creato con stream.py -t in una collezione di
stringhe in cui ogni elemento rappresenta un tweet. Scomponiamo ciascun tweet in
parole con tweet.split(\\s+) ed emettiamo ogni parola che coincide con
lespressione regolare dellhashtag, serializzata come Writable. Notate che le
operazioni di scomposizione e filtro vengono eseguite in parallelo dai job di
MapReduce creati con la chiamata parallelDo. Creiamo una PTable che associa
ciascun hashtag, rappresentato come stringa, con il numero di volte che ricorre nei
dataset. Infine, scriviamo i conteggi della PTable in HDFS come file di testo. La
pipeline viene eseguita con pipeline.done().
Per compilare ed eseguire la pipeline, possiamo utilizzare Gradle per gestire le
dipendenze necessarie, come segue:
$ ./gradlew jar
$ ./gradlew copyJars

Aggiungiamo le dipendenze di Crunch e Avro scaricate con copyJars alla


variabile dambiente LIBJARS:
$ export CRUNCH_DEPS=build/libjars/crunch-example/lib
$ export LIBJARS=${LIBJARS},${CRUNCH_DEPS}/crunch-core-0.9.0-cdh5.0.3.jar,${CRUNCH_DEPS}/avro-
1.7.5-cdh5.0.3.jar,${CRUNCH_DEPS}/avro-mapred-1.7.5-cdh5.0.3-hadoop2.jar

Eseguiamo poi lesempio in Hadoop:


$ hadoop jar build/libs/crunch-example.jar \
com.learninghadoop2.crunch.HashtagCount \
tweets.json count-out \
-libjars $LIBJARS

Serializzazione dei dati


Uno degli obiettivi del framework quello di elaborare i record complessi che
contengono strutture di dati annidate e ripetute, come i buffer di protocollo e i
record di Thrift.
Linterfaccia org.apache.crunch.types.PType definisce la mappatura tra un tipo di
dato che usato in una pipeline di Crunch pipeline e un formato di serializzazione
e storage che viene usato per leggere/scrivere dati da/in HDFS. A ogni PCollection
associato un PType che dice a Crunch come leggere/scrivere i dati.
Linterfaccia org.apache.crunch.types.PTypeFamily fornisce invece un factory astratto
per implementare istanze di PType che condividono lo stesso formato di
serializzazione. Attualmente, Crunch supporta due tipi di famiglie: una basata
sullinterfaccia Writable e laltra su Apache Avro.
AT T ENZIONE
Sebbene Crunch permetta di mescolare interfacce della PCollection che utilizzano istanze
diverse del PType nella stessa pipeline, ogni PType dellinterfaccia PCollection deve appartenere
a una sola famiglia. Per esempio, non possibile avere una PTable con una chiave serializzata
come Writable e il valore serializzato usando Avro.

Le due famiglie supportano un set di tipi di primitive comuni (stringhe, long,


integer, float, double, boolean e byte) oltre a interfacce PType pi complesse che
possono essere costruite da altri PType. Fra queste abbiamo le tuple e le collezioni
di altri PType. Un tipo particolarmente importante e complesso di PType tableOf, che
determina se il tipo di ritorno di paralleDo sar una PCollection o una PTable.
Nuovi PType possono essere creati ereditando ed estendendo i built-in delle
famiglie Writable e Avro. Perch questo accada necessario implementare la
classe di input MapFn<S, T> e quella di output MapFn<T, S>. Implementeremo PType per
quei casi in cui S il tipo originale e T il tipo nuovo.
Nella classe PTypes si possono trovare PType derivati. Tra questi abbiamo il
supporto della serializzazione per i buffer di protocollo, i record di Thrift, le enum
Java, BigInteger e gli UUID. La libreria Elephant Bird che abbiamo presentato nel
Capitolo 6 contiene ulteriori esempi.

Pattern di elaborazione dei dati


Il package org.apache.crunch.lib implementa alcuni pattern di design per le
operazioni pi comuni di manipolazione dei dati.

Aggregazione e ordinamento
Molti dei pattern di elaborazione dei dati forniti dallorg.apache.crunch.lib si
basano sul metodo groupByKey di PTable. Il metodo si presenta in tre forme diverse.
groupByKey(): consente al planner di determinare il numero delle partizioni.
groupByKey(int numPartitions): viene usato per impostare il numero delle

partizioni specificato dallo sviluppatore.


groupByKey(GroupingOptions options): consente di specificare delle partizioni

personalizzate e dei comparatori per lo shuffling.


La classe org.apache.crunch.GroupingOptions prende le istanze delle classi Partitioner
e RawComparator di Hadoop per implementare un partizionamento personalizzato e le
operazioni di ordinamento.
Il metodo groupByKey restituisce unistanza di PGroupedTable, la rappresentazione di
Crunch di una tabella raggruppata. Corrisponde alloutput della fase di shuffle di
un job di MapReduce e permette di combinare i valori con il metodo combineValue.
Il package org.apache.crunch.lib.Aggregate espone alcuni metodi per eseguire
aggregazioni semplici (count, max, top e length) su istanze di PCollection.
Sort fornisce unAPI per ordinare le istanze di PCollection e PTable il cui contenuto

implementa linterfaccia Comparable.


Di default, Crunch ordina i dati usando un reducer. Questo comportamento pu
essere modificato passando il numero di partizioni necessarie al metodo sort. Il
metodo Sort.Order determina lordine in cui devessere effettuato lordinamento.
Quelle che seguono sono alcune opzioni di ordinamento che possono essere
specificate per le collezioni:
public static <T> PCollection<T> sort(PCollection<T> collection)
public static <T> PCollection<T> sort(PCollection<T> collection,
Sort.Order order)
public static <T> PCollection<T> sort(PCollection<T> collection,
int numReducers, Sort.Order order)

Le prossime opzioni valgono invece per lordinamento delle tabelle:


public static <K,V> PTable<K,V> sort(PTable<K,V> table)

public static <K,V> PTable<K,V> sort(PTable<K,V> table,


Sort.Order key)
public static <K,V> PTable<K,V> sort(PTable<K,V> table,
int numReducers, Sort.Order key)

Infine, sortPairs ordina la PCollection di coppie usando lordine di colonna


indicato in Sort.ColumnOrder:
sortPairs(PCollection<Pair<U,V>> collection,
Sort.ColumnOrder... columnOrders)

Join dei dati


Il package org.apache.crunch.lib.Join unAPI che collega le PTable in base a una
chiave comune. Sono supportate le seguenti quattro operazioni di join:
fullJoin;
join (di default come innerJoin);

leftJoin;

rightJoin.
I metodi hanno un tipo di ritorno e una segnatura comuni. Come riferimento,
descriveremo il metodo join usato di frequente che implementa un inner join:
public static <K,U,V> PTable<K,Pair<U,V>> join(PTable<K,U> left,
PTable<K,V> right)

Il package org.apache.crunch.lib.Join.JoinStrategy fornisce uninterfaccia per


definire strategie di join personalizzate. Quella di default di Crunch
(defaultStrategy) comporta il join dei dati sul lato reduce.

Implementazione ed esecuzione delle pipeline


Crunch offre tre implementazioni dellinterfaccia della pipeline. La pi vecchia,
utilizzata implicitamente in questo capitolo, org.apache.crunch.impl.mr.MRPipeline, che
si serve di MapReduce su Hadoop come motore di esecuzione.
org.apache.crunch.impl.mem.MemPipeline consente che tutte le operazioni avvengano in

memoria, senza alcuna serializzazione su disco. Crunch 0.10 ha poi introdotto


org.apache.crunch.impl.spark.SparkPipeline, che compila ed esegue un DAG di PCollection

su Apache Spark.

SparkPipeline
Con SparkPipeline, Crunch delega gran parte dellesecuzione a Spark e si
occupa relativamente poco delle attivit di pianificazione, con le seguenti
eccezioni:
input multipli;
output multipli;
serializzazione dei dati;
operazioni di checkpoint.
Al momento della stesura di queste righe, SparkPipeline ancora oggetto di un
pesante sviluppo e potrebbe non gestire tutti i casi duso di una MRPipeline
standard. La community di Crunch sta lavorando attivamente per garantire una
compatibilit completa tra le due implementazioni.

MemPipeline
MemPipeline viene eseguita in memoria su un client. A differenza di
MRPipeline, non viene creata esplicitamente ma viene referenziata chiamando il
metodo statico MemPipeline.getInstance(). Tutte le operazioni avvengono in memoria, e
luso di PType ridotto al minimo.

Esempi di Crunch
Utilizzeremo ora Apache Crunch per reimplementare in una maniera pi
modulare parte del codice di MapReduce scritto finora.

Co-occorrenza di parole
Nel Capitolo 3 abbiamo mostrato un job di MapReduce, BiGramCount, che
contava le co-occorrenze delle parole nei tweet. La stessa logica pu essere
implementata come una DoFn. Invece di emettere una chiave multicampo e doverla
sottoporre a parsing in una fase successiva, con Crunch possiamo usare un tipo
complesso Pair<String, String>, come segue:
class BiGram extends DoFn<String, Pair<String, String>> {
@Override
public void process(String tweet,
Emitter<Pair<String, String>> emitter) {
String[] words = tweet.split( ) ;

Text bigram = new Text();


String prev = null;

for (String s : words) {


if (prev != null) {
emitter.emit(Pair.of(prev, s));
}
prev = s;
}
}
}

Notate come, rispetto a MapReduce, limplementazione di BiGram di Crunch sia


una classe indipendente, facilmente riutilizzabile in qualsiasi altra base di codice.
Trovate il sorgente di questo esempio alla pagina http://bit.ly/1CVdX8H.

TF-IDF
Possiamo implementare la catena di job della TF-IDF con una MRPipeline, come
segue:
public class CrunchTermFrequencyInvertedDocumentFrequency
extends Configured implements Tool, Serializable {

private Long numDocs;

@SuppressWarnings(deprecation)

public static class TF {


String term;
String docId;
int frequency;

public TF() {}

public TF(String term,


String docId, Integer frequency) {
this.term = term;
this.docId = docId;
this.frequency = (int) frequency;

}
}

public int run(String[] args) throws Exception {


if(args.length != 2) {
System.err.println();
System.err.println(Usage: + this.getClass().getName() +
[generic options] input output);

return 1;
}
// Crea un oggetto per coordinare la creazione e lesecuzione della pipeline.
Pipeline pipeline =
new MRPipeline(TermFrequencyInvertedDocumentFrequency.class, getConf());

// abilita le opzioni di debug


pipeline.enableDebug();

// Fa riferimento a un file di testo dato come a una collezione di String.


PCollection<String> tweets = pipeline.readTextFile(args[0]);
numDocs = tweets.length().getValue();

// Usiamo le reflection di Avro per mappare TF POJO su avsc


PTable<String, TF> tf = tweets.parallelDo(new TermFrequencyAvro(),
Avros.tableOf(Avros.strings(), Avros.reflects(TF.class)));

// Calcola la DF
PTable<String, Long> df = Aggregate.count(tf.parallelDo( new DocumentFrequencyString(),
Avros.strings()));

// Infine calcoliamo la TF-IDF


PTable<String, Pair<TF, Long>> tfDf = Join.join(tf, df);
PCollection<Tuple3<String, String, Double>> tfIdf =
tfDf.parallelDo(new TermFrequencyInvertedDocumentFrequency(),
Avros.triples(
Avros.strings(),
Avros.strings(),
Avros.doubles()));

// Serializza come avro


tfIdf.write(To.avroFile(args[1]));

// Esegue la pipeline come un job di MapReduce.


PipelineResult result = pipeline.done();
return result.succeeded() ? 0 : 1;
}

}

Lapproccio che seguiamo qui presenta una serie di vantaggi rispetto allo
streaming. Prima di tutto, non occorre concatenare manualmente i job di
MapReduce usando uno script separato. Questa attivit lobiettivo principale di
Crunch. In secondo luogo, possiamo esprimere ciascun componente della metrica
come una classe distinta, facilitandone il riuso nelle applicazioni future.
Per implementare la frequenza dei termini, creiamo una classe DoFn che prende
come input un tweet ed emette Pair<String, TF>. Il primo elemento un termine,
mentre il secondo unistanza della classe POJO che verr serializzata tramite Avro.
La parte TF contiene tre variabili: term, documentId e frequency. Nellimplementazione
del riferimento, ci aspettiamo che i dati di input siano una stringa JSON che
deserializzeremo e sottoporremo a parsing. Includiamo anche la scomposizione
come sotto-attivit del metodo di elaborazione.
A seconda dei casi duso potremmo astrarre entrambe le operazioni in DoFn
separate, cos:
class TermFrequencyAvro extends DoFn<String,Pair<String, TF>> {
public void process(String JSONTweet,
Emitter<Pair <String, TF>> emitter) {
Map<String, Integer> termCount = new HashMap<>();

String tweet;
String docId;

JSONParser parser = new JSONParser();

try {
Object obj = parser.parse(JSONTweet);

JSONObject jsonObject = (JSONObject) obj;

tweet = (String) jsonObject.get(text);


docId = (String) jsonObject.get(id_str);

for (String term : tweet.split(\\s+)) {


if (termCount.containsKey(term.toLowerCase())) {
termCount.put(term,
termCount.get(term.toLowerCase()) + 1);
} else {
termCount.put(term.toLowerCase(), 1);
}
}

for (Entry<String, Integer> entry : termCount.entrySet()) {


emitter.emit(Pair.of(entry.getKey(), new TF(entry.getKey(), docId,
entry.getValue())));
}
} catch (ParseException e) {
e.printStackTrace();
}
}
}
}

Lindividuazione della frequenza del documento immediata. Per ogni


Pair<String, TF> generata nel passo relativo alla frequenza del termine, emettiamo il

termine, cio il primo elemento della coppia. Per ottenere la frequenza del
documento aggreghiamo e contiamo la PCollection di termini risultante:
class DocumentFrequencyString extends DoFn<Pair<String, TF>, String> {
@Override
public void process(Pair<String, TF> tfAvro,
Emitter<String> emitter) {
emitter.emit(tfAvro.first());
}
}

Infine colleghiamo la TF della PTable con la DF della PTable sulla chiave


condivisa (il termine) e inviamo loggetto Pair<String, Pair<TF, Long>> risultante a
TermFrequencyInvertedDocumentFrequency.

Per ciascun termine e documento, calcoliamo la TF-IDF e restituiamo una


tripletta costituita da term, docI e tfIdf:
class TermFrequencyInvertedDocumentFrequency extends MapFn<Pair<String, Pair<TF, Long>>,
Tuple3<String, String, Double> > {
@Override
public Tuple3<String, String, Double> map(
Pair<String, Pair<TF, Long>> input) {

Pair<TF, Long> tfDf = input.second();


Long df = tfDf.second();

TF tf = tfDf.first();
double idf = 1.0+Math.log(numDocs / df);
double tfIdf = idf * tf.frequency;

return Tuple3.of(tf.term, tf.docId, tfIdf);


}

Qui usiamo MapFn perch la nostra intenzione produrre in output un record per
ciascun input. Trovate il codice sorgente di questo esempio alla pagina
http://bit.ly/1Hx2Twe.

Lesempio pu essere compilato ed eseguito con i seguenti comandi:


$ ./gradlew jar
$ ./gradlew copyJars

Se non lavete gi fatto, aggiungete alla variabile dambiente LIBJARS le


dipendenze di Crunch e Avro scaricate con copyJars, cos:
$ export CRUNCH_DEPS=build/libjars/crunch-example/lib
$ export LIBJARS=${LIBJARS},${CRUNCH_DEPS}/crunch-core-0.9.0-
cdh5.0.3.jar,${CRUNCH_DEPS}/avro-1.7.5-cdh5.0.3.jar,${CRUNCH_DEPS}/avro-
mapred-1.7.5-cdh5.0.3-hadoop2.jar

Aggiungete poi il JAR json-simple a LIBJARS:


$ export LIBJARS=${LIBJARS},${CRUNCH_DEPS}/json-simple-1.1.1.jar

Infine, eseguite CrunchTermFrequencyInvertedDocumentFrequency come job di


MapReduce:
$ hadoop jar build/libs/crunch-example.jar \
com.learninghadoop2.crunch.CrunchTermFrequencyInvertedDocumentFrequency \
-libjars ${LIBJARS} \
tweets.json tweets.avro-out

Kite Morphlines
Kite Morphlines una libreria di trasformazione dei dati che si ispira ai pipe di
Unix, sviluppata originariamente come parte di Cloudera Search. Una morphline
una catena di comandi di trasformazione in memoria che si affida a una struttura di
plug-in per sfruttare fonti di dati eterogenee. Utilizza dei comandi dichiarativi per
effettuare operazioni ETL sui record. I comandi vengono definiti in un file di
configurazione che viene passato successivamente a una classe driver.
Lobiettivo quello di semplificare lattivit di incorporamento della logica
ETL in una base di codice Java attraverso una libreria che consente agli
sviluppatori di sostituire la programmazione con una serie di impostazioni di
configurazione.

Concetti
Le morphline sono costruite attorno a due astrazioni: Command e Record. I record
sono implementazioni dellinterfaccia org.kitesdk.morphline.api.Record:
public final class Record {
private ArrayListMultimap<String, Object> fields;

private Record(ArrayListMultimap<String, Object> fields) {}


public ListMultimap<String, Object> getFields() {}
public List get(String key) {}
public void put(String key, Object value) {}

}

Un record un insieme di campi denominati, dove ogni campo contiene un


elenco di uno o pi valori. Un Record viene implementato sulle classi ListMultimap e
ArrayListMultimap di Google Guava. Tenete presente che un valore pu essere
qualsiasi oggetto Java, i campi possono avere pi valori e due record non devono
utilizzare necessariamente nomi di campo comuni. Un record pu contenere un
campo con _attachment_body, che pu essere un java.io.InputStream o un array di byte.
I comandi implementano linterfaccia org.kitesdk.morphline.api.Command:
public interface Command {
void notify(Record notification);
boolean process(Record record);
Command getParent();
}

Un comando trasforma un record in zero o pi record. I comandi possono


chiamare i metodi sullistanza di Record fornita per le operazioni di lettura e
scrittura, oltre che per aggiungere o rimuovere i campi.
I comandi sono concatenati, e a ogni passo di una morphline il comando genitore
invia i record al comando figlio, che li elabora a sua volta. Genitori e figli si
scambiano le informazioni attraverso due canali di comunicazione (piani); le
notifiche vengono inviate attraverso il piano di controllo, mentre i record
attraverso un piano dei dati. I record sono elaborati dal metodo process(), che
restituisce un valore booleano per indicare se una morphline pu proseguire o
meno.
I comandi non vengono istanziati direttamente ma attraverso unimplementazione
dellinterfaccia org.kitesdk.morphline.api.CommandBuilder:
public interface CommandBuilder {
Collection<String> getNames();
Command build(Config config,
Command parent,
Command child,
MorphlineContext context);
}

Il metodo getNames restituisce i nomi con cui il comando pu essere invocato.


Sono supportati anche pi nomi per consentire modifiche al nome compatibili
retroattivamente. Il metodo build() crea e restituisce un comando di root alla
configurazione della morphline.
Linterfaccia org.kitesdk.morphline.api.MorphlineContext consente il passaggio di
ulteriori parametri a tutti i comandi della morphline.
Il modello di dati delle morphline strutturato seguendo un pattern sorgente-
pipe-sink, dove i dati vengono recuperati da una sorgente, convogliati attraverso
alcuni passi di elaborazione e il cui output viene infine rilasciato in un sink.
Comandi di Morphlines
Kite Morphlines offre alcuni comandi predefiniti che implementano le
trasformazioni dei dati o dei formati di serializzazione comuni (testo semplice,
Avro e JSON). I comandi attualmente disponibili sono organizzati come
sottoprogetti delle morphline e includono i seguenti.
kite-morphlines-core-stdio: legge i dati da grandi oggetti binari (BLOB) e dal
testo.
kite-morphlines-core-stdlib: raccoglie i tipi i dati Java per la manipolazione e la
rappresentazione dei dati.
kite-morphlines-avro: viene usato per la serializzazione e la deserializzazione

dei dati nel formato di Avro.


kite-morphlines-json: serializza e deserializza i dati nel formato JSON.

kite-morphlines-hadoop-core: viene usato per accedere a HDFS.

kite-morphlines-hadoop-parquet-avro: viene usato per serializzare e deserializzare i

dati nel formato di Parquet.


kite-morphlines-hadoop-sequencefile: viene usato per serializzare e deserializzare i

dati nel formato Sequencefile.


kite-morphlines-hadoop-rcfile: viene usato per serializzare e deserializzare i dati

nel formato RCfile.


Trovate un elenco di tutti i comandi disponibili alla pagina
.
http://kitesdk.org/docs/0.17.0/kite-morphlines/morphlinesReferenceGuide.html

I comandi vengono definiti dichiarando una catena di trasformazioni in un file di


configurazione, morphline.conf, che viene poi compilato ed eseguito da un
programma driver. Per esempio, possiamo specificare una morphline read_tweets
che caricher i tweet salvati come dati JSON, li serializzer e deserializzer
usando Jackson e mostrer a video i primi 10, combinando i comandi di default
readJson ed head contenuti nel package org.kitesdk.morphline, cos:

morphlines : [{
id : read_tweets
importCommands : [org.kitesdk.morphline.**]

commands : [{
readJson {
outputClass : com.fasterxml.jackson.databind.JsonNode
}}
{
head {
limit : 10
}}
]
}]

Illustreremo ora come eseguire questa morphline da un programma Java


indipendente e da MapReduce.
MorphlineDriver.java mostra come utilizzare la libreria incorporata in un sistema

host. Il primo passo da compiere nel metodo main consiste nel caricare la
configurazione JSON della morphline, costruire un oggetto MorphlineContext e
compilarlo in unistanza di Command che agisce da nodo di inizio della morphline.
Notate che Compiler.compile() prende un parametro finalChild, in questo caso
RecordEmitter. Lo utilizzeremo perch agisca da sink per la morphline, visualizzando

un record in stdout o salvandolo in HDFS. Nellesempio del MorphlineDriver,


utilizziamo org.kitesdk.morphline.base.Notifications per gestire e monitorare il ciclo di
vita della morphline secondo una modalit di transazione.
Una chiamata a Notifications.notifyStartSession(morphline) d il via alla catena di
trasformazione allinterno di una transazione definita chiamando
Notifications.notifyBeginTransaction. Se lazione ha successo, terminiamo la pipeline

con Notifications.notifyShutdown(morphline); in caso contrario, riprendiamo la


transazione, chiamiamo Notifications.notifyRollbackTransaction(morphline) e passiamo un
handler di eccezioni dal contesto della morphline al codice Java chiamante:
public class MorphlineDriver {
private static final class RecordEmitter implements Command {
private final Text line = new Text();

@Override
public Command getParent() {
return null;
}

@Override
public void notify(Record record) {

@Override
public boolean process(Record record) {
line.set(record.get(_attachment_body).toString());

System.out.println(line);

return true;
}
}
public static void main(String[] args) throws IOException {
/* carica un file di configurazione della morphline e lo imposta */
File morphlineFile = new File(args[0]);
String morphlineId = args[1];
MorphlineContext morphlineContext = new MorphlineContext.Builder().build();
Command morphline = new Compiler().compile(morphlineFile, morphlineId,
morphlineContext, new RecordEmitter());

/* Prepara la morphline per lesecuzione


*
* Le notifiche sono inviate attraverso il canale di comunicazione
* */

Notifications.notifyBeginTransaction(morphline);

/* Notate che usiamo il file system locale, e non HDFS*/


InputStream in = new BufferedInputStream(new FileInputStream(args[2]));

/* compila un record e lo passa */


Record record = new Record();
record.put(Fields.ATTACHMENT_BODY, in);

try {

Notifications.notifyStartSession(morphline);
boolean success = morphline.process(record);
if (!success) {
System.out.println(Morphline failed to process record: + record);
}
/* Conferma la morphline */
} catch (RuntimeException e) {
Notifications.notifyRollbackTransaction(morphline);
morphlineContext.getExceptionHandler().handleException(e, null);
}
finally {
in.close();
}

/* esce */
Notifications.notifyShutdown(morphline);
}
}

In questo esempio, carichiamo i dati nel formato JSON dal file system locale in
un oggetto InputStream e lo impieghiamo per inizializzare una nuova istanza di Record.
La classe RecordEmitter contiene lultima istanza elaborata del record nella catena,
sulla quale estraiamo l_attachment_body visualizzandolo nelloutput standard.
Trovate il codice sorgente di MorphlineDriver alla pagina http://bit.ly/1DTXp0y.
Utilizzare la stessa morphline da un job di MapReduce un gioco da ragazzi.
Durante la fase di setup del mapper, costruiamo un contesto che contiene la logica
di istanziazione, mentre il metodo map imposta loggetto Record e invia la logica di
elaborazione, come segue:
public static class ReadTweets
extends Mapper<Object, Text, Text, NullWritable> {
private final Record record = new Record();
private Command morphline;

@Override
protected void setup(Context context)
throws IOException, InterruptedException {
File morphlineConf = new File(context.getConfiguration()
.get(MORPHLINE_CONF));
String morphlineId = context.getConfiguration()
.get(MORPHLINE_ID);
MorphlineContext morphlineContext =
new MorphlineContext.Builder()
.build();

morphline = new org.kitesdk.morphline.base.Compiler()


.compile(morphlineConf,
morphlineId,
morphlineContext,
new RecordEmitter(context));
}

public void map(Object key, Text value, Context context)


throws IOException, InterruptedException {
record.put(Fields.ATTACHMENT_BODY,
new ByteArrayInputStream(
value.toString().getBytes(UTF8)));
if (!morphline.process(record)) {
System.out.println(
Morphline failed to process record: + record);
}

record.removeAll(Fields.ATTACHMENT_BODY);
}
}

Nel codice di MapReduce, modifichiamo RecordEmitter per estrarre il contenuto


dei Field dai record elaborati successivamente e salviamolo in questo contesto.
Questo ci permette di scrivere i dati in HDFS specificando un FileOutputFormat nello
standard di configurazione di MapReduce:
private static final class RecordEmitter implements Command {
private final Text line = new Text();
private final Mapper.Context context;

private RecordEmitter(Mapper.Context context) {


this.context = context;
}

@Override
public void notify(Record notification) {
}

@Override
public Command getParent() {
return null;
}

@Override
public boolean process(Record record) {
line.set(record.get(Fields.ATTACHMENT_BODY).toString());
try {
context.write(line, null);
} catch (Exception e) {
e.printStackTrace();
return false;
}
return true;
}
}

Ora possiamo intervenire sul comportamento della pipeline di elaborazione e


aggiungere altre trasformazioni dei dati modificando morphline.conf senza che
occorra cambiare esplicitamente la logica di istanziazione e di elaborazione.
Trovate il codice sorgente del driver di MapReduce alla pagina
http://bit.ly/1CmoA26.

Entrambi gli esempi possono essere compilati da ch9/kite/ con i seguenti


comandi:
$ ./gradlew jar
$ ./gradlew copyJar

Aggiungiamo le dipendenze di runtime a LIBJARS:


$ export KITE_DEPS=/home/cloudera/review/hadoop2book-private-reviews-
gabriele-ch8/src/ch8/kite/build/libjars/kite-example/lib
export LIBJARS=${LIBJARS},${KITE_DEPS}/kite-morphlines-core-
0.17.0.jar,${KITE_DEPS}/kite-morphlines-json-0.17.0.jar,${KITE_
DEPS}/metrics-core-3.0.2.jar,${KITE_DEPS}/metrics-healthchecks-
3.0.2.jar,${KITE_DEPS}/config-1.0.2.jar,${KITE_DEPS}/jackson-databind-
2.3.1.jar,${KITE_DEPS}/jackson-core-2.3.1.jar,${KITE_DEPS}/jackson-
annotations-2.3.0.jar

Possiamo eseguire il driver di MapReduce come segue:


$ hadoop jar build/libs/kite-example.jar \
com.learninghadoop2.kite.morphlines.MorphlineDriverMapReduce \
-libjars ${LIBJARS} \
morphline.conf \
read_tweets \
tweets.json \
morphlines-out

Il driver Java standalone pu essere eseguito con questo comando:


$ export CLASSPATH=${CLASSPATH}:${KITE_DEPS}/kite-morphlines-core-
0.17.0.jar:${KITE_DEPS}/kite-morphlines-json-0.17.0.jar:${KITE_
DEPS}/metrics-core-3.0.2.jar:${KITE_DEPS}/metrics-healthchecks-
3.0.2.jar:${KITE_DEPS}/config-1.0.2.jar:${KITE_DEPS}/jackson-databind-
2.3.1.jar:${KITE_DEPS}/jackson-core-2.3.1.jar:${KITE_DEPS}/jackson-
annotations-2.3.0.jar:${KITE_DEPS}/slf4j-api-1.7.5.jar:${KITE_DEPS}/
guava-11.0.2.jar:${KITE_DEPS}/hadoop-common-2.3.0-cdh5.0.3.jar
$ java -cp $CLASSPATH:./build/libs/kite-example.jar \
com.learninghadoop2.kite.morphlines.MorphlineDriver \
morphline.conf \
read_tweets tweets.json \
morphlines-out
Riepilogo
In questo capitolo abbiamo presentato quattro strumenti che facilitano lo
sviluppo su Hadoop. In particolare, abbiamo trattato quanto segue.
Come Hadoop Streaming permette di scrivere job di MapReduce utilizzando
linguaggi dinamici.
Come Kite Data semplifica linterfacciamento con sorgenti di dati eterogenee.
Lastrazione di alto livello fornita da Apache Crunch per scrivere pipeline di
job di Spark e MapReduce che implementano pattern di design comuni.
In che modo Morphlines consente di dichiarare catene di comandi e di
trasformazione dei dati che possono essere incorporate in qualsiasi base di
codice Java.
Nel prossimo capitolo sposteremo lattenzione dal campo dello sviluppo
software a quello dellamministrazione del sistema. Vedremo come impostare,
gestire e scalare un cluster Hadoop, considerando anche aspetti quali il
monitoraggio e la sicurezza.
Capitolo 10
Eseguire un cluster Hadoop

In questo capitolo sposteremo leggermente il nostro focus e ci soffermeremo su


alcune considerazioni relative allesecuzione di un cluster operativo di Hadoop. In
particolare, tratteremo i seguenti argomenti.
Perch uno sviluppatore dovrebbe preoccuparsi delle operations e perch
quelle con Hadoop sono diverse.
Un approfondimento su Cloudera Manager, le sue capacit e i suoi limiti.
Progettare un cluster da utilizzare sia sullhardware fisico sia su EMR.
Proteggere un cluster Hadoop.
Monitorare Hadoop.
Risoluzione dei problemi di unapplicazione che gira su Hadoop.
Sono uno sviluppatore, le operations
non mi interessano!
Prima di proseguire, vogliamo spiegarvi perch abbiamo inserito un capitolo
sulle operations in un libro destinato specificatamente agli sviluppatori. Chiunque
abbia sviluppato per piattaforme pi tradizionali (web app, programmazione dei
database e cos via) sa che di norma si tende a fare una netta distinzione tra il
gruppo che si occupa dello sviluppo e quello che si occupa delle operations. Il
primo costruisce il codice e lo assembla, mentre il secondo controlla e rende
operativo lambiente in cui il codice viene utilizzato.
In anni recenti, il movimento DevOps ha iniziato a concepire lidea secondo cui
sarebbe meglio per tutti se questa separazione venisse eliminata a favore di team
che lavorino in modo pi ravvicinato e coordinato. Quando si tratta di sistemi e
servizi basati su Hadoop, questo approccio diventa fondamentale.

Best practice per Hadoop e DevOps


Se vero che uno sviluppatore pu costruire concettualmente unapplicazione
pronta per essere rilasciata in YARN e poi dimenticarsene, la realt spesso pi
sfaccettata. Lo sviluppatore, per esempio, in genere desidera dire la sua sullentit
delle risorse che vengono allocate allapplicazione in fase di runtime, cos come,
una volta che lapplicazione operativa, lo staff delle operations vorr darle
unocchiata per meglio ottimizzare il cluster. Questa non la tipica divisione netta
delle responsabilit che si riscontra negli IT tradizionali. E probabilmente
meglio cos.
In altre parole, gli sviluppatori devono conoscere al meglio tutti gli aspetti
relativi alle operations, mentre il gruppo delle operations deve essere a
conoscenza di tutto quanto gli sviluppatori stanno facendo. Considerate questo
capitolo il nostro contributo a favorire questa discussione con il vostro staff
operations. Non intendiamo trasformarvi in amministratori esperti di Hadoop, un
ruolo emergente delicato e con un proprio insieme di competenze; vogliamo invece
guidarvi in un tour passo passo tra gli aspetti di cui dovete essere consapevoli e
che vi faciliteranno la vita una volta che le applicazioni gireranno su dei cluster
live.
Secondo il consueto approccio, toccheremo alcuni argomenti senza
approfondirli eccessivamente; se vi interessano, troverete alcuni link di
riferimento da consultare. Assicuratevi per di coinvolgere lo staff delle
operations!
Cloudera Manager
In questo libro, abbiamo usato la piattaforma pi comune, la Cloudera Hadoop
Distribution (CDH), con la sua QuickStart VM e la potente applicazione Cloudera
Manager. Con un cluster basato su Cloudera, Cloudera Manager diventer, almeno
allinizio, la vostra interfaccia principale con il sistema per gestire e monitorare il
cluster, quindi esploriamola un po.
Cloudera Manager dispone di una documentazione online ricca e articolata. Qui
non la riporteremo, ma cercheremo di evidenziare i punti in cui Cloudera Manager
si adatta ai vostri flussi di lavoro di sviluppo e operativi e in cosa potrebbe (o non
potrebbe) fare al caso vostro. Per accedere alla documentazione relativa alle
versioni pi recenti e precedenti di Cloudera Manager visitate lhome page
allindirizzo http://www.cloudera.com/content/support/en/documentation.html.

Pagare o non pagare?


Prima di entusiasmarsi per Cloudera Manager, importante consultare la
documentazione per quanto riguarda le funzionalit disponibili nella versione
gratuita e per sapere quali richiedono invece la sottoscrizione di unofferta a
pagamento. Se volete assolutamente qualche funzionalit presente solo nella
versione a pagamento ma non potete o non volete sborsare dei soldi, allora
Cloudera Manager, ed eventualmente lintera distribuzione di Cloudera, non fa per
voi. Torneremo sullargomento nel Capitolo 11.

Gestione dei cluster con Cloudera Manager


Se si lavora sulla QuickStart VM, Cloudera Manager lo strumento principale
da utilizzare per la gestione di tutti i servizi nel cluster. Se volete abilitare un
nuovo servizio utilizzerete Cloudera Manager, e lo stesso vale se volete
modificare una configurazione o aggiornare allultimo rilascio.
Anche se la gestione primaria del cluster compito dello staff operativo, in
quanto sviluppatori dovreste comunque acquisire un po di familiarit con
linterfaccia di Cloudera Manager, se non altro per capire esattamente come il
cluster configurato. Se i vostri job sono lenti, dare unocchiata alla
configurazione corrente in Cloudera Manager dovrebbe essere il primo passo da
compiere. La porta predefinita per le interfacce web di Cloudera Manager la
7180, quindi ci si pu connettere alla home page attraverso un URL come
http://<hostname>:7180/cmf/home (Figura 10.1).

Figura 10.1 Lhome page di Cloudera Manager.

Muovetevi un po nellinterfaccia per capirla meglio. Se per vi connettete con


un account utente con privilegi di amministratore, fate attenzione.
Fate clic sul link Clusters; si aprir un elenco dei cluster attualmente gestiti da
questa istanza di Cloudera Manager; da questo potete dedurre che una singola
istanza di Cloudera Manager pu gestire pi cluster, il che molto utile quando
avete diversi cluster distribuiti tra sviluppo e produzione.
Per ogni cluster espanso, ci sar un elenco dei servizi che girano su di esso.
Fate clic su un servizio, e vedrete un elenco di scelte supplementari. Selezionate
Configuration, e potrete iniziare a navigare tra i dettagli della configurazione di
quel servizio. Fate clic su Actions per ottenere alcune opzioni specifiche del
servizio, che solitamente includono linterruzione, il riavvio e altre azioni di
gestione del servizio.
Fate clic sullopzione Hosts invece che su Clusters, e potrete entrare nei
meandri dei server gestiti da Cloudera Manager, e vedere quali componenti del
servizio si trovano su ciascun host.

Cloudera Manager e altri strumenti di gestione


Lultimo richiamo potrebbe sollevare una domanda: come si integra Cloudera
Manager con altri sistemi di gestione? O meglio, nellottica della filosofia del
movimento DevOps, come si integra con gli strumenti privilegiati dagli ambienti
DevOps?
Volete una risposta sincera? Non molto bene. Per quanto il server di Cloudera
Manager principale possa essere gestito di per s con degli strumenti di
automazione, come Puppet o Chef, si presume esplicitamente che Cloudera
Manager controlli linstallazione e la configurazione di tutto il software che gli
necessario su tutti gli host che verranno inclusi nei suoi cluster. Per alcuni
amministratori, tutto questo rende lhardware dietro Cloudera Manager come una
gigantesca scatola nera; possono controllare linstallazione del sistema operativo,
ma tutto quanto va oltre il livello di configurazione viene gestito per intero da
Cloudera Manager. Non c molto da fare; cos e basta. Per ottenere i vantaggi di
Cloudera Manager, questo andr aggiunto come sistema di gestione
nellinfrastruttura, e il suo grado di adattamento allambiente operativo pi ampio
varier da caso a caso.

Monitorare con Cloudera Manager


Un discorso analogo pu essere fatto per il monitoraggio dei sistemi. Se per
iniziate a fare clic qua e l nellinterfaccia, vi accorgerete che Cloudera Manager
fornisce un set di strumenti ricchissimo per determinare la salute e le prestazioni
dei cluster gestiti.
tutto raccolto in ununica interfaccia: dai grafici sulle prestazioni delle query
di Impala, allo stato di un job mostrato per le applicazioni YARN, fino ai dati di
basso livello sui blocchi in HDFS. Successivamente nel capitolo ci occuperemo
delle sfide poste dalla risoluzione dei problemi su Hadoop, ma il fatto che
Cloudera Manager fornisca un singolo punto di visibilit lo rende uno strumento
eccezionale quando si vogliono determinare quali sono le condizioni di un cluster
e la sua performance. Parleremo del monitoraggio tra breve.

Trovare i file di configurazione


Una delle cose che crea maggiore confusione quando si esegue un cluster gestito
da Cloudera Manager trovare i file di configurazione da questo utilizzati. Nelle
release vanilla di Apache dei prodotti, come il core di Hadoop, i file si trovano
solitamente in /etc/hadoop, in /etc/hive per Hive, in /etc/oozie per Oozie e cos
via.
In un cluster gestito da Cloudera Manager, invece, i file di configurazione
vengono rigenerati ogni volta che il servizio viene avviato, e invece di risiedere
nelle directory /etc del file system, si trovano in /var/run/cloudera-scm-agent-
process/<pid>-<nome attivit>/, dove lultima directory potrebbe avere un nome
come 7007-yarn-NODEMANAGER. Pu sembrare strano per chi abituato a
lavorare sui cluster Hadoop pi vecchi o su altre distribuzioni che non si
comportano nella stessa maniera. Tuttavia, in un cluster controllato da Cloudera
Manager, spesso potrebbe essere pi semplice utilizzare linterfaccia web per
muoversi nella configurazione invece di cercare i file sottostanti. Qual
lapproccio migliore? una questione un po filosofica, e ogni team dovr
decidere quale metodo il pi adatto per le proprie esigenze.

API Cloudera Manager


Finora abbiamo effettuato una panoramica a volo duccello su Cloudera
Manager, ignorando del tutto un ambito che invece potrebbe essere cruciale per
alcune organizzazioni: Cloudera Manager offre unAPI che consente lintegrazione
delle sua capacit in altri sistemi e strumenti. Se largomento vi interessa,
consultate la documentazione.

Il lock-in di Cloudera Manager


Tutto questo ci conduce a una questione implicita nella discussione su Cloudera
Manager, e cio il grado di lock-in che comporta rispetto a Cloudera e alle sue
distribuzioni. Il lock-in potrebbe esistere solo in alcune forme; per esempio, il
codice dovrebbe essere portabile tra i cluster a prescindere dalle versioni, ma
riconfigurare un cluster in modo tale che utilizzi una distribuzione diversa
potrebbe non essere semplice. Il passaggio a unaltra distribuzione implica
unattivit di rimozione, riformattazione e reinstallazione completa.
Non vi diciamo di non usarlo, ma cinteressa che siate consapevoli del lock-in
insito in Cloudera Manager. Nel caso di team piccoli con un supporto operations
ridotto o uninfrastruttura gi presente, limpatto del lock-in probabilmente
controbilanciato dalle ottime funzionalit che Cloudera Manager offre.
Nel caso invece di team pi grandi o che lavorano in un ambiente in cui
lintegrazione con strumenti e processi esistenti ha un peso maggiore, la decisione
potrebbe non essere cos scontata. Studiate Cloudera Manager, parlatene con chi si
occupa delle operations e stabilite cos meglio per voi.
Notate che possibile scaricare e installare manualmente i vari componenti
della distribuzione di Cloudera senza usare Cloudera Manager per gestire il
cluster e i suoi host. Questo potrebbe essere un buon compromesso per alcuni
utenti, perch permette di utilizzare il software di Cloudera realizzando al
contempo la distribuzione e la gestione con strumenti specifici esistenti. anche
un modo per evitare potenzialmente la spesa supplementare per le funzionalit a
pagamento citate in precedenza.
Ambari, lalternativa open source
Ambari un progetto Apache ( http://ambari.apache.org) che, in teoria, fornisce
unalternativa open source a Cloudera Manager. la console di amministrazione
per la distribuzione di Hortonworks. Al momento della stesura di queste righe, i
dipendenti di Hortonworks sono anche la maggioranza di coloro che collaborano
al progetto.
Ambari, come ci si aspetta dalla sua natura, si basa su prodotti open source,
come Puppet e Nagios, per la gestione e il monitoraggio dei cluster. Ha anche una
funzionalit di alto livello simile a Cloudera Manager e cio linstallazione, la
configurazione, la gestione e il monitoraggio di un cluster Hadoop e alcuni
servizi al suo interno.
Quando si considera il progetto Ambari bisogna essere consapevoli che la
scelta non tra il lock-in totale a Cloudera e Cloudera Manager o a un cluster
gestito manualmente. Ambari fornisce uno strumento grafico che vale la pena
prendere in considerazione e tenere docchio nella sua evoluzione. Su un cluster
HDP, linterfaccia di Ambari equivalente allhome page di Cloudera Manager
mostrata in precedenza pu essere raggiunta allindirizzo
http://<hostname>:8080/#/main/dashboard (Figura 10.2).
Le operations nel mondo di Hadoop 2
Come illustrato nel Capitolo 2, alcune delle modifiche principali ad HDFS in
Hadoop 2 implicano la sua tolleranza ai guasti e una migliore integrazione con i
sistemi esterni. Non si tratta solo di una curiosit: le funzioni HA del NameNode,
in particolare, hanno costituito una differenza tuttaltro che trascurabile nella
gestione dei cluster dai tempi di Hadoop 1. Nel 2012 o gi di l, una parte
significativa della prontezza operativa di un cluster Hadoop di fronte al fallimento
del NameNode si otteneva con alcuni alleggerimenti e processi di ripristino.

Figura 10.2 Ambari.

In Hadoop 1, se il NameNode si rompeva e non avevate un backup del file dei


metadati fsimage di HDFS, perdevate laccesso a tutti i dati. Persi definitivamente i
metadati, persi anche i dati.
Hadoop 2 ha aggiunto il NameNode HA e tutti i meccanismi per farlo
funzionare. Inoltre vi sono componenti, come un gateway NFS a HDFS, che lo
rendono un sistema molto pi flessibile. Tuttavia questa funzionalit aggiuntiva
implica che ci siano pi parti non stabili. Per abilitare il NameNode HA, ci
sono altri componenti in JournalManager e FailoverController, e il gateway NFS
richiede implementazioni specifiche per Hadoop per quanto riguarda la portmap e
i servizi nfsd.
Hadoop 2 ha ora altri punti di integrazione estesa con i servizi esterni, oltre a
una selezione molto pi ampia di applicazioni e servizi che girano su di esso. Lo
si pu quindi ritenere utile dal punto di vista delle operations, perch ha barattato
la semplicit di Hadoop 1 con una complessit che lo rende una piattaforma
sostanzialmente pi potente.
Condividere le risorse
In Hadoop 1, lunica situazione in cui si poteva pensare alla condivisione delle
risorse era quando si doveva decidere quale scheduler utilizzare per il JobTracker
di MapReduce. Poich tutti i job alla fine venivano tradotti nel codice di
MapReduce, disporre di una policy di condivisione delle risorse a livello di
MapReduce era solitamente sufficiente per gestire i carichi di lavoro dei cluster.
Hadoop 2 e YARN hanno cambiato il quadro. Oltre a eseguire molti job di
MapReduce, un cluster potrebbe anche eseguire molte altre applicazioni sugli
ApplicationMaster di YARN. Tez e Spark sono a tutti gli effetti dei framework che
eseguono ulteriori applicazioni nelle relative interfacce.
Se tutto gira su YARN, questo fornisce le modalit per configurare lallocazione
massima delle risorse (in termini di CPU, memoria e poi I/O) consumate da
ciascun contenitore destinato a unapplicazione. Lobiettivo principale quello di
garantire che ci siano risorse sufficienti per la piena utilizzazione dellhardware,
evitando che ce ne siano di inutilizzate o di troppo utilizzate.
Le cose si fanno ancora pi interessanti quando delle applicazioni non YARN,
come Impala, girano sul cluster e vogliono prendersi parte delle risorse gi
allocate (soprattutto la memoria nel caso di Impala). Lo stesso potrebbe accadere,
per esempio, quando eseguite Spark sugli stessi host nella modalit non YARN, o
con altre applicazioni distribuite che potrebbero trarre vantaggio dal condividere
la stessa posizione sulle macchine Hadoop.
In Hadoop 2, il cluster va considerato come un ambiente multi-tenancy che
richiede una maggiore attenzione allallocazione delle risorse ai vari tenant.
Una soluzione ideale non esiste; la configurazione giusta dipender in tutto e per
tutto dai servizi che condividono la stessa posizione e dai carichi di lavoro che
elaborano. Questo un altro ambito in cui potreste lavorare da vicino con il team
delle operations, effettuando una serie di test con diverse soglie di carico per
determinare quali e quante sono le risorse richieste dai varie client e qual
lapproccio che pu garantire il massimo delluso e delle prestazioni. Il post degli
ingegneri di Cloudera allindirizzo http://bit.ly/1xyX3KT fornisce una panoramica sul
modo in cui affrontano questo problema, in particolare per quanto riguarda la
coesistenza efficace di Impala e MapReduce.
Costruire un cluster fisico
Prima di passare allallocazione delle risorse hardware, occorre compiere un
ulteriore passo: definire e selezionare lhardware utilizzato per il cluster. In questo
paragrafo ci occuperemo di un cluster fisico, e poi passeremo ad Amazon EMR.
Qualsiasi suggerimento sullhardware specifico sar gi datato nel momento in
cui leggere queste righe. Vi consigliamo di consultare i siti web dei vari produttori
delle distribuzioni Hadoop, che riportano nuovi articoli sulle configurazioni pi
recenti.
Invece di dirvi quanti core o gigabyte di memoria vi servono, porteremo la
scelta dellhardware a un livello leggermente superiore. La prima cosa che dovete
sapere che gli host che eseguono il vostro cluster Hadoop saranno probabilmente
diversi da quelli del resto della vostra organizzazione. Hadoop ottimizzato per
un hardware di basso costo, quindi, invece di vedere un numero ridotto di server
molto grandi, aspettatevi di vedere un numero molto pi grande di macchine con
meno funzioni. Non pensate per che Hadoop possa girare su qualsiasi residuato
bellico che avete in giro per lufficio. Potrebbe farlo, ma il profilo dei server
tipici di Hadoop di recente si comunque alzato, quindi lideale sarebbe avere dei
server di medio livello in cui i core/dischi/memoria massimi possono essere
ottenuti a un buon prezzo.
Prevedete anche dei requisiti diversi per le risorse per gli host che eseguono
servizi come il NameNode di HDFS o il ResourceManager di YARN e per i nodi
worker che memorizzano i dati ed eseguono la logica dellapplicazione. Nel primo
caso sono necessarie meno risorse per lo storage, ma spesso servono pi memoria
ed eventualmente dischi pi veloci.
Nel caso dei nodi worker di Hadoop, la cosa pi importante da ottenere il
rapporto giusto tra le tre categorie principali dellhardware, cio core, memoria e
I/O. Sar questo che guider le decisioni da prendere rispetto ai carichi di lavoro
e allallocazione delle risorse.
Per esempio, molti carichi di lavoro tendono a essere legati allI/O, e avere
spesso tanti contenitori allocati su un host quanti sono i dischi fisici pu
comportare un rallentamento generale, dovuto alla coesistenza dei dischi che
girano. Al momento della stesura di queste righe, si raccomandava che il numero
dei contenitori di YARN non superasse pi di 1,8 volte il numero dei dischi. Se
avete carichi di lavoro legati allI/O, potreste ottenere delle prestazioni migliori
aggiungendo altri host al cluster invece che cercando di avere pi contenitori che
girano, processori pi veloci o pi memoria sugli host correnti.
Per contro, se prevedete di eseguire simultaneamente molti job di Impala o
Spark, o comunque job che richiedono parecchia memoria, questa diventer
rapidamente la risorsa pi richiesta. Ecco perch, anche se potete ottenere consigli
aggiornati sullhardware dei cluster general purpose dai produttori delle
distribuzioni, dovrete sempre verificarli alla luce dei carichi di lavoro previsti e
adattarli di conseguenza. EMR una piattaforma eccezionale per esplorare i
requisiti delle risorse di molte applicazioni che possono determinare le decisioni
di acquisto dellhardware. EMR potrebbe essere lambiente che fa per voi; ne
parleremo in uno dei prossimi paragrafi.

Layout fisico
Se utilizzate un cluster fisico, dovrete considerare alcuni aspetti che su EMR
sono alla portata di tutti.

Conoscere i rack
I rack sono la prima cosa da conoscere nel caso di cluster abbastanza grandi da
usare pi di un rack nello spazio del data center. Come spiegato nel Capitolo 2,
quando HDFS colloca delle repliche dei nuovi file, cerca di porre la seconda
replica su un host che non sia il primo, e la terza replica in un rack diverso nel
caso di un sistema a pi rack. Lo scopo massimizzare la resilienza; anche se
fallisce lintero rack, rester almeno una replica disponibile. MapReduce utilizza
una logica analoga per cercare di bilanciare e distribuire al meglio le attivit.
Se non intervenite in alcun modo, ogni host verr specificato nellunico rack di
default. Se per il cluster cresce oltre questa soglia, dovrete aggiornare il nome
del rack.
Dietro le quinte, Hadoop determina il rack di un nodo eseguendo uno script
fornito dallutente che mappa lhostname del nodo sui nomi dei rack. Cloudera
Manager consente che questi nomi siano impostati su un dato host, recuperandoli
quando gli script specifici vengono chiamati da Hadoop. Per impostare il rack per
un host, fate clic su Hosts > <nomehost> > Assign Rack e assegnate il rack dalla
home page di Cloudera Manager.

Layout dei servizi


Come detto in precedenza, molto probabile che nel vostro cluster abbiate due
tipi di hardware: le macchine che eseguono i nodi worker e quelle che eseguono i
server. Quando distribuite un cluster fisico, dovete decidere quali servizi e loro
sottocomponenti eseguire su quali macchine fisiche.
Nel caso dei nodi worker la decisione piuttosto semplice; la maggior parte, se
non tutti i servizi, ha un modello di worker agent su tutti gli host worker. Nel caso
dei componenti master/server, invece, occorre applicarsi un po di pi. Se avete
tre nodi master, come distribuire i NameNode primari e di backup: il
ResourceManager di YARN, magari Hue, qualche server di Hive e un manager di
Oozie? Alcune di queste funzionalit sono HA, altre no. Man mano che aggiungete
servizi al cluster, vedrete anche crescere questo elenco di servizi.
In un mondo ideale, potreste avere un host per ogni servizio master, ma questo
solo in cambio di cluster molto grandi; nelle installazioni pi piccole, sarebbe
troppo costoso, oltre che inefficiente. Non ci sono per regole rigide: considerate
lhardware che avete a disposizione e cercate di distribuire il pi possibile i
servizi tra i nodi. Per esempio, non destinate due nodi ai due NameNode mettendo
poi tutto il resto su un terzo nodo. Pensate allimpatto che pu avere il fallimento
di un unico host e studiate un layout che lo riduca al minimo. Man mano che il
cluster cresce su pi rack, dovrete anche pensare a come sopravvivere ai
fallimenti di uno dei rack. Hadoop di per s vi d gi una mano, perch HDFS
cercher di fare in modo che ogni blocco di dati abbia delle repliche almeno su
due rack; questo tipo di resilienza tuttavia messa alla prova se, per esempio, tutti
i nodi master risiedono su un unico rack.

Aggiornare i servizi
Da sempre, laggiornamento di Hadoop unoperazione che porta via molto
tempo e in qualche modo rischiosa. Questo vale tuttora nel caso di cluster
distribuiti manualmente, ossia che non sono gestiti da uno strumento come
Cloudera Manager.
Cloudera Manager si prende in carico la parte dellattivit legata ai tempi, ma
non necessariamente il rischio. Qualsiasi aggiornamento dovrebbe sempre essere
considerato come unazione dagli esiti spesso imprevisti, tra cui un certo tempo di
inattivit. Niente pu sostituire un aggiornamento di prova eseguito su un cluster di
test; solo cos si pu capire perch importante trattare Hadoop come un
componente che ha un ciclo di vita di distribuzione pari a quello di qualsiasi altro
componente.
A volte un aggiornamento richiede la modifica dei metadati di HDFS, e
potrebbe influenzare il file system. proprio qui che sta il rischio. Oltre a
effettuare un aggiornamento di prova, sfruttate la possibilit di impostare HDFS in
modalit di upgrade, che crea una snapshot dello stato del file system precedente
allaggiornamento che verr conservata finch laggiornamento non viene
applicato. Pu rivelarsi molto utile, perch anche un aggiornamento che va storto e
corrompe i dati pu essere potenzialmente annullato.
Costruire un cluster su EMR
Elastic MapReduce una soluzione flessibile che, a seconda dei requisiti e dei
carichi di lavoro, pu risiedere su un cluster fisico di Hadoop o sostituirlo. Come
visto finora, EMR fornisce alcuni cluster precaricati e configurati con Hive,
Streaming e Pig, oltre che cluster di JAR personalizzati che consentono
lesecuzione di applicazioni di MapReduce.
Una seconda distinzione da fare quella tra cicli di vita transitori e a
esecuzione prolungata. Un cluster EMR transitorio viene generato su richiesta; i
dati sono caricati in S3 o HDFS, vengono elaborati alcuni workflow, i risultati di
output vengono salvati e il cluster viene chiuso automaticamente. Un cluster a
esecuzione prolungata viene mantenuto attivo anche dopo che il flusso di lavoro
terminato, e resta disponibile per copiarvi nuovi dati o per lesecuzione di nuovi
workflow. Sono i cluster pi adatti per i data warehouse o per lavorare con
dataset abbastanza grandi la cui elaborazione e il cui caricamento sarebbero
inefficaci se paragonati a un cluster transitorio.
In un documento imprescindibile per i potenziali utenti
(https://media.amazonwebservices.com/AWS_Amazon_EMR_Best_Practices.pdf), Amazon fornisce
uneuristica per determinare quale tipo di cluster pi adatto:
Se il numero di job per giorno * (tempo per impostare il cluster compreso il tempo di caricamento dei dati
di Amazon S3 usando Amazon S3 + tempo di elaborazione dei dati) < 24 ore, valutate ladozione di
cluster Amazon EMR transitori o di istanze fisiche. I cluster a esecuzione prolungata vengono istanziati
passando largomento alive al comando ElasticMapreduce, che abilita lopzione Keep Alive e
disabilita la terminazione automatica.

Notate che i due tipi di cluster condividono le stesse propriet e gli stessi limiti;
in particolare, i dati su HDFS non sono pi disponibili una volta che il cluster
viene chiuso.

Considerazioni sui file system


Nei nostri esempi abbiamo presupposto che i dati siano disponibili in S3. In
questo caso, in EMR viene montato un bucket come un file system s3n, e viene
utilizzato come sorgente di input oltre che come file system temporaneo per
salvare i dati parziali durante lelaborazione. Con S3 introduciamo un potenziale
overhead di I/O, operazioni come letture e scritture che innescano richieste GET e
PUT HTTP.

NOTA
EMR non supporta lo storage dei blocchi S3. LURI di s3 mappato su s3n.

Unaltra possibilit potrebbe essere quella di caricare i dati nellHDFS del


cluster e avviare lelaborazione da qui. In questo caso, otterremmo un I/O e un
posizionamento dei dati pi rapidi, ma perderemmo persistenza. Quando il cluster
viene chiuso, i dati spariscono. La regola dice che se eseguite un cluster transitorio
ha senso usare S3 come backend. Nella pratica, si dovrebbe monitorare e prendere
decisioni in base alle caratteristiche del flusso di lavoro. I job di MapReduce
iterativi e in pi passi traggono molti vantaggi da HDFS; si potrebbe dire che per
questi tipi di workflow sarebbero pi adatti dei motori di esecuzione come Tez o
Spark.

Ottenere i dati in EMR


Quando si copiano i dati HDFS in S3, consigliabile utilizzare s3distcp
(http://docs.aws.amazon.com/ElasticMapReduce/latest/DeveloperGuide/UsingEMR_s3distcp.html)
invece di Apache distcp o Hadoop distcp. Questo approccio si presta anche per il
trasferimento dei dati allinterno di EMR e da S3 a HDFS. Per spostare grandi
quantit di dati dal disco locale a S3, Amazon raccomanda di parallelizzare il
carico di lavoro utilizzando Jets3t o GNU Parallel. In generale, importante
ricordare che le richieste PUT a S3 sono incapsulate a 5 GB per file. Per caricare
file pi grandi, ci si deve affidare a Multipart Upload (https://aws.amazon.com/about-
aws/whats-new/2010/11/10/Amazon-S3-Introducing-Multipart-Upload/), unAPI che consente di

suddividere i file grandi in parti pi piccole per poi riassemblarle al momento


dellupload. I file possono essere copiati anche con strumenti come la CLI di AWS
o la famosa utility S3CMD, ma queste alternative non forniscono i vantaggi del
parallelismo offerti da s3distcp.

Istanze di EC2 e raffinamento


La dimensione di un cluster EMR dipende dalla dimensione del dataset, dal
numero di file e blocchi (che determina il numero di split) e dal tipo di carico di
lavoro (cercate di evitare lo spill su disco quando unattivit viene eseguita out
of memory). Come regola di base, una dimensione buona quella che massimizza
il parallelismo. Il numero di mapper e reducer per istanza, oltre che la dimensione
dellheap per il daemon della JVM, vengono in genere configurati da EMR quando
il cluster viene raffinato a seguito di modifiche alle risorse disponibili.
Raffinamento dei cluster
Oltre alle indicazioni precedenti relative a un cluster che gira su EMR, ci sono
alcuni aspetti da tenere a mente quando si eseguono carichi di lavoro su qualsiasi
tipo di cluster. La cosa ovviamente pi chiara quando lesecuzione avviene al di
fuori da EMR, poich questo astrae spesso alcuni dei dettagli.

Considerazioni sulla JVM


Il consiglio quello di operare con la versione a 64 bit di una JVM usando la
modalit server. I tempi della produzione di un codice ottimizzato si
allungheranno, ma si possono utilizzare strategie pi aggressive e riottimizzare il
codice nel tempo. Questo approccio si presta particolarmente per i servizi a
esecuzione prolungata, come i processi di Hadoop.
Accertatevi di allocare abbastanza memoria per la JVM per impedire pause
eccessivamente frequenti per la Garbage Collection (GC). Il collector simultaneo
mark-and-sweep attualmente il pi collaudato e consigliato per Hadoop. Il
collector Garbage First (G1) diventata lopzione privilegiata di GC in molti
altri carichi di lavoro fin dalla sua introduzione in JDK7, quindi vale la pena
tenere docchio le best practice suggerite man mano che evolve. Queste opzioni
possono essere impostate come argomenti Java personalizzati in Cloudera
Manager nella sezione di configurazione di ciascun servizio.

Il problema dei file piccoli


Lallocazione dellheap a processi Java sui nodi worker qualcosa di cui tener
conto quando si prende in considerazione la posizione del servizio. Esiste per un
caso particolare riguardante il NameNode: il problema dei file piccoli.
Hadoop ottimizzato per file molto corposi con dimensioni di blocco grandi,
ma a volte alcuni carichi di lavoro o sorgenti di dati trasferiscono molti file
piccoli su HDFS. Non lideale, perch in questo modo ogni attivit che elabora
un blocco alla volta legger solo una piccola parte dei dati prima del
completamento, comportando inefficienza.
Il fatto di avere molti file piccoli consuma anche pi memoria del NameNode;
questo trattiene in memoria la mappatura dai file ai blocchi e di conseguenza
conserva i metadati per ogni file e blocco. Se il numero di file e quindi di blocchi
aumenta rapidamente, lo stesso accadr con luso della memoria del NameNode. Il
NameNode toccher solo un sottoinsieme del sistema poich al momento della
stesura di queste righe 1 GB di memoria pu supportare 2 milioni di file o
blocchi; ma con una dimensione predefinita dellheap di 2 o 4 GB, questo limite
pu essere raggiunto facilmente. Se previsto che un NameNode parta in modo
aggressivo eseguendo la garbage collection o se dovesse esaurire la memoria,
allora il vostro cluster si troverebbe in una pessima situazione. Per alleggerirla,
potete assegnare pi heap alla JVM; sul lungo termine, invece, potete combinare
pi file piccoli in un numero pi ridotto di file grandi, ancora meglio se compressi
con un codec di compressione divisibile.

Ottimizzazione di map e reduce


I mapper e i reducer forniscono entrambi alcuni contesti in cui ottimizzare le
prestazioni; vediamo alcune indicazioni di cui tener conto.
Il numero di mapper dipende dal numero di split. Quando i file sono pi
piccoli della dimensione predefinita del blocco o sono compressi in un
formato non divisibile, il numero dei mapper equivarr a quello dei file.
Altrimenti, il numero dei mapper dato dalla dimensione totale di ciascun
file divisa per la dimensione del blocco.
Comprimete loutput dei mapper per ridurre le scritture su disco e aumentare
lI/O. Un buon formato per questo LZO.
Evitate lo spill su disco: i mapper dovrebbero avere abbastanza memoria per
trattenere la maggior quantit di dati possibile.
Numero dei reducer: si consiglia di usare meno reducer della capacit
complessiva dei reducer per evitare attese nellesecuzione.
Sicurezza
Quando avete finito di costruire il cluster, la prima cosa che vi siete chiesti
stata come proteggerlo, giusto? Non preoccupatevi, la maggior parte delle persone
non se lo chiede. Ma poich Hadoop non pi solo uno strumento che esegue
analisi interne nel reparto Ricerca bens anche uno strumento che guida
direttamente i sistemi critici, laspetto della sicurezza non pu essere trascurato a
lungo.
La protezione di Hadoop non qualcosa che si pu ottenere in un attimo o senza
test significativi. Non ci stancheremo mai di darvi dei consigli su questo
argomento, n di sottolineare quanto vada preso seriamente e affrontato nel modo
opportuno. un ambito che richiede tempo, e probabilmente soldi, ma pensate a
quanto pu costarvi la compromissione del vostro cluster
La sicurezza va poi molto oltre i cluster Hadoop. Considereremo qui alcune
delle funzioni di sicurezza disponibili in Hadoop, ma a voi toccher elaborare una
strategia di protezione coerente nella cui integrare i singoli componenti.

Evoluzione del modello di sicurezza di Hadoop


In Hadoop 1, non cera alcun tipo di protezione, poich il modello di sicurezza
fornito aveva dei punti facilmente attaccabili. LID utente Unix con il quale ci si
connetteva al cluster era considerato valido, e voi avevate tutti i privilegi di
quellutente; questo significava che chiunque avesse un accesso di amministratore
su un host che poteva arrivare al cluster poteva effettivamente impersonare
qualsiasi altro utente.
Questo port allo sviluppo del cosiddetto modello di accesso dellhead node,
nel quale il cluster Hadoop respingeva qualsiasi utente tranne uno, lhead node
appunto, e tutti gli accessi al cluster erano mediati da questo nodo controllato
centralmente. Questo approccio mitigava lassenza di un vero e proprio modello di
sicurezza, ed utile ancora oggi anche in situazioni in cui sono utilizzati schemi di
protezione pi ricchi.

Oltre lautorizzazione di base


In Hadoop sono state aggiunte altre funzioni di sicurezza che affrontano le
preoccupazioni sopra citate e altre.
Un cluster pu richiedere che un utente si autentichi attraverso Kerberos e che
dimostri di essere chi dice di essere.
In modalit protetta, il cluster pu usare Kerberos anche per tutte le
comunicazioni da nodo a nodo, garantendo che i nodi di comunicazione siano
autenticati e impedendo che dei nodi malintenzionati cerchino di unirsi al
cluster.
Per facilitare la gestione, gli utenti possono essere raccolti in gruppi per i
quali si possono definire alcuni privilegi di accesso ai dati. il Role-Based
Access Control (RBAC), ed un prerequisito per un cluster sicuro con un
certo numero di utenti. Le mappature utente-gruppo possono essere recuperate
da sistemi aziendali come LDAP o dalla directory attiva.
HDFS pu applicare le ACL per sostituire il modello corrente
proprietario/gruppo/mondo nello stile di Unix.
Queste capacit pongono Hadoop in una posizione decisamente pi forte che in
passato riguardo alla sicurezza, ma la community corre veloce, e stanno gi
nascendo altri progetti Apache dedicati per affrontare aree specifiche della
sicurezza.
Apache Sentry (https://sentry.incubator.apache.org) un sistema che permette di
fornire unautorizzazione pi granulare ai dati e ai servizi di Hadoop. Altri servizi
costruiscono mappature di Sentry, e questo consente per esempio di imporre
restrizioni specifiche non solo su determinate directory di HDFS, ma anche su
entit come le tabelle di Hive.
Laddove Sentry punta a strumenti pi ricchi per gli aspetti interni e specializzati
della sicurezza di Hadoop, Apache Knox (http://knox.apache.org) fornisce un
ingresso protetto ad Hadoop che si integra con i sistemi esterni di gestione
dellidentit, e offre dei meccanismi di controllo per consentire o rifiutare
laccesso a determinati servizi e operazioni di Hadoop. Per farlo, utilizza
uninterfaccia di tipo REST per Hadoop proteggendo tutte le chiamate per questa
API.
Il futuro della sicurezza di Hadoop
Nel mondo di Hadoop lo sviluppo non si ferma mai. Core Hadoop 2.5 ha
aggiunto alcuni attributi di file estesi ad HDFS che possono essere utilizzati come
base per ulteriori meccanismi di controllo dellaccesso. Versioni future
incorporeranno le capacit per meglio supportare la codifica dei dati in transito o
fermi; liniziativa Project Rhino guidata da Intel (https://github.com/intel-
hadoop/project-rhino/) sta costruendo un supporto pi ricco per i moduli crittografici

per il file system, un file system protetto e (prima o poi) uninfrastruttura di


gestione delle chiavi pi completa.
I produttori delle distribuzioni di Hadoop si muovono in fretta per aggiungere
queste funzionalit alle loro release; se quindi siete preoccupati della sicurezza (e
lo siete), consultate la documentazione relativa allultimo rilascio della vostra
distribuzione. Nuove caratteristiche di sicurezza vengono aggiunte continuamente
anche negli aggiornamenti, senza aspettare gli upgrade principali.

Conseguenze delluso di un cluster protetto


Dopo avervi stuzzicato con la solidit della sicurezza ora disponibile e dei
prodotti che stanno emergendo, corretto darvi un avvertimento. Spesso difficile
impostare bene la sicurezza, e altrettanto spesso avere la sensazione di una
sicurezza male impostata con una distribuzione piena di bachi peggio che sapere
di non essere protetti per niente.
Ammesso comunque che abbiate fatto tutto per bene, lavorare su un cluster
sicuro ha alcune conseguenze. Intanto rende le cose difficili agli amministratori, e
a volte anche agli utenti, quindi comporta un overhead. I vari strumenti e servizi di
Hadoop lavoreranno in modo diverso a seconda del tipo sicurezza implementata su
un cluster.
Oozie, di cui abbiamo parlato nel Capitolo 8, utilizza i suoi token di delega
dietro le quinte. Questo permette a un utente di Oozie di inviare dei job che
vengono eseguiti per conto dellutente originario; in un cluster che usa solo il
meccanismo di autorizzazione di base, la cosa si pu configurare facilmente, ma se
si utilizza Oozie in un cluster sicuro si dovr aggiungere una logica supplementare
alle definizioni del workflow e alla configurazione generale. Con Hadoop o Oozie
non un problema; tuttavia, come accade con la complessit che deriva
dallaggiunta di migliori funzioni di HA di HDFS in Hadoop 2, meccanismi
avanzati di protezione comportano dei costi e delle conseguenze di cui dovete
tener conto.
Monitorare
In precedenza nel capitolo abbiamo parlato di Cloudera Manager come di uno
strumento visivo di monitoraggio, e abbiamo suggerito che pu essere integrato in
modo programmatico con altri sistemi analoghi. Prima di implementare Hadoop in
un framework di monitoraggio qualsiasi, occorre valutare cosa significa
monitorare un cluster Hadoop dal punto di vista operativo.

Hadoop, dove i fallimenti non contano


I sistemi di monitoraggio tradizionali tendono a essere binari: qualcosa
funziona o non funziona. Un host vivo o morto; un server web server risponde o
non risponde. Nel mondo di Hadoop le cose vanno un po diversamente; quello
che conta la disponibilit del servizio, e questo pu essere trattato ancora come
attivo anche se parti dellhardware o del software falliscono. Nessun cluster
Hadoop dovrebbe avere problemi qualora un singolo nodo worker andasse in
crash. Da Hadoop 2, anche il fallimento di processi del server, come il
NameNode, non dovrebbero costituire una preoccupazione se configurata lHA.
Il monitoraggio di Hadoop dovr allora tener conto della salute del servizio e non
di quella delle macchine dellhost. Il team delle operations, che garantisce la
reperibilit 24 ore al giorno sette giorni su sette, non sar felice di essere chiamato
alle tre del mattino perch un nodo worker in un cluster di 10.000 saltato. vero
per che con il crescere della scala del cluster oltre un certo livello, il fallimento
dei singoli componenti hardware tuttaltro che raro.

Monitoraggio integrato
improbabile che costruiate da soli i vari strumenti di monitoraggio, mentre
pi probabile che ne integriate di gi esistenti, insieme ad altri framework. Nel
caso di alcuni strumenti open source, come Nagios e Zabbix, ci sono diversi
template di esempio che possono arricchire le metriche dei servizi e dei nodi di
Hadoop.
Tutto questo contribuisce alla separazione suggerita in precedenza; il fallimento
del ResourceManager di YARN potrebbe essere un evento ad altra criticit che
genera degli avvertimenti da inviare allo staff delle operations, mentre un carico
elevato su alcuni host specifici dovrebbe essere rilevato senza comportare linvio
degli avvisi. Abbiamo dunque avvisi attivati quando qualcosa va storto e
lindividuazione e linvio delle informazioni necessarie per investigare nei dati
del sistema nel tempo per effettuare unanalisi delle tendenze.
Cloudera Manager fornisce uninterfaccia REST, che si integra anchessa con
strumenti come Nagios, e che incorpora delle metriche dei servizi definite da
Cloudera Manager invece di doverne definire di proprie.
Nel caso di infrastrutture aziendali costruite su framework come IBM Tivoli o
HP OpenView per le quali il monitoraggio pi impegnativo ed esteso, Cloudera
Manager pu inviare gli eventi attraverso dei trap (cio degli avvisi) SNMP che
verranno raccolti da questi sistemi.

Metriche a livello di applicazione


Pu accadere che vogliate che le vostre applicazioni raccolgano delle metriche
che possano essere individuate a livello centrale dal sistema. I meccanismi per
ottenere questo risultato variano a seconda dei modelli computazionali, ma i pi
noti sono i contatori disponibili in MapReduce.
Quando un job di MapReduce completato, genera alcuni contatori che vengono
raccolti dal sistema durante tutta lesecuzione del job e che riguardano il numero
di attivit di map, i byte scritti, le attivit fallite e cos via.
Potete anche scrivere delle metriche specifiche per lapplicazione che
affiancheranno i contatori di sistema e che vengano raggruppate automaticamente
durante le attivit di map/reduce. Definite prima unenum Java e assegnate un nome
alle metriche desiderate:
public enum AppMetrics{
MAX_SEEN,
MIN_SEEN,
BAD_RECORDS
};

A seguire, nei metodi map, reduce, setup e cleanup delle vostre implementazioni di
map o reduce, potrete incrementare un contatore di uno, cos:

Context.getCounter(AppMetrics.BAD_RECORDS).increment(1);

Consultate il JavaDoc dellinterfaccia org.apache.hadoop.mapreduce.Counter per


ulteriori dettagli.
Risoluzione dei problemi
Il monitoraggio e la registrazione dei contatori o di informazioni supplementari
sono sicuramente di aiuto, ma imparare a trovare effettivamente le indicazioni
necessarie durante la risoluzione di un problema pu spaventare. In questo
paragrafo vedremo come Hadoop salva i dati dei log e di sistema. Possiamo
distinguere tre tipi di log.
applicazioni YARN, compresi i job di MapReduce;
log dei daemon (NameNode e ResourceManager);
servizi che registrano carichi di lavoro non distribuiti, per esempio il log di
HiveServer2 in /var/log.
Accanto a queste tipologie, Hadoop fornisce alcune metriche a livello di file
system (disponibilit dello storage, fattore di replica e numero di blocchi) e di
sistema.
Come gi detto, sia Apache Ambari sia Cloudera Manager, che centralizzano
laccesso alle informazioni di debug, fanno un buon lavoro come frontend.
Tuttavia, dietro le quinte, ciascun servizio registra su HDFS o sul file system a
nodo singolo. Inoltre, YARN, MapReduce e HDFS visualizzano i loro logfile e le
loro metriche attraverso interfacce web e API programmatiche.

Livelli di log
Di default, Hadoop registra i messaggi su Log4j. Log4j configurato attraverso
log4j.properties nel percorso delle classi. Il file definisce cosa viene registrato e in

che forma:
log4j.rootLogger=${root.logger}
root.logger=INFO,console
log4j.appender.console=org.apache.log4j.ConsoleAppender
log4j.appender.console.target=System.err
log4j.appender.console.layout=org.apache.log4j.PatternLayout
log4j.appender.console.layout.ConversionPattern=%d{yy/MM/dd HH:mm:ss} %p %c{2}: %m%n

Il logger di root predefinito INFO,console, che registra tutti i messaggi al livello


INFO e sullo stderr della console. Le singole applicazioni distribuite su Hadoop

possono avere le proprie log4j.properties e impostare il livello e altre propriet dei


log emessi secondo necessit.
I daemon di Hadoop hanno una pagina web dove possibile determinare e
impostare il livello di log per qualsiasi propriet di Log4j. Questa interfaccia
viene esposta dallendpoint /LogLevel in tutte le interfacce utente dei servizi web.
Per abilitare la registrazione del debug per la classe ResourceManager, accedete a
http://resourcemanagerhost:8088/LogLevel (Figura 10.3).

In alternativa, il comando daemonlog <host:port> di YARN fa da interfaccia con


lendpoint service /LogLevel. Possiamo analizzare il livello associato a
mapreduce.map.log.level per la classe ResourceManager utilizzando il parametro getlevel

<property>:

$ hadoop daemonlog -getlevel localhost.localdomain:8088 mapreduce.map.


log.level
Connecting to http://localhost.localdomain:8088/logLevel?log=mapreduce.
map.log.level Submitted Log Name: mapreduce.map.log.level Log Class: org.
apache.commons.logging.impl.Log4JLogger Effective level: INFO

Figura 10.3 Determinare e impostare il livello del log nel ResourceManager.

Il livello vero e proprio pu essere modificato usando lopzione -setlevel


<property> <level>:

$ hadoop daemonlog -setlevel localhost.localdomain:8088 mapreduce.map.


log.level DEBUG
Connecting to http://localhost.localdomain:8088/logLevel?log=mapreduce.
map.log.level&level=DEBUG
Submitted Log Name: mapreduce.map.log.level
Log Class: org.apache.commons.logging.impl.Log4JLogger
Submitted Level: DEBUG
Setting Level to DEBUG ...
Effective level: DEBUG

Notate che questa impostazione influir su tutti i log prodotti dalla classe
ResourceManager, compresi quelli generati dal sistema e dalle applicazioni che girano

su YARN.
Accedere ai logfile
La posizione dei logfile e le convenzioni di denominazione in genere
differiscono a seconda della distribuzione. Apache Ambari e Cloudera Manager
accentrano laccesso ai logfile, sia per i servizi sia per le singole applicazioni.
Sulla Cloudera QuickStart VM, trovate una panoramica dei processi attualmente in
esecuzione e dei link ai rispettivi logfile, i canali stderr e stdout, allindirizzo
http://localhost.localdomain:7180/cmf/hardware/hosts/1/processes (Figura 10.4).

Ambari fornisce un riepilogo simile attraverso la dashboard Services


allindirizzo http://127.0.0.1:8080/#/main/services su HDP Sandbox (Figura 10.5).
I log non distribuiti si trovano solitamente in /var/log/<servizio> su ogni nodo
del cluster. Anche i contenitori di YARN e le posizioni dei log di MRv2 dipendono
dalla distribuzione. Su CDH5 queste risorse sono disponibili in HDFS sotto a
/tmp/logs/<utente>.
La modalit standard per accedere ai log distribuiti passa dagli strumenti a riga
di comando o dalluso delle interfacce utente dei servizi web.

Figura 10.4 Accedere alle risorse di registro in Cloudera Manager.


Figura 10.5 Accedere alle risorse di registro in Apache Ambari.

Per esempio, il comando


$ yarn application -list -appStates ALL

elencher tutte le applicazioni YARN in esecuzione o ritirate. LURL nella


colonna dellattivit punta a uninterfaccia web che ne visualizza il log, come
segue:
14/08/03 14:44:38 INFO client.RMProxy: Connecting to ResourceManager
at localhost.localdomain/127.0.0.1:8032 Total number of applications
(application-types: [] and states: [NEW, NEW_SAVING, SUBMITTED, ACCEPTED,
RUNNING, FINISHED, FAILED, KILLED]):4 Application-Id Application-Name
Application-Type User
Queue State Final-State Progress
Tracking-URL application_1405630696162_0002 PigLatin:DefaultJobName MAPREDUCE
cloudera root.cloudera FINISHED SUCCEEDED 100%
http://localhost.localdomain:19888/jobhistory/job/job_1405630696162_0002
application_1405630696162_0004 PigLatin:DefaultJobName MAPREDUCE cloudera
root.cloudera FINISHED SUCCEEDED 100%
http://localhost.localdomain:19888/jobhistory/job/job_1405630696162_0004
application_1405630696162_0003 PigLatin:DefaultJobName MAPREDUCE cloudera
root.cloudera FINISHED SUCCEEDED 100%
http://localhost.localdomain:19888/jobhistory/job/job_1405630696162_0003
application_1405630696162_0005 PigLatin:DefaultJobName MAPREDUCE cloudera
root.cloudera FINISHED SUCCEEDED 100%
http://localhost.localdomain:19888/jobhistory/job/job_1405630696162_0005

Per esempio, http://localhost.localdomain:19888/jobhistory/job/job_1405630696162_0002,


un link a unattivit che appartiene allutente cloudera, il frontend del contenuto
salvato in hdfs:///tmp/logs/cloudera/logs/application_1405630696162_0002/.
Nei prossimi paragrafi forniremo una panoramica delle interfacce utente
disponibili per i vari servizi.
SUGGERIM ENT O
Se si fornisce lopzione log-uri s3://<bucket> a un cluster EMR, si potr essere sicuri che i
log di Hadoop vengano copiati nella posizione s3://<bucket>.

ResourceManager, NodeManager e Application


Manager
Su YARN, linterfaccia utente web del ResourceManager fornisce informazioni
e statistiche generali sui job del cluster Hadoop, sui job in
esecuzione/completati/falliti e un logfile cronologico del job. Di default,
linterfaccia si trova allindirizzo http://<resourcemanagerhost>:8088/ (Figura 10.6).

Applicazioni
Nel riquadro di sinistra troviamo un elenco dei vari stati possibili
dellapplicazione: NEW, SUBMITTED, ACCEPTED, RUNNING, FINISHING,
FINISHED, FAILED e KILLED. A seconda dello stato, sono disponibili le
seguenti informazioni:
lID dellapplicazione;
lutente;
il nome dellapplicazione;
la coda dello scheduler in cui viene posta lapplicazione;
gli orari e lo stato di inizio/fine;
il link allinterfaccia utente di Tracking per una cronologia dellapplicazione.
Figura 10.6 Il Resource Manager.

La vista Cluster Metrics fornisce poi alcuni dati su questi aspetti:


lo stato complessivo dellapplicazione;
il numero di contenitori in esecuzione;
luso della memoria;
lo stato dei nodi.

Vista Nodes
La vista Nodes un frontend del menu del servizio del NodeManager, che
mostra lo stato di salute e la posizione delle applicazioni in esecuzione sul nodo
(Figura 10.7).

Figura 10.7 Lo stato dei nodi.

Ogni singolo nodo del cluster mostra ulteriori informazioni e statistiche a livello
di host attraverso la sua interfaccia utente. Si tratta di dati quali la versione di
Hadoop in esecuzione sul nodo, quanta memoria disponibile su di esso, lo stato
del nodo e un elenco delle applicazioni in esecuzione e dei contenitori (Figura
10.8).

Figura 10.8 Informazioni su un singolo nodo.

Finestra Scheduler
La Figura 10.9 mostra la finestra Scheduler.

Figura 10.9 Scheduler.

MapReduce
Per quanto le stesse informazioni e i dettagli dei log siano disponibili sia in
MapReduce v1 sia in MapReduce v2, la modalit di accesso leggermente
diversa.
MapReduce v1
La Figura 10.10 mostra linterfaccia utente del JobTracker di MapReduce.
disponibile di default allindirizzo http://<jobtracker>:50070, e mostra informazioni
su tutto quanto attualmente in esecuzione, oltre che sui job di MapReduce ritirati,
insieme a un riepilogo delle risorse e dello stato di salute del cluster e
informazioni sui tempi programmazione e la percentuale di completamento (Figura
10.11).

Figura 10.10 Linterfaccia utente del Job Tracker.


Figura 10.11 I dettagli del job.

Sono disponibili i dettagli per ogni job in esecuzione e ritirato, compresi lID, il
proprietario, la priorit, lassegnazione delle attivit e lavvio dellattivit per il
mapper. Facendo clic su un link jobid si aprir una pagina di riepilogo (lo stesso
URL esposto dal comando mapred job list), che visualizza i dettagli sulle attivit di
map e reduce e alcune statistiche generali del contatore ai livello di job, file system e

MapReduce; tra queste vi sono la memoria usata, il numero di operazioni di


lettura/scrittura e il numero di byte letti e scritti.
Per ogni operazione di map e reduce, il JobTracker mostra le attivit totali, in
attesa, in esecuzione, completate e fallite (Figura 10.12).
Figura 10.12 Panoramica sulle attivit del job.

Facendo clic sui link nella tabella Job si apre unulteriore pagina dettagliata
sullattivit e i suoi tentativi (Figura 10.13).

Figura 10.13 Tentativi dellattivit.

Da qui, possiamo accedere ai log di ogni tentativo, sia per le attivit riuscite sia
per quelle fallite o soppresse per ogni singolo host del TaskTracker. Questo log
contiene le informazioni pi granulari sullo stato del job di MapReduce, compreso
loutput degli appender di Log4j oltre che delloutput convogliato sui canali stdout
e stderr e il syslog (Figura 10.14).
Figura 10.14 I log del TaskTracker.

MapReduce v2 (YARN)
Come abbiamo visto nel Capitolo 3, con YARN, MapReduce solo uno dei
numerosi framework di elaborazione che possono essere distribuiti. Ricorderete
dai capitoli precedenti che i servizi del JobTracker e del TaskTracker sono stati
sostituiti rispettivamente dal Resource Manager e dal NodeManager.
In quanto tali, le interfacce utente del servizio e dei logfile di YARN sono pi
generiche di quelle di MapReduce v1. Il nome application_1405630696162_0002
mostrato nel ResourceManager corrisponde a un job di MapReduce con ID
job_1405630696162_0002. LID dellapplicazione appartiene allattivit in
esecuzione nel contenitore, e facendo clic su di esso si pu ottenere una
panoramica sul job di MapReduce e scendere di livello tra le singole attivit nelle
varie fasi finch non si raggiunge il log dellattivit specifica (Figura 10.15).
Figura 10.15 Unapplicazione YARN che contiene un job di MapReduce.

JobHistory
YARN offre un servizio REST JobHistory che visualizza i dettagli sulle
applicazioni completate. Attualmente supporta solo MapReduce e fornisce
informazioni sui job portati a termine. Tra queste abbiamo lo stato finale del job
SUCCESSFUL o FAILED , chi ha inviato il job, il numero totale delle attivit di
map e reduce e informazioni temporali.

Trovate uninterfaccia utente allindirizzo http:///<jobhistoryhost<:19888 (Figura


10.16).
Figura 10.16 Linterfaccia utente di JobHistory.

Facendo clic sullID di un job si ottiene linterfaccia del job di MapReduce


mostrata nella schermata dellapplicazione YARN.

NameNode e DataNode
Linterfaccia web per lHadoop Distributed File System (HDFS) mostra le
informazioni sul NameNode e sul file system in generale. Di default, si trova
allindirizzo http://<namenodehost>:50070/ (Figura 10.17).
Figura 10.17 Linterfaccia utente del NameNode.

Il menu Overview visualizza le informazioni del NameNode sulla capacit DFS


e sulluso e lo stato del pool del blocco, oltre che un riepilogo sullo stato di salute
e la disponibilit del DataNode. I dettagli contenuti in questa pagina equivalgono
per la gran parte a quanto appare al prompt della riga di comando:
$ hdfs dfsadmin report

Il menu DataNodes fornisce informazioni ancora pi dettagliate sullo stato di


ciascun nodo e offre uninvestigazione approfondita a livello di singolo host, sia
per i nodi disponibili sia per quelli ritirati (Figura 10.18).
Figura 10.18 Linterfaccia utente del DataNode.
Riepilogo
In questo capitolo abbiamo affrontato un passo alla volta gli aspetti legati
allesecuzione di un cluster operativo Hadoop. Non abbiamo cercato di
trasformare gli sviluppatori in amministratori, ma questa prospettiva pi ampia vi
aiuter probabilmente a collaborare meglio con lo staff delle operations. In
particolare, abbiamo trattato i seguenti argomenti.
Come Hadoop si adatta naturalmente agli approcci del movimento DevOps:
la sua complessit fa s che non sia accettabile n desiderabile la presenza di
uno scarto tra quanto sa lo staff di sviluppo e quanto sa lo staff delle
operations.
Cloudera Manager e la sua forza come strumento di gestione e monitoraggio;
pu sollevare tuttavia dei problemi di integrazione qualora lavoriate con altri
strumenti aziendali, e ha in s un rischio di lock-in.
Ambari, lalternativa open source di Apache a Cloudera Manager, e come
viene utilizzato nella distribuzione di Hortonworks.
Come scegliere lhardware per un cluster fisico di Hadoop, e come la scelta
si inserisce nelle considerazioni relative alla coesistenza senza conflitti su
risorse condivise di carichi di lavoro multipli di Hadoop 2.
Perch optare per cluster EMR e come questi possono essere tanto
unaggiunta quanto unalternativa ai cluster fisici.
Lecosistema di sicurezza di Hadoop, come si sta trasformando velocemente e
come le funzionalit oggi disponibili siano decisamente migliori di quelle di
qualche anno fa (e c ancora di pi dietro langolo).
Monitorare un cluster Hadoop, tenendo presente quali eventi sono importanti
e contano di pi nellapproccio di Hadoop ai fallimenti, e come gli avvisi e
le metriche possono essere integrati in altri framework di monitoraggio
aziendali.
Come risolvere i problemi di un cluster Hadoop, sia nei termini di quello che
sarebbe potuto succedere sia per trovare informazioni utili per la proprie
analisi.
Un rapido giro tra le varie interfacce utente fornite da Hadoop, che offrono un
colpo docchio sugli accadimenti allinterno dei vari componenti del sistema.
Con questo capitolo si conclude la trattazione approfondita di Hadoop. Nel
prossimo capitolo troverete alcune riflessioni sullecosistema pi ampio di
Hadoop; vi daremo alcuni suggerimenti su strumenti e prodotti utili e interessanti
che non abbiamo lopportunit di affrontare nel libro e vi indicheremo come
partecipare al lavoro della community.
Capitolo 11
Come proseguire

Nei capitoli precedenti abbiamo esaminato molte parti di Hadoop 2 e del suo
ecosistema. Tuttavia, per ragioni di spazio, non abbiamo potuto approfondire certi
argomenti, ad altri abbiamo solo accennato e di alcuni non abbiamo parlato affatto.
Lecosistema di Hadoop, con le varie distribuzioni progetti Apache e non
Apache , un contesto incredibilmente vivace e attivo. In questo capitolo
intendiamo completare il materiale precedentemente affrontato nel dettaglio
proponendo una sorta di guida verso altre destinazioni stimolanti. In particolare,
tratteremo quanto segue.
Distribuzioni di Hadoop.
Altri progetti interessanti, Apache e non Apache.
Risorse di informazione e di aiuto.
Tenete conto che, ovviamente, qualsiasi panoramica sullecosistema risente dei
nostri interessi e delle nostre preferenze, e che quando leggerete queste righe
probabilmente sar gi datata.
In altre parole, non pensate neanche per un momento che quanto presentato qui
sia tutto quello che disponibile: solo uno stuzzichino!
Distribuzioni alternative
In genere, in questo libro abbiamo utilizzato la distribuzione Cloudera per
Hadoop, ma abbiamo cercato di mantenere la trattazione il pi indipendente
possibile dalla distribuzione. Abbiamo anche parlato dellHortonworks Data
Platform (HDP), ma queste non sono le uniche opzioni di distribuzione
disponibili.
Prima di dare unocchiata in giro, valutate se vi serve una distribuzione.
Potreste accedere al sito web di Apache, scaricare i tarball dei progetti che vi
interessano e lavorare per assemblare il tutto. Tuttavia, considerate le dipendenze
della versione, probabile che loperazione impegni pi tempo del previsto.
Inoltre, al prodotto finale mancher probabilmente una rifinitura in termini di
strumenti o script per il rilascio e la gestione operativa. Questi sono i motivi per
cui la maggior parte degli utenti preferisce utilizzare una distribuzione di Hadoop
esistente.
Una nota sulle estensioni gratuite e commerciali: essendo questo un progetto
open source con una licenza piuttosto aperta, i creatori della distribuzione sono
anche liberi di migliorare Hadoop con alcune estensioni proprietarie che vengono
rese disponibili gratuitamente come open source o come prodotti commerciali.
Potrebbe crearsi una situazione conflittuale, perch alcuni difensori dellopen
source non gradiscono la commercializzazione dei loro progetti di successo; ai
loro occhi come se lentit commerciale godesse dei frutti del lavoro della
community open source senza impegnarsi in prima persona. Per altri invece un
segno di salute della licenza Apache flessibile; il prodotto rimarr sempre gratuito,
e i singoli e le aziende possono decidere se procedere o meno con le estensioni
commerciali. Non diamo un giudizio, ma tenete presente che una questione che vi
troverete ad affrontare quasi sicuramente.
Dovete quindi valutare se vi occorre una distribuzione e se s per quali ragioni:
che vantaggi vi darebbe rispetto al farne a meno? Sceglierete un prodotto del tutto
open source o siete disposti a pagare per le estensioni commerciali? Con queste
domande in mente, diamo unocchiata ad alcune delle distribuzioni principali.
Distribuzione di Cloudera per Hadoop
La distribuzione di Cloudera (http://www.cloudera.com) dovrebbe esservi familiare,
visto che labbiamo utilizzata in tutto il libro. CDH stata la prima distribuzione
alternativa ampiamente disponibile, e la sua portata in termini di software, elevato
livello di qualit e gratuit lha resa una scelta molto popolare.
Recentemente, Cloudera sta estendendo i prodotti che inserisce nelle sue
distribuzioni oltre i progetti core di Hadoop. Accanto a Cloudera Manager e
Impala (entrambi sviluppati da Cloudera), ha aggiunto altri strumenti come
Cloudera Search (basato su Apache Solr) e Cloudera Navigator (una soluzione di
amministrazione dei dati). Mentre le versioni CDH precedenti alla 5 erano
maggiormente concentrate sui vantaggi dellintegrazione di una distribuzione, la
versione 5 (e presumibilmente anche quelle successive) sta inserendo sempre pi
capacit nella base dei progetti Hadoop Apache.
Cloudera offre inoltre un supporto commerciale per i suoi prodotti, insieme a
servizi di formazione e consulenza. Trovate i dettagli sulla pagina web della
compagnia.

Hortonworks Data Platform


Nel 2011, dalla divisione di Yahoo! responsabile di gran parte dello sviluppo di
Hadoop nacque una nuova compagnia chiamata Hortonworks, che produceva una
distribuzione Hadoop pre-integrata denominata Hortonworks Data Platform
(HDP; http://hortonworks.com/products/hortonworksdataplatform/).
Da un punto di vista concettuale, HDP simile a CDH, ma i due prodotti hanno
focus diversi. Hortonworks trae vantaggio dal fatto che HDP completamente
open source, compreso lo strumento di gestione Ambari, di cui abbiamo parlato
brevemente nel Capitolo 10. Inoltre pone HDP come una piattaforma di
integrazione chiave attraverso il suo supporto a strumenti quali Talend Open
Studio. Hortonworks non offre software proprietario; il suo business model si
concentra invece sullofferta di servizi professionali e di un supporto per la
piattaforma.
Cloudera e Hortonworks sono imprese con una notevole competenza
ingegneristica, e in entrambe lavorano molti dei collaboratori pi prolifici di
Hadoop. La tecnologia sottostante rimane comunque la stessa dei progetti Apache;
i fattori discriminanti sono il modo in cui i progetti vengono assemblati, la
versione impiegata e il valore aggiunto delle offerte specifiche delle due
compagnie.

MapR
Un tipo differente di distribuzione offerto da MapR Technologies (la
compagnia e la distribuzione sono solitamente indicate come MapR). La
distribuzione disponibile allindirizzo http://www.mapr.com basata su Hadoop, ma
prevede alcune modifiche e miglioramenti.
MapR si concentra sulle prestazioni e la disponibilit. Per esempio, stata la
prima distribuzione a offrire una soluzione HA per il NameNode e il JobTracker di
Hadoop, che, come ricorderete dal Capitolo 2, era un punto debole significativo in
Hadoop 1. Ha anche offerto unintegrazione nativa con i file system NFS molto
prima di Hadoop 2, facilitando di parecchio lelaborazione dei dati esistenti. Per
raggiungere questa funzionalit, MapR ha sostituito HDFS con un file system
completamente compatibile con POSIX che non prevede NameNode, producendo
cos un vero sistema distribuito senza nodo master, e un uso migliore
dellhardware rispetto allHDFS di Apache.
MapR fornisce unedizione community ed enterprise della sua distribuzione. Nel
prodotto gratuito non sono disponibili tutte le estensioni. La compagnia offre anche
servizi di supporto come parte delliscrizione alla versione enterprise del
prodotto, oltre a servizi di formazione e consulenza.

E il resto
Le distribuzioni di Apache Hadoop non sono solo territorio delle giovani start-
up, n rappresentano un mercato statico. Intel aveva una propria distribuzione fino
ai primi mesi del 2014, quando ha deciso di inglobare le modifiche in CDH. IBM
ha una sua distribuzione chiamata IBM Infosphere Big Insights, disponibile in
unedizione gratuita e in una commerciale. Alcuni grandi compagnie che
producono distribuzioni proprie hanno poi diversi store, alcuni apertamente
disponibili, mentre altri no. Non avete che limbarazzo della scelta
Scegliere una distribuzione
Come scegliere una distribuzione? Come abbiamo visto, quelle disponibili (e
non le abbiamo trattate tutte) variano da package e integrazioni di prodotti
completamente open source fino a soluzioni di integrazione e analisi su misura.
Non esiste la distribuzione migliore in assoluto: pensate alle vostre esigenze e
valutate le alternative. Poich offrono tutte la possibilit di scaricare gratuitamente
almeno una versione di base, pu essere una buona idea provarne qualcuna
direttamente.
Altri framework di calcolo
Abbiamo parlato pi volte della miriade di possibilit portate nella piattaforma
Hadoop da YARN. Abbiamo visto nel dettaglio due modelli nuovi, Samza e Spark.
Sul framework sono stati portati anche altri framework collaudati, come Pig.
Per fornire un quadro dinsieme pi ampio, in questo paragrafo illustreremo la
portata dellelaborazione possibile presentando una serie di modelli di calcolo
attualmente disponibili per Hadoop su YARN.

Apache Storm
Storm (http://storm.apache.org) un framework di calcolo distribuito scritto quasi
per intero nel linguaggio di programmazione Clojure. Utilizza sprout e bolt creati
dagli utenti per definire fonti di informazione e manipolazioni che consentono
lelaborazione distribuita dei dati in streaming. Unapplicazione Storm concepita
come una topologia di interfacce che crea uno stream di trasformazioni. Fornisce
una funzionalit simile a un job di MapReduce tranne per il fatto che, in teoria,
questa topologia continua a essere eseguita a tempo indeterminato finch non viene
interrotta manualmente.
stata costruita inizialmente come distinta da Hadoop, ma Yahoo! sta
sviluppando il porting di YARN (https://github.com/yahoo/storm-yarn).

Apache Giraph
Giraph nata come implementazione open source del documento Pregel di
Google (http://kowshik.github.io/JPregel/pregel_paper.pdf). Sia Giraph sia Pregel si
ispirano al modello del calcolo distribuito Bulk Synchronous Parallel (BSP)
introdotto da Valiant nel 1990. Giraph aggiunge ulteriori funzionalit compreso il
calcolo sul master, gli aggregatori e il calcolo parallelo. Trovate il porting di
YARN allindirizzo https://issues.apache.org/jira/browse/GIRAPH-13.

Apache HAMA
Hama un progetto Apache di alto livello che mira, come altri metodi che
abbiamo incontrato finora, ad azzerare i punti deboli di MapReduce rispetto alla
programmazione iterativa. Simile al gi citato Giraph, Hama implementa le
tecniche BSP e si ispira nella sua essenza al documento Pregel. Trovate il porting
di YARN alla pagina https://issues.apache.org/jira/browse/HAMA-431.
Altri progetti interessanti
Che utilizziate una distribuzione in bundle o che vi limitiate al download
standard di Apache Hadoop, troverete diversi riferimenti a progetti correlati. In
questo libro ne abbiamo trattati molti, come Hive, Samza e Crunch; facciamo un
rapido accenno ad altri.
Ricordate che lobiettivo evidenziare alcuni strumenti interessanti (dal punto
di vista dellautore) e suggerire la vastit dei progetti disponibili. Come gi detto,
guardatevi intorno, perch ne vengono lanciati sempre di nuovi.

HBase
Il progetto correlato ad Apache Hadoop forse pi famoso che non abbiamo
considerato in queste pagine HBase (http://hbase.apache.org). Basato sul modello
BigTable per lo storage dei dati diffuso da Google in un documento accademico
(vi suona familiare?), HBase un datastore non relazionale che risiede su HDFS.
Laddove MapReduce e Hive si concentrano su pattern di accesso ai dati di tipo
batch, HBase cerca di fornire un accesso ai dati a bassissima latenza. Di
conseguenza, a differenza delle altre tecnologie citate, pu supportare direttamente
i servizi di interazione con lutente.
Il modello di dati di HBase non segue lapproccio relazionale che era stato
utilizzato in Hive e in tutti gli altri RDBMS, n offre quelle garanzie di ACID
(Atomicity, Consistency, Isolation, Durability) che sono date per scontate con gli
store relazionali. invece una soluzione senza schema chiave-valore che prende
una vista dei dati orientata alle colonne; queste possono essere aggiunte in fase di
runtime e dipendono dai valori immessi in HBase. Ogni operazione di ricerca
risulta quindi molto rapida, poich consiste in una mappatura efficace dalla chiave
della riga alla colonna desiderata. HBase considera anche i timestamp come
unaltra dimensione sui dati, cos che sia possibile recuperare direttamente i dati
da un punto nel tempo.
Il modello di dati molto potente, ma non si adatta a tutti i casi duso, proprio
come il modello relazionale non applicabile universalmente. Se per vi
occorrono viste strutturate a bassa latenza di dati su vasta scala salvati in Hadoop,
allora HBase unopzione assolutamente da considerare.

Sqoop
Nel Capitolo 7, abbiamo introdotto gli strumenti per presentare uninterfaccia di
tipo relazionale per i dati salvati in HDFS. Spesso necessario recuperare questi
dati da un database relazionale esistente o salvare loutput di elaborazione.
Apache Sqoop (http://sqoop.apache.org) fornisce un meccanismo per specificare in
modo dichiarativo il movimento dei dati tra i database relazionali e Hadoop.
Prende una definizione, e da questa genera dei job di MapReduce per eseguire il
recupero o lo storage dei dati necessario. Produce inoltre del codice per aiutare
nella manipolazione dei record relazionali con classi Java personalizzate, e pu
integrarsi con with HBase e Hcatalog/Hive attraverso un set di opzioni molto
ricco.
Al momento della stesura di queste righe, Sqoop era in via di sviluppo, anche se
non in modo radicale. La sua versione originale, Sqoop 1, era unapplicazione lato
client pura. Analogamente al primo strumento a riga di comando di Hive, Sqoop 1
non ha server e genera tutto il codice sul client. Purtroppo questo significa che
ogni client deve conoscere molti dettagli sulle sorgenti fisiche dei dati, compresi i
nomi esatti degli host e le credenziali di autenticazione.
Sqoop 2 fornisce un server di Sqoop centralizzato che incorpora tutti questi
dettagli e offre varie sorgenti di dati configurate ai client che si connettono. un
modello superiore, ma almeno fino a poco tempo fa la community consigliava di
continuare a utilizzare Sqoop 1 fino a unulteriore evoluzione della nuova
versione. Se vi interessa questo tipo di strumento, verificatene lo stato dellarte.

Whirr
Quando si intende utilizzare dei servizi cloud come Amazon AWS per le
distribuzioni di Hadoop, in genere molto pi semplice impiegare un servizio di
livello pi elevato come Elastic MapReduce piuttosto che impostare il proprio
cluster su EC2. Per quanto esistano alcuni script che possono darci una mano, non
va trascurato loverhead implicito nelluso di distribuzioni basate su Hadoop sulle
infrastrutture cloud. qui che ci viene in soccorso Apache Whirr
(http://whirr.apache.org).
Whirr non centrato su Hadoop; ha a che fare con unistanziazione indipendente
dal fornitore di servizi di cui Hadoop solo un esempio. Il suo scopo quello di
fornire una modalit programmatica per specificare e creare delle distribuzioni
basate su Hadoop su strutture cloud gestendo tutti i servizi sottostanti al vostro
posto. Lo fa a prescindere dal provider, per cui, per esempio, una volta che avete
eseguito un cluster su EC2, potete utilizzare lo stesso codice per creare
unimpostazione identica su un altro provider come Rightscale o Eucalyptus.
Questo riduce il rischio di lock-in del produttore, che spesso fonte di
preoccupazione quando si distribuisce sul cloud.
Attualmente Whirr ancora limitato nei servizi che pu creare e supportare, ma
vale la pena tenerne docchio i progressi se siete interessati alla distribuzione
semplificata sul cloud.
SUGGERIM ENT O
Se state costruendo lintera infrastruttura su Amazon Web Services, potreste trovare un aiuto
anche per quanto riguarda la definizione dei requisiti delle applicazioni, anche se, ovviamente,
nello stile specifico di AWS.

Mahout
Apache Mahout (http://mahout.apache.org/) una raccolta di algoritmi distribuiti,
classi Java e strumenti per eseguire analisi avanzate su Hadoop. Simile alla
MLLib di Spark citata brevemente nel Capitolo 5, Mahout dotato di alcuni
algoritmi per i casi duso pi comuni: raccomandazioni, creazione di cluster,
regressioni ed engineering delle funzioni. Sebbene il sistema sia focalizzato su
attivit di elaborazione di linguaggio naturale e di text mining, i suoi elementi
costitutivi (operazioni algebriche lineari) sono applicabili in vari campi. Dalla
versione 0.9, il progetto stato scorporato dal framework di MapReduce a favore
di modelli di programmazione pi ricchi come Spark. Lobiettivo ultimo della
community quello di ottenere una libreria indipendente dalla piattaforma basata
su Scala DSL.

Hue
Sviluppato inizialmente da Cloudera e commercializzato come linterfaccia
utente per Hadoop, Hue (http://gethue.com/) un insieme di applicazioni accorpate
sotto uninterfaccia web comune che agiscono come client per i servizi core con
alcuni componenti dellecosistema di Hadoop.
Hue supporta molti degli strumenti di cui abbiamo parlato nei capitoli
precedenti e fornisce uninterfaccia integrata per analizzare e visualizzare i dati.
Ci sono due componenti particolarmente interessanti.
Da una parte, abbiamo un query editor che consente allutente di creare e
salvare interrogazioni di Hive (o Impala), esportare il risultato impostato nel
formato CSV o in quello di Microsoft Excel e poi strutturarlo nel browser. Leditor
include la capacit di condividere HiveQL e i set di risultati, facilitando la
collaborazione allinterno di unorganizzazione.

Figura 11.1 Il Query Editor di Hue per Hive.

Dallaltra parte, disponibile un editor dei workflow e del coordinatore di


Oozie che consente allutente di creare e distribuire manualmente i job di Oozie,
automatizzando la generazione di configurazioni e standard XML.
Le distribuzioni Cloudera e Hortonworks sono entrambe dotate di Hue e
includono in genere quanto segue:
un file manager per HDFS;
un job browser per YARN (MapReduce);
un browser Apache HBase;
un explorer del metastore di Hive;
query editor per Hive e Impala;
uno script editor per Pig;
un job editor per MapReduce e Spark;
un job editor di Sqoop 2;
un editor e una dashboard dei workflow di Oozie;
un browser Apache ZooKeeper.
Hue inoltre un framework con SDK che contiene alcune risorse web, API e
pattern per lo sviluppo di applicazioni di terze parti che interagiscono con
Hadoop.
Altre astrazioni di programmazione
Hadoop non viene esteso esclusivamente aggiungendo delle funzionalit; ci sono
infatti strumenti che forniscono paradigmi completamente differenti per scrivere il
codice utilizzato per elaborare i dati in esso.

Cascading
Sviluppato da Concurrent, e rilasciato come open source sotto una licenza
Apache, Cascading (http://www.cascading.org/) un framework diffuso che astrae la
complessit di MapReduce e consente di creare dei flussi di lavoro complessi su
Hadoop. I suoi job possono essere compilati ed eseguiti su MapReduce, Tez e
Spark. Da un punto di vista concettuale, il framework simile ad Apache Crunch,
trattato nel Capitolo 9, sebbene nella pratica ci siano alcune differenze per quanto
riguarda lastrazione dei dati e gli obiettivi finali. Cascading adotta un modello di
dati a tuple (analogamente a Pig) invece che oggetti arbitrari, e spinge lutente ad
affidarsi a un DSL di livello pi elevato e a potenti tipi interni e strumenti per
manipolare i dati. Si potrebbe dire che Cascading sta a PigLatin e ad HiveQL
come Crunch sta a una funzione definita dallutente.
Come Morphlines, trattato anchesso nel Capitolo 9, il modello di dati di
Cascading segue un approccio sorgente-pipe-sink, nel quale i dati vengono
catturati da una sorgente, convogliati in un elaborazione in pi passi e il cui output
viene rilasciato in un sink, pronto per essere prelevato da unaltra applicazione.
Cascading incoraggia gli sviluppatori a scrivere il codice in diversi linguaggi
JVM. possibile il porting del framework per Python (PyCascading), JRuby
(Cascading.jruby), Clojure (Cascalog, e Scala (Scalding). Cascalog e Scalding in
particolare hanno trovato riscontro e diffusione al di fuori dei loro ecosistemi
specifici.
Un ambito in cui Cascading eccelle quello della documentazione. Il progetto
fornisce Javadoc articolati dellAPI, tutorial ben fatti
(http://www.cascading.org/documentation/tutorials/) e un ambiente di apprendimento
interattivo basato su alcuni esercizi (https://github.com/Cascading/Impatient).
Un altro punto di forza di Cascading sta nella sua integrazione con ambienti di
terze parti. Amazon EMR lo supporta come framework di elaborazione di prima
classe e consente di avviarne dei cluster sia dalla riga di comando sia tramite
delle interfacce web
(http://docs.aws.amazon.com/ElasticMapReduce/latest/DeveloperGuide/CreateCascading.html).
Esistono plug-in per SDK per gli ambienti di sviluppo integrato IntelliJ IDEA ed
Eclipse. Uno dei progetti principali del framework, Cascading Patterns, una
raccolta di algoritmi ad apprendimento automatico, offre unutility per tradurre i
documenti scritti con il Predictive Model Markup Language (PMML) in
applicazioni su Apache Hadoop, facilitando linteroperabilit con ambienti
statistici e strumenti scientifici diffusi come R (http://cran.r-
project.org/web/packages/pmml/index.html).
Risorse per gli AWS
Molte tecnologie Hadoop possono essere distribuite su AWS come parte di un
cluster autogestito. Tuttavia, cos come Amazon offre il supporto per Elastic
MapReduce, che amministra Hadoop come un servizio gestito, ci sono altri servizi
che vale la pena citare.

SimpleDB e DynamoDB
Per qualche tempo, AWS ha offerto SimpleDB come servizio hosted che forniva
un modello di dati di tipo HBase.
Oggi stato sopravanzato da un servizio pi recente, DynamoDB
(http://aws.amazon.com/dynamodb). Sebbene il suo modello di dati sia simile a quello di
SimpleDB e HBase, concepito per un tipo di applicazioni molto diverso.
Laddove SimpleDB ha unAPI di ricerca piuttosto ricca ma abbastanza limitato
in termini di dimensioni, DynamoDB fornisce unAPI pi vincolata ma in costante
evoluzione che offre una garanzia di scalabilit pressoch illimitata del servizio.
Il piano tariffario di DynamoDB particolarmente interessante; invece di
pagare per un certo numero di server che ospitano il servizio, allocate una
determinata capacit per le operazioni di lettura e scrittura, e DynamoDB gestir
le risorse necessarie a soddisfare la capacit indicata. Si tratta quindi di un
modello di servizio puro, nel quale il meccanismo di fornitura delle prestazioni
desiderate del tutto nascosto allutente. Date unocchiata a DynamoDB, ma solo
se vi occorre uno storage di dati pi grande di quello offerto da SimpleDB;
valutate attentamente le tariffe, perch lallocazione di una capacit molto elevata
pu diventare presto molto costosa. Amazon fornisce alcune best practice per
DynamoDB che illustrano come ridurre al minimo i costi del servizio possa
portare a unulteriore complessit del livello dellapplicazione
(http://docs.aws.amazon.com/amazondynamodb/latest/developerguide/BestPractices.html).
NOTA
Ovviamente DynamoDB e SimpleDB presuppongono un modello di dati non relazionale; per un
database relazionale sul cloud fate riferimento ad Amazon Relational Database Service
(Amazon RDS).
Kinesis
Cos come EMR gira su Hadoop e DynamoDB ha delle analogie con un hosted
HBase, non stata una sorpresa, nel 2013, lannuncio di AWS riguardo a Kinesis,
un servizio hosted di dati in streaming. Lo trovate allindirizzo
http://aws.amazon.com/kinesis e ha una struttura concettualmente simile a quella dello

stack di Samza su Kafka. Kinesis fornisce una vista partizionata dei messaggi sotto
forma di uno stream di dati e unAPI per lesecuzione di callback allarrivo dei
messaggi. Come accade con la maggior parte dei servizi AWS, c una stretta
integrazione con altri servizi che facilita il recupero e la generazione di dati in
posti come S3.

Data Pipeline
Lultimo servizio AWS di cui parliamo Data Pipeline
(http://aws.amazon.com/datapipeline). Come si pu intuire dal nome, si tratta di un
framework per costruire job di elaborazione dei dati che implicano pi passi,
alcuni spostamenti dei dati e delle trasformazioni. Da un certo punto di vista si
sovrappone concettualmente a Oozie, ma con alcune differenze. Come prima cosa,
e come prevedibile, Data Pipeline si integra in profondit con molti altri servizi
AWS, consentendo di definire facilmente i workflow di dati che incorporano vari
repository come RDS, S3 e DynamoDB. Oltre a questo, ha la capacit di integrare
degli agent installati sullinfrastruttura locale, fornendo un percorso interessante
per creare workflow che si estendono sugli AWS e sugli ambienti di costruzione.
Fonti di informazione
Non vi servono solo nuove tecnologie e nuovi strumenti, per quanto stimolanti.
A volte un piccolo aiuto da una fonte pi esperta pu tiravi fuori dai pasticci. Da
questo punto di vista siete in una botte di ferro, perch la community di Hadoop
estremamente solida in molti ambiti.

Codice sorgente
In genere un aspetto che si trascura, ma Hadoop e tutti gli altri progetti Apache
sono open source. Il codice sorgente la fonte definitiva delle informazioni su
come il sistema lavora. Acquisendo familiarit con esso e tenendo traccia della
sua funzionalit si possono ottenere diverse indicazioni. Per non parlare di quando
incappate in qualche comportamento imprevisto

Mailing list e forum


Quasi tutti i progetti e i servizi elencati in questo capitolo hanno le proprie
mailing list e/o i propri forum; vistate le varie home page per trovare i link
specifici. Anche la maggior parte delle distribuzioni prevede dei forum e altri
meccanismi per condividere la conoscenza e ottenere un aiuto (non commerciale)
dalla community. Inoltre, se usate gli AWS, controllate i relativi forum degli
sviluppatori allindirizzo https://forums.aws.amazon.com.
Ricordate sempre di leggere attentamente le linee guida per linvio di post e di
aver capito le regole di netiquette. I forum e le mailing list sono risorse
eccezionali, e sono spesso visitate dagli sviluppatori del progetto specifico.
Vedrete sviluppatori di Hadoop nelle mailing list di Hadoop, sviluppatori di Hive
in quelle di Hive, sviluppatori di EMR nei forum su EMR e cos via.

Gruppi di LinkedIn
Trovate alcuni gruppi di Hadoop o a essi correlati anche su LinkedIn. Potete
eseguire una ricerca per una determinata area di interesse, ma un buon punto di
partenza potrebbe essere il gruppo utenti generico di Hadoop allindirizzo
.
http://www.linkedin.com/groups/Hadoop-Users-988957

HUG
Se desiderate uninterazione pi faccia a faccia, allora cercate un Hadoop
User Group (HUG) nella vostra zona, la maggior parte dei quali elencata
allindirizzo http://wiki.apache.org/hadoop/HadoopUserGroups. Questi gruppi organizzano
abbastanza regolarmente degli incontri in cui combinano presentazioni di qualit,
la possibilit di discutere la tecnologia con altri individui che condividono lo
stesso interesse e spesso anche una pizza e qualche bevuta. Non ci sono HUG
vicino a voi? Potreste crearne uno!

Conferenze
Laddove in diversi settori ci vogliono decenni per riuscire a pianificare un
circuito di conferenze, Hadoop molto attivo nel campo, organizzando incontri
che coinvolgono i mondi accademico, commerciale e dellopen source. Eventi
come lHadoop Summit e Strata sono piuttosto importanti; trovate alcuni
riferimenti alla pagina http://strataconf.com/.
Riepilogo
In questo capitolo abbiamo compiuto una rapida cavalcata nellecosistema di
Hadoop, trattando quanto segue.
Perch esistono distribuzioni alternative di Hadoop e quali sono le pi
famose.
Altri progetti che forniscono funzionalit, estensioni o strumenti di supporto
ad Hadoop.
Modi alternativi per lavorare con Hadoop.
Risorse per entrare nella comunit di Hadoop.
Ora tocca voi creare qualcosa di entusiasmante!
Indice
Introduzione
Struttura del libro
Cosa serve per questo libro
Lo scopo del libro
Convenzioni
Codice degli esempi
Gli autori
I revisori
Capitolo 1 - Per iniziare
Una nota sulle versioni
Panoramica su Hadoop
Componenti di Hadoop
Hadoop 2: dov laffare?
Distribuzioni di Apache Hadoop
Un doppio approccio
AWS: infrastruttura on demand di Amazon
Come iniziare
Eseguire gli esempi
Elaborazione dei dati con Hadoop
Riepilogo
Capitolo 2 - Storage
Funzionamento interno di HDFS
Accedere al file system HDFS tramite riga di comando
Proteggere i metadati del file system
Apache ZooKeeper: un file system diverso
Failover automatico dei NameNode
Snapshot HDFS
File system di Hadoop
Gestire e serializzare i dati
Storage dei dati
Riepilogo
Capitolo 3 - Elaborazione: MapReduce e oltre
MapReduce
API Java per MapReduce
Scrivere programmi MapReduce
Panoramica sullesecuzione di un job di MapReduce
YARN
YARN nel mondo reale: il calcolo oltre MapReduce
Riepilogo
Capitolo 4 - Computazione in tempo reale con Samza
Elaborazione degli stream con Samza
Riepilogo
Capitolo 5 - Computazione iterativa con Spark
Apache Spark
Lecosistema di Spark
Elaborare i dati con Apache Spark
Spark e Samza Streaming a confronto
Riepilogo
Capitolo 6 - Analisi dei dati con Apache Pig
Panoramica su Pig
Per iniziare
Eseguire Pig
Fondamenti di Apache Pig
Programmare Pig
Estendere Pig (UDF)
Analizzare lo stream di Twitter
Riepilogo
Capitolo 7 - Hadoop e SQL
Perch SQL su Hadoop
Prerequisiti
Larchitettura di Hive
Hive e Amazon Web Services
Estendere HiveQL
Interfacce programmatiche
Liniziativa Stinger
Impala
Riepilogo
Capitolo 8 - Gestione del ciclo di vita dei dati
Cos la gestione del ciclo di vita dei dati
Costruire la capacit per lanalisi dei tweet
Le sfide dei dati esterni
Raccogliere dati supplementari
Assemblare il tutto
Riepilogo
Capitolo 9 - Facilitare il lavoro di sviluppo
Scegliere un framework
Hadoop Streaming
Kite Data
Apache Crunch
Riepilogo
Capitolo 10 - Eseguire un cluster Hadoop
Sono uno sviluppatore, le operations non mi interessano!
Cloudera Manager
Ambari, lalternativa open source
Le operations nel mondo di Hadoop 2
Condividere le risorse
Costruire un cluster fisico
Costruire un cluster su EMR
Raffinamento dei cluster
Sicurezza
Monitorare
Risoluzione dei problemi
Riepilogo
Capitolo 11 - Come proseguire
Distribuzioni alternative
Altri framework di calcolo
Altri progetti interessanti
Altre astrazioni di programmazione
Risorse per gli AWS
Fonti di informazione
Riepilogo