replicas 1개인 Deployment는 왜 기본값만으로 무중단 롤링이 되는가 — maxSurge/maxUnavailable 비대칭 라운딩

seonwoo_jung·2026년 8월 5일

1. 도입

kubectl create deployment demo --image=nginx --replicas=1로 파드가 딱 하나뿐인 Deployment를 만들고 rollout restart를 걸어봤다. replicas가 1개니까 지워졌다가 새로 뜨는 그 짧은 틈은 요청을 못 받을 거라 생각했는데, kubectl get rs -w로 보니 새 파드가 Ready가 될 때까지 옛 파드가 그대로 살아 있었다. maxSurge/maxUnavailable을 따로 건드린 적도 없는데 왜 기본값만으로 이런 동작이 나오는지 컨트롤러 소스를 따라가봤다.

2. 핵심 개념

maxSurge와 maxUnavailable은 둘 다 기본값이 25%지만, 이 둘은 "한 롤아웃에서 같이 쓰는 예산"이 아니라 컨트롤러가 매 reconcile마다 독립적으로 확인하는 두 개의 울타리(fencepost)다.

  • replicas + maxSurge — 전체 Pod 수가 넘으면 안 되는 상한
  • replicas - maxUnavailable — 가용 Pod 수가 그 아래로 떨어지면 안 되는 하한

Kubernetes 소스의 ResolveFenceposts(pkg/controller/deployment/util/deployment_util.go)가 이 두 퍼센트를 정수로 바꾸는데, 여기서부터 이미 대칭이 아니다.

3. 내부 동작

ResolveFenceposts는 두 값을 서로 다른 방향으로 반올림한다.

  • maxSurge올림(roundUp=true)
  • maxUnavailable내림(roundUp=false)
  • 변환 결과가 둘 다 0이면 maxUnavailable을 강제로 1로 올린다. 그렇지 않으면 스케일업도 스케일다운도 못 해 롤아웃이 완전히 멈추기 때문이다.

replicas=1일 때 25%는 0.25다. 올림이면 1, 내림이면 0. 그래서 replicas 1개짜리 Deployment는 기본값만으로 maxSurge=1, maxUnavailable=0이 된다 — 옛 파드를 먼저 지우는 경로는 애초에 막혀 있고, 새 파드를 하나 더 띄우는 경로만 열려 있다. 처음 본 "다운타임 없음"은 우연이 아니라 이 라운딩 규칙의 필연적 결과였다.

이 상한·하한은 스케일업(reconcileNewReplicaSet)과 스케일다운(reconcileOldReplicaSets)이라는 완전히 분리된 함수가 각각 확인한다. rolloutRolling은 호출될 때마다 스케일업을 먼저 시도하고, 그게 안 됐을 때만 스케일다운을 시도한다. 예산을 미리 나눠 쓰는 게 아니라 "지금 이 경계를 넘는가"만 그때그때 다시 계산하는 폴링 구조라, 옛 파드가 지워지는 시점은 미리 정해져 있지 않고 매 사이클의 Pod 개수·가용성에 달려 있다.

4. 예시 / 코드

기본값 25%가 replicas 수에 따라 어떻게 갈리는지 직접 계산해보면, 이 비대칭이 언제 무너지는지가 보인다.

replicas25% 원값maxSurge (올림)maxUnavailable (내림)
10.2510
20.510
30.7510
41.011
102.532

replicas 1~3에서는 maxUnavailable=0이 유지돼서 "기본값만으로 무중단"이라는 성질이 그대로 이어진다. 그런데 replicas=4부터는 두 값이 동시에 1이 돼서, 컨트롤러가 새 파드를 올리는 것과 별개로 옛 파드를 하나 지우는 것도 허용된다. replicas 개수가 특정 임계를 넘는 순간부터 "항상 먼저 늘리고 나중에 줄인다"는 보장이 옅어지는 셈이다.

kubectl create deployment demo --image=nginx --replicas=1
kubectl rollout restart deployment/demo
kubectl get rs -w   # 잠깐 RS 2개, 총 Pod 2개(surge=1)가 뜨는지 확인
kubectl get deployment demo -o jsonpath='{.spec.strategy.rollingUpdate}'

5. 정리

maxSurge와 maxUnavailable은 한 롤아웃이 나눠 쓰는 공동 예산이 아니라, 퍼센트를 서로 다른 방향(올림/내림)으로 반올림해 만든 독립된 상한·하한이다. 그 비대칭 덕분에 replicas가 작을 때는 "먼저 늘리고 나중에 줄인다"는 동작이 기본값만으로도 보장된다. 다음엔 이 fencepost 방식과 전혀 다르게 ordinal 경계선으로 롤링을 제어하는 StatefulSet의 RollingUpdate.partition을 비교해볼 만하다. 다만 스케일다운이 "허용"되는 것과 옛 파드가 실제로 안전하게 종료되는 것은 별개 문제라, SIGTERM과 엔드포인트 제거 사이의 경쟁 조건도 같이 봐야 롤링 업데이트 중 순간적인 connection refused를 온전히 설명할 수 있다.

참고 자료

  • Kubernetes Docs — Workloads / Deployments §Rolling Update Deployment
  • Kubernetes 소스 pkg/controller/deploymentutil/deployment_util.go(ResolveFenceposts), rolling.go(rolloutRolling/reconcileNewReplicaSet/reconcileOldReplicaSets)

0개의 댓글