브로커 롤링 업그레이드
브로커 롤링 업그레이드 (Rolling upgrade of brokers)
브로커 롤링 업그레이드는 새 소프트웨어 버전이나 구성 변경을 점진적으로 적용하면서, Pulsar 서비스를 계속 사용 가능하게 하고 중단을 최소화하는 것을 목표로 해요. 모든 브로커가 의도한 버전과 구성을 실행할 때까지 브로커를 한 번에 하나씩 또는 통제된 배치로 재시작·교체해요. 재시작이 필요한 구성 설정은 모든 브로커가 재시작·교체된 후에만 완전히 적용돼요.
출처: 문서
본문
브로커 롤링 업그레이드는 새 소프트웨어 버전이나 구성 변경을 점진적으로 적용하면서 Pulsar 서비스를 계속 사용 가능하게 하고 중단을 최소화하는 것을 목표로 해요. 브로커를 한 번에 하나씩 또는 통제된 배치로 재시작·교체해요. 재시작이 필요한 구성 설정은 모든 브로커가 재시작·교체된 후에만 완전히 적용돼요.
이 가이드는 그 과정에서 브로커 가용성, 번들 배치, 로드 분산에 초점을 맞춰요. 클러스터 전체 업그레이드는 컴포넌트 순서와 부키·메타데이터 스토어·브로커·프록시·클라이언트 업그레이드를 다루는 Cluster upgrade guide를 따라요.
이 가이드를 언제 사용할까 (When to use this guide)
이 가이드의 통제된 절차를 따르는 것은 브로커 롤링 업그레이드를 수행하는 데 필요하지 않아요. Kubernetes에서 가장 간단한 옵션은 Apache Pulsar Helm chart의 기본 RollingUpdate 전략이에요. 템플릿이 바뀌면 Kubernetes가 브로커 파드를 자동으로 교체해요. 애플리케이션이 그 결과로 생기는 클라이언트 중단을 견딜 수 있다면 이 기본 전략을 사용해요. 여기에 설명된 추가 단계를 오케스트레이션할 필요는 없어요.
이 기본 접근 방식에서 주로 조정할 설정은 terminationGracePeriodSeconds로, Helm chart에서 broker.gracePeriod로 구성돼요. Kubernetes가 강제 종료하기 전에 브로커가 번들을 배수(drain)하고 종료를 마칠 충분한 시간을 주세요. chart 기본 30초는 너무 짧을 수 있어요. Allow enough time for termination에 설명된 대로 관찰된 종료 시간으로 값을 정해요.
이 절차들이 해결하는 문제는 브로커 간 전환 중의 중단이에요. 브로커가 멈추면 그 네임스페이스 번들은 사용 가능한 소유자가 필요하고 클라이언트는 재연결해야 해요. 수신 브로커는 더 많은 부하를 받고, 방금 시작한 브로커는 처음에 거의 없거나 전혀 부하가 없어요. Kubernetes 준비 상태(readiness)만으로는 전송된 트래픽이 복구됐는지, 업데이트된 부하 보고서가 로드 매니저에 도달했는지를 확인하지 못해요. 너무 빨리 진행하거나 자동 리밸런싱이 롤아웃 중 번들을 반복 이동하게 두면 클라이언트 중단이 길어지고 부하가 불균등해질 수 있어요.
기본 롤아웃에서는 이런 클라이언트 마이크로-중단이 종료·시작 시간, 소유권 복구, 부하, 클라이언트 재시도 동작에 따라 몇 초에서 몇 분까지 지속될 수 있어요. 이런 중단이 허용되지 않거나 브로커 업그레이드 중 로드 불균형과 반복 번들 이동을 줄여야 한다면 이 가이드를 사용해요. 이 절차들은 정상 종료, 교체 용량, 부하 보고, 리밸런싱을 조정해 서비스 중단을 최소화해요. 토픽 핸드오프와 클라이언트 재연결은 여전히 잠깐이나마 연산을 방해할 수 있어요.
통제된 절차 사용하기 (Using the controlled procedures)
아래 단계는 가독성을 위해 pulsar-admin 명령을 사용해요. 각각은 admin API 호출이며, 브로커 업그레이드를 구동하는 어떤 것에서든 발행할 수 있어요. REST API를 curl로 호출하는 스크립트, OpenAPI 스펙에서 생성된 클라이언트, Kubernetes 오퍼레이터나 배포 파이프라인 안의 Java·Go admin 라이브러리 등이요. 인터페이스 개요와 설정 방법은 Admin API - Get started를 참고해요.
Kubernetes에서 Pulsar를 실행하고 있나요?
Kubernetes deployments부터 시작하는 것이 도움이 될 수 있어요. 그 섹션은 롤아웃 전략을 비교하고, 공통 요구 사항을 설명하며, 정상 종료·부하 보고·리밸런싱을 포함해 이 가이드의 다른 곳의 관련 절차로 연결해요.
브로커가 멈출 때 일어나는 일 (What happens when a broker stops)
브로커가 SIGTERM을 받거나 pulsar-admin brokers shutdown을 실행하면 다음 단계를 거쳐요.
- 브로커가 로드 매니저에서 자신을 비활성화해 새 번들 할당을 받지 않게 해요. 확장 가능한 로드 매니저에서는 브로커가 등록 해제되기 전에 이 단계의 일부로 소유권 정리가 일어나요.
- 브로커가 소유한 번들을 해제해요.
모듈식 로드 매니저에서는 브로커가 소유한 번들을 하나씩 언로드해요. 번들의 토픽은 병렬로 닫힌 다음 메타데이터 스토어에서 번들 소유권이 해제돼요. 닫힌 토픽의 클라이언트는 룩업을 통해 재연결하고, 룩업이 그들을 리더 브로커로 리다이렉트하며, 리더가 번들을 새 소유자에게 할당해요.
확장 가능한 로드 매니저에서는 중지 중인 브로커가 새 소유자를 선택하고 소유권 채널을 통해 소유권을 전송해요. 소유권 채널은 브로커 간 번들 소유권 상태를 조정해요. 소유권 재정의는 loadBalancerServiceUnitStateMaxConcurrentOverrides(기본 64)가 제어하는 동시 배치로 제출되며, 시스템 번들은 다른 번들 다음에 처리돼요. 브로커 리다이렉션을 지원하는 클라이언트의 경우 새 소유자가 클라이언트의 토픽을 닫는 메시지에 포함되어 룩업 없이 재연결할 수 있어요(PIP-307). 종료 목적지는 배치 전략으로 선택되므로 방금 재시작된 저부하 브로커가 한 번에 많은 전송을 받을 수 있어요.
사용되지 않는 번들은 종료 시간에 추가되지 않아요. 브로커는 Disable broker in load manager가 몇 초에 완료됐다고 로그를 남기고, 그다음 Unloading namespace-bundles가 몇 초에 완료됐다고 로그를 남겨요. 배수를 측정할 때 두 단계를 모두 포함해요. 확장 가능한 소유권 전송은 첫 단계에서 일어나요. 이후의 번들별 언로드 루프는 각 번들에 namespaceBundleUnloadingTimeoutMs(기본 60초)를 사용하는데, 이는 총 배수 시간의 제한이 아니에요.
- 브로커가 나머지 서비스를 닫고 종료해요.
brokerShutdownTimeoutMs(60초)는 이 마지막 단계를 제한하지, 그 이전 단계의 번들 해제를 제한하지 않아요.
소유권 임대와 강제 종료 (Ownership leases and forced termination)
모듈식 로드 매니저에서 네임스페이스 번들의 소유권은 브로커의 메타데이터-스토어 세션에 묶인 임대(lease)로 작동해요. Pulsar는 메타데이터-스토어 잠금 API를 통해 이 임대를 구현하며, ZooKeeper에서는 임시(ephemeral) znode, Oxia에서는 임시 레코드를 사용해요. 그 엔트리는 소유 브로커의 광고된 주소를 포함해요. 소유권 엔트리가 존재하는 동안 룩업은 그 소유자를 반환하고, 이전 브로커의 TCP 연결이 실패했다고 다른 소유자가 단순히 번들을 가져갈 수는 없어요. 정상 언로드 중 브로커는 토픽을 닫고 임시 엔트리를 삭제해 소유권 임대를 명시적으로 해제해서, 세션 만료를 기다리지 않고 번들이 다시 할당될 수 있게 해요.
브로커 프로세스가 크래시하거나 Kubernetes가 SIGKILL(신호 9, kill -9처럼)로 종료하면, 소유권 해제와 메타데이터-스토어 세션 닫기를 수행하는 종료 훅을 실행할 수 없어요. ZooKeeper나 Oxia는 세션이 만료될 때까지 세션과 임시 엔트리를 유지할 수 있어요. Pulsar는 기본적으로 metadataStoreSessionTimeoutMillis=30000의 세션 타임아웃을 요청해요. 이전 세션이 만료되면 메타데이터 스토어가 임시 엔트리를 삭제해 소유권 임대를 해제해요. 다른 정체성을 가진 살아있는 브로커는 소유권을 획득하기 전에 이 정리를 기다려야 해서, 영향을 받는 토픽에 사용 불가 창이 생겨요. 지연은 남은 세션 수명과 스토어의 만료 처리에 의해 결정되지 파드 삭제부터 정확히 30초가 아니에요. ZooKeeper는 또한 서버 측 한도 내에서 타임아웃을 협상해요. ZooKeeper sessions과 Oxia ephemeral records를 참고해요.
같은 광고된 소유권 정보로 교체하는 경우 중요한 예외가 있어요. Pulsar는 그것을 같은 논리적 소유자로 인식하고, 이전 세션이 만료되기 전에 낡은 임시 엔트리를 교체해 새 세션에서 소유권 임대를 다시 확립할 수 있어요. StatefulSet의 안정적인 파드별 호스트 이름은 광고된 URL, 포트, 리스너가 동일할 때 이를 가능하게 해요. 완전한 소유권 레코드가 일치해야 하며, disabled 플래그도 포함해요. 언로드 중 브로커가 번들을 비활성으로 표시한 후 사망하면 교체 브로커는 그 번들에 대해 이 같은-소유자 회수 경로를 쓸 수 없고 세션 만료를 기다려야 할 수 있어요. 광고된 값이 바뀌면 안정적인 파드 이름만으로는 충분하지 않아요. 클라이언트는 여전히 교체 브로커가 도달 가능하고 그들의 토픽을 로드할 때까지 그 브로커를 사용할 수 없어요.
확장 가능한 로드 매니저는 이 번들별 세션 기반 임대 메커니즘 대신 소유권 채널을 사용해요. 그 리더가 비활성 브로커를 감지하고, 헬스 체크로 사라졌는지 확인하고, 그 번들을 재할당해요. 정리 스케줄링도 리더의 메타데이터-스토어 연결에 의존해요. 안정적인 세션은 스케줄링 지연을 추가하지 않고, 최근에 다시 확립된 세션은 정리를 180초 연기하며, 불안정한 세션은 복구가 허용될 때까지 정리를 건너뛰게 해요. 이는 토픽이 언제 사용 가능해지는지에 대한 보장이 아니라 스케줄링 결정이에요. 모듈식 매니저의 임대-만료 타이밍을 확장 가능한 소유권 복구에 적용하지 마세요. 어느 경우든 정상 종료가 끝나도록 허용해 추가 복구 지연을 피해요.
통제된 속도로 브로커 배수 (Drain a broker at a controlled rate)
가능한 한 빨리 모든 번들을 해제하면 나머지 브로커에 재연결 클라이언트, 룩업, 토픽 로드가 폭증해요. 모듈식 로드 매니저에서는 SIGTERM을 보내는 대신 admin API로 번들별 언로드 루프를 속도 제한해요.
pulsar-admin --admin-url http://broker-0.example.com:8080 brokers shutdown --max-concurrent-unload-per-sec 5
개별 브로커의 admin URL을 대상으로 하는데, 클러스터에 필요한 인증과 TLS 설정을 갖고요. 공유 Service나 프록시를 통해 이 명령을 보내면 재시작하려는 것과 다른 브로커를 종료할 수 있어요.
이 옵션은 언로드 루프가 번들을 시작하는 속도를 제한하고, 그 후 브로커가 종료돼요. 옵션이 없거나 SIGTERM이면 그 루프는 속도 제한이 없어요. 클라이언트가 먼저 연결을 끊기를 기다리지 않고 토픽을 닫으려면 --forced-terminate-topic을 추가하는데, SIGTERM 경로는 이미 그 동작을 사용해요. CLI와 달리 REST 종료 엔드포인트는 forcedTerminateTopic을 생략하면 기본 true로 두고, 클라이언트가 연결을 끊기를 기다리려면 forcedTerminateTopic=false를 전달해요.
확장 가능한 로드 매니저에서는 브로커가 자신을 비활성화할 때 소유권 전송이 일어나므로, 이 속도 제한 루프 전에요. 따라서 --max-concurrent-unload-per-sec는 그 전송을 스로틀하지 않아요. 동적 설정 loadBalancerServiceUnitStateMaxConcurrentOverrides(기본 64)가 소유권 재정의의 배치 크기를 제어해요. 낮추면 함께 제출되는 수가 줄지만 초당 전송 속도를 설정하거나 모든 클라이언트 재연결·토픽 로드를 제한하지는 않아요. 워크로드 이동을 조절하려면 브로커를 종료하기 전에 번들을 명시적으로 이동해요.
속도가 어떻든 프로세스는 끝나도록 허용해야 해요. 가장 느린 브로커의 전체 배수(disable과 unload 단계 포함)를 측정한 다음, 나머지 서비스에는 brokerShutdownTimeoutMs를, 그리고 변동에 대한 여유를 더해요. Kubernetes에서는 이것이 terminationGracePeriodSeconds이며, Allow enough time for termination을 참고해요.
업그레이드 중 자동 리밸런싱 일시 중지 (Pause automatic rebalancing during the upgrade)
로드 매니저는 모든 재시작에 반응해요. 중지하는 브로커가 해제한 번들은 다른 브로커의 부하를 높이고, 방금 시작한 브로커는 거의 유휴예요. 브로커가 아직 재시작 중일 때 로드 셰딩이 실행되면, 다음 재시작이 다시 이동할 번들을 이동해요. loadBalancerAutoUnloadSplitBundlesEnabled=true이면 분할이 더 많은 이동을 추가할 수 있어요. 두 로드 매니저 모두 다음 설정을 매 주기마다 다시 읽으므로, 이 설정을 적용하기 위해 브로커를 재시작하지 않고 업그레이드 기간 동안 꺼둘 수 있어요. 먼저 pulsar-admin brokers get-all-dynamic-config로 기존 동적 재정의를, pulsar-admin brokers get-runtime-config로 적용된 설정을 기록해요.
pulsar-admin brokers update-dynamic-config --config loadBalancerSheddingEnabled --value false
pulsar-admin brokers update-dynamic-config --config loadBalancerAutoBundleSplitEnabled --value false
첫 브로커를 중지하기 전에 설정이 적용되기를 기다려요. 자동 셰딩을 일시 중지해도 정상 종료 전송, 수동 언로드, 크래시 후 복구는 막지 않아요.
특히 loadBalancerSheddingEnabled=false는 이미 소유된 번들의 주기적 리밸런싱을 멈춰요. 미소유 번들의 할당은 비활성화하지 않아요. 클라이언트는 정상적으로 해제된 번들을 룩업하고 셰딩이 일시 중지된 동안 살아있는 브로커에 재연결할 수 있어요. 따라서 셰딩을 일시 중지하는 것 자체가 토픽을 사용 불가로 만들지 않지만, 토픽을 닫고 다시 여는 것은 핸드오프 중 여전히 클라이언트 중단을 일으켜요. loadBalancerEnabled를 활성화된 상태로 유지하고 나머지 브로커에 충분한 용량을 유지해요.
생존 브로커가 해제된 번들을 받으면, 다른 종료·실패·명시적 언로드가 없으면 그 부하는 셰딩이 재개될 때까지 그 브로커에 남아요. 빈 교체본을 시작하는 것이 셰딩이 비활성화된 동안 기존 부하를 자동으로 그 위로 옮기지는 않아요. 다른 브로커를 중지하기 전에 수신 브로커의 증가된 부하를 확인해요. 그렇지 않으면 연속 종료가 트래픽을 너무 적은 브로커에 집중시킬 수 있어요.
마지막 브로커가 상태가 좋고 부하를 보고한 후 이전 설정을 복원해요. 둘 다 활성화되어 있었다면 다음으로 복원해요.
pulsar-admin brokers update-dynamic-config --config loadBalancerSheddingEnabled --value true
pulsar-admin brokers update-dynamic-config --config loadBalancerAutoBundleSplitEnabled --value true
이미 비활성화되어 있던 설정은 보존해요. 업그레이드가 끝났다고 해서 그것을 활성화하지 마세요. 존재하지 않던 곳에 임시 동적 재정의를 추가했다면 pulsar-admin brokers delete-dynamic-config --config <config-name>으로 제거해 파일 구성으로 돌아가요.
리밸런싱은 임계값, 쿨다운, 번들 크기, 배치 정책에 따라 여러 셰딩 주기가 걸릴 수 있어요. 기본 셰딩 전략은 둘 다 브로커 롤링 업그레이드 후 클러스터가 있는 형태를 다뤄요. 몇 개 브로커가 몫보다 많이, 마지막으로 재시작된 브로커는 거의 비어 있는 형태요.
- 모듈식 로드 매니저는 Pulsar 5.0.0부터 기본으로 AvgShedder를 사용해요(2.10에서 4.x 및 5.0.0-M1/M2 마일스톤에서는 ThresholdShedder). AvgShedder는 가장 부하가 많은 브로커와 가장 적게 부하가 걸린 브로커를 짝짓고, 그 차이가
loadBalancerAvgShedderHighThreshold(40)를loadBalancerAvgShedderHitCountHighThreshold(2) 연속 주기 동안 초과하거나loadBalancerAvgShedderLowThreshold(15)를 8주기 동안 초과하면 그 둘 사이에서 번들을 이동해요. 또한 이동하는 각 번들의 목적지를 미리 계획해요. 여전히 ThresholdShedder를 실행한다면lowerBoundarySheddingEnabled=true가 아니면 유휴 브로커 하나만으로는 셰딩이 트리거되지 않을 수 있어요. ThresholdShedder 참고. - 확장 가능한 로드 매니저는
loadBalancerBrokerLoadTargetStd(0.25)를 목표로 하고 표준편차 목표가 충족돼도 저부하·과부하 브로커를 확인하는 TransferShedder를 사용해요. 그 쿨다운은 소스 브로커의 마지막 예정된 언로드 이후 최소loadBalanceSheddingDelayInSeconds(180)로 타임스탬프가 찍힌 부하 데이터를 요구해요. 이는 브로커 시작부터 측정한 지연이 아니에요. 등록된 브로커에 부하 데이터가 없으면 셰딩을 건너뛰어요. TransferShedder 참고.
모듈식 로드 매니저의 경우 Pulsar 5.0.0부터(5.0.0-M1/M2 마일스톤 이후) 기본으로 구성된 것처럼 AvgShedder를 셰딩과 배치 전략 모두로 사용해요. 셰딩이 재개되고 리더가 최신 부하 보고서를 갖게 되면 AvgShedder는 부하가 큰 브로커와 부하가 적은 교체 브로커를 짝짓고 셰딩된 번들을 그 교체 브로커로 보낼 수 있어요. 셰딩이 일시 중지된 동안 계획된 셰딩 목적지가 없는 일반 할당은 적격 후보 중 무작위 선택을 사용해요. AvgShedder는 해제된 각 번들이 방금 시작된 가장 부하가 적은 브로커로 간다는 것을 보장하지 않아요. 롤아웃에 그 배치 보장이 필요하다면 명시적 목적지 언로드를 사용해요.
한 번에 하나씩 브로커 재시작 (Restart one broker at a time)
이 롤아웃에서 현재 리더 브로커를 마지막에 재시작해요. 모듈식 로드 매니저에서 각 브로커는 로드 매니저 인스턴스를 실행하지만, 리더가 보통 미소유 번들의 할당 결정을 내리고 셰딩을 스케줄해요. 다른 브로커가 교체되는 동안 리더를 실행 상태로 두면 그 결정들과 그 부하 보고서 보기의 연속성이 보존돼요.
현재 리더를 조회해요.
pulsar-admin brokers leader-broker
동등한 Admin REST API는 GET /admin/v2/brokers/leaderBroker이며, 응답에는 brokerId와 serviceUrl이 포함돼요. 그 정체성을 브로커 파드에 매핑하고 다른 브로커가 업그레이드될 때까지 그대로 두세요. 롤아웃 중 리더가 바뀔 수 있으므로 각 파드를 선택하기 전에 리더십을 다시 확인해요. 리더는 다른 브로커가 아래 검사를 통과한 후에만 재시작하고, 그다음 새 리더가 선출되고 교체된 브로커가 복구되는지 확인해요. 리더-마지막 순서는 다른 교체 중간에 선거가 일어나는 것을 피해요. Pulsar는 리더 실패에서 복구할 수 있으므로, 이것은 같은 리더가 무기한 생존해야 한다는 요구 사항이 아닌 오케스트레이션 규칙이에요.
이전 브로커가 돌아와 로드 밸런싱에 참여할 때만 다음 브로커를 중지해요.
- 브로커가 나열될 때까지 기다려요:
pulsar-admin brokers list <cluster-name> - 재시작된 브로커의 헬스 체크가 그 브로커 자신의 admin URL로 통과할 때까지 기다려요:
pulsar-admin --admin-url http://broker-0.example.com:8080 brokers healthcheck공유 Service나 프록시를 통한 헬스 체크는 다른 브로커에서 성공할 수 있어요. Kubernetes에서는 교체 파드가 Ready이고 광고된 파드별 이름이 해석 가능하며 그것을 사용하는 브로커·프록시에서 도달 가능한지도 확인해요. 모듈식 로드 매니저에서 리더는 보통 각 브로커를 직접 검사하지 않고 메타데이터 스토어의 임시 등록에서 활성 브로커를 식별해요. 등록은 번들별 소유권 임대와 별개이며 네트워크 도달 가능성이나 상태의 증거가 아니에요. 크래시된 브로커는 세션 만료까지 등록된 채로 남을 수 있어요. 이것이 브로커 나열에 더해 개별 헬스 체크가 필요한 이유예요. 확장 가능한 매니저의 비활성-브로커 복구는 위에서 설명했듯 헬스 체크를 포함해요. - 부하 보고를 기다려요. 모듈식 로드 매니저에서 교체 브로커는 등록 중 초기 부하 데이터를 게시하고, 리더는 메타데이터-스토어 알림에 응답해 그 보고서를 자체 로드 매니저 보기로 읽어요. 리더가 교체 브로커의 현재 부하 정보를 가질 때까지 다음 브로커를 중지하지 마세요. 로컬 등록이나 교체 브로커가 보고서를 게시했다는 로그만으로는 리더가 그것을 처리했는지 확인되지 않아요. 리더의 로드 매니저 진단 또는 모니터링에서 교체 브로커 보고서 읽기 오류가 없는지 확인해요.
확장 가능한 로드 매니저는 들어오는 요청에 준비된 후
loadBalancerReportUpdateMinIntervalMillis(기본 5초)에서 주기적 보고를 스케줄해요. 스케줄링, 게시, 전파가 더 걸릴 수 있어요. 시작 후 최소 한 보고 간격을 두고 보고서가 배치 결정을 내리는 로드 매니저와 셰더를 실행하는 리더에게 도달했는지 확인해요.loadBalancerDebugModeEnabled=true는 진단 보고 로그를 활성화해요. 이후 이전 값으로 복원해요. 고정 수면이나 성공적인 헬스 체크만으로는 부하 데이터가 로드 매니저에 도달했다는 것이 증명되지 않아요. 중지된 브로커의 번들을 받은 브로커들의 업데이트된 보고서도 확인해요. 그들의 번들 수, 메시지 비율, 처리량, 리소스 사용량이 다음 종료 전에 결정하는 로드 매니저의 보기에 추가 워크로드를 반영해야 해요. 전송된 클라이언트 트래픽이 복구되기를 기다린 다음, 그 부하가 샘플링·게시·처리되기를 기다려요. 게시 임계값과 평활화된 리소스 측정은 그 보기를 단일 보고 간격을 넘어 지연시킬 수 있어요. 빈 교체 브로커의 보고서만 확인하면 배치 결정이 수신자의 전송 전의 낮은 부하에 기반할 수 있어요. 부하 데이터 누락은 확장 가능한 셰딩을 차단하고 브로커를 부하 기반 순위에서 제외하지만, 선호 후보가 없을 때 배치는 무작위 폴백에서 여전히 그것을 선택할 수 있어요. 부하 데이터 누락에 의존해 조기 할당을 막지 마세요.
큰 클러스터에서 배치로 업그레이드한다면 나머지 브로커가 해제된 번들을 감당할 수 있도록 배치를 충분히 작게 유지하고, 배치 사이에 같은 대기를 적용해요.
롤아웃을 더 빨리 끝내려고 클라이언트 연산·재시도 타임아웃을 단축하지 마세요. 소유권 핸드오프와 재연결에 시간을 주세요. 공격적인 타임아웃은 일시적인 중단을 반복된 애플리케이션 재시도로 바꿀 수 있어요.
번들을 직접 이동 (Move bundles yourself)
재시작하는 각 브로커가 로드 매니저가 두는 곳에 번들을 해제하도록 두는 대신, 브로커를 중지하기 전에 그것들을 선택한 브로커로 이동할 수 있어요. 예를 들어 이미 재시작된 브로커를 채우기 위해서요.
pulsar-admin namespaces unload my-tenant/my-namespace --bundle 0x00000000_0x08000000 --destinationBroker broker-2.example.com:8080
두 로드 매니저 모두 목적지를 존중해요. 확장 가능한 로드 매니저는 번들을 직접 전송하고, 모듈식 로드 매니저는 다음 번들 할당에 목적지를 적용해요.
pulsar-admin brokers namespaces <cluster-name> --url <broker-url>은 브로커가 소유한 번들을 나열해요.
두 번째 브로커 풀로 롤아웃한다면, 번들을 언로드하기 전에 교체 용량을 시작하고, 워크로드가 이동한 후에만 이전 브로커를 은퇴시켜요. 두 로드 매니저 모두 이 롤아웃을 지원하며, 확장 가능한 로드 매니저는 추가로 호환 클라이언트를 또 다른 룩업 없이 대상 브로커로 리다이렉트해요. 필요한 Kubernetes 오케스트레이션은 Replace the existing broker StatefulSet with a new broker StatefulSet을 참고해요.
롤아웃이 로드 매니저 유형도 바꾼다면 load manager migration을 따라요. Pulsar 5.0은 새 유형을 도입하기 전에 기존 브로커에서 loadManagerMigrationEnabled=true가 필요해요. 새 로드 매니저의 초기 브로커 풀이 새 할당을 받도록 계획하고, 소유권-스토어 마이그레이션을 이 롤아웃과는 별도로 유지해요.
Kubernetes 배포 (Kubernetes deployments)
아래 통제된 롤아웃 전략으로 중단을 최소화하려면, Pulsar Helm chart가 사용하는 StatefulSet 배포에 롤아웃 제어, 충분한 종료 시간, two-Service 레이아웃이 필요해요. 사용자 정의 매니페스트와 오퍼레이터에도 같은 요구 사항을 적용해요. Pulsar Proxy를 사용한다고 해서 요구 사항이 사라지지는 않아요.
롤아웃 자동화 (Rollout automation)
Apache Pulsar Helm chart는 브로커용으로 이 Pulsar 인지 롤링 업그레이드 절차를 자동으로 수행하지 않아요. 기본 StatefulSet 롤아웃은 리밸런싱을 일시 중지·복원하지 않고, 번들 전송을 조절하지 않으며, 다음 브로커를 교체하기 전에 모든 Pulsar 재시작 검사를 기다리지 않아요.
완전히 자동화된 롤아웃의 일부는 클러스터 상태를 지속적으로 관찰하고 복구·이후 동작을 조정하는 Kubernetes 오퍼레이터 로직 또는 동등한 사용자 지정 컨트롤러가 필요해요. Helm 구성만으로는 그 조정을 제공할 수 없어요. Apache Pulsar 프로젝트는 Pulsar용 Kubernetes 오퍼레이터를 제공하지 않아요. 이 절차를 수동으로 따르거나, 필요한 절차를 구현하는 자동화 솔루션을 구현하기 위한 참고 자료로 사용할 수 있어요.
브로커가 StatefulSet을 사용하는 이유 (Why brokers use StatefulSets)
브로커는 로컬에 영속 메시지 데이터를 저장하지 않지만 안정적인 네트워크 정체성은 유용해요. StatefulSet 교체는 pulsar-broker-1이나 pulsar-broker-2 같은 순번과 이름을 유지하고, headless Service가 파드 IP가 바뀌어도 파드별 DNS 이름을 안정적으로 공급해요. 이런 이름은 생성된 Deployment 파드 이름보다 관리 명령과 모니터링에서 타기팅하기 쉬워요. 또한 와일드카드 인증서가 금지될 때 인증서가 예측 가능한 개별 DNS 이름을 나열할 수 있게 해요. 각 인증서는 실제 광고된 엔드포인트를 덮어야 해요.
안정적인 정체성은 모듈식 로드 매니저에서 크래시 후 복구에도 도움이 될 수 있어요. 클라이언트는 백오프로 연결을 재시도하고, 룩업은 그 번들 소유권 임대가 남아 있는 동안 이전 광고된 소유자를 계속 반환할 수 있어요. 이전 메타데이터-스토어 세션이 만료되기 전에 교체 브로커가 같은 광고된 소유권 정보로 시작하면, Ownership leases and forced termination에 설명된 것처럼 그 임대를 회수하고 같은 번들을 서빙할 수 있어요. 만료와 재할당이 먼저 일어나면 다른 브로커가 대신 소유할 수 있어요. 이것은 경쟁에 따라 달라지는 최선의 복구이며, 이전 번들 각각이 교체본으로 돌아올 것이라는 보장은 아니에요.
Deployment는 보통 교체 파드에 새 이름을 주므로 파드 교체 시 이 안정적인 파드별 정체성을 제공하지 못해요. 기존 Deployment 파드 안의 컨테이너 재시작은 정체성을 유지할 수 있지만, 파드 교체와는 달라요. 사용자 정의 배포는 같은 복구 동작을 얻으려면 같은 광고된 엔드포인트를 보존할 다른 방법이 필요할 거예요.
롤아웃 전략 선택 (Choose a rollout strategy)
주된 차이는 교체 시점과 번들 배치에 대한 제어가 얼마나 필요한가예요.
| Strategy | Control over the rollout | Load distribution and bundle movement | When to choose it |
|---|---|---|---|
| 기본 RollingUpdate | Kubernetes가 파드를 자동으로 교체하고 준비 상태에 따라 진행. terminationGracePeriodSeconds를 조정. | 중지하는 브로커가 생존 브로커에 번들을 해제. 자동 셰딩이 롤아웃 중 다시 이동할 수 있음. | 클라이언트 마이크로-중단과 일시적 로드 불균등이 허용 가능할 때 가장 간단한 옵션. 아래 통제된 절차는 선택사항. |
| OnDelete로 제자리 교체 | 각 파드 삭제를 제어하고 Pulsar 헬스·부하 보고 검사를 기다림. | 일시 중지된 셰딩은 자동 재섞기를 피하지만 종료는 여전히 번들을 생존 브로커에 재분배. 교체본은 처음에 부하가 거의 없음. | 추가 브로커를 프로비저닝하지 않고 시점을 더 제어해야 할 때. |
| 기존 브로커 StatefulSet을 새 브로커 StatefulSet으로 교체 | 먼저 교체 용량을 시작하고 각 이전 파드를 삭제하기 전에 번들 이동의 목적지·속도를 선택. | 직접 이동으로 분산을 보존하거나 의도적으로 조정할 수 있고, 생존 브로커에 걸친 중간 분산과 이후 재섞기를 피함. | 로드 밸런스에 대한 가장 큰 제어와 가장 적은 불필요한 번들 이동을 원할 때 선호. 추가 브로커의 임시 용량과 더 많은 오케스트레이션이 필요. |
교체-StatefulSet 전략에서는 각 수신 브로커를 이동시키는 워크로드에 맞게 크기를 정하고 진행 전에 결과 부하를 확인해요. 명시적 배치는 균형 잡힌 롤아웃을 유지하기 쉽게 하지만, 이미 불균등한 분산을 자동으로 고치거나 용량 부족을 보상하지는 않아요.
두 통제된 전략은 아래 전제 조건을 공유해요. 기본 RollingUpdate는 그 절차를 구현하지 않고도 사용 가능하며, 주요 조정 요구 사항은 충분한 종료 예산이에요.
두 통제된 전략의 전제 조건 (Prerequisites for both controlled strategies)
브로커 파드를 삭제하기 전에 다음을 구성해요. 제자리로 교체하든 새 StatefulSet으로 이동 후 은퇴시키든 마찬가지예요. 어느 전략도 Apache Pulsar Helm chart가 자동화하지 않아요.
- 정상 파드 종료: 브로커 컨테이너의 preStop 훅을 구성해 파드가 삭제될 때 그 브로커의 종료 API를 호출하도록 해요.
- 종료 예산: terminationGracePeriodSeconds를 훅, 번들 배수, 프로세스 종료를 커버하도록 설정해요.
- 브로커 도달 가능성: 기존과 교체 StatefulSet 모두에 필요한 Service 레이아웃을 사용해요.
- 롤아웃 순서: 각 브로커를 선택하기 전에 리더십을 다시 확인하고, 현재 리더를 마지막까지 두고, 진행 전에 헬스·트래픽 복구·부하 보고 검사를 완료해요.
어느 전략을 시작하기 전에 Pause automatic rebalancing during the upgrade에 설명된 대로 자동 로드 셰딩과 번들 분할을 비활성화해요. 롤아웃 전체 동안 비활성화 상태로 유지해요. 모든 이전 브로커 파드가 교체되고 헬스·부하 보고 검사가 통과된 후에만 다시 활성화하고, 롤아웃 전에 이미 비활성화되어 있었다면 이전 설정을 복원해요.
정상 파드 종료 구성 (Configure graceful pod termination)
두 통제된 전략 모두 파드를 정상적으로 삭제하고 preStop 훅이 종료 API를 호출하게 해요. 브로커 컨테이너의 lifecycle.preStop을 구성해 개별 브로커의 종료 API를 클러스터에 필요한 인증·TLS 설정과 함께 호출하게 해요. 훅은 배수 요청이 완료될 때까지 기다려야 해요. API 호출을 백그라운드로 시작하지 마세요. Kubernetes는 종료 신호를 보내기 전에 훅을 실행해요. Kubernetes container lifecycle hooks 참고.
Apache Pulsar Helm chart(버전 4.7.0까지)는 브로커 preStop 훅이나 그 설정을 제공하지 않아요. Helm post-renderer, Kustomize 패치, 또는 사용자 정의 StatefulSet 매니페스트로 브로커 컨테이너에 lifecycle.preStop을 추가해요. 이 사용자 정의 없이 일반 파드 삭제는 SIGTERM을 보내는데, 이는 여전히 정상적으로 번들을 배수하지만 종료 API의 언로드 속도 제한은 없고 강제 토픽 닫기가 있어요. 여전히 broker.gracePeriod 안에 끝나야 해요. chart에서 브로커 lifecycle-hook 설정을 지원하면 더 쉬울 텐데, 기여를 환영해요.
종료 API는 브로커 프로세스를 중지해요. 파드를 삭제하거나 업데이트된 StatefulSet 템플릿을 적용하지는 않아요. 생존 파드에서 호출하면 Kubernetes가 이전 구성을 가진 컨테이너를 재시작할 수 있어요. StatefulSet을 고아화(orphan)해도 이 컨테이너 재시작 동작은 바뀌지 않아요. 파드를 삭제하면 API 기반 배수와 그 파드의 은퇴가 조정되는데, 제자리 전략에서는 기존 StatefulSet이 교체본을 만들고, 고아 파드는 그것을 재생성할 StatefulSet 컨트롤러가 없어요.
훅을 미리 구성하고 은퇴 중인 실행 파드와 교체 파드 템플릿 모두에 훅이 있는지 확인해요. StatefulSet 템플릿 업데이트는 기존 파드에 훅을 추가하지 않아요. 롤아웃 중 그에 의존하기 전에 훅과 API 접근을 검증해요.
종료에 충분한 시간 허용 (Allow enough time for termination)
종료 예산은 Helm chart에서 broker.gracePeriod로 설정되는 파드의 terminationGracePeriodSeconds예요. 카운트다운은 파드 종료가 시작될 때 시작되며 preStop 실행과 브로커 프로세스 종료를 모두 포함해요. 훅은 별도 허용량을 받지 않아요. Kubernetes pod termination flow 참고.
이 예산은 가장 느린 브로커의 전체 disable-and-unload 기간에 더해 brokerShutdownTimeoutMs를 초로 변환한 것, 추가 훅 오버헤드, 변동에 대한 여유로 정해요. 배수가 preStop 안에서 실행될 때 그것을 두 번 세지 마세요. chart 기본 30초는 brokerShutdownTimeoutMs 단독보다도 짧아요. 배수는 그 타임아웃이 시작되기 전에 일어나요. Kubernetes 데드라인이 만료되면 종료가 끝나기 전에 브로커가 죽을 수 있고, 완료되지 않은 핸드오프는 크래시 복구가 필요해요. 아래 300초 예시는 보편적인 설정이 아니라 설명용이에요.
broker:
# Example only: size from the full drain, shutdown, hook overhead, and a margin.
gracePeriod: 300
브로커 도달 가능성 보장 (Ensure broker reachability)
두 전략 모두 각 브로커 StatefulSet이 spec.serviceName으로 headless Service에 바인딩되어야 하고, publishNotReadyAddresses: true를 가져 준비 상태에 의존하는 DNS 지연으로 클라이언트 중단이 늘어나는 것을 피해야 해요. 이는 기존 StatefulSet, 그리고 새 풀을 사용할 때 교체 StatefulSet에도 적용돼요. 이렇게 하면 각 교체 브로커의 광고된 이름이 준비성 프로브가 성공하기 전에 해석될 수 있어요. 피할 수 있는 다운타임 원인 하나가 제거되지만, 브로커 시작·소유권 복구·토픽 로드·클라이언트 재시도 타이밍이 여전히 트래픽이 언제 재개되는지 결정해요. required Service layout 참고.
컴포넌트 조정과 배포 검증 (Coordinate components and verify the deployment)
두 브로커 전략 모두 ZooKeeper, BookKeeper, 프록시 변경을 별도로 조정해요. 브로커 롤아웃 설정이 그 컴포넌트를 제어하지 않아요. ZooKeeper를 재시작해야 하면 쿼럼을 보존하고 다음 것을 재시작하기 전에 각 멤버가 다시 합류할 때까지 기다려요. upgrade sequence를 따라요.
첫 삭제 전에 실행 파드의 훅과 종료 예산을 포함해 배포된 리소스를 검사해요. 롤아웃 중 교체 브로커와 그 전임자의 번들을 받는 브로커를 모두 확인해요. Kubernetes 준비 상태만으로는 트래픽 복구나 부하 보고 전파를 검증하지 않아요.
통제된 롤아웃 절차 (Controlled rollout procedures)
통제된 전략 중 하나를 선택하고 시작 전에 공유 전제 조건(리더-마지막 순서 포함)을 적용해요. StatefulSet을 새 풀로 교체하려면 먼저 제자리 OnDelete 절차를 수행할 필요는 없어요.
OnDelete로 브로커 제자리 교체 (Replace brokers in place with OnDelete)
이 OnDelete 절차는 StatefulSet에 배포된 브로커에 적용되며, 위에서 설명한 안정적인 정체성을 갖고요. Kubernetes Deployment는 StatefulSet OnDelete 업데이트 전략을 지원하지 않으므로 그 컨트롤러에 적합한 롤아웃 오케스트레이션을 사용해요.
브로커 이미지나 파드 구성을 바꾸기 전에 broker.updateStrategy.type: OnDelete(chart 4.6.0 이상)를 설정해요.
broker:
updateStrategy:
type: OnDelete
chart 기본값은 RollingUpdate예요. 그러면 Kubernetes는 파드 준비 상태와 구성된 minReadySeconds에 따라 진행해요. Pulsar 등록, 부하 보고서, 전송된 트래픽 복구 여부를 확인하지 않아요. 파드는 이런 조건이 충족되기 전에 준비성 프로브를 통과할 수 있어요. 준비성 프로브 지연을 길게 하는 것만으로는 검증되지 않아요.
기존 StatefulSet의 spec.updateStrategy.type을 재생성 없이 OnDelete로 바꿀 수 있어요. 그 변경을 Helm values나 배포 매니페스트에 영속화하고 파드 템플릿을 바꾸기 전에 적용해요. RollingUpdate로 다시 전환하면 여전히 이전 리비전을 가진 파드의 자동 교체가 시작될 수 있으므로, 통제된 롤아웃 전체 동안 OnDelete를 유지해요.
OnDelete로 새 파드 템플릿을 적용해도 기존 파드를 재시작하지 않아요. 스크립트, 오퍼레이터, 또는 수동 절차가 공유 정상-종료 절차를 사용해 한 번에 하나의 브로커 파드를 삭제해야 해요. StatefulSet이 업데이트된 템플릿으로 파드를 재생성해요. 다음 브로커로 진행하기 전에 그 교체본이 모든 재시작 검사를 통과할 때까지 기다려요. 파드를 순번으로만 삭제하지 말고 위의 리더-마지막 순서를 따라요. 모든 브로커가 의도한 리비전을 실행할 때까지 반복해요. 다른 롤아웃 컨트롤러를 사용한다면 진행 전에 같은 검사를 강제해야 해요. Kubernetes StatefulSet update strategies 참고.
제자리에서 파드를 교체하면 교체본이 시작될 때까지 그 브로커의 용량이 사라져요. 제거된 브로커에 연결된 클라이언트는 재연결해야 하므로, 이 절차는 중단 없는 클라이언트 연산을 보장하지 않아요. 번들이 생존 브로커에 정상적으로 핸드오프되면 클라이언트는 교체본이 시작되기 전에 복구할 수 있어요. 소유권이 여전히 제거된 브로커를 가리키면 클라이언트는 교체본이 시작되어 소유권을 회수할 때까지, 또는 세션 만료가 이전 소유권 임대를 해제하고 다른 브로커가 소유권을 획득할 때까지 사용 불가로 남을 수 있어요. Ownership leases and forced termination 참고. 두 번째 브로커 풀은 이전 파드를 은퇴시키기 전에 교체 용량을 키울 수 있게 해요.
기존 브로커 StatefulSet을 새 브로커 StatefulSet으로 교체 (Replace the existing broker StatefulSet with a new broker StatefulSet)
이것은 브로커만의 blue-green 배포와 비슷해요. 기존과 교체 브로커 StatefulSet이 같은 Pulsar 클러스터를 서빙하고, BookKeeper와 메타데이터 스토어는 그대로 있어요. 워크로드는 한 번에 하나의 브로커로 점진적으로 이동해요. 업그레이드에 문제가 생기면 롤아웃을 중지하고 이전 버전과 구성을 실행하는 브로커로 워크로드를 되돌릴 수 있어요. Roll back a broker rollout 참고.
이 롤아웃은 모듈식과 확장 가능한 로드 매니저 모두에서 동작해요. 각 교체 브로커를 시작하고, 명시적 목적지 언로드로 번들을 이동하고, 이전 브로커를 은퇴시켜요. 교체 용량을 먼저 시작하고 정상적으로 번들을 이동하면 제자리 교체본이 시작되거나 버려진 소유권 임대를 세션 만료가 해제하기를 기다리는 것을 피해요.
확장 가능한 로드 매니저는 클라이언트 핸드오프를 추가해요. 호환 클라이언트를 토픽에서 연결 해제할 때 새 브로커 주소를 제공해 추가 룩업을 피하고 직접 재연결하게 해요(PIP-307). 모듈식 로드 매니저에서는 클라이언트가 룩업을 통해 목적지를 발견해요. 두 매니저 모두 토픽 핸드오프와 클라이언트 재연결은 여전히 짧은 중단을 일으켜요.
이 대안은 파드를 제자리에서 업데이트하는 대신 전체 StatefulSet을 교체해요. OnDelete에 의존하지 않아요. 이전 파드를 고아화하고 명시적으로 은퇴시켜 제거를 제어해요. 성장시키는 동안 새 StatefulSet의 파드 템플릿은 변경하지 않은 채 유지해요.
같은 Pulsar 클러스터에서 두 브로커 풀을 사용하고, 하나의 추가 브로커를 위한 충분한 임시 용량과 전송된 워크로드를 위한 충분한 목적지별 용량을 갖고요. 다음은 오케스트레이션 패턴이며 자동화된 Helm 기능이 아니에요. 선택한 네임스페이스로 교체본을 먼저 테스트하려면 step 2에서 기존 StatefulSet을 고아화하기 전에 선택적 canary 단계를 완료해요.
-
자동 셰딩·분할이 이미 일시 중지된 상태에서, 한 복제본과 구별되는 이름, 그리고 이전 파드와 일치하지 않는 선택자로 교체 StatefulSet을 만들어요. headless Service, 광고된 주소, 인증서를 포함한 공유 배포 전제 조건을 적용해요. 준비성 게이트 클라이언트 Service가 두 풀 모두로 라우팅할 수 있는지 확인해요. 이전 headless Service와 DNS 이름을 모든 이전 파드가 사라질 때까지 유지해요.
-
브로커를 교체하기 전에 Helm, GitOps, 또는 다른 컨트롤러가 이전 StatefulSet을 재생성하지 않게 중지하고, 실행 중인 파드를 고아화해요.
kubectl -n <namespace> delete statefulset <old-broker-statefulset> --cascade=orphan고아 삭제는 파드를 실행 상태로 두지만, 삭제된 파드를 교체할 StatefulSet 컨트롤러를 제거해요. 롤아웃 컨트롤러나 오퍼레이터가 이제 그 고아 파드의 실패를 처리해야 해요.
-
새 StatefulSet의 첫 브로커가 모든 등록·헬스·도달 가능성·부하 보고 검사를 통과할 때까지 기다려요.
-
현재 리더가 아닌 이전 브로커를 선택해요. 그 워크로드 번들을 열거하고 통제된 속도로 새 브로커를 가리키는 명시적 목적지로 언로드해요. 목적지의 소유권과 클라이언트 트래픽을 확인해요. 종료 명령 단독은 적격 브로커에서 목적지를 선택해요. 모든 트래픽이 새 브로커로 이동한다는 보장은 없어요. 오케스트레이션은 배수 중 이전 브로커에 새 할당이 생기는 것과 최종 종료 시 남아 있는 번들을 고려해야 해요.
-
워크로드가 이동한 후 공유 정상-종료 절차로 이전 파드를 삭제해 남은 소유권을 해제해요. 배수된 고아 파드를 삭제하면 이전 StatefulSet이 교체본을 만들지 않고 그 파드를 은퇴시켜요. 제거를 확인하고 그 번들을 받는 모든 브로커의 업데이트된 부하 보고서를 확인해요.
-
새 StatefulSet을 한 복제본 늘리고 3~5단계를 반복해요. 새 브로커를 확인하고, 이전 브로커의 번들을 이동하고, 이전 파드를 삭제해요. 각 반복마다 공유 리더-마지막 순서를 따라요. 새 풀이 의도한 크기가 되고 모든 이전 파드가 사라질 때까지 계속하고, 이전 셰딩·분할 설정을 복원하고 복구를 확인해요.
신뢰할 수 있는 자동화는 전송 완료, 동시 할당, 브로커·컨트롤러 실패, 재시도 간 진행을 추적해야 해요. 이 오케스트레이션을 개선하는 것은 기여 기회예요. 두 로드 매니저 모두 선택한 목적지로 번들을 이동하는 브로커 쪽 기능을 제공하지만, Kubernetes 오퍼레이터 또는 동등한 컨트롤러 로직이 롤아웃을 조정해야 해요.
카나리 네임스페이스로 교체본 테스트 (Test the replacement with canary namespaces)
네임스페이스 격리 정책은 두 로드 매니저 모두에서 카나리 단계를 지원해요. 기존 StatefulSet을 그대로 유지하면서 교체 StatefulSet의 첫 브로커에서 선택한 네임스페이스를 실행해요.
교체 브로커를 primary 그룹으로 사용해 일반 네임스페이스 할당에서 예약하고, 기존 브로커를 선택적 secondary 그룹으로 사용해 폴백을 제공해요. 시작 전에 broker reservation, secondary broker failover, placement and rebalancing considerations를 검토해요. 다른 정책이 프로덕션 네임스페이스를 교체 그룹에 허용하지 않는지 기존 정책을 감사해요.
교체 StatefulSet을 만들기 전에 네임스페이스 격리 정책을 만들어요. 정책은 브로커 주소 패턴을 저장하며, 일치하는 브로커가 생성될 때 온라인이나 등록되어 있을 필요는 없어요. 기존 정책을 저장하고, 자동 셰딩·분할을 일시 중지한 다음, 계획된 교체 브로커 이름에 대한 임시 정책을 만들어요. 예를 들어 브로커가 pulsar Kubernetes 네임스페이스에서 파드별 FQDN을 광고하는 경우:
# Primary brokers are excluded from normal bundle assignment and load balancing.
pulsar-admin ns-isolation-policy set my-cluster broker-upgrade-canary \
--namespaces 'my-tenant/canary' \
--primary 'broker-green-\d+[.].*' \
--secondary 'broker-blue-\d+[.].*' \
--auto-failover-policy-type min_available \
--auto-failover-policy-params min_limit=1,usage_threshold=100 \
--unload-scope none
예시 호스트 이름과 네임스페이스를 Match broker addresses에 따라 자신의 것으로 교체해요. 교체 StatefulSet의 전체 호스트 이름 범위를 예약해서 이후 복제본도 예약된 채로 남게 해요. 교체 StatefulSet을 만들기 전에 정책이 전파될 때까지 기다려, 그 첫 브로커가 합류할 때 예약되게 해요. 합류 후 실제 번들 소유권을 확인해요. 이 절차 전체에서 --unload-scope none을 사용해 번들이 언제 이동할지 제어해요. Control unloading when updating a policy 참고.
- 격리 정책이 마련되고 전파된 후, 한 복제본으로 교체 StatefulSet을 만들어요. 등록·헬스·도달 가능성·부하 보고서를 기다려요. 평가 중에는 기존 StatefulSet과 파드를 실행 상태로 유지해요.
- 카나리 네임스페이스의 번들만 통제된 속도로 교체본으로 이동해요. 소유자를 확인하고 클라이언트 오류, 지연, 브로커 부하를 모니터링해요. 모든 명시적 --destinationBroker를 의도한 카나리 정책에 대해 검증해요.
- 카나리를 확장하려면 --unload-scope none으로 선택한 네임스페이스를 임시 정책에 추가하고, 전파를 기다린 다음 번들을 명시적으로 이동해요. 각 확장 전에 용량을 확인해요.
- 프로모션하려면, 교체본이 일반 워크로드에 승인된 후 임시 카나리 정책을 제거하거나 기존 프로덕션 격리 정책을 업데이트해 새 브로커를 허용해요. 결과 배치 규칙을 확인한 다음 step 2에서 StatefulSet 교체 절차를 계속해요. 완전한 롤아웃 동안 자동 셰딩을 일시 중지 상태로 유지해요.
- 카나리를 포기하려면 그 예약 정책을 유지하고, 교체 StatefulSet이 파드를 재생성하지 못하게 하고, 카나리 번들을 정상 이전 브로커로 되돌리고, 공유 정상-종료 절차로 교체 파드를 삭제해요. 등록 해제 후 임시 정책을 제거하고 원래 정책을 복원해요.
프로모션 후 격리 정책을 유지한다면 로드 셰딩을 복원하기 전에 자동 리밸런싱에 미치는 영향을 확인해요.
브로커 롤아웃 롤백 (Roll back a broker rollout)
두 전략 모두 업그레이드에 문제가 생기면 통제된 롤백을 허용해요. 이전 브로커 이미지, 매니페스트, 구성을 유지하고, Pulsar 버전과 구성 변경이 롤백을 지원하는지 확인해요. StatefulSet을 복원해도 공유 클러스터 메타데이터나 다른 컴포넌트의 변경은 되돌려지지 않아요.
- OnDelete로 제자리 교체: updateStrategy.type: OnDelete를 유지하면서 StatefulSet의 이전 파드 템플릿(이미지와 구성 포함)을 복원해요. 기존 파드는 실행 상태로 남아요. 이전 템플릿을 적용해도 자동으로 교체되지 않아요. 공유 정상-종료 절차로 업그레이드된 파드만 한 번에 하나씩 수동 삭제해요. 그 교체본은 복원된 템플릿을 사용해요. 이미 이전 버전을 실행 중인 파드는 그대로 둘 수 있어요.
- 새 브로커 StatefulSet으로 교체: 업그레이드된 브로커로 번들 이동을 중지해요. 저장된 구성에서 원래 StatefulSet을 OnDelete, 일치하는 선택자, 유지된 이전 파드를 보존하는 복제본 수로 재생성해요. 이전 버전에 충분한 용량을 복원하고 헬스·부하 보고 검사를 기다려요. 그 브로커로 번들을 이동한 다음 업그레이드된 파드를 정상적으로 은퇴시켜요. 파드를 삭제하기 전에 업그레이드된 StatefulSet을 고아화해 재생성하지 않게 하고, 정방향 롤아웃처럼 Helm·GitOps 조정을 조정해요. 각 StatefulSet의 Service를 마지막 브로커가 은퇴될 때까지 사용 가능하게 유지해요.
어느 롤백이든 공유 전제 조건(일시 중지된 리밸런싱, 리더-마지막 순서, 브로커 간 복구 검사)을 따라요. 롤백 완료 후 이전 밸런싱 설정을 복원해요.
브로커와 ZooKeeper의 필수 Service (Required Services for brokers and ZooKeeper)
브로커와 ZooKeeper StatefulSet 각각에 두 개의 Service를 배포해요. Chart 4.6.0 이상이 이 레이아웃을 제공해요. headless Service는 준비 상태 전에 파드별 정체성을 공급하고, 다른 Service는 Ready 파드에만 초기 클라이언트 연결을 받아요. 하나의 Service가 두 동작을 모두 제공할 수는 없어요.
아래 이름은 chart의 기본 컴포넌트 이름을 사용해요. 자신의 릴리스와 네임스페이스에 맞게 조정해요.
| Component | Service | Required configuration | Use |
|---|---|---|---|
| Brokers | <release>-broker-headless |
clusterIP: None, publishNotReadyAddresses: true; 브로커 StatefulSet의 spec.serviceName이 참조 |
직접 연결과 lookup 리다이렉트를 위해 브로커가 광고하는 파드별 이름 |
| Brokers | <release>-broker |
type: ClusterIP; publishNotReadyAddresses를 설정하지 않거나 false로 둠 |
브로커가 직면하는 클라이언트 serviceUrl / webServiceUrl, 및 프록시 brokerServiceURL / brokerWebServiceURL(TLS 변형 포함) |
| ZooKeeper | <release>-zookeeper-headless |
clusterIP: None, publishNotReadyAddresses: true; ZooKeeper StatefulSet의 spec.serviceName이 참조 |
앙상블 피어 발견·통신을 위한 안정적인 파드별 이름 |
| ZooKeeper | <release>-zookeeper |
type: ClusterIP; publishNotReadyAddresses를 설정하지 않거나 false로 둠 |
브로커, 부키, 다른 컴포넌트의 ZooKeeper 클라이언트 연결 |
한 컴포넌트의 두 Service 모두 그 컴포넌트의 파드를 선택해야 해요. headless Service의 type도 ClusterIP일 수 있어요. 구별 필드는 clusterIP: None이에요. 클라이언트 직면 Service는 할당된 클러스터 IP가 있어야 해요. StatefulSet의 serviceName을 클라이언트 직면 Service로 가리키지 말고, headless Service를 공유 클라이언트 진입점으로 사용하지 마세요.
브로커는 개별적으로 도달 가능한 주소를 광고해야 해요. chart 레이아웃에서 이것은 파드별 이름 <pod>.<headless-service>.<namespace>.svc.<cluster-domain>이에요. advertisedAddress를 설정하지 않으면 표준 호스트 이름을 사용하는데, 그것이 의도한 파드별 이름으로 해석되는지 확인하거나 그 이름을 명시적으로 구성해요. 공유 ClusterIP Service를 모든 브로커의 주소로 광고하지 마세요. advertised listener를 구성한다면 각 광고된 엔드포인트가 사용되는 네트워크에서 그 특정 브로커에 도달하는지 확인해요.
브로커는 Kubernetes가 파드를 Ready로 표시하기 전에 로드 매니저에 등록하고 번들을 받을 수 있어요. headless Service에 publishNotReadyAddresses: true가 없으면 Kubernetes는 준비 상태까지 파드의 DNS 레코드를 보류해요. 그 브로커로 리다이렉트된 클라이언트는 이름을 해석하지 못하고, 네거티브 DNS 캐싱이 중단을 연장할 수 있어요. 준비 상태 전에 파드별 주소를 게시하면 준비성 프로브에 대한 이 의존성이 제거돼요. DNS 전파 시간을 없애거나 브로커가 상태 좋다는 것을 증명하지는 않아요.
프록시 프론트 클러스터에도 같은 레이아웃이 필요해요. 프록시는 룩업에 준비성 게이트 브로커 Service를 사용한 다음 소유 브로커의 광고된 파드별 주소로 연결을 엽니다. 따라서 해석되지 않은 브로커 이름은 프록시 뒤의 외부 클라이언트도 중단시켜요. 외부 클라이언트는 계속 프록시의 엔드포인트를 사용하고, 프록시의 브로커 직면 URL은 브로커 ClusterIP Service를 사용해요. 공유 ClusterIP 이름은 또한 모든 브로커 IP를 하나의 DNS 응답에 반환하는 것을 피하는데, TCP 폴백이 없는 클라이언트에서 UDP DNS 한도를 초과할 수 있어요.
ZooKeeper는 보완적인 이유로 분할이 필요해요. 앙상블 피어는 시작 중 파드별 발견이 필요하지만, 브로커와 부키는 여전히 시작 중이거나 상태가 좋지 않은 ZooKeeper 서버로 보내지면 안 돼요. 그들의 ZooKeeper 연결 문자열을 준비성 게이트 클라이언트 Service로 가리켜요. 그 클라이언트 Service에 publishNotReadyAddresses: true를 설정하면 이 보호를 무너뜨려요.
이 요구 사항과 업그레이드 함의는 chart의 Service-split upgrade notes에 문서화되어 있어요.
Service 레이아웃으로 기존 배포 마이그레이션 (Migrate an existing deployment to the Service layout)
4.6.0보다 이전 chart에서의 첫 업그레이드는 브로커와 ZooKeeper StatefulSet serviceName 값을 모두 바꿔요. 그 필드는 변경할 수 없으므로 StatefulSet을 재생성해야 해요. chart는 broker.statefulsetUpgrade.enabled와 zookeeper.statefulsetUpgrade.enabled가 제어하는 컴포넌트별 pre-upgrade Job을 제공하며, 실행 파드와 ZooKeeper 데이터를 교체본이 입양할 수 있도록 보존하기 위해 --cascade=orphan으로 이전 StatefulSet을 삭제해요.
Helm 매니페스트를 렌더·적용하는 GitOps 도구의 경우 pre-upgrade Job이 교체 StatefulSet이 적용되기 전에 실행되는지 확인해요. 도구가 그 순서를 존중하지 않으면 해당 훅 플래그를 비활성화하고 고아 삭제를 계획된 마이그레이션 단계로 수행해요. 일반 연쇄(cascading) 삭제는 동등하지 않아요. 파드도 삭제할 거예요.
파드별 호스트 이름도 바뀌어요. TLS가 활성화되어 있으면 새 *-headless 이름을 SAN(subject alternative names)에 넣어 인증서를 다시 발급한 다음, 일치하는 인증서를 로드하도록 ZooKeeper와 브로커를 롤해요. OnDelete로 입양된 브로커 파드는 명시적으로 교체할 때까지 이전 파드 구성을 유지해요. 새 정체성과 인증서를 적용하려면 통제된 롤아웃을 완료해요.
롤링 전 배포 검증 (Verify the deployment before rolling)
오퍼레이터나 GitOps 도구가 적용한 재정의를 포함해 배포된 리소스를 검사해요.
kubectl -n <namespace> get statefulset <release>-broker <release>-zookeeper -o yaml
kubectl -n <namespace> get pod <broker-pod> -o yaml
kubectl -n <namespace> get service <release>-broker <release>-broker-headless <release>-zookeeper <release>-zookeeper-headless -o yaml
브로커 업데이트 전략, lifecycle.preStop 훅, 종료 유예 기간을 템플릿과 실행 파드 모두에서 확인해요. 두 StatefulSet의 serviceName 값, Service 선택자, 클러스터 IP, publishNotReadyAddresses 설정을 위 요구 사항에 맞춰 확인해요. ZooKeeper가 없는 배포에서는 ZooKeeper 리소스를 생략해요. 클라이언트·프록시 URL, 광고된 브로커 이름, TLS 인증서도 확인해요.
카나리 브로커 업그레이드 중 브로커/프록시 네트워크에서 교체 브로커의 광고된 이름이 준비성 프로브가 통과하기 전에 해석되는지, 공유 Service가 준비되지 않은 파드를 제외하는지, 다음 삭제 전에 브로커가 상태가 좋아지고 부하를 보고하는지 확인해요. DNS 도달 가능성과 부하 보고는 별개의 요구 사항이며, 둘 중 하나만 충족하는 것은 불충분해요.
chart 업그레이드 절차 자체는 Upgrade Pulsar Helm release를 참고해요.
업그레이드 후 복구 검증 (Verify recovery after the upgrade)
리소스 사용량과 번들 이동과 함께 클라이언트 오류와 게시 지연을 모니터링해요. 다른 용량의 브로커에서 동일한 트래픽을 요구하지 않고 제한 리소스(CPU나 대역폭 등)를 사용해 가장 바쁜 브로커와 가장 덜 바쁜 브로커를 비교해요. 확장 가능한 로드 매니저는 pulsar_lb_resource_usage_stats{feature="max_ema",stat="std"}와 pulsar_lb_unload_broker_breakdown_total의 언로드 결정 이유를 노출해요. 셰딩 활동에 대해 pulsar_lb_unload_bundle_total을 추적해요. 이것은 모든 종료 핸드오프의 개수가 아니에요. Load balancing metrics 참고.
이전 밸런싱 설정을 복원한 후 부하는 지속적 반복 언로드 없이 안정되어야 해요. 그렇지 않으면 보고 실패, 쿨다운·히트 카운트 결정, 배치 정책, 번들 세분성을 확인해요. 어떤 셰더도 크기가 큰 번들의 일부를 이동할 수 없어요. 롤아웃 중 임계값을 조이기 전에 load balancing configuration으로 조사해요.
더 알아보기 (Learn more)
- 클러스터 전체 업그레이드 순서는 Cluster upgrade guide 문서를 참고해요.
- Kubernetes의 롤아웃 전략과 요구 사항은 Kubernetes deployments 문서를 참고해요.
- 로드 밸런싱 개념과 셰딩 전략은 Broker load balancing 문서를 참고해요.
- 네임스페이스 격리 정책으로 카나리 테스트는 네임스페이스 격리 문서를 참고해요.