banana-creator-dev opened a new issue, #73147:
URL: https://github.com/apache/airflow/issues/73147
### Apache Airflow version
3.3.1
### If "Other Airflow 2/3 version" selected, which one?
_No response_
### What happened?
With OTel metrics enabled, the scheduler exports **two copies of its
metrics**: one from the main process, which behaves correctly, and one from a
forked child that never advances. The child's copy is republished on every
export interval, indefinitely, holding the counter values it inherited at the
moment of the fork.
The scheduler container runs the main process plus four forked children:
```
401440 ppid=401414 threads=18 elapsed=2488 airflow scheduler <- main
402459 ppid=401440 threads=17 elapsed=2473 airflow scheduler
402492 ppid=401440 threads=18 elapsed=2473 airflow scheduler
402524 ppid=401440 threads=15 elapsed=2473 airflow scheduler
407463 ppid=401440 threads=17 elapsed=2240 airflow scheduler <- exports
the frozen copy
```
The three children forked at startup are quiet. The one forked ~4 minutes
later — after the parent had already been counting — inherited a non-zero
accumulation and exports it forever:
```
airflow_scheduler_heartbeat{service_instance_id="airflow-scheduler-6545f66d48-tqgjv"}
277 (climbing)
airflow_scheduler_heartbeat{service_instance_id="c674b1e6-8df6-4fb1-ba05-161e0ce45725"}
44 (frozen, last sample 8s old)
```
Seven of the ten live `airflow_scheduler_*` series are duplicated this way,
and each scheduler restart mints another frozen set.
This looks like the other side of the fix for #64690. `get_otel_logger()`
now resets the SDK's `Once()` guard before `set_meter_provider()`:
```python
_metrics_internal._METER_PROVIDER_SET_ONCE._done = False
_metrics_internal._METER_PROVIDER = None
```
That gives a forked child a working provider where before it had a dead one.
Children that never emit anything again still get a live
`PeriodicExportingMetricReader` and keep flushing the instrument state they
inherited.
Two consequences:
1. **Duplicated series.** Anything that sums or averages across instances
double-counts while both copies are live.
2. **Per-series alerting on a heartbeat is impossible.**
`rate(airflow_scheduler_heartbeat[5m]) == 0` is the natural liveness check, and
it fires forever on the frozen copy while the scheduler is perfectly healthy.
The only workaround is to aggregate first — `sum(rate(...)) == 0` — which
silently gives up the ability to alert on one unhealthy instance among several.
### Why the two copies have different identities
Worth noting for anyone reproducing on an older SDK. `opentelemetry-sdk`
1.44 added `ServiceInstanceIdResourceDetector`, whose docstring says:
> because this detector is process dependent, providers refresh it
automatically after a fork and merge the newly detected value on top of the
existing resource. This means that if a user explicitly sets
`service.instance.id` (e.g. via OTEL_RESOURCE_ATTRIBUTES or
Resource.create(attributes=...)), that value will be overwritten with a newly
generated UUID the next time the process forks.
So on 1.44 the child's resource carries a fresh UUID and the frozen copy
lands in its own series, which is how this became visible at all. The child's
environment is correct — `Resource.create()` run by hand inside the same
container returns the pod name — the UUID comes from the post-fork refresh.
On earlier SDKs the child would carry the *same* `service.instance.id` as
the parent, so the two copies collide on one series and the frozen values
interleave with the real ones instead of sitting beside them. That is worse and
harder to notice.
### What you think should happen instead?
A forked child that does not emit metrics should not export. Stopping the
inherited metric reader in the child — for example from an
`os.register_at_fork(after_in_child=...)` hook, or by shutting the provider
down before re-initialising — would leave one copy of each series and make a
per-instance heartbeat check work again.
### How to reproduce
1. Airflow 3.3.1, `opentelemetry-sdk` 1.44.0, KubernetesExecutor.
2. Enable OTel metrics: `AIRFLOW__METRICS__OTEL_ON=True`, plus
`OTEL_METRICS_EXPORTER=otlp`, `OTEL_EXPORTER_OTLP_METRICS_ENDPOINT`,
`OTEL_EXPORTER_OTLP_METRICS_PROTOCOL=http/protobuf`,
`OTEL_METRIC_EXPORT_INTERVAL=30000`.
3. Let the scheduler run for ten minutes or so, long enough for it to fork a
child after the heartbeat has been counting.
4. Query `airflow_scheduler_heartbeat`. Two series appear: one climbing, one
frozen and still being re-sent.
`ps -eo pid,ppid,etimes,args` inside the scheduler container shows the
child, and its elapsed time matches the moment the frozen series first appears.
### Operating System
Ubuntu 24.04.4 LTS
### Versions of Apache Airflow Providers
_No response_
### Deployment
Official Apache Airflow Helm Chart
### Deployment details
KubernetesExecutor, one scheduler replica, k3s 1.36. Metrics go to an
OpenTelemetry Collector on each node over OTLP HTTP and on to Prometheus by
remote write.
### Anything else?
Happens on every scheduler start. The number of frozen series grows by one
set per restart.
### Are you willing to submit PR?
- [ ] 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]