Zeile 20 installiert einen Watcher auf dem Datenbank-Socket, der den Event-Loop in Zeile 30 beendet, sobald das Resultat der SQL-Operation vorliegt. Interessanter sind die zwei Signal-Watcher, die Zeile 26 erzeugt. Die
»$cancel
«
-Funktion, die von ihnen aufgerufen wird, bricht die Datenbankoperation ab und beendet den Event-Loop.
Um wiederum eine Race-Condition zu vermeiden, dürfen die angefangenen Signale jedoch nicht zwischen dem Installieren der Handler und dem Ende der Execute-Operation, also zwischen den Zeilen 26 und 27 auftreten. Das stellen die beiden
»sigprocmask
«
-Aufrufe in den Zeilen 25 und 28 sicher.
Was passiert im Detail, wenn Apache das SIGTERM-Signal schickt? Trifft es vor Zeile 25 ein, wird das Programm einfach abgebrochen. Da keine SQL-Abfrage läuft, ist das unkritisch. Zwischen den Zeilen 25 und 28 wird die Zustellung auf Kernel-Ebene verzögert. Sollte in diesem Zeitraum das Signal eintreffen, kriegt der Prozess das erst nach Zeile 28 mit. Hier sind die Signal-Handler aber schon installiert und die SQL-Operation initiiert. Der von AnyEvent installierte Signal-Handler auf C-Ebene schreibt in den Eventfd oder die Self-Pipe und kehrt zurück. Jetzt ist wenigstens ein Dateideskriptor, nämlich der Eventfd, bereit gelesen zu werden. Das heißt, der vom Event-Loop benutzte System-Aufruf
»epoll
«
meldet das. Der Signal-Watcher wird aufgerufen. Der Rest ist dann offensichtlich.
Es ist schon erstaunlich, in welchen Fallstricken man sich mit einem einfachen CGI-Programm verheddern kann. Glücklicherweise lässt sich die Query-Funktion aus der Endversion problemlos in ein Modul auslagern und immer wieder neu verwenden. Der aufmerksame Programmierer sollte aber die Fallstricke kennen.
Leider löst die ganze Arbeit bisher nur die Hälfte des Problems. Wenn der Timeout nämlich nicht im Webserver, sondern vor dem Bildschirm eintritt, und der Benutzer die Seite immer wieder neu lädt, entsteht auch ganz schnell ein Engpass in der Datenbank.
Mit Mod-Perl statt Mod-CGI kann man recht einfach auch das Verschwinden des Browsers feststellen. Der Timeout auf Benutzerseite könnte also recht schnell erkannt und die laufende SQL-Operation daraufhin abgebrochen werden. Diesem Lösungsansatz wird sich ein Artikel in der kommenden ADMIN-Ausgabe widmen.
Was genau macht pg_cancel ?
Bei der Arbeit an dem Artikel tauchte die Frage auf, ob
»pg_cancel
«
den Abbruch nur initiiert und sofort zurückkehrt oder ob es die Bestätigung durch den Server abwartet, ob also kurz nach der Rückkehr I/O-Operationen auf dem Socket zu erwarten sind. Zur Beantwortung könnte man den Sourcecode lesen. Einfacher ist es jedoch, mit
»strace
«
zuzusehen. Dazu umschloss ich den Aufruf mit zwei Ausgaben auf Stderr.
warn '>>>'; $dbh->pg_cancel; warn '<<<';
Das relevante Stück aus dem
»strace
«
-Output sieht so aus:
write(2, ">>> at -e line 1.\n", 18>>> at -e line 1. ) = 18 socket(PF_INET, SOCK_STREAM, IPPROTO_IP) = 4 connect(4, {sa_family=AF_INET, sin_port=htons(5432), ... sendto(4, "\0\0\0\20\4\322\26.\0\0.\0016\252\36\264", 16, 0, NULL, 0) = 16 recvfrom(4, "", 1, 0, NULL, NULL) = 0 close(4) = 0 poll([{fd=3, events=POLLIN|POLLERR}], 1, -1) = 1 ([{fd=3, revents=POLLIN}]) recvfrom(3, "2\0\0\0\4T\0\0\0!\0\1pg_sleep\0"..., 16384, 0, NULL, NULL) = 143 write(2, "<<< at -e line 1.\n", 18<<< at -e line 1. ) = 18
Interessant ist die Tatsache, dass eine zusätzliche TCP-Verbindung aufgebaut wird. Darüber wird der Datenbankserver offensichtlich angewiesen, dem Backend-Prozess, der die asynchrone Anfrage ausführt, ein Signal zu schicken. Ein paralleler
»strace
«
-Aufruf auf dem Server bestätigte, dass ein SIGINT geschickt wird. Das deckt sich auch mit der Beschreibung zur Funktion
»pg_cancel_backend(int)
«
in
[2]
.
Der folgende Teil, der sich auf Dateideskriptor 3 abspielt, ist aber interessanter. Er zeigt, dass
»pg_cancel
«
das Ende der Operation abwartet. Nach der Rückkehr ist also kein I/O mehr zu erwarten.
AnyEvent – eine Kurzübersicht
AnyEvent bietet ein Gerüst zur Event-basierten Programmierung in Perl. Damit erinnert die Vorgehensweise ein wenig an alte Windows-Programme oder auch an Javascript-Programme für den Browser. Die zentrale Idee ist dabei, dass es genau einen Punkt im Programm, den Event-Loop, gibt, der auf externe Ereignisse wartet. Trifft ein Ereignis ein, wird es bearbeitet, ohne dass der Prozess blockiert. Danach kehrt das Programm zum Event-Loop zurück und wartet auf das nächste Ereignis.
Strikt ist die Idee sehr schwer umzusetzen. So dürfte man beispielsweise die
»pg_cancel
«
-Funktion nicht benutzen, weil sie an mehreren Stellen blockiert (etwa bei
»connect
«
,
»poll
«
).
Der zentrale Teil eines Event-basierten Programms ist der Event-Loop. In AnyEvent wird er mit einer sogenannten Condition-Variablen erzeugt. In
Listing 5
erzeugt Zeile 15 eine solche. Der Aufruf
»$done->wait
«
in Ziele 30 stellt dann den eigentlichen Event-Loop dar.
Nun ist der Loop eine unendliche Schleife. Es muss also einen Weg geben, ihn abzubrechen. In AnyEvent übernimmt das die Methode
»send
«
(beispielsweise in Zeile 18). Damit wird ein Flag gesetzt, das den Event-Loop veranlasst, nach dem Ende der Bearbeitung des aktuellen Ereignisses die Schleife zu verlassen. Zeile 31 wird also erst erreicht, wenn in Zeile 18 oder in Zeile 21
»$done->send
«
aufgerufen wurde.
Eine weitere wichtige Zutat zur Event-basierten Programmierung sind die Ereignisse selbst. Der Javascript-Programmierer denkt hier wahrscheinlich sofort an
»addEventListener
«
oder die verschiedenen
»onXYZ
«
-Attribute in HTML. AnyEvent benutzt sogenannte Watcher. Das Programm in Listing 5 verwendet zwei Arten: Watcher für Signale und Watcher für I/O. Der Aufruf
»AE::io ...
«
in Zeile 20 erzeugt einen Watcher, der die als letzten Parameter übergebene Funktion aufruft, sobald der Dateideskriptor
»$dbh->{pg_socket}
«
Daten bereithält. In Javascript wäre das am ehesten vergleichbar mit dem
»readystatechange
«
-Ereignis des XMLHTTPRequest-Objekts.
Wie steht's mit der Performance?
Handelt man sich mit den asynchronen Anfragen einen Engpass ein? Wird damit vielleicht der Normalbetrieb verlangsamt? Dazu habe ich ein kleines Benchmark Programm geschrieben, das die Query-Funktion aus Listing 5 mit folgender Funktion vergleicht:
sub query_sync { my $sql=pop; state $dbh||=DBI->connect('dbi:Pg:dbname=r2', 'ipp', undef, {RaiseError=>1}); my $stmt=$dbh->prepare_cached($sql); return $stmt->execute(@_), $stmt; }
Wird zum Vergleich das SQL-Statement
»select 1
«
benutzt, ist die synchrone Version in der Tat deutlich schneller. Sie schafft auf meinem Testrechner gut 2500 Operationen pro Sekunde, während die asynchrone Variante nur auf knapp 1000 kommt.
Betrachtet man aber reale Anfragen, die auch eine Weile dauern können, relativiert sich das Ganze. Wird
»select pg_sleep(0.05)
«
benutzt, also eine SQL-Anfrage, die auf dem Server 50 ms benötigt, so kommt die synchrone Funktion auf 18,6 Operationen pro Sekunde und die asynchrone auf 17,9. Der Unterschied ist klein, aber noch spürbar.
Benötigt die Operation auf dem Server 200 ms, schafft die synchrone Variante 4,83 Operationen pro Sekunde und die asynchrone 4,90. Der Unterschied ist vernachlässigbar.
Reale Anfragen, wie sie bei Webapplikationen üblich sind, liegen meist irgendwo zwischen diesen beiden Werten.
Infos