Rerouting the Stream: How Lyft Moved to the Apache Flink Operator
Author: Maheep Myneni, Arda Kuyumcu, Prem Santosh Udaya Shankar (Lyft) | Source: eng.lyft.com | Published: 2026-09-01
한 줄 요약
2020년에는 커뮤니티에 없어서 직접 만든 Flink Kubernetes operator가 유지보수 부담·기능 격차·의존성 고착으로 무너지자, CRD를 다시 쓰는 대신 deploy API 경계에서 번역해 앱 단위로 점진 이관했고, 그 뒤에야 오토스케일링·오토튜닝이라는 진짜 이득을 얻기 위한 후속 작업이 시작되었다.
핵심 주장/내용
- 왜 마이그레이션했나 — 세 가지 균열: 2020년에 Lyft Flink Kubernetes Operator를 만들었을 때는 정확히 필요한 것이었다. 그때는 오픈소스 커뮤니티에 전용 control plane이 아직 없었다. 초기 워크로드에는 잘 작동했지만 실시간 데이터 요구가 늘고 엔지니어의 소유 범위가 넓이와 복잡도 양쪽에서 커지자 균열이 보이기 시작했다
- 유지보수 부담 — 자체 operator가 Lyft 초기 Kubernetes 시절의 유물이 되어 늘어나는 커스텀 코드 더미로 연명했다. 모든 Flink 버전 업그레이드가 누적된 기술 부채 층을 조심스럽게 헤치며 도중에 아무것도 깨지지 않기를 바라는 일이었다
- 기능 격차 — 엔지니어들이 다른 곳에서는 이미 기본이 된 능력을 계속 요청했다 — 잡 right-sizing을 위한 오토스케일링, 실패한 배포의 자동 롤백, CPU·메모리 수동 튜닝의 종식. 매 요청마다 두 선택지밖에 없었고 둘 다 이상적이지 않았다 — 아직 지원할 수 없는 이유를 설명하거나, 오픈소스 커뮤니티가 이미 출시한 것을 몇 주 들여 다시 만들거나
- 의존성 문제 — 오래된 라이브러리에 고정되어 있었다. 오늘 뭔가를 깨뜨리지는 않지만 거의 항상 나중에 새 문제를 만든다. 보안 패치가 뒤처지고, 현대 Kubernetes 기능에 손이 닿지 않고, 기다리는 매 분기가 결국의 탈출을 더 지저분하게 만들었다
- 한편 Apache Flink Kubernetes Operator가 프로덕션급 인프라로 성숙했고 그 기능 목록이 우리 위시리스트처럼 읽혔다 — backpressure를 죽이는 지능적 job-vertex 오토스케일링, 자동 메모리 튜닝, 안전한 업그레이드 메커니즘, 견고한 reconciliation 루프. 게다가 대규모 기업들에 의해 battle-tested되었다
- 제약: 기존 배포 패턴을 방해하거나, 팀 워크플로를 바꾸거나, 프로덕션에 대규모 장애를 일으키지 않고 수백 개 중요 스트리밍 잡을 이관해야 했다
- 레거시의 복잡성은 state machine에 있었다: stateful 스트리밍 애플리케이션 배포는 stateless 웹 서비스 배포와 다르다. 옛 파드를 죽이고 새 것을 시작할 수 없다 — 그러면 in-flight 데이터와 상태가 전부 사라진다
- Dual Deployment Mode (기본) — 최소지만 0은 아닌 다운타임. 돌아가는 클러스터 옆에 완전히 새 Flink 클러스터를 띄우고, 새 클러스터가 healthy해지면 옛 잡에 savepoint를 트리거하고, 완료를 기다리고, 옛 잡을 취소하고, 옛 클러스터를 해체하고, 마지막으로 savepoint에서 새 클러스터에 잡을 제출
- BlueGreen Mode — 진짜 zero-downtime. 두 클러스터가 동시에 돌고(“dual running”) 새 버전이 잘 작동하는지 검증한 뒤
tearDownVersionHash필드를 설정해 수동으로 트래픽을 전환. 새 버전을 커밋 전에 검사하고 싶은 중요 잡에 수동 kill switch를 제공 - 추적한 상태:
ClusterStarting,Savepointing,SubmittingJob,Running,DeployFailed, 그리고 디버깅 단짝Recovering(savepoint가 실패해 externalized checkpoint로 폴백해야 할 때)
- 결정을 불가피하게 만든 두 pain point — 둘 다 작은 fleet에는 맞았지만 워크로드가 다양해지며 무너진 초기 설계 선택에서 왔다:
- savepoint 취약성 — savepoint가 업그레이드 중 상태 보존의 유일한 일급 메커니즘이었다. 실패하면 externalized checkpoint를 찾는 것으로 폴백했지만 checkpoint 폴백은 사용자가 의도적으로 고를 수 있는 전략이 아니라 최후의 복구 경로였다. savepoint 트리거에 재시도 로직도 idempotency 보장도 없었다. 최근 checkpoint가 없으면 배포가 실패하거나 상태 없이 잡을 재시작했다. savepoint가 느리거나 가끔 타임아웃되는 큰 상태 잡에서는 일시적 실패가 전체 복구 사이클로 확대되었다
- Apache operator가
last-state를 명시적 업그레이드 모드로 도입 — 설계상 high-availability 메타데이터나 최신 checkpoint를 사용한다. 특히 unhealthy한 JobManager가 savepoint 요청에 협조할 수 없는 잡에 정확히 필요했던 견고함
- Apache operator가
- 경직된 리소스 모델 — 자체 operator는
systemMemoryFraction손잡이 하나만 노출했다(비-JVM 용도로 예약된 컨테이너 메모리 비율). 순수 Flink Java 앱에는 이 오버헤드가 종종 불필요했고, Python SDK harness를 쓰는 Apache Beam 워크로드에는 필수적이었다. 그런데 그 Python 프로세스가 실제로 얼마나 필요한지는 본질적으로 추측이었다 — 사용이 Flink 메모리 모델 안에서 일어나 활용도에 대한 관측성이 없고 체계적으로 튜닝할 방법도 없었다. 너무 낮게 잡으면 TM이 OOM-kill되고, 너무 높게 잡으면 리소스를 낭비
- savepoint 취약성 — savepoint가 업그레이드 중 상태 보존의 유일한 일급 메커니즘이었다. 실패하면 externalized checkpoint를 찾는 것으로 폴백했지만 checkpoint 폴백은 사용자가 의도적으로 고를 수 있는 전략이 아니라 최후의 복구 경로였다. savepoint 트리거에 재시도 로직도 idempotency 보장도 없었다. 최근 checkpoint가 없으면 배포가 실패하거나 상태 없이 잡을 재시작했다. savepoint가 느리거나 가끔 타임아웃되는 큰 상태 잡에서는 일시적 실패가 전체 복구 사이클로 확대되었다
- 마이그레이션 청사진 — 경계에서 번역하기: 새 시스템을 세우는 것은 쉬운 부분이다. 어떤 마이그레이션이든 진짜 악몽은 여전히 옛 것을 가리키는 모든 것을 풀어내는 일이다
- Lyft의 모든 Flink 배포 설정은 Jsonnet 파일로 시작하고, 공유 모듈이 운영 기본값을 세팅하고 개별 팀이 자기 앱 설정을 얹는다. 전체 시스템이 레거시 operator의
FlinkApplicationCRD를 중심으로 지어져 있었다. 모든 라이브러리와 애플리케이션 오버레이를 Apache operator의FlinkDeploymentCRD를 네이티브로 생산하도록 다시 쓰는 것은 거대하고 위험한 작업이었을 것 - 대신 경계에서 번역하기로 선택해 팀들이 늘 하던 대로 애플리케이션을 계속 정의할 수 있게 했다.
FlinkApplication→FlinkDeployment번역이 deploy API 안에서 일어난다. 구조적 매핑:- Job Spec —
jarName,parallelism같은 flat 필드를 Apache operator의 중첩 spec으로 조립.jarName이jarURI가 된다 - Upgrade Mode —
last-state를 기본으로 해서 가능하면 마지막 checkpoint에서 상태를 복원하고,savepointDisabled같은 레거시 설정은stateless로 매핑 - Pod Templates — 레거시 spec의 인라인 컨테이너 설정(node selector, toleration, sidecar annotation)을 적절한
PodTemplateSpec객체로 승격 - 하위 호환성 —
APP_NAME,HOST_NAME같은 레거시 환경 변수를 주입해 애플리케이션 코드가 깨지지 않게
- Job Spec —
- 결정적으로 마이그레이션이 점진적이었다. 동적 설정으로 staging부터 시작해 한 번에 한 애플리케이션씩 이관할 수 있었고, 문제가 생기면 즉시 레거시 operator로 롤백할 수 있었다. 개발자에게 변화는 대체로 보이지 않았다
- Lyft의 모든 Flink 배포 설정은 Jsonnet 파일로 시작하고, 공유 모듈이 운영 기본값을 세팅하고 개별 팀이 자기 앱 설정을 얹는다. 전체 시스템이 레거시 operator의
- 참호에서 — operator 통합을 위한 추가 작업: 번역 레이어가 Apache operator 위에 올려놨지만 마이그레이션은 아직 끝나지 않았다. Apache operator의 이득을 실제로 보려면 후속 문제들을 다뤄야 했다
- BlueGreen Deployments — 앱을 이관하자 모든 배포가 평균 3~6분, 가장 큰 잡은 20분에 가까운 다운타임을 냈다. Lyft operator는 복잡한 연산 그래프로 JM과 TM 라이프사이클을 함께 관리해 다운타임을 최소화했었다. Apache operator는 다른 접근을 취해 JobManager만 띄우고 관련 savepoint·리소스 spec으로 설정한 뒤 TaskManager 조율을 맡긴다. 매 배포가 savepoint 찍기 → JM 파드가 TM 파드를 해체하도록 트리거 → JM 파드 해체 → 방금 찍은 savepoint로 새 FlinkDeployment의 JM 시작 → 새 TM 파드 기동 의 전체 시퀀스를 걷는다. Lyft 같은 실시간 비즈니스에는 여러 중요 유스케이스에서 받아들일 수 없었다
- 수정은
FlinkBlueGreenDeployment— 기저 FlinkDeployment 객체를 유지해 cutover 전에 새 버전이 옛 것 옆에 올라올 수 있게 하는 신규 CRD. 테스트 시작 시점에는 아직 개발 중이었고 BlueGreen에서 Flink 설정 필드 이름을 바꾸는 방식에서 버그를 만나 upstream에 마이너 수정을 기여했다. 오픈소스 커뮤니티의 노력에서 이득을 보고 되기여할 수 있다는 것이 오픈소스 솔루션으로 옮긴 결정을 더욱 강화했다
- 수정은
- 오토스케일링 — Apache operator의 오토스케일러는 operator busyness, backpressure, Kafka·Kinesis의 record backlog 지표로 각 잡의 parallelism을 조정한다. Lyft의 대부분 유스케이스에서 트래픽이 매우 가변적이고 스트림 처리가 비싸므로 이론상 명백한 승리였다. 실제로는 당시 Flink 버전(v1.17)이 rescale을 위해 앱 재시작을 요구했고, 오토스케일링에서 가장 이득을 볼 앱들이 바로 재시작 다운타임을 감당할 수 없는 앱들이었다
- Flink v1.19 업그레이드가 두 가지를 풀었다: in-place 오토스케일링 — 잡이 재시작 없이 parallelism을 바꿀 수 있어 데이터 freshness를 보존하고 잡이 몇 분 전 오프셋에서 재개할 때 보던 spiky catch-up 트래픽을 피한다 / 새 Amazon Kinesis Source Connector 업그레이드 — Kafka source와 같은 방식으로 Kinesis 스트림의 record backlog 지표를 방출해 이전에 사각지대였던 Kinesis 기반 잡에 대해 오토스케일러가 필요한 신호를 얻었다
- 오토튜닝 — 동적 right-sizing으로 클라우드 비용 절감과 수동 리소스 설정 튜닝 해방을 약속했다. 그런데 롤아웃 후 예상치 못한 문제를 만났다
- Apache Beam 워크로드가 Flink 메모리 모델 밖에 사는 Python SDK harness에서 사용자 코드를 돌린다. Apache operator는 우리가 그 Python 프로세스를 위해 JVM overhead 할당 안에 메모리를 예약해두고 있다는 것을 인식하지 못했다. 대부분의 잡이 JVM overhead를 거의 안 건드렸기 때문에 몇 년간 거기 밀어 넣어두던 hack이었다. 오토튜닝을 켜자 operator가 그 메모리 풀이 Flink에게 실제로 쓰이지 않는 것을 보고 회수해 Beam fleet 전반의 TaskManager를 OOM-kill했다
- 결국 Python harness를 Flink 컨테이너 밖으로 완전히 옮겨 같은 파드의 sidecar로 자체 리소스 한도를 갖고 돌게 했다
- 또 다른 어색한 속성: 오토튜닝이 in-place 오토스케일링과 잘 어울리지 않는다. 잡의 리소스를 튜닝하려면 operator가 잡을 재시작해야 한다. 둘 다 포기할 수 없어 2-tier 접근으로 정착했다 — 가격·라우팅 등 실시간 의사결정을 구동하는 비즈니스 크리티컬 앱은 오토튜닝을 끄고 in-place 오토스케일링만 유지, 어느 정도 다운타임을 감내할 수 있는 덜 중요한 앱은 재시작을 동반한 오토튜닝 + 오토스케일링 둘 다 사용
- Karpenter — 이 기능들이 노드 인프라를 낡게 만들었다. 오토튜닝 전에는 power-of-two 값의 1:4 또는 1:8 CPU:메모리 비율을 강제해 AWS 노드에 파드를 효율적으로 bin-pack했다. 오토튜닝이 그것을 깨뜨렸다 — 같은 앱이 튜닝 사이클마다 매우 다른 비율을 가질 수 있고 정적 노드 풀이 따라갈 수 없다. 각 파드가 실제로 필요로 하는 것에 기반해 on-demand로 EC2 노드를 프로비저닝하는 Karpenter를 채택
- 2-tier 오토튜닝 모델이 예상치 못한 효율 이득을 하나 더 냈다 — unused spend(지불하는데 할당되지 않는 컴퓨트 비율) 관련. 8코어 32GB 노드에 지불하면서 4코어 16GB만 할당하면 노드 비용의 절반이 미사용이다. 이미 EKS 클러스터에 “most-allocated” 전략으로 바쁜 노드에 먼저 파드를 채우는 커스텀 Kubernetes 파드 스케줄러를 배포해뒀는데, 오토스케일링이 rescale을 위해 앱을 재시작할 때 파드가 저활용 노드에서 밀집 노드로 옮겨갈 기회를 얻어 저활용 노드를 떨어낼 수 있었다
- BlueGreen Deployments — 앱을 이관하자 모든 배포가 평균 3~6분, 가장 큰 잡은 20분에 가까운 다운타임을 냈다. Lyft operator는 복잡한 연산 그래프로 JM과 TM 라이프사이클을 함께 관리해 다운타임을 최소화했었다. Apache operator는 다른 접근을 취해 JobManager만 띄우고 관련 savepoint·리소스 spec으로 설정한 뒤 TaskManager 조율을 맡긴다. 매 배포가 savepoint 찍기 → JM 파드가 TM 파드를 해체하도록 트리거 → JM 파드 해체 → 방금 찍은 savepoint로 새 FlinkDeployment의 JM 시작 → 새 TM 파드 기동 의 전체 시퀀스를 걷는다. Lyft 같은 실시간 비즈니스에는 여러 중요 유스케이스에서 받아들일 수 없었다
주요 수치 / 사실
- 이관 대상: 수백 개의 중요 스트리밍 잡, 여러 분기에 걸친 여정
- Apache operator 초기 이관 후 배포 다운타임 평균 3~6분, 최대 잡은 약 20분 → FlinkBlueGreenDeployment로 해결
- Flink v1.17 → v1.19 업그레이드가 in-place 오토스케일링과 Kinesis backlog 지표를 풀었다
- 오토스케일러가 연 수백만 달러 규모로 과프로비저닝되어 있던 fleet을 right-sizing하고 있다
- 정량화하기 어렵지만 중요한 이득: “우리 operator와 인프라의 유일한 기여자이기를 그만뒀다. BlueGreen 버그를 발견했을 때 이슈를 올렸고 수정이 며칠 안에 upstream에 착지했다. 팀의 자세가 ‘우리가 이걸 유지한다’에서 ‘우리는 이걸 쓰고, 가끔 모두를 위해 개선한다’로 바뀌었고, 그것이 훨씬 나은 자리다”
- “비행 중에 엔진을 갈아 끼우는 것처럼 느껴지는 여정이었다. state machine 특이사항, 메모리 모델 불일치, 깔끔히 번역되지 않는 CRD의 엉망 때문에 사내 operator를 완전히 일몰시키는 데 예상보다 오래 걸렸다”
- “때로는 당신이 쓸 수 있는 최고의 코드는 당신이 지우는 코드다”
관련 위키
- Stream Autoscaling
- Real-Time Stream Processing
- Open Source Data Stack
- Data Engineering FinOps
- Large-Scale Data Migration
- Spot Instance Management
- Distributed Systems Reliability
- Workflow Orchestration
Source: 원문 보기