Comment prendre le contrôle d’un tas de 5 Go dans Haskell?

Actuellement, j’expérimente un petit serveur Web Haskell écrit en Snap qui charge et met à la disposition du client beaucoup de données. Et j’ai beaucoup de mal à prendre le contrôle du processus serveur. À des moments aléatoires, le processus utilise beaucoup de CPU pendant quelques secondes à quelques minutes et devient irresponsable aux demandes des clients. Parfois, l’utilisation de la mémoire augmente (et parfois diminue) des centaines de mégaoctets en quelques secondes.

J’espère que quelqu’un aura plus d’expérience avec les processus Haskell de longue durée qui utilisent beaucoup de mémoire et peuvent me donner des conseils pour rendre la chose plus stable. Je suis en train de déboguer le truc depuis des jours et je commence à devenir un peu désespéré ici.

Un petit aperçu de ma configuration:

  • Au démarrage du serveur, j’ai lu environ 5 gigaoctets de données dans une grande structure Data.Map-like (nestede) en mémoire. La carte nestede est une valeur ssortingcte et toutes les valeurs à l’intérieur de la carte sont des types de données dont tous les champs sont également ssortingcts. J’ai mis beaucoup de temps à faire en sorte qu’il ne rest plus de thunks non évalués. L’importation (en fonction de la charge de mon système) prend environ 5 à 30 minutes. Ce qui est étrange, c’est que la fluctuation des courses consécutives est beaucoup plus grande que ce à quoi je m’attendrais, mais c’est un problème différent.

  • La structure de données volumineuses se trouve dans un «TVar» partagé par tous les threads client générés par le serveur Snap. Les clients peuvent demander des parties arbitraires des données en utilisant un petit langage de requête. La quantité de demande de données est généralement petite (jusqu’à 300 Ko environ) et ne touche qu’une petite partie de la structure de données. Toutes les requêtes en lecture seule sont effectuées à l’aide d’un «readTVarIO», de sorte qu’elles ne nécessitent aucune transaction STM.

  • Le serveur est démarré avec les indicateurs suivants: + RTS -N -I0 -qg -qb. Cela démarre le serveur en mode multithread, désactive le temps d’inactivité et le GC parallèle. Cela semble accélérer le processus.

Le serveur fonctionne généralement sans aucun problème. Cependant, de temps en temps, une demande du client expire et le processeur atteint 100% (voire plus de 100%) et continue de le faire pendant longtemps. Pendant ce temps, le serveur ne répond plus à la demande.

Il y a peu de raisons pour lesquelles je peux penser à l’utilisation du processeur:

  • La demande prend beaucoup de temps car il y a beaucoup de travail à faire. C’est assez improbable, car cela arrive parfois pour les requêtes qui se sont avérées très rapides lors des précédentes exécutions (avec rapide je veux dire environ 20-80 ms).

  • Il rest des unités non évaluées à calculer avant que les données puissent être traitées et envoyées au client. Cela est également improbable, avec la même raison que le point précédent.

  • La récupération de la mémoire débute et commence à parsingr l’intégralité de mon segment de mémoire de 5 Go. Je peux imaginer que cela peut prendre beaucoup de temps.

Le problème est que je ne sais pas comment comprendre ce qui se passe exactement et que faire à ce sujet. Parce que le processus d’importation prend beaucoup de temps, les résultats de profilage ne me montrent rien d’utile. Il semble y avoir aucun moyen d’activer et de désactiver conditionnellement le profileur à partir du code.

Personnellement, je soupçonne que le GC est le problème ici. J’utilise GHC7 qui semble avoir beaucoup d’options pour modifier le fonctionnement du GC.

Quels parameters GC recommandez-vous lors de l’utilisation de gros tas avec des données généralement très stables?

Une grande quantité de mémoire et des pics de CPU occasionnels sont presque certainement le gâchis du GC. Vous pouvez voir si cela est effectivement le cas en utilisant des options RTS comme -B , ce qui fait que le GHC émet un bip statistiques après le fait (en particulier, voir si les temps de GC sont vraiment longs) ou -Dg , qui active les informations de débogage pour les appels GC (bien que vous deviez comstackr avec -debug ).

Vous pouvez faire plusieurs choses pour résoudre ce problème:

  • Lors de l’importation initiale des données, GHC perd beaucoup de temps à développer le tas. Vous pouvez lui demander de récupérer toute la mémoire dont vous avez besoin en spécifiant un grand -H .

  • Un gros tas avec des données stables sera promu à une ancienne génération. Si vous augmentez le nombre de générations avec -G , vous pourrez peut-être obtenir les données stables dans la génération la plus ancienne, très rarement GC’d, alors que vous avez les plus anciennes et les plus anciennes au-dessus.

  • En fonction de l’utilisation de la mémoire du rest de l’application, vous pouvez utiliser -F pour modifier le niveau de croissance de l’ancienne génération avant de le récupérer. Vous pourrez peut-être modifier ce paramètre pour supprimer cette opération.

  • S’il n’y a pas d’écritures et que vous avez une interface bien définie, il peut être intéressant de rendre cette mémoire non gérée par GHC (utilisez le C FFI) pour qu’il n’y ait aucune chance de super GC.

Ce sont toutes des spéculations, alors testez-les avec votre application particulière.

J’ai eu un problème très similaire avec un tas de cartes nestedes de 1,5 Go. Avec le GC inactif par défaut, j’obtiendrais 3 à 4 secondes de gel sur chaque GC, et avec le GC inactif (+ RTS -I0), j’obtiendrais 17 secondes de gel après quelques centaines de requêtes, provoquant un temps client -en dehors.

Ma “solution” a d’abord consisté à augmenter le délai d’attente du client et à demander aux utilisateurs de tolérer que 98% des requêtes atteignent environ 500 ms, mais qu’environ 2% des requêtes soient lentes. Cependant, voulant une meilleure solution, j’ai fini par exécuter deux serveurs à charge équilibrée et les avoir déconnectés du cluster pour effectuer des performancesGC toutes les 200 requêtes, puis de nouveau en action.

Ajouter l’insulte à la blessure, c’était une réécriture d’un programme Python original, qui n’a jamais eu de tels problèmes. En toute justice, nous avons obtenu une augmentation des performances d’environ 40%, une parallélisation facile et une base de code plus stable. Mais ce problème gênant GC …