我前面有篇文章已经详细介绍了一下 Python 的日志模块。Python 提供了非常多的可以运用在各种不同场景的 Log Handler.

TimedRotatingFileHandler 是 Python 提供的一个可以基于时间自动切分日志的 Handler 类,他继承自 BaseRotatingHandler -> logging.FileHandler

但是他有一个缺点就是没有办法支持多进程的日志切换,多进程进行日志切换的时候可能会因为重命名而丢失日志数据。

来看下他的实现(我默认大家已经知道了 FileHandler 的实现和 logging 模块的调用机制 如果还不清楚可以先去看下我前面那篇文章 https://www.cnblogs.com/piperck/p/9634133.html):

def doRollover(self):
"""
do a rollover; in this case, a date/time stamp is appended to the filename
when the rollover happens. However, you want the file to be named for the
start of the interval, not the current time. If there is a backup count,
then we have to get a list of matching filenames, sort them and remove
the one with the oldest suffix.
"""
if self.stream:
self.stream.close()
self.stream = None
# get the time that this sequence started at and make it a TimeTuple
currentTime = int(time.time())
dstNow = time.localtime(currentTime)[-1]
t = self.rolloverAt - self.interval
if self.utc:
timeTuple = time.gmtime(t)
else:
timeTuple = time.localtime(t)
dstThen = timeTuple[-1]
if dstNow != dstThen:
if dstNow:
addend = 3600
else:
addend = -3600
timeTuple = time.localtime(t + addend)
dfn = self.baseFilename + "." + time.strftime(self.suffix, timeTuple)
if os.path.exists(dfn):
os.remove(dfn)
# Issue 18940: A file may not have been created if delay is True.
if os.path.exists(self.baseFilename):
os.rename(self.baseFilename, dfn)
if self.backupCount > 0:
for s in self.getFilesToDelete():
os.remove(s)
if not self.delay:
self.stream = self._open()
newRolloverAt = self.computeRollover(currentTime)
while newRolloverAt <= currentTime:
newRolloverAt = newRolloverAt + self.interval
#If DST changes and midnight or weekly rollover, adjust for this.
if (self.when == 'MIDNIGHT' or self.when.startswith('W')) and not self.utc:
dstAtRollover = time.localtime(newRolloverAt)[-1]
if dstNow != dstAtRollover:
if not dstNow: # DST kicks in before next rollover, so we need to deduct an hour
addend = -3600
else: # DST bows out before next rollover, so we need to add an hour
addend = 3600
newRolloverAt += addend
self.rolloverAt = newRolloverAt

doRollover 其实就是 rotate 的具体实现。在具体详细分析为什么会出现多进程问题的时候我先来说下 rotate 的原理。

首先日志会被打印在一个叫 baseFilename 名字的文件中。然后在 Rotate 的时候会根据你想要打印的参数生成对应新文件的名字也就是上面函数的 dfn 的值。

然后会将现在的文件重命名为 dfn 的值。之后在重新创建一个 baseFilename 的文件。然后继续往这个文件里面写。

举个例子,我们一直往 info.log 中写日志。现在该 rotate 了我们会把 info.log rename 成 info.log.2018-10-23 然后再创建一个 info.log 继续写日志,过程就是这样。

让我们来注意导致多进程问题的最关键的几句话:

    if os.path.exists(dfn):
os.remove(dfn)
# Issue 18940: A file may not have been created if delay is True.
if os.path.exists(self.baseFilename):
os.rename(self.baseFilename, dfn)

我们就根据上面的例子继续来描述。比如现在 dfn 就是 info.log.2018-10-23 。那么我会看有没有存在这个文件,如果有我就会先删除掉,然后再看下 info.log 是否存在,如果存在就执行 rename.

所以问题就很明确了,如果同时有多个进程进入临界区,那么会导致 dfn 文件被删除多次,另外下面的 rename 可能也会产生混乱。

现在我们要做的就是首先认为文件存在即是已经有人 rename 成功过了,并且在判断文件不存在的时候只允许一个人去 rename ,其他进程如果正好进入临界区就等一等。

让我们来实现这个函数:

class MultiCompatibleTimedRotatingFileHandler(TimedRotatingFileHandler):

    def doRollover(self):
if self.stream:
self.stream.close()
self.stream = None
# get the time that this sequence started at and make it a TimeTuple
currentTime = int(time.time())
dstNow = time.localtime(currentTime)[-1]
t = self.rolloverAt - self.interval
if self.utc:
timeTuple = time.gmtime(t)
else:
timeTuple = time.localtime(t)
dstThen = timeTuple[-1]
if dstNow != dstThen:
if dstNow:
addend = 3600
else:
addend = -3600
timeTuple = time.localtime(t + addend)
dfn = self.baseFilename + "." + time.strftime(self.suffix, timeTuple)
# 兼容多进程并发 LOG_ROTATE
if not os.path.exists(dfn):
f = open(self.baseFilename, 'a')
fcntl.lockf(f.fileno(), fcntl.LOCK_EX)
if not os.path.exists(dfn):
os.rename(self.baseFilename, dfn)
# 释放锁 释放老 log 句柄
f.close()
if self.backupCount > 0:
for s in self.getFilesToDelete():
os.remove(s)
if not self.delay:
self.stream = self._open()
newRolloverAt = self.computeRollover(currentTime)
while newRolloverAt <= currentTime:
newRolloverAt = newRolloverAt + self.interval
# If DST changes and midnight or weekly rollover, adjust for this.
if (self.when == 'MIDNIGHT' or self.when.startswith('W')) and not self.utc:
dstAtRollover = time.localtime(newRolloverAt)[-1]
if dstNow != dstAtRollover:
if not dstNow: # DST kicks in before next rollover, so we need to deduct an hour
addend = -3600
else: # DST bows out before next rollover, so we need to add an hour
addend = 3600
newRolloverAt += addend
self.rolloverAt = newRolloverAt

我们判断一下如果该文件不存在,我们就进入临界区,然后利用 linux 的 fcntl 用阻塞模式获得一把文件锁。然后判断一下 info.log 是否已经被 rename 过了(这里用于同时进入临界区的其他进程判断前面持锁人是否已经结束完成了文件的 rename)。如果文件还在说明是第一个进来的进程则执行 rename 操作。将 info.log -> info.log.2018-10-23 。完成了之后这个时候 info.log 就已经暂时不存在于当前目录了。而且 dfn 文件已经创建。所以后面进来的进程会跳过这个判断直接执行下面的逻辑 open 一个新的基于 self.baseFilename 的文件。这里同时打开就无所谓了,因为 FileHandler 默认使用的 mode 是 'a' appending 模式,所以当 open 的时候就不会存在覆盖的情况了。

到此就补偿了无法兼容多进程的问题。这里还想多提一句,在写这个的时候经过了很长时间对 fcntl 模块的调研。他是一个基于 linux 的 voluntary 锁。也就是说是一把自愿锁。虽然我在调用的时候加了强制排它锁,但是其他不自愿的比如我再开一个 vim 去编辑该文件是可以绕过这个锁的这个一定要注意。

Reference:

https://gavv.github.io/blog/file-locks/  File locking in Linux

https://www.cnblogs.com/gide/p/6811927.html  python中给程序加锁之fcntl模块的使用

http://blog.jobbole.com/104331/  Linux 中 fcntl()、lockf、flock 的区别

用 Python 写一个多进程兼容的 TimedRotatingFileHandler的更多相关文章

  1. 用Python写一个简单的Web框架

    一.概述 二.从demo_app开始 三.WSGI中的application 四.区分URL 五.重构 1.正则匹配URL 2.DRY 3.抽象出框架 六.参考 一.概述 在Python中,WSGI( ...

  2. 十行代码--用python写一个USB病毒 (知乎 DeepWeaver)

    昨天在上厕所的时候突发奇想,当你把usb插进去的时候,能不能自动执行usb上的程序.查了一下,发现只有windows上可以,具体的大家也可以搜索(搜索关键词usb autorun)到.但是,如果我想, ...

  3. [py]python写一个通讯录step by step V3.0

    python写一个通讯录step by step V3.0 参考: http://blog.51cto.com/lovelace/1631831 更新功能: 数据库进行数据存入和读取操作 字典配合函数 ...

  4. 【Python】如何基于Python写一个TCP反向连接后门

    首发安全客 如何基于Python写一个TCP反向连接后门 https://www.anquanke.com/post/id/92401 0x0 介绍 在Linux系统做未授权测试,我们须准备一个安全的 ...

  5. Python写一个自动点餐程序

    Python写一个自动点餐程序 为什么要写这个 公司现在用meican作为点餐渠道,每天规定的时间是早7:00-9:40点餐,有时候我经常容易忘记,或者是在地铁/公交上没办法点餐,所以总是没饭吃,只有 ...

  6. 用python写一个自动化盲注脚本

    前言 当我们进行SQL注入攻击时,当发现无法进行union注入或者报错等注入,那么,就需要考虑盲注了,当我们进行盲注时,需要通过页面的反馈(布尔盲注)或者相应时间(时间盲注),来一个字符一个字符的进行 ...

  7. python写一个能变身电光耗子的贪吃蛇

    python写一个不同的贪吃蛇 写这篇文章是因为最近课太多,没有精力去挖洞,记录一下学习中的收获,python那么好玩就写一个大一没有完成的贪吃蛇(主要还是跟课程有关o(╥﹏╥)o,课太多好烦) 第一 ...

  8. python写一个邮箱伪造脚本

    前言: 原本打算学php MVC的思路然后写一个项目.但是贼恶心, 写不出来.然后就还是用python写了个邮箱伪造. 0x01 第一步先去搜狐注册一个邮箱 然后,点开设置,开启SMTP服务. 当然你 ...

  9. 用python写一个非常简单的QQ轰炸机

    闲的没事,就想写一个QQ轰炸机,按照我最初的想法,这程序要根据我输入的QQ号进行轰炸,网上搜了一下,发现网上的案列略复杂,就想着自己写一个算了.. 思路:所谓轰炸机,就是给某个人发很多信息,一直刷屏, ...

随机推荐

  1. ActiveMQ的作用总结(应用场景及优势)

    业务场景说明: 消息队列在大型电子商务类网站,如京东.淘宝.去哪儿等网站有着深入的应用, 队列的主要作用是消除高并发访问高峰,加快网站的响应速度. 在不使用消息队列的情况下,用户的请求数据直接写入数据 ...

  2. 自定义的库加载不进来,因为库中import的PIL和pillow文件没有pip install

    1.自定义的库,加载进来,提示red不能识别这个class或moudle 2.应该展开细节多看下,细节中提示,没有PIL和pillow 3.这个时候在cmd中使用pip安装PIL和pillow pip ...

  3. springboot获取applicationcontext

    使用springboot之前,我们通过ClassPathXmlApplicationContext加载spring xml配置文件来获取applicationcontext,使用springboot后 ...

  4. springboot配置文件中使用当前配置的变量

    在开发中,有时我们的application.properties某些值需要重复使用,比如配置redis和数据库或者mongodb连接地址,日志,文件上传地址等,且这些地址如果都是相同或者父路径是相同的 ...

  5. [MicroPython]TPYBoard智能小车“飞奔的TPYBoard装甲一号”

    智能小车作为现代的新发明,是以后的发展方向,他可以按照预先设定的模式在一个环境里自动的运作,不需要人为的管理,可应用于科学勘探等等的用途.智能小车能够实时显示时间.速度.里程,具有自动寻迹.寻光.避障 ...

  6. MongoDB的分片集群搭建

    MongoDB的最为自豪的一个特色之一,分片. 参考官方文档: https://docs.mongodb.com/manual/sharding/  单机压力,高频查询CPU,IO 单表压力,百万千万 ...

  7. ASP.NET Core中代码使用X509证书,部署到IIS上后报错:System cannot find the specified file 的解决办法(转载)

    问: I am trying to embrace the mysteries of SSL communication and have found a great tutorial on this ...

  8. hibernate 解决诡异的mysql存入中文乱码

    使用hibernate查询mysql,通过bean的get方法拿到字符串再写入mysql中的字段会中文乱码,需要String string = xxx.get(),把get方法拿到的值传入到新的str ...

  9. Taro开发微信小程序的初体验

    了解Taro 听说Taro是从几个星期前开始的,在一次饭桌上,一个小伙伴说:"Hey, 你听说了Taro么,听说只需要写一套程序就可以生成H5,小程序以及RN的代码模板,并且类似于React ...

  10. spark 2.3 导致driver OOM的一个SparkPlanGraphWrapper源码的bug

    背景 长话短说,我们部门一个同事找到我,说他的spark 2.3 structured streaming程序频繁报OOM,从来没有坚持过超过三四天的,叫帮看一下. 这种事情一般我是不愿意看的,因为大 ...