J’ai une application Play! en production

Je tenais à faire un article sur un sujet bien précis qui reflète un retour d’expérience sur le déploiement d’applications Play! Framework en production. Je vais prendre l’exemple de ce site Web e-commerce : www.labottegardiane.com que j’ai développé from scratch à l’aide de Play! framework et jQuery en 2009 pour le compte d’un de mes clients. Il a été mis en production fin 2009, soit il y a plus de deux ans à l’écriture de cet article et ce sur un serveur Gandi. Il faut savoir qu’à l’époque c’était « osé » car l’offre ne proposait que des parts de 0,5 CPU et 128Mo de mémoire vive. Ces quota ont été doublé pour une part seule depuis mais la puissance disponible reste malgré tout un peu juste pour déployer du Java.

Donc comment faire pour héberger une application Java avec un minimum de performance ? Les premiers tests ont été désastreux avec uniquement quelques requêtes par secondes… J’ai donc effectuer du profiling du front avec notamment Firebug et YSlow afin d’apporter les optimisations nécessaires à mon code source. Ensuite, j’ai optimiser la configuration de mon application et mis en place un cache applicatif avec ehCache afin de soulager la base de donnée MySQL.

Cependant, la meilleur des optimisations a été de mettre en place un frontal Nginx avec une configuration des headers pour le cache et une compression zip adaptée couplés à la mise en place d’un webcache avec Memcached. Les performances sont désormais au rendez vous et le serveur ne pâti plus de pic de charges CPU sous mes agressions avec l’outil que j’utilisait à l’époque pour simuler la charge : apache-benchmark. Ainsi sur la page des produits, on obtient encore 124 requêtes par seconde aujourd’hui (je ne me rappel plus du score de l’époque qui était du même ordre) :

Concurrency Level:      10
Time taken for tests:   8.049 seconds
Complete requests:      1000
Failed requests:        0
Write errors:           0
Total transferred:      5478000 bytes
HTML transferred:       5245000 bytes
Requests per second:    124.25 [#/sec] (mean)
Time per request:       80.485 [ms] (mean)
Time per request:       8.049 [ms] (mean, across all concurrent requests)
Transfer rate:          664.67 [Kbytes/sec] received

 

L’application n’a pas été redémarré depuis plus d’un an et demi et les performances, comme le bon vin, ne font que s’améliorer. On remarque que le débit obtenu est très proche du débit théorique fourni par Gandi (5Mb). Pour preuve de l’ancienneté de l’application :

 

Aucun problème à signaler depuis malgré l’arrêt des mises à jour des logiciels suite à l’arrêt de la collaboration avec le commerçant pour le moment. Comme quoi performances et disponibilité ne sont pas si difficile a obtenir … même en environnement « hostile ». Cela dit, aujourd’hui je ne proposerais pas une telle solution à un client qui n’a pas de gros besoins de performances. Il faut savoir que la société derrière Play! framework (Zenexity) propose une plateforme d’hébergement d’applications : www.playapps.net Pour 10€, les résultats sont au rendez vous et c’est clef en main… pourquoi se priver ! Par ailleurs, d’autres plateformes « cloud » proposent de plus en plus une compatibilité avec Play! framework comme par exemple Heroku. Peut être qu’un jour je ferais un article sur le sujet… :-)

 

Mettre en place le sharding et le failover avec MongoDB

Cela fait quelques mois que j’utilise MongoDB au boulot dans le cadre du développement d’applications Web et ça marche plutôt pas mal. Dans cet article, on va voir comment mettre en place une architecture redondée qui permettra de profiter des joies de la scalabilité horizontale : il suffit d’ajouter des serveurs à la volée et comme par magie notre application est capable d’absorber plus de charge.

Note : je préfère travaillé avec le paquet 10gen qui est celui qui est le plus à jour. Vous trouverez la procédure d’installation ici.

$ mongo --version

MongoDB shell version: 1.8.2

On commence par mettre en place la réplication

Afin d’assurer la redondance et la haute disponibilité, MongoDB possède une fonctionnalité appelée « replica set » qui permet aux données d’être dupliquées de manière transparente pour le développeur. Le concept est de créer un groupe de serveur (set) qui possédera un nœud principal (primary) et n serveurs de backup (secondary). Si a un moment donné le nœud principal est inopérant, automatiquement l’un des serveurs de backup deviendra serveur primaire.

On commence par arrêter le daemon :

# /etc/init.d/mongodb stop

Puis on crée les dossiers où seront stockées les bases :

# mkdir /data/r0
# mkdir /data/r1
# mkdir /data/r2

Puis on lance les instances :

# mongod --port 27017 --dbpath /data/r0 --replSet foo
# mongod --port 27018 --dbpath /data/r1 --replSet foo
# mongod --port 27019 --dbpath /data/r2 --replSet foo

Ici, on a appelé notre replica set « foo ». Libre à vous de l’appeler comme vous voulez et de mettre autant de serveurs que souhaité.

Ensuite on va configurer le replica set. On se connecte :

$ mongo

Puis on écris notre configuration :

> cfg = {
_id : "foo",
members : [
{ _id : 0, host : "localhost:27017"},
{ _id : 1, host : "localhost:27018"},
{ _id : 2, host : "localhost:27019"},
] }

Et on initialise :

> rs.initiate(cfg)
{
"info" : "Config now saved locally. Should come online in about a minute.",
"ok" : 1
}

Après quelques secondes, l’invite changera lorsque vous taperez « entrée » :

foo:PRIMARY>

C’est bon : La réplication est active. A partir de maintenant vous pouvez connecter votre application sur l’instance MongoDB lancée sur le port 27017. Les données en insertions seront automatiquement répliquées sur les autres instances. Cependant, le failover n’est pas encore en place. Même si on constate que l’élection automatique du master est en place, notre application ne peut pas basculer seule d’une instance vers une autre.

Nous allons donc utiliser le sharding pour y parvenir de manière transparente pour l’application.

Mise en place du sharding

Une infrastructure typique MongoDB consiste en plusieurs replica set ainsi que de plusieurs instances mongo dédiées à la configuration du sharding. Enfin, il faudra également une instance mongos par serveur d’application. Mongos n’est ni plus ni plus ni moins qu’un loadbalancer servant de point d’entrée/sortie pour les applications.

 

Pour le vocabulaire, chaque replica set est aussi appelé shardn. Le sharding consiste en une distribution du stockage des données sur les différentes instances Mongo au sein de ce shard. Chaque machine stock donc un sous ensemble des données. Souvent, c’est la clef (id) de chaque document qui permet de définir le serveur qui stockera l’information.

Concrètement, il nous faudra simplement arrêter nos trois instances et leur rajouter le paramètre suivant permettant d’activer le sharding :

--shardsvr

Ensuite, on lance nos serveurs de configuration :

# mkdir /data/configdb0
# mkdir /data/configdb1
# mkdir /data/configdb2
# mongod --port 27020 --configsvr --dbpath /data/configdb0
# mongod --port 27021 --configsvr --dbpath /data/configdb1
# mongod --port 27022 --configsvr --dbpath /data/configdb2

C’est ce serveur qui va stocker les informations utiles au bon fonctionnement du sharding. On lance ensuite le routeur :

# mongos --port 27023 --configdb localhost:27020,localhost:27021,localhost:27022

puis on se connecte sur le routeur :

$ mongo --port 27023

Puis on ajoute les nœud au shard :

> use admin
> db.runCommand({addshard : "foo/localhost:27017,localhost:27018,localhost:27019"})

Vous noterez la syntaxe particulière qui commence par le nom du replica set (foo) suivi d’un slash et de la liste des serveurs. Si tout c’est bien passé vous devriez avoir ce message :

{ "shardAdded" : "foo", "ok" : 1 }

Testons notre infrastructure

Par défaut, le sharding n’est pas activé. On peut l’activer au niveau d’une base de donnée ou bien au niveau d’une collection. Ici nous activons le sharding sur toute la base de données.

> db.runCommand({enablesharding: "test"})

Puis nous définissons la clef à utiliser pour le sharding :

> db.runCommand({shardcollection: "test.hits", key:{"_id":1}})

On va commencer par charger un peu en données. J’ai pris ici l’exemple de hits de statistiques
> use test
> for(i=0;i<1000000;i++) {
db.hits.insert({tag: "search", ctx : ["blah"] });
db.hits.insert({tag: "video", ctx : [parseInt(Math.random() * 100), "play"] });
db.hits.insert({tag: "video", ctx : [parseInt(Math.random() * 1000), "play"] });
}

On insère donc 3 millions de hits en base avec notamment des tags « video » dont l’identifiant est le premier élément du tableau de contexte (ctx). L’insertion va prendre quelques minutes et on pourra observer l’utilisation des ressources. On s’aperçoit que les traitement sont bien paralléliser et que l’utilisation mémoire est constante :-)

Une fois fini on peut constater que les différentes bases MongoDB utilisent 14Go. Attention : MongoDB pré-alloue les fichiers de stockage : 64Mo, 128Mo, 256Mo, 512Mo etc. jusqu’à créer des fichiers de 2Go. Les explications ici.

 On peut avoir les statistiques de la collection :

> db.hits.stats()

Conclusion

La mise en place du sharding et de la réplication est assez simple. Ainsi on peut rapidement mettre en place une infrastructure fiable. Pour augmenter la capacité de montée en charge il suffira d’ajouter des des réplica set au shard. MongoDB s’occupera automatiquement de répartir le stockage des données et l’exécution des différentes requêtes. Pour augmenter la fiabilité, on pourra également augmenter le nombre de noeud dans chaque replica set. Dans un prochain article je montrerais l’utilisation de map reduce.

Aller en haut