Kaustubh Butte created FLINK-40407:
--------------------------------------

             Summary: [FLINK-autoscaler] Add reason tags to 
`autoscaler.scalings` counter to explain why scaling was triggered
                 Key: FLINK-40407
                 URL: https://issues.apache.org/jira/browse/FLINK-40407
             Project: Flink
          Issue Type: Improvement
          Components: Autoscaler
    Affects Versions: 1.20.1, 2.0.0
            Reporter: Kaustubh Butte


Currently, the `autoscaler.scalings` counter is incremented whenever a scaling 
decision is applied, but there is no metric indicating WHY the autoscaler 
decided to scale. Understanding the scaling trigger is critical for:
 - Incident triage ("did it scale because of a traffic spike or a Kafka lag 
buildup?")
 - Validating autoscaler behavior ("is this job scaling due to genuine load or 
a false-positive signal?")
 - Building targeted alerts ("alert on backlog-driven scale-ups but not routine 
load adjustments")

The information needed to classify the reason is already computed in the 
autoscaler's evaluation pipeline but is discarded after the scaling decision is 
made.

*Proposed reasons:*

*Scale-up:*
|Reason|Condition|Meaning|
|`backlog`|`CATCH_UP_DATA_RATE.current > 0` and contributes >10% of 
`targetCapacity`|Consumer lag accumulated; scaling up to catch up|
|`high_load`|`CATCH_UP_DATA_RATE ~ 0` and `TRUE_PROCESSING_RATE < 
SCALE_UP_RATE_THRESHOLD`|Sustained input rate exceeds processing capacity|
|`input_spike`|`TARGET_DATA_RATE.current > 1.5 * 
TARGET_DATA_RATE.average`|Sudden traffic increase|

*Scale-down:*
|Reason|Condition|Meaning|
|`low_utilization`|`TRUE_PROCESSING_RATE > 
SCALE_DOWN_RATE_THRESHOLD`|Processing capacity far exceeds demand|

*Classification logic:*

The target capacity is already decomposed in 
`AutoScalerUtils.getTargetProcessingCapacity()`:
```
targetCapacity = lagCatchupRate + restartCatchupRate + inputTargetAtUtilization
```

A single vertex can have multiple reasons simultaneously (e.g., backlog AND 
high load both contributing to targetCapacity). And a single scaling cycle 
evaluates all vertices, each potentially with different reasons. Therefore, 
reasons are collected as a set per vertex, then aggregated across all vertices 
into a sorted pipe-delimited string.

*Per-vertex classification* (inside 
`JobVertexScaler.computeScaleTargetParallelism()`, inline with the decision):
 - If `lagCatchupRate / targetCapacity > 0.1` then add `backlog`
 - If `TARGET_DATA_RATE.current / TARGET_DATA_RATE.average > 1.5` then add 
`input_spike`
 - If scale-up and no specific reason matched, add `high_load` (fallback)
 - If scale-down, add `low_util`

*Job-level aggregation* (in `ScalingExecutor`):
 - Collect all reason sets from all vertices in the scaling summary
 - Merge into a single `TreeSet<String>` (alphabetically sorted)
 - Join with `|` delimiter: e.g., `"backlog|high_load"`

This approach prevents algorithm-reason drift because the classification is 
done inline in the same function that computes the scale factor, reading the 
same computed variables. If the algorithm changes, the developer sees the 
reason-tagging code right there.

*Maximum cardinality:* 15 distinct tag values (2^4 - 1 combinations of 4 atomic 
reasons). In practice fewer, since `high_load` is only added when no other 
scale-up reason applies.

*Implementation:*

1. Add `Set<String> scaleReasons` to `ParallelismChange` in `JobVertexScaler`, 
tag reasons inline alongside scale factor computation
2. Add `Set<String> reasons` to `ScalingSummary`, propagated from 
`ParallelismChange`
3. In `ScalingExecutor`, aggregate reasons from all vertices into a sorted 
pipe-delimited string
4. In `AutoscalerFlinkMetrics`, register tagged sub-counters dynamically using 
`MetricGroup.addGroup("reason", reasonTag).counter("scalings")`
5. Keep the existing `numScalings` counter incrementing for backward 
compatibility

*Result in metric reporters:*
 - `autoscaler.scalings` (total, backward compatible)
 - `autoscaler.scalings \{reason=backlog}` – pure backlog catch-up

 - `autoscaler.scalings \{reason=backlog|high_load}` – backlog + steady-state 
overload

 - `autoscaler.scalings \{reason=high_load}` – sustained input rate exceeds 
capacity

 - `autoscaler.scalings \{reason=input_spike}` – sudden traffic increase

 - `autoscaler.scalings \{reason=low_util}` – scale-down, overcapacity

 - (and other sorted combinations up to 15 total)

*Files affected:*
 - `JobVertexScaler.java` – add `Set<String> scaleReasons` to 
`ParallelismChange`, tag reasons inline
 - `ScalingSummary.java` – add `Set<String> reasons` field
 - `ScalingExecutor.java` – aggregate reasons across vertices into 
pipe-delimited string
 - `AutoscalerFlinkMetrics.java` – add dynamically registered tagged counters
 - `JobAutoScalerImpl.java` – pass aggregated reason string to metrics

All changes are in the `flink-autoscaler` module (standalone, 
platform-agnostic). Works with both K8s and YARN deployments.

*Relationship to other tickets:*

This is a companion to the `balanced` counter disambiguation ticket. Both can 
be implemented together or independently. Together they provide complete 
observability over every autoscaler decision cycle: why it scaled, why it 
didn't scale, or why it errored.



--
This message was sent by Atlassian Jira
(v8.20.10#820010)

Reply via email to