mardi 30 décembre 2008

MySQL Cluster 6.4

La version 6.4 du moteur de stockage NDB utilisé par MySQL Cluster pointe son nez et nous offre les nouvelles fonctionnalités suivantes :

- Possibilité d'ajouter des noeuds et des groupes de noeuds online, c'est à dire sans recréer totalement le cluster et donc d'avoir un arrêt de service non négligeable
- Support du multithreading au niveau des datanodes
- Base NDB$INFO permettant d'avoir accès aux informations du cluster
- Disponibilité sous Windows

Pour les fonctionnalités apportées par chaque version 6.x vous pouvez vous rendre sur la roadmap du développement de MySQL Cluster. Les versions 6.x étaient disponibles en GPL ou en version commercial dans l'édition Carrier Grade de MySQL Cluster qui maintenant est obsolète et devient tout simplement MySQL Cluster. Toutes les sources (GPL/mysql et Commerciales/mysql-com) sont disponibles ici.

Concernant les performances, le blog de Jonas Oreland, Leader de l'équipe d'ingénieur MySQL (Cluster) depuis Août 2003, annonce des résultats très probants, avec par exemple 950 000 lectures/seconde sur un seul datanode grâce au multithreading du processus ndbd, dont le nouveau binaire est ndbmt, et jusqu'à 600 000 écritures/seconde. Et selon Mikael Ronstrom, Architecte logiciel sénior chez MySQL AB (SUN), on peut donc imaginer atteindre des dizaines de millions de lectures/seconde en multipliant le nombre de noeuds de données.

Ce sont donc de très bonnes nouvelles pour les heureux utilisateurs de MySQL Cluster et d'après Jonas on peut s'attendre à avoir une beta-release pour l'année 2009. Il faudra donc encore être patient mais on devrait pouvoir en profiter bientôt.

Enfin, pour ceux qui utilisent le moteur 6.3 je vous conseille de passer au moins à la version 6.3.19, si vous en avez la possibilité, car elle apporte une modification dans la création des redos en ne flushant sur disque qu'après la création du fichier et non plus tous les 32k blocs, ce qui ralentissait énormément le temps d'initialisation d'un noeud. Ainsi le blog de Shinguz a pu diviser le temps d'initialisation d'un noeud par 8.
Blogged with the Flock Browser

mercredi 17 décembre 2008

Calculer une durée en MySQL

Il est très simple avec PostgreSQL de calculer une différence entre deux dates avec simplement une commande du genre :

SELECT TIMESTAMP '2008-12-17 09:17:31' - TIMESTAMP '2008-12-15 11:28:06';


Mais qu'en est il avec MySQL ?

Eh bien ce n'est pas aussi simple ! En effet, MySQL dispose d'un tas de fonctions mais il ne surcharge pas l'opérateur de soustraction si les arguments sont des dates, à moins que vous utilisiez la syntaxe date-INTERVAL X UNIT
Il faut donc jongler avec la fonction TIMESTAMPDIFF pour obtenir le même résultat qu'avec Postgresql et ne pas rencontrer des problèmes avec les heures d'hiver/d'été.

Vous utiliserez donc la requête suivante :

SELECT @s:= '2008-12-15 11:28:06',@e:='2008-12-17 09:17:31',CONCAT(IF((@days:=TIMESTAMPDIFF(DAY,@s,@e))>0,CONCAT(@days,IF(@days>1,' days ',' day ')),''),SEC_TO_TIME(TIMESTAMPDIFF(SECOND,@s,@e)-86400*@days)) duree;

Vous remarquerez que j'utilise des variables utilisateurs afin de ne pas alourdir la requête en répétant les dates de début et de fin.
Blogged with the Flock Browser

mercredi 10 décembre 2008

Ordres DDL sur un cluster Slony

Introduction

Il est important de connaître les différentes méthodes pour exécuter des instructions DDL en production dans un environnement utlisant Slony.

Première méthode


Nous considérerons dans cette partie que le fichier slon_tools.conf est correctement configurées.

Créer un script SQL pour chaque SET Slony qui doit être modifié.

alter-set1.sql :

ALTER TABLE table1 ADD COLUMN vcol1 VARCHAR(5000),
ADD COLUMN icol2 smallint,
ADD COLUMN ccol3 CHAR(10);
ALTER TABLE table2 ADD COLUMN vcol1 VARCHAR(5000),
ADD COLUMN icol2 smallint,
ADD COLUMN ccol3 CHAR(10);
ALTER TABLE table3 ADD COLUMN vcol1 VARCHAR(5000),
ADD COLUMN icol2 smallint,
ADD COLUMN ccol3 CHAR(10);

alter-set2.sql :
ALTER TABLE table4 ADD COLUMN vcol1 VARCHAR(5000),
ADD COLUMN icol2 smallint,
ADD COLUMN ccol3 CHAR(10);
ALTER TABLE table5 ADD COLUMN vcol1 VARCHAR(5000),
ADD COLUMN icol2 smallint,
ADD COLUMN ccol3 CHAR(10);

Il ne reste plus qu'à effectuer les modifications de structures sur chacun des sets en exécutant les commandes suivantes sur n'importe quel noeud du cluster Slony.

slonik_execute_script 1 /scripts/alter-set1.sql
slonik_execute_script 2 /scripts/alter-set2.sql

Le script slonik_execute_script va exécuter la commande Slony EXECUTE SCRIPT avec les arguments adéquates sur le noeud source de chaque SET, et cet ordre sera propagé sur les noeuds cibles. Le principal problème dans l'utilisation de cette commande est qu'elle vérrouille toutes les tables de la base durant l'exécution des scripts SQL ce qui peut être très problématique pour un service 24/7

Seconde méthode


Cette méthode est valable pour l'ajout de colonnes mais non la suppression.


Comme dans le cas précédent, vous disposez des scripts alter-set1.sql et alter-set2.sql nécessaires pour effectuer les modifications de structures. Exécuter sur chacun des noeuds cibles (subscribers) du cluster Slony les scripts SQL :

psql -U mon_user ma_base < /scripts/alter-set1.sql
psql -U mon_user ma_base < /scripts/alter-set2.sql

Créer 2 nouveaux scripts comme suit :

update_triggers-set1.sql

UPDATE pg_trigger SET tgargs=substring(tgargs for octet_length(tgargs)-1)||E'vvv\000'
WHERE tgname='_mon_cluster' AND tgrelid IN
(SELECT oid FROM pg_class WHERE relname IN ('table1','table2','table3'));

update_triggers-set2.sql

UPDATE pg_trigger SET tgargs=substring(tgargs for octet_length(tgargs)-1)||E'vvv\000'
WHERE tgname='_mon_cluster' AND tgrelid IN
(SELECT oid FROM pg_class WHERE relname IN ('table4','table5'));


Le nombre de caractères v dans la requête de mise à jour correspond au nombre de nouvelles colonnes. Si nous avions ajouté une seule colonne, nous aurions concaténé la valeur E'v\000' au lieu de E'vvv\000'

Dans le cas de la modification du type d'une colonne il n'y a pas besoin de mettre à jour le trigger Slony de la table concernée puisque le nombre de colonnes reste inchangé.


Puis exécuter les scripts suivants sur le noeud source et mettre à jour les triggers Slony :

psql -U mon_user ma_base < /scripts/alter-set1.sql
psql -U mon_user ma_base < /scripts/update_triggers-set1.sql

psql -U mon_user ma_base < /scripts/alter-set2.sql
psql -U mon_user ma_base < /scripts/update_triggers-set2.sql

Cette méthode permet de ne pas utiliser la commande EXECUTE SCRIPT et donc ne pas vérrouiller toutes les tables de la base.

Conclusion

Ce document a permis de comprendre qu'il y a une autre méthode que l'utilisation de la commande EXECUTE SCRIPT pour mettre à jour la structure des tables répliquées par Slony (dans le cas de l'ajout/modification de colonnes), ce qui permet de réduire considérablement l'impact de ces modifications sur une base de production 24/7.

mardi 9 décembre 2008

Utilisation de LAST_INSERT_ID()

La fonction MySQL LAST_INSERT_ID() permet de récupérer la valeur du dernier incrément généré lors de l'insertion de données dans une table contenant une colonne auto_increment.
Il peut être utile de récupérer cette valeur pour la réinjecter dans une seconde table. J'ai par exemple sur mon serveur les 2 tables suivantes :

mysql [localhost] {msandbox} (test) > select * from t1;
+----+------+
| id | name |
+----+------+
| 1 | c |
| 2 | c2 |
| 3 | c3 |
+----+------+
3 rows in set (0.00 sec)

mysql [localhost] {msandbox} (test) > select * from t2;
+------+------------+
| id | dt |
+------+------------+
| 2 | 2008-12-09 |
+------+------------+
1 row in set (0.00 sec)

Ayant eu l'occasion de vérifier le code de certains développeurs j'ai repéré du code utilisant cette fonction mais faisant appel à la table en question sans pour autant que cela soit nécessaire pour le résultat. Je me suis alors posé la question suivante : Y a t il un impact à faire appel à la table sans que cela soit nécessaire ?
Si on regarde les 2 plans d'exécution cela donne :

mysql [localhost] {msandbox} (test) > explain select last_insert_id();
+----+-------------+-------+------+---------------+------+---------+------+------+----------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+------+---------+------+------+----------------+
| 1 | SIMPLE | NULL | NULL | NULL | NULL | NULL | NULL | NULL | No tables used |
+----+-------------+-------+------+---------------+------+---------+------+------+----------------+
1 row in set (0.00 sec)

mysql [localhost] {msandbox} (test) > explain select last_insert_id() from t2;
+----+-------------+-------+------+---------------+------+---------+------+------+-------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+-------+------+---------------+------+---------+------+------+-------+
| 1 | SIMPLE | t2 | ALL | NULL | NULL | NULL | NULL | 1 | |
+----+-------------+-------+------+---------------+------+---------+------+------+-------+
1 row in set (0.00 sec)


L'appel à la table semble avoir besoin d'accéder à une ligne. Cela semble louche...

Vous l'aurez compris il faut bien sûr bencher pour savoir s'il y a une grosse différence. Pour cela MySQL met à disposition la fonction BENCHMARK() qui n'est pas très connue :

mysql [localhost] {msandbox} (test) > insert into t1(name) values('c4');
Query OK, 1 row affected (0.01 sec)

mysql [localhost] {msandbox} (test) > SELECT BENCHMARK(1000000,(SELECT last_insert_id() from t2));
+------------------------------------------------------+
| BENCHMARK(1000000,(SELECT last_insert_id() from t2)) |
+------------------------------------------------------+
| 0 |
+------------------------------------------------------+
1 row in set (5.09 sec)

mysql [localhost] {msandbox} (test) > SELECT BENCHMARK(1000000,(SELECT last_insert_id()));
+----------------------------------------------+
| BENCHMARK(1000000,(SELECT last_insert_id())) |
+----------------------------------------------+
| 0 |
+----------------------------------------------+
1 row in set (0.03 sec)


Voilà donc notre réponse. Accéder à la table, chose inutile pour notre résultat, prend plus de temps.

lundi 8 décembre 2008

Calcul de la médiane en SQL

Depuis Oracle 10g il est possible d'effectuer le calcul de la médiane très facilement en utilisant la nouvelle fonction MEDIAN disponible.
Je suppose que vous disposez d'une table data constituée de 2 colonnes semaine et duree. Pour calculer la médiane sous Oracle vous n'avez plus qu'à utiliser la requête suivante :

SELECT semaine,MEDIAN(duree) FROM data GROUP BY semaine;

Pour MySQL c'est beaucoup plus compliqué puisqu'on ne dispose pas de cette magnifique fonction. J'ai donc fait quelques recherches et je suis tombé sur les travaux de frédéric Brouard. J'ai pu trouver la procédure adéquate basée sur la solution de Chris Date.

En supposant que vous disposez toujours de la table data qui chez moi utilise le moteur CSV, la méthode est la suivante :

CREATE TABLE d AS SELECT * FROM data;
ALTER TABLE d ADD COLUMN id INT AUTO_INCREMENT PRIMARY KEY FIRST, ADD INDEX (semaine,duree) ;

SELECT semaine,round(AVG(duree),2) AS MEDIANE
FROM (
   SELECT semaine,MIN(duree) AS duree
   -- valeurs au dessus
   FROM
   (
       SELECT ST1.semaine,ST1.id, ST1.duree
       FROM    d AS ST1
       INNER JOIN d AS ST2
       ON ST1.duree <= ST2.duree AND ST1.semaine=ST2.semaine
       GROUP BY ST1.id, ST1.duree
       HAVING COUNT(*) <= (SELECT CEILING(COUNT(*) / 2.0)
       FROM d d2 where d2.semaine=ST1.semaine)
   ) SUR
   GROUP BY semaine
   UNION
   SELECT semaine,MAX(duree) AS duree
   -- valeurs en dessous
   FROM
   (
       SELECT ST1.semaine, ST1.duree
       FROM    d AS ST1
       INNER JOIN d AS ST2
       ON ST1.duree >= ST2.duree AND ST1.semaine=ST2.semaine
       GROUP BY ST1.id, ST1.duree
       HAVING COUNT(*) <= (SELECT CEILING(COUNT(*) / 2.0)
       FROM d d2 where d2.semaine=ST1.semaine)
   ) SOU
   GROUP BY semaine
   ) T
GROUP BY semaine;

DROP TABLE d;

Sous Oracle, la requête est très rapide et il n'est pas nécessaire de créer un index particulier. Par contre sous MySQL, si vous vous amusez à supprimer l'index vous le ressentirez très vite si vous disposez de beaucoup de données. Dans mon cas, ma table contient 4874 lignes. Sans l'index, la requête dure près de 5 minutes contre moins de 5 secondes une fois indexée.
Lire la suite...

vendredi 28 novembre 2008

Bienvenue à MySQL 5.1 GA

Si vous surfez régulièrement sur la blogosphère MySQL, vous avez pu voir que la release MySQL 5.1 est enfin sortie. Elle était prévue pour le 6 décembre mais finalement elle est sortie 10 jours plus tôt. Ainsi, la version 5.1.30  est la première GA (Generally Available) release. Remercions aussi l'équipe Debian MySQL Maintainers qui a déjà sorti le paquet debian pour l'architecture amd64 ce qui démontre une certaine réactivité.

Cependant, il ne faut pas perdre à l'esprit que la version 5.1.31 commence déjà à montrer son nez pour corriger certains bugs.

Enfin pour un petit récapitulatif des nouvelles fonctionnalités de cette version n'hésitez pas à lire l'article de Jay Pipes sur le sujet "MySQL 5.1 Article Recap" ou a lire la section "What's New in MySQL 5.1" de la documentation MySQL.
Blogged with the Flock Browser

vendredi 21 novembre 2008

Utilisation du moteur Blackhole de MySQL

Le moteur Blackhole, comme son nom peut le sous-entendre, ne stocke aucune information. Tous les ordres DML sont effectués mais les données ne sont pas enregistrées.

Voici quelques cas où il devient intéressant d'utiliser ce moteur :
* Estimer la taille de vos binlogs générer en simulant un nombre hypothétique de requêtes par secondes sur votre serveur avec vos tables utilisant le moteur Blackhole
* Utiliser une table Blackhole pour générer des ordres DML sur d'autres tables sans avoir besoin de stocker d'information
* Peut jouer le rôle de distributeur de binary logs pour soulager la charger i/o d'un maître

Nous allons étudier la deuxième proposition qui consiste à utiliser une sorte de table de routage pour nos ordres DML.

Création d'une table Blackhole :

CREATE TABLE `mes_logs` (
`program` enum('MySQL','Apache','Autre',) DEFAULT NULL,
`msg` varchar(1000) DEFAULT NULL
) ENGINE=BLACKHOLE;


Création d'un trigger :

DELIMITER ;;
CREATE TRIGGER `redirect_logs` BEFORE INSERT ON `mes_logs` FOR EACH ROW BEGIN

IF ucase(NEW.program)='MYSQL' THEN
INSERT INTO mysql_logs VALUES(NEW.msg);

ELSEIF ucase(NEW.program)='APACHE' THEN
INSERT INTO apache_logs VALUES(NEW.msg);

ELSE
INSERT INTO autre_logs VALUES(NEW.msg);

END IF;
END */;;
DELIMITER ;


A partir de là, toute insertion dans la table mes_logs déclenchera le trigger redirect_logs qui mettra à jour la table correspondant au type de log tandis que les données ne seront pas stockées dans la table mes_logs. Ceci permet d'effectuer le routage des données de logs au niveau MySQL et non applicatif, dans le but par exemple d'archiver les tables, de les partitionner (et de dépasser la limite de 1024 sous partitions pour une table), de les vider sans influer sur les autres types de logs et tout en étant transparent pour les utilisateurs. Il sera aussi possible de modifier le trigger pour modifier ce routage.
Blogged with the Flock Browser


lundi 17 novembre 2008

L'importance des headers de Mysqldump

Ne vous est il jamais arrivé d'effectuer un dump MySQL et de n'en récupérer seulement qu'une partie pour la réinjecter sur un autre serveur ?

Eh bien, si c'est le cas, prenez soin de toujours récupérer les entêtes du dump. A titre d'exemple, depuis la version 5.0.15 MySQL s'assure d'affecter la time zone UTC à la connection de mysqldump.

Il le fait en rajoutant dans l'entête du dump :

SET TIME_ZONE='+00:00'


Si vous retirez cette entête et que vous injectez des dates sur un autre serveur qui a par exemple la même timezone (UTC +1) et bien vous obtiendrez 1 heure de décalage dans les données de type date/datetime injectées sur votre nouveau serveur.

A mûrement réfléchir...


vendredi 31 octobre 2008

Mysql Sandbox 2.0 - les slides

MySQL Sandbox est une boîte à outils permettant de configurer rapidement un serveur MySQL ou une replication MySQL quelque soit la version de MySQL que vous voulez tester. Pour cela, il suffit simplement de disposer des binaires MySQL de la version que vous voulez installer.

Un exemple rapide

Je suppose bien sûr que Mysql Sandbox 2.0 est installé dans /usr/local/mysql/mysql_sandbox_2.0.*

cd /usr/local/mysql
SITE="http://dev.mysql.com"
MPATH="get/Downloads/MySQL-5.1"
BINARY="mysql-5.1.29-rc-linux-i686-glibc23.tar.gz"
MIRROR="from/http://mir2.ovh.net/ftp.mysql.com/"
wget $SITE/$MPATH/$BINARY/$MIRROR
mkdir sandbox_dir
cd mysql_sandbox_2.0.*
export USER=mysql
./make_replication_sandbox --how_many_nodes=1 --upper_directory=/usr/local/mysql/sandbox_dir /usr/local/mysql/$BINARY

Et voilà, vous disposer d'une réplication MySQL entre un master et slave tous deux en version 5.1.26

Pour en savoir plus je vous conseil de lire les slides de Giuseppe Maxia qui peuvent vous servir de Bible.

mardi 14 octobre 2008

Chargement de données avec Slony

Introduction

Slony est un outil de réplication basé sur des triggers. Dans le cas de l'import ou de la mise à jour massive des données de la base, le retard de réplication peut devenir un réel problème. En effet, un UPDATE sur 100 mille lignes va générer 100 mille instructions à exécuter sur les esclaves. Il peut donc devenir judicieux de ne pas utiliser la réplication Slony dans le cas cité.

Première méthode

Cette méthode consiste à supprimer totalement la configuration slony des bases de données en supprimant purement et simplement les schemas créés par slony, c'est à dire les schemas de la forme _nomdecluster (au sens slony bien sûr, http://slony.info/documentation/clustername.html), et à arrêter les démons slon de chaque machine faisant partie du cluster.
Ensuite les données sont importées sur le provider source, et le cluster est réinitialisé en réutilisant les scripts qui avaient servi à initialiser la configuration. L'initialisation du cluster va engendrer la recopie par slony des données du provider sur l'ensemble des receivers en utilisant la commande postgresql COPY qui est très performante.

Seconde méthode

Cette méthode consiste à désactiver la réplication sur l'ensemble des tables concernées sur chacune des bases faisant partie du cluster Slony. Ensuite les données sont importées et la réplication est réactivée.
Cette méthode s'appuie sur les fonctions Slony alterTableRestore et alterTableForReplication qui respectivement désactive et active les triggers sur la table dont l'identifiant est fourni en paramètre.


Voici par exemple le déroulement de cette méthode sur ma plateforme (mes tables se nomment 's_xxx') :

Génération des scripts pour désactiver la réplication et vider les tables

schema=_nomdecluster
rm -f /var/tmp/disabletriggers-$schema.sql /var/tmp/truncate-$schema.sql
psql ma_base -U mon_user -c \
"SELECT 'SELECT $schema.altertablerestore('||relname||');' FROM pg_class where relname like E's\\\\_%'" >> /var/tmp/disabletriggers-$schema.sql
psql ma_base -U mon_user -c \
"SELECT 'TRUNCATE TABLE '||relname||';' FROM pg_class where relname like E's\\\\_%'" >> /var/tmp/truncate-$schema.sql

Génération des scripts pour réactiver la réplication

schema=_nomdecluster
rm -f /var/tmp/enabletriggers-$schema.sql
psql ma_base -U mon_user -c "SELECT 'SELECT $schema.alterTableForReplication('||relname||');' FROM pg_class where relname like E's\\\\_%'" >> /var/tmp/enabletriggers-$schema.sql

Désactivation de la réplication

schema=_nomdecluster
psql ma_base -U mon_user < /var/tmp/disabletriggers-$schema.sql

Suppression des données existentes

psql ma_base -U mon_user < /var/tmp/truncate-$schema.sql

Chargement des données à partir d'un dump

# On se rend dans le répertoire où se trouvent les backups
cd /dir
schema=_clustername
dumpfile=$schema-lastbackup.sql.gz

# On génère la liste des objets à restaurer
zcat $dumpfile |pg_restore -l|egrep -v 'slony' > /tmp/objects.list

# On restaure les données de la base ma_base
zcat $dumpfile |pg_restore -d ma_base --no-owner --data-only -L /tmp/objects.list

Rétablissement de la réplication

psql ma_base -U mon_user < /var/tmp/enabletriggers-$schema.sql

Il est clair que les écritures autres que le chargement des données doivent être interrompues au niveau applicatif pour ne pas désynchroniser les bases de données entre elles.

Conclusion

Il y a donc la possibilité de charger des données sans utiliser la réplication Slony courante, ce qui permet d'accélérer le processus. Cependant, cela demande dans ces cas là de désactiver le service (s'il y a besoin de supprimer les données au préalable) ou au moins de désactiver les écritures.
La seconde méthode peut aussi être utilisée dans le cas où vous n'auriez plus sous la main les scripts d'initialisation de la configuration Slon, ce qui ne devrait pas être le cas pour des plateformes de production.
Lire la suite...

Mettre à jour Slony

Introduction

La mise à jour de Slony peut être nécessaire pour améliorer les performances de l'outil ou corriger un BUG comme dans la version 1.2.15 par exemple qui corrige un problème de fuite mémoire (http://bugs.slony.info/bugzilla/show_bug.cgi?id=52). Cette mise à jour nécessite l'arrêt total de la réplication sur l'ensemble des noeuds d'un même cluster (http://www.slony.info/documentation/concepts.html), mais permet de continuer à mettre à jour les données sur le noeud source.

Algorithmie

Voici les différentes étapes qui composent ce mode opératoire :

* Mettre à jour la configuration apt pour pouvoir récupérer le nouveau paquet Slony
* Arrêter tous les processus slon sur tous les serveurs
* Installer le nouveau paquet Slony
* Mettre à jour Slony
* Redémarrer tous les processus slon sur tous les serveurs

Procédure

Le fichier /etc/slony/slon_tools.conf, il est possible d'utiliser un autre nom, doit être correctement renseigné pour que cette procédure fonctionne correctement. Par exemple, vous pouvez avoir la configuration suivante :

postgres@McQueen:~$ cat /etc/slony1/slon_tools.conf
&add_node(host => 'localhost', dbname => 'pgbench', port =>5432,
user=>'postgres', password=>'', node=>1 );
&add_node(host => 'localhost', dbname => 'pgbenchslave', port =>5432,
user=>'postgres', password=>'', node=>2 , parent=>1);
@KEYEDTABLES=(
"public.accounts",
"public.branches",
"public.tellers",
);
@SERIALTABLES=(
"public.history",
);
$CLUSTER_NAME = 'mycluster';
$MASTERNODE = 1;

Pour vérifier que votre fichier est correct vous pouvez utiliser la commande suivante :

postgres@McQueen:~$ slonik_print_preamble
cluster name = mycluster;
node 1 admin conninfo='host=localhost dbname=pgbench user=postgres port=5432';
node 2 admin conninfo='host=localhost dbname=pgbenchslave user=postgres port=5432';

Si votre fichier de configuration est différent, utilisez l'option --config sur la ligne de commande, mais dans tous les cas vous devez obtenir la configuration de votre cluster sous la forme présentée précédemment.

Une fois ce prérequis validé, il est possible de commencer la procédure en suivant l'algorithmie énoncée plus haut :

* Mettre à jour la configuration apt pour pouvoir récupérer le nouveau paquet Slony

* Arrêter tous les processus slon sur tous les serveurs

dsh -r ssh -o -lroot -m node1,node2,node3,node4 /etc/init.d/slony1 stop

* Installer le nouveau paquet Slony

apt-get install slony1-bin=1.2.15-1~bpo1-2 postgresql-8.2-slony1=1.2.15-1~bpo1-2

* Mettre à jour le schema Slony de tous les noeuds (à ne faire qu'une fois et sur un seul noeud)

slonik_update_nodes |slonik

* Redémarrer tous les processus slon sur tous les serveurs

dsh -r ssh -o -lroot -m node1,node2,node3,node4 /etc/init.d/slony1 start


Conclusion

Il est donc possible de mettre à jour rapidement la version de Slony sans impacter les écritures. Cependant, la réplication étant coupée, les lectures seront désynchronisées sauf si elles sont faites directement sur le noeud source durant la procédure de mise à jour.
La mise à jour du schema Slony sera par contre à faire sur tous les clusters. Dans le cas où les objets d'une base sont répliquées sur 2 clusters différents, il faudra donc effectuer 2 mises à jour. Ceci est à prendre en considération si les serveurs des clusters Slony sont mutualisés.

lundi 18 août 2008

Sauvegardes MySQL avec LVM2

Les avantages du LVM2

La nouvelle mouture du LVM (Logical Volume Manager) qui a vu le jour le 20 février 2002 avec la première version stable apporte les améliorations suivantes :

  • compatible avec les noyaux 2.6.x
  • compatible avec le LVM1
  • mise à jour transactionnelle des metadata (permet une récupération plus rapide dans le cas d'un crash serveur)
  • supporte le clustering de fichiers (gfs, ocfs)
  • supporte l'extension des volumes strippés
  • supporte le mirroring des volumes
  • supporte beaucoup plus de volumes physiques et logiques (256 -> 232)
  • snapshots en lecture/écriture

Sauvegarde MySQL

Les 2 types de sauvegardes possibles vont être étudiées dans le cas où LVM2 est disponible, permettant une interruption de service des plus courtes surtout dans le cas de bases très sollicitées et/ou ayant une volumétrie importante.

Sauvegarde Physique

Pour effectuer une sauvegarde physique il faut que les données sur disque soient dans un état consistent, c'est à dire que les fichiers ne soient pas modifiés durant la sauvegarde. Pour cela, la méthode habituelle consiste à arrête le serveur de base de données et à sauvegarder les fichiers. Cependant, cela nécessite un arrêt de service, et plus les fichiers sont nombreux et de taille importante, plus la sauvegarde durera longtemps. Ceci n'est souvent pas applicable dans les services ayant une activité de 24/24 7j/7. L'utilisation du LVM2 satisfait donc pleinement à cette situation.

Cette étape se déroule en 8 temps :

  • vérouiller en lecture toutes les tables avec la commande FLUSH TABLES WITH READ LOCK
  • créer un snapshot
  • relever la position des binlogs au cas où il faudrait construire un slave à partir de cette sauvegarde
  • dévérouiller des tables
  • monter le volume logique correspondant au snapshot
  • copier les fichiers se trouvant dans le snapshot
  • démonter le volume logique
  • supprimer le snapshot

Passons à la pratique :

Voilà le volume logique que j'utilises pour stocker mes données MySQL :

 root@McQueen:~# lvscan |grep mysql
ACTIVE '/dev/vg01/lv_mysqldb' [1.00 GB] inherit

root@McQueen:~# mount|grep mysql
/dev/mapper/vg01-lv_mysqldb on /var/lib/mysql type ext3
(rw,noatime)

Commençer la procédure de sauvegarde en se connectant au serveur :

 mysql> flush tables with read lock;
Query OK, 0 rows affected (0.00 sec)

mysql> show master status\G
*************************** 1. row ***************************
File: db10-bin.000005
Position: 1081
Binlog_Do_DB:
Binlog_Ignore_DB:
1 row in set (0.00 sec)

Garder la connexion précédente ouverte, car sinon le verrou global posé est libéré, et créer le snapshot qui sera utilisé pour la sauvegarde :

root@McQueen:~# lvcreate --snapshot --name mysql_datadir_snapshot
-L 200M /dev/vg01/lv_mysqldb
Logical volume "mysql_datadir_snapshot" created

Vous avez remarqué que je n'utilise pas la même taille pour mon snapshot (200Mo) que pour la base (1Go). En effet, la taille allouée au snapshot ne sera utilisée que pour les nouvelles écritures et pour les héberger les blocs modifiés sur le volume logique source (pour plus d'information voir http://www.tldp.org/HOWTO/LVM-HOWTO/snapshotintro.html)

Vous pouvez ensuite libérer le verrou acquis en vous déconnectant du serveur MySQL ou en utilisant la commande UNLOCK TABLES

Monter le volume logique correspondant au snapshot créé pour effectuer la sauvegarde physique des fichiers

 mount /dev/vg01/mysql_datadir_snapshot /mnt

Sauvegarder les fichiers MySQL

 tar cvzf mysqlbackup.tgz /mnt /etc/mysql

Démonter le snapshot et le supprimer

 umount /mnt && lvremove -f /dev/vg01/mysql_datadir_snapshot

Sauvegarde Logique

Pour effectuer une sauvegarde logique il faut que le serveur de bases de données soit démarré et acquérir un verrou global à chaque base pour sauvegarder les tables sans qu'elles soient désynchronisées. Selon l'activité de la base et sa volumétrie, l'impact sur les traitements courants et la durée de la sauvegarde peuvent être importants.

Cette étape se déroule en 9 temps :

  • vérouiller en lecture de toutes les tables avec la commande FLUSH TABLES WITH READ LOCK
  • créer du snapshot
  • dévérouiller les tables / se déconnecter
  • monter le volume logique correspondant au snapshot
  • démarrer un second serveur avec les fichiers se trouvant dans le snapshot
  • effectuer la sauvegarde logique
  • arrêter le second serveur de base de données
  • démonter le volume logique
  • supprimer le snapshot

Passons à la pratique :

Voilà le volume logique que j'utilises pour stocker mes données MySQL :

 root@McQueen:~# lvscan |grep mysql
ACTIVE '/dev/vg01/lv_mysqldb' [1.00 GB] inherit

root@McQueen:~# mount|grep mysql
/dev/mapper/vg01-lv_mysqldb on /var/lib/mysql type ext3
(rw,noatime)


Commençer la procédure de sauvegarde en se connectant au serveur :

 mysql> flush tables with read lock;
Query OK, 0 rows affected (0.00 sec)

Garder la connexion précédente ouverte, car sinon le verrou global posé est libéré, et créer le snapshot qui sera utilisé pour la sauvegarde :

root@McQueen:~# lvcreate --snapshot --name mysql_datadir_snapshot
-L 200M /dev/vg01/lv_mysqldb
Logical volume "mysql_datadir_snapshot" created

Vous pouvez ensuite libérer le verrou acquis en vous déconnectant du serveur MySQL ou en utilisant la commande UNLOCK TABLES

Monter le volume logique correspondant au snapshot créé

 mount /dev/vg01/mysql_datadir_snapshot /mnt

Démarrer un second serveur de base de données à partir des fichiers du snapshot, en prenant soin de désactiver le réseau puisque l'on se connectera directement en utilisant la nouvelle socket

 mysqld --basedir=/usr --datadir=/mnt --user=mysql --skip-networking
--socket=/mnt/mysqld.sock

Exporter les données

 mysqldump -S /mnt/mysqld.sock -B base1 base2

Il est aussi possible d'utiliser l'option --master-data pour exporter la position du serveur au niveau des fichiers de log binaires pour mettre en place une réplication

Arrêter le second serveur de base de données

 mysqladmin --defaults-file=/etc/mysql/debian.cnf
-S /mnt/mysqld.sock shutdown

Démonter le snapshot et le supprimer

 umount /mnt && lvremove -f /dev/vg01/mysql_datadir_snapshot

Conclusion

Cet article permet de comprendre l'intérêt d'utiliser le LVM2 pour effectuer ses sauvegardes MySQL et éviter d'avoir à interrompre le service utilisant le serveur de base de données. Il faudra cependant considérer l'impact sur les performances que peut induire l'existence d'un snapshot. En effet, les données modifiées sur le volume logique source sont déplacées vers le snapshot ce qui a un coup supplémentaire. Si votre serveur n'est pas déjà submergé d'activité i/o vous n'avez pas à vous inquiéter, dans le cas contraire rabattez vous sur un esclave ou tout autre machine moins sollicitée ou disposant d'une bande passante i/o plus importante.

lundi 28 juillet 2008

Query Cache MySQL Vs Oracle

Le Query Cache fait partie intégrante de MySQL depuis la version 3.23 (si ce n'est plus tôt). Il aura fallu attendre jusqu'à la version 11G pour voir arriver cette fonctionnalité chez Oracle. Nous allons tester cette fonctionnalité chez MySQL et Oracle et voir si le fait que l'on utilise le partitionnement apporte quelque chose.

Pour MYSQL (5.1 requis) :

CREATE TABLE table_mysql_partitionned(ident int unsigned, quand date, valeur int unsigned)
ENGINE=INNODB
PARTITION BY RANGE( YEAR(quand) )
(
PARTITION p2006 VALUES LESS THAN (2007),
PARTITION p2007 VALUES LESS THAN (2008),
PARTITION p2008 VALUES LESS THAN (2009),
PARTITION p2009 VALUES LESS THAN (2010)
);

INSERT INTO table_mysql_partitionned VALUES(1,'2008-06-04',20),(2,'2007-05-04',35),(3,'2008-07-04',3),(4,'2009-10-03',11),(1,'2009-12-25',46),
(2,'2008-10-12',43),(10,'2006-05-03',32),(5,'2007-06-12',56);


mysql> select * from table_mysql_partitionned where quand between '2008-01-01' and '2008-10-01';
+-------+------------+--------+
| ident | quand | valeur |
+-------+------------+--------+
| 1 | 2008-06-04 | 20 |
| 3 | 2008-07-04 | 3 |
+-------+------------+--------+
mysql> show global status like 'Qcache%';
+-------------------------+----------+
| Variable_name | Value |
+-------------------------+----------+
| Qcache_free_blocks | 1 |
| Qcache_free_memory | 20958592 |
| Qcache_hits | 4 |
| Qcache_inserts | 4 |
| Qcache_lowmem_prunes | 0 |
+-------------------------+----------+
8 rows in set (0,00 sec)

mysql> select * from table_mysql_partitionned where quand between '2008-01-01' and '2008-10-01';
+-------+------------+--------+
| ident | quand | valeur |
+-------+------------+--------+
| 1 | 2008-06-04 | 20 |
| 3 | 2008-07-04 | 3 |
+-------+------------+--------+
2 rows in set (0,00 sec)

mysql> show global status like 'Qcache%';
+-------------------------+----------+
| Variable_name | Value |
+-------------------------+----------+
| Qcache_free_blocks | 1 |
| Qcache_free_memory | 20958592 |
| Qcache_hits | 5 |
| Qcache_inserts | 4 |
| Qcache_lowmem_prunes | 0 |
+-------------------------+----------+
8 rows in set (0,01 sec)

mysql> INSERT INTO table_mysql_partitionned VALUES(1,'2009-06-04',34);
Query OK, 1 row affected (0,04 sec)

mysql> select * from table_mysql_partitionned where quand between '2008-01-01' and '2008-10-01';
+-------+------------+--------+
| ident | quand | valeur |
+-------+------------+--------+
| 1 | 2008-06-04 | 20 |
| 3 | 2008-07-04 | 3 |
+-------+------------+--------+
2 rows in set (0,00 sec)

mysql> show global status like 'Qcache%';
+-------------------------+----------+
| Variable_name | Value |
+-------------------------+----------+
| Qcache_free_blocks | 1 |
| Qcache_free_memory | 20959616 |
| Qcache_hits | 5 |
| Qcache_inserts | 5 |
| Qcache_lowmem_prunes | 0 |
+-------------------------+----------+
8 rows in set (0,00 sec)

La partition n'a pas été modifiée et pourtant le résultat du query cache a été invalidé :-(


Pour Oracle :

SQLPLUS SCOTT/****

CREATE TABLE table_oracle_partitionned(ident NUMBER, quand date, valeur NUMBER)
PARTITION BY RANGE (quand)
INTERVAL (numtoyminterval(1,'YEAR'))
(
PARTITION p2006 VALUES LESS THAN (to_date('2007-01-01','yyyy-mm-dd'))
);

INSERT INTO table_oracle_partitionned VALUES(1,to_date('2008-06-04','yyyy-mm-dd'),20);
INSERT INTO table_oracle_partitionned VALUES(2,to_date('2007-05-04','yyyy-mm-dd'),35);
INSERT INTO table_oracle_partitionned VALUES(3,to_date('2008-07-04','yyyy-mm-dd'),3);
INSERT INTO table_oracle_partitionned VALUES(4,to_date('2009-10-03','yyyy-mm-dd'),11);
INSERT INTO table_oracle_partitionned VALUES(1,to_date('2009-12-25','yyyy-mm-dd'),46);
INSERT INTO table_oracle_partitionned VALUES(2,to_date('2008-10-12','yyyy-mm-dd'),43);
INSERT INTO table_oracle_partitionned VALUES(10,to_date('2006-05-03','yyyy-mm-dd'),32);
INSERT INTO table_oracle_partitionned VALUES(5,to_date('2007-06-12','yyyy-mm-dd'),56);
EXIT;

SQLPLUS / AS SYSDBA
SQL> EXECUTE dbms_result_cache.flush;
SQL> SELECT * FROM scott.table_oracle_partitionned WHERE quand BETWEEN to_date('2008-01-01','yyyy-mm-dd') AND to_date('2008-10-01','yyyy-mm-dd');

IDENT QUAND VALEUR
---------- -------- ----------
1 04/06/08 20
3 04/07/08 3

Si on lance plusieurs fois la requête et que l'on regarde les informations du result_cache :

SQL> select name, value from v$result_cache_statistics;

NAME VALUE
-------------------- ----------
Create Count Success 1
Create Count Failure 0
Find Count 4
Invalidation Count 0
Delete Count Invalid 0

On s'aperçoit que le résultat est bien récupéré à partir du result_cache.

J'effectue une mise à jour sur la table mais en prenant soin de ne pas intervenir sur les enregistrements de la partition contenant les données de l'année 2008 :

SQL> UPDATE scott.table_oracle_partitionned SET VALEUR=VALEUR+1 WHERE quand BETWEEN to_date('2007-01-01','yyyy-mm-dd') AND to_date('2007-12-30','yyyy-mm-dd');

2 rows updated.

Je relance la même requête pour récupérer les enregistrements de l'année 2008 et je regarde à nouveau les statistiques du result_cache :

SQL> SELECT name, value FROM v$result_cache_statistics;

NAME VALUE
-------------------- ----------
Create Count Success 2
Create Count Failure 0
Find Count 4
Invalidation Count 1
Delete Count Invalid 0

En conclusion :

Le Result Cache d'Oracle se comporte comme le Query Cache de MySQL dans le sens où chaque modification d'une table engendre l'invalidation des SELECTs concernés. J'espérais qu'Oracle aurait su prendre en compte le partitionnement de la table pour obtenir une invalidation plus fine ce qui dans l'exemple plus haut aurait eu pour conséquence de ne pas recréer un enregistrement dans le Result Cache.

ASUS F9S : Mise à jour de Gutsy vers Hardy

J'ai enfin franchi le pas et mis à jour mon ASUS. Après quelques péripéties auxquelles je m'attendaient celà fonctionne.
Voici en résumé les quelques modifications que j'ai effectuées :

- Pour continuer à écouter vos mp3 préférés, ajouter dans le fichier /etc/modprobe.d/alsa-base la ligne suiante :
options snd-hda-intel model=lenovo

- S'assurer que le fichier /etc/default/linux-restricted-modules-common contienne la ligne :
DISABLED_MODULES="nv nvidia"

- Installer les drivers propriétaires nvidia en les récupérant directement sur le site officiel :
cd /tmp
wget -P /tmp/ http://fr.download.nvidia.com/XFree86/Linux-x86/173.14.05/NVIDIA-Linux-x86-173.14.05-pkg1.run
sudo su -
/etc/init.d/gdm stop
chmod u+x /tmp/NVIDIA-Linux-x86-173.14.05-pkg1.run
cd /tmp
./NVIDIA-Linux-x86-173.14.05-pkg1.run

Le reste de l'installation devrait se passer sans souci. sauvegarder votre fichier /etc/X11/xorg.conf et lancer nvidia-xconfig pour regénérer la configuration nvidia pour xorg.

- Mon pauvre gdm étant passé en querty j'ai dû lancer :
sudo dpkg-reconfigure xserver-xorg

- Glipper crash de façon aléatoire
wget -P /tmp http://launchpadlibrarian.net/14965046/glipper_1.0-1ubuntu2%7Eppa1_i386.deb
gdebi-gtk /tmp/glipper_1.0-1ubuntu2~ppa1_i386.deb
sudo sed -i -e 's/time\.sleep([0-9]\+)/time\.sleep(20)/g' /usr/lib/glipper/glipper

- Firefox 3 est inutilisable. Constamment en train d'accéder à mon disque dur ce qui a pour effet de le figer qqs secondes et de m'énerver. Je suis donc repassé à Firefox 2 :
sudo apt-get install firefox-2
sudo dpkg-divert --divert /usr/bin/firefox.ubuntu --rename /usr/bin/firefox
sudo ln -s /usr/bin/firefox-2 /usr/bin/firefox

- Google Desktop Linux ne fonctionne plus depuis la mise à jour :
Se rendre dans System/Preferences/Applications Preferees et modifier le naviguateur web utilisé pour pointer vers firefox (le chemin est incorrect)

- Audacity retourne une erreur du genre AlsaOpen( hostApi, parameters, streamDir, &pcm )' failed in 'src/hostapi/alsa/pa_linux_alsa.c
sudo apt-get remove jackd

- Vmplayer ne fonctionne plus car hardy ne dispose pas des paquets nécessaires (modules noyaux, etc...) :
Télécharger la dernière version sur http://www.vmware.com/download/player/download.html
sudo apt-get remove vmware-player
sudo apt-get install linux-headers-2.6.24-19-generic build-essential
bien sûr vous disposer du gcc installé ou faudra le faire
sudo su -
cd /opt
tar xvzf /tmp/VMware-player-2.0.4-93057.i386.tar.gz
cd vmware-player-distrib
./vmware-install.pl
il suffit ensuite de répondre aux questions et de lancer le player une fois le tout terminé

- Le Wifi s'interrompt régulièrement et impossible de passer en veille correctement (ça semble être corrélé) :
sudo apt-get install linux-backports-modules-hardy-generic
echo "options iwl3945 disable_hw_scan=1" > /etc/modprobe.d/iwl3945

- Une fenêtre active est stickée sur l'espace de travail sur lequel elle a été ouverte. Assez génant sur zim, pidgin et certainement plein d'autres applications :
Un BUG a été identifié relatif à Metacity. Donc en attendant que ce soit corrigé, mieux vaut ne rien faire. Je ne me vois pas du tout me mettre à patcher toutes les applications que j'utilise en attendant que Metacity soit modifié.

Voilà ce sont les quelques modifications que j'ai dû effectuer ce week end pour pouvoir continuer à utiliser mon portable.