13558 Go de rames


J’ai une tache celery qui me génère des données de test. Elle est lancée toutes les 5 minutes pour simuler le crawling d’un site qui popule une base de données, le tout piloté par l’ORM django, puis dumpé dans redis.

Jusqu’ici tout va bien.

Mais après un certain temps, ma machine rame, puis se frise.

Et j’ai du mal à y croire. J’ai 8 coeurs, un 32 Go de mémoire vive, un SSD d’un putain de To. On va pas me la faire à l’envers, c’est pas un def tout moisi qui va me faire trembler les genoux. C’est forcément un de ces cons de wallets que j’ai laissé ouvert, encore codé par un nantais ça !

Mais après une enquête minutieuse, qui a consisté en subtilement killer tous mes processus un par un avec echo "douceur" | sed s/c/l, le constat est là.

Eukekaca

Eukekaca.

Cette fonction est responsable:

def generate_fake_stats(x=1000):
 
    cur_stats = {
        s.currency.short_code: s
        for s in CurrencyMarketStatsFactory.build_batch(x)
    }
 
    mn_stats: Dict[str, MNMarketStats] = {
        s.masternode.coin.short_code: s
        for s in MNMarketStatsFactory.build_batch(x)
    }
 
    stats = []
 
    for code, mns in mn_stats.items():
 
        cstats: CurrencyMarketStats = cur_stats.get(code)
 
        if not cstats:
            continue
 
        dollar_value = float(cstats.dollar_value),
        collateral = mns.masternode.collateral
 
        stats = {
            'created': int(mns.created.timestamp()),
            'name': cstats.currency.name,
            'code': code,
            'title': f'{cstats.currency.name} ({code})',
            'marketcap': dollar_value * cstats.supply,
            'dollar_value': dollar_value,
            'change_rate': cstats.change_rate,
            'volume': float(cstats.volume),
            'supply': cstats.supply,
            'roi': mns.roi,
            'mn_worth': collateral * dollar_value,
            'node_count': mns.node_count,
            'required_coins': collateral,
        }
 
    RedisClient.get_instance().jset('marketstats', stats)
    return stats

Il m’a fallu une bonne heure pour trouver ma connerie. J’ai changé plein d’options, mis DEBUG sur False, limité la mémoire de Redis, etc.

Mais non, c’était mon code. Qui générait au bas mot 847390982*1000*16 octets de données, soit 1,355825571×10¹³ pour mes objets Python.

J'ai un nouveau mapping clavier qui permet de n'utiliser qu'un doigt

J’ai un nouveau mapping clavier qui permet de n’utiliser qu’un doigt

Il y en a un peu plus madame, je vous le mets quand même ?

Nan parce que 13 To pour une pov liste de dicos, c’est la boucherie.

Alors, sachant que je vous ai éliminé la recherche des causes parallèles et que vous savez que le bug est de ma (grande et stupide) faute, saurez-vous trouver dans ces lignes le d20 qui tombe sur un 1 à tous les jets ?

Si vous ne trouvez pas, la réponse dans quelques jours.

Explain all the humans !

Explain all the humans !

EDIT pour la réponse:

Comme plusieurs personnes l’ont compris, c’est la virgule sur:

    dollar_value = float(cstats.dollar_value),

Qui est responsable de tout ce malheur.

En effet plus loin on fait:

    'mn_worth': collateral * dollar_value,

Ce qui, au lieu de multiplier un entier par un float, multiplie un entier par un tuple. En python, c’est légal, et ça donne ça:

>>> 10 * (7808979.8989,)
(7808979.8989, 7808979.8989, 7808979.8989, 7808979.8989, 7808979.8989, 7808979.8989, 7808979.8989, 7808979.8989, 7808979.8989, 7808979.8989)

Si collateral est élevé, ce qui est ici mon cas, ça fait de très gros tuples, et le tout dans une boucle.

25 thoughts on “13558 Go de rames

  • Brice

    Y’a quelque chose de bizarre, et d’autres que j’aurais probablement pas écrits comme ça, mais j’imagine pas que ces choses stockent trop en mémoire, au contraire même.

    On déclare une liste vide (stats), puis on reassigne un nouveau dict à stats à chaque passage dans la boucle (ce qui devrait vider son précédent contenu et libérer la mémoire associée). Donc on devrait utiliser relativement peu, puisqu’on stocke seulement les stats du dernier passage, à tout instant. Est-ce que le but n’était pas plus de faire un stats.append({…})? Sinon, pourquoi itérer sur tous les items du dict si on ne se sert que du dernier élément valide? Mais ça résoud pas le problème initial, au contraire: stats devrait donc devenir 1000 fois plus gros qu’aujourd’hui si le but était de récupérer les stats de toutes les itérations (à éviter donc, non?).

    À part ça, je me demande si c’est pas la sérialisation de stats à la fin qui consomme, avec peut-être des références circulaires ou ce genre de blagues.

    Ou quelque chose qui n’a rien à voir, dur à dire avec un simple lecture!

  • =°.°=

    ché po.

    Vite fait là, tu réinstancies stats, qui est en fait un dict. C’est lié ?

  • lulu la nantaise

    Au fait Sam, les Nantais t’enculent, ça te fera des trucs à raconter dans ta rubrique “Cul” de gros mytho. La prochaine fois que tu fais un code de merde comme celui-là, surtout garde-le pour toi ça m’évitera de me mettre de mauvaise humeur le matin.

  • Pouet

    Non, à mon avis il a juste oublié le append quand il a formaté le code. C’est pas de là que vient l’erreur.

    Par contre :

    dollar_value = float(cstats.dollar_value),

    C’est quoi cette virgule à la fin, hein ? :)

    Et après :

    'mn_worth': collateral * dollar_value,

    Boom, tu multiplies pas un float mais un tuple, et tu pètes ta ram.

    J’ai gagné quoi ?

  • David CHANIAL

    rien à voir, mais est-ce volontaire de définir dollar_value autrement qu’un “simple” float ? (‘,’ à la fin de la ligne)

  • utopman
    import q
    q(stats)
    
    RedisClient.get_instance().jset('marketstats', stats)
    

    Il est apellé souvent ton code ?

    et avec quelle quantité de données à chaque fois ?

    c’est un générateur ta fonction ?

    import q
    q(len(MNMarketStatsFactory.build_batch(x))
     mn_stats: Dict[str, MNMarketStats] = {
            s.masternode.coin.short_code: s
            for s in MNMarketStatsFactory.build_batch(x)
        }
    

    Ca fait quelle taille en moyenne ca ?

  • David CHANIAL

    et par ailleurs, tu fais passer stats de list à dictionnaire dès la première itération, en l’écrasant ensuite par un nouveau dictionnaire à chaque itération, au lieu d’append ce dictionnaire à la list.

    Mais ça n’explique pas la consommation mémoire…

  • David CHANIAL

    Ahah :

    'mn_worth': collateral * dollar_value,

    dollar_value n’étant pas un float mais un iterable….

    la multiplication fait mal non ?

  • YvesD

    Je ne comprends pas ces lignes: suis- je neuneu ?

    mn_stats: Dict[str, MNMarketStats] = {

    ….

    }.

    cstats: CurrencyMarketStats = cur_stats.get(code)

    etiquette (?): instruction C’est du celery ?

  • pouet

    @YvesD

    Il s’agit des type hints, dans python depuis 3.5

    Pourquoi les commentaires apparaissent au bout d’une heure ? Quand j’ai écrit mon précédent y’avait que deux autres commentaires, et là je les vois tous apparaitre un à un.

  • ZZ

    mn_stats: Dict[str, MNMarketStats] = … est une annotation de variable (https://www.python.org/dev/peps/pep-0526)

    Il n’y aurait pas un problème d’indentation dans le code ?

    En effet, la boucle for construit le dictionnaire stats (presque) à chaque itération mais au final on pousse dans redis que le dernier stats.

  • Romain

    Bien vu David pour le tuple caché !

    Sam, j’imagine que tu voulais faire un stats.append({ dans la boucle plutôt que de redéfinir la variable. Ce qui aurait fait encore plus de données \o/

  • Brice

    @David: Bien vu pour la virgule !

    J’imagine que t’as mis le doigt sur le pb.

  • Loïc

    Je parie sur une de ces multiplications :

    ‘mn_worth’: collateral * dollar_value,

    ou

    ‘marketcap’: dollar_value * cstats.supply,

  • hljd

    Il n’empêche, c’est pas normal qu’en 2018 n’importe quelle machine linux se freeze et devienne unresponsive dès qu’un processus réclame trop de mémoire, au lieu de juste le buter en bonne et due forme. Je sais que blablabla c’est compliqué à cause du swap et l’OOM tout ça mais bon, si tout le monde dans les milieux tech se branle collectivement pour envoyer des Tesla sans conducteurs sur Mars payées en etherium via une architecture de microservices serverless de mes couilles, y a aucune excuse pour ne pas faire en sorte que le kernel le plus utilisé au monde trouve un moyen de dire au processus trublion qui essaie de créer un trilliard d’objets “non c’est pas possible connard, ta gueule et crashe” au lieu de “ok cool pas de problème, reviens dans 3 mois le temps que je swappe tout ça, par contre t’as plus de clavier lol”.

    En attendant une meilleure solution j’utilise thrash-protect en tache de fond, c’est un peu moche mais ça marche.

  • Sam Post author

    @pouet: parce qu’on valide les commentaires manuellement quand notre anti-spam les marquent comme suspects.

    @Brice : c’est juste que j’avais éliminé ça dans mon débugage, ça vous évite de poser la question. Bien entendu que dans dans le vrais code y a un append.

    @David CHANIAL @Pouet et @Loïc ont juste :) la petite virgule de merde cause effectivement la création d’un tuple, et Python multiplie tout ça pour en créer un énorme.

    Time to update.

  • Morgotth

    D’où la preuve qu’avec des langages dynamiques les tests sont encore plus nécessaire, car Python permet de remplacer un int/float par tout, meme un tuple.

  • YvesD

    Merci à Pouet, ZZ, Ecolpe pour l’explication sur les annotations

    Pas encore l’habitude au niveau des variables

  • Pouet

    @Sam

    Ah, ça doit être à cause de mon adresse email j’imagine ? Désolé !

    Tant que j’y suis, histoire que tu aies pas à valider ce message pour rien, je viens de choper mon premier job de dev python, et c’est en partie grâce à ton blog (bisous à Max aussi) que je suis depuis quelques années. Si tu as un truc autre que le bitcoin pour te payer une ou deux bières / lait fraise, je prends ! (sinon donne-moi une org ou un projet que tu aimes bien, je leur fais un don)

  • Sam Post author

    @Pouet : cherche “les dons du mois” sur le blog, et fait une donation à n’importes lequel des sujets des articles.

  • Lucas-C

    Pour info, pylint l’aurait détecté ;)

    $ pylint sametmax.py
    R: 22, 0: Disallow trailing comma tuple (trailing-comma-tuple)
    
  • Sam Post author

    En effet, mais je ne lance pas pylint à la main, et j’ai été victime de ce bug : https://github.com/DonJayamanne/pythonVSCode/issues/798

    Le seul moyen de voir l’erreur est une toute petite ligne rouge en première ligne du fichier. Comme pylint et mypy marchait, ça m’a pris plusieurs jours pour m’en apercevoir.

    Moralité, l’outillage c’est bien, mais avoir un cerveau alerte reste indispensable.

Comments are closed.

Des questions Python sans rapport avec l'article ? Posez-les sur IndexError.