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이 아직 없었다. 초기 워크로드에는 잘 작동했지만 실시간 데이터 요구가 늘고 엔지니어의 소유 범위가 넓이와 복잡도 양쪽에서 커지자 균열이 보이기 시작했다
    1. 유지보수 부담 — 자체 operator가 Lyft 초기 Kubernetes 시절의 유물이 되어 늘어나는 커스텀 코드 더미로 연명했다. 모든 Flink 버전 업그레이드가 누적된 기술 부채 층을 조심스럽게 헤치며 도중에 아무것도 깨지지 않기를 바라는 일이었다
    2. 기능 격차 — 엔지니어들이 다른 곳에서는 이미 기본이 된 능력을 계속 요청했다 — 잡 right-sizing을 위한 오토스케일링, 실패한 배포의 자동 롤백, CPU·메모리 수동 튜닝의 종식. 매 요청마다 두 선택지밖에 없었고 둘 다 이상적이지 않았다 — 아직 지원할 수 없는 이유를 설명하거나, 오픈소스 커뮤니티가 이미 출시한 것을 몇 주 들여 다시 만들거나
    3. 의존성 문제 — 오래된 라이브러리에 고정되어 있었다. 오늘 뭔가를 깨뜨리지는 않지만 거의 항상 나중에 새 문제를 만든다. 보안 패치가 뒤처지고, 현대 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 요청에 협조할 수 없는 잡에 정확히 필요했던 견고함
    • 경직된 리소스 모델 — 자체 operator는 systemMemoryFraction 손잡이 하나만 노출했다(비-JVM 용도로 예약된 컨테이너 메모리 비율). 순수 Flink Java 앱에는 이 오버헤드가 종종 불필요했고, Python SDK harness를 쓰는 Apache Beam 워크로드에는 필수적이었다. 그런데 그 Python 프로세스가 실제로 얼마나 필요한지는 본질적으로 추측이었다 — 사용이 Flink 메모리 모델 안에서 일어나 활용도에 대한 관측성이 없고 체계적으로 튜닝할 방법도 없었다. 너무 낮게 잡으면 TM이 OOM-kill되고, 너무 높게 잡으면 리소스를 낭비
  • 마이그레이션 청사진 — 경계에서 번역하기: 새 시스템을 세우는 것은 쉬운 부분이다. 어떤 마이그레이션이든 진짜 악몽은 여전히 옛 것을 가리키는 모든 것을 풀어내는 일이다
    • Lyft의 모든 Flink 배포 설정은 Jsonnet 파일로 시작하고, 공유 모듈이 운영 기본값을 세팅하고 개별 팀이 자기 앱 설정을 얹는다. 전체 시스템이 레거시 operator의 FlinkApplication CRD를 중심으로 지어져 있었다. 모든 라이브러리와 애플리케이션 오버레이를 Apache operator의 FlinkDeployment CRD를 네이티브로 생산하도록 다시 쓰는 것은 거대하고 위험한 작업이었을 것
    • 대신 경계에서 번역하기로 선택해 팀들이 늘 하던 대로 애플리케이션을 계속 정의할 수 있게 했다. 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 같은 레거시 환경 변수를 주입해 애플리케이션 코드가 깨지지 않게
    • 결정적으로 마이그레이션이 점진적이었다. 동적 설정으로 staging부터 시작해 한 번에 한 애플리케이션씩 이관할 수 있었고, 문제가 생기면 즉시 레거시 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을 위해 앱을 재시작할 때 파드가 저활용 노드에서 밀집 노드로 옮겨갈 기회를 얻어 저활용 노드를 떨어낼 수 있었다

주요 수치 / 사실

  • 이관 대상: 수백 개의 중요 스트리밍 잡, 여러 분기에 걸친 여정
  • 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를 완전히 일몰시키는 데 예상보다 오래 걸렸다”
  • “때로는 당신이 쓸 수 있는 최고의 코드는 당신이 지우는 코드다”

관련 위키


Source: 원문 보기