最近遇到一个django日志相关的问题,之前线上服务的日志用的是,
django FileHandler + logrotate copytruncate
但上周两次在日志rotate的时候挂掉,经过分析应当是日志copy时带来大量的磁盘io、cpu消耗导致了整个机器无响应。当时的处理策略是改用,
django WatchedFileHandler + logrotate create
刚改完后运行良好,但几天下来出现了两次错误,
ValueError: I/O operation on closed file
上面这个error一旦出现则是会导致整个django进程就挂了,因为每个请求多少都会进行log操作,于是都在log这步挂掉,带来了大量500错误。
在上述error之前log中另有一次错误,
IOError: [Errno 13] Permission denied:
从两个错误信息可以得知,django在操作系统执行logrotate时候出现问题,导致log文件关闭后未能打开,而后写log的时候则是一直失败。
分析下WatchedFileHandler源代码,
class WatchedFileHandler(logging.FileHandler):
def __init__(self, filename, mode='a', encoding=None, delay=0):
logging.FileHandler.__init__(self, filename, mode, encoding, delay)
if not os.path.exists(self.baseFilename):
self.dev, self.ino = -1, -1
else:
stat = os.stat(self.baseFilename)
self.dev, self.ino = stat[ST_DEV], stat[ST_INO]
def emit(self, record):
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)
仔细分析上述代码还是能看出问题所在的,WatchedFileHandler每次会检查log文件是否改变,若改变后则会执行flush + close + open,但若是open操作挂掉,那么下回进入emit函数时self.stream就会是一个closed状态,再也没机会复原。
采用logrotate create进行日志操作时,logrotate会首先rename原来的日志文件,然后创建新的同名文件。在创建文件的那个瞬间,很难说self._open()函数能够执行成功。虽然“Permission denied”这个错误消息有点小不理解,但解决这个问题的方法至此应当明显了。
在self.stream.flush之前判断self.stream是否为closed状态即可,
def emit(self, record):
....
if changed and self.stream is not None:
if not self.stream.closed:
self.stream.flush()
self.stream.close()
...
...
多加上这么个if判断就可以,虽然那一次“Permission denied”错误可能避免不了,但那之后的flush操作至少能够正常运行。
实际项目中到处是陷阱到处是坑,这次深有体会。