Bien que Twitter reste certainement un des plus gros sites web basé sur Ruby on Rails au monde, la société passe progressivement de plus en plus d'éléments sur la JVM. Le changement est à la fois motivé par les avantages fréquemment cités de la JVM, tels que la performance ou la scalabilité, mais aussi par le désir de mieux découper en services indépendants et de mieux adresser d'autres problématiques d'architecture.
L'année dernière l'entreprise a annoncé simultanément, que ses back-end pour la file des messages et pour le stockage des Tweets avaient été ré-écrits en Scala, puis au printemps 2010, l'équipe de R&D Twitter a commencé à ré-écrire le moteur de recherche. Dans ce même effort, Twitter a remplacé le moteur de recherche basé sur MySQL, par une version temps-réel batie sur Lucène. Plus récemment l'équipe a annoncé qu'elle remplacait le front-end pour la recherche, écrit en Ruby on Rails, par un serveur Java qu'ils ont baptisé Blender. De ces changements découlent une réduction par 3 de la latence lors des recherches.
InfoQ a rencontré un ingénieur de Twitter, Evan Weaver, pour en savoir davantage sur les raisons de ces changements.
L'architecture Twitter
Une des principales observations que l'on peut faire en regardant l'architecture de Twitter, c'est que la plupart des choix de conception sont admirablement pragmatiques. Par exemple, le back-end de Twitter utilise à la fois MySQL et la base de données distribuée Cassandra. Gizzard, le framework maison (open source) pour créer des bases de données distribuées, est utilisé pour partitionner MySQL. D'après E. Weaver, ceci est "principalement utilisé pour les données fortement structurées, avec un SLA très élevé, parce que c'est assez peu flexible".
Toutes les données runtime sont servies soit par Gizzard/MySQL, soit par Cassandra. Twitter utilise aussi intensivement HDFS dans Hadoop, pour les calculs en tâche de fond, et met en ligne un système qui utilise Gizzard pour partitionner une base de données clé-valeur Redis.
Nous avions des schémas dans MySQL qui fonctionnaient, donc nous les avons gardés et nous les avons partitionnés grâce à Gizzard plutôt que de les migrer dans un système avec des caractéristiques de performance différentes. Mais Cassandra est vraiment plus flexible, et l'expérience acquise sur ce produit pour les nouvelles choses est vraiment positive.
Pour le mid-tier, qui nécessitait d'être plus flexible avec les données, en raison des nouvelles fonctionnalités, les time series en particulier, ou bien pour les choses qui ont vraiment besoin d'une écrire des données à très grande vitesse, comme servir les résultats de calculs issus de Hadoop, nous utilisons intensivement Cassandra.
La communication entre les services du front-end et du back-end utilise Thrift, le mécanisme de RPC développé par Facebook. Tandis que pour tout le RPC public, c'est du JSON sur REST qui est utilisé, notamment par les clients Twitter, dont le nouveau site web fait partie.
Le choix des langages
Une approche tout aussi pragmatique a été adoptée pour le choix des langages. Les langages de premier ordre chez Twitter sont JavaScript, Ruby, Scala et Java. Le C est aussi utilisé, mais rarement pour les nouveaux développements de services. D'après E. Weaver, en général, les développeurs issus du milieu Ruby préfèrent travailler en Scala, alors que les développeurs venant du monde C ou C++ choisissent Java.
Dans le cas de l'équipe moteur de recherche, comme leur tâche impliquait de beaucoup travailler avec Lucene, qui est écrit en Java, et qu'ils ont plus d'expérience en Java, ils ont trouvé plus pratique de travailler en Java plutôt qu'en Scala ou dans un autre langage.
Pour permettre aux développeurs de choisir le langage le plus adapté à leur tâche, Twitter a beaucoup investi dans l'écriture de frameworks internes pour encapsuler les problématiques habituelles. Par exemple Finagle est une librairie pour construire des clients et des serveurs RPC asynchrones en Java, Scala ou n'importe quel langage pour la JVM. Elle est écrite en Scala, mais supporte aussi une API Java très idiomatique.
Alors que le code du back-end est progressivement migré sur la JVM, le code du front-end fait une utilisation de plus en plus intensive de JavaScript comme la plupart des applications web contemporaines. Par conséquent, la composante Ruby rétrécit.
À l'origine nous étions une boutique basée sur Rails, et je crois que nous étions le plus grand site au monde à utiliser Rails, mais nous avons grandi en tant qu'organisation, et en tant que service, les performances et l'encapsulation sont devenues critiques. Je ne dirais pas que Rails nous a deçu d'une façon ou d'une autre, c'est juste que nous avons grandi très vite. Au final il y a deux choses qui font que Rails n'est plus la solution idéale à notre besoin actuel.
D'abord, le runtime Ruby est lent, en particulier comparé à la JVM. Nous avons travaillé dur sur le garbage collector pour obtenir des performances acceptables.
Ensuite parce que le modèle LAMP qu'incarne Rails, où il y a une succession de couches dans laquelle chacune appelle celle du dessus et du dessous, sans un découpage vertical clair, ne convient pas à une organisation aussi grande que la notre.
Pendant que nous nous concentrions sur les performances et l'encapsulation, nous avons corrigé les problèmes de performances autant que nécessaire, avec des caches ou en travaillant sur les VMs.
La majorité des requêtes sur Twitter traversent Rails pour l'instant, mais quand nous développons de nouveaux services, si nous choisissons de les redévelopper entièrement pour améliorer l'encapsulation, alors nous passons sur la JVM, parce que les problèmes de performances dépassent les gains de productivité ou d'agilité que Ruby pourraient nous apporter. Donc quand nous avons redéveloppé le stockage des Tweets, nous l'avons construit avec Gizzard sous la forme d'un service homogène, il expose une interface métier, et c'est le système Scala qui partitionne et gère des noeuds MySQL non coordonnés. Cela a efficacement éliminé l'utilisation d'ActiveRecord dans la pile Rails originale pour les tweets.
La même chose s'est produite avec la file; quand nous avons voulu la ré-écrire et la ré-encapsuler pour des raisons de performances nous avons pris la JVM. Au fur et à mesure que ces petits projets orientés services sont repris, de plus en plus de responsabilités sont retirées de l'application Rails originale.
De l'autre côté, nous avons déplacé le code de rendu dans du code JavaScript qui s'exécute dans le navigateur, nous n'avons plus d'intérêt à utiliser le moteur de templating de Rails pour construire des pages web. Donc on retire du code Rails de plusieurs façons, quand nous avons besoin de le réécrire, nous optons pour la solution la plus rapide, parce que les performances sont vitales pour nous. Nous sommes un des plus grands sites web au monde, mais il fonctionne sur du matériel très simple, en comparaison de ce qu'il faut pour les autres grands sites web dynamiques.
Conserver un matériel simple a des avantages en termes de cout, mais évite aussi des problèmes de scalabilité secondaires, tels que les performances de la pile TCP, qui peut avoir un impact sur les sites qui utilisent du matériel plus évolué.
Vous pourriez supposer que le passage sur la JVM a été essentiellement dicté par des problèmes de performances et de scalabilité, mais il n'en est rien, le code actuel de Twitter fonctionne plutôt bien en fait. L'entreprise n'est pas contrainte de tout réécrire du sol au plafond juste pour pouvoir continuer à grandir. En réalité, le passage sur la JVM est autant motivé par la productivité des développeurs que par les performances.
Honnêtement, la raison première est l'encapsulation, qui nous permettra d'itérer plus rapidement en tant qu'entreprise. Avoir une seule application monolithique n'est pas adaptée à un développement rapide et par équipes. Donc quand nous avons décidé de découper quelque chose, quitte à tout redévelopper, il vaut mieux s'appuyer sur la JVM pour des raisons de performances, au lieu de repartir sur du Ruby.
Ceci mis de côté, parce que nous nous appuyons encore beaucoup sur Ruby, nous avons beaucoup investi sur notre infrastructure, et ça marche bien.
Moteur de recherche: de Ruby à Java
La transition de Ruby à un serveur Java basé sur Blender a été concrètement réalisée en deux étapes. La première a été de remplacer le back-end MySQL par un index inversé et temps réel que l'équipe a développé en s'appuyant sur Lucène et nommé Earlybird. Earlybird a doublé l'efficacité et a apporté suffisamment de flexibilité pour ajouter une recherche avec la notion de pertinence, ce qui aide à répondre à la demande grandissante du service de recherche. D'après un article de blog technique
En 2008, la recherche Twitter supportait une moyenne de 20 TPS (tweets par seconde) et 200 QPS. Fin octobre 2010, quand nous avons remplacé MySQL par Earlybird, le système supportait 1000 TPS et 12000 QPS en moyenne... Cependant, nous avions toujours besoin de remplacer le front-end Ruby on Rails qui était seulement capable d'appels synchrones à Earlybird, et amenait une dette technique significative après la transition sur Earlybird.
Pour résoudre cela, l'équipe a entamé le développement du serveur Java Blender. Blender est un serveur Thrift et HTTP construit à l'aide de Netty, une librairie haute scalable (NIO) écrite en Java qui permet le développement de serveurs aux protocoles variés. Netty permet à Twitter de créer un service d'agrégation complètement asynchrone, qui est capable d'agréger les résultats de plusieurs services back-end tels que les index temps réel, les tops tweets et la géolocalisation. D'après le blog technique:
Netty définit une abstraction principale, nommée canal, qui encapsule une connexion à une socket réseau et fournit une interface contenant un ensemble d'opérations d'entrée/sortie comme read, write, connect, et bind. Toutes les opérations d'entrée/sortie sur un canal sont asynchrones par nature.
Cela signifie que n'importe quel appel I/O produit une instance de ChannelFuture qui notifiera le succès, l'échec ou l'annulation de l'opération demandée.
Quand un serveur Netty accepte une nouvelle connexion, il crée un nouveau pipeline de canaux pour la traiter. Un pipeline de canaux n'est rien d'autre qu'une suite de gestionnaires de canaux qui contiennent la logique métier nécessaire au traitement de la requête.
Ces pipelines sont ensuite associés à un ensemble de services backend, qui gèrent automatiquement les dépendances transitives entre eux. Tout au long du workflow, aucun thread n'est en attente d'une entrée/sortie, ce qui permet de maximiser l'utilisation du CPU et de supporter un grand nombre de requêtes concurrentes. De plus, de nombreuses requêtes vers les services back-end peuvent être traitées en parallèle, ce qui réduit significativement la latence.
Gains de performance
La recherche Twitter est un des moteurs de recherche les plus utilisés au monde, plus d'un milliard de requêtes sont servies chaque jour. L'impact de Blender a été dramatique
À la suite du lancement de Blender, le 95ème percentile de latence a été divisé par 3, de 800ms il est passé à 250ms et la charge CPU sur nos serveurs front-end a été réduite de moitié. Nous avons à présent la capacité de servir 10 fois plus de requêtes par machine. Cela signifie que nous pouvons traiter le même nombre de requêtes sur moins de serveurs, ce qui réduite le coût de nos services de front-end.
Le typage statique: une aubaine de productivité
Alors que performance et scalabilité sont souvent citées comme des avantages des langages basés sur la JVM, Twitter a aussi trouvé des avantages dans le typage statique, au moins pour ses services de back-end. E. Weaver nous a raconté:
Je dirais que la moitié du gain de productivité est due à la dette technique accumulée dans le moteur de recherche de la pile Rails. L'autre moitié est que, pendant que la recherche passait sur une architecture orientée services et exposait diverses APIs, le typage statique est devenu très pratique pour assurer la cohérence de l'ensemble des systèmes. Vous pouvez garantir que le flux de données va plus ou moins bien fonctionner, et vous concentrer sur les aspects fonctionnels. Alors que pour quelque chose comme la réalisation d'une page web vous n'avez pas besoin de la recompiler en permanence, vous vous moquez de savoir si cette condition à la marge pourrait vous retourner un type différent de celui attendu. À mesure que nous passions sur une architecture orientée service léger, le typage statique est devenu un gain de productivité inespéré. Et Scala vous apporte le même avantage.
La possibilité d'itérer plus vite est évidemment crucial pour l'entreprise. Dans le cas de la recherche, l'équipe a ajouté le filtrage par pertinence et la personnalisation au site web Twitter pour améliorer la qualité des résultats, et ils ont aussi étendu les structures de données d'Earlybird pour supporter des recherches d'entités contenues dans les Tweets, comme les images et les vidéos. Ils travaillent maintenant sur la pertinence du filtrage sur mobile, et ils continuent de travailler à améliorer la scalabilité de l'infrastructure utilisée pour la recherche et la qualité de ces résultats.
Ruby MRI contre JRuby
Alors que dans bien des cas, il y avait de bonnes raisons pour que Twitter passe de Ruby à Java ou Scala, il y a certains services où Ruby reste le meilleur choix. Twitter est actuellement basé sur Ruby MRI (aussi appelé CRuby) en version 1.8, mais avec un garbage collector largement réécrit. Ils évaluent l'effort qu'il faudrait fournir pout passer à Ruby 1.9 et ré-implementer le garbage collector maison par rapport à l'effort nécessaire pour migrer vers JRuby ce code qu'il n'ont pas prévu de ré-encapsuler.
Le gros problème est que l'impact sur les performances est lié à la qualité des différents clients sur lesquels on s'appuie. Par exemple, notre client Memcached pour CRuby est extrêmement rapide. De ce que j'ai compris, les clients JRuby sont largement moins performants. Donc même si JRuby lui-même est deux fois plus rapide, passer sur un client Memcached plus lent annihilerait le gain de performance obtenu.
Pour pouvoir vraiment évaluer JRuby, Twitter devrait réécrire complètement son client Thrift, son client Memcached, et peut-être même son client MySQL, avant même de pouvoir espérer en tirer un quelconque bénéfice.
Ce n'est pas la faute de JRuby, c'est juste que pour le moment l'écosystème est toujours un peu immature. CRuby était comme ça aussi avant; mais nous avons tellement investi dedans que nous pouvons à présent en profiter, il faudrait faire de même avec JRuby.
Conclusion
Au cours des dernières années, la combinaison de Ruby on Rails et MySQL a été très populaire dans les start-up. Cela semble un choix raisonnable dans beaucoup de cas, cela permet aux ingénieurs d'essayer rapidement de nouvelles idées et de voir lesquelles trouvent des clients. Mais cela va de pair avec des coûts bien connus, à la fois en termes de performances et de scalabilité, dus peut-être à la maturité relative de la suite logicielle. De plus l'expérience Twitter suggère que la pile Ruby on Rails peut induire des problématiques d'architecture significatives lorsque le code grossit.
A propos de l'auteur
Charles Humble (@charleshumble sur twitter) est CTO pour PRPi Consulting avec la responsabilité globale de tous les développements spécifiques réalisés dans la société. Il travaille dans l’industrie logicielle depuis environ 15 ans en tant que développeur, architecte et manager. Il a aussi co-fondé Conissaunce, une entreprise anglaise de conseil informatique spécialisée dans le secteur de la grande distribution et de la finance, il reste directeur de cette firme. Il passe autant de temps que possible avec sa jeune famille et écrit de la musique avec twofish.