divaltor opened a new issue, #77019:
URL: https://github.com/apache/airflow/issues/77019

   ### Under which category would you file this issue?
   
   Airflow Core
   
   ### Apache Airflow version
   
   3.4.0
   
   ### What happened and how to reproduce it?
   
   In Airflow 3.4.0 (Task SDK 1.4.0), `ActivitySubprocess.start` imports the 
task runner in the supervisor (`_import_task_runner_before_fork`). 
`WatchedSubprocess.start` then forks the task process with a bare `os.fork()`. 
After the fork, the cyclic GC in each process writes to the headers of the 
inherited objects. This splits the copy-on-write pages into private copies, so 
the supervisor and the task process each end up with their own copy of most of 
the inherited heap.
   
   With KubernetesExecutor, every task pod runs this pair of processes, so the 
duplicated heap is paid once per running task.
   
   `gc.freeze()` before the bare fork, with no unfreeze afterwards (the same 
pattern as #76016 for the triggerer), lowers the combined PSS of the two 
processes by 27-49 MB:
   
   | DAG file | PSS without freeze | PSS with freeze |
   |---|---|---|
   | One `EmptyOperator`, no other imports | 178-200 MB | 151 MB |
   | Same, plus `import pandas` at top level | 225-234 MB | 198 MB |
   
   Three runs each on `apache/airflow:slim-3.4.0-python3.12` (linux/arm64, 
pandas 3.1.1 installed for the second DAG). With the freeze, the result was the 
same on every run. The script below copies the supervisor's import and fork 
order, then reads `Pss` from `/proc/<pid>/smaps_rollup` after both processes 
run `gc.collect()`. This is a snapshot after the DAG parse. In a long task, 
each later full GC in the supervisor splits more pages, so the saving can grow 
over the task's runtime.
   
   
   ### What you think should happen instead?
   
   <details><summary>Repro script</summary>
   
   ```python
   import gc, os, sys, time
   import airflow.sdk.execution_time.supervisor  # noqa: F401
   import airflow.sdk.execution_time.task_runner  # noqa: F401  (as 
_import_task_runner_before_fork does)
   
   def pss(pid):
       with open(f"/proc/{pid}/smaps_rollup") as f:
           return next(int(l.split()[1]) // 1024 for l in f if 
l.startswith("Pss:"))
   
   mode, dag_file = sys.argv[1], sys.argv[2]
   if mode == "freeze":
       gc.freeze()
   r, w = os.pipe()
   pid = os.fork()
   if pid == 0:
       from airflow.dag_processing.dagbag import DagBag
       DagBag(dag_folder=dag_file)
       gc.collect()
       os.write(w, b"x")
       time.sleep(3)
       os._exit(0)
   os.read(r, 1)
   gc.collect()
   s, t = pss(os.getpid()), pss(pid)
   print(f"{mode} supervisor={s} task={t} total={s + t}")
   os.waitpid(pid, 0)
   ```
   
   Run it as `python repro.py nofreeze dag.py`, then as `python repro.py freeze 
dag.py`.
   
   </details>
   
   The same fix already exists for the LocalExecutor (#58365), the Dag 
processor (#60505), the Celery worker (#62212) and the triggerer (#76016). I 
found no issue or PR for the task supervisor.
   
   A possible placement is in `ActivitySubprocess.start`, so the Dag 
processor's per-file forks through `WatchedSubprocess.start` are not affected:
   
   ```python
   use_exec = target is _subprocess_main and task_process_uses_exec()
   if target is _subprocess_main and not use_exec:
       _import_task_runner_before_fork()
       # Forked once per supervisor and never unfrozen: an unfreeze would let 
the next
       # full GC split the pages the task process shares copy-on-write.
       gc.freeze()
   ```
   
   Why it looks safe:
   
   - The supervisor forks the task process once. The parent memory growth from 
repeated freeze/unfreeze cycles in the Dag processor (#60505) needs many forks.
   - Objects frozen in the supervisor are never collected. The supervisor lives 
for one task and keeps these objects anyway.
   - The freeze is skipped when `task_process_uses_exec()` is true (macOS, 
`execute_tasks_new_python_interpreter`), because `posix_spawn` shares no pages.
   - The task process still collects every object it creates after the fork.
   
   ### Operating System
   
   _No response_
   
   ### Deployment
   
   None
   
   ### Apache Airflow Provider(s)
   
   _No response_
   
   ### Versions of Apache Airflow Providers
   
   _No response_
   
   ### Official Helm Chart version
   
   Not Applicable
   
   ### Kubernetes Version
   
   _No response_
   
   ### Helm Chart configuration
   
   _No response_
   
   ### Docker Image customizations
   
   _No response_
   
   ### Anything else?
   
   _No response_
   
   ### Are you willing to submit PR?
   
   - [x] Yes I am willing to submit a PR!
   
   ### Code of Conduct
   
   - [x] I agree to follow this project's [Code of 
Conduct](https://github.com/apache/airflow/blob/main/CODE_OF_CONDUCT.md)
   


-- 
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]

  • [I] Freeze the GC before the t... divaltor (via GitHub) via commits

Reply via email to