gain de memoire avec Python

Le problème exposé dans ce sujet a été résolu.

Bonjour

j’ai un script python qui fait un job il mets en parallèle des appels réseaux

les appels réseaux sont aussi en python et il y a un traitement dessus (leger et rapide) Toutefois, il demande pas mal de ressource (je fais beaucoup de thread (vive les pools) comme les appels réseaux sont long (1 a 8s)),

je me demandais, si je fais la partie "appel réseaux, et traitement" en C puis je l’interface avec python pour la gestion de la pool je vais gagner de la ram, CPU ? ou pour espérer ça, faut que je fasse aussi la partie "gestion thread" (donc refaire de zéro ce que j’ai fait)

Merci d’avance

Faire un appel réseau, ça consiste surtout à… attendre. Nul besoin de performance brute de calcul pour cela, comme l’on pourrait aisément en convenir. Je ne suis donc pas convaincu que l’écriture d’un module externe en C puisse t’être d’une quelconque aide en l’occurrence.

Ajoutons à cela que le fait de lancer beaucoup de threads OS implique un coût d’overhead (RAM et CPU) qui devient de plus en plus non négligeable. En C ou en Python, il s’agit du même problème qui est propre à l’OS lui-même. En effet, plus il y a de threads, plus l’OS chargé de leur gestion doit fournir de travail.

Une application qui lance une dizaine de threads ce n’est pas gênant en pratique. Combien de threads lances-tu ?

Tu as, je pense, trois solutions :

  • les thread pool que tu utilises déjà : une bonne solution mais ce n’est pas fait pour lancer 1000 threads (est-ce ton cas ?)
  • utiliser du Python asynchrone (async)
  • utiliser gevent sur du Python non asynchrone
+0 -0

comme je l’ai dit c’est pas tant pour des performance, mais c’est surtout l’attente, pour être tranquille, je dois être a 30 threads (ça permet a l’OS de gérer ses affaire a coté) et ça prends 15 min mais demain je vais avoir beaucoup de thread a faire tourner (pas forcement en même temps :), a la rigueur l’async pourrait être une solution, je remplace la partie "synchrone" par un verrou gevent ? je connais pas

merci ^^

edit: c’est surtout pour le gain de memoire et de ram ce qui permettra plus de thread donc plus de vitesse

+0 -0

comme je l’ai dit c’est pas tant pour des performance, mais c’est surtout l’attente, pour être tranquille,

On est d’accord là-dessus.

je dois être a 30 threads (ça permet a l’OS de gérer ses affaire a coté) et ça prends 15 min mais demain je vais avoir beaucoup de thread a faire tourner (pas forcement en même temps :)

Je t’avoue que j’ai du mal à voir. Tu pourrais me montrer le code (même épuré) que tu as pour le moment ?

Tu as combien d’URL à aller chercher ? Si tu en as énormément, même avec un pool d’exécution de 30 threads parallèles, ça va prendre du temps, en effet.

a la rigueur l’async pourrait être une solution, je remplace la partie "synchrone" par un verrou gevent ? je connais pas

Effectivement, l’asynchrone permet de résoudre ce genre de problème, surtout quand on s’attend à faire beaucoup d’appels externes avec une latence élevée comme en l’occurrence. Le Python asynchrone (Python 3.5+) peut le faire nativement, ou via une lib externe (gevent).

Le modèle de pool est implémentable avec gevent. Voici ce qu’en dit leurs docs :

A pool is a structure designed for handling dynamic numbers of greenlets which need to be concurrency-limited. This is often desirable in cases where one wants to do many network or IO bound tasks in parallel.

Je pense qu’on est dans le thème ;)

Avec le Python asynchrone, avec asyncio.gather on ferait ça :

async def get(url):
    data = await http_get(url)
    do_something(data)

async def process_all_urls():
    await asyncio.gather(
        get(url_1),
        get(url_2),
        ...
        get(url_n)
    )

Si tu as vraiment beaucoup d’URL, le modèle de pool reste quand même plus intéressant, je pense. En effet, « lancer » des milliers de coroutines asynchrones dans un gather n’est peut-être pas une bonne idée (je n’en ai jamais fait l’expérience réelle, c’est une supposition).

Avec les pool, tu peux garder cette idée de faire paquet par paquet, et ainsi mieux gérer les ressources pour éviter de les saturer. Si tu utilises asyncio.gather, tu peux gérer ça à la main avec un boucle :

async def process_all_urls(all_urls):
    for chunk in chunked(all_urls):
        await asyncio.gather(*[get(u) for u in chunk])

Dans mon exemple, on admet que chunked serait une fonction qui retourne une liste séparée tous les n éléments (par exemple 100, comme ici) :

all_urls = [u1, u2, u3, u4, ..., u1000]
chunked(all_urls) = [
    [u1, ..., u100],
    [u101, ..., u200],
    ...,
    [u901, ..., u1000],
]

Et ainsi on traiterait les appels 100 par 100 sur un total du 1000, par exemple.

+0 -0

je reviens ce soir je donne juste l’exemple de code

from Chassis import *
from conf import GetListRouteur
from GestionFichier import GestionFichier
import os

from multiprocessing import Pool, TimeoutError
from os import listdir
from os.path import isfile, join

commandesAFaire = ['Commande 1', 'Commande 1']
data_dir = os.getcwd()+'\\temp\\'
NombreThread = 10 # sur mon pc il est plus faible car il est pas ouf coté perf


all_File = [f for f in os.listdir(data_dir) if isfile(join(data_dir, f))]

def main():
    # Gestion de tout les routeurs
    t = GetListRouteur(user='USER',password='MDP') # Retourne un tableau
    pool = Pool(processes=NombreThread)
    pool.map(Action, t)

    # Reuni tout les txt de temp dans 'fini.txt'
    out = []
    for fil in all_File:
        with open (data_dir + fil, 'r') as myfile:
            data=myfile.readlines()
            out.append(fil + '\n' + ''.join(data) )
    GestionFichier.WriteFile("fini.txt", '\n\n\n\n'.join(out))

def Action(srv):
    print('---- {} en cours '.format(srv['ip']))
    fileName = data_dir + srv['ip']
    retourScript = Chassis.getInfo(srv, commandesAFaire) # traitement SSH faite
    GestionFichier.WriteFile(fileName, retourScript)


if __name__== '__main__':
  main()

t est un tableau contenant

{
    'device_type': 'cisco_ios',
    'ip':  x,
    'username': user,
    'password': password
}

avec autant d’IP qu’indiquer

il s’agit d’un ancien script, celui que j’utilise est un similaire dans l’archi mais utilise le SNMP et non le SSH) et le traitement consiste a découvrir les liens entre routeur

dans le script (au dessus) je suis a 150 requêtes a faire (donc ca prend 15 a 25 min c’est correct pour du facilement modifiable), mais le code lui sera a terme vers 600 (et doit écrire dans une base SQlitle (qui étrangle aussi un peu (mais async pourrait corriger via le verrou))

faut que je tente une réécriture avec async, voir ce que ça donne

+0 -0

RE ^^

edit : je viens de voir que j’utilisais "multiprocessing pool" et non Threadpool et les perfs ont rien a voir…

je laisse ce que j’avais ecrit avant : (mais demain je tenterais en async et en Threadpool

j’ai ecrit un test rapidement (j’ai pas tester) en gardant l’esprit de la pool (via Semaphore https://docs.python.org/3/library/asyncio-sync.html#asyncio.Semaphore ca permet de faire un truc assez propre juste en rajoutant un peu de code (je trouve)) basé sur https://stackoverflow.com/questions/48483348/how-to-limit-concurrency-with-python-asyncio

voici le résultât (non tester car j’ai pas acces au machine test du boulot)

import os
from os import listdir
from os.path import isfile, join
import asyncio
from random import randint

from Chassis import *
from conf import GetListRouteur
from GestionFichier import GestionFichier



commandesAFaire = ['Commande 1', 'Commande 1']
USER = 'USER'
MDP = 'MDP'
NombreThread = 10
data_dir = os.getcwd()+'\\temp\\'


sem = asyncio.Semaphore(NombreThread)
async def SSH(code):
   print('---- {} en cours '.format(srv['ip']))
   fileName = data_dir + srv['ip']
   retourScript = Chassis.getInfo(srv, commandesAFaire) # traitement SSH faite
   GestionFichier.WriteFile(fileName, retourScript)

async def safe_SSH(i):
   async with sem:  # semaphore limits num of simultaneous downloads
       return await SSH(i)

async def main():
   t = GetListRouteur(user=USER,password=MDP) # Retourne un tableau
   tasks = [
       asyncio.ensure_future(safe_SSH(i))  # creating task starts coroutine
       for i
       in t
   ]
   await asyncio.gather(*tasks)  # await moment all downloads done
   for fil in all_File:
       with open (data_dir + fil, 'r') as myfile:
           data=myfile.readlines()
           out.append(fil + '\n' + ''.join(data) )
   GestionFichier.WriteFile("fini.txt", '\n\n\n\n'.join(out))


if __name__ ==  '__main__':
   loop = asyncio.get_event_loop()
   try:
       loop.run_until_complete(main())
   finally:
       loop.run_until_complete(loop.shutdown_asyncgens())
       loop.close()

il me reste plus qu’a faire un bench, pour voir si le CPU et la ram vont mieux

(toujours en wait random) mais sinon (pour 150 en même temps) via async il mange 16Mo, via multiprocessing il mange 700Mo et multiprocessing.pool il utilise 10 Mo coté CPU c’est zero % pour les deux tellement ce que je leurs demande est ridicule déjà avec ça devrait aller bien plus vite XD

+0 -0

Hello, je vois ! En effet, process ou threads, ça change tout ! Chaque processus fils va être une sorte de réplication du père, ce qui inclut aussi la mémoire. En ayant 30 processus, tu avais donc (en gros) une consommation qui faisait ×30 (une zone mémoire par processus) ! Ce n’est pas le cas des threads qui restent sur la même zone mémoire du fil parent (d’où les problèmes classiques d’accès à la mémoire de façon concurrente par plusieurs threads, même si en Python y a le GIL).

(et doit écrire dans une base SQlitle (qui étrangle aussi un peu (mais async pourrait corriger via le verrou))

Fais attention, avec SQLite. Il supporte bien les accès concurrents en mode lecture s’il est configuré pour être en mode WAL (via la pragma pragma journal_mode = wal), mais les écritures restent quand même séquentielles (via un lock global géré par SQLite). Donc même si ton script en Python peut lancer les écritures de façon concurrentes, sache que SQLite ne peut pas le supporter : chaque écriture attendra donc son tour. Mais, selon la vitesse typique qu’une écriture prendra, ça peut ne pas être un problème majeur. Le seul moyen de savoir c’est de constater à l’usage ;)

Si tes appels externes consistent surtout à lancer des commandes, sache enfin que tu n’as pas forcément besoin de threads ou d’asynchrone. En effet, le simple fait d’invoquer les commandes est déjà asynchrone, par exemple :

import subprocess as sp

# Lancer 10 commandes (`sleep 10`) en parallèle et attendre qu'elles finissent toutes
processes = [sp.Popen(['sleep', '10']) for _ in range(10)]
for p in processes:
    p.wait()

Le code ci-dessus mettra bien 10 secondes à s’exécuter, et pas 10×10 secondes. En fait, tu devras attendre le temps de la commande la plus longue de ton batch.

Une méthode plus sophistiquée serait de faire un pool qui n’a pas besoin d’attendre que les 10 commandes finissent toutes d’être exécutées avant de passer au 10 prochaines autres, mais qui peut les gérer au fur et à mesure que sa capacité (de 10 max, par exemple) se libère pour maintenir 10 exécutions en continu.

+0 -0

c’est pour ca l’usage de la pool ^^ ça gère automatiquement le principe de "lancer le suivant quand une place est libre" et non c’est les commandes a faire sur des routeurs a distance via SSH(netmioko) (ou SNMP, Pysnmp avec une surcouche maison pour un usage courant)

et concernant LiteSQl, je compte bien faire un lock coté python, car même si le coté "gestion multi écriture" est intéressent ailleurs(PostGreSQL, mariaDb, n’importe quoi d’autre). c’est pour le moment un poil inutile

+0 -0
Connectez-vous pour pouvoir poster un message.
Connexion

Pas encore membre ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte