Skip to content

Commit 64a3d8d

Browse files
authored
Add support for partition based scaling on the kafka scaler (#6558)
Scaling kafka consumers should be done in factors of partition count on the topic. This is to ensure that the partitions are evenly spread across all consumers. If the paritions are not evenly spread we run the risk of some partitions being consumed faster than other. This PR adds a new property on the kafka scaler `ensureEvenDistributionOfPartitions`. When this property is set to true the scaler ensure that the number of pods are always evenly spread across the number of topics on the partition. Signed-off-by: Vishal Patel <38330754+patelvp@users.noreply.github.com>
1 parent 3615974 commit 64a3d8d

9 files changed

Lines changed: 509 additions & 161 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,12 +85,14 @@ To learn more about active deprecations, we recommend checking [GitHub Discussio
8585
- **General**: Allow excluding labels from being propagated from ScaledObject and ScaledJob to generated HPA and Job objects ([#6849](https://github.com/kedacore/keda/issues/6849))
8686
- **General**: chore: only add webhook DNS names when webhook patching is enabled ([#7002](https://github.com/kedacore/keda/issues/7002))
8787
- **General**: Improve Events emitted from ScaledObject controller ([#6802](https://github.com/kedacore/keda/issues/6802))
88+
- **Apache Kafka Scaler**: Add support for even distribution of partitions to consumers ([#2581](https://github.com/kedacore/keda/issues/2581))
8889
- **Azure Pipelines Scaler**: Add new `fetchUnfinishedJobsOnly` property to fetch only unfinished pipeline jobs for a pool ([#6819](https://github.com/kedacore/keda/issues/6819))
8990
- **Datadog Scaler**: Add a specific timeout configuration parameter for the Datadog trigger ([#6999](https://github.com/kedacore/keda/pull/6999))
9091
- **Datadog Scaler**: Fix bug with datadogNamespace config ([#6828](https://github.com/kedacore/keda/pull/6828))
9192
- **Datadog Scaler**: Improve Datadog scaler error messages ([#6999](https://github.com/kedacore/keda/pull/6999))
9293
- **Github Scaler**: Add support to control unlabeled job/runner matching ([#6900](https://github.com/kedacore/keda/issues/6900))
9394
- **InfluxDB Scaler**: Add support for InfluxDB v3 ([#6981](https://github.com/kedacore/keda/issues/6981))
95+
- **Kafka Scaler**: Add support for even distribution of partitions to consumers ([#2581](https://github.com/kedacore/keda/issues/2581))
9496
- **Metrics API Scaler**: Support AuthParams for authMode ([#6939](https://github.com/kedacore/keda/issues/6939))
9597
- **Metrics API Scaler**: Support multiple auth methods simultaneously ([#6642](https://github.com/kedacore/keda/issues/6642))
9698
- **RabbitMQ Scaler**: add `DeliverGetRate`, `PublishedToDeliveredRatio` and `ExpectedQueueConsumptionTime` trigger modes to RabbitMQ scaler ([#7071](https://github.com/kedacore/keda/issues/7071))

‎pkg/scalers/apache_kafka_scaler.go‎

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,9 @@ type apacheKafkaMetadata struct {
6060

6161
// If an invalid offset is found, whether to scale to 1 (false - the default) so consumption can
6262
// occur or scale to 0 (true). See discussion in https://github.com/kedacore/keda/issues/2612
63-
ScaleToZeroOnInvalidOffset bool `keda:"name=scaleToZeroOnInvalidOffset, order=triggerMetadata, optional"`
64-
LimitToPartitionsWithLag bool `keda:"name=limitToPartitionsWithLag, order=triggerMetadata, optional"`
63+
ScaleToZeroOnInvalidOffset bool `keda:"name=scaleToZeroOnInvalidOffset, order=triggerMetadata, optional"`
64+
LimitToPartitionsWithLag bool `keda:"name=limitToPartitionsWithLag, order=triggerMetadata, optional"`
65+
EnsureEvenDistributionOfPartitions bool `keda:"name=ensureEvenDistributionOfPartitions, order=triggerMetadata, optional"`
6566

6667
// SASL
6768
SASLType kafkaSaslType `keda:"name=sasl, order=triggerMetadata;authParams, enum=none;plaintext;scram_sha256;scram_sha512;gssapi;aws_msk_iam, default=none"`
@@ -505,9 +506,15 @@ func (s *apacheKafkaScaler) getTotalLag(ctx context.Context) (int64, int64, erro
505506

506507
s.logger.V(1).Info(fmt.Sprintf("Kafka scaler: Consumer offsets %v, producer offsets %v", consumerOffsets, producerOffsets))
507508

508-
if !s.metadata.AllowIdleConsumers || s.metadata.LimitToPartitionsWithLag {
509+
if !s.metadata.AllowIdleConsumers || s.metadata.LimitToPartitionsWithLag || s.metadata.EnsureEvenDistributionOfPartitions {
509510
// don't scale out beyond the number of topicPartitions or partitionsWithLag depending on settings
510511
upperBound := totalTopicPartitions
512+
// Ensure that the number of partitions is evenly distributed across the number of consumers
513+
if s.metadata.EnsureEvenDistributionOfPartitions {
514+
nextFactor := getNextFactorThatBalancesConsumersToTopicPartitions(totalLag, totalTopicPartitions, s.metadata.LagThreshold)
515+
s.logger.V(1).Info(fmt.Sprintf("Kafka scaler: Providing metrics to ensure even distribution of partitions on totalLag %v, topicPartitions %v, evenPartitions %v", totalLag, totalTopicPartitions, nextFactor))
516+
totalLag = nextFactor * s.metadata.LagThreshold
517+
}
511518
if s.metadata.LimitToPartitionsWithLag {
512519
upperBound = partitionsWithLag
513520
}

‎pkg/scalers/apache_kafka_scaler_test.go‎

Lines changed: 47 additions & 39 deletions
Large diffs are not rendered by default.

‎pkg/scalers/kafka_scaler.go‎

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@ import (
2323
"context"
2424
"errors"
2525
"fmt"
26+
"math"
2627
"os"
28+
"sort"
2729
"strconv"
2830
"strings"
2931
"sync"
@@ -68,8 +70,9 @@ type kafkaMetadata struct {
6870

6971
// If an invalid offset is found, whether to scale to 1 (false - the default) so consumption can
7072
// occur or scale to 0 (true). See discussion in https://github.com/kedacore/keda/issues/2612
71-
scaleToZeroOnInvalidOffset bool
72-
limitToPartitionsWithLag bool
73+
scaleToZeroOnInvalidOffset bool
74+
limitToPartitionsWithLag bool
75+
ensureEvenDistributionOfPartitions bool
7376

7477
// SASL
7578
saslType kafkaSaslType
@@ -594,6 +597,22 @@ func parseKafkaMetadata(config *scalersconfig.ScalerConfig, logger logr.Logger)
594597
}
595598
}
596599

600+
meta.ensureEvenDistributionOfPartitions = false
601+
if val, ok := config.TriggerMetadata["ensureEvenDistributionOfPartitions"]; ok {
602+
t, err := strconv.ParseBool(val)
603+
if err != nil {
604+
return meta, fmt.Errorf("error parsing ensureEvenDistributionOfPartitions: %w", err)
605+
}
606+
meta.ensureEvenDistributionOfPartitions = t
607+
608+
if meta.limitToPartitionsWithLag && meta.ensureEvenDistributionOfPartitions {
609+
return meta, fmt.Errorf("limitToPartitionsWithLag and ensureEvenDistributionOfPartitions cannot be set simultaneously")
610+
}
611+
if len(meta.topic) == 0 && meta.ensureEvenDistributionOfPartitions {
612+
return meta, fmt.Errorf("topic must be specified when using ensureEvenDistributionOfPartitions")
613+
}
614+
}
615+
597616
meta.version = sarama.V1_0_0_0
598617
if val, ok := config.TriggerMetadata["version"]; ok {
599618
val = strings.TrimSpace(val)
@@ -972,9 +991,15 @@ func (s *kafkaScaler) getTotalLag() (int64, int64, error) {
972991
}
973992
s.logger.V(1).Info(fmt.Sprintf("Kafka scaler: Providing metrics based on totalLag %v, topicPartitions %v, threshold %v", totalLag, len(topicPartitions), s.metadata.lagThreshold))
974993

975-
if !s.metadata.allowIdleConsumers || s.metadata.limitToPartitionsWithLag {
994+
if !s.metadata.allowIdleConsumers || s.metadata.limitToPartitionsWithLag || s.metadata.ensureEvenDistributionOfPartitions {
976995
// don't scale out beyond the number of topicPartitions or partitionsWithLag depending on settings
977996
upperBound := totalTopicPartitions
997+
// Ensure that the number of partitions is evenly distributed across the number of consumers
998+
if s.metadata.ensureEvenDistributionOfPartitions {
999+
nextFactor := getNextFactorThatBalancesConsumersToTopicPartitions(totalLag, totalTopicPartitions, s.metadata.lagThreshold)
1000+
s.logger.V(1).Info(fmt.Sprintf("Kafka scaler: Providing metrics to ensure even distribution of partitions on totalLag %v, topicPartitions %v, evenPartitions %v", totalLag, totalTopicPartitions, nextFactor))
1001+
totalLag = nextFactor * s.metadata.lagThreshold
1002+
}
9781003
if s.metadata.limitToPartitionsWithLag {
9791004
upperBound = partitionsWithLag
9801005
}
@@ -986,6 +1011,16 @@ func (s *kafkaScaler) getTotalLag() (int64, int64, error) {
9861011
return totalLag, totalLagWithPersistent, nil
9871012
}
9881013

1014+
func getNextFactorThatBalancesConsumersToTopicPartitions(totalLag int64, totalTopicPartitions int64, lagThreshold int64) int64 {
1015+
factors := FindFactors(totalTopicPartitions)
1016+
for _, factor := range factors {
1017+
if factor*lagThreshold >= totalLag {
1018+
return factor
1019+
}
1020+
}
1021+
return totalTopicPartitions
1022+
}
1023+
9891024
type brokerOffsetResult struct {
9901025
offsetResp *sarama.OffsetResponse
9911026
err error
@@ -1051,3 +1086,28 @@ func (s *kafkaScaler) getProducerOffsets(topicPartitions map[string][]int32) (ma
10511086

10521087
return topicPartitionsOffsets, nil
10531088
}
1089+
1090+
// FindFactors returns all factors of a given number
1091+
func FindFactors(n int64) []int64 {
1092+
if n < 1 {
1093+
return nil
1094+
}
1095+
1096+
var factors []int64
1097+
sqrtN := int64(math.Sqrt(float64(n)))
1098+
1099+
for i := int64(1); i <= sqrtN; i++ {
1100+
if n%i == 0 {
1101+
factors = append(factors, i)
1102+
if i != n/i {
1103+
factors = append(factors, n/i)
1104+
}
1105+
}
1106+
}
1107+
1108+
sort.Slice(factors, func(i, j int) bool {
1109+
return factors[i] < factors[j]
1110+
})
1111+
1112+
return factors
1113+
}

0 commit comments

Comments
 (0)