我已经写了几篇关于这个主题的文章了,不过我觉得再写一篇把我的观点说清楚是个不错的主意。
用于像Django和FastAPI这样的框架的后台任务管理器,实际上只是围绕消息代理构建的简单包装器。它们允许你将一些代码标记为后台任务,并在某些情况下支持调度任务,但这些功能仍然基于消息代理。换句话说,使用它们中的任何一个,你都需要安装一个消息代理。我的建议是直接使用消息代理。
RabbitMQ 主题交换
所以你可能会对自己说“消息代理似乎有些奇怪。它们使用一种奇怪的语言,我还得学这个新东西”。不过,读一点之后,你会发现消息代理其实并没有那么复杂。以上提到的主题交换就是这样。这里你只需将消息发送给代理,并附上一个路由码,这就像一个邮箱地址。在另一个进程中,你可以连接到代理并要求接收这个邮箱地址的消息。除了基础功能之外,你还可以让多个进程同时从这个邮箱地址接收消息,还可以指定每个进程一次能从邮箱地址获取多少消息。最后,你可以处理那些崩溃的进程,但如果消息很重要,就必须确保消息能够继续传递。
这一切都为你准备好了,随时等你来用。
我还想说一点其他的看法。Celery和其他任务管理器其实并不容易设置。你需要定义工作者、任务的位置、它们何时运行,以及其他很多内容。直接处理消息代理和进程会让你明白自己在做什么,而不会被其他试图为你简化事情的代码隐藏真相。而在大多数情况下,这些代码实际上并不能真正帮到你。
消费者们(#消费者)
消息中间件可以以其他方式工作,但最常见的是发布者—消费者模型。这意味着一个进程发送消息,而另一个进程接收消息。发布者和消费者之间没有直接联系,并且发布者不会等待回复。
那么我们来看看一个实际的消费者。
import asyncio
import aio_pika
import json
async def process_message(message: aio_pika.abc.AbstractIncomingMessage) -> None:
# 处理接收到的消息
async with message.process():
msg = message.body.decode()
data = json.loads(msg)
# 打印数据
print(data)
await asyncio.sleep(1)
async def main() -> None:
# 连接到AMQP服务器
conn = await aio_pika.connect_robust("amqp://guest:guest@127.0.0.1/")
channel = await conn.channel()
exchange = await channel.declare_exchange("test_exchange", type="topic")
queue = await channel.declare_queue()
await queue.bind(exchange, routing_key="routine1_json")
await queue.bind(exchange, routing_key="routine2_json")
# 消费消息
await queue.consume(process_message)
try:
await asyncio.Future()
finally:
# 关闭连接
await conn.close()
if __name__ == "__main__":
# 运行主函数
asyncio.run(main())
首先要注意的是,这里我使用了异步处理,因此我选择了aio-pika,而不是Pika。其次,需要注意的是,提到的“queue”与你在Python中的“队列”概念无关。这里只展示了一部分连接的代码。
代码的重要部分是它链接到了一个类似于邮局的交换,然后请求标记为“routine1_json”和“routine2_json”的消息。需要注意的是,我本可以在tasks中启动4到5个消费者,然后在main()
中启动它们,这样在一个进程中可以同时运行4到5个异步线程。另外需要注意的是,当消息进来时,它会被传递给另一个函数处理。这种情况在使用这种异步系统的编码中非常常见。不需要使用循环来处理消息。
与消费者不一样,发布者只需发送消息,随后就不再管了。除了经纪人确认收到消息之外,发布者不会等待消息反馈。
import asyncio
import aio_pika
import json
# 这里我们使用 aio_pika 连接到 RabbitMQ 服务器,使用的是 amqp 协议
async def main() -> None:
conn = await aio_pika.connect_robust("amqp://guest:guest@127.0.0.1/") # 连接是健壮的,能够处理网络问题
async with conn:
routing_key = "test_json_topic" # 路由键用于匹配交换器中的消息
channel = await conn.channel()
data = json.dumps({"message": "Hello World!", "detail": "example json"}).encode() # 将数据编码为二进制格式
exchange = await channel.declare_exchange("test_exchange", type="topic") # 声明一个 topic 类型的交换器
await exchange.publish(aio_pika.Message(body=data), routing_key=routing_key) # 发布消息到指定的路由键
if __name__ == "__main__":
asyncio.run(main())
这个运行超级快,所以你可以从视图内部调用该程序而没有任何问题。我们只需生成一个文本字符串,并使用路由键将其发送给交换机。搞定!
注意一下,出版社并没有说明是谁。没有发件人,只有收件人地址。
与任务管理器相比你可以用任务管理器标记代码中的某些功能,以便在后台运行。然后启动工作进程,等待消息来指定运行哪个任务。这在处理许多不同的小任务时非常有用。但实际上情况通常并非如此。很多时候,实际上只有少数几个任务需要在后台运行,而在某些情况下,这些任务可能非常复杂,耗时很长,或占用大量CPU资源。
在大多数情况下,将代码库分开也会更理想。你可能还想控制这些任务的运行时间、数量和地点。直接与消息代理打交道会更好处理这种情况。你可以创建拥有不同路由键的多个消息交换。你可以通过让进程休眠或在进程停止时确保队列不被删除来停止队列中的消息处理。
超越后台任务,更上一层楼想象你有这样一个系统,它包含20个应用程序,你希望所有应用的消息都能够回传到一个中心应用。你可能认为这很简单,只需要将结果发送到中心应用的API端点就可以了。
这里有几个你可能没想到的点。
中央应用程序与其他应用程序隔离开来,这些应用不得调用该应用程序的 API 接口。
位置(如 IP 地址等)可能会在中央应用那边变动,你不能将它硬编码到 20个应用中。
主应用必须依次处理消息,以避免竞态条件。
如果我们用消息代理,我们就能解决这些问题。
将消息代理服务器放置在一个可以让这20个应用访问的位置。它们可以将消息发送到该代理服务器。中心应用会从代理获取消息,因此只要消费者位于防火墙内部,防火墙就不会成为问题。
通过使用消息代理,这20个应用程序不必关心中心组件的地址,只需要关心消息代理的地址。同时,如果中心组件被移动,也不会有影响,因为它只关心消息代理的地址。
我们可以向消息代理发送数百条消息,这也没问题。中央应用程序只需建立一个连接,逐条获取消息。这也可以作为一种方式来控制流入中央应用程序的消息量,防止其过载。此过程还可以用于平衡传入消息的负载。
结论部分如果你处理的是处理大量数据的应用程序,负载均衡、任务分配和流量限制是关键特性。后台任务管理系统无法处理这种类型的工作,而像 RabbitMQ 这样的消息中间件就是为此而设计的。直接学习如何使用这些代理可以为你的应用程序提供未来的保障,并从而使其更加可扩展。