garbage-collection

7 개의 포스트

dropbox

불변 블롭 저장소 Magic Pocket의 저장 효율성 개선 (새 탭에서 열림)

Dropbox의 엑사바이트급 불변(Immutable) 블록 저장소인 'Magic Pocket'은 데이터 무결성과 효율적인 확장을 위해 설계된 핵심 인프라입니다. 최근 새로운 데이터 배치 서비스인 'Live Coder' 도입 과정에서 볼륨이 채 5%도 채워지지 않는 심각한 파편화 문제가 발생하여 스토리지 오버헤드가 급격히 증가하는 위기를 맞았습니다. 이를 해결하기 위해 Dropbox 엔지니어링 팀은 기존의 안정 상태 압축 방식을 넘어, 저밀도 볼륨을 집중적으로 재구성하는 다각적인 전략을 통해 스토리지 효율을 이전 수준보다 더욱 개선하는 데 성공했습니다. **불변 스토리지 구조와 공간 회수 메커니즘** * Magic Pocket은 데이터를 한 번 쓰면 수정하지 않는 불변 방식을 채택하고 있어, 사용자 데이터가 삭제되거나 수정되어도 기존 데이터는 즉시 지워지지 않고 볼륨 내에 그대로 남습니다. * 이러한 특성상 시간이 지남에 따라 실제 데이터보다 더 많은 디스크 공간을 점유하는 '파편화'가 발생하며, 이를 관리하지 않으면 스토리지 오버헤드가 기하급수적으로 늘어납니다. * 공간 회수는 가비지 컬렉션(참조되지 않는 데이터 식별)과 압축(Compaction)이라는 두 단계로 이루어지며, 압축은 유효한 데이터만 골라 새 볼륨에 옮겨 적고 기존 볼륨을 퇴거시키는 방식으로 수행됩니다. * 데이터 가용성을 위해 에러저 코딩(Erasure Coding)을 사용하여 복제 방식보다 적은 비용으로 결함 허용 능력을 유지합니다. **신규 서비스 도입으로 인한 저밀도 볼륨 문제** * 배경 쓰기 증폭을 줄이기 위해 도입된 'Live Coder' 서비스가 예상치 못하게 할당량의 5% 미만만 채워진 수많은 저밀도 볼륨을 생성하는 부작용을 일으켰습니다. * Magic Pocket의 볼륨은 고정된 크기를 가지기 때문에, 데이터가 적게 들어있어도 전체 용량을 점유하게 되어 실제 저장된 바이트 대비 물리적 디스크 소비량이 급등했습니다. * 기존의 압축 방식은 데이터 분포가 비교적 균일한 상태를 가정하고 설계되었기에, 엑사바이트 규모에서 발생한 대량의 저밀도 볼륨을 빠르게 정리하기에는 역부족이었습니다. **기존 L1 압축 전략의 동작과 한계** * 기존의 L1 전략은 거의 다 채워진 '호스트 볼륨'의 빈 공간에 파편화된 '공여 볼륨(Donor)'의 데이터를 채워 넣는 방식입니다. * 이 방식은 단순하고 메타데이터 업데이트 비용이 적지만, 한 번의 실행으로 회수되는 공간이 제한적이라는 단점이 있습니다. * 데이터 밀도가 극도로 낮은 볼륨이 대량으로 존재하는 상황에서는 L1 전략의 처리 속도가 파편화 속도를 따라잡지 못해 스토리지 비용을 실시간으로 절감하기 어려웠습니다. **실용적인 결론 및 권장 사항** 대규모 불변 스토리지 시스템을 운영할 때는 데이터의 배치 로직 변화가 스토리지 효율에 미치는 영향을 실시간으로 모니터링하는 것이 필수적입니다. 특히 데이터 밀도가 급격히 변하는 이례적인 상황에 대비하여, 단순히 빈 공간을 채우는 방식의 압축뿐만 아니라 저밀도 볼륨들을 한데 묶어 대량으로 정리할 수 있는 유연한 압축 전략을 병행 구축하는 것이 인프라 비용 최적화의 핵심입니다.

datadog

수백 개의 파드에서 Go 1.24 메모리 회귀를 추적해 찾아낸 방법 (새 탭에서 열림)

Go 1.24로의 업그레이드 이후, 새로운 맵 구현인 스위스 테이블(Swiss Tables)에 대한 기대와 달리 일부 서비스에서 메모리 사용량(RSS)이 약 20% 증가하는 현상이 발견되었습니다. 조사 결과, Go 런타임 내부의 메모리 관리 지표는 안정적이었으나 시스템 레벨의 실제 물리 메모리 점유가 늘어난 것으로 확인되었습니다. 이는 Go 1.24에서 진행된 `mallocgc` 함수의 리팩토링 과정에서 발생한 미묘한 메모리 할당자 회귀(Regression) 현상이 원인이었습니다. ### 런타임 지표와 시스템 지표의 불일치 * Go 1.24 업그레이드 후 데이터 처리 서비스의 RSS(Resident Set Size)가 눈에 띄게 증가했으나, Go 런타임 지표와 힙 프로파일상에는 아무런 변화가 기록되지 않았습니다. * 이는 Go 런타임 입장에서는 메모리를 더 사용하고 있지 않다고 판단하지만, 운영체제(Linux) 입장에서는 프로세스가 더 많은 물리 메모리를 점유하고 있는 상태임을 의미합니다. * Kubernetes의 메모리 제한(Limit)이나 OOM 킬러는 시스템 지표인 RSS를 기준으로 작동하기 때문에, 런타임 지표에 나타나지 않는 이러한 증가는 서비스 안정성에 치명적일 수 있습니다. ### 주요 변경 사항에 대한 가설 검증 * 먼저 Go 1.24의 핵심 변화인 '스위스 테이블'과 '스핀 비트 뮤텍스(Spin bit mutex)'를 원인으로 의심하고 실험을 진행했습니다. * `GOEXPERIMENT=noswissmap` 및 `GOEXPERIMENT=nospinbitmutex` 플래그를 사용하여 해당 기능들을 각각 비활성화한 후 빌드하여 배포했으나, 메모리 증가 현상은 해결되지 않았습니다. * 이를 통해 이번 문제는 새로운 기능 자체가 아니라, 런타임의 더 깊은 곳에서 발생한 변화 때문임을 확인했습니다. ### 가상 메모리와 물리 메모리의 매핑 분석 * 리눅스의 `/proc/[pid]/smaps` 파일을 분석하여 프로세스의 메모리 영역별 가상 메모리(Size)와 물리 메모리(RSS)의 차이를 추적했습니다. * 분석 결과, Go 1.23에서는 힙 영역의 RSS가 가상 메모리 크기보다 약 300 MiB 낮게 유지되었으나, Go 1.24에서는 가상 메모리 크기와 RSS가 거의 일치하는 현상이 발견되었습니다. * 결과적으로 Go 1.24의 런타임이 이전 버전보다 가상 메모리를 실제 물리 RAM에 더 공격적으로 할당(Commit)하고 있다는 사실을 밝혀냈습니다. ### mallocgc 리팩토링과 할당자 이슈 * Go 1.24 변경 로그를 정밀 분석한 결과, 메모리 할당의 핵심 로직인 `mallocgc` 함수에 대대적인 리팩토링이 있었음을 확인했습니다. * 이 과정에서 발생한 의도치 않은 로직 변화가 할당된 메모리를 실제 물리적 공간에 매핑하는 방식에 영향을 주어 RSS 상승을 유도한 것으로 파악되었습니다. * 작성자는 이 문제를 Go 개발 팀과 공유하여 원인을 확인했으며, 이는 런타임 리팩토링으로 인한 성능 회귀의 일종으로 결론지어졌습니다. Go 1.24 업그레이드를 고려 중인 팀은 런타임 내부 지표(Heap usage)뿐만 아니라 시스템 레벨의 RSS 지표를 면밀히 모니터링해야 합니다. 비록 메모리 할당자에서 미묘한 RSS 증가가 관측되었지만, 동시에 도입된 스위스 테이블은 대규모 인메모리 맵을 사용하는 서비스에서 수백 기가바이트의 메모리를 절약할 수 있는 잠재력을 가지고 있으므로 서비스 특성에 따른 비교 분석이 필요합니다.

datadog

Go 1.24의 스위스 테이블로 수백 기가바이트를 절약한 방법 (새 탭에서 열림)

Go 1.24에서 도입된 새로운 맵(map) 구현체인 '스위스 테이블(Swiss Tables)'은 대규모 인메모리 데이터를 다루는 서비스에서 획기적인 메모리 절감 효과를 제공합니다. Datadog의 실제 서비스 적용 사례에 따르면, 특정 고부하 환경에서 라이브 힙(Live Heap) 사용량이 500 MiB 감소했으며, 가비지 컬렉터(GOGC)의 영향을 고려할 때 전체 물리 메모리(RSS)는 약 1 GiB까지 절약되었습니다. 이는 Go 1.24의 다른 런타임 오버헤드를 상쇄하고도 남는 수준의 성능 향상을 보여줍니다. **실서비스에서의 메모리 절감 수치** * `ShardRouter` 패키지 내의 `shardRoutingCache`라는 대형 맵에서 약 500 MiB의 라이브 힙 사용량이 감소했습니다. * Go의 기본 GOGC 설정(100)을 기준으로 계산하면, 힙 사용량 감소는 실제 물리 메모리(RSS)에서 약 1 GiB(500 MiB x 2)의 절감으로 이어집니다. * Go 1.24의 다른 회귀 문제(mallocgc 이슈)로 인해 예상되는 400 MiB의 RSS 증가를 고려하더라도, 결과적으로 600 MiB의 순 메모리 감소가 확인되었습니다. **데이터 구조와 메모리 추정** * 해당 맵은 `string`을 키로, `Response` 구조체를 값으로 가집니다. * `Response` 구조체는 `ShardID`(int32), `ShardType`(int), `RoutingKey`(string header), `LastModified`(*time.Time)로 구성됩니다. * 64비트 아키텍처 기준으로 키-값 쌍 하나당 패딩을 포함해 약 56바이트를 차지하며, 서비스 시작 시 대량으로 생성된 후 런타임 중에는 거의 변경되지 않는 특성을 보입니다. **Go 1.23의 버킷 기반 맵 방식과 한계** * 기존 Go 1.23은 8개의 슬롯을 가진 '버킷' 배열로 해시 테이블을 관리했으며, 버킷 수는 항상 2의 거듭제곱으로 유지되었습니다. * 데이터 삽입 시 버킷 내부의 모든 요소를 순차적으로 스캔해야 하므로 CPU 오버헤드가 발생하며, 버킷이 가득 차면 '오버플로우 버킷'을 체이닝 방식으로 추가했습니다. * 평균 로드 팩터(Load Factor)가 13/16(약 81%)을 초과하면 버킷 배열의 크기를 2배로 늘리는 재할당이 발생하는데, 이 과정에서 점진적 복사(Evacuation) 방식을 사용하여 지연 시간을 관리했습니다. **결론 및 권장사항** 대규모 맵 데이터를 메모리에 유지하는 Go 애플리케이션은 Go 1.24로의 업그레이드만으로도 상당한 메모리 효율성 개선을 기대할 수 있습니다. 특히 읽기 중심의 거대 캐시 시스템이나 데이터 라우팅 테이블을 운영하는 경우, 스위스 테이블 기반의 최적화된 메모리 레이아웃이 비용 절감과 성능 향상에 큰 기여를 할 것입니다.

datadog

수백 개의 파드에서 (새 탭에서 열림)

제공된 내용에는 본문이 포함되어 있지 않고, Datadog 웹사이트의 메뉴 목록과 링크 정보만 있습니다. 따라서 글의 주장, 기술적 접근법, 결론을 정확히 요약할 수 없습니다. 확인되는 정보로는 글의 URL에 `go-memory-regression`이 포함되어 있어 Go 애플리케이션의 메모리 회귀 문제를 다룬 글일 가능성이 있다는 점뿐입니다. 본문 요약을 위해서는 다음 중 하나를 보내 주세요. - 블로그 본문 전체 - 본문이 포함된 HTML 또는 Markdown - 글의 실제 텍스트가 보이는 캡처나 링크 내용 현재 제공된 메뉴에는 인프라 모니터링, APM, 로그 관리, 보안, RUM, CI/CD, AI 등 Datadog 제품 내비게이션만 포함되어 있으며, 기술적인 설명이나 분석 내용은 없습니다.

datadog

.NET 연속 프로파일러: 메모리 사용량 (새 탭에서 열림)

Datadog의 .NET 프로파일러는 가비지 컬렉션(GC)의 효율성과 메모리 할당 패턴을 분석하여 애플리케이션의 성능 병목 현상을 진단합니다. 이 시스템은 모든 할당을 추적하는 대신 `AllocationTick` 이벤트를 활용한 샘플링 방식을 채택하여 운영 환경에서의 오버헤드를 최소화하면서도 정밀한 데이터를 제공합니다. 특히 .NET 7의 최신 API를 통해 객체의 생존 주기를 추적함으로써, CPU 부하의 원인이 되는 과도한 GC 작업과 잠재적인 메모리 누수 지점을 정확히 찾아내는 데 결론적인 도움을 줍니다. ### 가비지 컬렉터가 CPU에 미치는 영향 측정 * **전용 스레드 모니터링**: 서버 GC 설정 시 CLR이 생성하는 코어당 전용 스레드(.NET Server GC 및 .NET BGC)의 CPU 소비량을 운영체제로부터 직접 수집합니다. * **Pull 모델 채택**: GC 발생 시마다 이벤트를 받는 Push 방식과 달리, 프로파일러가 1분마다 주기적으로 GC 스레드의 CPU 사용 통계를 가져와 'Garbage Collector'라는 단일 프레임을 가진 네이티브 콜 스택 샘플로 기록합니다. * **버전별 차이**: .NET 5 이상에서는 GC 스레드 식별이 가능하여 정확한 측정이 가능하지만, 이전 버전에서는 스레드 ID 정보 부족으로 인해 이 기능을 완벽히 지원하기 어렵습니다. ### AllocationTick을 활용한 효율적인 할당 추적 * **샘플링 기반 추적**: 모든 객체 할당을 기록하는 `ObjectAllocated` 방식은 성능 저하가 극심하므로, 약 100KB의 할당이 누적될 때마다 발생하는 `AllocationTick` 이벤트를 사용하여 데이터를 수집합니다. * **상세 정보 수집**: 이벤트 페이로드에서 클래스 ID(ClassID), 타입 이름, 메모리 주소, 객체 크기뿐만 아니라 해당 객체가 할당된 힙의 종류(SOH, LOH, POH)까지 식별합니다. * **동기적 콜 스택 캡처**: 해당 이벤트는 할당을 수행한 스레드에서 동기적으로 발생하므로, 즉시 콜 스택을 워킹(Stack Walking)하여 어떤 비즈니스 로직이 메모리 압박을 유발하는지 특정할 수 있습니다. ### Weak Handle을 이용한 생존 객체 및 누수 탐지 * **객체 이동 대응**: GC의 컴팩션(Compaction) 단계에서 객체 주소가 변경되는 문제를 해결하기 위해, 샘플링된 객체에 대해 Weak 핸들을 생성하여 관리합니다. * **ICorProfilerInfo13 활용**: .NET 7에서 추가된 이 프로파일링 API를 통해, GC 이후에도 핸들이 가리키는 객체가 여전히 메모리에 살아있는지(`IsAllocated`)를 확인합니다. * **생명 주기 분석**: GC가 끝날 때마다 참조되지 않는 객체의 핸들은 제거하고, 생존한 객체들은 다음 프로필에 포함시켜 어떤 데이터가 메모리에 오래 머무르며 누수를 유발하는지 추적합니다. 운영 환경에서 메모리 문제를 분석할 때는 단순한 할당량 확인을 넘어, GC로 인한 CPU 점유율과 객체의 생존 기간을 함께 살펴야 합니다. 특히 .NET 7 이상의 최신 런타임을 활용하면 프로파일러의 Weak 핸들 추적 기능을 통해 메모리 누수 탐지의 정확도를 대폭 높일 수 있습니다.

datadog

.NET Continuous Profiler: 내부 동작 원리 (새 탭에서 열림)

Datadog의 .NET 프로파일러는 운영 환경에서 24시간 내내 실행될 수 있도록 설계된 상시(continuous) 프로파일링 도구로, 애플리케이션 성능에 미치는 영향을 최소화하면서 CPU, 메모리, 잠금 경합 등 다양한 런타임 지표를 수집합니다. 이를 통해 개발자는 별도의 재현 환경을 구축하지 않고도 실제 운영 상황의 성능 병목 현상을 정밀하게 분석할 수 있으며, 수집된 데이터는 효율적인 집계와 가독성 높은 호출 스택 변환 과정을 거쳐 백엔드로 전달됩니다. ## .NET 프로파일러의 구조와 데이터 수집 * 개별 리소스(CPU, 예외, 잠금 경합 등)를 담당하는 여러 독립적인 프로파일러와 샘플러, 데이터를 노출하는 프로바이더로 구성된 모듈형 아키텍처를 가집니다. * 수집된 샘플은 호출 스택(Call Stack), 컨텍스트 정보(Labels), 수치 데이터 벡터를 포함하며, 동일한 스택과 레이블을 가진 샘플은 하나로 병합하여 데이터 크기를 최적화합니다. * 샘플 집계 및 `.pprof` 형식으로의 직렬화 로직은 성능 향상과 타 언어 프로파일러와의 공유를 위해 Rust 언어로 구현되었습니다. ## 분산 추적 연동 및 백엔드 처리 * 'Runtime ID'를 사용하여 하나의 프로세스 내에서 여러 서비스(예: IIS AppDomain)가 실행되더라도 각 서비스를 정확히 식별하고 분산 추적(Traces/Spans) 데이터와 프로파일을 정교하게 연결합니다. * 트레이서(Tracer)가 런타임 ID와 서비스 이름(DD_SERVICE)의 매핑 정보를 프로파일러에 전달함으로써, 백엔드에서 특정 서비스에 해당하는 프로파일을 정확히 찾아낼 수 있게 합니다. ## 호출 스택(Call Stack) 가독성 개선 * 닷넷 런타임 API가 제공하는 로우(raw) 데이터를 개발자가 소스 코드 수준에서 즉시 이해할 수 있도록 정제(Clean-up)하는 과정을 거칩니다. * 생성자(`.ctor`)는 실제 클래스 이름으로, 익명 메서드나 람다식은 원래 메서드명에 `_AnonymousMethod`나 `_Lambda` 접미사를 붙여 변환합니다. * 로컬 메서드나 컴파일러가 생성한 비동기 상태 머신의 `MoveNext` 메서드 등 복잡한 이름들도 소스 코드 구조를 반영하도록 가공하여 분석의 혼선을 줄입니다. ## 네이티브 구현을 통한 성능 최적화 * 프로파일러 자체의 메모리 할당이 대상 애플리케이션의 가비지 컬렉션(GC)에 압박을 주지 않도록 핵심 로직을 C#(Managed code)이 아닌 네이티브 수준에서 구현하는 방향을 선택했습니다. * 이러한 설계는 운영 환경에서 성능 저하를 무시할 수 있는 수준으로 유지하면서도 상세한 런타임 성능 데이터를 지속적으로 수집할 수 있는 기반이 됩니다. 운영 환경의 부하를 최소화하면서 실제 트래픽 상황의 성능 문제를 정확히 진단하고 싶다면, 네이티브 수준에서 최적화되고 소스 코드 가독성까지 고려된 상시 프로파일링 도구를 도입하는 것이 가장 효과적인 전략입니다.

figma

Mozilla의 Rust가 어떻게 (새 탭에서 열림)

Figma는 TypeScript 기반 멀티플레이어 서버의 예측 불가능한 지연 문제를 해결하기 위해 성능에 민감한 부분을 Rust로 재작성했다. Rust 프로세스를 문서별로 분리하고 병렬 처리한 결과, 메모리 사용량을 크게 줄이고 직렬화 성능을 10배 이상 개선했다. 다만 Rust 생태계와 언어 자체의 성숙도가 낮아 전체 서버를 Rust로 전환하지 않고 핵심 성능 영역에만 적용했다. ## TypeScript 서버의 확장성 문제 - Figma의 멀티플레이어 서버는 고정된 수의 머신과 워커로 구성되며, 각 문서는 하나의 워커에 전적으로 할당됐다. - 기존 서버는 TypeScript와 Node.js 기반의 단일 스레드 구조였다. - 문서 인코딩처럼 오래 걸릴 수 있는 작업이 실행되면 해당 워커 전체가 차단됐다. - 일부 대형 문서만 문제를 일으켰지만, 같은 워커에 연결된 다른 모든 문서의 동기화도 지연됐다. - 하드웨어를 추가해도 단일 워커 내부의 작업 차단 문제는 해결되지 않았다. - 문서마다 별도의 Node.js 프로세스를 실행하는 방식은 JavaScript VM의 메모리 오버헤드가 커서 현실적이지 않았다. ## 임시 대응과 한계 - Figma는 문제가 큰 문서를 별도의 “heavy worker” 풀로 수동 이동했다. - 이 방식으로 서비스 장애를 방지할 수는 있었지만, 문제가 되는 문서를 계속 찾아서 재배치해야 했다. - 문서 특성에 따라 운영자가 직접 관리해야 하므로 확장성과 자동화 측면에서 한계가 있었다. ## Rust 기반 아키텍처 - 성능에 민감한 멀티플레이어 서버 일부를 별도의 자식 프로세스로 분리했다. - 자식 프로세스는 Rust로 작성했으며, 기존 호스트 프로세스와 표준 입력(stdin)·표준 출력(stdout)으로 통신했다. - Rust 프로세스는 기존 시스템보다 메모리를 훨씬 적게 사용했다. - 그 결과 문서별로 별도의 프로세스를 실행해 모든 문서를 완전히 병렬화할 수 있었다. - 특정 대형 문서의 느린 작업이 다른 문서의 동기화를 차단하지 않게 됐다. - 문서 직렬화 시간은 기존보다 10배 이상 빨라졌다. ## 운영 성능 개선 - 점진적 배포를 통해 Rust 기반 시스템을 단계적으로 확대했다. - 전체 배포가 완료된 시점에 서버 성능 지표가 크게 개선됐다. - 개선 효과는 주로 서버 내부 성능에 해당하며, 클라이언트 기능 자체가 빨라졌다기보다 서버가 부하 상황에서도 안정적으로 동작하도록 만든 것이다. - 결과적으로 대형 문서나 느린 작업 때문에 전체 사용자가 겪던 동기화 지연과 서비스 품질 저하가 완화됐다. ## Rust를 선택한 이유 - Rust는 C++에 가까운 성능과 낮은 수준의 메모리 제어 능력을 제공한다. - 가비지 컬렉터가 없어 메모리 사용량과 지연을 더 세밀하게 제어할 수 있다. - 타입 시스템이 C++에서 흔한 여러 종류의 버그를 자동으로 방지한다. - 저수준 성능과 서버 언어 수준의 안전성을 함께 확보할 수 있다는 점이 선택의 핵심이었다. - 특히 기존 서버의 성능 문제 중 일부가 가비지 컬렉터와 관련되어 있었기 때문에 낮은 메모리 사용량이 중요했다. ## Rust 도입의 장점과 한계 - 장점 - 메모리 사용량이 매우 낮았다. - 가비지 컬렉터 없이 메모리 레이아웃을 세밀하게 제어할 수 있었다. - 문서별 Rust 프로세스 실행이 가능할 정도로 프로세스 비용이 작았다. - 높은 성능과 병렬 처리 구조를 구현할 수 있었다. - 한계 - 당시 Rust는 일반적인 서버 언어보다 훨씬 새로운 언어였다. - 언어와 생태계가 충분히 성숙하지 않아 여러 거친 부분이 있었다. - 처음 계획했던 전체 서버의 Rust 재작성은 포기했다. - 대신 성능에 가장 큰 영향을 주는 부분에만 Rust를 제한적으로 적용했다. Figma의 사례는 새로운 언어를 전면 도입하기보다, 병목이 명확한 핵심 컴포넌트만 별도 프로세스로 분리해 적용하는 전략이 현실적임을 보여준다. 특히 메모리 사용량과 지연 시간이 중요한 서비스에서는 Rust가 효과적이지만, 언어의 성숙도와 팀의 운영 역량을 고려해 점진적으로 도입하는 것이 바람직하다.