Django里面的AppConfig的ready()为什么被执行两次

为了在Django应用中,执行一些初始化,我们有时候会重载AppConfig的ready()方法。

这是因为,django.setup()方法中,会依次调用每个应用的ready()方法。

但是,如果我们使用python manage.py runserver来执行的时候,却会发现我们的ready()方法,会被执行两次。使用其它方法执行,比如在wsgi中,却没有这种现象。

这是什么原因呢?

我们先看一下django.setup()的实现,之后看一下python manage.py runserver的执行流程,就明白了。

django.setup()实现

在Django 5.1.7的django.setup(),是这样实现的:

def setup(set_prefix=True):  
    """  
    Configure the settings (this happens as a side effect of accessing the    first setting), configure logging and populate the app registry.    Set the thread-local urlresolvers script prefix if `set_prefix` is True.    """    from django.apps import apps  
    from django.conf import settings  
    from django.urls import set_script_prefix  
    from django.utils.log import configure_logging  
  
    configure_logging(settings.LOGGING_CONFIG, settings.LOGGING)  
    if set_prefix:  
        set_script_prefix(  
            "/" if settings.FORCE_SCRIPT_NAME is None else settings.FORCE_SCRIPT_NAME  
        )  
    apps.populate(settings.INSTALLED_APPS)

其中,settings.INSTALLED_APPS就是我们定义的应用列表,而apps.populate的执行过程如下:

    def populate(self, installed_apps=None):
        """
        Load application configurations and models.

        Import each application module and then each model module.

        It is thread-safe and idempotent, but not reentrant.
        """
        if self.ready:
            return

        # populate() might be called by two threads in parallel on servers
        # that create threads before initializing the WSGI callable.
        with self._lock:
            if self.ready:
                return

            # An RLock prevents other threads from entering this section. The
            # compare and set operation below is atomic.
            if self.loading:
                # Prevent reentrant calls to avoid running AppConfig.ready()
                # methods twice.
                raise RuntimeError("populate() isn't reentrant")
            self.loading = True

            # Phase 1: initialize app configs and import app modules.
            for entry in installed_apps:
                if isinstance(entry, AppConfig):
                    app_config = entry
                else:
                    app_config = AppConfig.create(entry)
                if app_config.label in self.app_configs:
                    raise ImproperlyConfigured(
                        "Application labels aren't unique, "
                        "duplicates: %s" % app_config.label
                    )

                self.app_configs[app_config.label] = app_config
                app_config.apps = self

            # Check for duplicate app names.
            counts = Counter(
                app_config.name for app_config in self.app_configs.values()
            )
            duplicates = [name for name, count in counts.most_common() if count > 1]
            if duplicates:
                raise ImproperlyConfigured(
                    "Application names aren't unique, "
                    "duplicates: %s" % ", ".join(duplicates)
                )

            self.apps_ready = True

            # Phase 2: import models modules.
            for app_config in self.app_configs.values():
                app_config.import_models()

            self.clear_cache()

            self.models_ready = True

            # Phase 3: run ready() methods of app configs.
            for app_config in self.get_app_configs():
                app_config.ready()

            self.ready = True
            self.ready_event.set()

所以,只要我们重载了AppConfig的ready()函数,就可以在这个函数里为所欲为。

总结一下就是,django.setup()会调用AppConfig的ready()方法。

runserver.py的执行流程

django/core/management/commands/runserver.py中,handle()的最后进入了run()方法:

def handle(self, *args, **options):  
    if not settings.DEBUG and not settings.ALLOWED_HOSTS:  
        raise CommandError("You must set settings.ALLOWED_HOSTS if DEBUG is False.")  
  
# 省略部分代码
    self.run(**options)

而run()的实现如下:

def run(self, **options):  
    """Run the server, using the autoreloader if needed."""  
    use_reloader = options["use_reloader"]  
  
    if use_reloader:  
        autoreload.run_with_reloader(self.inner_run, **options)  
    else:  
        self.inner_run(None, **options)

这里,按照是否开启--noreload参数的分支,有两个实现:autoreload.run_with_reload与self.inner_run

autoreload.run_with_reload的实现如下:

def run_with_reloader(main_func, *args, **kwargs):  
    signal.signal(signal.SIGTERM, lambda *args: sys.exit(0))  
    try:  
        if os.environ.get(DJANGO_AUTORELOAD_ENV) == "true":  
            reloader = get_reloader()  
            logger.info(  
                "Watching for file changes with %s", reloader.__class__.__name__  
            )  
            start_django(reloader, main_func, *args, **kwargs)  
        else:  
            exit_code = restart_with_reloader()  
            sys.exit(exit_code)  
    except KeyboardInterrupt:  
        pass

其中,DJANGO_AUTORELOAD_ENV的值为"RUN_MAIN"。

如果找到这个环境变量,则执行start_django()。如果没有找到,则执行restart_with_reloader()

restart_with_reloader()的实现为:

def restart_with_reloader():  
    new_environ = {**os.environ, DJANGO_AUTORELOAD_ENV: "true"}  
    args = get_child_arguments()  
    while True:  
        p = subprocess.run(args, env=new_environ, close_fds=False)  
        if p.returncode != 3:  
            return p.returncode

可以明显看出,restart_with_reloader的实现就是两步:

  1. 创建一个DJANGO_AUTORELOAD_ENV环境变量。
  2. 使用subprocess重新执行自身。

所以,我们明白了,runserver在没有开启–reload的情况下,会启动一个子进程,重新执行自身。

有了这两个实现打底,我们在回过头来,看一下manage.py调用runserver.py的过程。

manage.py执行流程

当我们在终端中输入python manage.py runserver之后,首先manage.py执行了main()函数,它调用了execute_from_command_line(sys.argv)

在execute_from_command_line中调用了execute:

def execute_from_command_line(argv=None):  
    """Run a ManagementUtility."""  
    utility = ManagementUtility(argv)  
    utility.execute()

ManagementUtility的execute方法实现如下:

def execute(self):  
    """  
    Given the command-line arguments, figure out which subcommand is being    run, create a parser appropriate to that command, and run it.    """    try:  
        subcommand = self.argv[1]  
    except IndexError:  
        subcommand = "help"  # Display help if no arguments were given.  
  
    # Preprocess options to extract --settings and --pythonpath.    # These options could affect the commands that are available, so they    # must be processed early.    parser = CommandParser(  
        prog=self.prog_name,  
        usage="%(prog)s subcommand [options] [args]",  
        add_help=False,  
        allow_abbrev=False,  
    )  
    parser.add_argument("--settings")  
    parser.add_argument("--pythonpath")  
    parser.add_argument("args", nargs="*")  # catch-all  
    try:  
        options, args = parser.parse_known_args(self.argv[2:])  
        handle_default_options(options)  
    except CommandError:  
        pass  # Ignore any option errors at this point.  
  
    try:  
        settings.INSTALLED_APPS  
    except ImproperlyConfigured as exc:  
        self.settings_exception = exc  
    except ImportError as exc:  
        self.settings_exception = exc  
  
    if settings.configured:  
        # Start the auto-reloading dev server even if the code is broken.  
        # The hardcoded condition is a code smell but we can't rely on a        # flag on the command class because we haven't located it yet.        if subcommand == "runserver" and "--noreload" not in self.argv:  
            try:  
                autoreload.check_errors(django.setup)()  
            except Exception:  
                # The exception will be raised later in the child process  
                # started by the autoreloader. Pretend it didn't happen by                # loading an empty list of applications.                apps.all_models = defaultdict(dict)  
                apps.app_configs = {}  
                apps.apps_ready = apps.models_ready = apps.ready = True  
  
                # Remove options not compatible with the built-in runserver  
                # (e.g. options for the contrib.staticfiles' runserver).                # Changes here require manually testing as described in                # #27522.                _parser = self.fetch_command("runserver").create_parser(  
                    "django", "runserver"  
                )  
                _options, _args = _parser.parse_known_args(self.argv[2:])  
                for _arg in _args:  
                    self.argv.remove(_arg)  
  
        # In all other cases, django.setup() is required to succeed.  
        else:  
            django.setup()  
  
    self.autocomplete()  
  
    if subcommand == "help":  
        if "--commands" in args:  
            sys.stdout.write(self.main_help_text(commands_only=True) + "\n")  
        elif not options.args:  
            sys.stdout.write(self.main_help_text() + "\n")  
        else:  
            self.fetch_command(options.args[0]).print_help(  
                self.prog_name, options.args[0]  
            )  
    # Special-cases: We want 'django-admin --version' and  
    # 'django-admin --help' to work, for backwards compatibility.    elif subcommand == "version" or self.argv[1:] == ["--version"]:  
        sys.stdout.write(django.get_version() + "\n")  
    elif self.argv[1:] in (["--help"], ["-h"]):  
        sys.stdout.write(self.main_help_text() + "\n")  
    else:  
        self.fetch_command(subcommand).run_from_argv(self.argv)

这里面,django.setup()会调用各个AppConfig的ready()方法,而最后的fetch_command会找到子命令的类:

# 省略部分
if isinstance(app_name, BaseCommand):  
    # If the command is already loaded, use it directly.  
    klass = app_name  
else:  
    klass = load_command_class(app_name, subcommand)  
return klass

,之后把命令行参数传给它。

而执行runserver的时候,如果没有开启--noreload参数,会再次创建一个子进程,执行整个命令行参数。

所以,我们的ready()会在django.setup()中被调用一次,之后再被传入runserver.py中,再次被reloader创建一个子进程,执行一次。

解决

源码看到这里,其实解决ready()调用两次的解决方案,已经有好几个了。

  1. 使用python manage.py调用runserver的时候,加上--noreload参数。
  2. ready()方法中,检查是否有DJANGO_AUTORELOAD_ENV,即"RUN_MAIN"环境变量。
  3. 不使用manage.py执行。
<think>嗯,用户想在Django项目启动的时候同时启动Kafka消费者来监听并消费消息。这个问题看起来需要把Kafka消费者集成到Django中,并且在服务启动时自动运行。那我应该怎么开始呢? 首先,Django项目启动的主要入口是使用manage.py运行服务器,比如runserver或者uwsgi等。但用户的需求是当项目启动时自动执行消费者,所以需要找到一个合适的切入点。 我记得Django有一些应用配置的类,可以在应用加载的时候执行某些代码。对的,AppConfigready()方法。这个方法会在Django启动的时候被调用,所以可能在这里启动消费者线程或者进程会比较合适。 那具体的步骤应该是这样的:首先在某个应用(比如kafka_app)的apps.py中定义自定义的AppConfig,重写ready()方法,然后在里面启动Kafka消费者。不过要注意,ready()方法可能会被调用多次,尤其是在开发环境下使用runserver的时候,可能会启动两个实例(一个是主进程,另一个是重载的进程)。所以需要确保消费者只启动一次。 接下来,需要处理消费者的启动方式。如果消费者是一个阻塞的过程,直接在主线程中启动会导致Django服务器无法继续运行。所以应该使用多线程或者异步任务来启动消费者,这样主线程可以继续处理Django的请求。 比如,在ready()方法中检查当前是否是主线程,然后启动一个守护线程来运行消费者的循环。或者使用像Kombu这样的库,但可能更简单的是直接使用threading模块。 然后是关于Kafka消费者的实现。需要安装confluent-kafka或者kafka-python这样的库。比如使用kafka-python中的KafkaConsumer,配置好topic、group_id等参数,然后在一个循环中不断拉取消息进行处理。 另外,需要考虑在Django项目关闭时如何优雅地关闭消费者。可能需要注册一个信号,比如在接收到SIGINT或SIGTERM时,调用消费者的close()方法,确保资源正确释放。 还有,可能需要处理数据库连接的问题,因为在多线程环境下,每个线程可能需要自己的数据库连接。Django的ORM默认是线程安全的,但最好在每个线程中按需创建连接,或者使用django.db.connection.ensure_connection()来确保连接。 测试的时候需要注意,如果在开发环境中使用runserver,可能会有两个进程,所以可能需要设置环境变量来判断是否是主进程,避免重复启动消费者。例如,可以通过os.environ.get('RUN_MAIN')来检查是否是自动重载的进程,这在runserver的情况下可能会有帮助。 还有,用户可能需要将消费者的逻辑放在一个单独的文件中,比如consumers.py,然后在AppConfig中导入并启动。这样结构更清晰,也方便维护。 另外,生产环境下可能需要用supervisor或者systemd来管理消费者进程,而不是依赖Django的启动。但用户的问题明确要求是在Django启动的同时执行,所以必须整合到Django项目中。 总结一下步骤: 1. 创建自定义AppConfig,在ready()方法中启动消费者线程。 2. 确保ready()方法只在主进程中执行一次。 3. 使用多线程或者异步方式启动消费者,避免阻塞主线程。 4. 处理消费者的正确关闭,注册信号处理函数。 5. 确保数据库连接在多线程环境下的正确处理。 6. 测试在开发和生产环境下的行为,避免重复启动。 可能的代码结构: 在apps.py中: ```python from django.apps import AppConfig import os class KafkaAppConfig(AppConfig): name = 'kafka_app' def ready(self): if os.environ.get('RUN_MAIN') == 'true' or os.environ.get('RUN_WEB') == 'true': # 根据环境变量判断 from .kafka_consumer import start_consumer start_consumer() ``` 或者使用线程: ```python import threading from django.apps import AppConfig import os class KafkaAppConfig(AppConfig): name = 'kafka_app' def ready(self): if not os.environ.get('DJANGO_CONSUMER_STARTED'): os.environ['DJANGO_CONSUMER_STARTED'] = '1' from .consumers import start_consumer consumer_thread = threading.Thread(target=start_consumer, daemon=True) consumer_thread.start() ``` 然后,在consumers.py中编写消费者逻辑: 使用kafka-python的话,可能像这样: ```python from kafka import KafkaConsumer import json def start_consumer(): consumer = KafkaConsumer( 'my_topic', bootstrap_servers='localhost:9092', group_id='my_group', auto_offset_reset='earliest', value_deserializer=lambda x: json.loads(x.decode('utf-8')) ) for message in consumer: process_message(message.value) def process_message(data): # 处理消息,可能需要调用Django的ORM from myapp.models import MyModel MyModel.objects.create(**data) ``` 不过要注意,在子线程中使用Django的ORM可能需要初始化,可以使用django.setup()或者在ready()方法中已经确保Django环境加载完成。 此外,处理信号关闭的话,可能需要添加信号处理: ```python import signal import sys def shutdown_handler(signum, frame): consumer.close() sys.exit(0) signal.signal(signal.SIGINT, shutdown_handler) signal.signal(signal.SIGTERM, shutdown_handler) ``` 不过这些可能需要放在消费者线程中处理。 还有,如果使用uWSGI,可能需要使用uWSGI的钩子函数,比如在ini文件中配置post-fork,但这样可能和Django的集成方式不同,需要进一步调整。 综上所述,关键点是在AppConfigready()中正确启动消费者线程,并确保只启动一次,同时处理好线程中的Django环境以及消费者的优雅关闭。</think>在Django项目中集成Kafka消费者并实现服务启动时自动监听消息,可以通过以下步骤实现: --- ### 一、核心思路 1. **利用DjangoAppConfig机制**:通过自定义应用配置类的`ready()`方法,在Django启动时触发消费者。 2. **多线程/异步执行**:避免阻塞主线程,使用守护线程运行消费者。 3. **环境判断**:防止开发服务器(`runserver`)因自动重载导致重复启动消费者。 4. **优雅关闭**:注册信号处理器,确保服务终止时正确关闭消费者。 --- ### 二、具体实现步骤 #### 1. 创建Django应用(如`kafka_app`) ```bash python manage.py startapp kafka_app ``` #### 2. 修改应用配置(`apps.py`) ```python # kafka_app/apps.py from django.apps import AppConfig import os import threading class KafkaAppConfig(AppConfig): name = 'kafka_app' def ready(self): # 仅在主线程且非自动重载时启动(避免runserver重复启动) is_runserver = os.environ.get('RUN_MAIN') == 'true' is_not_reloader = not os.environ.get('DJANGO_AUTORELOAD') if is_runserver or is_not_reloader: from .consumers import start_consumer consumer_thread = threading.Thread( target=start_consumer, daemon=True # 设置为守护线程,主进程退出时自动终止 ) consumer_thread.start() ``` #### 3. 注册自定义配置类(`__init__.py`) ```python # kafka_app/__init__.py default_app_config = 'kafka_app.apps.KafkaAppConfig' ``` #### 4. 编写Kafka消费者逻辑(`consumers.py`) ```python # kafka_app/consumers.py from kafka import KafkaConsumer import json import signal import sys from django.db import close_old_connections def process_message(message): # 在此处理业务逻辑(如操作Django ORM) from myapp.models import MyModel # 延迟导入避免循环依赖 data = json.loads(message.value) MyModel.objects.create(**data) close_old_connections() # 关闭旧数据库连接 def start_consumer(): consumer = KafkaConsumer( 'your_topic', bootstrap_servers=['localhost:9092'], group_id='django_consumer_group', auto_offset_reset='earliest', value_deserializer=lambda x: x.decode('utf-8') ) # 注册信号处理(优雅关闭) def shutdown(signum, frame): consumer.close() sys.exit(0) signal.signal(signal.SIGINT, shutdown) signal.signal(signal.SIGTERM, shutdown) # 开始消费消息 for message in consumer: process_message(message) ``` --- ### 三、关键注意事项 1. **依赖安装**: ```bash pip install kafka-python # 或使用confluent-kafka ``` 2. **线程安全**: - 使用`close_old_connections()`确保数据库连接池正确管理。 - 避免在消费者线程中直接使用全局Django对象。 3. **生产环境部署**: - 使用`uWSGI`或`gunicorn`时,建议通过`postfork`钩子启动消费者。 - 示例(uWSGI配置): ```ini [uwsgi] post-fork = exec(import('kafka_app.consumers').start_consumer()) ``` 4. **开发环境调试**: - `runserver`会启动两个进程(主进程+重载器),通过`RUN_MAIN`环境变量区分。 --- ### 四、替代方案:使用Django管理命令 若需更灵活控制(如单独启停消费者),可创建自定义命令: ```python # kafka_app/management/commands/consume_kafka.py from django.core.management.base import BaseCommand from kafka_app.consumers import start_consumer class Command(BaseCommand): help = '启动Kafka消费者' def handle(self, *args, **options): start_consumer() ``` 启动命令: ```bash python manage.py consume_kafka ``` --- ### 五、总结 通过上述方法,Kafka消费者将与Django服务生命周期绑定,实现: - **自动启动**:无需额外操作,随Django服务启动。 - **资源隔离**:独立线程运行,不阻塞主进程。 - **健壮性**:信号处理确保优雅关闭,避免消息丢失。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值