=encoding utf-8 =head1 AnyEvent - Eine Welt ohne Religion In diesem Vortrag möchte ich die neuesten Entwicklungen in Sachen AnyEvent vorstellen (asynchrones IPv4/IPv6, asynchrones DNS, TLS uvm.), sowie einige Hintergrundinformationen zu bestehenden Event-Modulen auf CPAN, sowie Benchmarks, vorstellen. Doch zuerst: der Titel klingt sicherlich etwas seltsam - was bedeutet er? Nun, AnyEvent ist, wie im Namensteil "Event" steht, ein Event-Modul bzw. eine Event-Bibliothek. Event-Module oder -Bibliotheken stellen im allgemeinen Funktionalität bereit, mit der man auf Ereignisse (Events), warten kann, speziell Ereignisse der Art "Zeitpunkt soundso ist erreicht" und "File Descriptor soundso kann jetzt nichtblockierend gelesen/geschrieben werden." Derartige Event-Bibliotheken gibt es zuhauf (Tk, Glib, Event, EV uvm.), doch trotz der offensichtlichen Vorteile Event-basierter Programmierung (z.B. beinahe automatische Parallelisierbarkeit) werden sie kaum in CPAN-Modulen eingesetzt. Meiner Meinung nach liegt der Grund schlicht darin, daß alle diese Event-Bibliotheken exklusiv sind: sie dulden niemanden neben sich. Wie bei den meisten Religionen auch, muss man sich einem dieser Modelle ausliefern. Konkret bedeutet dies für Modul-Autoren: benutzt man z.B. Glib in seinem Modul, muss auch jeder Benutzer dieses Modules Glib benutzen, was natürlich eine starke Einschränkung darstellt, vor allem, wenn das Modul selbst eigentlich nur "irgendwelche" Events benötigt, die im Prinzip jede beliebige Event-Bibliothek liefert. Einige Module auf CPAN haben versucht, aus dieser Not eine Tugend zu machen: POE z.B. kann andere Event-Module als Implementation benutzen (in der Praxis funktioniert das zwar nur mit einigen wenigen, aber die Idee zählt). Lieder verschlimmert dies das Problem nur: POE z.B. verlangt beinahe religiöse Glaubensbekenntnisse und ist kein bisschen kompatibler, im Gegenteil: obwohl POE Backends für Gtk+ und Event besitzt, kann man POE-Module nicht in einem Gtk+ oder Event-basiertem Programm benutzen: setzt man POE in seinem Modul ein, so muss auch jeder Benutzer des Moduls POE benutzen, sowie auch das Hauptprogramm selbst. AnyEvent dagegen ist I anders: Im Stile von anderen C-Modulen ist es lediglich eine Schnittstelle zu diesen. Dabei passt es sich an jedes unterstützte Event-Modell an, ohne daß sich die API ändert. Das bedeutet, daß man getrost Module benutzen kann, die AnyEvent benötigen und intern auch benutzen, aber der Benutzer des Moduls sieht davon erstmal nichts und kann jede beliebige Event-Bibliothek benutzen. AnyEvent ist 100% Pure-Perl, und kommt mit eigener, schneller, Event-Bibliothek, so daß auch auf Systemen, wo kein anderes Event-Modell verfügbar ist, eventbasiert programmiert werden kann. =head2 AnyEvent am Beispiel AnyEvent::CouchDB Ein gutes Beispiel dafür ist C. Außer im Namen, kommt AnyEvent in der API dieses Moduls nicht vor. Eine einfache Datenbankabfrage sieht dementsprechend auch ganz langweilig aus: my $data = $db->view('users/all', { key => 'b' })->recv; Obwohl die Abfrage selbst ereignisgesteuert ist, muss man als Benutzer des Moduls kaum darunter leiden, lediglich die Zweiteilung der Aufgabe in "Start-Phase" (C-Methode) und "Resultat-Phase" (C-Methode) ist typisch. Daß intern AnyEvent werkelt, fällt nicht auf, außer, daß die Abfrage in einem laufenden Tk/Gtk usf.-Programm dieses nicht anhält. Im Code werde die Vorteile sichtbar, wenn man mehrere Abfragen "gleichzeitig" (genauer: miteinander verflochten) ausführt: my @data = map $_->recv, map $db->view ('users/al', { key => $_ }), qw(key1 key2 key3 key4); Auch hierbei muss man nicht darauf achten, welches Event-Modul benutzt wird (oder ob man überhaupt ein spezielles benutzt) - es funktioniert einfach. Richtig Spaß macht es, wenn man realisiert, daß man praktisch alle AnyEvent-benutzenden Module beliebig kombinieren kann, und Aufgaben quasi "im Hintergrund" erledigen kann, z.B. mit C und C: $db->view('users/all', { key => 'b' })->cb (sub { my $data = shift->revc; # daten erhalten }); AnyEvent::HTTP::http_get "http://www.porttracker.co.uk", sub { my $html = $_[1]; # daten erhalten }; # hier kann man weitere Dinge erledigen Und all das kann man in den eigenen Modulen nutzen, ohne sich irgendwie auf ein bestimmtes Event-Modell festzulegen. =head2 AnyEvent - Grundlegendes AnyEvent selbst wurde letztes Jahr an dieser Stelle ausführlich vorgestellt, daher möchte ich hier nur eine kurze Übersicht bzw. Einführung in AnyEvent als rein Event-Bibliothek geben: Wie viele andere Bibliotheken auch, benutzt AnyEvent das Konzept eines "Watchers", eines Objekts, das wie ein Wachhund auf ein bestimmtes Ereignis reagiert (watch/watcher = Wache/Beobachter) und bei Eintreten eine Aktion auslöst, z.B. Aufrufen eines Callbacks. Diese Watcher gibt es für die folgenden Ereignistypen: =over 4 =item Zeit - timer my $timer = AnyEvent->timer (after => 7, cb => sub { warn "Sieben Sekunden sind abgelaufen!"; }); Timer können nach einer gewissen Zeit ablaufen, oder wiederholt (z.B. alle 3s) Ereignisse auslösen und Callbacks aufrufen. =item I/O auf File Handles - io my $watcher = AnyEvent->io (fh => *STDIN, poll => 'r', cb => sub { my $line = ; # zeile von stdin gelesen }); I/O-Watcher beobachten einen File Handle und können einen Callback aufrufen, wenn gelesen oder geschrieben werden kann. =item Signale - signal my $quit = AnyEvent->condvar; my $term = AnyEvent->signal (signal => "TERM", cb => sub { $quit->send; }); ... $quit->recv Mit Signal-Watchers kann man synchron auf das Auftreten von Signalen warten. Das obige Beispiel benutzt eine "condition variable", um mit dem Hauptprogramm zu kommunizieren (mehr Info s.u.). =item Kindprozesse sterben sehen - child my $w = AnyEvent->child (pid => $pid, cb => sub { warn "pid $pid hat sich beendet\n"; }); Eher selten benötigt, aber sehr schwer zu implementieren (wenn man es richtig tun möchte :), sind Child-Watcher: mit diesen kann man auf das Beenden von ge-fork-ten Prozessen warten. =back =head3 Wenn man mal nichts tun möchte Event-basierte APIs haben eines gemein: Im allgemeinen werden Vorgänge nur gestartet und laufen dann quasi-parallel im Hintergrund ab. Aber wenn man nun genügend solche Vorgänge gestartet hat, möchte man ja irgendwann auch mal auf die Ergebnisse warten. Dies erledigt entweder das Hauptprogramm, indem es die entsprechenden Funktionen der Event-Bibliothek aufruft (z.B. C oder C
), oder man benutzt sogenannte "condition variables", mit denen man auf das eintreten eines bestimmten Zustands warten kann. "Condition Variables" sind so etwas wie "Rendezvouspunkte" zwischen dem Erzeuger von Daten oder einem Ereignis (z.B. Cview> oder C, aber auch genauso C<< AnyEvent->timer >>) und dem Konsumenten (der Benutzer dieser Funktionen, der die Daten haben oder einfach auf das Ereignis warten möchte). =head1 Neues! Auf zu neuen Ufern - im letzten Jahr hat sich viel getan, sowohl in AnyEvent selbst, als auch in Sachen "andere Module die AnyEvent benutzen". =head2 Altes! Die wichtigsten Eigenschaften von AnyEvent sind allerdings beim alten geblieben: =over 4 =item * Die API hat sich kaum verändert, bzw. ist 100% rückwärtskompatibel. =item * AnyEvent ist nach wie vor "Pure-Perl". =item * Der Event-Teil ist nach wie vor getrennt von den anderen Teilen, das Event-Modul ist klein und wenn man bezahlt nur für das, was man benutzt. =back Aber nun zu den Neuigkeiten: =head2 AnyEvent::Strict Aus Geschwindigkeitsgründen macht AnyEvent selbst keine Parameterprüfung, ganz einfach deshalb, weil die Prüfung meistens mehr Zeit in Anspruch nimmt als der Rest der Funktion. Für einen erfahrenen AnyEvent-Benutzer ist das in der Praxis kein Problem, aber wenn man die API noch nicht genau kennt, können sich so Fehler einschleichen (z.B. alte Event-Hasen benutzen gerne C statt C um einen I/O-Watcher zu erzeugen). Daher bietet AnyEvent optional eine strenge Prüfung aller Parameter an. Die kann man ganz einfach aktivieren, in dem man die Environment-Variable C auf C<1> setzt, was auf allen meinen Entwicklungsrechnern der Fall ist. Falls man diese Prüfung grundsätzlich wünscht (was ich ganz dringend I empfehle, siehe aber den Punkt "keine Religion"...), kann man diese auch immer aktivieren: use AnyEvent; use AnyEvent::Strict; # jetzt wird brutal geprüft. Ich gebe zu, das war sicher noch nicht der Brüller, aber sehen wir weiter: =head2 AnyEvent::Util Dies ist meine Grabbelkiste für nützliche aber eklige Funktionen: =over 4 =item fork_call BLOCK $callback Führt den angegebenen BLOCK in einem separaten Prozess aus und ruft danach den $callback mit den Resultatwerten auf. Ist ideal, um lange Berechnungen in einen separaten Prozess zu verlagern: fork_call { find_primes_till 1e9 } sub { warn "alle primzahen bis 10**9: ", join " ", @_; }; Die Funktion leaked allerdings unter Windows, aber ich habe bisher noch keinen Weg gefunden, unter einem native-win32-Perl zu forken und das Kind sauber zu beenden (ich glaube, es gibt keinen...). =item my $guard = guard BLOCK Führt den angegebenen BLOCK aus, wenn C<$guard> zerstört wird. Ist bei ereignisgesteuerter Programmierung total hilfreich, um Sachen aufzuräumen. Wenn es vorhanden ist, benutzt AnyEvent das C-Modul (ja, das ist Schleichwerbung). =item fh_nonblocking $fh, $nonblocking Einen File Handle non-blocking zu machen, ist nicht ganz einfach (Windows, Windows, kaputt, kaputt...). C erledigt dies mit furchtbaren Hacks unter Windows und elegant überall sonst. =back Immer noch nicht überzeugt? Ok, dann machen wir mal mit Netzwerkprogrammierung weiter: =head2 AnyEvent::Socket Sockets in Perl haben ein paar gravierende Nachteile: non-blocking connects sind extrem umständlich, IPv6 und IPv4 brauchen völlig inkompatible APIs, DNS blockiert den gesamtem Prozess und liefert darüber hinaus auch nur altmodische IP-Adressen. Anders mit AnyEvent::Socket: tcp_connect "www.google.com", "80", sub { my ($fh) = @_ or die "www.google.com: $!"; ... }; Dieser Aufruf löst zuerst C auf - im Hintergrund, ohne das Programm anzuhalten - und baut dann, ebenfalls im Hintergrund, eine TCP-Verbindung dorthin auf. Es versteht sich von selbst, daß Multi-Homed Hosts genauso unterstützt werden wie IPv6, und das ohne zusätzliche XS-Module. C kann aber noch mehr. Z.b. auf Jabber-Server connecten, was mit Perl alleine fast unmöglich ist, da man mit Perl keine SRV-Records auflösen kann: tcp_connect "jabber.org", "xmpp-server=5269", sub { # Die pseudo-XML-Kacke parsed ihr bitte selbst... }, 60; # <- timeout Der Name C ist leider etwas naiv, tatsächlich werden auch andere Protokolle unterstützt, z.B. local unix Sockets: tcp_connect "unix/", "/tmp/.X11-unix/X0", sub { ... Listening-Sockets funktionieren prinzipiell genauso einfach, z.B. hier für einen TCPv6-Port: tcp_server "::", 80, sub { my ($fh, $host, $port) = @_; # jede neue Verbindung führt zu einem Aufruf dieses Callbacks }; Oder einen beliebigen TCPv4-Port: tcp_server "0", undef, sub { my ($fh, $host, $port) = @_; ... }, sub { my ($thishost, $thisport) = @_; warn "bound to port $thisport\n"; }; Leider muss man sich selbst darum kümmern, ob IPv6-Sockets auch auf IPv4 hören - üblicherweise ja, aber die BSDs haben natürlich mal wieder verbockt und halten sich nicht ans RFC. Komplizierteres ist immer möglich - anders als z.B. mit C hat man volle Kontrolle über die eigentliche Socket und braucht nicht für jeden etwas spezielleren Wunsch extra Support. Und anders als bei anderen Lösungen funktionieren diese Funktionen auch unter Windows korrekt (modulo Tk, daß wie üblich zu kaputt ist, um non-blocking connects stabil zu kriegen, aber AnyEvent arbeitet auch um diese Bugs herum). Neben diesen beiden Funktionen gibt es in AnyEvent::Socket eine Unmenge an Utility-Funktionen zum parsen/anzeigen von IP-Adressen, Socket-Adressen und DNS: $ipn = parse_ipv4 $dotted_quad $ipn = parse_ipv6 $textual_ipv6_address $ipn = parse_address $text $text = AnyEvent::Socket::aton $ipn ($host, $service) = parse_hostport $string[, $default_service] $sa_family = address_family $ipn $text = format_address $ipn $text = AnyEvent::Socket::ntoa $ipn inet_aton $name_or_address, $cb->(@addresses) $sa = AnyEvent::Socket::pack_sockaddr $service, $host ($service, $host) = AnyEvent::Socket::unpack_sockaddr $sa resolve_sockaddr $node, $service, $proto, $family, $type, $cb->([$family, $type, $proto, $sockaddr], ...) Hier verweise ich auf die Dokumentation. Einzig C möchte ich genauer erklären: Selbst die Aufgabe, einen Hostnamen und einen Port anzugeben, ist nicht ganz einfach wenn man es richtig machen will. C nimmt ein oder zwei Strings und zerlegt diesen in Host und Port, z.B. diese: www.linux.org www.x.de:443 www.x.de:https=443 127.1:22 ::1 affe::1 [10.0.1]:80 [www.x.org] 17 10.0.0.1 smtp Richtig, IPv6 macht das ganze deutlich komplexer. Und IPv6 ist die Zukunft, ob man das nun gut findet oder nicht :/ Ach ja, Stichwort IPv6 - es kann auch furchtbar nervig sein, wenn Programme grundsätzlich die IPv6-Adresse bevorzugen, die gerade nicht erreichbar ist. Der Standard bei AnyEvent ist zur Zeit IPv4 den Vorzug zu geben, was sich allerdings über die Environment-Variable C beeinflussen lässt. =head2 AnyEvent::DNS Nur am Rande möchte ich AnyEvent::DNS erwähnen. Ursprünglich war es nur dazu gedacht, die Adressen für C zu resolven. Das Modul ist aber ein kompletter Stub-Resolver, der beliebige DNS-Record-Typen, EDNS0, IPv6 und virtual circuit mode supported. Das Modul ist in der Praxis ca. halb so schnell wie z.b. EV::ADNS (braucht natürlich viel mehr Speicher, blockiert aber im Gegensatz zu EV::ADNS nie). Es gibt Hilfsunktionen (hier nur eine Auswahl): AnyEvent::DNS::a $domain, $cb->(@addrs) AnyEvent::DNS::mx $domain, $cb->(@hostnames) AnyEvent::DNS::srv $service, $proto, $domain, $cb->(@srv_rr) AnyEvent::DNS::ptr $domain, $cb->(@hostnames) AnyEvent::DNS::reverse_lookup $ipv4_or_6, $cb->(@hostnames) AnyEvent::DNS::reverse_verify $ipv4_or_6, $cb->(@hostnames) Und eine Resolver-Klasse: use Data::Dumper; use AnyEvent::DNS; AnyEvent::DNS::resolver->resolve ( "google.com", "*", my $cv = AnyEvent->condvar); warn Dumper [$cv->recv]; # gekürztes Ergebnis: # [ # [ 'google.com', 'soa', 'in', 'ns1.google.com', 'dns-admin.google.com', # 2008052701, 7200, 1800, 1209600, 300 ], # [ # 'google.com', 'txt', 'in', # 'v=spf1 include:_netblocks.google.com ~all' # ], # [ 'google.com', 'a', 'in', '64.233.187.99' ], # [ 'google.com', 'mx', 'in', 10, 'smtp2.google.com' ], # [ 'google.com', 'ns', 'in', 'ns2.google.com' ], # ] =head2 AnyEvent::Handle Nun zum ehrgeizigsten Teil der Neuerungen: C. Die Probleme, die dieses Modul lösen möchte, kommen in der Praxis häufig vor: Formatted-I/O, Pipelining und TLS-Verschlüsselung. Früher habe ich immer ad-hoc Lösungen für diese Probleme implementiert, da mir schlicht die Erfahrung gefehlt hat, wie man alle diese Features in ein allgemeines Perl-Modul "gießt". C versucht diese Lösung zu sein: AnyEvent::Handle-Objekte sind Wrapper um existierende File Handles. Im folgenden beschreibe ich die Lösung, die AnyEvent::Handle für die oben beschriebenen Probleme bietet: =over 4 =item Formatted I/O Um auf ein AnyEvent::Handle-Objekt zu schreiben, benutzt man C. Das C deutet darauf hin, daß es einen Puffer für die Daten gibt, und C hängt die Daten einfach an diesen an: $hdl->push_write ("GET / HTTP/1.0\015\012\015\012"); Mit mehr als einem Argument fordert man zusätzliche Formatierungen an, die meisten dieser Formatierungen gibt es sowohl für die Schreib- als auch die Leseseite: $hdl->push_write (netstring => "xyz"); # djb netsrings $hdl->push_write (packstring => "w", "data"); # benutzt pack() $hdl->push_write (json => [1, 2, 3, 4]); # benutzt JSON $hdl->push_write (storable => $ref); # benutzt Storable Schreiben ist einfach, Lesen ist schwieriger. Anders als beim Schreiben enthält der Puffer keine Daten, sondern die Callbacks, die der Reihe nach aufgerufen werden, wenn Daten eintreffen. Die Flexibilität entsteht dadurch, daß man Callbacks ans Ende des Puffers anhängen kann: $hdl->push_read (json => sub { my $data = $_[1] }); Aber auch vor dem Anfang einschieben kann: $hdl->unshift_read (json => sub { my $data = $_[1] }); Interessant ist dies vor allem, weil man hierdurch recht einfach auch komplexe Protokolle mit Pipelining implementieren kann. Man kann jederzeit eigene Formatierungstypen global für alle Handles hinzufügen. Zusätzlich zu den schon vorgestellten Typen für die Schreibrichtung, die auch für die Leserichtung existieren, gibt es noch: $hdl->push_read (line => $eol, $cb); # zeilen $hdl->push_Read (regex => $accept[, $reject[, $skip], $cb); =item Pipelining Pipelining bedeutet, daß man auf einer bestehenden Verbindung mehrere Anfragen gleichzeitig losschickt. Die Herausforderung liegt darin, daß man beim Empfang der Antworten flexibel auf Ausnahmen reagieren können muss. Bei HTTP beispielsweise weiß man erst nach Parsen des Headers, wie lang der Body wird. AnyEvent::Handle erledigt dies mit einer Kombination aus C und C: # sende requests for (@urls) { $hdl->push_write ("GET $_ HTTP/1.0\015\012\015\012"); } # parse antworten for (@urls) { # lese header $hdl->push_read (line => "\015\012\015\012", sub { my ($hdl, $header) = @_; my $content_length = extract "Content-Length", $header; $hdl->unshift_read (chunk => $content_length, sub { my ($hdl, $body) = @_; # fertig }); }); } Der springende Punkt ist, daß C erst I dem Empfang des Headers ausgeführt wird. Dadurch kann man z.B. verschiedene Arten von Antworten parsen, oder auf unerwartete Fehler reagieren, obwohl vielleicht schon Antworten auf die nächsten Requests empfangen wurden. =item TLS/SSL TLS ist der Horror. Nicht nur, aber vor allem auch wegen der grauenvollen OpenSSL-API (GnuTLS ist geringfügig besser, doch sucht man vergebens nach einem guten Perl-Modul dafür). Der Beweis dafür ist, daß es praktisch kein Modul gibt, daß non-blocking TLS connects korrekt und wirklich non-blocking durchführt. Es hilft auch nicht gerade, daß Net::SSLeay nur einen Bruchteil der OpenSSL-API umsetzt. AnyEvent::Handle (to boldly go...) versucht einen ganz neuen Ansatz für diese Problematik, bei der OpenSSL vom eigentlichen File Handle komplett getrennt wird, d.h. gar keinen Zugriff auf diesen hat. Dadurch wird garantiert, daß OpenSSL auf gar keinen Fall blockieren kann. Ganz wie der Rest von AnyEvent ist einfach einfach: wenn man zu einem TLS-Server connecten möchte, setzt man C auf C<"connect">, als TLS-Server dagegen setzt man es auf C<"accept">. Hat man kompliziertere Ansprüche, wie Zertifikate oder Verification-Callbacks, so kann man sein eigenes Net::SSLeay::CTX-Objekt oder das Net::SSLeay-Objekt für die Verbindung selbst übergeben. Selbstverständlich kann man den TLS-Handshake zu einem beliebigen Zeitpunkt beginnen, nicht nur am Anfang der Verbindung. =back Mit AnyEvent::Handle wird manches erstaunlich einfach. Hier ist z.B. ein (ganz primitiver) HTTPS-Client (übrigens das komplette Programm): use AnyEvent; use AnyEvent::Socket; use AnyEvent::Handle; my $cv = AnyEvent->condvar; tcp_connect "www.google.com", "443", sub { my ($fh) = @_ or die; my $hdl; $hdl = new AnyEvent::Handle fh => $fh, timeout => 60, tls => "connect", on_eof => sub { undef $hdl; # gibt handle frei $cv->send; # isch hab' ferdisch }, on_read => sub { my ($hdl) = @_; print $hdl->{rbuf}; $hdl->{rbuf} = ""; }; $hdl->push_write ("GET / HTTP/1.0\015\012\015\012"); }; $cv->recv; Zuerst zum Einsatz kommt C, um den non-blocking Connect durchzuführen. Danach wird das Handle-Objekt erzeugt, konfiguriert, und danach lediglich der Request gesendet, der Rest des Protokolls wird ereignisgesteuert abgehandelt. Der Unterschied zwischen TLS (https) und nicht-TLS (http) besteht lediglich in dem Parameter C "connect">, der einen sofortigen TLS-Client-Handshake auslöst (will man in der Mitte der Verbindung switchen, z.b. für SMTP, kann man jederzeit die C-Methode aufrufen). Darüber hinaus gibt es eine Menge Parameter, mit dem man Verhalten, Fehlerbehandlung, Ressourcenbenutzung auf seine Anforderungen abstimmen kann. Für ganz spezielle Anwendungen kann man von AnyEvent::Handle auch eigene Klassen ableiten, aber in den meisten Fällen ist dies unnötig. Ein Nachteil von AnyEvent::Handle ist allerdings, daß die Abstraktion selbst nicht kostenlos ist: Methodenaufrufe in Perl sind vergleichsweise langsam, und AnyEvent::Handle benutzt sehr viele davon. In den meisten Fällen merkt man davon allerdings nichts, da der Overhead durch AnyEvent::Handle immer noch sehr gering ist, wenn man mit den Daten auch noch etwas tun möchte. =back =head2 Benchmarks Ich bin es gewohnt, meine eigene Lösungen zu schreiben, notfalls immer und immer wieder. Bevor ich eine (hilfreiche) Abstraktion ruhigen Gewissens benutzen kann, muss ich immer erst wissen, wie viel sie kostet, damit ich eine vernünftige Abwägung machen kann. Deshalb habe ich drei Benchmarks durchgeführt, um herauszufinden, wie hoch der Overhead von AnyEvent gegenüber der direkten Benutzung von Event-Bibliotheken ist, und wie verschiedene Event-Bibliotheken bei typischen Server-Situationen skalieren, sowohl für Server mit sehr vielen Verbindungen, als auch für Server mit sehr wenigen. =head3 Benchmark 1: Overhead Um den Overhead von AnyEvent selbst zu testen, erzeugt dieser Benchmark sehr viele Timer (mit einem Time-out von 0 Sekunden) und ebensoviele I/O watcher auf STDOUT (üblicherweise eine tty), der auf den "schreibbar"-Zustand wartet. Dann lässt er alle Watcher genau einmal ihren Callback aufrufen und zerstört die Watcher dann. Gemessen wird die Zeit, die es jeweils braucht, einen Watcher zu erzeugen, den Callback auszuführen und den Watcher wieder zu zerstören. Die Kernel-Schnittstelle sollte einen relativ geringen Einfluss besitzen, sofern die Event-Bibliothek einigermaßen effizient implementiert wurde, da nur ein einziges mal ein einziger File-Descriptor abgefragt werden muss. (Der Benchmark liegt AnyEvent als F bei). Die Spalten in der Tabelle haben folgende Bedeutung: I ist die Anzahl der erzeugten Watcher-Objekte, d.h. die Anzahl der Timer + I/O-Watcher. I ist die Anzahl der RAM-Bytes, die ein Watcher-Objekt im Schnitt verbraucht. Dabei wird der Perl- und C-Anteil gezählt. I ist die durchschnittliche Zeit für die Erzeugung eines Watchers in millionstel Sekunden. I ist die Zeit, in millionstel Sekunden, die die Ausführung des Callbacks dauerte. Der Callback selbst zählt lediglich eine Variable herunter. I schließlich ist die durchschnittliche Zeit für das freigeben eines Watcher-Objektes, in millionstel Sekunden. Alle Benchmarks wurden auf einem Core2-Quad/3.6GHz durchgeführt, wobei das Programm mit Echtzeitpriorität auf CPU #3 lief. Name Watcher Bytes Create Invoke Destroy Hinweise /uSec /uSec EV/EV 400000 224 0.47 0.35 0.27 EV direkt EV/Any 100000 224 2.88 0.34 0.27 EV über AnyEvent Perl/Any 100000 452 4.13 0.73 0.95 AnyEvent/pure perl Event/Event 16000 517 32.20 31.80 0.81 Event direkt Event/Any 16000 590 35.85 31.55 1.06 Event über AnyEvent Glib/Any 16000 1357 102.33 12.31 51.00 >quadratisches Wachstum Tk/Any 2000 1860 27.20 66.31 14.00 SEGV bei >> 2000 watchers POE/Event 2000 6328 109.99 751.67 14.02 via POE::Loop::Event POE/Select 2000 6027 94.54 809.13 579.80 via POE::Loop::Select Wie erwähnt, ist der Benchmark so designed, daß das Kernel-Interface (select, epoll etc.) keinen Einfluss hat. Das bedeutet, daß die Skalierbarkeit der Event-Bibliothek hier I gemessen wird. Die Anzahl der Watcher wurde so gewählt, daß die Ausführungszeit für jede Event-Bibliothek ähnlich war: würde man mit EV weniger Watcher wählen, bekäme man keine aussagekräftigen Zahlen, würde man mit Tk mehr wählen, würde es crashen, und würde man bei Glib oder POE mehr wählen, würde ich wohl noch nächstes Jahr auf die Ergebnisse warten. Wie man sieht, ist die Ausführung von Callbacks und das Zerstören von Watchern bei EV direkt (ohne AnyEvent) und bei AnyEvent mit EV als backend identisch, was daran liegt, daß AnyEvent keinerlei Overhead hinzufügt. Lediglich beim Erzeugen von Watchern ist AnyEvent+EV deutlich langsamer als EV direkt. Das liegt daran, daß AnyEvent a) einen Methodenaufruf und b) benannte Parameter benutzt. Diese beiden Umstände machen das Erzeugen von Watchern mehr als fünf mal langsamer als die direkte Benutzung von EV. Auch ist der Speicherverbrauch mit durchschnittlich 224 Bytes bei beiden gleich. Sehr erfreulich ist auch, daß die Pure-Perl Event-Implementation nicht viel hinterherhinkt: Erzeugen von Watchern und die Callback-Aufrufe sind ungefähr halb so schnell wie mit der stark optimierten libev C-Bibliothek. Der Speicherverbrauch ist ebenfalls noch im Rahmen und liegt ca. beim doppelten dessen von EV. Etwas enttäuscht hat mich Event: Event war meine Bibliothek der Wahl, ich hielt sie für sehr schnell und korrekt, musste aber im letzten Jahrzehnt einige größere Design-Schwächen erkennen. Überrascht hat mich allerdings, wie viel langsamer Event gegenüber einer Pure-Perl Implementation ist. Tatsächlich habe ich einige Zeit mit dem Quellcode von Event verbracht und musste feststellen,daß Event in der Tat gewaltigen Aufwand beim Aufruf von Callbacks betreibt, was sich in der Zeit niederschlägt. Die langsame Erzeugungszeit liegt dagegen schlicht daran, daß jeder Parameter intern in einen dynamischen Methodenaufruf umgesetzt wird. Wie man am Speicherverbrauch und dem Zeitunterschied sieht, ist die direkte Benutzung von Event etwas effizienter als den Umweg über AnyEvent zu gehen, allerdings ist der Overhead im Vergleich zur Langsamkeit von Event sehr gering. Wie man an den anderen Zeilen sieht, liegt Event allerdings recht gut: Glib ruft Callbacks zwar schneller auf, die anderen Zeiten sind aber grauenhaft. Auch Tk (sofern es nicht crasht :) ist nicht wirklich schneller, und POE ist jenseits von Gut und Böse. Tatsächlich ist die Implementation von AnyEvent auf POE nicht sehr effizient, da POE dies ausschließt. Wie der nächste Benchmark beweist, ist POE allerdings unter realistischen Bedingungen genauso langsam. Eine anderen Sicht der obigen Resultate ist, daß die Behandlung eines Ereignisses mit EV ca. 1_600 Taktzyklen, mit AnyEvents Pure Perl Implementation ca. 3_100 Zyklen und mit POE beinahe 3_000_000 Taktzyklen benötigt_. =head3 Benchmark 2: ein großer Server Was den AnyEvent-Overhead angeht, reicht mir der obige Benchmark völlig, um mich zu beruhigen: der Overhead von AnyEvent über die Event-Bilbiothek hinaus ist gering, wenn man von EV absieht (was natürlich daran liegt, daß EV so schnell ist). Um die Pure-Perl Event-Bibliothek von AnyEvent zu testen, muss aber ein anderer Benchmark her. Außerdem wollte ich unbedingt wissen, wie verschiedene Systeme in der Praxis abschneiden. Um einen großen Server zu simulieren, gibt es ein paar Standardbenchmarks. Einer davon ist es, sehr sehr viele Socketpaare zu erzeugen. Auf der einen Seite jedes Paares ist ein "Server", der ein einzelnes Octet liest, und dieses dann auf eine zufällige andere Socket schreibt. Jedes Socketpaar hat einen assoziierten Timer, der bei jeder Aktivität zurückgesetzt wird. Auf diese Weise wandern die Octets auf einem Teil der Verbindungen hin und her, was in etwa einem realen Server entspricht: sehr viele Verbindungen, aber nur ein kleiner Teil davon ist in jeder Iteration aktiv. Im folgenden werden 10_000 Socketpaare (d.h. 20_000 Sockets) erzeugt, und davon sind jeweils 100 "aktiv". (Der Benchmark liegt AnyEvent als F bei). Die Spalten bedeuten im einzelnen: I ist die Zeit, die es braucht, um ein Socketpaar zu erzeugen und alle Watcher zu registrieren, wieder in millionstel Sekunden. I ist bei weitem die wichtigste Größe. Sie gibt an, wie lange es im Schnitt dauert, um eine "Anfrage" zu bearbeiten und den Timeout zurückzusetzen. Name Create Request /uSec /uSec EV 69.01 11.16 Perl 73.32 35.87 Event 212.62 257.32 Glib 651.16 1896.30 POE 349.67 12317.24 mit POE::Loop::Event Dieser Benchmark misst nun direkt die Skalierbarkeit und die Geschwindigkeit der Event-Bibliothek. EV hat hier einen großen Vorteil, da es epoll, einen weitaus effizienteren Mechanismus als select oder poll, benutzen kann, während alle anderen entweder select oder poll benutzen. Wenig verwunderlich ist EV hier wieder mit Abstand am schnellsten. Sehr erstaunt war ich über das sehr gute Abschneiden der Pure-Perl Implementation. Sie ließ die anderen C-Bibliotheken weit hinter sich. Möglicherweise liegt der Grund hierfür darin, daß AnyEvent ganz ähnliche Algorithmen wie EV benutzt um I/O-Watcher und Timer zu verwalten, und ansonsten sehr abgespeckt ist. Event leidet sicher daran, daß es so teuer ist, Watcher zu erzeugen und upzudaten. Woran Glib leidet, ist schlichtweg unglaubliche Ineffizienz: Alle Watcher werden bei jeder Iteration vielmals angeschaut, teilweise sogar verglichen. Ich glaube nicht, daß man eine derart ineffiziente Event-Bibliothek erhielte, wenn man sich hinsetzte und einfach drauf los schriebe ohne sich Gedanken zu machen. Tatsächlich ist Glib auch nicht dafür gedacht, effizient zu sein oder mehr als einen File Descriptor zu unterstützen (den zum X-Server nämlich). POE ist sicherlich etwas langsamer, als es sein könnte, da für jede Verbindung drei "sessions" eingesetzt wurden, während POE eine Session pro "Verbindung" vorschlägt. Allerdings ist derlei Kritik am Benchmark irrelevant: Auch in einem echten Server würde man für jede Session eine Session benutzen, und selbst wenn POE dreimal schneller würde wäre es immer noch über 100 mal langsamer als die Pure-Perl Event-Implementation von AnyEvent. Im übrigen wurde in diesem Benchmark sogar die Pure-Perl Version von POE durch eine optimierte Variante ersetzt wurde, die das C-Modul benutzt. =head3 Benchmark 3: ein klitzekleiner Server Benchmarks für riesige Systeme sind sicher wichtig, aber wenn man "nur eben" mal AnyEvent benutzt, sollte es auch schnell sein, wenn man nur sehr wenige aktive Verbindungen hat. Im folgenden wurde daher nur acht Socketpaare benutzt, von denen jeweils drei aktiv sind: Name Create Request /uSec /uSec EV 20.00 6.54 Perl 25.75 12.62 Event 81.27 35.86 Glib 32.63 15.48 POE 261.87 276.28 mit POE::Loop::Event Wie man sieht, ändert sich nichts drastisch: Glib ist nun so schnell wie die anderen, aber EV und AnyEvents Pure-Perl Implementation sind trotzdem schneller. Während POE weiterhin stark hinterherhinkt, für drei Verbindungen aber absolut adäquat ist :-> =head2 Ausblick auf CPAN AnyEvent existiert nicht in Isolation: Es gibt inzwischen eine stattliche Anzahl von CPAN-Modulen die AnyEvent und nur AnyEvent als Abhängigkeit besitzen. Für AnyEvent ist es nicht so wichtig wie für andere "Frameworks" (wie POE), viele weitere CPAN-Module zu benutzen, da AnyEvent-Module ja mit anderen Frameworks kombiniert werden können, umgekehrt ist dies allerdings nicht der Fall (AnyEvent + POE Programm geht, POE + Tk-Programm geht nicht). Hier ist eine fast erschöpfende Auswahl von AnyEvent-Modulen auf CPAN: =over 4 =item AnyEvent::HTTP Ein ganz einfacher HTTP-Client. Da man mit LWP keine parallelen Abfragen durchführen im selben Prozess kann, ist es recht hilfreich :) =item AnyEvent::AIO Integriert IO::AIO transparent in AnyEvent. AnyEvent::AIO ist ein ideales Pendant zu AnyEvent: AnyEvent abstrahiert non-blocking I/O, IO::AIO abstrahiert asynchrones I/O. =item AnyEvent::DBI Langsames, aber asynchrones, DBI-Frontend. =item AnyEvent::CouchDB Eine Schnittstelle zur CouchDB. Ein must have! =item AnyEvent::BDB Asynchrone Schnittstelle zur guten alten BerkelyDB. =item AnyEvent::IRC Event basiertes IRC. =item AnyEvent::XMPP Alternative zum eigenen Parser für Jabber-Pseudo-XML. =item AnyEvent::FastPing Wenn man mal eine Million Ping-Pakete pro Sekunde verschicken will. Eignet sich ideal um Firewalls abzuschießen :) =item AnyEvent::Mojo Wenn ich's bloß wüsste... =item AnyEvent::HTTPD Ein einfacher Webserver zum Einbetten in die eigene Applikation. =item Coro::AnyEvent Die Kombination von Coro-Threads und AnyEvent ist ideal - komplexe Logik kann man bequem per Thread implementieren, Low-Level Event-Handling geht meist per AnyEvent angenehmer. =back