Entkopplung und Skalierung extrem

Bandbreite auf einem Frontendserver während einer DDOS-Attacke. Das System war für Endkunden ohne Einschränkungen verfügbar.

Anzahl gleichzeitiger und dynamischer Zugriffe zu Spitzenzeiten auf einem einzelnen CORE-I5-System.

Techniken: PHP, C, Javascript, Varnish, CQRS, Python, Redis, MySQL, D3.js

Eines der Projekt,an denen die ich in den letzten Jahren arbeiten und maßgeblich die Architektur mitbestimmen durfte, war in vielerlei Hinsicht spannend. Die Kombination verschiedener Techniken, im Alltag ein Besuchervolumen, das die Seite in die TOP 100 bei Alexa gebracht hat, und wiederkehrende DDOS-Angriffe vermutlich eifersüchtiger Konkurrenten boten viel Potential für ein vielseitiges Projektumfeld.

Personalisierte Werbung ohne Kundenprofile

Was unsere Software machen sollte, ähnelt ein bisschen dem, was Google mit einem FLoC-Framework als Alternative zu personenbezogenen Cookies für die Individualisierung von Werbung plant. Dazu haben wir bei Klicks auf Werbebanner auf sehr vielen Partnerwebseiten in Echtzeit viele aus dem HTTP-Header auslesbare oder anhand der über die IP-Adresse zu ermittelnden Informationen wie Referrer, Betriebssystem, Gerät, Browser, Tageszeit, Land, Verbindungsgeschwindigkeit und Ähnlichem, insgesamt mehr als 20 Attribute bestimmt. In einem Backend konnten Anhand dieser Attribute Regeln definiert werden, wohin die Besucher*innen bei einem Bannerklick geleitet werden würden. Das ist für sich genommen keine Raketenwissenschaft. Interessant wird es durch den erwartbaren und dann auch eingetretenen Besucherstrom.

Entworfen für maximale Skalierbarkeit

Schon zum Projektstart war zu erwarten, dass wir nicht mit kleinen Zahlen arbeiten würden. Von einige Partnerseiten auf denen die Banner platziert werden würden, wurden uns sehr große Zahlen genannt. Gleichzeitig war klar, dass wir es nicht mit kleinen Regelbäumen zu tun haben werden. Aus 20 Attributen mit vielen Auswahlmöglichkeiten (beim Land sind es zum Beispiel 195 inklusive der Länderkennung für Satelliten) in einem sehr komplexen Baum unterbringen. In der Praxis haben wir schnell Regelbäume mit vielen Millionen Einträgen kombiniert.

Die Auswertung komplexer dynamischer Regeln in Echtzeit bei vielen gleichzeitigen Zugriffen macht dieses Projekt aus technischer Sicht so spannend.

Die Architektur, die wir gewählt haben wurde, wie ich heute weiß, in weiten Teilen von Greg Young mit CQRS (Command Query Request Segregation) beschrieben. Was wir gemacht haben war, dass das Backend geschrieben in PHP und mit MySQL als primärer Datenquelle bei schreibenden Änderungen im Regelbaum alle Informationen in eine Redis-Datenbank kopiert hat. Diese Redis-Datenbank haben wir dann auf eine größere Anzahl sehr simpler Frontend-Server kopiert (tatsächlich waren das im Alltag einfache CORE-I 5 Server der ersten Generation). Da das Durchlaufen des Regelbaums zusammen mit ein bisschen HTTP-Logik und Logging selbst eine vergleichsweise wenig komplexe Angelegenheit ist, haben wir die Frontendsoftware leistungsoptimiert direkt in C geschrieben.

Das Ergebnis konnte sich sehen lassen. Im Alltag haben wir jeden der einzelnen Server mit etwa 1000 Anfragen pro Sekunde unter sehr kleiner Last gefahren. Während den oben erwähnten DDOS-Angriffen haben wir jedes System mit mehr als 3000 Anfragen pro Sekunde arbeiten lassen, ohne dass reguläre Anfragen verzögert ausgeliefert worden wären.
 

Auswertung komplett asynchron

Um die Regelbäume stetig zu verbessern, waren natürlich Statistiken sehr wichtig. Die auf den Frontendservern getroffenen Entscheidungen wurden daher zusammen mit den ermittelten Attributen in ein Logfile geschrieben. Das Backend konnte diese dann in Ruhe einsammeln, verarbeiten und in der MySQL-Datenbank für die Werbemanager aufarbeiten.
Dort durften wir uns mit Werkzeugen wie D3.js, Neo4J nach Belieben austoben ohne auf das letzte Quäntchen Echtzeitperformance achten zu müssen.

Codebeispiel: Regeln im Entscheidungsbaum suchen

/**
 *
 * matches redirect rules against decision tree
 *
 * @param char connection
 * @param char country
 * @param char isp
 * @param char device
 * @param char agent
 * @param tree decisionTree
 * @param char result pointer to char array containing result
 */
int match_rules(        
		char* connection,
        char* country,
        char *isp,
        char* device,
        char* agent,
        tree *decisionTree,
        char**result){

        if(strcmp(*result,"false")!=0){
                //printf("%s\n",*result);
                return(0);
        }

        list *helpList;

        if(checkForMatch(connection,country,isp,device,agent,decisionTree,result)==0
                && decisionTree->nodeid!=0){
                return(0);//do not check children if current node does not match condition
        }
        helpList = decisionTree->children;
        while(helpList!=NULL){
                if(helpList->child->children!=NULL){
                        match_rules(connection,country,isp,device,agent,helpList->child,result);
                }
                else{//we are in a leaf
                        if(checkForMatch(connection,country,isp,device,agent,helpList->child,result)==1){
                                return(1);
                        }
                }
                helpList=helpList->next;
        }
        return(0);
}//match_rule