Montag, 2. Dezember 2013

YASS - Java RPC leicht gemacht

Frameworks für Serialisierung (Google Protobuf, Protostuff etc) und Transport von serialisierten Daten (Netty, Apache Mina etc) gibt es bekanntlich sehr viele. Erstaunlicherweise gibt es aber nicht sehr viele Kombinationen davon, welche man Out-of-the-box einfach verwenden kann. Klar gibt es Java RMI, Corba, Ice etc, welche aber alle erstaunlich viel Code und Komplexität mit sich bringen.

Wer eine einfache und effiziente Lösung für RPC zwischen verschiedenen Java Prozessen sucht, sollte auch einen Blick auf YASS werfen. Die Vorteile von YASS sind:
  • kleine Bibliothek ohne weitere Abhängigkeiten
  • Schnittstelle wird als POJO mit Annotationen definiert
  • Bi-direktionale Kommunikation über die gleiche Verbindung (Stichwort Server-Push)
  • Effiziente binäre Serialisierung
  • Optionale Felder für rückwärtskompatible Erweiterungen
  • Interceptors für Logging, Sicherheit, Performance etc
  • Asynchrone Kommunikation mit Oneway-Methoden

Dies reicht meiner Erfahrung nach für die Umsetzung vieler Projekte, aber natürlich hat jedes Projekt wieder seine eigenen Anforderungen.

Samstag, 14. September 2013

Segway Tour

Unser diesjähriger Teamevent führte uns nach Innerthal ins beschauliche Wägital. Dort unternahmen wir eine Segway Tour rund um den Wägitalersee. Nach einer genauen Einführung ging es los. Es dauerte gar nicht so lange, bis man dieses Gefährt einigermassen im Griff hat. Schade, dass sie so teuer sind, denn Spass machen sie alleweil.



 

Montag, 4. März 2013

Play Framework und Nginx: Durchreichen von Medien

Der Verbund von Play! und einem Reverse Proxy wie Nginx funktioniert problemlos für HTML-Dateien. Sobald aber andere Dateien wie Bilder oder XML-Dokumente zum Client durchgereicht werden sollen, werden sie von Nginx blockiert. Der Grund dafür ist, dass Nginx nicht mit gechunkten Antworten auskommt. In diesem Fix wurde das Problem bei Play! behoben, allerdings nur für Dateien im Assets-Verzeichnis (CSS, Logo, etc). Wie können nun aber Dateien, die nicht in den Assets liegen, erfolgreich geladen werden?
Als Beispiel nehmen wir die Sitemap, ein zentraler Baustein vom SEO, welcher auf jeder Homepage verfügbar sein sollte. Da die Sitemap oft wechselt, ist es sinnlos, diese statisch in Play! einzubinden (zusammen mit den CSS oder Javascripts im Assets-Vezeichnis). Die Sitemap ist als XML-Datei irgendwo auf dem Server gespeichert und soll beim Aufruf von beispielsweise http://mydomain.com/sitemap.xml angezeigt werden. Nachdem in Play! eine Route und der dazugehörige Controller erstellt wurde, können wir die Methode in der Controller-Klasse implementieren.
Route
 GET          /sitemap.xml                    controllers.SEO.getSitemap()  
Controller: SEO.java
 public static Result getSitemap() {  
      final File file = new File("/location/of/sitemap.xml");  
      if (!file.canRead()) {  
           return notFound();  
      }  
        
      return ok(file);  
 }  

Das File wird vom Dateisystem gelesen und an den Client weitergeleitet. Ohne Reverse Proxy funktioniert der obige Code tadellos. Ein Reverse Proxy vor dem Server erfordert aber ein paar zusätzliche Zeilen:
Controller: SEO.java
 public static Result getSitemap() {  
      final File file = new File("/location/of/sitemap.xml");  
      if (!file.canRead()) {  
           return notFound();  
      }  
   
      FileInputStream stream;
      ByteArrayOutputStream bos;
      try {  
           // convert the file to a byte-array  
           stream = new FileInputStream(file);  
           bos = new ByteArrayOutputStream();  
           byte[] buffer = new byte[1024];  
           for (int readNum; (readNum = stream.read(buffer)) != -1;) {  
                bos.write(buffer, 0, readNum);  
           } 
   
           // get the mime type. For a XML-document it is "application/xml"  
           String mimeType = MimeTypes.forFileName(file.getName()).get();  
           return ok(bos.toByteArray()).as(mimeType);  
      } catch (IOException e) {  
           // return an error to indicate the exception  
           return internalServerError();  
      } finally {
           if(stream != null) { stream.close(); }
      }
 }  

Die XML-Datei muss als Byte-Array durch Nginx durchgeschleust werden, damit sie nicht blockiert wird. Die selbe Prozedur kann bei Bildern oder anderen Dateitypen angewendet werden. Wichtig dabei ist, dass der Mime-Type entsprechend angepasst wird.

Montag, 7. Januar 2013

Defaultsettings für JVM Optionen


Die Java Virtual Machine (JVM) unterstützt bekanntlich sehr viele Optionen, um deren Laufzeitverhalten zu kontrollieren und zu optimieren. Siehe dazu
http://www.oracle.com/technetwork/java/javase/tech/vmoptions-jsp-140102.html.

Leider ist es jedoch schwieriger herauszufinden, was nun der Defaultwert für eine bestimmte Option ist, d.h., ist die gewünschte Option bereits aktiv oder muss ich diese via Argument einschalten? Um dies einfach herauszufinden zu können, gibt es in der Java-Distribution das Tool jinfo. Wenn man zum Beispiel herausfinden möchte, ob die Option AggressiveOpts eingeschaltet ist kann man folgenden Befehl ausführen:

jinfo -flag AggressiveOpts <Prozess-ID der JVM>
was bei mir (mit JRE 1.6.0_30-b12 unter RHEL 5.8) zu folgender Ausgabe führt:
-XX:-AggressiveOpts
d.h., diese Option ist standardmässig nicht eingeschaltet.

jinfo bietet weiter die Möglichkeit die Optionen zur Laufzeit zu verändern oder alle Optionen und System-Properties aufzulisten. Ein nettes Tool, welches ich bis jetzt übersehen habe...

Dienstag, 13. November 2012

Mahout: Recommender kombinieren und mit JUnit verbessern

Wie weiss ein Computer, was der Benutzer eventuell mag? Amazon ist der Vorreiter bei der Anwendung dieser Algorithmen. Wenn ein Artikel angeschaut wird, erscheint die Sektion „Andere Benutzer haben auch gekauft...“.

In jedem Buch, das erklärt, wie solche Recommender funktionieren, wird klargemacht, dass es nicht einen „besten“ Algorithmus für die Erzeugung von Empfehlungen gibt. Welcher Algorithmus der beste ist, muss jeweils empirisch an realen Daten ermittelt werden. Wieso nicht eine Kombination verschiedener Recommender? Skalierbarkeit und Performance leiden stark darunter – hier trotzdem ein Versuch...

Kombination verschiedener Algorithmen
Um das beste Resultat zu erhalten, habe ich eine eigene Implementierung vom Interface Recommender.java, Teil der Apache Mahout Library, erstellt, welche mit variabler Gewichtung Empfehlungen aus drei bestehenden Algorithmen generiert:
  • User-Similarity: Für jeden Nutzer wird eine „Nachbarschaft“ von den ähnlichsten anderen Nutzern generiert (z.B. aus den meisten ähnlichen Bewertungen). Empfohlen sind alle Events, welche die Nachbarschaft bewertet hat, der Nutzer selbst aber noch nicht kennt.
  • Item-Similarity: Ähnlich wie Tanimoto-Koeffizient: Die Ähnlichkeit zwischen Benutzern wird aus der Anzahl der gemeinsamen Events berechnet. Die Mathematik dahinter könnte alleine ein ganzes Buch füllen. Für die Distanz zwischen den Items wird mit der Log-Likelihood-Funktion berechnet.
  • Slope-One: Empfehlungen werden berechnet, indem der durchschnittliche Unterschied zwischen den Bewertungen der Events berücksichtigt wird. Beispielsweise wird Zirkus Knie immer um 1 besser bewertet als der Zirkus Monti. Und zudem wird Zirkus Knie durchschnittlich gleich bewertet wie Zirkus Royal. Ein Nutzer bewertet nun Monti mit 2, und Royal mit 4. Wie wäre nun die geschätzte Wertung für Knie? Wenn wir von Monti ausgehen würden, wäre dies 2 + 1 = 3. Ausgegangen von Royal wäre es 4 + 0 = 4 (immer gleich wie Knie). Als Schätzung gibt der Computer nun eine durchschnittliche Bewertung von 3.5 an.

 public class MyRecommenderBuilder implements RecommenderBuilder {  
      @Override  
      public Recommender buildRecommender(DataModel dataModel) throws TasteException {  
           // this map holds all recommenders  
           Map<Recommender, Integer> recommenders = new HashMap<Recommender, Integer>();  
   
           if (userSimilarityFactor > 0) recommenders.put(getUserSimilarityRecommender(dataModel), userSimilarityFactor);  
           if (logLikelihoodFactor > 0)recommenders.put(getLogLikelihoodRecommender(dataModel), logLikelihoodFactor);  
           if (slopeOneFactor > 0) recommenders.put(getSlopeOneRecommender(dataModel), slopeOneFactor);  
   
               // create the recommender  
           return new CombinatedRecommender(recommenders);  
      }  
 }  
 public class CombinatedRecommender implements Recommender {  
      private final Map<Recommender, Integer> recommenders; // holds all recommenders  
      private int sumWeight = 0; // total weight of the recommenders  
   
      public CombinatedRecommender(Map<Recommender, Integer> recommenders) {  
           this.recommenders = recommenders;  
           for (Map.Entry<Recommender, Integer> recommender : recommenders.entrySet()) {  
                sumWeight += recommender.getValue();  
      }  
   
      @Override  
      public List<RecommendedItem> recommend(long userID, int howMany) throws TasteException {  
           // Map holding all recommendations and the according score (weighted)  
           Map<Long, Float> allRecommendations = new Hashtable<Long, Float>();  
   
           // get recommendations of all recommenders  
           for (Map.Entry<Recommender, Integer> recommender : recommenders.entrySet()) {  
                // iterate through all recommendations of this recommender  
                for (RecommendedItem thisRec : recommender.getKey().recommend(userID, howMany)) {  
                     long itemID = thisRec.getItemID();  
   
                     // to weight the recomendations, multiply the accuracy of the recommendation with the  
                     // weight of the recommender  
                     float thisScore = thisRec.getValue() * recommender.getValue();  
   
                     // adds this recommendation to the map of all recommended items. If this recommendation has already
                     // been done by another recommender, increase the score of the recommendation  
                     addOrInsert(allRecommendations, itemID, thisScore);  
                }  
           }  
   
           // ... next steps ...  
           // 1. sort the result descending (highest score first) and limit the size to ‘howMany’  
           // 2. return this list  
      }  
 }  
Code 1: MyRecommenderBuilder.java erstellt drei Recommender und gewichtet diese (in einer Map abgelegt). CombinatedRecommender.java erhält diese Map im Konstruktor . Der Aufruf von recommend(...) iteriert durch alle vorhandenden Recommender und lässt, unabhängig von den anderen Recommendern, Empfehlungen generieren. Die Resultate werden in allRecommendations gespeichert. Wenn mehrere Recommender dieselbe Empfehlung machen, erhält diese Empfehlung mehr Gewicht.

Mit JUnit die Faktoren verbessern
Eine flexible Gewichtung ermöglicht die einfache Anpassung an reale Daten. In einer Test-Klasse werden alle möglichen Kombinationen von Gewichtungen ausprobiert und die Deckung (0-100%) berechnet. Die Kombination mit der grössten Deckung wird dann übernommen. Der nächste Code-Ausschnitt zeigt einen Teil aus dem parametrierten JUnit-Test, der die beste Faktorenkombination berechnet. Diese Faktoren können dann im Code 1 produktiv eingesetzt werden.


 private final int userSimFactor;  
 private final int logLikelihoodFactor;  
 private final int slopeOneFactor;  
   
 private static int bestFactor1;  
 private static int bestFactor2;  
 private static int bestFactor3;  
 private static double bestScore = Double.NaN;  
   
 @Test  
 public void testFactors() throws TasteException {  
      RecommenderEvaluator evaluator = new AverageAbsoluteDifferenceRecommenderEvaluator();  
      MyRecommenderBuilder recommenderBuilder = new MyRecommenderBuilder(userSimFactor,  
                                     logLikelihoodFactor, slopeOneFactor);  
   
      // how accurate are those factors?  
      double score = evaluator.evaluate(recommenderBuilder, null, dataModel, 0.9, 1.0);  
      if (score < bestScore) {  
           // new improved score --> Save these factors  
           bestFactor1 = userSimFactor;  
           bestFactor2 = logLikelihoodFactor;  
           bestFactor3 = slopeOneFactor;  
           bestScore = score;  
      }  
 }  
Code 2: Test-Methode berechnet mittels RecommenderEvaluator.evaluate(...) den Deckungsgrad für beliebige Faktoren und ermittelt die beste Kombination.

Weitere Verbesserungen konnten innerhalb der bereits bestehenden Recommendern vorgenommen werden. Beispielsweise wird für die User-Similarity eine Nachbarschaft mit einer vordefinierten Anzahl von nächsten Usern berechet. Je nach Test-Daten ist es möglich, dass eine Nachbarschaft von nur zwei Benutzern das beste Resultat liefert. Bei anderen Test-Daten ist ein Optimum bei einer Nachbarschaft von über 100 weiteren Benutzern durchaus normal. Code 3 zeigt einen weiteren JUnit-Test, der, analog zu testFactors() die beste Nachbarschaftsgrösse ermittelt.


 private final int neighborhoodSize;  
 private static int bestNeighborhoodSize;  
 private static double bestScore = Double.NaN;  
   
 @Test  
 public void testNeighborhoodSize() {  
      RecommenderEvaluator evaluator = new AverageAbsoluteDifferenceRecommenderEvaluator();  
      RecommenderBuilder recommenderBuilder = new RecommenderBuilder() {  
           @Override  
           public Recommender buildRecommender(DataModel dataModel) throws TasteException {  
                // use the Euclidian distance. Test with oder Similarities can be done  
                UserSimilarity userSimilarity = new EuclideanDistanceSimilarity(dataModel, Weighting.WEIGHTED);  
                // calculate neighborhood of the users with the parameterized size  
                UserNeighborhood neighborhood = new NearestNUserNeighborhood(neighborhoodSize, userSimilarity, dataModel);  
                // get recommendations  
                return new GenericUserBasedRecommender(dataModel, neighborhood, userSimilarity);  
           }  
      };  
             
      // how accurate is this neighborhood size  
      double score = evaluator.evaluate(recommenderBuilder, null, dataModel, 0.9, 1.0);  
      if (score < bestScore) {  
           // found a better neighborhood than before --> save better  
           bestNeighborhoodSize = neighborhoodSize;  
           bestScore = score;  
          }  
 }  
Code 3: Wie bei ‚Code 1’ berechnet diese Test-Methode den Deckungsgrad, dieses Mal aber mit variierender Nachbarschaftsgrösse.


Performance – was noch zu retten ist
Somit wäre noch das Thema Performance anzusprechen. Solche Empfehlungsgeneratoren skalieren sehr schnell (oft quadratisch mit der Anzahl der User). Wenn Real-Time-Empfehlungen berechnet werden sollen, müssen Resultate in genügend schneller Zeit zurückgeliefert werden. Ein grosser Nachteil der obigen Implementierung ist, dass gleich drei Recommender parallel laufen. Wenn nun beispielsweise 25 Empfehlungen gefragt sind, müssen alle drei Recommender je 25 Empfehlungen erstellen. Danach werden diese gewichtet und zusammengemischt. Der Speicherbedarf der Empfehlungs-Engine ist von deren Konfiguration abhängig.
Eine Performance-Verbesserung für Live-Empfehlungen wäre eine gleichzeitige Berechnung der Empfehlungen durch mehrere Recommender. Mehrere Threads erlauben eine optimale Speicher- und Prozessorauslastung.

Samstag, 20. Oktober 2012

Eclipse Finance Day - Résumé

Diese Woche fand bei der UBS in Zürich der Eclipse Finance Day statt. Es wurden interessante Einblicke gewährt wie Eclipse Technologie im Bankenumfeld und darüber hinaus eingesetzt wird.
Der Fokus lag dabei auf Modellierung (EMF, Xtext, ...) - es wurden aber auch andere Themen wie Modularisierung (OSGi), RCP-Entwicklung, ALM, etc. präsentiert.
Alles Technologien welche auch wir bereits seit Jahren in Projekten erfolgreich einsetzen.

Insgesamt ein gelungener Event mit knapp 100 Besuchern. Zu einigen Vorträgen sind die Folien bereits online verfügbar.

Samstag, 15. September 2012

Team-Event in Filzbach (Kerenzerberg)

Auch dieses Jahr haben wir wieder einen Team-Event organisiert. Dieser führte uns nach Filzbach auf den "Erlebnisberg". Hier haben wir verschiedene Aktivitäten unternommen. Vor den Aktivitäten mussten wir uns natürlich zuerst mit dem Älpler-Grill stärken:
Danach ging es los mit der ersten Aktivität. Hier ging es darum, mittels Kompass möglichst genau westwärts zu gehen und Objekte zu finden. Wie bei unserer alltäglichen Arbeit war Genauigkeit und Teamarbeit gefragt:
Im unwegsamen Gelände war dies gar nicht so einfach:
Danach ging es weiter mit dem "Flying Fox". Die Schlucht, welche wir zu überwinden hatten, war echt tief. Für mich galt einfach "nicht runterschauen". Anderen machte dies weniger aus:
Spass machte es auf jeden Fall:
Die nächste Aktivität war dann Abseilen über eine 25 hohe Steilwand:
Danach ging es weiter mit Bogenschiessen, was natürlich viel schwieriger ist, als es aussieht. Ich war jeweils froh die Zielscheibe zu treffen:
Michael konnte man am ehesten noch als "Robin Hood" bezeichnen. Meine Haltungsnote für ihn "10.0":
Nach all diesen Aktivitäten darf natürlich der gemütliche Teil auch nicht fehlen:
Die verschiedenen Aktivitäten haben mir viel Spass gemacht und die Organisation durch erlebnisberg.ch war tadellos.