rabbitmq 單, 多進程併發 消費 python 腳本

問題

python 腳本進行 MQ 消費過慢, rabbitmq 服務端開始累積隊列的時候, 會發現 python 腳本跑滿 CPU
假如消費隊列中, 消息具有前後依賴關係,那麼多線程併發 python 腳本可以保持這個依賴關係嗎

例如
消息 ID 1 openstack NOVA 發送消息我要創建雲主機
消息 ID 2 openstack CINDER 發送信息,我要創建一個 10GB 的雲盤
消息 ID 3 把創建的雲盤掛載到雲主機中

上述問題純屬假設, 我們不考慮程序的重試機制, 超時機制, 回源機制
那麼就是說,假如如果不順序執行 MQ 任務, 可能會導致錯誤發生

說明

本文只針對 customer 部分進行單進程, 多進程測試

pika 說明

當使用 centos7 進行程序開發時, 可以通過兩種方法安裝 pika 模塊

yum install -y python2-pika-0.10.0-9.el7.noarch (安裝 pika-0.9 版本)
pip install pika (安裝了 pika 1.1.0 版本)

區別

pika 0.9.0

進行 rabbitmq 消費時, 經常會遇到服務器中有消費堆積現象發生
當服務器端有消費堆積時候, 運行 customer python 進程 CPU 消耗會達到 100%

注意使用時參數卻別

result = channel.queue_declare(exclusive=True)   可以不指定 QUEUE 參數
channel.basic_consume(callback,
                      queue_name='fanout_cmdb3_mq_exchange',
                      no_ack=False)

pika 1.1.0

解決了對服務端消費堆積問題
沒有再發現 python 腳本 CPU 消耗 100% 問題

注意使用時參數卻別

 result = channel.queue_declare(exclusive=True, queue='fanout_cmdb3_mq_exchange')  新版需要指定 queue  
 
 channel.basic_consume(queue_name,           參考源碼後發現新版參數位置改變, 另外直接放置 queue_name, 不需要參數, no_ack 參數要替換爲 auto_ack
                       callback,
                       auto_ack=False)

python 腳本

rabbitmq 入隊 python 腳本

目的

隊列需要消費者返回消費確認信息
每次執行腳本都會隨機生成 600 條以內的信息, 並向 RABBITMQ 入隊
隊列信息內容 {“id”: 0, “time”: “2020-07-02_13:35:44”}

參考腳本

#!/usr/bin/python
# -*- coding:utf-8 -*-
import pika
import json
import time
import random

parameters = pika.ConnectionParameters(host='rabbitmqserver',
                                           credentials=pika.PlainCredentials('mquser', 'mqpassword'))
connection = pika.BlockingConnection(parameters=parameters)
channel = connection.channel()
#  'win-test'  爲消息隊列
#  durable = true 用於持久性保留消息, 只要消費確認後纔會刪除消息
channel.queue_declare(queue='win-test' , durable=True)  
now = time.strftime("%Y-%m-%d_%H:%M:%S",time.localtime(time.time()))
randomNum = random.randint(1,600)

for i in range(10):
    data = {}
    data["id"] = i
    data["time"] = now
    message=json.dumps(data)
    channel.basic_publish(exchange = '',routing_key = 'win-test',body = message,
                          properties=pika.BasicProperties(delivery_mode = 2))   #  必須確認消費
    print(message)

connection.close()

rabbitmq python 消費腳本(單線程)

目的

從 rabbitmq server 中獲取消費隊列
消費並完成腳本操作後進行消息確認
吧消費信息依次寫入到本地文本中, 用於隊列消費順序確認

參考腳本

#!/usr/bin/python
# -*- coding:utf-8 -*-
import pika
import sys
import json
def callback(ch, method, properties, body):
    try:
        jsonObject = json.loads(body)
    except :
        print("not json info")
    if len(jsonObject) > 0:
        writeFile(jsonObject)
    ch.basic_ack(delivery_tag = method.delivery_tag)  <- 確認消費完成

def writeFile(data):
    with open("/tmp/test.txt", 'a') as fileObj:
        json.dump(data ,fileObj)
        fileObj.write("\n")

if __name__ == '__main__':
    parameters = pika.ConnectionParameters(host='rabbitmq-server',
         credentials=pika.PlainCredentials('admin', 'admin'))
    connection = pika.BlockingConnection(parameters=parameters)
    channel = connection.channel()
    #  fanout 使用的是廣播模式
    channel.exchange_declare(exchange='win-test',exchange_type='fanout')
    result = channel.queue_declare(exclusive=True)
    queue_name = result.method.queue
    channel.queue_bind(exchange='',
                    queue='win-test')
    channel.basic_consume(callback,
                       queue='win-test',
                       no_ack=False)           # no_ack=False  確保需要 ACK 迴應再確認消費
    print(' [*] Waiting for logs. To exit press CTRL+C')
    channel.start_consuming()

rabbitmq python 消費腳本(多線程)

說明

腳本工作原理跟單線程一樣
但使用了 5 個進程進行併發消費 用於驗證消費過程中無法按照 queue 中隊列順序進行處理
單線程使用了 pika 0.9 模板, 多線程使用了 pika 1.1.0 模塊,因此登錄方式,callback 方法不一樣

參考腳本

#!/usr/bin/python
# -*- coding:utf-8 -*-

import multiprocessing
import time
import pika
import json


def callback(ch, method, properties, body):
    print " [x] %r received %r" % (multiprocessing.current_process(), body,)

    try:
        jsonObject = json.loads(body)
    except :
        print("not json info")

    if len(jsonObject) > 0:
        writeFile(jsonObject)

    ch.basic_ack(delivery_tag = method.delivery_tag)


def writeFile(data):

    with open("/tmp/test.txt", 'a') as fileObj:
        json.dump(data ,fileObj)
        fileObj.write("\n")



def consume():
    credentials = pika.PlainCredentials('admin', 'admin')
    parameters = pika.ConnectionParameters('mqserver', credentials=credentials, heartbeat=5)

    connection = pika.BlockingConnection(parameters)
	
    channel = connection.channel()
    channel.exchange_declare(exchange='win-test',exchange_type='fanout')
    channel.queue_declare(queue='win-test', durable=True)

    channel.basic_consume("win-test",
                          callback,
                          auto_ack=False)

    print ' [*] Waiting for messages. To exit press CTRL+C'
    try:
        channel.start_consuming()
    except KeyboardInterrupt:
        pass

workers = 5
pool = multiprocessing.Pool(processes=workers)
for i in xrange(0, workers):
    pool.apply_async(consume)

try:
    while True:
        continue
except KeyboardInterrupt:
    print ' [*] Exiting...'
    pool.terminate()
    pool.join()

結論

參考輸出文件可以知道, 當使用多進行連接 MQ , 無法保證隊列獲取先後順序

{"id": 292, "time": "2020-07-01_17:47:57"}
{"id": 297, "time": "2020-07-01_17:48:33"}
{"id": 298, "time": "2020-07-01_17:48:33"}
{"id": 293, "time": "2020-07-01_17:47:57"}
{"id": 299, "time": "2020-07-01_17:48:33"}
{"id": 294, "time": "2020-07-01_17:47:57"}
{"id": 300, "time": "2020-07-01_17:48:33"}
{"id": 278, "time": "2020-07-01_17:47:59"}
{"id": 279, "time": "2020-07-01_17:47:59"}
{"id": 301, "time": "2020-07-01_17:48:33"}
{"id": 280, "time": "2020-07-01_17:47:59"}
{"id": 302, "time": "2020-07-01_17:48:33"}
{"id": 281, "time": "2020-07-01_17:47:59"}
{"id": 303, "time": "2020-07-01_17:48:33"}
{"id": 283, "time": "2020-07-01_17:50:01"}
{"id": 304, "time": "2020-07-01_17:48:33"}
{"id": 284, "time": "2020-07-01_17:50:01"}
{"id": 305, "time": "2020-07-01_17:48:33"}
{"id": 285, "time": "2020-07-01_17:50:01"}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章