xBis7 opened a new pull request, #71737:
URL: https://github.com/apache/airflow/pull/71737
<!-- SPDX-License-Identifier: Apache-2.0
https://www.apache.org/licenses/LICENSE-2.0 -->
<!--
Thank you for contributing!
Please provide above a brief description of the changes made in this pull
request.
Write a good git commit message following this guide:
https://chris.beams.io/posts/git-commit/
Please make sure that your code changes are covered with tests.
And in case of new features or big changes remember to adjust the
documentation.
For user-facing UI changes, please attach before/after screenshots (or a
short
screen recording) so reviewers can assess the visual impact.
Feel free to ping (in general) for the review if you do not see reaction for
a few days
(72 Hours is the minimum reaction time you can expect from volunteers) - we
sometimes miss notifications.
In case of an existing issue, reference it using one of the following:
* closes: #ISSUE
* related: #ISSUE
-->
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
https://github.com/apache/airflow/pull/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)
<img width="1980" height="614" alt="image"
src="https://github.com/user-attachments/assets/1593966c-285c-45c2-b008-28e3d765240d"
/>
Before it reached up to 94ms while after it never exceeded 20ms.
### Scheduler loop duration per iteration
<img width="1978" height="608" alt="image"
src="https://github.com/user-attachments/assets/d9ad6409-2bb1-4a71-8190-08f883f57fed"
/>
Iterations became faster.
### Scheduler vs worker vs worker slot utilization
<img width="3966" height="1218" alt="image"
src="https://github.com/user-attachments/assets/509bd139-742a-4a6a-a07a-ae0de0d7e29d"
/>
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
<img width="3944" height="614" alt="image"
src="https://github.com/user-attachments/assets/f9a521a9-48d3-4f24-b5ff-e33970749cc3"
/>
At the end, tasks end up waiting for workers instead of the scheduler.
### Increased number of running tasks
<img width="1992" height="624" alt="image"
src="https://github.com/user-attachments/assets/64d41d65-7866-423e-a955-9c18b69db2b0"
/>
### Waiting on the scheduler (SCHEDULED -> QUEUED) vs waiting for the
workers (QUEUED -> RUNNING)
<img width="3968" height="1216" alt="image"
src="https://github.com/user-attachments/assets/a52846cd-32df-44f0-a294-75dcea3a5267"
/>
Tasks wait less on the scheduler and more for a worker.
### Same number of tasks, finish faster
<img width="3970" height="624" alt="image"
src="https://github.com/user-attachments/assets/740afcdd-2748-4349-81aa-dbcf969af398"
/>
---
##### Was generative AI tooling used to co-author this PR?
<!--
If generative AI tooling has been used in the process of authoring this PR,
please
change below checkbox to `[X]` followed by the name of the tool, uncomment
the "Generated-by".
-->
- [X] Yes (please specify the tool below)
Claude code Opus 4.8
<!--
Generated-by: [Tool Name] following [the
guidelines](https://github.com/apache/airflow/blob/main/contributing-docs/05_pull_requests.rst#gen-ai-assisted-contributions)
-->
---
* Read the **[Pull Request
Guidelines](https://github.com/apache/airflow/blob/main/contributing-docs/05_pull_requests.rst#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](https://cwiki.apache.org/confluence/display/AIRFLOW/Airflow+Improvement+Proposals))
is needed.
* When adding dependency, check compliance with the [ASF 3rd Party License
Policy](https://www.apache.org/legal/resolved.html#category-x).
* For significant user-facing changes create newsfragment:
`{pr_number}.significant.rst`, in
[airflow-core/newsfragments](https://github.com/apache/airflow/tree/main/airflow-core/newsfragments).
You can add this file in a follow-up commit after the PR is created so you
know the PR number.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]