Asynchrone Anfragen? Das kann MySQL seit 2008 …

Asynchrone Anfragen in MySQL
Kommentare

Auf der International PHP Conference 2008 – vor beinahe fünf Jahren also – wurden nicht-blockierende, asynchrone Anfragen im PHP MySQL API mysqli eingeführt. Als Anwendungsbeispiel dafür diente Sharding.

Mit der stetig steigenden Popularität von JavaScript und der Vorstellung von Node.js in 2009, gewannen asynchrone Programmierschnittstellen an Aufmerksamkeit. Ereignisbasierte APIs rückten in das Bewusstsein des Webprogrammierers. Im einfachsten Fall wartet ein Skript auf das Eintreten von Ereignissen im Browser, im komplizierteren Fall wird die asynchrone Ein-/Ausgabeverarbeitung für Hintergrundanfragen verwendet. Das zugrundeliegende Modell ist anders als beim Sharding, denn diesmal werden nicht zwingend mehrere entfernte Server angefragt. In beiden Fällen wird vorhandene Rechenleistung optimal ausgeschöpft: Zum einen durch das Vermeiden von unerwünschten Wartezeiten, zum anderen durch das Abarbeiten anderer, lokaler Aufgaben, während im Hintergrund ein entfernter Server eine Anfrage bearbeitet.

Sharding

Beim Sharding erfolgt eine horizontale Partitionierung von Daten. Die Inhalte einzelner Tabellen werden anhand eines Kriteriums aufgespalten, um die entstehenden Partitionen auf mehrere Datenbankserver zu verteilen. Sofern die typischen Anfragen an die Datenbank nur die Daten einer Partition umfassen, können mehrere Server parallel arbeiten: der Durchsatz des Systems wird erhöht. Mit MySQL 5.7 Fabric stellte MySQL unlängst eine Lösung zur einfachen Erstellung von Clustern vor, die Sharding als Skalierungsoption bieten.

Während Anfragen innerhalb einer Partition besser skaliert werden können, werden Anfragen, die mehrere Partitionen umfassen, langsamer. Umfasst eine Anfrage mehrere Partitionen, dann müssen Klienten Ergebnisse von mehreren Servern zusammentragen und auswerten. Um nicht auf die Ergebnisse eines jeden einzelnen Server warten zu müssen, bevor ein anderer Server angefragt werden kann, wurden nicht-blockierende Anfragen eingeführt. Ein Klient schickt parallel Anfragen an mehrere Server und verarbeitet die Antworten, asynchron sobald diese eintreffen.

Das MySQL Client-Server-Protokoll erlaubt grundsätzlich die Erstellung eines nicht-blockierenden APIs. Eine solche ist nur relevant für Programmierumgebungen, in denen ein Einsatz von Threads nicht möglich, mit einem nennenswerten Aufwand verbunden oder nicht gewünscht ist. PHP ist eine solche Umgebung.

In der Kommunikation zwischen einem Klienten und MySQL treten an zwei Stellen nennenswerte Wartezeiten auf: beim Verbindungsaufbau und beim Warten auf Ergebnisse von Anfragen. Alle anderen Operationen werden entweder lokal auf dem Klienten ausgewertet oder sind mit einem einfachen Round-Trip erledigt. Die Kosten beim Verbindungsaufbau lassen sich durch den Einsatz von Verbindungspools verringern. Die Persistenten Verbindungen von mysqli verwenden standardmäßig mysqli_change_user(), um Artefakte einer früheren Benutzung zu beseitigen, womit einer der Hauptkritikpunkte behoben wurde. Als wichtigste Aufgabe bleibt, nicht-blockierende Anfragen zu erlauben.

Der MySQL-Server ist nicht in der Lage, auf einer Verbindung mehrere Anfragen parallel zu verarbeiten. Anfragen werden stets sequentiell abgearbeitet. Stellt man sich eine Sequenz aus CREATE TABLE, INSERT und SELECT vor, so würde bei einer Umsortierung auch wenig Gutes entstehen.

<?php
$num_parallel = 3;
$connections = array();

$begin = microtime(true);

for ($i = 1; $i connect_errno > 0) {
    die(sprintf("[%d] %sn", $mysqli->connect_errno, $mysqli->connect_error));
  }
  $query = sprintf("SELECT CONNECTION_ID() AS _conn_id, SLEEP(%d) AS _sleep FROM DUAL", $i);
  $mysqli->query($query, MYSQLI_ASYNC);
  $connections[$mysqli->thread_id] = $mysqli;
}

printf("... %2.4fs - connect and sending %d queries donen",
       microtime(true) - $begin,
       $num_parallel);

$processed = 0;
$outstanding = count($connections);
do {
  $links = $errors = $reject = array();
  foreach ($connections as $connection) {
    $links[] = $errors[] = $rejected[] = $connection;
  }
  if (0 == ($num_ready = mysqli_poll($links, $errors, $rejected, 0, 500000))) {
    printf("... %2.4fs - nothing to fetch, already processed: %dn",
           microtime(true) - $begin,
           $processed);
    continue;
  }
  printf("... %2.4fs - %d/%d result[s] to process, %d error[s], %d rejectedn",
         microtime(true) - $begin,
         count($links), $num_ready,
         count($errors),
         count($rejected));

  foreach ($links as $link) {
    if ($result = $link->reap_async_query()) {

      printf("... %2.4fs - query result arrived on connection %d, dumping resultn",
             microtime(true) - $begin,
             $link->thread_id);

      var_dump($result->fetch_all(MYSQLI_ASSOC));
      $result->free();

      if ($processed thread_id]->query(
          "SELECT CONNECTION_ID() AS _conn_id, SLEEP(1) AS _sleep FROM DUAL",
          MYSQLI_ASYNC);
        $outstanding++;
      }
    } else {
      die(sprintf("[%d] %sn", $link->errno, $link->error));
    }
    $processed++;
  }
} while ($processed < $outstanding);

Angesichts dieser Vorgaben erklärt sich das Codebeispiel fast von selbst. Um mehrere Anfragen parallel an MySQL zu senden, muss PHP mehrere Verbindungen öffnen (Zeilen 7-16). Auf jeder dieser Verbindungen kann eine Anfrage gesendet werden. Beim Senden kann entweder auf eine Antwort gewartet (blockierend) oder die Antwort später abgerufen (nicht-blockierend) werden. Wählt man die asynchrone Variante (Zeile 14, mysqli_query(string query, MYSQLI_ASYNC)), dann müssen die Ergebnisse der Anfrage abgeholt werden, bevor die betreffende Verbindung für eine neue Anfrage benutzt werden kann.

Nach dem Stellen der asynchronen Anfragen wird periodisch mittels mysqli_poll() geprüft, ob bereits Ergebnisse vorliegen. Die Funktion erwartet als Eingabewert eine Liste von Verbindungen, die geprüft werden sollen (Zeile 29, $links). Als Antwort wird eine Liste von Verbindungen geliefert, auf denen Probleme auftraten ($errors) oder die keine asynchronen Anfragen mehr enthalten auf die es zu Warten gilt ($rejected). Der vierte Parameter bestimmt, ob die Funktion blockiert oder sofort zum Aufrufer zurückkehrt, sofern keine Ergebnisse vorliegen. Ist der Wert 0 und wird kein fünftes Argument übergeben, wird nicht blockiert. Im Beispiel legt sich die Funktion für eine halbe Sekunde schlafen sofern beim Aufruf noch keine Ergebnisse vorliegen und prüft dann erneut, um einen entsprechenden Rückgabewert zu generieren. Der Rückgabewert zeigt an, auf wie vielen Verbindungen Ergebnisse auf Abholung erwarten.

mysqli_poll(array links, array errors, array rejected, int wait_seconds[, int wait_usec])

Sobald Ergebnisse vorliegen, werden sie mit mysqli_reap_async() „geerntet“ (Zeilen 41-63). Wer mag, schickt gleich eine neue Anfrage nach dem Abholen von Ergebnissen (Zeilen 51-58). Allerdings gilt: Prepared Statements werden ebenso wie Multi-Statements nicht vom API unterstützt.

Aufmacherbild: Question and information speech bubble icons, three-dimensional rendering von Shutterstock / Urheberrecht: Oakozhan

Unsere Redaktion empfiehlt:

Relevante Beiträge

Meinungen zu diesem Beitrag

X
- Gib Deinen Standort ein -
- or -