在使用
tornado
作为mqtt
的consumer
来订阅消息,当接收到消息后使用tornado
提供的AsyncHTTPClient
进行异步HTTP请求的时候出现list changed size during iteration
的异常
异常信息如下:
Exception in thread Tornado:
Traceback (most recent call last):
...
tornado.ioloop.IOLoop.instance().start()
File "/usr/lib64/python2.7/site-packages/tornado/ioloop.py", line 815, in start heapq.heappop(self._timeouts)
RuntimeError: list changed size during iteration
依赖的组件
- tornado ==4.4.2
- paho-mqtt==1.3.1
修改前
问题代码如下:
client = mqtt.Client(protocol=mqtt.MQTTv31)
def on_connect(client, userdata, flags, rc):
gen_log.debug("Connected with result code " + str(rc))
from threading import current_thread
gen_log.debug('----------on_connect thread name:{}'.format(current_thread().getName()))
client.subscribe("chinasws")
@coroutine
def on_message(client, userdata, msg):
from threading import current_thread
gen_log.debug('----------on_message thread name:{}'.format(current_thread().getName()))
def on_response(response):
gen_log.debug("on_response running result:{} message:{}".format(response.code,msg.payload))
http_client=AsyncHTTPClient()
http_request=HTTPRequest('http://192.168.2.40/?body={}'.format(random.randint(1,100000)))
response=yield http_client.fetch(request=http_request)
gen_log.debug("on_response running result:{} message:{}".format(response.code,msg.payload))
class PubMessageHandler(RequestHandler):
@coroutine
def get(self):
topic=self.get_argument('topic')
body=self.get_argument('body')
#info=yield Task(client.publish,topic,payload=body,qos=1,retain=True)
info=client.publish(topic,payload=body+'{}'.format(random.randint(1,10000)),qos=1,retain=True)
self.write(str(info.mid))
@coroutine
def mqtt_subscribe():
#host="192.168.2.40"
host='iot.eclipse.org'
gen_log.debug('connecting to {}'.format(host))
#client.connect_async("iot.eclipse.org", 1883, 60)
#client.connect("iot.eclipse.org", 1883, 60)
client.connect(host, 1883, 60)
client.on_connect = on_connect
client.on_message = on_message
gen_log.debug('connected to {}'.format(host))
client.loop_start()
def main():
parse_command_line()
handlers=[
(r'/pub',PubMessageHandler),
]
setting={
'debug':True
}
from threading import current_thread
gen_log.debug(current_thread().getName())
app=Application(handlers=handlers,**setting)
app.listen(11108)
ioloop=IOLoop.current()
ioloop.add_callback(mqtt_subscribe)
gen_log.debug('server running at 11108')
ioloop.start()
if __name__ == '__main__':
main()
测试方法
python2.7 app.py --logging=debug #运行tornado服务器
输出
george@george-HP-Z230-SFF-Workstation:~/workspace/mqttdemo$ python2.7 app.py --logging=debug
[D 171113 08:51:38 app:100] MainThread
[D 171113 08:51:38 app:105] server running at 11108
[D 171113 08:51:38 app:64] connecting to 192.168.2.40
[D 171113 08:51:38 app:70] connected to 192.168.2.40
[D 171113 08:51:38 app:22] Connected with result code 0
[D 171113 08:51:38 app:24] ----------on_connect thread name:Thread-1
[D 171113 08:51:38 app:38] ----------on_message thread name:Thread-1
[D 171113 08:51:38 app:46] on_response running result:200 message:efgh9023
curl http://localhost:11108/pub?topic=chinasws\&body=aaaa #需进行压力测试话可以使用`ab`或`jmeter`等工具
在配置比较差的服务器上进行压力测试很容易出现list changed size during iteration的异常,通过观察服务器启动的输出发现 在
on_connect
和on_message
,两个方法是在线程Thread-1
中运行,在on_message
使用了tornado
提供的AyncHTTPClient
,而IOLoop
是在线程MainThread
中运行,结合https://github.com/tornadoweb/tornado/issues/1773的描述,基本断定是多线程互操作heapq
导致的异常.
tornado推荐的用法是单线程事件循环的工作模式,所以解决该问题的思路是取消多线程的模式,利用tornado
本身的事件循环来完成mqtt
的订阅
修改后
代码如下
client = mqtt.Client(protocol=mqtt.MQTTv31)
# The callback for when the client receives a CONNACK response from the server.
def on_connect(client, userdata, flags, rc):
gen_log.debug("Connected with result code " + str(rc))
from threading import current_thread
gen_log.debug('----------on_connect thread name:{}'.format(current_thread().getName()))
# Subscribing in on_connect() means that if we lose the connection and
# reconnect then subscriptions will be renewed.
client.subscribe("chinasws")
#client.subscribe("$SYS/#")
# The callback for when a PUBLISH message is received from the server.
@coroutine
def on_message(client, userdata, msg):
from threading import current_thread
gen_log.debug('----------on_message thread name:{}'.format(current_thread().getName()))
gen_log.debug('----------on_message message body:{}'.format(msg.payload))
def on_response(response):
gen_log.debug("on_response running result:{} message:{}".format(response.code,msg.payload))
http_client=AsyncHTTPClient()
http_request=HTTPRequest('http://192.168.2.40/?body={}'.format(random.randint(1,100000)))
response=yield http_client.fetch(request=http_request)
gen_log.debug("on_response running result:{} message:{}".format(response.code,msg.payload))
class PubMessageHandler(RequestHandler):
@coroutine
def get(self):
topic=self.get_argument('topic')
body=self.get_argument('body')
#info=yield Task(client.publish,topic,payload=body,qos=1,retain=True)
#info=client.publish(topic,payload=body+'{}'.format(random.randint(1,10000)),qos=1,retain=True)
info=client.publish(topic,payload=body+'{}'.format(random.randint(1,10000)))
self.write(str(info.mid))
@coroutine
def mqtt_subscribe():
host="192.168.2.40"
#host='iot.eclipse.org'
gen_log.debug('connecting to {}'.format(host))
#client.connect_async("iot.eclipse.org", 1883, 60)
#client.connect("iot.eclipse.org", 1883, 60)
client.connect(host, 1883, 60)
client.on_connect = on_connect
client.on_message = on_message
gen_log.debug('connected to {}'.format(host))
#client.loop_start()
#关键修改
def connection_read(fd,events):
client.loop()
io_loop=IOLoop.current()
io_loop.add_handler(client.socket().fileno(),connection_read,IOLoop.READ|IOLoop.WRITE)
def main():
parse_command_line()
handlers=[
(r'/pub',PubMessageHandler),
]
setting={
'debug':True
}
from threading import current_thread
gen_log.debug(current_thread().getName())
app=Application(handlers=handlers,**setting)
app.listen(11108)
ioloop=IOLoop.current()
ioloop.add_callback(mqtt_subscribe)
gen_log.debug('server running at 11108')
ioloop.start()
if __name__ == '__main__':
main()
关键点
注释了
mqtt
loop_start
的调用,增加了tornado
的事件注册
def connection_read(fd,events):
client.loop()
io_loop=IOLoop.current()
io_loop.add_handler(client.socket().fileno(),connection_read,IOLoop.READ|IOLoop.WRITE)
python2.7 app.py --logging=debug
输出
george@george-HP-Z230-SFF-Workstation:~/workspace/mqttdemo$ python2.7 app.py --logging=debug
[D 171113 09:40:43 app:102] MainThread
[D 171113 09:40:43 app:107] server running at 11108
[D 171113 09:40:43 app:66] connecting to 192.168.2.40
[D 171113 09:40:43 app:72] connected to 192.168.2.40
[D 171113 09:40:43 app:22] Connected with result code 0
[D 171113 09:40:43 app:24] ----------on_connect thread name:MainThread
[D 171113 09:40:44 app:38] ----------on_message thread name:MainThread
[D 171113 09:40:44 app:39] ----------on_message message body:efgh1068
[I 171113 09:40:48 web:2063] 200 GET /pub?topic=chinasws&body=efgh (127.0.0.1) 2.01ms
[D 171113 09:40:48 app:38] ----------on_message thread name:MainThread
[D 171113 09:40:48 app:39] ----------on_message message body:efgh2680
[D 171113 09:40:49 app:47] on_response running result:200 message:efgh1068
[D 171113 09:40:55 app:47] on_response running result:200 message:efgh2680
通过调整代码后,从输出可以看出on_connect
和on_message
都运行在线程MainThread
中,修改后的代码在配置低的服务器上进行大量的压力测试未出现list changed size duration iternation
的异常,至此解决了该问题
小结
tornado
是一个比较灵活的应用程序框架,开发web
站点或网络应用程序等都有相关模块提供支持,但是某些第三方的功能(如mqtt
)需要第三方库(如paho-mqtt
)进行支持,在集成第三方库的时候需要比较小心处理,有些第三方库本身没有提供对tornado
的支持或官方文档没有相关指导说明,集成时要小心线程间的互操作问题。在本文的案例中如果在on_message
中使用requests
库放弃AsyncHTTPClient
那么就不会出现list changed size duration iternation
的异常。另外一个项目https://github.com/hulingfeng211/sshwall.git也是集成了paramiko
,利用tornado
实现webssh
的功能的案例.