Python 多进程日志记录

刚开始用 Python 做 web 开发的时候我就想一个问题,如果 Python 应用需要自己记录一些比 accesslog 更详细的日志(使用 Python 的 logging module),又有多个进程,怎么办最好呢?多个进程往同一个日志文件写入会不会出问题?

最近有个在 Apache 里用 mod_wsgi 运行的程序,设置了4个 process. 最初没有设置日志的 rotation,看起来一切正常。有一天设定了每天 midnight rotate(换成 TimedRotatingFileHandler), 第二天就出问题了,前一天的日志完全丢失,当天日志分散在前一天和当天的两个文件里,并且两个文件都在增长。比如今天是2011-08-14,现在去观察就会发现昨天的 customlog-20110813 和今天的 customlog 两个文件都在被写入。

看了一下 TimedRotatingFileHandler 的 doRollover 方法:

    t = self.rolloverAt - self.interval
    if self.utc:
        timeTuple = time.gmtime(t)
    else:
        timeTuple = time.localtime(t)
    dfn = self.baseFilename + "." + time.strftime(self.suffix, timeTuple)
    if os.path.exists(dfn):
        os.remove(dfn)
    os.rename(self.baseFilename, dfn)
    ...

这就了然了。每个进程在过了 rotate 时间点之后写第一条日志的时候,都会执行这个 doRollover,先看有没有 customlog-20110813 存在,存在的话删掉,把 customlog 改名为 customlog-20110813(注意这一步,文件名只是文件的一个属性,如果有进程已经打开该文件正在写入,并不会受影响,除非文件被删除),然后往新的 customlog 里写入。等四个进程都执行完这个方法的时候,就是一团糟了,不仅昨天的日志完全被删除,今天的日志也会有一小部分被删掉了。

多进程 (Multiprocessing) vs. 多线程 (multithreading)

假如只有一个进程,就不用操心这些了。我这个 qingbo.net 一直都是设置1个进程。于是我在 Stack Overflow 上问,如果 multithreading 就够了,为啥还要 multiprocessing 呢?回答者 Graham 好像是 mod_wsgi 的作者,从他的回答看,对于 Python 来说,由于 GIL 的限制,多进程才能利用多 CPU 或 core 的架构。

好吧,想想有什么办法可以解决这个问题。

Write separate files

取得当前进程 ID (os.getpid()),然后把这个 pid 作为日志文件名的一部分,这样各个进程的日志操作就完全不会互相影响了。

缺点是当服务器重启的时候,新的进程会有新的 pid,于是旧的文件不会被改成带有日期标识的名字,看起来不是那么干净。不过这个问题通过写一个简单的 shell 脚本即可修复,可以放在 crontab 里每天凌晨检查前一天的日志。

结合 WatchedFileHandler 与 TimedRotatingFileHandler?

以下是 WatchedFileHandler 的 emit 方法:

def emit(self, record):
    """
    Emit a record.

    First check if the underlying file has changed, and if it
    has, close the old stream and reopen the file to get the
    current stream.
    """
    if not os.path.exists(self.baseFilename):
        stat = None
        changed = 1
    else:
        stat = os.stat(self.baseFilename)
        changed = (stat[ST_DEV] != self.dev) or (stat[ST_INO] != self.ino)
    if changed and self.stream is not None:
        self.stream.flush()
        self.stream.close()
        self.stream = self._open()
        if stat is None:
            stat = os.stat(self.baseFilename)
        self.dev, self.ino = stat[ST_DEV], stat[ST_INO]
    logging.FileHandler.emit(self, record)

每写一条日志之前它先判断日志文件是不是被改名了,如果是的话就重新创建一个。如果 TimedRotatingFileHandler 在 doRollover 之前做这么一个检查,不就解决问题了吗?不过实际上这又牵扯到进程间同步的问题,还挺复杂的。

WatchedFileHandler 结合 cron job

一个取巧的办法是使用 WatchedFileHandler,并在 crontab 里每天零点添加一个任务,把日志文件改名。于是所有的进程在之后写第一条日志的时候会发现这一点,开始往新的文件里写入。

我没有尝试这个办法所以不知到是不是有问题。但是它的前提是没一个写入操作是一个 atom operation,否则两条日志有可能混杂在一起,就坏了。我不太了解,不过这里有个讨论,看起来每条日志不超过 4K 的话是安全的。该文还展示了另一种办法——

将日志发给一个独立的进程

Python 的 logging module 还提供了一个 SocketHandler,用来把日志通过网络发送出去。文档里有一个非常基本的服务器端的实现。有人还基于 Twisted 写了一个更好的 logging server.

但是这样我们除了需要维护 web server,又要维护 logging server. 万一 logging server 挂了怎么办?我有点担心。

把日志发给 web server

其实也是发给一个独立进程了。mod_wsgi 提供了一种途径,可以把 debug 信息写到 Apache 的 errorlog 里。不过把应用日志跟服务器 errorlog 混在一起,这显然不是什么好办法。

比起多进程的解决方案,我更倾向于使用单线程省去这些烦恼。一方面是我的服务器比较弱,只有两个 core :) 正如 Graham 所说,除了 python code,还有许多处理比如接受、分发请求,发送响应回客户端,处理静态文件请求等都是在 Apache 的 C code里执行的,所以不会让一个 core 忙不过来另一个却闲着。我倒没有实际经验看看单个 Python 应用进程在4核或8核机器上面对大并发量的情况。

单进程的另一个好处,可以不用 memcached 之类的东西,自己直接在进程的内存里方便地作缓存。如果有多个进程这么搞,那就太浪费内存了。

你可能感兴趣的:(日志,python,进程)