…Cassandra tombe à l’eau. Que fait PHP ?

Introduction

L’objectif est de tester la haute disponibilité de Cassandra pour une application PHP. Pour cela, 2 modes de connexion à la base de données sont testés :

Pour simplifier les tests, l’application est déployée dans des conteneurs Docker via Docker Compose.

Le scénario utilisé est assez simple :

  • Déploiement :
    • d’un cluster Cassandra avec 3 nœuds
    • d’une application PHP FPM exposée via un serveur NGINX
  • arrêt/démarrage de différents nœuds et vérification de la disponibilité des données à tout instant

Remarques

NGINX et PHP FPM

La configuration utilisé par NGINX pour exécuter les scripts PHP FPM est assez classique. La configuration globale de NGINX se trouve dans le fichier nginx.conf :

user  nginx;
worker_processes  1;

error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;

events {
    worker_connections  1024;
}

http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    sendfile        on;
    #tcp_nopush     on;

    keepalive_timeout  65;

    gzip  on;

    include /etc/nginx/conf.d/*.conf;
}

De même, la configuration du serveur HTTP se trouve dans le fichier default.conf :

server {
    listen 80;
 
    server_name localhost;
 
    root /usr/share/nginx/html;
    index index.html;
 
     location / {
        root   /usr/share/nginx/html;
        index  index.html;
    }

    location ~* \.php$ {
        include fastcgi_params;
        fastcgi_index index.php;
        fastcgi_pass php:9000;
        fastcgi_param   SCRIPT_FILENAME    /var/www/html$fastcgi_script_name;
        fastcgi_param   SCRIPT_NAME        $fastcgi_script_name;
    }
}

Docker Compose et scaling

Après un docker-compose scale cassandra=2, il faut faire un docker-compose stop php && docker-compose rm -f php && docker-compose up -d pour que Docker Compose injecte les nouveaux liens dans le conteneur PHP.

L’arrêt ou le redémarrage d’un conteneur est fait directement avec docker (et non pas avec docker-compose).

Construction des drivers

Pour faire plus simple, le driver PDO est mis à disposition dans l’image slecache/php-cassandra:fpm-pdo. De même, le driver Datastax se trouve dans l’image slecache/php-cassandra:fpm-datastax.

L’application PHP

Un script PHP cassandra.php a été écrit pour jouer le rôle de l’application. Il effectue les tâches suivantes :

  • Ouverture d’un connexion persistante sur la base de données
  • Création du keyspace et de la table s’ils n’existent pas
  • Insertion dans la table d’une “transaction” pour tracer l’appel
  • Récupération de l’adresse IP du nœud pour vérifier où est exécuté la requête
  • Récupération de l’ensemble des “transactions” pour vérifier la persistance des données
  • Affichage d’une réponse JSON listant toutes les “transactions” et l’adresse IP du nœud Cassandra

Ce n’est pas ce qui se fait de plus élégant, ni de plus performant mais c’est plutôt simple et efficace pour le besoin.

Driver PDO YACassandra

Ce driver propose une implémentation de PDO via le CQL3 de Cassandra. Sur la page du site, il est indiqué que toutes les versions de Cassandra jusqu’à la version 2.0 sont supportées. Après quelques tests, il s’avère que ce driver arrive à se connecter à une version 3.0. Comme le comportement du cluster Cassandra n’était pas stable en version 3.0 1, les tests ont été fait avec Cassandra 2.1.
Pour l’installer, il est nécessaire de le compiler. A noter que le code source n’est pas compatible avec PHP7.

Mise en place

Le déploiement de l’application est configuré dans le fichier docker-compose.yml

cassandra0:
  image: cassandra:2.1
  environment:
   - CASSANDRA_START_RPC=true

cassandra:
  image: cassandra:2.1
  links:
   - cassandra0:seed
  environment:
   - CASSANDRA_START_RPC=true
   - CASSANDRA_SEEDS=seed

php:
  image: slecache/php-cassandra:fpm-pdo
  volumes:
    - ./php:/var/www/html:ro
    - ./pdo.ini:/usr/local/etc/php/conf.d/pdo.ini
  links:
   - cassandra

web:
  image: nginx
  ports:
    - "80:80"
  volumes:
    - ./html:/usr/share/nginx/html:ro
    - ./nginx/nginx.conf:/etc/nginx/nginx.conf
    - ./nginx/default.conf:/etc/nginx/conf.d/default.conf
  links:
    - php

Le code du script PHP cassandra.php ci-dessous est assez commun puisqu’il utilise l’API PDO pour exécuter les requêtes CQL3 qui sont elles-mêmes assez proches du SQL :

<?php
$connection = new \PDO("cassandra:host=cassandra_1;port=9160,host=cassandra_2;port=9160;cqlversion=3.0.0", '', '', array(
        \PDO::ATTR_PERSISTENT => true
    ));
$connection->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);

$sql="CREATE KEYSPACE IF NOT EXISTS demo WITH REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor' : 3 };";
$stmt = $connection->prepare($sql);
$stmt->execute();

$sql='CREATE TABLE IF NOT EXISTS demo.pings (id text PRIMARY KEY, create_time timestamp);';
$stmt = $connection->prepare($sql);
$stmt->execute();

date_default_timezone_set('Europe/Paris');
$sql='INSERT INTO demo.pings (id, create_time) values (:id, :date);';
$stmt = $connection->prepare($sql);
$stmt->bindValue(':id', uniqid());
$now = DateTime::createFromFormat('U.u', number_format(microtime(true), 6, '.', ''));
$stmt->bindValue(':date', substr($now->format('Y-m-d H:i:s.u'),0,23));
$stmt->execute();

$sql='SELECT broadcast_address FROM system.local';
$stmt = $connection->prepare($sql);
$stmt->execute();
$results = $stmt->fetchAll();
$host = inet_ntop($results[0]['broadcast_address']);

$sql='SELECT * FROM demo.pings';
$stmt = $connection->prepare($sql);
$stmt->execute();
$results = $stmt->fetchAll();

print "{\"host\";\"$host\",\"pings\": [\n";
foreach($results as $ping) {
	print "{\"id\":\"{$ping['id']}\", \"create_time\":\"{$ping['create_time']}\"}\n";
}
print ']}';

Après, l’exécution des commandes suivantes, l’application est fonctionnelle, a accès au cluster Cassandra et est accessible à l’adresse http://192.168.99.100/cassandra.php :

$ docker-compose up -d
$ docker-compose scale cassandra=2
$ docker-compose stop php && docker-compose rm -f php && docker-compose up -d

Ensuite, il suffit de faire des docker stop et/ou docker start sur n’importe quel conteneur cassandra0 ou cassandra.

Constatations

Quelque soit le nœud qui tombe, les données sont toujours accessibles. Le driver tente de se connecter à la liste de nœuds passés en paramètre du driver dans l’ordre de cette liste. Si le premier nœud est tombé. La connexion peut être assez longue. Dans un souci de performance, il est vraiment nécessaire d’activer les connexions persistantes. Dans ce cas, PHP reste connecté au même nœud tant qu’il est disponible. Sinon, il retentera une connexion sur chacun des nœuds.

Driver DataStax

Ce driver propose une API spécifique de communication avec Cassandra. Comme indiqué sur le site, seule la version 2.0 de Cassandra est pleinement supportée. Cassandra 2.1 est partiellement supporté. Mais le support de cette version est suffisant pour le test. Cassandra 3.0 n’est pas encore supporté 2.
Il est possible d’installer ce driver avec PECL. Pour ce test, le driver et ses dépendances ont été installés manuellement en suivant les instructions du projet.

Mise en place

Le déploiement est presque le même que pour le driver PDO. La seule différence est qu’il n’est pas nécessaire d’activer le service RPC puisque le driver ne passe pas par Thrift mais par le protocole natif de Cassandra comme il est possible de le voir dans le fichier docker-compose.yml suivant :

cassandra0:
  image: cassandra:2.1

cassandra:
  image: cassandra:2.1
  links:
   - cassandra0:seed
  environment:
   - CASSANDRA_SEEDS=seed

php:
  image: slecache/php-cassandra:fpm-datastax
  volumes:
    - ./php:/var/www/html:ro
    - ./pdo.ini:/usr/local/etc/php/conf.d/pdo.ini
  links:
   - cassandra

web:
  image: nginx
  ports:
    - "80:80"
  volumes:
    - ./html:/usr/share/nginx/html:ro
    - ./nginx/nginx.conf:/etc/nginx/nginx.conf
    - ./nginx/default.conf:/etc/nginx/conf.d/default.conf
  links:
    - php

En revanche, le code PHP est différent. En effet, Datastax propose une API spécifique. Les concepts sont assez proches de PDO mais l’API propose des fonctionnalités propres à Cassandra 3. Le script cassandra.php ressemble donc à ceci :

<?php
$session = Cassandra::cluster()->withContactPoints('cassandra_1', 'cassandra_2')->withPort(9042)->build()->connect();

$sql="CREATE KEYSPACE IF NOT EXISTS demo WITH REPLICATION = { 'class' : 'SimpleStrategy', 'replication_factor' : 3 };";
$session->execute(new Cassandra\SimpleStatement($sql));

$sql='CREATE TABLE IF NOT EXISTS demo.pings (id text PRIMARY KEY, create_time timestamp);';
$stmt = new Cassandra\SimpleStatement($sql);
$session->execute($stmt);

date_default_timezone_set('Europe/Paris');
$now = DateTime::createFromFormat('U.u', number_format(microtime(true), 6, '.', ''));
$sql='INSERT INTO demo.pings (id, create_time) values (? , ?)';
$stmt = $session->prepare($sql);
$session->execute($stmt, new Cassandra\ExecutionOptions(array(
    'arguments' => array(uniqid(), new Cassandra\Timestamp($now->getTimestamp())),
    'consistency' => Cassandra::CONSISTENCY_ANY
)));

$sql='SELECT broadcast_address FROM system.local';
$stmt = new Cassandra\SimpleStatement($sql);
$results = $session->execute($stmt);
$host = $results[0]['broadcast_address'];

$sql='SELECT * FROM demo.pings';
$stmt = new Cassandra\SimpleStatement($sql);
$results = $session->execute($stmt, new Cassandra\ExecutionOptions(array(
    'consistency' => Cassandra::CONSISTENCY_ONE
)));

print "{\"host\";\"$host\",\"pings\": [\n";
foreach($results as $ping) {
	print "{\"id\":\"{$ping['id']}\", \"create_time\":\"{$ping['create_time']}\"}\n";
}
print ']}';

La procédure de test de l’accès à la base Cassandra quel que soit le nœud tombé est la même que pour le driver PDO.

Constatations

Comme précédemment, les données sont toujours accessibles quel que soit le ou les nœuds tombés. Point important, ce driver permet de découvrir automatiquement tous les nœuds du cluster. Il n’est donc pas nécessaire de tous les déclarer lors de l’initialisation du driver. D’ailleurs, pendant le test, l’application peut basculer sur le nœud cassandra0 qui n’est pourtant pas déclaré en tant que lien avec le conteneur php.
Le basculement d’un nœud tombé à un autre nœud disponible semble plus rapide et ne suit pas l’ordre de déclaration des nœuds se trouvant dans le script PHP.

Conclusion

Tout d’abord, cette expérimentation montre la facilité à réaliser ce type de test d’architecture avec Docker.

Si l’on revient au sujet initial : la haute disponibilité de Cassandra pour une application PHP. Il se trouve que les 2 solutions permettent de répondre à ce besoin. Même si lors de la bascule, le driver Datastax semble plus réactif, il faut faire un choix sur d’autres éléments en comparant les 2 projets :

  • YACassandraPDO :
    • Implémentation PDO : code standard ++
    • Compatible avec Cassandra 3.0 ++
    • Non compatible PHP7 —
    • Le projet ne bouge plus depuis plus d’un an —
  • Driver Datastax :
    • API spécifique : code non standard –
    • Fonctionnalités avancées : requêtes asynchrones, paramétrage fin des coefficient de réplication, keep-alive pour passer les firewalls, etc… ++
    • Non compatible Cassandra 3.0 —
    • Compatible PHP7 ++
    • Ne nécessite pas l’activation du RPC sur Cassandra +
    • Projet actif ++

Si Cassandra 3.0 ou PHP7 est un prérequis obligatoire, alors le choix est rapide. Sinon, il faut se poser la question de PDO ou non. Si vous êtes prêt à produire un code spécifique, la solution Datastax offre de nombreuses fonctionnalités intéressantes et bien plus évoluées que YACassandra.

Un point sûrement important qui n’a pas été abordé ici, c’est la performance de ces 2 drivers. Lequel propose les meilleurs temps de réponse ? Lequel consomme le moins de ressource côté client et côté serveur ?

Vous avez maintenant toutes les clés pour à la fois réaliser simplement des tests architecture technique et pour choisir la solution la plus adaptée pour faire de la haute disponibilité Cassandra avec PHP.

  1. Les nœuds refusaient régulièrement de redémarrer avec le message suivant : A node required to move the data consistently is down (/172.17.0.6). If you wish to move the data from a potentially inconsistent replica, restart the node with -Dcassandra.consistent.rangemovement=false
  2. Un travail de réécriture du driver est en cours pour supporter cette nouvelle version
  3. De nombreux exemples de code sont disponibles sur le projet.