KEDA External Scaler
작동 방식
KEDA는 KEDA operator (0 ↔ 1 스케일링)와 Kubernetes HPA (1 ↔ N 스케일링)를 활용한 2단계 접근 방식을 사용하여 scaleTargetRef (e.g., Deployment)를 스케일링합니다.
다음 은 단계별 메커니즘입니다.
-
0에서 1로 스케일링 (활성화); KEDA operator는 scaleTargetRef를 "깨우는" 역할을 합니다.
- Loop: KEDA는 정기적으로 외부 스케일러의
IsActiveRPC 메서드를 폴링합니다. - Logic:
IsActive가 true를 반환하면: KEDA는 scaleTargetRef를 minReplicaCount (기본값 1)로 스케일링합니다.IsActive가 (쿨다운 기간 동안) false를 반환하면: KEDA는 scaleTargetRef를 0으로 스케일링합니다.
- Loop: KEDA는 정기적으로 외부 스케일러의
-
1에서 N으로 스케일링 (HPA); scaleTargetRef가 활성화되면 (replicas > 0), KEDA는 스케일링을 처리하기 위해 표준 Kubernetes HPA 리소스를 생성합니다.
- Setup: KEDA는 HPA 규칙을 정의하기 위해 스케일러의
GetMetricSpecRPC를 호출합니다 (e.g.,TargetSize: 10). - Loop: Kubernetes HPA 컨트롤러는 KEDA metrics server를 폴링합니다.
- Data flow:
- HPA는 KEDA metrics server에 질문합니다.
- KEDA metrics server는 스케일러의
GetMetricsRPC를 호출합니다. - 스케일러는 현재 값을 반환합니다 (e.g., 50).
- HPA는 표준 공식을 사용하여 desiredReplicas를 계산합니다. Kubernetes HPA # Algorithm
- Setup: KEDA는 HPA 규칙을 정의하기 위해 스케일러의
desiredMetricValueisTargetSize- If
metricTypeisAverageValue: - If
metricTypeisValue:
Scaler 서비스 구현
References
아래와 같은 구조의 프로젝트를 생성합니다.
<externalscaler>
├── cmd/
│ └── scaler/
│ └── main.go
├── internal/
│ ├── application/
│ │ └── externalscaler.go
│ └── pkg/
│ └── externalscaler/
│ ├── externalscaler.pb.go
│ ├── externalscaler.proto
│ └── externalscaler_grpc.pb.go
├── go.mod
├── go.sum
├── Makefile
└── README.md
curl https://raw.githubusercontent.com/kedacore/keda/main/pkg/scalers/externalscaler/externalscaler.proto \
--create-dirs \
-o internal/pkg/externalscaler/externalscaler.proto
protoc \
--go_out=. \
--go_opt=paths=source_relative \
--go-grpc_out=. \
--go-grpc_opt=paths=source_relative \
internal/pkg/externalscaler/externalscaler.proto
internal/application/externalscaler.go
package application
import (
"context"
pb "github.com/hhk7734/externalscaler-test/internal/pkg/externalscaler"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
type ExternalScaler struct {
pb.UnimplementedExternalScalerServer
}
// IsActive는 대상이 활성화되어야 하는지 여부를 반환합니다. false를 반환하면 대상의 replicas를 0으로
// 설정합니다.
func (e *ExternalScaler) IsActive(ctx context.Context, scaledObject *pb.ScaledObjectRef) (*pb.IsActiveResponse, error) {
// value := scaledObject.ScalerMetadata["<customKey>"]
return &pb.IsActiveResponse{Result: true}, nil
}
// GetMetricSpec은 대상이 스캐일링되기 위한 메트릭의 기준값을 반환합니다.
func (e *ExternalScaler) GetMetricSpec(ctx context.Context, scaledObject *pb.ScaledObjectRef) (*pb.GetMetricSpecResponse, error) {
// value := scaledObject.ScalerMetadata["<customKey>"]
return &pb.GetMetricSpecResponse{
MetricSpecs: []*pb.MetricSpec{
{
MetricName: "custom-metric",
TargetSize: 10,
},
},
}, nil
}
// GetMetrics은 메트릭 값을 반환합니다.
func (e *ExternalScaler) GetMetrics(ctx context.Context, request *pb.GetMetricsRequest) (*pb.GetMetricsResponse, error) {
// value := request.ScaledObjectRef.ScalerMetadata["<customKey>"]
return &pb.GetMetricsResponse{
MetricValues: []*pb.MetricValue{
{
MetricName: "custom-metric",
MetricValue: 1,
},
},
}, nil
}
// StreamIsActive은 spec.triggers.type: external-push인 경우에만 사용됩니다.
func (e *ExternalScaler) StreamIsActive(scaledObject *pb.ScaledObjectRef, stream pb.ExternalScaler_StreamIsActiveServer) error {
// value := scaledObject.ScalerMetadata["<customKey>"]
// for {
// select {
// case <-stream.Context().Done():
// return nil
// case <-time.Tick(30 * time.Second):
// if err := stream.Send(&pb.IsActiveResponse{Result: true}); err != nil {
// return err
// }
// }
// }
return status.Error(codes.Unavailable, "external-push is not supported")
}
cmd/scaler/main.go
package main
import (
"net"
"github.com/hhk7734/externalscaler-test/internal/application"
pb "github.com/hhk7734/externalscaler-test/internal/pkg/externalscaler"
"google.golang.org/grpc"
)
func main() {
grpcServer := grpc.NewServer()
lis, _ := net.Listen("tcp", ":6000")
pb.RegisterExternalScalerServer(grpcServer, &application.ExternalScaler{})
if err := grpcServer.Serve(lis); err != nil {
}
}
CRD에 External scaler 설정하기
spec:
triggers:
- type: external
metricType: AverageValue
metadata:
scalerAddress: <host>:<port>
# External scaler에 전달할 메타데이터
# <customKey>: <value>