为了账号安全,请及时绑定邮箱和手机立即绑定

基于Django的微服务架构中使用Kafka实现可靠事件驱动通信(第二部分)

照片由 Dmitry Mashkin 拍摄,来自 Unsplash

在本系列的第一部分(点击这里阅读:第一部分)中,我们探讨了Kafka生产者在基于Django的微服务架构中的角色。我们讨论了生产者如何向Kafka主题发布消息,为服务之间的高效事件驱动通信打下了基础。

第二部分将重点转移到Kafka消费者,这些组件负责接收和处理这些消息。如前所述,消费者会自动从其订阅的主题中接收消息,并根据其业务逻辑处理这些消息。重要的是要指出,虽然一个消费者订阅了一个主题,但一个主题可以有多个订阅者,以相同的逻辑处理相同的数据。这些消费者在一起组成一个消费组。此外,即使是一个单独运行的消费者,也可以被视为是某个消费组的一部分。

为什么我们需要一个消费者团体呢?

基本上让一个消费者监听并处理一个主题的事件是没有问题的。然而,引入消费者组可以更好地平衡各个消费者接收和处理事件的工作量,确保没有单个消费者因为事件发布的速度而负担过重。

消费者集群中的负载均衡

一个消费者组可以由一个组ID来识别。通过这个ID,我们可以知道消费者属于哪个组。在演示过程中,我们将看到这个ID有多重要。

目前,我们还没有对库存微服务做太多工作,因为这里用户互动很少。然而,预设了一些食品项目的库存。这些都是与前一个微服务中订购项目具有相同名称的相同食品项目。

食品库存记录

我们如何配置消费者取决于其将要接收的消息的重要性。作为一家公司,我们发布的每一条消息都非常重要。我们希望库存表中的quantity数据与实际仓库中的物理数量保持一致;换句话说,确保零数据丢失是我们的首要任务。

当库存微服务出现问题但订单服务仍在发布消息时,即使在库存微服务恢复之后,也无法处理一些消息以更新食品库存数量数据。为了解决这个问题,偏移提交确认和消费者组ID都起着重要作用,来确保消息的正确处理。

可以把偏移想象成书中的页码。每个数字都对应那一页的内容。这样,我们就可以快速引用一条信息,而无需重新翻阅整本书。在这个上下文中,偏移就像是发往某个主题的消息的页码。

提交就是消费者跟踪偏移量的过程。假设你因为某种原因暂时停止了阅读一本小说。当你再次拿起书时,你可能会忘记上次读到的那一页,很难找到你上次停下来的地方。这是因为你没有记住页码。记住页码可能意味着你在那一页做个记号或者把那一页折起来,虽然有些人可能认为这是不好的做法。提交偏移确保了当消费者重新开始阅读(或消费消息)时,可以从上次停止的地方继续,不会错过或重复任何数据。

配置消费者需要一些参数:

  • 引导服务器(bootstrap servers): 此参数包含 broker 的连接字符串值,用于将消费者连接到 Kafka 集群。
  • 键反序列化器和值反序列化器: 它们的工作方式与键和值序列化器相反。它们接收字节数组并将其转换回数据类型。
  • enable-auto-commit: 我们可以手动提交消费者的偏移量,但当此参数默认设置为 true 时,消费者的偏移量将自动在周期间隔内提交。
  • 组 ID: 这是一个可选特性,用于标识消费者所属的消费者组。如果没有提供组 ID,消费者的偏移量提交将被禁用。在这里,我们希望它存在。
  • auto-commit-interval-ms: 这只是消费者应定期提交偏移量的间隔时间(以毫秒为单位)。默认情况下,此值为 5000 毫秒,即 5 秒。可以将其设置为较低的值,从而加快偏移量提交的速度,从而减少发布的数据丢失。

有了对这些论点的理解后,我们现在可以为我们的库存微服务配置消费者了。我们安装 kafka-python

在命令行输入以下命令来安装kafka-python包:`pip install kafka-python`。kafka-python是一个用于与Apache Kafka集成的Python库。

在这一步之后,我们稍微改变了设置。与生产者不同,生产者只在需要时才向代理发送消息,消费者则不能如此运作。它必须时刻准备接收新消息,不论是否有新的消息被发送。与此同时,它还必须继续执行其当前的任务流程。因此,生产者和消费者之间的任务量比率大约为1:2。

知道这一点,我们如何确保在执行其他进程的同时,库存服务以消费者模式运行呢?虽然Django默认采用单线程模式,但它也可以支持多线程。这意味着它最初设计为处理单一任务序列,但Python的多线程能力使得它可以同时处理程序的多个部分。换句话说,我们可以让消费程序在与框架主线程不同的独立线程上运行。

你可能想知道为什么需要为消费者单独设置一个线程。这是因为在一个主线程中运行消费者会阻塞Django服务器的运行。一旦消费者开始监听消息时,它就会阻止服务器执行其他指令,从而实际上让整个应用程序停止运行。通过在单独的线程中运行消费者,这样服务器就能保持响应,并继续处理请求,而消费者则可以在后台处理消息,而不影响其他操作。

配置消费者端:

    ## utils/kafka/消费者.py  ## utils/kafka/consumer.py 
    from kafka import KafkaConsumer  # 导入KafkaConsumer类
    import json  # 导入json模块
    from utils.update_inventory import update_inventory  # 从utils.update_inventory导入update_inventory函数
    topics = ['orders']  # 定义主题为'订单'
    consumer = KafkaConsumer(bootstrap_servers="localhost:29092",  # 创建一个Kafka消费者实例
                            value_deserializer=lambda m: json.loads(m.decode('ascii')),
                            group_id="inventory_relation",
                            auto_commit_interval_ms=1000,
                            api_version=(2, 0, 0)
    )
    consumer.subscribe(topics)  # 订阅主题列表
    while True:  # 开始无限循环
      all_records = consumer.poll(timeout_ms=100, max_records=100)  # 获取消费者的消息
      for topic_partition, messages in all_records.items():  # 遍历所有消息
        if topic_partition.topic == "orders":  # 如果主题为'订单'
          update_inventory(messages)  # 更新库存

一开始,我们就能看到消费者将订阅的 Kafka 主题列表。在我们的演示中,这个列表中仅包含 orders 主题。我们的消费者配置为使用 bootstrap_servers 参数连接到本地的 Kafka 代理端点。通过 value_deserializer,消息值将被反序列化为 Python 对象。消费者组被配置为 group_idinventory_relation。然后我们将消费者的自动提交间隔设置为 1000 毫秒(即 1 秒),它将订阅 Kafka 主题。

当我们的消费者在订阅的主题中无限循环时,它对每个主题以100毫秒的轮询超时进行轮询,每个轮询最多获取100条记录。这些主题的记录被打包成一个字典并返回处理。该字典的键是Kafka的主题分区,每个主题分区的值是消息列表。

对于我们业务逻辑来说,每当有属于 orders 主题的消息时,我们就会调用 update_inventory 函数。下面就是这个函数的定义。

    ## utils/更新库存.py  # 更新库存量
    from inventories.models import FoodItemInventory
    def 更新库存量(messages):
      for message in messages:
          订购商品 = message.value

          for 商品 in 订购商品:
            库存食品项 = FoodItemInventory.objects.get(name=商品['name'])
            当前库存量 = 库存食品项.quantity
            新库存量 = 当前库存量 - 商品['quantity']
            库存食品项.quantity = 新库存量
            库存食品项.save()
            print(f" 食品项 {库存食品项.name} 的数量已从 {当前库存量} 减少为 {库存食品项.quantity}。")

我们可以看到这个函数处理一个消息列表。对于每个消息,它会处理该消息相关的项目列表。对于每个项目,会获取一个与项目名称相同的 FoodItemInventory 记录。该记录的数值会减去从订单微服务中订购的相应食品的数量。然后更新并保存库存记录,并打印一条消息,显示更新后的食品数量。

配置了消费者并应用了业务逻辑之后,这只是我们需要为消费者完成的工作的一半。接下来的任务是添加配置,以确保消费者能够在Django服务器运行的同时继续运行。我们可以使用Python的Threading模块来实现这一点,该模块可以帮助程序启动额外的线程,并使用Django的自定义命令,以创建一个激活我们线程的命令。

定义我们Kafka消费者端的线程(Thread):

## threads/kafka_consumer_thread.py  
from threading import Thread  
from utils.kafka.consumer import consume_kafka_messages  
class ConsumeKafkaMessageThread(Thread):  
  def __init__(self):  
    Thread.__init__(self)  

  def run(self):  
    print("启动 Kafka 消费者线程")  
    consume_kafka_messages()

在上面的代码段中,我们从 threading 模块导入了 Thread 类,用于多线程操作。接着,我们从 kafka.consumer.py 文件导入了 consume_kafka_messages 函数。定义了一个继承自 Thread 类的 ConsumeKafkaMessageThread 类。我们在 run 方法中执行消费 Kafka 消息的函数。然后我们回到 consumer.py 文件,对其进行了一些调整,以满足线程类的需求。

    ## utils/kafka/consumer.py  

    # 最近修改  
    from kafka import KafkaConsumer  
    import json  
    from utils.update_inventory import update_inventory  
    def consume_kafka_messages():  
      topics = ['orders']  
      consumer = KafkaConsumer(bootstrap_servers="localhost:29092",  
                              value_deserializer=lambda m: json.loads(m.decode('ascii')),  
                              group_id="inventory_relation",  
                              auto_commit_interval_ms=1000,  
                              api_version=(2, 0, 0)  
      )  
      consumer.subscribe(topics)  
      while True:  
        all_records = consumer.poll(timeout_ms=100, max_records=100)  
        for topic_partition, messages in all_records.items():  
          if topic_partition.topic == "orders":  
            update_inventory(messages)

现在我们已经定义好了消费者线程,让我们为激活它定义一个命令。我们希望消费者线程在执行命令 python3 manage.py runserver 时,立即开始运行。在我们的 Django inventories 应用里,创建 management/commands/ 文件夹。其中 management 是父文件夹,commands 是它的子文件夹。在 commands 文件夹中,创建两个文件:__init__.pykafka_consumer.py

Kafka 消费者自定义命令文件夹的结构。

在《kafka_consumer.py》文件中,我们编写了如下代码:

    from typing import Any  
    from django.core.management.base import BaseCommand  
    from threads import kafka_consumer_thread  

    class Command(BaseCommand):  
      help = "启动Kafka消费者"  
      def handle(self, *args: Any, **options: Any) -> str | None:  
        kafka_consumer = kafka_consumer_thread.ConsumeKafkaMessageThread()  
        kafka_consumer.start()

这创建了一个用来运行我们 ConsumerKafkaMessageThread 类的命令。该命令的描述是“启动 Kafka 消费者”。为了使该命令在服务器启动时自动运行,我们需要对我们的 inventories 应用配置做一些小的修改。

    ## inventories/apps.py

    from django.apps import AppConfig
    import os
    from django.core.management import call_command

    class InventoriesConfig(AppConfig):
        default_auto_field = 'django.db.models.BigAutoField'
        name = 'inventories'
        def ready(self) -> None:
            if os.environ.get('RUN_MAIN'):
                如果环境变量 RUN_MAIN 存在,则向 kafka 问好...
                print("向 kafka 问好...")
                调用命令 'kafka_consumer'
                call_command('kafka_consumer')

注意:在保持代码风格一致的前提下,对注释进行了调整,使其更符合中文口语习惯,同时保持了代码的准确性和可读性。

这个代码片段定义了 ready 方法。当应用准备就绪时,Django 将会检查它是否运行在主线程上。如果是的话,会调用 kafka_consumer 命令来执行相应的操作。这相当于在终端中运行 python3 manage.py kafka_consumer 命令。

在不同的运行环境中运行我们的微服务:

订单处理微服务

库存管理微服务运行

为了测试我们的服务,我们会用 docker compose up 来启动我们的 Kafka 服务,并点一些食品。我们用三个不同的食品来测试。

下单前的商品数量

你可以看到,在我们下单前,我们有300个鸡排、50个苹果和683根香蕉。

我们现在把6个鸡翅加到购物车里了。

加 4 个苹果。

还有10根香蕉。

于是我们就下单了。

我们的订单项会作为消息发布到Kafka代理服务器。我们应该看到库存已经相应地更新了。

下单后的数量

为了测试我们消费者配置在停机期间的效果,暂停库存微服务几秒钟,然后下单购买6个苹果,几秒钟后再启动库存微服务。

再次运行微服务后,你会看到更新后的值。现在苹果的价格已经降到了40元。

订购的商品数量

最后啊……

本文探讨了Kafka消费者在Django微服务架构中确保可靠的事件驱动通信的可靠性所起的关键作用。我们探讨了消费者如何接收和处理来自Kafka主题的消息,以及偏移管理的重要性,因此,偏移管理对于确保即使在服务中断期间也能保持数据的一致性至关重要。

我们也详细介绍了在Django中配置Kafka消费者的实际步骤,包括线程处理以保持应用程序的响应性。通过采用这些方法,您可以增强微服务的可靠性,确保您的系统能够处理事件并最大限度地减少数据丢失的风险。

希望下次见,亲爱的朋友或读者,继续加油……

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消