mercredi 29 février 2012

Technocrati

A fake post to validate the blog on Technocrati 98M73DYY5FUQ

samedi 11 février 2012

Filtrer les requêtes dans pgFouine

Si vous utilisez pgFouine, un analyseur de log PostgreSQL, il vous ai peut être déjà arrivé de ne vouloir analyser que certaines requêtes. J'ai récemment soumis un patch sur pgFoundry permettant de filtrer les requêtes analysées en se basant sur une regex (le patch peut également être téléchargé ici).

Il suffit par exemple d'ajouter un mot clé dans les requêtes que vous voulez analyser

/* my keyword */ select * from foo;

Ensuite vous n'avez plus qu'à executer pgFouine en utilisant l'option -onlypattern

./pgfouine.php -file /var/log/syslog -onlypattern "/^\/\* my keyword/" > output.html

Bien entendu vous pouvez aussi filtrer sur les requêtes utilisant une table en particulier

./pgfouine.php -file /var/log/syslog -onlypattern "a_huge_table" > output.html

jeudi 9 février 2012

PostregSQL partitioning : retour d'expérience

Après plus de 2 ans d'utilisation de la méthode de partitionnement dynamique PosgreSQL décrite sur ce blog, voici un petit retour d'expérience. Tout d'abord les performances ont bien été améliorées. Cependant quelques inconvénients nous ont incité à faire évoluer cette méthode.

1. Le problème de la réplication

Le fait de créer les partitions dynamiquement rend extrêmement difficile, pour ne pas dire impossible, la réplication des partitions via Slony. Des méthodes alternatives doivent donc être mises en place (comme des dump/re-import nocturne) mais avec un risque de perte de données, si la base master tombe, les données ajoutées depuis le dernier dump seront perdues.

2. Le coût du PostgreSQL Query Planner

Même en écrivant correctement les requêtes SQL de sorte qu'une seule partition ne soit effectivement utilisée, le nombre de partitions fait tout de même augmenter considérablement le temps d’exécution. Prenons par exemple 2 tables identiques partitionnées selon le même critère, la première aura 1000 partitions et la seconde 4000. Les tables ne contiennent aucune données, ceci afin d'obtenir un temps d’exécution assez représentatif du temps pris par le query planner. Sur une requête simple, les explain sur ces deux tables sont identiques :

test=> explain select * from foo where other_id=3;
                              QUERY PLAN                               
-----------------------------------------------------------------------
 Result  (cost=0.00..73.50 rows=22 width=8)
   ->  Append  (cost=0.00..73.50 rows=22 width=8)
         ->  Seq Scan on foo  (cost=0.00..36.75 rows=11 width=8)
               Filter: (other_id = 3)
         ->  Seq Scan on foo_3 foo  (cost=0.00..36.75 rows=11 width=8)
               Filter: (other_id = 3)
(6 rows)

test=> explain select * from bar where other_id=3;
                              QUERY PLAN                               
-----------------------------------------------------------------------
 Result  (cost=0.00..73.50 rows=22 width=8)
   ->  Append  (cost=0.00..73.50 rows=22 width=8)
         ->  Seq Scan on bar  (cost=0.00..36.75 rows=11 width=8)
               Filter: (other_id = 3)
         ->  Seq Scan on bar_3 bar  (cost=0.00..36.75 rows=11 width=8)
               Filter: (other_id = 3)
(6 rows)

Mais lorsque l'on execute ces 2 requêtes, l'on obtient des temps d’exécution complètement différents:

test=> select * from foo where other_id=3;
 id_foo | other_id 
--------+----------
(0 rows)

Time: 37,419 ms
test=> select * from bar where other_id=3;
 id_bar | other_id 
--------+----------
(0 rows)

Time: 136,268 ms

Et en y réfléchissant c'est parfaitement logique, avant de savoir qu'il faut utiliser la partition bar_3, parce qu'elle correspond à la contrainte other_id = 3, le PostgreSQL Query Planner doit tout de même vérifier toutes les tables filles pour trouver lesquels correspondent à cette contrainte.

Une solution

Ces deux problèmes ont été résolu en utilisant un nombre fixe de partitions (20 dans notre cas semble être un bon compromis). Les tables filles n'étant plus créer dynamiquement, on peut les répliquer comme des tables normales avec Slony. Le nombre de partitions n'étant que de 20, on conserve un coût d'analyse de la requête par le PostgreSQL Query Planner tout à fait correct.

Les partitions ont été créées de la façon suivante

CREATE TABLE children.part_0 (
   CHECK (id_question % 20 = 0)
) INHERITS (answers);

CREATE TABLE children.part_1 (
   CHECK (id_question % 20 = 1)
) INHERITS (answers);

...

CREATE TABLE children.part_19 (
   CHECK (id_question % 20 = 19)
) INHERITS (answers);

la seule contrainte de ce nouveau système est que pour tirer plainnement parti du partitionnement il faut légèrement modifier les requêtes du type

select * from answers where id_question = 12;

pour plutôt faire

select * from answers where id_question = 12 and id_question % 20 = 12 % 20;

Cette modification a permis un gains significatif des performances avec une amélioration du temps d'éxécution allant jusqu'à 97% sur certaines requêtes.

mercredi 7 décembre 2011

Le "Dogpile effect"

L'utilisation  du cache pour stocker des données, peu importe leur provenance et le système de cache, est une  technique éprouvée. Tellement évidente et simple à mettre en place que  l'on ne se pose pas forcément de questions sur la mise à jour du contenu stocké en cache.

La  méthode la plus répandue consiste à essayer de récupérer les données en  cache, si elles ont expirées ou ne sont pas en cache alors on relance  le calcul et on met à jour le cache. Dans le cas d'une application web  le client qui arrivera au moment où le cache aura expiré devra juste  attendre un peu (parfois longtemps) qu'il soit  rafraîchi. Dans un contexte de forte charge, et/ou avec des données longues  à calculer, l'on va se retrouver avec plusieurs connexions arrivants au  moment où le cache est expiré et pas encore rafraîchi, ce qui aura pour  effet immédiat de lancer plusieurs fois la mise à jour du cache, de  charger la base de données, de faire monter les cpus de serveurs, l'utilisation de la mémoire, etc.  C'est cela que l'on nome le dogpile effect (terme, à priori, emprunter à l'argo du foot US pour désigner un tackle impliquant au moins 3 joueurs).

Différente technique "anti-dogpiling" existent pour sinon résoudre complètement ce problème, au moins limiter les risques qu'il ne se présente:

1. Mettre en place des tâches planifiées.
Cette  méthode est très simple à mettre en place, une ou plusieurs tâches  planifiées iront rafraîchir vos données en cache selon la fréquence que  vous aurez définie, les clients n'ayant plus à mettre à jour les  données, ont ne risque plus d'avoir de multiples demande de  rafraîchissement du cache. L'inconvénient et que toutes les données en  cache seront rafraîchies, qu'elles soient utiles ou non et qu’il est  toujours possible qu’un ou plusieurs clients se connectent alors que les  données ne sont pas présentent en cache (Memcache peut par exemple, de lui même, supprimer certaines données).

2. Verrouiller la procédure de mise à jour du cache.
L'on  peut aussi choisir de laisser aux clients la responsabilité de mettre à  jour le cache, dans ce cas pour éviter le dogpile effect l'on va poser  des verrous avant de relancer le calcul des données en utilisant, en fonction de votre architecture, le système  de fichier ou la base de données par exemple. Lorsque le verrou est  posé, les autres instances du clients qui essaient d'accéder aux données vont  attendre qu'elles soient mis à jour sans relancer le calcul. Cette méthode à  l'avantage de ne mettre à jour le cache que pour les données utilisées,  l'inconvénient et que même si la mise à jour du cache n'est lancée  qu'une fois, tous les clients sont bloqués et doivent attendre qu'elle  soit terminée.

3. Anticiper l'expiration du cache.
Cette  dernière méthode se rapproche de la seconde, la différence est que en  plus des données, l'on stockera également en cache le temps nécessaire  au calcul de ces données. Lorsqu'une requête sera faite, si la durée de  vie du cache restante est inférieure au temps nécessaire au  rafraîchissement du cache alors on posera les verrous et l'on lancera la  mise à jour du cache en asynchrone. L'avantage de cette technique est  qu'aucun client n'aura plus à attendre que le cache soit mis à jour, la  demande sera anticipé. L'inconvénient et qu'il faut tout de même garder  une solution de secours avec une des autre méthode dans le cas, par exemple, où aucun  appel ne serait fait dans le délai activant la mise à jour du cache.

samedi 16 avril 2011

Lancer des traitements asynchrones en PHP

Il y a différentes raisons de vouloir lancer des traitements asynchrones en PHP, les insertions de logs par exemple, où l’on a pas besoin d’attendre un retour pour continuer. Ou encore pour récupérer les différents éléments de contenu d’une page web simultanément, et ainsi réduire le temps nécessaire à l’affichage de la page. On peut aussi vouloir mettre en place un service qui lancera d'autres scripts, indépendants les uns des autres. Voici quelques une des possibilités qui existent pour effectuer des traitements asynchrones en PHP.
  • exec permet de faire des appels systèmes. Cette méthode prend en paramètre une ligne de commande comme vous la taperiez dans un terminal. Ajouter un & à la fin de cette ligne de commande permet de la lancer en tâche de fond et vous permet de ne pas attendre le résultat.
  • curl_multi_init permet de lancer plusieurs traitements cURL en parallèle. La fonctionnalité est très intéressante, en particulier quand il s’agit de faire plusieurs appels à des web-services pour récupérer du contenu.
  • Pcntl est une extension PHP implémentant un système de contrôle des processus permettant la gestion de tâches en parallèle. Comme le précise les premières lignes de la documentation, Pcntl n'est pas fait pour l'utilisation en mode web. Le contrôle de processus pouvant se révéler assez complexe, son utilisation est à réserver à des utilisateurs avertis.
  • fsockopen permet d’initialiser une connexion par socket, il suffit ensuite de poster des données avec fwrite par exemple. A noter qu’il est possible d’ouvrir une connexion persistante avec pfsockopen.
  • pg_send_query permet d’envoyer une requête SQL à une base de données PostgreSQL sans attendre le résultat. Si vous utilisez une base de données PostgreSQL, cette solution est sûrement la plus élégante et la plus simple à mettre en place pour insérer des logs dans une base de données. Si toutefois vous avez besoin du résultat de la ou des requêtes exécutées, pg_get_result permet de récupérer les résultats des différentes requêtes.

vendredi 1 avril 2011

Fonctions natives vs Objets

La programmation orientée objet (POO) a de nombreux avantages sur la programmation procédurale, le code est plus simple à lire, à maintenir et à faire évoluer, l’on peut réutiliser ses objets dans différents projets, mettre en place des tests unitaires pour s’assurer de leur bon fonctionnement, et même générer la documentation à partir des commentaires. Bref à priori que des avantages. À priori car à force de vouloir développer un objet pour la moindre opération on fini par tomber dans l’excés. Prenons par exemple une opération simple de formatage de date où l’on doit transformer une date au format standard issue d’une base de données dans un format plus “user friendly”. J’utilise ZendFramework depuis maintenant un certains temps et j’ai vu ces dernières années cette manipulation faite très souvent en utilisant Zend_Date. ZendFramework est sans aucun doute un très bon outil, les objets sont riches et répondent à grand nombre de besoin ; un test similaire pourrait sûrement être effectué en utilisant des objets de Symphony ou de CakePHP.

Voici le test effectué, il donne le temps nécessaire et la mémoire utilisée pour réaliser la conversion de date en utilisant Zend_Date, DateTime un objet natif fourni par PHP et les fonctions natives de PHP strftime et strtotime

$d = '2011-03-28 07:28:12';

$loop=1000;
$i=0;

$start = microtime(true);
$memory_start = memory_get_usage();
while ($i++ < $loop) {
    $date = new Zend_Date($d, 'MM.dd.yyyy hh:mi:ss');
}
$memory_end = memory_get_usage();
$end = microtime(true);
echo 'Zend_Date - Time : ', $end - $start,
    ' / Memory : ', $memory_end - $memory_start, "\n";

unset ($date);

$i=0;
$start = microtime(true);
$memory_start = memory_get_usage();
while ($i++ < $loop) {
    $date = new DateTime($d);
    $date = $date->format('j M Y H:i:s');
}
$memory_end = memory_get_usage();
$end = microtime(true);
echo 'Native PHP Object - Time : ', $end - $start,
    ' / Memory : ', $memory_end - $memory_start, "\n";

unset ($date);

$i=0;
$start = microtime(true);
$memory_start = memory_get_usage();
while ($i++ < $loop) {
    $date = strftime('%d %b %Y %H:%M:%S', strtotime($d));
}
$memory_end = memory_get_usage();
$end = microtime(true);
echo 'Native PHP - Time : ', $end - $start,
    ' / Memory : ', $memory_end - $memory_start, "\n";


Et voici le résultat,

Zend_Date - Time : 6.49872088432 / Memory : 5401472
Native PHP Object - Time : 0.00977492332458 / Memory : 400
Native PHP - Time : 0.00840377807617 / Memory : 176


Sans surprises l’utilisation des fonctions natives est beaucoup moins gourmande en ressources. Attention je ne fait aucun reproche à Zend_Date, cet objet à été designé pour remplir des tâches autrement plus compliquées que la simple conversion de format. Le problème c’est l’utilisation que certains développeurs font des outils à leur disposition, à grand principe de “qui peut le plus, peut le moins” ils vont avoir tendance à sortir un objet implémentant un algorithme complexe développer pour détecter des mots clés dans un texte pour... afficher un “Hello word!”.

Quelque soit le langage, les objets disponibles sont conçus dans un but bien défini, vérifiez toujours que l’objet que vous voulez utiliser est fait pour ce que vous voulez en faire, et si possible, utilisez les fonctions natives du langage.

mardi 23 novembre 2010

GIT en mode collaboratif

Ou plus exactement git en mode collaboratif, sans push, et en forçant les responsables à relire le code...

Gros programme... Mais si il y a un scm qui permet cela facilement c'est bien GIT. Voici donc un exemple complet expliquant aux développeur voulant débuter avec GIT comment utiliser les clones et les branches. Ont retrouve dans cet exemple deux intervenants, le developpeur lead qui est responsable du code et de sa relecture et le developpeur qui va faire les modifications.

Les avantages de l'utilisation décrite si dessous sont les suivants :
  • Pas de PUSH, et donc pas de PULLs risqués, en effet les développeurs qui viennent de SVN ont souvent au début le reflex de remplacer le svn commit par un git commit et git push affin de publier leur modif. On se retrouve alors avec des git pull difficile à gérer, et on a alors recours a git cherry-pick qui est franchement pénible à utiliser.
  • Le développeur lead est obligé de vérifier le travail du dev (et ca c'est bien

Création du projet

Premièrement, créons un projet...

guil@laptop:~/dev$ mkdir -p project/trunk

guil@laptop:~/dev$ cd project/trunk

guil@laptop:~/dev/project/trunk$ git init
Initialized empty Git repository in /home/gluchet/dev/project/trunk/.git/

Chaque projet a besoin d'un fichier README, ont prend donc un puissant éditeur de texte, vim (troll inside),  pour en créer un...

guil@laptop:~/dev/project/trunk$ vim README.txt

Voyons ce que donne une des commandes de base de git "git status". Status affiche le... status du répertoire, les fichiers ajoutés, supprimés, à commité, etc
guil@laptop:~/dev/project/trunk$ git status
# On branch master
#
# Initial commit
#
# Untracked files:
#   (use "git add ..." to include in what will be committed)
#
# README.txt
nothing added to commit but untracked files present (use "git add" to track)

il nous dit d'ajouter le fichier, on obéit...

guil@laptop:~/dev/project/trunk$ git add README.txt

Maintenant il dit quoi ?

guil@laptop:~/dev/project/trunk$ git status
# On branch master
# Changes to be committed:
#   (use "git reset HEAD ..." to unstage)
#
# new file:   README.txt
#

"Changes to be committed", ok on commit...

guil@laptop:~/dev/project/trunk$ git commit -m 'initial commit' README.txt
[master (root-commit) 90bd4da] initial commit
 1 files changed, 1 insertions(+), 0 deletions(-)
 create mode 100644 README.txt

Créer les clones

Maintenant le travail va commencer, il nous faut cloner le dépôt. Pour le devlead, on clone le trunk...

guil@laptop:~/dev/project$ git clone trunk/.git devlead
Initialized empty Git repository in /home/gluchet/dev/project/devlead/.git/

Pour le dev, on clone le devlead...

guil@laptop:~/dev/project$ git clone devlead/.git dev1
Initialized empty Git repository in /home/gluchet/dev/project/dev1/.git/

Contrairement à svn où l'on fait de multiple checkout d'un dépôt, on se retrouve ici avec une arborescence plus proche d'un arbre généalogique.

Et on a donc les répertoires suivants...

guil@laptop:~/dev/project$ ls
dev1  devlead  trunk

Le dev travaille

Il faut maintenant commencer à coder... Quelles branches sont dispo...

guil@laptop:~/dev/project/dev1$ git branch
* master

On crée une branche pour la nouvelle feature...

guil@laptop:~/dev/project/dev1$ git branch issue1

guil@laptop:~/dev/project/dev1$ git checkout issue1
Switched to branch 'issue1'

guil@laptop:~/dev/project/dev1$ git branch
* issue1
  master

L'étoile devant le nom de la branche indique la branche active.

On modifie des trucs, toujours avec vim (troll again ?), puis on commit...

guil@laptop:~/dev/project/dev1$ git status
# On branch issue1
# Changed but not updated:
#   (use "git add ..." to update what will be committed)
#   (use "git checkout -- ..." to discard changes in working directory)
#
# modified:   README.txt
#
no changes added to commit (use "git add" and/or "git commit -a")

guil@laptop:~/dev/project/dev1$ git add README.txt

guil@laptop:~/dev/project/dev1$ git commit -m 'Fix the issue1' README.txt
[issue1 ad0e465] Fix the issue1
 1 files changed, 3 insertions(+), 0 deletions(-)

Arriver là le dev doit juste informer le dev lead qu'il a implémenter le truc ds la branche "isseu1" de son clone. Pour ca il peut envoyer un mail, un sms, un tweet, lui taper sur l'épaule, laisser un post-it sur sa tasse de café, ou utiliser je ne sais quelle autre méthode de communication.

Le dev lead récup les commits du dev

On repasse au point de vue dev lead, dans son clone, le fichier README.txt est toujours l'ancien...

guil@laptop:~/dev/project/devlead$ more README.txt 
This is my project README file

Le dev lead va créer une branche pour récupérer les modifs du dev sans pourrir tout son clone...

guil@laptop:~/dev/project/devlead$ git branch issue1bydev1

guil@laptop:~/dev/project/devlead$ git checkout issue1bydev1 
Switched to branch 'issue1bydev1'

guil@laptop:~/dev/project/devlead$ git branch
* issue1bydev1
  master

Reste plus qu'à récup les commits du dev... on prend les commit dans la branche issue1 du repertoire dev1 et on les met ds la branche courante...

guil@laptop:~/dev/project/devlead$ git pull ../dev1 issue1
remote: Counting objects: 5, done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 3 (delta 0), reused 0 (delta 0)
Unpacking objects: 100% (3/3), done.
From ../dev1
 * branch            issue1     -> FETCH_HEAD
Updating 90bd4da..ad0e465
Fast-forward
 README.txt |    3 +++
 1 files changed, 3 insertions(+), 0 deletions(-)

On vérifie le code

guil@laptop:~/dev/project/devlead$ more README.txt 
This is my project README file

you can install this amazing software using the Makefile (as soon as it will be
available)

Le dev lead merge les modifs

Le dev lead a peut être modifié le code en vérifiant le travail du dev, on vérifie...

guil@laptop:~/dev/project/devlead$ git status
# On branch issue1bydev1
nothing to commit (working directory clean)

Tout est ok, le dev lead peut merger sur son master, on repasse sur la branche master...

guil@laptop:~/dev/project/devlead$ git checkout master
Switched to branch 'master'

Vous vous souvenez du contenu du fichier README ? (regardez plus haut dans la branche issue1bydev1). Il est différent sur la branche master...

guil@laptop:~/dev/project/devlead$ more README.txt 
This is my project README file

On merge la branche issue1bydev1 ds la branche courante (master)...

guil@laptop:~/dev/project/devlead$ git merge issue1bydev1
Updating 90bd4da..ad0e465
Fast-forward
 README.txt |    3 +++
 1 files changed, 3 insertions(+), 0 deletions(-)

On vérifie le résultat du merge, le fichier README.txt a changé...

guil@laptop:~/dev/project/devlead$ more README.txt 
This is my project README file

you can install this amazing software using the Makefile (as soon as it will be
available)

On peut supprimer la branche utilisé pour récupérer les commits du dev...

guil@laptop:~/dev/project/devlead$ git branch -d issue1bydev1
Deleted branch issue1bydev1 (was ad0e465).
guil@laptop:~/dev/project/devlead$ git branch
* master

Le dev lead met à jour le trunk

Maintenant la modification peut être intégré au trunk, ce qui permettra à tous ceux qui ont cloner le trunk de la récupérer....

guil@laptop:~/dev/project/trunk$ git pull ../devlead master
remote: Counting objects: 5, done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 3 (delta 0), reused 0 (delta 0)
Unpacking objects: 100% (3/3), done.
From ../devlead
 * branch            master     -> FETCH_HEAD
Updating 90bd4da..ad0e465
Fast-forward
 README.txt |    3 +++
 1 files changed, 3 insertions(+), 0 deletions(-)

Le dev met régulièrement son clone à jour

Que se passe-t-il pour le dev ? Ds quel état sont ses branches...

guil@laptop:~/dev/project/dev1$ git branch
* issue1
  master

guil@laptop:~/dev/project/dev1$ git checkout master
Switched to branch 'master'

guil@laptop:~/dev/project/dev1$ git branch
  issue1
* master

Le fichier README.txt est toujours l'ancien... Normal le dev n'a pas mergé les modifications faites dans la branche issue1...

guil@laptop:~/dev/project/dev1$ more README.txt 
This is my project README file

Essayons un pull...

guil@laptop:~/dev/project/dev1$ git pull
From /home/gluchet/dev/project/devlead/
   90bd4da..ad0e465  master     -> origin/master
Updating 90bd4da..ad0e465
Fast-forward
 README.txt |    3 +++
 1 files changed, 3 insertions(+), 0 deletions(-)

Oh! Mes modifications ont été vérifiées et acceptées! Elles apparaissent...

guil@laptop:~/dev/project/dev1$ more README.txt 
This is my project README file

you can install this amazing software using the Makefile (as soon as it will be
available)