Mit Hardware-Beschleunigung und schnellem Netz hilft Desktop-Virtualisierung, Administrationsaufwand und Kosten sparen, ADMIN 04/2013 verrät, wie die ... (mehr)

Signale im Blick

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.

Fazit

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

  1. DBD::Pg: http://search.cpan.org/perldoc?DBD::Pg
  2. PostgreSQL-Funktionen zur Systemadministration: http://www.postgresql.org/docs/8.4/static/functions-admin.html
  3. Andreas Scherbaum, "PostgreSQL": Open Source Press 2009
  4. Linux::FD: http://search.cpan.org/perldoc?Linux::FD
  5. AnyEvent: http://search.cpan.org/perldoc?AnyEvent
  6. Libev-Dokumentation: http://search.cpan.org/perldoc?libev
  7. Self-Pipe-Trick: http://lwn.net/Articles/177897/
  8. pselect(2): http://linux.die.net/man/2/pselect
  9. Boost application performance using asynchronous I/O: http://www.ibm.com/developerworks/linux/library/l-async/

Der Autor

Dipl.-Inf. Torsten Förtsch http://foertsch.name/ bearbeitet Projekte für seine Kunden im In- und Ausland. Als Freiberufler programmiert und betreut er Webserver. Mit Perl beschäftigt er sich seit 1998. In seiner Freizeit arbeitet er an Mod-Perl und behebt Bugs, wo immer er welche findet.

Ähnliche Artikel

comments powered by Disqus
Mehr zum Thema

PostgreSQL Notifications mit Perl

Eine SQL-Datenbank wird oft als passive Datenablage betrachtet, die nur zuständig für die Integrität der Daten ist. PostgreSQL kann aber auch aktiv externe Ereignisse auslösen. Mit Perl lässt sich dies für eigene Zwecke ausnutzen.
Einmal pro Woche aktuelle News, kostenlose Artikel und nützliche ADMIN-Tipps.
Ich habe die Datenschutzerklärung gelesen und bin einverstanden.

Konfigurationsmanagement

Ich konfiguriere meine Server

  • von Hand
  • mit eigenen Skripts
  • mit Puppet
  • mit Ansible
  • mit Saltstack
  • mit Chef
  • mit CFengine
  • mit dem Nix-System
  • mit Containern
  • mit anderer Konfigurationsmanagement-Software

Ausgabe /2023