PHP et Cassandra sont sur un même porte-conteneurs…
…Cassandra tombe à l’eau. Que fait PHP ?
Sommaire
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 :
- Un driver PDO nommé YACassandraPDO
- Le driver PHP officiel de Datastax
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.
- 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
↩ - Un travail de réécriture du driver est en cours pour supporter cette nouvelle version ↩
- De nombreux exemples de code sont disponibles sur le projet. ↩
Can you give us the structure of the project please !
Malheureusement, je ne peux pas.
Pulling php (slecache/php-cassandra:fpm-datastax)…
ERROR: The image for the service you’re trying to recreate has been removed. If you continue, volume data could be lost. Consider backing up your data before continuing.
Ce sont des images privées. Elles n’ont pas été publiée sur Docker Hub.