Comment améliorer significativement les performances Java?

23

L'équipe du LMAX a présenté comment elle a réussi à faire 100 000 TPS avec moins de 1 ms de latence . Ils ont soutenu cette présentation avec un blog , un document technique (PDF) et le code source lui-même.

Récemment, Martin Fowler a publié un excellent article sur l'architecture LMAX et mentionne qu'ils sont maintenant capables de gérer six millions de commandes par seconde et met en évidence quelques-unes des étapes que l'équipe a prises pour augmenter un autre ordre de grandeur dans les performances.

Jusqu'à présent, j'ai expliqué que la clé de la vitesse du Business Logic Processor est de tout faire séquentiellement, en mémoire. Le fait de faire cela (et rien de vraiment stupide) permet aux développeurs d'écrire du code capable de traiter le TPS 10K.

Ils ont ensuite découvert que se concentrer sur les éléments simples d'un bon code pourrait faire remonter cela dans la gamme 100K TPS. Cela nécessite simplement un code bien factorisé et de petites méthodes - cela permet essentiellement à Hotspot de faire un meilleur travail d'optimisation et aux CPU d'être plus efficaces dans la mise en cache du code pendant son exécution.

Il a fallu un peu plus d'intelligence pour remonter d'un autre ordre de grandeur. Il y a plusieurs choses que l'équipe LMAX a trouvées utiles pour y arriver. L'une consistait à écrire des implémentations personnalisées des collections Java conçues pour être compatibles avec le cache et soigneuses avec les déchets.

Une autre technique pour atteindre ce niveau supérieur de performances consiste à mettre l'accent sur les tests de performances. J'ai longtemps remarqué que les gens parlent beaucoup de techniques pour améliorer les performances, mais la seule chose qui fait vraiment la différence est de les tester

Fowler a mentionné qu'il y avait plusieurs choses qui ont été trouvées, mais il n'en a mentionné que quelques-unes.

Y a-t-il d'autres architectures, bibliothèques, techniques ou "choses" qui sont utiles pour atteindre de tels niveaux de performances?

Dakotah North
la source
11
"Quelles autres architectures, bibliothèques, techniques ou" choses "sont utiles pour atteindre de tels niveaux de performances?" Pourquoi demander? Cette citation est la liste définitive. Il y a beaucoup, beaucoup d'autres choses, dont aucune n'a les effets aimables des éléments de cette liste. Tout ce que n'importe qui d'autre peut nommer ne sera pas aussi utile que cette liste. Pourquoi demander de mauvaises idées lorsque vous avez cité l'une des meilleures listes d'optimisation jamais produites?
S.Lott
Ce serait bien d'apprendre quels outils ils ont utilisés pour voir comment le code généré fonctionnait sur le système.
1
J'ai entendu parler de gens jurant par toutes sortes de techniques. Ce que j'ai trouvé le plus efficace, c'est le profilage au niveau du système. Il peut vous montrer des goulots d'étranglement dans la façon dont votre programme et votre charge de travail exercent le système. Je suggérerais d'adhérer à des directives bien connues concernant les performances et d'écrire du code modulaire afin que vous puissiez facilement le régler plus tard ... Je ne pense pas que vous puissiez vous tromper avec le profilage du système.
ritesh

Réponses:

21

Il existe toutes sortes de techniques pour le traitement des transactions hautes performances et celle de l'article de Fowler n'est qu'une des nombreuses à la pointe. Plutôt que d'énumérer un tas de techniques qui peuvent ou non s'appliquer à la situation de quiconque, je pense qu'il vaut mieux discuter des principes de base et de la façon dont LMAX aborde un grand nombre d'entre eux.

Pour un système de traitement des transactions à grande échelle, vous souhaitez effectuer toutes les opérations suivantes autant que possible:

  1. Minimisez le temps passé dans les niveaux de stockage les plus lents. Du plus rapide au plus lent sur un serveur moderne, vous avez: CPU / L1 -> L2 -> L3 -> RAM -> Disque / LAN -> WAN. Le passage du disque magnétique moderne même le plus rapide à la RAM la plus lente est supérieur à 1000x pour un accès séquentiel ; l'accès aléatoire est encore pire.

  2. Minimisez ou éliminez le temps passé à attendre . Cela signifie partager le moins d'état possible et, si l'état doit être partagé, éviter autant que possible les verrous explicites.

  3. Répartissez la charge de travail. Les processeurs ne sont pas devenus beaucoup plus rapides au cours des dernières années, mais ils sont devenus plus petits et 8 cœurs sont assez courants sur un serveur. Au-delà de cela, vous pouvez même répartir le travail sur plusieurs machines, ce qui est l'approche de Google; la grande chose à ce sujet est qu'il met à l'échelle tout, y compris les E / S.

Selon Fowler, LMAX adopte l'approche suivante pour chacun d'eux:

  1. Gardez tous les états en mémoire à tout moment. La plupart des moteurs de base de données le feront de toute façon, si la base de données entière peut tenir en mémoire, mais ils ne veulent rien laisser au hasard, ce qui est compréhensible sur une plateforme de trading en temps réel. Afin de réussir cela sans ajouter une tonne de risques, ils ont dû créer un tas d'infrastructures de sauvegarde et de basculement légères.

  2. Utilisez une file d'attente sans verrou ("perturbateur") pour le flux d'événements d'entrée. Contrairement aux files d'attente de messages traditionnelles traditionnelles qui ne sont définitivement pas verrouillées, et impliquent en fait généralement des transactions distribuées extrêmement lentes .

  3. Pas tant. LMAX jette celui-ci sous le bus sur la base que les charges de travail sont interdépendantes; le résultat de l'un modifie les paramètres des autres. Il s'agit d'une mise en garde critique , et que Fowler appelle explicitement. Ils font une utilisation afin de la concurrence pour fournir des capacités de basculement, mais toute la logique métier est traité sur un seul thread .

LMAX n'est pas la seule approche de l'OLTP à grande échelle. Et bien qu'il soit assez brillant en soi, vous n'avez pas besoin d'utiliser des techniques de pointe pour atteindre ce niveau de performance.

De tous les principes ci-dessus, le n ° 3 est probablement le plus important et le plus efficace, car, franchement, le matériel est bon marché. Si vous pouvez correctement partitionner la charge de travail sur une demi-douzaine de cœurs et plusieurs dizaines de machines, alors le ciel est la limite pour les techniques conventionnelles de calcul parallèle . Vous seriez surpris du débit que vous pouvez retirer avec rien d'autre qu'un tas de files d'attente de messages et un distributeur à tour de rôle. Ce n'est évidemment pas aussi efficace que LMAX - en fait même pas proche - mais le débit, la latence et la rentabilité sont des préoccupations distinctes, et ici nous parlons spécifiquement de débit.

Si vous avez le même type de besoins spéciaux que LMAX - en particulier, un état partagé qui correspond à une réalité commerciale par opposition à un choix de conception hâtif - alors je suggère d'essayer leur composant, car je n'ai pas vu beaucoup sinon cela est adapté à ces exigences. Mais si nous parlons simplement de haute évolutivité, je vous exhorte à faire plus de recherches sur les systèmes distribués, car ils sont l'approche canonique utilisée par la plupart des organisations aujourd'hui (Hadoop et projets connexes, ESB et architectures connexes, CQRS, que Fowler a également mentions, etc.).

Les SSD vont également changer la donne; sans doute, ils le sont déjà. Vous pouvez maintenant avoir un stockage permanent avec des temps d'accès similaires à la RAM, et bien que les SSD de qualité serveur soient toujours horriblement chers, ils finiront par baisser de prix une fois que les taux d'adoption augmenteront. Il a fait l'objet de recherches approfondies et les résultats sont assez époustouflants et ne feront que s'améliorer avec le temps, de sorte que l'ensemble du concept de "garder tout en mémoire" est beaucoup moins important qu'auparavant. Donc, une fois de plus, j'essaierais de me concentrer sur la concurrence dans la mesure du possible.

Aaronaught
la source
Discuter des principes est des principes sous-jacents est génial et votre commentaire est excellent et ... à moins que l'article de Fowler n'ait pas eu de référence dans une note de bas de page pour mettre en cache les algorithmes inconscients en.wikipedia.org/wiki/Cache-oblivious_algorithm (qui s'intègre parfaitement dans catégorie 1 que vous avez ci-dessus) je ne serais jamais tombé dessus. Donc ... en ce qui concerne chaque catégorie que vous avez ci-dessus, connaissez-vous les 3 principales choses qu'une personne devrait savoir?
Dakotah North
@Dakotah: Je ne commencerais même pas à m'inquiéter de la localisation du cache à moins que et jusqu'à ce que j'aie complètement éliminé les E / S de disque, où la grande majorité du temps est consacrée à attendre dans la grande majorité des applications. En dehors de cela, que voulez-vous dire par "les 3 principales choses qu'une personne devrait savoir"? Top 3 quoi, savoir quoi?
Aaronaught
Le saut de la latence d'accès à la RAM (~ 10 ^ -9s) à la latence du disque magnétique (~ 10 ^ -3s dans le cas moyen) est un autre ordre de grandeur supérieur à 1000x. Même les SSD ont encore des temps d'accès mesurés en centaines de microsecondes.
Sedate Alien
@Sedate: Latence oui, mais c'est plus une question de débit que de latence brute, et une fois que vous avez passé les temps d'accès et atteint la vitesse de transfert totale, les disques ne sont pas si mauvais. C'est pourquoi j'ai fait la distinction entre l'accès aléatoire et séquentiel; pour les scénarios d'accès aléatoires , il ne surtout devenir un problème de latence.
Aaronaught
@Aaronaught: En relisant, je suppose que vous avez raison. Peut-être faudrait-il souligner que tous les accès aux données doivent être aussi séquentiels que possible; des avantages significatifs peuvent également être obtenus lors de l'accès aux données dans l'ordre dans la RAM.
Sedate Alien
10

Je pense que la plus grande leçon à tirer de cela est que vous devez commencer par les bases:

  • Bons algorithmes, structures de données appropriées et ne rien faire de "vraiment stupide"
  • Code bien factorisé
  • Test de performance

Pendant les tests de performances, vous profilez votre code, trouvez les goulots d'étranglement et corrigez-les un par un.

Trop de gens sautent directement à la partie «réparez-les un par un». Ils passent beaucoup de temps à écrire "des implémentations personnalisées des collections java", car ils savent juste que la raison pour laquelle leur système est lent est à cause des échecs de cache. Cela peut être un facteur contributif, mais si vous passez directement au réglage de code de bas niveau comme ça, vous risquez de manquer le plus gros problème de l'utilisation d'une ArrayList lorsque vous devriez utiliser une LinkedList, ou que la vraie raison pour laquelle votre système est lent parce que votre ORM charge paresseusement les enfants d'une entité et effectue ainsi 400 voyages distincts dans la base de données pour chaque demande.

Adam Jaskiewicz
la source
7

Je ne commenterai pas particulièrement le code LMAX car je pense qu'il est amplement décrit, mais voici quelques exemples de choses que j'ai faites qui ont entraîné des améliorations de performances mesurables significatives.

Comme toujours, ce sont des techniques qui doivent être appliquées une fois que vous savez que vous avez un problème et que vous devez améliorer les performances - sinon vous risquez juste de faire une optimisation prématurée.

  • Utilisez la bonne structure de données et créez-en une personnalisée si nécessaire - une conception de structure de données correcte éclipse l'amélioration que vous obtiendrez jamais des micro-optimisations, alors faites-le d'abord. Si votre algorithme dépend des performances de nombreuses lectures à accès aléatoire O (1) rapides, assurez-vous que vous disposez d'une structure de données qui le prend en charge! Cela vaut la peine de sauter à travers certains cercles pour obtenir ce droit, par exemple trouver un moyen de représenter vos données dans un tableau pour exploiter des lectures indexées O (1) très rapides.
  • Le processeur est plus rapide que l'accès à la mémoire - vous pouvez faire beaucoup de calculs dans le temps nécessaire pour lire une mémoire aléatoire si la mémoire n'est pas dans le cache L1 / L2. Cela vaut généralement la peine de faire un calcul s'il vous permet d'économiser une lecture en mémoire.
  • Aidez le compilateur JIT avec les champs, méthodes et classes de finalisation final permet des optimisations spécifiques qui aident vraiment le compilateur JIT. Exemples spécifiques:

    • Le compilateur peut supposer qu'une classe finale n'a pas de sous-classes, il peut donc transformer les appels de méthode virtuelle en appels de méthode statique
    • Le compilateur peut traiter un champ final statique comme une constante pour une amélioration des performances agréable, surtout si la constante est ensuite utilisée dans des calculs qui peuvent être calculés au moment de la compilation.
    • Si un champ contenant un objet Java est initialisé comme final, l'optimiseur peut éliminer à la fois la vérification nulle et l'envoi de méthode virtuelle. Agréable.
  • Remplacez les classes de collection par des tableaux - cela se traduit par du code moins lisible et est plus difficile à maintenir, mais est presque toujours plus rapide car il supprime une couche d'indirection et bénéficie de nombreuses optimisations intéressantes pour l'accès aux tableaux. Habituellement, une bonne idée dans les boucles internes / le code sensible aux performances après l'avoir identifié comme un goulot d'étranglement, mais évitez le contraire pour des raisons de lisibilité!

  • Utilisez des primitives dans la mesure du possible - les primitives sont fondamentalement plus rapides que leurs équivalents basés sur des objets. En particulier, la boxe ajoute une énorme quantité de frais généraux et peut provoquer des pauses GC désagréables. Ne laissez pas de primitives encadrées si vous vous souciez des performances / latence.

  • Minimisez le verrouillage de bas niveau - les verrous sont très chers à bas niveau. Trouvez des moyens d'éviter de verrouiller complètement ou de verrouiller à un niveau grossier afin que vous n'ayez besoin de verrouiller que rarement sur de gros blocs de données et que le code de bas niveau puisse continuer sans avoir à vous soucier des problèmes de verrouillage ou de concurrence.

  • Évitez d'allouer de la mémoire - cela pourrait en fait vous ralentir dans l'ensemble, car la récupération de place JVM est incroyablement efficace, mais est très utile si vous essayez d'obtenir une latence extrêmement faible et que vous devez minimiser les pauses du GC. Il existe des structures de données spéciales que vous pouvez utiliser pour éviter les allocations - la bibliothèque http://javolution.org/ en particulier est excellente et remarquable pour celles-ci.
mikera
la source
Je ne suis pas d'accord pour que les méthodes soient définitives . Le JIT est capable de comprendre qu'une méthode n'est jamais remplacée. De plus, si une sous-classe est chargée plus tard, elle peut annuler l'optimisation. Notez également que "éviter d'allouer de la mémoire" peut également rendre le travail du GC plus difficile et donc vous ralentir - utilisez donc avec prudence.
maaartinus
@maaartinus: en ce qui concerne finalcertains JIT, cela pourrait le comprendre, d'autres non. Cela dépend de l'implémentation (tout comme de nombreux conseils d'optimisation des performances). D'accord sur les allocations - vous devez comparer cela. Habituellement, j'ai trouvé qu'il était préférable d'éliminer les allocations, mais YMMV.
mikera
4

Autre que déjà indiqué dans une excellente réponse d'Aaronaught, je voudrais noter qu'un code comme celui-ci pourrait être assez difficile à développer, à comprendre et à déboguer. "Bien que très efficace ... il est très facile de foirer ..." comme l'a mentionné l'un de leurs gars sur le blog LMAX .

  • Pour un développeur habitué aux requêtes et verrous traditionnels , coder pour une nouvelle approche pourrait ressembler à monter à cheval. C'était du moins ma propre expérience en expérimentant avec Phaser, concept qui est mentionné dans le document technique LMAX. En ce sens, je dirais que cette approche échange la contention de verrouillage pour la contention du cerveau du développeur .

Étant donné ci-dessus, je pense que ceux qui choisissent Disruptor et des approches similaires s'assurent mieux de disposer de ressources de développement suffisantes pour maintenir leur solution.

Dans l'ensemble, l'approche Disruptor me semble assez prometteuse. Même si votre entreprise ne peut pas se permettre de l'utiliser, par exemple pour les raisons mentionnées ci-dessus, pensez à convaincre votre direction "d'investir" un peu d'efforts pour l'étudier (et SEDA en général) - parce que si ce n'est pas le cas, il y a une chance qu'un jour leurs clients les laisseront en faveur d'une solution plus compétitive nécessitant 4x, 8x etc moins de serveurs.

moucheron
la source