Intro

argo-rollout은 kubernetes nativ의 deployment 기능에 넘어서, blue-green 및 canary 배포를 진행하며 워크로드를 안전하게 배포할 수 있게 도와준다. 그런데 canary 배포만하면 매트릭이 안정적인데 HPA가 증가하는 문제가 있었다.

지표가 잘못 보고된걸까? HPA가 잘못된걸까? argo 문제일까?

cpu/memory 지표는 cAdvisor에서 cgroups에서 매트릭을 참조하여 가져오는것이기 떄문에, 보통 잘못될 일이 없다. 잘못된거면, 커널 이슈를 찾아봐야겠지..?

argo-rollout

argo-rollout은 deployment와 비슷하게 template에 Pod설정과 replicas를 기입한다. 여여기에 추가로 strategy 으로 배포전략을 추가할 수 있다.

kubernetes native와 비교하면 아래 이미지와 같이, rollout CRD가 canary 배포시 replicaset을 2개를 소유한다는 점이다. 이 때, stable/canary replicaset의 label은 동일하다.

rollout에 stable/canaryService 를 연결하면 pod-hash-template을 argo-rollout에서 적절하게 업데이트 한다.

여기에 argo-rollout은 istio, AWS ALB 등 service mesh나 traffic management 도구를 명시적으로 연결할 수 있다.

문제 상황

canary 배포중, HPA에 의해 Pod가 스케일링 되지만 리소스 사용량은 이상이 없어보인다.

# kubectl get hpa
NAME      REFERENCE               TARGETS           MINPODS   MAXPODS   REPLICAS   AGE
rollout   Rollout/rollouts-demo   memory: 30%/54%   4         10        5          6h9m

# HPA event
Normal  SuccessfulRescale  17s (x4 over 6h4m)  horizontal-pod-autoscaler  New size: 5; reason: memory resource utilization (percentage of request) above target

promote를 계속 진행하게되면, pod수가 계속해서 늘어나는 것을 확인할 수 있었다.

troubleshooting

가장 먼저, HPA가 desiredCount를 어떻게 계산하는지 확인했다. [1]

여기서, HPA 매트릭은 작게 보고하고 있으므로 currentReplicas가 잘못보고된 것으로 예상했다.

HPA[2] 코드를 보니, selector만을 가지고 Pod를 선택하는 것을 볼 수 있었다.

validateAndParseSelector 으로, selector를 parse하고 computeReplicasForMetric 으로 넘겨서 replicas를 계산한다.

func (a *HorizontalController) computeReplicasForMetrics(ctx context.Context, hpa *autoscalingv2.HorizontalPodAutoscaler, scale *autoscalingv1.Scale,
	metricSpecs []autoscalingv2.MetricSpec) (replicas int32, metric string, statuses []autoscalingv2.MetricStatus, timestamp time.Time, err error) {
 
	selector, err := a.validateAndParseSelector(hpa, scale.Status.Selector)
...(중량)
		replicaCountProposal, metricNameProposal, timestampProposal, condition, err := a.computeReplicasForMetric(ctx, hpa, metricSpec, specReplicas, statusReplicas, selector, &statuses[i])

scale 값은 scaleForResourceMappings함수에서 가져오는데 “/scale” 엔드포인를 이용한다. 다음 argo-rollout 문서롤 보면 v0.3.0부터 /scale을 노출한다고 되어있다.

Since version v0.3.0, Argo Rollouts exposes its /scale subresource in the same way as a standard Kubernetes Deployment does. This allows the Horizontal Pod Autoscaler (HPA) to discover the Rollout resource. The HPA accesses the /scale subresource to get the current number of replicas from the status.replicas field of the Rollout.

func (a *HorizontalController) reconcileAutoscaler(ctx context.Context, hpaShared *autoscalingv2.HorizontalPodAutoscaler, key string) (retErr error) {
...(중략)
	scale, targetGR, err := a.scaleForResourceMappings(ctx, hpa.Namespace, hpa.Spec.ScaleTargetRef.Name, mappings)

결국 rollout의 selector를 가지고 pod를 다시 list해온다.

scale에 있는 current replicas를 쓰면 안되나..?

# rollout selector
  selector:
    matchLabels:
      app: rollouts-demo

# HPA scaleTargetRef
  scaleTargetRef:
    apiVersion: argoproj.io/v1alpha1
    kind: Rollout
    name: rollouts-demo

# Stable rs
Labels:         app=rollouts-demo
                istio-injection=enabled
                rollouts-pod-template-hash=97c5784fd
                sidecar.istio.io/inject=true

# canary rs     
Labels:         app=rollouts-demo
                istio-injection=enabled
                rollouts-pod-template-hash=546697457c
                sidecar.istio.io/inject=true

따라서 배포시 위와 같이, rollout의 selector는 app=rollouts-demo만 있기 때문에, stable/canary pod를 전부 가져오게 된다. 그래서 HPA는 currentReplicas를 비정상적으로 크게 보고한다.

아래와 같은 시나리오로 테스트했고, canary/stable 갯수를 더해서 current가 증가하면 HPA도 증가하게 보고되는 것을 확인할 수 있었다.

Scenario 1:

# kubectl get hpa
NAME      REFERENCE               TARGETS           MINPODS   MAXPODS   REPLICAS   AGE
rollout   Rollout/rollouts-demo   memory: 28%/54%   4         10        4          89m

# Pod count
canary pods: 3
stable pods: 4
--> HPA did not increase pod count
Calculation: ⌈(3 + 4) × (28 / 54)⌉ = ⌈3.62⌉ = 4

Scenario 2:

# kubectl get hpa
NAME      REFERENCE               TARGETS           MINPODS   MAXPODS   REPLICAS   AGE
rollout   Rollout/rollouts-demo   memory: 28%/54%   4         10        4          89m

# Pod count
canary pods: 4
stable pods: 4
--> HPA increased pod count to 5
Calculation: ⌈(4 + 4) × (28 / 54)⌉ = ⌈4.14⌉ = 5

좀 더 찾아보기

이러한 문제는 argo-rollout의 canary를 사용하는 경우뿐만 아니라 HPA 사용할 때 매우 빈번하게 발생할 것 같아서 좀 더 찾아보았다.

역시나 다음 KEP 문서에서 hpa의 Pod selection logic을 강화하는 문서를 찾을 수 있었다. 1.35 release 예정인거 같았고, 좀 지연된 것으로 보인다. [+] https://github.com/kubernetes/enhancements/tree/master/keps/sig-autoscaling/5325-hpa-pod-selection-accuracy

해당 문서의 골자는 OwnerReference으로 SelectionStrategy를 설정하면, replicaset에 연결된 OwnerReference를 참조해서 가져오는 것으로 보인다.

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
spec:
  # Existing fields...
  SelectionStrategy: OwnerReference  # Default: LabelSelector

argo rollout은 canary 배포시 결국 stable/canary 둘다 생성되어서 OwnerReference가 같아서 해당 문제가 해결 될 수 있는 것으로 보이지는 않는다.

추후 보아야 알겠지만 HPA에 current/desired count도 제대로 표시되게 바뀌게 되면, argo-rollout을 사용할 때 명확하게 오류를 인지할 수 있을 것이다.

그 외 workaround가 없을까 하여 설정을 찬찬히 살펴보니 dynamicStableScale 설정을 지원한다.

spec:
  strategy:
    canary:
      dynamicStableScale: true

이는 canary 배포시 argo-rollout의 replicaset의 desiredCount를 일관되게 유지시켜주는 설정이다. 기존에 stable이 4개면, canary도 4개로 배포되는데 위 설정을하면, stable+canary count를 기존에 4개 값으로 유지시켜준다.

[as-is] canary/stable -> 4/4
[to-be] canary/stable -> 2/2

참고자료

[1] https://kubernetes.io/docs/tasks/run-application/horizontal-pod-autoscale/#algorithm-details

[2] https://github.com/kubernetes/kubernetes/blob/f28b4c9efbca5c5c0af716d9f2d5702667ee8a45/pkg/controller/podautoscaler/replica_calculator.go#L323-L342

https://github.com/kubernetes/kubernetes/blob/579d4c6d06276d57d0fb0c206527bd44dfd2cc02/pkg/controller/podautoscaler/horizontal.go#L807