SignalR für verteiltes Arbeiten einsetzen

Ich hatte einigermaßen die Nase voll von den ganzen Chat-Beispielen für ASP.NET SignalR. „Das muss doch noch für mehr gut sein!“, dachte ich mir und versuche nun, SignalR für verteiltes Rechnen einzusetzen. Seht selbst.

Links

Die in Azure gehostete Instanz ist ziemlich limitiert, sodass es des öfteren zu Aussetzern kommen wird, sollten viele Leser des Blogs Lust bekommen, gleichzeitig zu testen. Das bedeutet, dass ich die Stabilität des Samples als eher kritisch sehe. Hier trotzdem der Link:
Demo-Applikation zum Probieren in Azure
VS-2012-Solution inkl. packages (7zip)
WCF-Problem mit Bidrektionalität
Responsive Design mit ASP.NET MVC

Vorbemerkungen

SignalR ist ein Projekt, dass es sich zum Ziel gesetzt hat, den Gedanken, den Websockets eingeführt haben, konsequent weiter zu denken. Microsoft hat sich der Sache mit dem Projekt ASP.NET SignalR kürzlich angenommen und das war für mich das Signal, mich genauer mit dem Thema zu befassen.

Im Kern geht es darum, dass eine Peer-2-Peer(P2P)-Kommunikation aufgebaut werden kann. Jeder Teilnehmer dieser Kommunikation agiert dabei als Sender und Empfänger gleichzeitig. Dies unterscheidet SignalR von klassischen Kommunikations-Konzepten, die auf Client-Server-Paradigmen beruhen. WCF ist ein gutes Beispiel für letztere Variante. Hier erstellt man einen Service, der durch einen zentralen Endpunkt angeboten wird und Clients können sich dann mit diesem Verbinden. Abb. 1 stellt das mal grob dar und Abb. 2 stellt den P2P-Ansatz dem gegenüber:

Abb. 1: Client-Server-Ansatz
Abb. 1: Client-Server-Ansatz
Abb. 2: P2P-Ansatz
Abb. 2: P2P-Ansatz

Soweit, so gut. Wie kann man nun aber die Verteile des P2P-Ansatzes geschickt ausnutzen? Die meisten kommen sofort auf das Chat-Pattern. Ein Client setzt eine Nachricht ab und alle Clients bekommen diese Nachricht quasi in Echtzeit. Das funktioniert ganz reibungslos und wird daher auch im Internet hoch und runter kolportiert.

Die einzig witzige und anspruchsvollere Adaption, die ich bisher kennen gelernt habe, ist Shootr, eine Spiel-Implementierung der Macher von SignalR selbst.

Die Frage war nun, ob es nicht etwas Sinnvolleres damit anzustellen gibt.

Idee

Das schwierigste an meinem kleinen Beispielprojekt war die Idee an sich. Es dauerte ein wenig, bis ich mich an verteilte Konzepte, wie z.B. Seti@Home erinnerte und die Idee plötzlich charmant fand. Diese ist die, dass man Ressourcen (in diesem Fall Rechnerkapazität) nicht unbedingt immer zentral bereit halten muss oder kann.

Stattdessen setzt man auf die Macht des Internet und seiner Community. Ist irgendwer nun bereit, einen Teil seiner Ressourcen für eine gemeinsame Sache bereit zu stellen, kann er dies in solchen Projekten, sie Seti@Home tun. Das funktioniert so, dass der Server „weiß“, welche Clients gerade online sind und diesen kleine Arbeitspakete zustellt. Der Client rechnet jetzt dran herum und liefert das Ergebnis an den Server zurück. Dieser fügt alle Teilergebnisse dann zu einem Gesamtergebnis zusammen. Man hat also Multiprocessing über Rechnergrenzen hinweg. Der Nachteil an bisherigen Konzepten des verteilten Rechnens war der, dass die Infrastruktur dafür meist proprietär aufgebaut werden musste bzw. dass man, wie bei SETI auch, einen Client installieren muss. So viel overhead muss es für bestimmte Aufgaben aber vielleicht gar nicht sein!

Mir fiel nun auf, dass man mit SignalR etwas ähnliches erreichen kann, wenn man die ausgetretenen Chat-Pfade ein wenig verlässt.

Das Projekt

Um ein möglichst einfaches und trotzdem funktionierendes Sample zusammen zu bekommen, habe ich mich für das folgende Shared-Working-Szenrio entschieden:

Ein Server basierend auf einer mit SignalR angereicherten MVC-Webseite gehostet in Microsoft Azure erlaubt es, beliebig vielen Clients, sich einzuloggen. Jeder eingeloggte Client erhält Arbeitspakete. Der Task ist, eine Guid per SHA-512 zu hashen und den Hash dann an den Server zurück zu liefern.

Die Arbeit ist relativ sinnfrei, zeigt aber die Idee recht einfach und erlaubt es zudem, dass man einfach die Arbeitspakete ändert und etwas anderes implementiert. Eine Idee wäre z.B., dass man die Technik nutzt, um Bilder zu rendern usw.

Damit das ganze ein wenig mehr Spaß macht, habe ich ein sehr simples Ranking auf Basis der berechneten Pakete sowie der Client-Performance eingebaut.

Die Umsetzung

Eins vielleicht vorweg: In diesem Artikel ist nicht genug Raum, um SignalR komplett zu erläutern. Ich werde dies u.U. in einem eigenen Beitrag nachholen. Mit anderen Worten: Ein paar Vorkenntnisse werden hier vorausgesetzt.

Alles begann mit einem neuen Projekt auf Basis der Internet-Vorlage von ASP.NET MVC 4. Zunächst gilt es, die notwendigen Packages per Nuget zum Projekt hinzu zu fügen:

install-package -pre Microsoft.AspNet.SignalR
install-package -pre Microsoft.AspNet.SignalR.Client

Das erste Package enthält nur den Server-Teil. Um das hier umzusetzen, brauchte ich aber auch den zweiten Teil.

Bei SignalR kann man auswählen, ob man eines der beiden Verbindungs-Muster „Persistent Connection“ oder „Hubs“ verwenden möchte. Da ich letzteres vorziehe, habe ich zunächst eine eigene Klasse implementiert, die von der Klasse „Hub“ erbt. Vereinfach ausgedrückt, definiert man hierin nun all die Methoden, die später allen Clients zur Verfügung stehen sollen. Die Bereitstellung übernimmt ASP.NET für uns. Dazu müssen wir 2 Dinge anpassen.

Zunächst wird in der Datei ~/AppStart/BundleConfig.cs ein neues Bundle für die SignalR-Javascript-Dateien erzeugt, die uns nuget gebracht hat:

bundles.Add(new ScriptBundle("~/bundles/signalr").Include(
            "~/Scripts/jquery.signalR-1.0.0-rc2.js"));

Nun müssen wir dieses Bundle in unserem Layout-View einbinden. Es ist darauf zu achten, dies unbedingt NACH dem Einbinden von jQuery zu tun.

@Scripts.Render("~/bundles/jquery")
@Scripts.Render("~/bundles/signalr") 
<script src="~/signalr/hubs" type="text/javascript"></script> 

Zeile 3 in Listing 3 zeigt außerdem das obligatorischen des dynamisch durch SignalR erzeugten Endpunkts „/signalr/hubs“.

SignalR basiert komplett auf JavaScript und somit müssen wir nun noch festsetzen, dass irgendwann eine SignalR-Connection zwischen unserem Browser und dem SignalR-Server durchgeführt werden soll. Den ganzen SignalR-spezifischen JS-Teil habe ich in die Datei „/Scripts/tools/worker.js“ ausgelagert. Dort taucht dann ziemlich weit unten folgender Code auf:

var proxy;
// ...
function Connect() {
    proxy = $.connection.workHub;
    $.connection.hub.start({ transport: 'longPolling' }).done(function () {
        // ...
    });
}

Zunächst erstellt man sich einen Proxy, der, ähnlich wie bei WCF, eine im Browser geladene „Kopie“ des serverseitigen Codes darstellt. Der Proxy bekommt den Namen der Klasse, die von Hub erbt bzw. der dort im Attribut Hubname eingestellt ist:

[HubName("workHub")]
public class WorkHub : Hub
{
    ...
}

In diesem Beispiel hätte ich das weg lassen können, denn man muss wissen, dass die javascript-seitigen Klassen und Methoden immer im Camel-Case benannt werden. Durch mein Attribut habe ich das nur explizit reingeschrieben. Dieses „workHub“ muss sich also decken mit dem aus Listing 4 Zeile 4.

Listing 4 ist insofern auch interessant, weil es das Attribut „transport“ auf „longPolling“ stellt. Bei mir war das nötig, da die Azure-Instanz ansonsten nur Proxy-Abort-Meldungen ausspuckte. Dazu später mehr.

Interessant ist auch, dass man nun auf Server- und Client-Seite in bestimmte Ereignisse und Eigenschaften der Verbindung eingreifen bzw. einsehen kann. Die Serverseite (das, was auch dem Webserver ausgeführt wird) ist bei uns durch die Klasse „WorkHub“ repräsentiert. Hier definiert man zunächst einfach fröhlich Methoden, sie z.B. diese:

public void SendMessage(string clientName, string message)
{
    Clients.All.broadcastMessage(clientName, message);
}

Das ist die klassische Chat-Methode. Ein client kann sie per Javascript so aufrufen:

proxy.server.broadcaseMessage('me', 'hello');

Diese Nachricht erhalten wegen „Clients.All“ dann alle verbundenen Clients und somit auch der Sender selbst. Interessant ist nun aber zunächst einmal, dass es überschreibbare Methoden in WorkHub gibt:

public override Task OnConnected() {}
public override Task OnDisconnected() {}

Das ist schon mal eine Erleichterung, wenn man Code ausführen will, sobald ein neuer Client sich an- oder abmeldet. Weiter hilfreich ist die Tatsache, dass man innerhalb des WorkHub jeder Zeit Kontextinformationen über die Eigenschaft „this.Context“ erhält. Im Kontext enthalten ist so etwas, wie das Äquivalent zum HTML Request. Man weiß also hier z.B. Details über den Aufrufenden Client zu berichten. SignalR hilft uns hier automatisch aus der Patsche, weil jeder Client eine GUID erhält, sobald er sich connected.

Ein Problem bleibt

Das ist bis hierher noch kein Hexenwerk. Das Problem an meinem Projekt ist aber, dass es irgendeine Art von Server geben muss, der die Arbeit an die schönen vielen Clients verteilt, sobald er welche vorfindet. Dieses Problem ist bei näherem Betrachten relativ schwierig zu durchdringen. Es stellen sich Fragen:

  1. Was wird da auf dem Webserver ausgeführt?
  2. Wann wird mit der Ausführung begonnen?
  3. Muss man das Etwas irgendwann auch mal stoppen?

Der Grund für diese Fragen ist der, dass ein Webserver eigentlich immer noch eine Client-Server-Infrastruktur darstellt. Der Server bedient einen Client und weiß nichts von anderen Clients bzw. er ist zustandslos (stateless). Wir brauchen aber einen State, sprich, wir wollen etwas haben, dass alle Clients sieht und füttert.

Um dies umzusetzen, habe ich die Klasse Scheduler im Ordner „Logic“ implementiert. Sie ist so etwas, wie ein Windows Dienst. Sie läuft also in einer Endlosschleife, sieht nach, ob es Clients gibt, die nichts zu tun haben und wirft Ihnen dann Arbeitspakete zu. Ein paar Modell-Klassen (ClientModel, TaskModel) im Ordner „Models“ helfen, dies strukturierter anzugehen.

Der Trick war, den Server einfach nur als weiteren Client aus SignalR-Sicht zu verstehen. Das heißt, wenn der Server einem Client Arbeit geben will, dann tut er das per SignalR und wenn der Client sein Ergebnis liefern will, stellt der WorkHub auch dafür eine Methode bereit.

Die Details kann man sich im Quellcode genauer ansehen. Es kommt noch hinzu, dass der Server gestoppt wird, sobald keine Clients mehr da sind, damit er sich nicht tot läuft. Beim nächsten Zugriff startet er dann wieder.

So siehts aus

Zunächst einmal ein Screenshot für all diejenigen, die keine Lust haben, am öffentlichen Hashing teilzunehmen. (Natürlich speichere ich keinerlei Daten und nein, ich benutze das ganze hier nicht als Rainbow-Table-Ersatz!). Hier nun das Tool in Aktion:

Abb. 3: Einsatz
Abb. 3: Einsatz

Wie man sieht, habe ich 3 Tabs geöffnet. Auf jedem der Tabs habe ich dann „Connect“ oben rechts angeklickt und schon gings los. Man sieht jetzt unter „Clients“ einfach die 3 aktuell verbundenen Instanzen. Rechts davon habe ich 2 einfache Listen mit einem Ranking jeweils nach Performance (Millisekunden/Hash) und Anzahl berechneter Hashes. Unten ist noch ein Flyout „Messages“, das man einblenden kann, um sich die Messages anzusehen, die über SignalR gelaufen sind (nur jeweils die letzten 20). Klickt man oben auf Disconnect, beendet der Client seine Verbindung und bekommt keine Pakete mehr. Unten habe ich noch den Firebug eingeblendet. Damit kann man hervorragend die angesprochenen Endpunkte und die Daten einsehen, die über die Leitung gehen.

Was fehlt?

Zunächst einmal ist mein Code noch teilweise buggy und krude. Ein paar Bugs sind auch noch drin. Er zählt z.B. immer einen Client zuviel. Die im Download enthaltenen JavaScript- und CS-Ansätze bitte ich außerdem nicht als Arbeitsanweisung zu interpretieren. Hier ging es darum, die Sache an sich zu zeigen.

Ich plane, das ganze ein wenig aufzuhübschen und gerade auf Client-Seite performanter zu halten. Ich habe außerdem vor, ein paar Stats grafisch aufzubereiten und übergreifend für den Server zu zeigen.

Das MVC-Template basiert übrigens auf Zurb Foundation. Dadurch ist es auch auf Mobilgeräten leidlich bedienbar. Details zu Foundation siehe Artikel-Link oben.

Ich hoffe, es ist mir gelungen, mal was anderes zu zeigen, als die ständig selben Chat-Beispiele. Ich denke, es gibt schon den ein oder anderen Einsatzzweck, für solch verteiltes Rechnen und hoffe, ein paar Leuten Anregungen gegeben zu haben.

Eine Antwort auf „SignalR für verteiltes Arbeiten einsetzen“

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.

Diese Website verwendet Akismet, um Spam zu reduzieren. Erfahre mehr darüber, wie deine Kommentardaten verarbeitet werden.