10 ans de Gandi: retour d'expérience
Par Kalou le mardi 16 mars 2010, 16:51 - Hébergement - Lien permanent
À l'occasion des dix ans de Gandi, l'idée un peu folle de donner, sur dix jours, 55000 domaines, a posé une question pratique. Comment, en ouvrant les vannes d'une telle opération, maintenir une qualité irréprochable sur le site ? L'ambiance festive aurait aussi bien pu se transformer en cauchemar pour nos clients si ils avaient été privés d'accès à leurs interfaces de gestion.
La décision fût donc prise d'héberger l'événement sur un site dédié. Occasion rêvée pour se mettre un peu à la place de nos clients, et utiliser notre infrastructure d'hébergement. Règles du jeu : on utilise exclusivement les outils fournis au client, on prépare une archi qui monte en charge facilement, ça ne doit pas coûter un bras, ça peut illustrer notre flexibilité légendaire.
Keep it simple, stupid
De la décision à l'implémentation, on avait une semaine. L'idée charmante de monter un site "cloudish" à base de technos modernes, et peu maîtrisées, est donc oubliée d'office. Pour être honnête, vu les délais, le développeur désigné d'office se voit choisir la techno : "là où t'es à l'aise, tu as une semaine". Ça sera PHP/mysql, ce qui n'est pas du goût de tout le monde ! Mais permettra de sortir le site testé et dans les temps.
Pour tenir une charge correcte, plusieurs serveurs seront nécessaires. Premier rappel de nos imperfections, Gandi ne fournit pas de load balancing ! Qu'importe, on ira donc sur un round robin DNS, à petit TTL pour pouvoir sortir facilement un frontal cassé de la production.
Notre distribution pour l'occasion, sera une ubuntu 9.10 - parce qu'elle est assez à jour, et qu'elle utilise un kernel 2.6.27.
Multiplier les petits serveurs
La meilleur façon de monter en charge sur notre infrastructure est d'isoler fonctionnellement l'architecture en nombreux petits serveurs. Là où nous assurons un minimum de "scalabilité" verticale (vous pouvez augmenter dynamiquement la RAM, le CPU alloués à un serveur), c'est à l'architecture de prévoir la scalabilité "horizontale".
Ainsi, on peut facilement monter les ressources là où les bottlenecks se font sentir, et ajouter/ou déplacer des parts d'un serveur vers l'autre lorsque la puissance allouée le demande. Les avantages des "nombreux petits serveurs" sont multiples :
- chaque serveur bénéficie au minimum d'un core CPU en burst (oui, un core entier, même sur une part : c'est nouveau)
- la redondance est assurée, et vos parts s'étalent un peu aléatoirement sur des centaine de serveurs physiques
- particulièrement en environnement virtualisé, la performance mémoire est meilleure sous 1G de RAM
- si vous avez 4 serveurs d'une part, au lieu d'un gros de 4 parts, ils montent à 8x4 parts dynamiquement, ou 24x4 parts en un reboot, tout ça sans toucher à l'archi - là ou le gros s'arrête à 8 sans reboot, ou 24
- les ressources sont faciles à déplacer vers les serveurs qui en ont besoin.
Nous partons sur l'infrastructure simpliste :
- 24 (!) serveurs d'une part, pour gérer le web et PHP : 10 en anglais, 10 en français, et 2 + 2 pour l'IPv6
- 2 serveurs de 4 parts, des memcached répliqués pour soulager la DB et gérer les sessions
- 1 serveur mysql de 4 parts, qui contiendra les coupons pré-générés (ils tiennent en RAM, la base doit théoriquement s'ennuyer)
Après quelques belles surcharges et un bout de code revu, la base de donnée sera finalement épargnée au maximum par memcached (voir chapitre « un code léger… »)
Soit 36 parts, pour une durée de dix jours : environ 140 euros.
C'est un admin qui s'usera les doigts sur notre site pour créer, et configurer les 24 serveurs en parallèle. Visiblement, la sortie de l'API hosting ou une fonction dans l'interface web seraient les bienvenus. (Merci à cssh pour l'occasion).
Sécuriser (un peu) les machines
Une distribution par défaut mérite toujours quelques retouches. Exporter mysql sur le réseau « public » du hosting gandi nous ennuyait un peu. À grands coups de netstat, fermer les services inutiles qui écoutent sur un port public. Avec l'aide de tcpwrappers (hosts.allow, hosts.deny), on ferme toutes les interfaces « privées » (sshd, mysql réservé aux machines frontales du web).
Restera à bien faire attention au code php et à nos queries mysql : le meilleur moyen d'éviter les injections est encore de binder tous les params après un prepare(). En plus, ça décharge la DB lorsqu'on effectue plusieurs execute.
Un détail important : notre site autorisant l'envoi de mail à un destinataire « arbitraire », il était critique de limiter au maximum son exploitation potentielle par un spammer malin : restrictions du nombre d'envoi par coupon, au minimum, et monitoring.
Prévoir l'environnement de dev et le déploiement
Partager les données entre les sites, c'est ajouter un "single point of failure" et un point de contention à l'archi. Nous optons donc pour un déploiement "local" à chaque serveur du contenu du site. Un site de développement et de "staging" sur une VM sert à tester et développer les mises à jour. Un script et quelques rsync permettent de déployer l'ensemble sur tous les frontaux de l'architecture. On a dit simple !
Surveiller ses ressources
Quelques minutes avant l'opération, pour prévenir plutôt que guérir, la totalité des machines virtuelles sont augmentées d'une à deux parts. En utilisant l'interface des stats, dès le premier jour, on peut constater l'ennui total qui habitait les machines virtuelles:

Il aurait été malin, à ce moment précis, de revenir à une part par machine. Ou d'exploiter gandi "autoflex", ou encore, vu le type de charge observée, de préparer des flexs programmés pour les heures de lâcher des coupons ! Malheureusement, beaucoup de monde sur beaucoup de ponts, on a raté l'occasion de vous faire cette belle démonstration (écono-techno-(éco ?)logique).
Un code léger vaut mieux que mille CPU costauds
Même si nous avions à notre disposition plusieurs milliers de CPU et beaucoup de teras de RAM, mardi fut suffisamment chaotique pour qu'on en reparle ici : après un lundi très bien tenu en charge, le plan d'exécution de notre unique SELECT COUNT changea brutalement et devint terriblement plus lent (300ms). Avec foi, nous avions pensé que ce query « unique », sur une table présente complètement en mémoire, ne poserait pas de problème : il était donc exécuté sur toutes les pages du site. La concurrence d'accès avec les UPDATEs des coupons finit par avoir raison de la base qui, malgré des stats systèmes proches de l'idle, entra discrètement en contention de lock.
La réaction habituelle est d'ajouter des parts pour monter en charge. C'est une solution temporaire acceptable, mais ça ne suffit pas !
Un analyse système, une remise en question du code, et une utilisation salvatrice de memcached permit de retrouver des performances acceptables. Une modification des queries utilisés aurait peut-être pu faire l'affaire également.
Une moralité à cet épisode: le code, les indexes, l'architecture, (…) sont les garants de votre montée en charge, et si ils sont « cpu friendly » vous épargneront, sinon une catastrophe, au moins un achat de parts inutiles.
En plus, comme on dit ici en plaisantant, c'est écolo.
Quelques chiffres
- 36 parts, mais on aurait pu tenir sur moins (snif)
- 5% de CPU en peak
- 4000 requêtes par frontal dans la première minute de chaque heure de pointe (environ 1400 req/seconde au total)
- un minimum de 11 secondes pour donner 1000 coupons
- pour le même nombre de coupons, un maximum de 40 minutes, pendant l'incident.













Commentaires
Belle démonstration
Vivement la ram en option, car pour memcached, 4 parts de CPU me semblent beaucoup pour 1go de ram (et moi personnellement 8 parts pour 2go de redis en ram, ça fait cher juste pour de la ram...)
Beau boulot les gars.
Twidi: on est dessus, c'est pour la version 3.0.3 (on est actuellement en 3.0.2)
Sympathique explication de la démarche.
Petite question rapide : votre count() n'était il pas fait sur une vilaine table MYISAM pleine de verrous ?
Twidi: salut! merci :)
Kioob: hé.. si!
ENGINE=MyISAM, tout par défaut (hm.. non, on a un peu augmenté les buffers/caches), donc lock de la table entière, au lieu de row-level. Mais le query en question, demandait de toute façon un lock de toute la table même en innodb (where "pas attribué"). Je crois qu'ils (alors que je skiais au soleil) ont aussi essayé des updates low priority, je laisse les gens qui ont réparé expliquer ça.
On aurait pu éviter mysql, aussi. Une table unique, pas de relations, .. un autre sujet ;)
@kalou : Non pas essayé les update en low priority. Et d'après ce que je vois de la doc ("UPDATE is delayed until no other clients are reading from the table") je suppose que c'était pas plus mal :)
Oh, la RAM en option est une excellente nouvelle !
Nicolas : rahhh je viendrai vous faire des bisous le jour où ça sort !!!! (ou juste un coucou hein, je veux pas vous faire peur et vous empêcher de le sortir !)
J'ai pas bien compris, vous avez utilisé 3 serveurs de 4 parts. 1 pour la base et les 2 autres pour?
Quels sont les infos que vous stockez en memcache à part les sessions?
Sinon simpa ce post, c'est plutot agréable de savoir ce qu'il y a sous le capot!
Très sympa. Il ne manque que quelques détaux et copies d'écran pour en faire un très bon tutorial !
Encore, encore !
Une autre question: comment faites vous pour installer 24 serveurs (LAMP sans M)?
Avec les outils actuels y-a-t-il moyen d'automatiser tout ça ? Ce serait pas possible de créer un serveur, d'installer tout ce qu'il faut, puis garder le disque sous le coude et de le dupliquer à escient si jamais on veut plus de serveurs. Ça permettrait d'avoir une image de base à instancier.
@spirit : Les 2 autres serveurs sont des serveurs memcache.
À part les données de la session, après le 2ème jour, le memcache stockait le nombre de coupons restant (le fameux "SELECT COUNT").
@spirit : Non pour le déploiement des serveurs pour le moment il n' y pas possibilité d'automatiser, donc ca reste assez laborieux sur un nombre important de serveur. En gros dans notre cas il a fallu à peu pret 1h30 pour créer les serveurs+disques en cliquant dans l'interface. Ce qui reste néanmoins beaucoup plus rapide que d 'installer des serveurs physique :)
L'automatisation viendra dans quelques temps si je ne me trompe pas, soit par une api ou des fonctionnalités avancées dans l'interface web :)
Merci pour ces réponses, quelque questions supplémentaires:
>> L'automatisation viendra dans quelques temps si je ne me trompe pas
Quel genre d'automatisation ce sera ? Des genre de snapshot?
>> Les 2 autres serveurs sont des serveurs memcache.
Donc au total: 24 serveurs frontend, 2 serveurs memcache et 1 serveur mysql.
Pourquoi 2 memcache? Replication? Redondance?
>> le fameux "SELECT COUNT"
La table qui était en mémoire (pour le show count) c'est en utilisant le moteur de table TYPE = MEMORY;?
__
Question subsidiaire: avec le round robin dns faible ttl, si un serveur tombe:
- L'internaute qui était deja sur le DNS du serveur sera routé vers celui-ci jusqu'a l'expiration du ttl.
- Un serveur down est il supprimé du DNS ou autre afin que plus aucun internaute ne soit redirigé vers ce serveur down?
Merci bcp pour vos réponses.
@spirit: une API ? une interface web maligne ? les deux ? :)
2 memcached pour répliquer et redonder un peu. (master/master).
La table était en mémoire parce qu'elle tenait dans peu d'octets. Donc dans le cache du filesystem au minimum, dans le cache de mysql aussi par endroits.
Pour le down d'un frontal, le client web utilise toutes les adresses en round robin (et reste scotchée à la même en KeepAlive). Le TTL sert uniquement à manuellement ajouter ou retirer un frontal en cas de problème.
Selon les browsers on aura des comportements différents, mais en gros avec un frontal "down" sur N, on aura une requête sur N qui n'aboutira pas, au pire après un timeout TCP "long", jusqu'à ce que l'admin réagisse et que le TTL expire, *et* sous reserve que l'internaute n'utilise pas un cache "buggé".
Il est donc préférable de déplacer l'IP du frontal down sur un frontal "UP" en attendant.
La solution est "cheap" mais est pratique à court terme, donc :)
Bonjour et merci pour ce feedback très instructif !
Juste une petite question : avez-vous sécurisé l'accès aux serveurs memcached ?
Etant donné qu'ils devaient être accessibles depuis les 24 frontend en écoutant sur un ip public, c'est une infrastructure qui ressemble à celle sur laquelle je travaille actuellement.
Sachant que memcached n'est pas absolument safe (même avec SASL), j'envisage d'utiliser le port forwarding via SSH, mais il faut encore que j'étudie la question de l'impact sur les performances.
Comment avez-vous traité ce problème ?
À bientôt
Encore une fois, très bon boulot ! :P
Merci!
iptables sur les memcached est une solution simple pour filtrer l'accès.
Dommage qu'il ne supporte pas tcpwrappers.
Le code SASL auth a l'air de refuser les commandes non authentifiées, pourquoi pas safe ?
http://code.google.com/p/memcached/...