pika消费者程序使用Python注册Windows为服务无法停止的问题
运行环境
- Python 3.7.7
- pika 0.10.0
问题描述
使用 pika 库,连接 rabbitmq,对队列进行监听,并处理监听到的消息。希望将程序注册为windows服务,后台运行,开机自启动。
在查阅有关资料以及实施过程中主要遇到几个问题:
- 编写完以后服务没有及时响应启动或控制请求
- pika库的stop_consuming方法没有响应,无法停止服务
- 程序编写有问题时,服务启动不起来,需要查看windows事件日志
基本实现
使用第三方库pywin32
这个库有一个基础类 win32serviceutil.ServiceFramework
,只需要继承该类,实现自己的方法即可,一个基础的服务模板如下面所示:
#encoding=utf-8
#ZPF
import win32serviceutil
import win32service
import win32event
class PythonService(win32serviceutil.ServiceFramework):
#服务名
_svc_name_ = "PythonService"
#服务在windows系统中显示的名称
_svc_display_name_ = "Python Service Test"
#服务的描述
_svc_description_ = "This code is a Python service Test"
def __init__(self, args):
win32serviceutil.ServiceFramework.__init__(self, args)
self.hWaitStop = win32event.CreateEvent(None, 0, 0, None)
def SvcDoRun(self):
# 把自己的代码放到这里,就OK
# 等待服务被停止
win32event.WaitForSingleObject(self.hWaitStop, win32event.INFINITE)
def SvcStop(self):
# 先告诉SCM停止这个过程
self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
# 设置事件
win32event.SetEvent(self.hWaitStop)
if __name__=='__main__':
win32serviceutil.HandleCommandLine(PythonService)
#括号里参数可以改成其他名字,但是必须与class类名一致;
需要在 SvcDoRun
中实现自己需要的服务,一般说来,需要注册为服务的任务都是堵塞型的,所以这个函数应该是有一个循环体不停在运行的,直到停止服务的时候,通过 SvcStop
传入一个状态位来让 SvcDoRun
结束运行。
对于我的场景来说,我需要在 SvcDoRun
中 channel.start_consuming()
,而在 SvcDoRun
中 channel.stop_consuming()
。
注册、启动、结束服务
在 PythonService.py
所在的目录CMD(管理员)中运行以下指令,可以实现服务的注册、启动与停止等。
1.安装服务
python PythonService.py install
2.让服务自动启动
python PythonService.py --startup auto install
3.启动服务
python PythonService.py start
4.重启服务
python PythonService.py restart
5.停止服务
python PythonService.py stop
6.删除/卸载服务
python PythonService.py remove
第一个问题:服务没有及时响应启动或控制请求
这主要是因为系统环境变量没设置好,需要在系统变量的path中添加如下四个路径,注意对应你的python安装路径。一般来说,python环境会配置前两个,后两个路径是我们安装完 pywin32
才有的,在注册启动服务时需要用到。
C:\Program Files\Python37\Scripts
C:\Program Files\Python37
C:\Program Files\Python37\Lib\site-packages\pywin32_system32
C:\Program Files\Python37\Lib\site-packages\win32
第二个问题:启动后报错,但不知道哪里出错
通过参考内容3(在win10中windows事件日志如何查看)的方式可以看到你注册的服务的报错信息,如果是由于代码编写有误,这里也能看出端倪
第三个问题:服务无法正确停止:无法完成操作。服务无法在此时接受控制信息
发生这个问题最可能的原因是, SvcDoRun
中的循环体没有退出,程序无法结束。
对应到我的程序来讲,在 SvcDoRun
中 channel.start_consuming()
开启消费程序后,而在 SvcDoRun
中 使用 channel.stop_consuming()
无法结束。主要原因还是 start_consuming()
的实现造成的,通过查看这个方法的源码:
def start_consuming(self):
"""Processes I/O events and dispatches timers and `basic_consume`
callbacks until all consumers are cancelled.
NOTE: this blocking function may not be called from the scope of a
pika callback, because dispatching `basic_consume` callbacks from this
context would constitute recursion.
:raises pika.exceptions.RecursionError: if called from the scope of a
`BlockingConnection` or `BlockingChannel` callback
"""
# Check if called from the scope of an event dispatch callback
with self.connection._acquire_event_dispatch() as dispatch_allowed:
if not dispatch_allowed:
raise exceptions.RecursionError(
'start_consuming may not be called from the scope of '
'another BlockingConnection or BlockingChannel callback')
# Process events as long as consumers exist on this channel
while self._consumer_infos:
print("t")
self.connection.process_data_events(time_limit=None)
消费消息通过 self.connection.process_data_events(time_limit=None)
来实现,设计的时候,while
的条件是 self._consumer_infos
,而 channel.stop_consuming()
正是通过清空 self._consumer_infos
来使消费行为停止,问题出在 (time_limit=None)
上,当 (time_limit=None)
时,process_data_events
方法本身是堵塞的,一直要等到有数据来才会循环执行一次,如果没有消息来,就一直等待,造成堵塞,永远不会退出。
解决的办法就是在调用 start_consuming
的地方,直接调用 process_data_events
并给time_limit
来设置一个时间,让这层循环即使在没有数据到来的时候也跑起来而不是一直堵塞,就能响应到 stop_consuming
了
值得注意的是:不同版本的pika
,start_consuming
中处理数据的函数名是不一样的,不一定对应了process_data_events
,要查看具体的实现来进行对应的调用。
最终的实现
import win32serviceutil
import win32service
import win32event
import pika
class PythonService(win32serviceutil.ServiceFramework):
#服务名
_svc_name_ = "PythonService"
#服务在windows系统中显示的名称
_svc_display_name_ = "Python Service Test"
#服务的描述
_svc_description_ = "This code is a Python service Test"
def __init__(self, args):
win32serviceutil.ServiceFramework.__init__(self, args)
self.hWaitStop = win32event.CreateEvent(None, 0, 0, None)
def SvcDoRun(self):
# 连接rabbitmq
# 创建通道的时候要将值存储到类成员变量 self.channel,以便 SvcStop 中还等访问到这个通道
# 绑定消费队列
while self.channel._consumer_infos:
# 开始消费
self.channel.connection.process_data_events(time_limit=1)
win32event.WaitForSingleObject(self.hWaitStop, win32event.INFINITE)
def SvcStop(self):
# 先告诉SCM停止这个过程
self.channel.stop_consuming()
self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
# 设置事件
win32event.SetEvent(self.hWaitStop)
if __name__=='__main__':
win32serviceutil.HandleCommandLine(PythonService)
#括号里参数可以改成其他名字,但是必须与class类名一致;