使用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)
语句的输出摘自官方文档:
TheINFO
message 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 |
|