【问题标题】:Python and RabbitMQ - Best way to listen to consume events from multiple channels?Python 和 RabbitMQ - 聆听来自多个渠道的消费事件的最佳方式?
【发布时间】:2015-04-17 11:28:42
【问题描述】:

我有两个独立的 RabbitMQ 实例。我正在尝试找到聆听两者事件的最佳方式。

例如,我可以通过以下方式消费其中的事件:

credentials = pika.PlainCredentials(user, pass)
connection = pika.BlockingConnection(pika.ConnectionParameters(host="host1", credentials=credentials))
channel = connection.channel()
result = channel.queue_declare(Exclusive=True)
self.channel.queue_bind(exchange="my-exchange", result.method.queue, routing_key='*.*.*.*.*')
channel.basic_consume(callback_func, result.method.queue, no_ack=True)
self.channel.start_consuming()

我还有第二个主机,“host2”,我也想听。我考虑过创建两个单独的线程来执行此操作,但根据我的阅读,pika 不是线程安全的。有没有更好的办法?或者创建两个单独的线程,每个线程都监听不同的 Rabbit 实例(host1 和 host2)就足够了?

【问题讨论】:

    标签: python rabbitmq pika


    【解决方案1】:

    Pika 可用于多线程消费者。唯一的要求是拥有Pika connection per thread

    Pika Github 存储库有 an example here

    来自 basic_consumer_threaded.py 的 sn-p:

    def on_message(ch, method_frame, _header_frame, body, args):
        (conn, thrds) = args
        delivery_tag = method_frame.delivery_tag
        t = threading.Thread(target=do_work, args=(conn, ch, delivery_tag, body))
        t.start()
        thrds.append(t)
    
    threads = []
    on_message_callback = functools.partial(on_message, args=(connection, threads))
    channel.basic_consume('standard', on_message_callback)
    

    【讨论】:

      【解决方案2】:
      import asyncio
      import tornado.ioloop
      import tornado.web
      
      from aio_pika import connect_robust, Message
      
      tornado.ioloop.IOLoop.configure("tornado.platform.asyncio.AsyncIOLoop")
      io_loop = tornado.ioloop.IOLoop.current()
      asyncio.set_event_loop(io_loop.asyncio_loop)
      
      QUEUE = asyncio.Queue()
      
      
      class SubscriberHandler(tornado.web.RequestHandler):
          async def get(self):
              message = await QUEUE.get()
              self.finish(message.body)
      
      
      class PublisherHandler(tornado.web.RequestHandler):
          async def post(self):
              connection = self.application.settings["amqp_connection"]
              channel = await connection.channel()
              try:
                  await channel.default_exchange.publish(
                      Message(body=self.request.body), routing_key="test",
                  )
              finally:
                  await channel.close()
                  print('ok')
              self.finish("OK")
      
      async def make_app():
          amqp_connection = await connect_robust()
          channel = await amqp_connection.channel()
          queue = await channel.declare_queue("test", auto_delete=True)
          await queue.consume(QUEUE.put, no_ack=True)
          return tornado.web.Application(
              [(r"/publish", PublisherHandler), (r"/subscribe", SubscriberHandler)],
              amqp_connection=amqp_connection,
          )
      
      if __name__ == "__main__":
          app = io_loop.asyncio_loop.run_until_complete(make_app())
          app.listen(8888)
          tornado.ioloop.IOLoop.current().start()
      

      您可以异步方式使用 aio-pika 更多例子在这里 https://buildmedia.readthedocs.org/media/pdf/aio-pika/latest/aio-pika.pdf

      编码愉快:)

      【讨论】:

        【解决方案3】:

        以下是我如何使用一个 rabbitmq 实例同时监听 2 个队列的示例:

        import pika
        import threading
        
        threads=[]
        def client_info(channel):    
           channel.queue_declare(queue='proxy-python')
           print (' [*] Waiting for client messages. To exit press CTRL+C')
        
        
           def callback(ch, method, properties, body):
               print (" Received %s" % (body))
        
           channel.basic_consume(callback, queue='proxy-python', no_ack=True)
           channel.start_consuming()
        
        def scenario_info(channel):    
           channel.queue_declare(queue='savi-virnet-python')
           print (' [*] Waiting for scenrio messages. To exit press CTRL+C')
        
        
           def callback(ch, method, properties, body):
              print (" Received %s" % (body))
        
           channel.basic_consume(callback, queue='savi-virnet-python', no_ack=True)
           channel.start_consuming()
        
        def manager():
           connection1= pika.BlockingConnection(pika.ConnectionParameters
          (host='localhost'))
           channel1 = connection1.channel()
          connection2= pika.BlockingConnection(pika.ConnectionParameters
          (host='localhost'))
           channel2 = connection2.channel()
           t1 = threading.Thread(target=client_info, args=(channel1,))
           t1.daemon = True
           threads.append(t1)
           t1.start()  
        
           t2 = threading.Thread(target=scenario_info, args=(channel2,))
           t2.daemon = True
           threads.append(t2)
        
        
           t2.start()
           for t in threads:
             t.join()
        
        
         manager()
        

        【讨论】:

          【解决方案4】:

          “什么是最好的方式”的答案很大程度上取决于您对队列的使用模式以及“最好”的含义。由于我还不能对问题发表评论,因此我将尝试提出一些可能的解决方案。

          在每个示例中,我将假设 exchange 已经声明。

          线程

          您可以使用 pika 在单个进程中使用来自不同主机上的两个队列的消息。

          你是对的 - 因为its own FAQ statespika 不是线程安全的,但它可以通过为每个线程创建到 RabbitMQ 主机的连接以多线程方式使用。使用threading 模块使这个示例在线程中运行如下所示:

          import pika
          import threading
          
          
          class ConsumerThread(threading.Thread):
              def __init__(self, host, *args, **kwargs):
                  super(ConsumerThread, self).__init__(*args, **kwargs)
          
                  self._host = host
          
              # Not necessarily a method.
              def callback_func(self, channel, method, properties, body):
                  print("{} received '{}'".format(self.name, body))
          
              def run(self):
                  credentials = pika.PlainCredentials("guest", "guest")
          
                  connection = pika.BlockingConnection(
                      pika.ConnectionParameters(host=self._host,
                                                credentials=credentials))
          
                  channel = connection.channel()
          
                  result = channel.queue_declare(exclusive=True)
          
                  channel.queue_bind(result.method.queue,
                                     exchange="my-exchange",
                                     routing_key="*.*.*.*.*")
          
                  channel.basic_consume(self.callback_func,
                                        result.method.queue,
                                        no_ack=True)
          
                  channel.start_consuming()
          
          
          if __name__ == "__main__":
              threads = [ConsumerThread("host1"), ConsumerThread("host2")]
              for thread in threads:
                  thread.start()
          

          我已将callback_func 声明为一种纯粹用于在打印消息正文时使用ConsumerThread.name 的方法。它也可能是ConsumerThread 类之外的函数。

          进程

          或者,您总是可以只运行一个带有消费者代码的进程,每个队列要消费事件。

          import pika
          import sys
          
          
          def callback_func(channel, method, properties, body):
              print(body)
          
          
          if __name__ == "__main__":
              credentials = pika.PlainCredentials("guest", "guest")
          
              connection = pika.BlockingConnection(
                  pika.ConnectionParameters(host=sys.argv[1],
                                            credentials=credentials))
          
              channel = connection.channel()
          
              result = channel.queue_declare(exclusive=True)
          
              channel.queue_bind(result.method.queue,
                                 exchange="my-exchange",
                                 routing_key="*.*.*.*.*")
          
              channel.basic_consume(callback_func, result.method.queue, no_ack=True)
          
              channel.start_consuming()
          

          然后运行:

          $ python single_consume.py host1
          $ python single_consume.py host2  # e.g. on another console
          

          如果您对来自队列的消息所做的工作是CPU-heavy,并且只要您的 CPU 中的核心数量 >= 消费者数量,通常最好使用这种方法 - 除非您的队列大部分时间都是空的时间和消费者不会使用此 CPU 时间*。

          异步

          另一种选择是涉及一些异步框架(例如Twisted)并在单线程中运行整个事情。

          您不能再在异步代码中使用BlockingConnection;幸运的是,pikaTwisted 的适配器:

          from pika.adapters.twisted_connection import TwistedProtocolConnection
          from pika.connection import ConnectionParameters
          from twisted.internet import protocol, reactor, task
          from twisted.python import log
          
          
          class Consumer(object):
              def on_connected(self, connection):
                  d = connection.channel()
                  d.addCallback(self.got_channel)
                  d.addCallback(self.queue_declared)
                  d.addCallback(self.queue_bound)
                  d.addCallback(self.handle_deliveries)
                  d.addErrback(log.err)
          
              def got_channel(self, channel):
                  self.channel = channel
          
                  return self.channel.queue_declare(exclusive=True)
          
              def queue_declared(self, queue):
                  self._queue_name = queue.method.queue
          
                  self.channel.queue_bind(queue=self._queue_name,
                                          exchange="my-exchange",
                                          routing_key="*.*.*.*.*")
          
              def queue_bound(self, ignored):
                  return self.channel.basic_consume(queue=self._queue_name)
          
              def handle_deliveries(self, queue_and_consumer_tag):
                  queue, consumer_tag = queue_and_consumer_tag
                  self.looping_call = task.LoopingCall(self.consume_from_queue, queue)
          
                  return self.looping_call.start(0)
          
              def consume_from_queue(self, queue):
                  d = queue.get()
          
                  return d.addCallback(lambda result: self.handle_payload(*result))
          
              def handle_payload(self, channel, method, properties, body):
                  print(body)
          
          
          if __name__ == "__main__":
              consumer1 = Consumer()
              consumer2 = Consumer()
          
              parameters = ConnectionParameters()
              cc = protocol.ClientCreator(reactor,
                                          TwistedProtocolConnection,
                                          parameters)
              d1 = cc.connectTCP("host1", 5672)
              d1.addCallback(lambda protocol: protocol.ready)
              d1.addCallback(consumer1.on_connected)
              d1.addErrback(log.err)
          
              d2 = cc.connectTCP("host2", 5672)
              d2.addCallback(lambda protocol: protocol.ready)
              d2.addCallback(consumer2.on_connected)
              d2.addErrback(log.err)
          
              reactor.run()
          

          这种方法会更好,您将使用的队列越多,消费者执行的工作对 CPU 的限制就越少*。

          Python 3

          由于您提到了pika,我将自己限制为基于 Python 2.x 的解决方案,因为尚未移植 pika

          但如果您想迁移到 >=3.3,一种可能的选择是将 asyncio 与 AMQP 协议之一(您与 RabbitMQ 对话的协议)一起使用,例如asynqpaioamqp

          * - 请注意,这些都是非常浅显的技巧 - 在大多数情况下,选择并不那么明显;什么对你最好取决于队列“饱和度”(消息/时间),你在收到这些消息后做了什么工作,你在什么环境中运行你的消费者等等;除了对所有实现进行基准测试之外,没有其他方法可以确定

          【讨论】:

          • 嗨@Unit03 我知道你已经在 2015 年回答了。我使用的是相同的扭曲适配器,我的心跳太多了。你可以访问我的问题以获取代码和更多描述。 stackoverflow.com/questions/62024116/…
          • 嗨 Vaibhav,我已经在这个问题下做出了回应。
          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多