web-dev-qa-db-fra.com

La journalisation python prend-elle en charge le multitraitement?

On m'a dit que la journalisation ne peut pas être utilisée en multitraitement. Vous devez effectuer le contrôle d'accès simultané au cas où le multitraitement gâcherait le journal.

Mais j'ai fait quelques tests, il semble qu'il n'y ait aucun problème à utiliser la journalisation en multiprocessing

import time
import logging
from multiprocessing import Process, current_process, pool


# setup log
logger = logging.getLogger(__name__)
logging.basicConfig(level=logging.DEBUG,
                    format='%(asctime)s %(filename)s[line:%(lineno)d] %(levelname)s %(message)s',
                    datefmt='%a, %d %b %Y %H:%M:%S',
                    filename='/tmp/test.log',
                    filemode='w')


def func(the_time, logger):
    proc = current_process()
    while True:
        if time.time() >= the_time:
            logger.info('proc name %s id %s' % (proc.name, proc.pid))
            return



if __name__ == '__main__':

    the_time = time.time() + 5

    for x in xrange(1, 10):
        proc = Process(target=func, name=x, args=(the_time, logger))
        proc.start()

Comme vous pouvez le voir dans le code.

J'ai délibérément laissé le sous-processus écrire le journal au même moment (5 secondes après le démarrage) pour augmenter les risques de conflit. Mais il n'y a aucun conflit du tout.

Ma question est donc la suivante: pouvons-nous utiliser la journalisation en multitraitement? Pourquoi tant de messages disent que nous ne pouvons pas?

23
Kramer Li

Comme Matino l'a correctement expliqué: la connexion dans une configuration multiprocesseur n'est pas sûre, car plusieurs processus (qui ne connaissent rien des autres existants) écrivent dans le même fichier, pouvant potentiellement intervenir les uns avec les autres.

Maintenant, ce qui se passe, c'est que chaque processus contient un descripteur de fichier ouvert et effectue une "écriture d'écriture" dans ce fichier. La question est de savoir dans quelles circonstances l'écriture ajoutée est "atomique" (c'est-à-dire qu'elle ne peut pas être interrompue par exemple par un autre processus écrivant dans le même fichier et entremêlant sa sortie). Ce problème s'applique à tous les langages de programmation, car à la fin ils feront un appel système au noyau. Cette réponse répond dans quelles circonstances un fichier journal partagé est correct.

Cela revient à vérifier la taille de la mémoire tampon de votre pipe, sur linux qui est définie dans /usr/include/linux/limits.h Et qui fait 4096 octets. Pour les autres OS, vous trouverez ici une bonne liste.

Cela signifie: si votre ligne de journal est inférieure à 4'096 octets (si sous Linux), alors l'ajout est sûr, si le disque est directement connecté (c'est-à-dire sans réseau entre les deux). Mais pour plus de détails, veuillez vérifier le premier lien dans ma réponse. Pour tester cela, vous pouvez faire logger.info('proc name %s id %s %s' % (proc.name, proc.pid, str(proc.name)*5000)) avec différentes longueurs. Avec 5000 par exemple, j'ai déjà mélangé des lignes de journal dans /tmp/test.log.

Dans cette question il y a déjà pas mal de solutions à cela, donc je n'ajouterai pas ma propre solution ici.

Mise à jour: Flask et multiprocessing

Les frameworks Web comme flask sera exécutée dans plusieurs travailleurs si elle est hébergée par uwsgi ou nginx. Dans ce cas, plusieurs processus peuvent écrire dans un seul fichier journal. Y aura-t-il des problèmes?

La gestion des erreurs dans flask se fait via stdout/stderr qui est ensuite acheté par le serveur web (uwsgi, nginx, etc.) qui doit veiller à ce que les journaux soient écrits de la bonne manière (voir par exemple [cet exemple flacon + nginx]) ( http://flaviusim.com/blog/Deploying-Flask-with-nginx-uWSGI-and-Supervisor/ ), ajoutant probablement aussi des informations de processus pour que vous puissiez associez les lignes d'erreur aux processus. De flasks doc :

Par défaut à partir de Flask 0.11, les erreurs sont automatiquement enregistrées dans le journal de votre serveur Web. Les avertissements ne le sont cependant pas.

Donc, vous auriez toujours ce problème de fichiers journaux entremêlés si vous utilisez warn et que le message dépasse la taille de la mémoire tampon du canal.

17
hansaplast

Il n'est pas sûr d'écrire dans un seul fichier à partir de plusieurs processus.

Selon https://docs.python.org/3/howto/logging-cookbook.html#logging-to-a-single-file-from-multiple-processes

Bien que la journalisation soit sécurisée pour les threads et que la journalisation dans un seul fichier à partir de plusieurs threads dans un seul processus soit prise en charge, la journalisation dans un seul fichier à partir de plusieurs processus n'est pas prise en charge, car il n'existe aucun moyen standard de sérialiser l'accès à un seul fichier sur plusieurs processus en Python.

Une solution possible serait que chaque processus écrive dans son propre fichier. Vous pouvez y parvenir en écrivant votre propre gestionnaire qui ajoute le processus pid à la fin du fichier:

import logging.handlers
import os


class PIDFileHandler(logging.handlers.WatchedFileHandler):

    def __init__(self, filename, mode='a', encoding=None, delay=0):
        filename = self._append_pid_to_filename(filename)
        super(PIDFileHandler, self).__init__(filename, mode, encoding, delay)

    def _append_pid_to_filename(self, filename):
        pid = os.getpid()
        path, extension = os.path.splitext(filename)
        return '{0}-{1}{2}'.format(path, pid, extension)

Ensuite, il vous suffit d'appeler addHandler:

logger = logging.getLogger('foo')
fh = PIDFileHandler('bar.log')
logger.addHandler(fh)
20
matino

Utilisez une file d'attente pour une gestion correcte de la simultanéité en récupérant simultanément des erreurs en alimentant tout le processus parent via un canal.

from logging.handlers import RotatingFileHandler
import multiprocessing, threading, logging, sys, traceback

class MultiProcessingLog(logging.Handler):
    def __init__(self, name, mode, maxsize, rotate):
        logging.Handler.__init__(self)

        self._handler = RotatingFileHandler(name, mode, maxsize, rotate)
        self.queue = multiprocessing.Queue(-1)

        t = threading.Thread(target=self.receive)
        t.daemon = True
        t.start()

    def setFormatter(self, fmt):
        logging.Handler.setFormatter(self, fmt)
        self._handler.setFormatter(fmt)

    def receive(self):
        while True:
            try:
                record = self.queue.get()
                self._handler.emit(record)
            except (KeyboardInterrupt, SystemExit):
                raise
            except EOFError:
                break
            except:
                traceback.print_exc(file=sys.stderr)

    def send(self, s):
        self.queue.put_nowait(s)

    def _format_record(self, record):
         # ensure that exc_info and args
         # have been stringified.  Removes any chance of
         # unpickleable things inside and possibly reduces
         # message size sent over the pipe
        if record.args:
            record.msg = record.msg % record.args
            record.args = None
        if record.exc_info:
            dummy = self.format(record)
            record.exc_info = None

        return record

    def emit(self, record):
        try:
            s = self._format_record(record)
            self.send(s)
        except (KeyboardInterrupt, SystemExit):
            raise
        except:
            self.handleError(record)

    def close(self):
        self._handler.close()
        logging.Handler.close(self)

Le gestionnaire effectue toute l'écriture de fichier à partir du processus parent et utilise un seul thread pour recevoir les messages transmis par les processus enfants

1
Prakhar Agarwal