Skip to content

Make finished TIs more lightweight when scanning task instances for scheduling - #71737

Merged
ashb merged 1 commit into
apache:mainfrom
xBis7:scheduler-ti-scan-improv
Oct 27, 2026
Merged

ashb merged 1 commit into
apache:mainfrom
xBis7:scheduler-ti-scan-improv

Conversation

@xBis7

@xBis7 xBis7 commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

The goal of this PR is to increase scheduler performance.

Scheduler investigation

In our production environment, sometimes schedulers become very slow under load.

During my investigation, I added debug spans for every major step of the scheduler's iteration loop to help understand the issue. There is an open PR with the changes

#69809

I ran various tests and with the help of the debug spans from the PR, I was able to identify a possible bottleneck.

When we have multiple dags with a lot of tasks running in parallel, the scheduler spends most of the time scanning the task instances of the active dag runs to decide which can be queued. It actually looks for SCHEDULED tasks that can be moved to QUEUED state.

Once the tasks are moved to QUEUED state, then the worker can pick them up and it's no longer up to the scheduler.

The scan is consisted of 2 parts. The actual DB query that fetches the data and the SQL-alchemy part which hydrates ORM objects with the query results. Basically, it creates taskinstance objects and fills them with data from each row that we get back. According to the spans, the query is very fast and the entire time is spent on the object hydration part.

The code distinguishes between finished and unfinished tasks but only after the query. It fetches all of them from the DB and creates objects and then splits them into 2 separate lists.

Solution

Let's assume that we have a dag run with 1000 tasks. If 100 tasks have finished, then during the scan we hydrate 100 objects for finished tasks and 900 for unfinished. Similarly, if 400 tasks have finished, then we hydrate 400 objects for finished tasks and 600 for unfinished. As the dag run progresses we get more and more finished tasks.

For finished tasks, we don't really need full ORM objects because we only read 5 fields and we never modify them. Instead of creating a full taskinstance object, we can hydrate a more lightweight immutable object with only the 5 necessary fields.

Instead of having 1 query that fetches everything, creates objects and then splits them into 2 lists, we can have 2 queries, one for unfinished tasks that hydrates full objects and one for finished tasks that hydrates lightweight read-only objects.

There is a scenario with dag callbacks where we need the actual taskinstance object in which case we fetch it from the DB. Still cheaper than the current approach.

Testing

Setup

  • 1 scheduler
  • 3 celery workers × 18 slots = 54 execution slots

Config

  • max_tis_per_query=16 (default)
  • max_dagruns_per_loop_to_schedule=20
  • parallelism, max_active_tasks_per_dag and default_pool_task_slot_count set to 4096 so that there isn't a limit other than the scheduler itself

Workload

  • 35 identical dag runs all start in parallel
  • each dag has a 1st deferrable task and then 9 sequential mapped task stages
  • when the deferrable task finishes, it returns a number that determines the number of tasks that each stage expands to

The deferrable task doesn't have any special meaning. It just helps to make sure that most of the mapped tasks are running in parallel.

So for example, if the 1st task returns 10, the 1st stage will expand into 10 tasks. Once these 10 finish, then the 2nd expands into 10 tasks. Once, these finish then the 3rd expands into 10 tasks and so on. And this is running for 35 parallel dags.

I used 70 for the number of mapped tasks, which translates into 9stages * 70tasks * 35dags = 22050 tasks and another 35 deferred, 22085 in total.

The code from main takes 35 minutes to run the 22000 tasks, while the code from this patch took 28 minutes. The more the load, the bigger the difference.

Here are some gathered metrics during the test. Left is the code from main and right is the code from this patch.

Time needed for each scan (the PR improvement)

image

Before it reached up to 94ms while after it never exceeded 20ms.

Scheduler loop duration per iteration

image

Iterations became faster.

Scheduler vs worker vs worker slot utilization

image

Scheduler is working at 100% in both cases but before it was spending most of its resources on the ORM hydration and it wouldn't hand enough tasks to workers.

I've set max slot occupancy to 54. Before the change, at any given moment we would have up to 34% of the 54 slots occupied. After, we reach 100% occupancy and that didn't occur once but for most of the time.

Red line is the worker slot occupancy.

Time waiting for the scheduler vs workers

image

At the end, tasks end up waiting for workers instead of the scheduler.

Increased number of running tasks

image

Waiting on the scheduler (SCHEDULED -> QUEUED) vs waiting for the workers (QUEUED -> RUNNING)

image

Tasks wait less on the scheduler and more for a worker.

Same number of tasks, finish faster

image
Was generative AI tooling used to co-author this PR?
  • Yes (please specify the tool below)

Claude code Opus 4.8


  • Read the Pull Request Guidelines for more information. Note: commit author/co-author name and email in commits become permanently public when merged.
  • For fundamental code changes, an Airflow Improvement Proposal (AIP) is needed.
  • When adding dependency, check compliance with the ASF 3rd Party License Policy.
  • For significant user-facing changes create newsfragment: {pr_number}.significant.rst, in airflow-core/newsfragments. You can add this file in a follow-up commit after the PR is created so you know the PR number.

@Nataneljpwd Nataneljpwd left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, hadn't had a look at the tests yet, there are questions that arose multiple times, I would appreciate if you could answer them

@xBis7

xBis7 commented Aug 26, 2026 •

Copy link
Copy Markdown
Contributor Author

@Nataneljpwd Thank you for the review! I've addressed all your questions.

@xBis7
xBis7 force-pushed the scheduler-ti-scan-improv branch from edd3176 to 06a3588 Compare September 7, 2026 09:50
@xBis7
xBis7 force-pushed the scheduler-ti-scan-improv branch from 06a3588 to 66da8df Compare September 14, 2026 11:05
@xBis7
xBis7 force-pushed the scheduler-ti-scan-improv branch 4 times, most recently from 24350a8 to 7644cf1 Compare September 30, 2026 13:49
@shahar1
shahar1 self-requested a review October 2, 2026 06:48

@shahar1 shahar1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall LGTM - could you please resolve all open threads if you resolved them?

@xBis7

xBis7 commented Oct 2, 2026

Copy link
Copy Markdown
Contributor Author

@shahar1 Thank you for the review! I've addressed all the comments but the threads have been open without any activity for more than a month, so I resolved them.

@Nataneljpwd If any of my answers don't make sense to you or you just disagree, add a comment to the corresponding thread and I will unresolve it.

@shahar1

shahar1 commented Oct 2, 2026

Copy link
Copy Markdown
Contributor

@ephraimbuddy / @uranusjr / @ashb - I'll appreciate a second look, thanks!

@xBis7

xBis7 commented Oct 2, 2026

Copy link
Copy Markdown
Contributor Author

@vatsrahul1001 has also worked on a similar improvement in the past. Rahul please take a look when you get a chance. Thanks!

@shahar1 shahar1 added this to the Airflow 3.4.0 milestone Oct 4, 2026
@xBis7
xBis7 force-pushed the scheduler-ti-scan-improv branch 3 times, most recently from 026dc2b to bbeaf74 Compare October 8, 2026 12:01
@xBis7
xBis7 force-pushed the scheduler-ti-scan-improv branch 3 times, most recently from e52699e to f3e3d8a Compare October 16, 2026 10:28
@vatsrahul1001 vatsrahul1001 added the backport-to-v3-4-test Backport to v3-4-test label Oct 18, 2026

@vatsrahul1001 vatsrahul1001 left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me. Finished task instances are read as lightweight rows while the unfinished ones stay full ORM objects, every consumer I traced only reads fields that FinishedTI has, and the Dag callbacks get a full TaskInstance back through the primary-key re-fetch.

@xBis7
xBis7 force-pushed the scheduler-ti-scan-improv branch 3 times, most recently from 3362392 to 5dee30e Compare October 19, 2026 12:06
@xBis7

xBis7 commented Oct 19, 2026

Copy link
Copy Markdown
Contributor Author

Because of all the task changes lately, I re-ran the benchmark tests and results are still good.

I also ran some tests with a small number of dag runs and tasks. The difference is negligible and it's actually in favor of the new code. The user won't know the difference but in the measurements, the total time is always 2-3 seconds less with the code in this PR.

@xBis7
xBis7 force-pushed the scheduler-ti-scan-improv branch from 5dee30e to e47d791 Compare October 21, 2026 17:38
@ashb ashb removed the backport-to-v3-4-test Backport to v3-4-test label Oct 23, 2026
@ashb

ashb commented Oct 23, 2026

Copy link
Copy Markdown
Member

Removing the backport label for now as it's not worth including this in 3.4.0. We will backport it once .0 is released.

@xBis7
xBis7 force-pushed the scheduler-ti-scan-improv branch 7 times, most recently from 1da0584 to 6b49ef1 Compare October 27, 2026 17:44
@xBis7
xBis7 force-pushed the scheduler-ti-scan-improv branch from 6b49ef1 to fe11402 Compare October 27, 2026 17:47
@xBis7

xBis7 commented Oct 27, 2026

Copy link
Copy Markdown
Contributor Author

@ashb I addressed all of your comments and I also re-ran all the benchmark tests. Results are still looking good. No issues from the new changes.

Please take another look when you get a chance. Thanks!

@ashb
ashb merged commit b5ce039 into apache:main Oct 27, 2026
68 checks passed
@xBis7
xBis7 deleted the scheduler-ti-scan-improv branch October 28, 2026 09:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants