使用logging模块时遇到的奇怪问题
背景
最近在写测试用例的自动化调度脚本时,用logging做的日志记录,但是脚本在运行时日志输出和预想的相差很多…
代码逻辑是这样:
- 主进程首先创建一个子进程collect_result来收集和处理每个TestCase进程的运行结果,如日志的上传等。
- 然后主程序循环串行的为每个要执行的测试用例创建新的进程testcase,在testcase进程中运行测试代码,这个进程在测试用例执行结束后会将结果通过队列传递给collect_result进程。
- 所有子进程的日志都通过
logging.handlers.QueueHandler传递给父进程的logging.handlers.QueueListener线程,由其统一处理。
代码
代码如下:
1 | |
在windows11(python3.12)上运行结果如下所示, 所有TestCase进程的输出都丢失了。
1 | |
而在debain12上(python3.11)运行结果更是奇怪,很多日志重复输出了很多遍,why?!
1 | |
问题分析
猜测应该是主程序创建进程时,子进程携带有父进程的一些对象信息,所有修改上面脚本加一些debug输出
1 | |
windows系统
Windows系统上,debug的输出如下
1 | |
对上面的输出,进行一步步分析
before listen: []
说明主程序在运行listener_configure函数进行logging配置前,root logger的handlers列表里没有任何handler, 这步okMainProcess 4908 INFO after listen: [<StreamHandler
(NOTSET)>]
运行listener_configure函数配置后,主程序root logger有一个StreamHandler, 它没有设置level,所以会使用与其关联的root logger的日志等级,即logging.DEBUG。这一步也okMainProcess 4908 INFO main start ;这步ok
第5行,是在TestCase里
__init__方法里调用worker_configure产生的输出。
这个输出就差不多发现问题了,本应该出现在子进程TestCase中的输出,出现在了主进程里,导致主进程handler列表额外多了一个QueueHandler对象,这也是出现后面第6-8行输出的原因。
由此可看出:继承自multiprocessing.Process的子类TestCase创建新进程时,__init__初始化方法是在父进程中执行的!第9行是collect_results进程的输出,经过
worker_configure配置后,collect_results进程有只有一个QueueHandler,这步ok,后面的第10行输出也ok接下来第11-13行是TestCase进程的输出:
其中第11, 12行是logging.info('%s, started', name)语句(由于日志等级低于WARNNING所以没有被输出)前后两个print语句的输出:- 在TestCase-2进程第一次调用logging函数前,
handlers列表为空 - 在第一次调用
logging.info函数后,root logger的handlers列表包含一个StreamHandler
第13行是
logging.warning('Doing some work from %s', name)语句的输出摘自官方文档:
TheINFOmessage doesn’t appear because the default level isWARNING.
The call tobasicConfig()should come before any calls todebug(),info(), etc. Otherwise, those functions will callbasicConfig()for you with the default options. As it’s intended as a one-off simple configuration facility, only the first call will actually do anything: subsequent calls are effectively no-ops.从第11-13行的输出可以看出:windows平台multiprocessing创建的子进程并没有继承父进程的环境,而是启动新的解释器进程,创建新的环境,windows上创建新进程的默认方法为spawn:
1
2
3
4
5
6>>> import multiprocessing
>>> multiprocessing.get_start_method
<bound method DefaultContext.get_start_method of <multiprocessing.context.DefaultContext object at 0x000001B5AEABA300>>
>>> multiprocessing.get_start_method()
'spawn'
>>>- 在TestCase-2进程第一次调用logging函数前,
后面每次创建新的TestCase子进程时,都会使父进程的root logger的handler(
QueueHandler)数量加1
经过上面分析,在windows上,脚本日志对不上的原因基本上已经很明了了:
- 继承自
multiprocessing.Process的TestCase子类创建新的进程时,__init__初始化方法是在父进程中执行的 - Windows平台
multiprocessing创建的子进程并没有继承父进程的环境,而是启动新的解释器环境
linux系统
debain系统上,debug的log多了很多,输出如下:
1 | |
分析过程和windows一样,这里只说明关键点:
- 第5行的输出说明,linux平台上multiprocessing模块在创建进程时,
Process子类的__init__方法也是在父进程执行,这点和windows的执行过程一样;即,不论windows还是linux,Process子类的__init__方法在创建多进程时都是在父进程执行。 - 第6行的collect_result进程调用
worker_configure函数产生的输出显示,在linux系统collect_result进程比windows系统多了一个StreamHandler对象(继承自父进程)。这说明:linux系统上multiprocessing模块(通过默认的fork调用)创建的新进程会继承父进程的环境 - 后面每次创建新的TestCase子进程时,都会使父进程的root logger的handler(
QueueHandler)数量加1, 而TestCase进程启动时又都会继承父进程所有的handler,所以TestCase进程的输出会一次比一次多。注意,新的TestCase进程只有一个线程,即父进程中调用fork的那个线程的副本,所以TestCase子进程中的QueueHandler输出还是通过父进程进行输出。
经过上面分析说明,在linux平台上当multiprocessing通过fork系统调用创建进程时,子进程会复制父进程的环境,所以才导致linux上的输出比windows上多了很多
优化后的脚本
通过上面的分析,优化点只要在以下两个方面:
- 对于以
multiprocessing.Process子类的方式创建子进程,需要将worker_configure函数的调用从__init__方法移动到run方法或其它方法。 - 如果在linux系统上运行,则需要在子进程开始时移除logging模块从父进程继承过来的
handler
优化后的脚本如下:
1 | |
运行上面优化后的脚本,不论是在linux系统还是windows系统,都会有一致的log输出!
Windows输出如下:
1 | |
Linux输出如下:
1 | |