Airflow2.0 Scheduler核心代码再解读

以下解读代码来源于Airflow2.0。
另外,使用非SQLite数据库的都是采用异步模式,下文讲解默认是指异步模式。

Airflow2.0给我的感觉有两点:

  1. 代码规范了很多,变量命名和模块划分更加清晰。
  2. 对于DAG的解析和scheduler过程已经使用DB来做解耦,这样不仅方便了Web页面的对DAG代码的展示,也方便分布式之间的协调。

Airflow Scheduler四个进程:

  • Scheduler 主进程
  • Executor 子进程
  • DagFileProcessorManager 子进程
  • DagFileProcessor 子进程(进程数量由DAG文件个数决定)

Airflow Scheduler启动过程

airflow控制台命令的解析与执行的代码都存放在cli目录,cli_parser.py是命令解析的入口,而启动scheduler的具体命令执行逻辑显然就存放在scheduler_command.py文件中。
在这里插入图片描述
以下代码来源于scheduler_command.py,当我们执行airflow scheduler -D 时会执行一下代码。

  • daemon作为一个守护进程,主要负责通过pidfile来保证不会重复运行多个scheduler进程,重定向stdout、stderr到指定的文件。
  • 然后执行了job.run()方法,job目前代码指定使用SchedulerJob类,SchedulerJob类是BaseJob类的子类,而BaseJob类还有其他两个子类:LocalTaskJob、BackfillJob,LocalTaskJob使用于单个任务实例,而BackfillJob适用于Backfill场景。
  • job.run()方法又调用了SchedulerJob实例的_executor()方法才真正进入scheduler环节。

with ctx 主要是fork了一个新进程,随后的job.run()在新进程执行,也即scheduler进程及其子进程都会在新进程执行。

    ctx = daemon.DaemonContext(
        pidfile=TimeoutPIDLockFile(pid, -1),
        files_preserve=[handle],
        stdout=stdout,
        stderr=stderr,
    )
    with ctx:
        job.run()

Airflow Scheduler运行原理

_executor()方法主要完成以下工作:

  1. 借助于父类BaseJob的变量初始化创建Executor实例,Executor实例由conf.get('core', 'EXECUTOR')获取,也就是从配置文件中获取,一般我们使用LocalExecutor或者CeleryExecutor,下面基于LocalExecutor讲解。

  2. 创建了DagFileProcessorAgent实例,传入DAG目录、processor_factory方法等参数。

    self.processor_agent = DagFileProcessorAgent(
        dag_directory=self.subdir, # DAG目录
        max_runs=self.num_times_parse_dags, # 仅用于测试,默认为-1,不会启用
        processor_factory=type(self)._create_dag_file_processor, # 创建DagFileProcessorProcess实例
        processor_timeout=processor_timeout, # 处理超时时间,由dag_file_processor_timeout参数决定,用于控制DagFileProcessor处理单个DAG的时间。
        dag_ids=[],
        pickle_dags=pickle_dags, #是否pickle dag,如果是就把pickle的结果写入dag_pickle表中
        async_mode=async_mode, # 是否支持异步模式,非sqlite的都是异步模式
    )
    
  3. 调用Executor实例的start()方法创建任务结果队列和work实例,并调用work.start()启动WORK进程用来执行任务或者命令,Airflow支持LocalExecutor、CeleryExecutor、DaskExecutor等,都是BaseExecutor的子类,此处针对LocalExecutor解析。
    1) 如果参数文件中[core]下的parallelism==0时work.start()方法并不会立即创建WORK进程,而是等其他进程调用了自己的execute_work方法时才会临时启动一个进程从队列获取任务并处理,然后退出。
    2) 如果该参数大于0时work.start()方法就会启动parallelism个WORK进程循环从队列中poll任务并执行,不会自动退出。
    当WORK进程执行完任务时会把任务结果put到任务结果队列中,这样其他进程就可以获取到任务结果。

    scheduler -->executor.start() -->LimitedParallelism实例-->QueuedLocalWorker进程
             |                      |
             |                      ------>UnlimitedParallelism实例->LocalWorkerBase进程
             |             
            -->executor.heartbeat -->executor.trigger_tasks -->executor.execute_async()-->limitedParallelism.execute_async()           
    
  4. 调用DagFileProcessorAgent实例的start()方法,启动DagFileProcessorManager进程。

       def start(self) -> None:
            """Launch DagFileProcessorManager processor and start DAG parsing loop in manager."""
            mp_start_method = self._get_multiprocessing_start_method()
            context = multiprocessing.get_context(mp_start_method)
            self._last_parsing_stat_received_at = time.monotonic()
    
            self._parent_signal_conn, child_signal_conn = context.Pipe()
            # 传递给_run_processor_manager的参数大多都来源于scheduler创建DagFileProcessorAgent实例时的参数,只是多了一个child_signal_conn管道用于接受管理信号。
            process = context.Process(
                target=type(self)._run_processor_manager,
                args=(
                    self._dag_directory,
                    self._max_runs,
                    # getattr prevents error while pickling an instance method.
                    getattr(self, "_processor_factory"),
                    self._processor_timeout,
                    child_signal_conn,
                    self._dag_ids,
                    self._pickle_dags,
                    self._async_mode,
                ),
            )
            self._process = process
    
            process.start()
    
            self.log.info("Launched DagFileProcessorManager with pid: %s", process.pid)
    
  5. 该进程的执行逻辑如下图,首先调用self._refresh_dag_dir()方法获取DAG文件目录的DAG文件到self._file_paths变量,然后调用self.prepare_file_path_queue()对self._file_paths中的DAG文件进行过滤再更新到self._file_path_queue。
    在这里插入图片描述
    在异步模式下会调用self.start_new_processes()方法为每一个DAG文件启动一个DagFileProcessor进程进行解析。self.start_new_processes()方法其实是先调用了scheduler传递给agent的_processor_factory参数值,也即 SchedulerJob的_create_dag_file_processor方法创建DagFileProcessorProcess实例,而代码中process.start()语句执行了以下代码创建DagFileProcessor处理进程。

    def start(self) -> None:
        """Launch the process and start processing the DAG."""
        start_method = self._get_multiprocessing_start_method()
        context = multiprocessing.get_context(start_method)
    
        _parent_channel, _child_channel = context.Pipe(duplex=False)
        process = context.Process(
            target=type(self)._run_file_processor,
            args=(
                _child_channel,
                _parent_channel,
                self.file_path,
                self._pickle_dags,
                self._dag_ids,
                f"DagFileProcessor{self._instance_id}",
                self._callback_requests,
            ),
            name=f"DagFileProcessor{self._instance_id}-Process",
        )
        self._process = process
        self._start_time = timezone.utcnow()
        process.start()
    
        # Close the child side of the pipe now the subprocess has started -- otherwise this would prevent it
        # from closing in some cases
        _child_channel.close()
        del _child_channel
    
        # Don't store it on self until after we've started the child process - we don't want to keep it from
        # getting GCd/closed
        self._parent_channel = _parent_channel
    

    需要注意的是,解析出的DAG实例并没有从DagFileProcessor进程返回,而是直接把DAG的调度信息写入dag表中,默认也会把DAG文件中的内容写入dag_code中,这个是由store_dag_code参数决定,这个和1.10版本差别比较大。

    # Whether to persist DAG files code in DB.
    # If set to True, Webserver reads file contents from DB instead of
    # trying to access files in a DAG folder.
    # Example: store_dag_code = False
    # store_dag_code =
    

    接DagFileProcessorManager进程的_run_parse_loop()方法就进入了while循环中,其逻辑也基本是_refresh_dag_dir、_find_zombies、_kill_timed_out_processors、prepare_file_path_queue、start_new_processes以及对Agent发送过来信号进行处理。

  6. 然后_executor方法进入loop状态,通过_do_scheduling调用_schedule_dag_run方法不断从数据库中检索所有满足条件dag_run,然后为每个dag_run的根据依赖关系生成即将执行的task_instance并更新其状态为SCHEDULED。然后_do_scheduling又调用_critical_section_execute_task_instances方法把SCHEDULED状态的任务如队列。这两步非常复杂,理解框架阶段不必过多纠结细节。不过_do_scheduling中的注释非常重要。在处理完_do_scheduling后scheduler主进程又调用了executor.heartbeat()方法,触发exector从任务队列中获取任务并执行。

    with create_session() as session:
    num_queued_tis = self._do_scheduling(session)
    
    self.executor.heartbeat()
    session.expunge_all()
    num_finished_events = self._process_executor_events(session=session)
    

    _do_scheduling中提到了:

    • dag_run的创建是相当耗费时间的,所以我们尽量不要有太多的DAG。另外,不要调大max_dagruns_to_create_per_loop参数
    • select dagruns的过程中使用了行级锁,但(Mariadb & MySQL < 8)不支持行级锁,所以使用使用MYSQL作为数据库的需要注意了。
    def _do_scheduling(self, session) -> int:
        """
        This function is where the main scheduling decisions take places. It:
    
        - Creates any necessary DAG runs by examining the next_dagrun_create_after column of DagModel
    
        Since creating Dag Runs is a relatively time consuming process, we select only 10 dags by default
        (configurable via ``scheduler.max_dagruns_to_create_per_loop`` setting) - putting this higher will
        mean one scheduler could spend a chunk of time creating dag runs, and not ever get around to
        scheduling tasks.
    
        - Finds the "next n oldest" running DAG Runs to examine for scheduling (n=20 by default, configurable
        via ``scheduler.max_dagruns_per_loop_to_schedule`` config setting) and tries to progress state (TIs
        to SCHEDULED, or DagRuns to SUCCESS/FAILURE etc)
    
        By "next oldest", we mean hasn't been examined/scheduled in the most time.
    
        The reason we don't select all dagruns at once because the rows are selected with row locks, meaning
        that only one scheduler can "process them", even it it is waiting behind other dags. Increasing this
        limit will allow more throughput for smaller DAGs but will likely slow down throughput for larger
        (>500 tasks.) DAGs
    
        - Then, via a Critical Section (locking the rows of the Pool model) we queue tasks, and then send them
        to the executor.
    
        See docs of _critical_section_execute_task_instances for more.
    
        :return: Number of TIs enqueued in this iteration
        :rtype: int
        """
    
  7. 最后,一旦_executor方法退出loop就会接着kill掉DagFileProcessorManager进程、Executor进程。当然,很少会出现这种情况。

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
你想了解关于 Airflow 代码方面的内容吗?可以具体说明一下你希望了解的部分吗?Airflow 是一个用于编排、调度和监控工作流的开源平台,它使用 Python 代码来定义工作流任务和依赖关系。这里有一些常见的 Airflow 代码示例: 1. 定义一个工作流任务(DAG): ```python from airflow import DAG from airflow.operators.bash_operator import BashOperator from datetime import datetime default_args = { 'start_date': datetime(2021, 1, 1), 'retries': 3, 'retry_delay': timedelta(minutes=5), } dag = DAG( 'my_dag', default_args=default_args, description='A simple DAG', schedule_interval='0 0 * * *' # 每天凌晨运行 ) task1 = BashOperator( task_id='task1', bash_command='echo "Hello, Airflow!"', dag=dag ) task2 = BashOperator( task_id='task2', bash_command='echo "Hello, Airflow again!"', dag=dag ) task1 >> task2 ``` 2. 定义一个 Python 函数作为任务: ```python from airflow import DAG from airflow.operators.python_operator import PythonOperator from datetime import datetime def my_function(): print("Hello, Airflow!") default_args = { 'start_date': datetime(2021, 1, 1), } dag = DAG( 'my_dag', default_args=default_args, description='A simple DAG', schedule_interval='0 0 * * *' ) task = PythonOperator( task_id='my_task', python_callable=my_function, dag=dag ) ``` 这些示例展示了如何使用 Airflow 定义工作流任务(DAG)以及任务之间的依赖关系。你可以根据自己的需求编写更复杂的任务和工作流。如果你有其他关于 Airflow 代码的问题,请随时提问!
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值