piątek, 24 czerwca 2011

(Prawdopodobnie ostatnie) Podsumowanie nowości w Java 7

Wpisów na temat rzeczy, które mają się pojawić w Java 7 już kilka u mnie powstało (3). Ale ten chyba będzie ostatnim, bo termin wydania jest już znany i bliski - 28 lipca 2011! Z tej okazji postanowiłem przejrzeć ten brzydki dokument JSR 336.

Żeby nie zanudzać i nie rozwodzić się za bardzo (większość rzeczy już jak wspomniałem opisałem), lista zwycięzców tej długiej bitwy:

Bytecode
Lepsze wsparcie dla języków dynamicznych opartych na JVM - dodanie instrukcji invokedynamic.

Java Beans
Tutaj głównie poprawki w API. Dodano adnotację @Transient i oznaczono nią trochę getterów, poza tym poprawki w API ;-)

Kolekcje
Tutaj podobnie dodatki w API. Głównie chwalą się dodaniem metod Collections.emptyIterator() oraz Collections.emptyEnumeration() oraz zaktualizowaniem dokumentacji.

I/O
Nowe IO! NIO.2 opisane w JSR 203 :-) Wsparcie dla POSIX i ACL, nowy pakiet java.nio.file zawierający stuff do obsługi metadanych, dowiązań, ułatwienia przeglądania katalogów (interface FileVisitor)  i jeszcze trochę tego, ale za bardzo się nie zagłębiałem akurat w ten temat.

java.lang
Tutaj tzw. project coin (JSR 334) z zestawem kilku zmian do samego języka: switch po stringu, blok try dla zasobów wymagających zamykania, multi-catch, rozszerzona składnia dla liczb, dedukcja typów (dla generyków). Poza tym wiadomo dodatki/usprawnienia w API (w tym już chyba sławny java.util.Objects z metodami do porównywania obiektów, obliczania hashcode itp.).

Class loader
Wsparcie dla współbieżnych class loaderów! :-)

Współbieżność
Dodano nowy framework ForkJoin przeznaczony dla aplikacji z intensywnymi obliczeniami. Posiada dobre wsparcie dla maszyn wieloprocesorowych i jest niezależny od platformy. + dodatki w standardowym API.

I18N
Wsparcie dla Unicode 6.0.0


To były główne zmiany, poza tym różnorakie zmiany w: java.util.loggingjava.util.regex, java.net, JMX, java.util.zip, bezpieczeństwie (obsługa TLS 1.2) oraz dodatki do Swinga i AWT.

Jak widać ostatecznie domknięcia nie zmieściły się w wersji finalnej. To samo tyczy się projektu Jigsaw czyli modularyzacji JDK (co mnie bardzo zdziwiło, bo myślałem, że to pewniak). Jestem z tego powodu bardzo zawiedziony, bo to dla mnie były właśnie te killing-features, które wprowadziłyby nowy blask językowi.

No cóż... z nadzieją musimy obserwować rozwój specyfikacji Javy 8... :-/

niedziela, 19 czerwca 2011

Pierwszy Job z Hadoop

Zanim napiszemy pierwszego joba trzeba przygotować sobie środowisko. Obsługa wszystkiego z konsoli może z czasem być nużąca (chociaż ja tam lubię;).

Ja do dewelopmentu używam NetBeansa i polecam wtyczkę Karmaspehere Community Edition. Pozwala ona w łatwy sposób definiować i odpalać sobie zadania Hadoopa.
Aby zainstalować tę wtyczkę należy dodać do listy 'update sites' adres: http://hadoopstudio.org/updates/updates.xml. Ja zainstalowałem wszystkie 4 dostępne pluginy od Karmasphere. Polecam też się zarejestrować i aktywować plugin, bo będą się pojawiać komunikaty o aktywacji (ale chyba można pracować bez niej).

Ok, możemy zacząć pisać :-) Projekt oparłem o maven, pom.xml nie jest zbyt rozdbudowany:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>pl.lstachowiak</groupId>
    <artifactId>HadoopSamples</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>HadoopSamples</name>
    <url>http://blog.lstachowiak.pl</url>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.8</version>
            <scope>test</scope>
        </dependency>
        
        <dependency>
            <groupId>org.apache.mahout.hadoop</groupId>
            <artifactId>hadoop-core</artifactId>
            <version>0.20.1</version>
        </dependency>
        <dependency>
            <groupId>commons-logging</groupId>
            <artifactId>commons-logging</artifactId>
            <version>1.1.1</version>
        </dependency>
    </dependencies>
</project>

Problem jaki wybrałem dla pierwszego przykładu to analiza statystyk serwera Apache. Bierzemy plik access.log i analizujemy typy żądań do serwera. Na końcu chcemy mieć liczbę żądań każdego typu.
Mój pomysł na Map i Reduce jest następujący:
Czyli:
- Hadoop wczytuje i dzieli plik na tzw 'input splits'
- przekazuje każdy split do funkcji map, gdzie kluczem jest numer linii w pliku a wartością sama linia
- implementowana przez nas funkcja map sprawdza jaki tym żądania HTTP znajduje się w linii i zwraca jako klucz typ żądania a jako wartość liczbę 1 (umownie sobie tak robimy)
- Hadoop wykonuje tzw. shuffle grupując nam wyniki z map w pary [TYP] -> [KOLEKCJA JEDYNEK]
- nasza funkcja reduce jedyne co robi to zlicza liczbę elementów w kolekcji i zwraca jako klucz typ żądania a jako wartość liczbę elementów w kolekcji co jest zarazem wynikiem końcowym
- Hadoop zapisuje dane na dysk

(Bardzo) Możliwe, że nie jest to idealne rozwiązanie, ale nie mam jeszcze wielkiego doświadczenia z Hadoop ;)

Przejdźmy do implementacji. Funkcja map:
HttpMethodMapper.java:
package pl.lstachowiak.hadoop.sample1;

import java.io.IOException;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;


public class HttpMethodMapper extends Mapper<LongWritable, Text, Text, IntWritable> { // 1

    @Override
    public void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
        HttpMethod method = getMethodFromLine(value); // 2
        context.write(new Text(method.name()), new IntWritable(1)); // 3
    }

    HttpMethod getMethodFromLine(Text value) {

        if (value.find(HttpMethod.CONNECT.name()) != -1) {
            return HttpMethod.CONNECT;
        }

        if (value.find(HttpMethod.DELETE.name()) != -1) {
            return HttpMethod.DELETE;
        }

        if (value.find(HttpMethod.GET.name()) != -1) {
            return HttpMethod.GET;
        }

        if (value.find(HttpMethod.HEAD.name()) != -1) {
            return HttpMethod.HEAD;
        }

        if (value.find(HttpMethod.OPTIONS.name()) != -1) {
            return HttpMethod.OPTIONS;
        }

        if (value.find(HttpMethod.PATH.name()) != -1) {
            return HttpMethod.PATH;
        }

        if (value.find(HttpMethod.POST.name()) != -1) {
            return HttpMethod.POST;
        }

        if (value.find(HttpMethod.PUT.name()) != -1) {
            return HttpMethod.PUT;
        }

        if (value.find(HttpMethod.TRACE.name()) != -1) {
            return HttpMethod.TRACE;
        }

        return HttpMethod.UNKNOWN;
    }
}
Krótki komentarz do tego co się dzieje w kodzie:
1 - klasa mappera musi (od wersji API 0.20) rozszerzać klasę Mapper. Parametrami są kolejno:
- typ klucza (numer linii)
- typ wartości (linia w pliku)
- typ klucza wynikowego (typ żądania http)
- typ wartości wynikowe (liczba 1)
2 - pobranie typu żądania z linii, wiem, że rozwiązanie jest dalekie od idealnego, ale nie o to chodzi w przykładzie ;)
3 - zapis wyniku

Jak widać Hadoop posiada własny zestaw typów danych. W większości są to obudowane typy podstawowe. Stworzone zostały one aby w troszkę lepszy sposób serializować dane i przesyłać przez sieć. Zaleca się ich używanie. Typ HttpRequest to zwykły enum z listą dostępnych typów żądań HTTP. Dostępny jest w źródłach.

Funkcja reduce:
HttpMethodReducer.java:
package pl.lstachowiak.hadoop.sample1;

import java.io.IOException;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Reducer;
import org.apache.hadoop.mapreduce.Reducer.Context;


public class HttpMethodReducer extends Reducer<Text, IntWritable, Text, IntWritable> { // 1

    @Override
    protected void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException { // 2
        int counter = countValues(values); // 3
        context.write(key, new IntWritable(counter)); // 4
    }
    
    <T> int countValues(Iterable<T> values) {
        int counter = 0;
        for (T value : values) {
            counter++;
        }
        
        return counter;
    }
}
Ponownie krótki komentarz:
1 - musimy rozszerzać klasę Reducer, parametry kolejno to:
- typ klucza z Map
- typ wartości z Map
- typ klucza wynikowego
- typ wartości wynikowej
2 - tutaj widać efekt działania operacji shuffle. Zamiast otrzymywać tą jedynkę z funkcji Map otrzymujemy iterator po kolekcji jedynek (w naszym przykładzie).
3 - zliczamy elementy
4 - zapisujemy w wynikach :-)

No to jeszcze program do uruchomienia zadania:
HttpMethodJob.java:
package pl.lstachowiak.hadoop.sample1;

import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;


public class HttpMethodJob {
    
    public static void main(String[] args) throws Exception {
        
        if (args.length != 2) {
            System.err.println("Usage: HttpMethodJob <input path> <output path>");
            System.exit(-1);
        }
        
        Job job = new Job();
        job.setJarByClass(HttpMethodJob.class); // 1
        
        FileInputFormat.addInputPath(job, new Path(args[0])); // 2
        FileOutputFormat.setOutputPath(job, new Path(args[1])); // 3
        
        job.setMapperClass(HttpMethodMapper.class); // 4
        job.setReducerClass(HttpMethodReducer.class); // 5
        
        job.setOutputKeyClass(Text.class); // 6
        job.setOutputValueClass(IntWritable.class); // 7
        
        System.exit(job.waitForCompletion(true) ? 0 : 1); // gogogo!
        
    }
}

1 - tworzymy zadanie i podajemy klasę główna
2,3 - podajemy źródło danych oraz miejsce zapisu danych wynikowych (wczytywane z argumentów)
4,5 - podajemy klasę mappera i reducera
6,7 - konfiguracja typów danych wynikowych
I uruchomienie zadania :-)

Zanim jednak zaczniemy potrzebne są jeszcze jakieś dane do analizy. Ja wygooglałem coś tutaj (4,3MB).

Uruchomienie

Tutaj obrazkowo:) Przechodzimy do zakładki Services (NetBeans) -> Servers -> Hadoop Jobs -> New Job. Wybieramy nazwę dla Joba i typ 'Hadoop Job from pre-existing JAR file'.
Następnie wpisujemy ścieżkę do JARa (ja podałem z targeta) oraz klasę z funkcją main.
W następnym oknie wybieramy klaster dla wersji 0.20 oraz wpisujemy argumenty dla zadania. Kolejno plik access.log oraz katalog w którym mają być składowane wyniki końcowe.
I klikamy 'finish' :-)

Teraz możemy odpalać!
Prawy guzik na nowego Joba -> Run Job -> Run.

Wszystko powinno pójść dobrze, stan konsoli na końcu (statystyki):
11/06/19 11:53:26 INFO mapred.JobClient: Job complete: job_local_0001
11/06/19 11:53:26 INFO mapred.JobClient: Counters: 12
11/06/19 11:53:26 INFO mapred.JobClient:   FileSystemCounters
11/06/19 11:53:26 INFO mapred.JobClient:     FILE_BYTES_READ=18153346
11/06/19 11:53:26 INFO mapred.JobClient:     FILE_BYTES_WRITTEN=9462349
11/06/19 11:53:26 INFO mapred.JobClient:   Map-Reduce Framework
11/06/19 11:53:26 INFO mapred.JobClient:     Reduce input groups=4
11/06/19 11:53:26 INFO mapred.JobClient:     Combine output records=0
11/06/19 11:53:26 INFO mapred.JobClient:     Map input records=17484
11/06/19 11:53:26 INFO mapred.JobClient:     Reduce shuffle bytes=0
11/06/19 11:53:26 INFO mapred.JobClient:     Reduce output records=4
11/06/19 11:53:26 INFO mapred.JobClient:     Spilled Records=34968
11/06/19 11:53:26 INFO mapred.JobClient:     Map output bytes=142014
11/06/19 11:53:26 INFO mapred.JobClient:     Combine input records=0
11/06/19 11:53:26 INFO mapred.JobClient:     Map output records=17484
11/06/19 11:53:26 INFO mapred.JobClient:     Reduce input records=17484
W katalogu wynikowym powinien znajdować się także plik part-r-00000 z zawartością:
GET 15341
HEAD 1744
POST 398
PUT 1

I mamy gotowe statystyki :-)
Pełne źródła dostępne są tutaj.

sobota, 18 czerwca 2011

Wideo z Poznańskiego Code Retreat

Trochę czasu minęło od pierwszego Poznańskiego Code Retreat, ale parę dni temu pojawił się w sieci krótki filmik z tej imprezy. Wg mnie jest świetny :) Dynamiczny i nawet gdzieś tam mnie widać! Brawa dla autora, którym jest Zbigniew Wantuch!

Zapraszam do obejrzenia:


Źródło: Poznań JUG

środa, 15 czerwca 2011

Szybka instalacja Apache Hadoop dzięki dystrybucji Cloudera

Mając w pamięci trudne doświadczenia z czasów studiów z frameworkiem Hadoop, postanowiłem wrócić do tematu dzięki prezentacji Eugiene Ciurana podczas tegorocznego GeeCONa. Eugienie opowiedział trochę o firmie Cloudera która odwaliła kawał dobrej roboty tworząc i udostępniając dla wszystkich łatwą w instalacji dystrybucję Hadoopa.

Oto jak w szybko zainstalować i uruchomić Hadoop na Ubuntu 11.04:

1. Dodanie nowego repozytorium
sudo touch /etc/apt/sources.list.d/cloudera.list
I dopisać do zawartości:
deb http://archive.cloudera.com/debian lucid-cdh3 contrib
deb-src http://archive.cloudera.com/debian lucid-cdh3 contrib
(Nie ma jeszcze dostępnej wersji dedykowane dla Natty :-/)

2. Instalacja!
sudo apt-get update
apt-cache search hadoop
sudo apt-get install hadoop-0.20

3. Odpalamy przykład :-)
hadoop jar /usr/lib/hadoop-0.20/hadoop-examples.jar pi 10 100

Jeśli wszystko przebiegło poprawnie na końcu powinniśmy otrzymać coś takiego w konsoli:
11/06/15 21:09:07 INFO mapred.JobClient: Job complete: job_local_0001
11/06/15 21:09:07 INFO mapred.JobClient: Counters: 14
11/06/15 21:09:07 INFO mapred.JobClient:   FileSystemCounters
11/06/15 21:09:07 INFO mapred.JobClient:     FILE_BYTES_READ=1642249
11/06/15 21:09:07 INFO mapred.JobClient:     FILE_BYTES_WRITTEN=2108417
11/06/15 21:09:07 INFO mapred.JobClient:   Map-Reduce Framework
11/06/15 21:09:07 INFO mapred.JobClient:     Reduce input groups=2
11/06/15 21:09:07 INFO mapred.JobClient:     Combine output records=0
11/06/15 21:09:07 INFO mapred.JobClient:     Map input records=10
11/06/15 21:09:07 INFO mapred.JobClient:     Reduce shuffle bytes=0
11/06/15 21:09:07 INFO mapred.JobClient:     Reduce output records=0
11/06/15 21:09:07 INFO mapred.JobClient:     Spilled Records=40
11/06/15 21:09:07 INFO mapred.JobClient:     Map output bytes=180
11/06/15 21:09:07 INFO mapred.JobClient:     Map input bytes=240
11/06/15 21:09:07 INFO mapred.JobClient:     Combine input records=0
11/06/15 21:09:07 INFO mapred.JobClient:     Map output records=20
11/06/15 21:09:07 INFO mapred.JobClient:     SPLIT_RAW_BYTES=1110
11/06/15 21:09:07 INFO mapred.JobClient:     Reduce input records=20
Job Finished in 2.361 seconds
Estimated value of Pi is 3.14800000000000000000

Oczywiście uruchomiony Hadoop działa w trybie standalone, który idealnie będzie się dla nas nadawać do deweloperskiej zabawy ;-)
W następnym odcinku trochę pokodzimy!