scala

3 개의 포스트

Background Coding Agents: Supercharging Downstream Consumer Dataset Migrations (Honk, Part 4) | Spotify Engineering (새 탭에서 열림)

Spotify는 배경 코딩 에이전트 'Honk'와 내부 플랫폼인 Backstage를 결합하여 약 1,800개의 데이터셋 소비자를 새로운 버전으로 마이그레이션하는 복잡한 과정을 자동화했습니다. 이 프로젝트를 통해 수동 작업 시 약 10주가 소요될 것으로 예상되었던 엔지니어링 공수를 획기적으로 절감하며 대규모 소프트웨어 유지보수의 새로운 가능성을 확인했습니다. 결과적으로 에이전트 기반의 자동화가 성공하려면 데이터 환경의 표준화와 명확한 컨텍스트 제공이 핵심적이라는 교훈을 얻었습니다. **데이터셋 마이그레이션의 도전 과제** * Spotify는 새로운 기능을 지원하기 위해 널리 사용되던 기존 데이터셋 2개를 폐기하고 새 버전으로 교체해야 하는 상황에 직면했습니다. * 마이그레이션 대상은 약 1,800개의 직접적인 파이프라인이었으며, SQL 기반(BigQuery Runner, dbt)과 Scala 기반(Scio)이라는 서로 다른 세 가지 프레임워크가 혼재되어 있었습니다. * 6개월이라는 짧은 기간 내에 수천 개의 저장소를 수정해야 했기에, 단순 수동 작업으로는 감당하기 어려운 규모였습니다. **Backstage와 Fleet Management를 통한 대상 식별** * 마이그레이션 전, Backstage의 'Endpoint Lineage'와 'Codesearch' 플러그인을 활용하여 폐기될 데이터셋을 사용하는 모든 저장소와 팀을 정확히 파악했습니다. * 식별된 대상 저장소들은 Spotify의 대규모 변경 관리 도구인 'Fleetshift'를 통해 관리 범주로 지정되었습니다. * 이를 통해 수천 개의 저장소에 걸친 변경 사항을 한곳에서 모니터링하고 조율할 수 있는 기반을 마련했습니다. **에이전트를 위한 컨텍스트 엔지니어링과 제약 사항** * Honk 에이전트가 정확한 수정을 수행할 수 있도록 인간용 마이그레이션 가이드를 재구성하여 상세한 컨텍스트 파일을 제공했습니다. * 초기에 에이전트가 잘못된 필드 매핑을 추측하는 문제를 해결하기 위해, 모든 필드 변경 사항을 명확한 테이블 형태로 프롬프트에 포함했습니다. * 프레임워크의 유연성이 너무 높아 표준화가 어려운 Scio 파이프라인은 자동화 대상에서 제외하고, 비교적 구조가 일관된 SQL 기반 프레임워크(dbt, BigQuery Runner)에 집중했습니다. * 에이전트가 스스로 판단하기 어려운 모호한 케이스의 경우, 코드를 직접 수정하는 대신 해당 위치에 인간 엔지니어가 참고할 수 있는 가이드 링크와 주석을 남기도록 설정했습니다. **자동화된 마이그레이션의 성과와 기술적 교훈** * Fleetshift를 통해 총 240개의 자동 마이그레이션 Pull Request(PR)를 성공적으로 배포했습니다. * 하지만 많은 SQL 저장소에 유닛 테스트가 부족하여, 에이전트가 수정한 내용을 스스로 검증하고 보완하는 '자가 수정 루프'를 완전히 활용하지 못한 점은 한계로 남았습니다. * 이번 프로젝트를 통해 데이터 환경의 전략적 표준화와 테스트 코드의 의무화가 배경 코딩 에이전트의 효율을 극대화하는 필수 조건임을 확인했습니다. 성공적인 에이전트 도입을 위해서는 코드의 표준화와 테스트 자동화가 선행되어야 합니다. 향후 Spotify는 Honk가 스스로 Jira 티켓이나 문서를 읽고 컨텍스트를 수집하는 기능을 추가하여, 인간이 사전 컨텍스트를 작성하는 수고를 더욱 줄이고 복잡한 작업의 성공률을 높일 계획입니다.

How we optimized our Akka application using Datadog’s Continuous Profiler (새 탭에서 열림)

Datadog은 Akka 프레임워크 기반 서비스에서 예상치 못한 20~30%의 CPU 과점유 문제를 발견했으며, 이는 성능 프로파일링 도구를 통해 `ForkJoinPool` 내부의 비효율성 때문인 것으로 밝혀졌습니다. 불규칙한 작업 부하를 가진 액터가 기본 디스패처에서 실행될 때, 스레드의 빈번한 생성과 정지(`park`/`unpark`)가 반복되면서 과도한 네이티브 호출 오버헤드가 발생한 것이 원인이었습니다. 이를 안정적인 작업 흐름을 가진 전용 디스패처로 분리함으로써 시스템 전반의 CPU 효율을 획기적으로 개선할 수 있었습니다. **성능 병목 현상의 발견과 프로파일링** * 로그 파싱 알고리즘을 최적화했음에도 불구하고 CPU 사용량이 줄어들지 않고 오히려 성능이 저하되는 기현상이 발생했습니다. * Datadog Continuous Profiler의 플레임 그래프 분석 결과, 예상 밖으로 `ForkJoinPool.scan()` 및 `Unsafe.park()` 메서드에서 전체 CPU의 상당 부분이 소모되고 있었습니다. * 상세 분석 결과, 특정 작업 풀(Work pool)이 아닌 Akka의 기본 디스패처(Default Dispatcher) 소속 스레드들이 대부분의 CPU 시간을 점유하고 있었으며, 이는 주로 지표를 보고하는 `LatencyReportActor`와 연관되어 있었습니다. **ForkJoinPool 오버헤드의 근본 원인** * `ForkJoinPool`은 대기 중인 작업량에 따라 내부 워커 스레드를 동적으로 추가하거나 `Unsafe.park()`/`unpark()`를 호출하여 스레드를 일시 중지 및 재개합니다. * 문제가 된 액터는 매초 수백 개의 이벤트를 순식간에 처리한 뒤 다음 배치가 올 때까지 대기하는 불규칙한(Irregular) 작업 흐름을 가지고 있었습니다. * 이로 인해 매초 가용한 모든 코어(예: 32코어)만큼의 스레드가 동시에 깨어났다가 작업 완료 후 즉시 잠드는 과정이 반복되었고, 이 과정에서 발생하는 네이티브 호출 비용이 실제 작업 비용보다 커지는 결과가 초래되었습니다. **디스패처 분리를 통한 문제 해결** * 불규칙한 부하를 가진 액터를 기본 디스패처에서 분리하여, 부하가 일정하게 유지되는 메인 "work" 디스패처로 이동시키는 단 한 줄의 설정 변경을 적용했습니다. * 수정 후 서비스 전체 CPU 사용량이 평균 30% 감소했으며, 프로파일링 결과 `ForkJoinPool.scan()`에서 소모되던 시간이 거의 사라진 것을 확인했습니다. * 기본 디스패처의 스레드 풀 크기가 32개에서 2개로 줄어들며 스레드 관리 효율이 극대화되었습니다. **안정적인 성능 유지를 위한 권장 사항** * `ForkJoinPool.scan()` 메서드의 CPU 점유율이 10~15%를 초과한다면 스레드 풀 설정과 부하의 안정성을 반드시 점검해야 합니다. * Akka 사용 시 액터 인스턴스 수를 제한하거나 스레드 풀의 최대 크기를 적절히 설정하여 무분별한 스레드 확장을 방지해야 합니다. * 작업 큐를 활용해 급격한 작업 스파이크를 완만하게 조절하거나, 사용되는 스레드 풀의 수를 줄여 부하 밀집도를 높임으로써 활성 스레드 수를 일정하게 유지하는 것이 중요합니다.

Datadog의 Continuous Profil (새 탭에서 열림)

Datadog은 자바 기반 어플리케이션에서 Akka 프레임워크를 사용할 때 발생하는 예상치 못한 CPU 사용량 급증 문제를 조사하였으며, 그 원인이 `ForkJoinPool`의 비효율적인 스레드 관리임을 밝혀냈습니다. 불규칙한 작업 흐름을 가진 액터(Actor)가 과도한 스레드 활성화 및 비활성화를 유발하여 전체 CPU의 20~30%를 낭비하고 있었으나, 이를 안정적인 작업 부하를 가진 디스패처로 이동시킴으로써 문제를 해결했습니다. 이 사례는 추상화된 프레임워크 하부의 스레드 풀 동작 원리를 이해하고 프로파일링을 통해 실질적인 병목 지점을 찾는 것이 얼마나 중요한지 보여줍니다. ### 프로파일링을 통한 병목 지점 탐색 * 로그 파싱 알고리즘을 최적화했음에도 불구하고 CPU 사용량이 줄어들지 않거나 오히려 늘어나는 현상이 발생하여 Datadog Continuous Profiler를 통해 심층 분석을 수행했습니다. * 플레임 그래프(Flame Graph) 분석 결과, 실제 비즈니스 로직이 아닌 `ForkJoinPool.scan()` 및 `Unsafe.park()` 메서드에서 상당한 CPU 시간이 소비되고 있음을 확인했습니다. * 특정 스레드 풀의 상태를 조사한 결과, 별도의 설정 없이 기본 Akka 디스패처를 사용하는 `LatencyReportActor`가 이 현상의 주범으로 지목되었습니다. ### ForkJoinPool의 동작 원리와 병목 원인 * `ForkJoinPool`은 대기 중인 작업 수에 따라 내부 워커 스레드를 동적으로 생성, 중지(`park`), 재개(`unpark`)하며 활성 스레드 수를 관리합니다. * 작업 흐름이 불규칙할 경우 스레드를 빈번하게 깨우고 다시 재우는 과정에서 비용이 큰 네이티브 호출인 `Unsafe.park()`와 `unpark()`가 대량으로 발생하여 CPU를 낭비하게 됩니다. * 문제의 액터는 매초 짧은 시간 동안만 데이터를 처리하고 나머지 시간은 대기하는 특성을 가졌는데, 이때마다 CPU 코어 수만큼 설정된 32개의 스레드가 일시에 깨어났다가 다시 잠드는 현상이 반복되었습니다. ### 설정 변경을 통한 성능 최적화 * 불규칙한 작업을 수행하는 액터를 기본 디스패처에서 이미 안정적인 작업 흐름을 유지하고 있는 메인 'work' 디스패처로 이동시키는 단 한 줄의 설정 변경을 적용했습니다. * 이 변경을 통해 `ForkJoinPool.scan()`에서 소비되는 CPU 시간이 급격히 감소하였으며, 서비스 전체의 평균 CPU 사용량이 약 30% 줄어드는 성과를 거두었습니다. * 또한 32개까지 치솟았던 기본 디스패처의 스레드 풀 크기가 2개로 줄어들어 불필요한 컨텍스트 스위칭과 리소스 낭비가 해결되었음을 확인했습니다. ### 효율적인 ForkJoinPool 관리를 위한 권장 사항 `ForkJoinPool`이나 Akka를 사용하는 환경에서 성능을 유지하려면 `ForkJoinPool.scan()` 메서드의 CPU 점유율을 모니터링해야 합니다. 만약 이 수치가 10~15%를 초과한다면 다음과 같은 조치를 고려하십시오. * **액터 인스턴스 제한**: 불필요하게 많은 액터가 생성되지 않도록 조절합니다. * **스레드 풀 최대치 제한**: 워크로드에 맞춰 스레드 풀의 최대 크기를 적절히 제한합니다. * **스레드 풀 통합**: 여러 개의 풀을 사용하기보다 풀의 개수를 줄여 작업 부하가 골고루 분산되도록 유도합니다. * **태스크 큐 활용**: 급격한 작업 급증(Spike)을 완충할 수 있는 큐를 도입하여 스레드 풀이 급격하게 변동하는 것을 방지합니다.