시간 경과에 따른 데이터

이 문서는 데이터베이스 확장성 작업 그룹에서 소개된 시간 경과 패턴에 대해 설명합니다.

시간 경과 데이터의 특성을 논의하고, GitLab 개발이 이 맥락에서 고려해야 할 모범 사례를 제안합니다.

일부 데이터셋은 강한 시간 경과 효과를 받아서 최근 데이터가 오래된 데이터보다 훨씬 더 자주 접근됩니다. 시간 경과의 또 다른 측면은 시간이 지남에 따라 일부 유형의 데이터가 덜 중요해진다는 것입니다. 이는 극단적인 경우에는 오래된 데이터를 덜 내구성 있는(가용성이 낮은) 저장소로 이동하거나 삭제할 수 있음을 의미합니다.

이러한 효과는 일반적으로 제품이나 애플리케이션의 의미와 연결되어 있습니다. 오래된 데이터에 접근하는 정도와 그 데이터가 사용자나 애플리케이션에 얼마나 유용하거나 필요한지에 따라 달라질 수 있습니다.

우선 데이터에 대한 고유한 시간적 편향이 없는 엔터티를 살펴보겠습니다.

사용자 또는 프로젝트에 대한 기록은 생성 시점과 상관없이 동일하게 중요하고 자주 접근될 수 있습니다. 사용자의 idcreated_at을 사용하여 관련 기록이 얼마나 자주 접근되거나 업데이트되는지 예측할 수 없습니다.

반면, 극단적인 시간 경과 효과를 가진 데이터셋의 좋은 예는 사용자 행동을 기록하는 이벤트와 같은 로그 및 시계열 데이터입니다.

대부분의 경우, 이러한 유형의 데이터는 며칠 또는 몇 주 후에는 비즈니스 용도가 없을 수 있으며, 데이터 분석 관점에서도 빠르게 덜 중요해집니다. 이러한 데이터는 현재 애플리케이션 상태와 점점 더 관련성이 떨어지는 스냅샷을 나타내며, 결국에는 실제로 가치가 없어집니다.

두 극단의 중간에는 우리가 보존하고 싶지만 생성 후 초기(소규모) 시간 기간이 지나면 잘 접근되지 않는 유용한 정보를 갖춘 데이터셋이 존재합니다.

시간 경과 데이터의 특성

우리는 다음과 같은 특성을 보여주는 데이터셋에 관심이 있습니다:

  • 데이터셋의 크기: 상당히 큰 데이터셋입니다.
  • 접근 방법: 시간 관련 차원 또는 시간 경과 효과가 있는 범주적 차원에 따라 데이터셋에 접근하는 대부분의 쿼리를 필터링할 수 있습니다.
  • 불변성: 시간 경과 상태는 변경되지 않습니다.
  • 보존: 오래된 데이터를 보존할지 여부, 또는 오래된 데이터가 애플리케이션을 통해 사용자에게 접근 가능해야 하는지 여부입니다.

데이터셋의 크기

강한 시간 경과 효과를 보이는 가변 크기의 데이터셋이 있을 수 있지만, 이 청사진의 맥락에서 우리는 상당히 큰 데이터셋을 가진 엔터티에 초점을 맞추고자 합니다.

작은 데이터셋은 데이터베이스 관련 리소스 사용에 크게 기여하지 않으며, 쿼리에 상당한 성능 패널티를 주지도 않습니다.

대조적으로, 약 5천만 개의 기록 또는 100GB 이상의 대규모 데이터셋은 매우 작은 데이터 부분에 지속적으로 접근할 때 상당한 오버헤드를 추가합니다. 이러한 경우, 우리는 시간 경과 효과를 우리의 장점으로 활용하고 적극적으로 접근되는 데이터셋을 줄이길 원합니다.

데이터 접근 방법

시간 경과 데이터의 두 번째이자 가장 중요한 특성은 대부분의 경우, 우리는 날짜 필터를 사용하여 데이터를 암묵적으로 또는 명시적으로 접근할 수 있다는 것입니다.

시간 관련 차원에 따라 결과를 제한하는 데이터 접근 방법이 있습니다.

그러한 차원은 여러 가지가 있을 수 있지만, 우리는 생성 날짜에만 집중합니다. 이는 가장 일반적으로 사용되며, 우리가 제어하고 최적화할 수 있는 것입니다.

  • 불변성입니다.
  • 기록이 생성될 때 설정됩니다.
  • 기록을 물리적으로 클러스터링하고 이동할 필요 없이 연결될 수 있습니다.

시간 경과 데이터가 기본적으로 애플리케이션에서 그렇게 접근되지 않더라도, 대부분의 쿼리를 명시적으로 필터링하여 데이터를 접근할 수 있도록 만들 수 있습니다. 이러한 시간 경과 관련 접근 방법 없이 시간 경과 데이터를 최적화 관점에서 사용할 수 없으며, 설정 및 스케일링 패턴을 따를 방법이 없습니다.

시간 경과 관련 접근 방법을 사용하여 항상 접근되는 데이터에 대한 정의를 제한하지 않으며, 일부 예외적인 작업이 있을 수 있습니다. 이러한 작업은 필요할 수 있으며, 다른 접근 방법이 스케일할 수 있는 경우에는 스케일하지 않더라도 이를 수용할 수 있습니다. 예를 들어: 관리자가 특정 유형의 모든 과거 이벤트에 접근하는 경우, 다른 모든 작업은 최대 6개월 전의 최대 1개월의 이벤트에만 접근합니다.

변하지 않는 특성

시간의 경과에 따른 데이터의 세 번째 특성은 시간 경과 상태가 변하지 않는다는 것입니다.

“오래된” 것으로 간주되면 다시 “새로운” 또는 관련으로 전환될 수 없습니다.

이 정의는 사소하게 들릴 수 있지만, “오래된” 데이터에 대한 작업을 비쌀 수 있도록(예: 아카이빙 또는 덜 비싼 저장소로 이동) 해야 하며, 관련성이 다시 회복되고 중요한 애플리케이션 작업이 저성능이 되는 것에 대해 걱정할 필요가 없습니다.

시간 경과 데이터 접근 패턴의 반례로, 업데이트된 시점에 따라 문제를 제시하는 애플리케이션 보기를 고려해 보겠습니다.

우리는 또한 “업데이트” 관점에서 가장 최근의 데이터에 관심이 있지만, 그 정의는 변동성이 있고 실행 가능하지 않습니다.

보존

마지막으로, 약간 다른 접근 방식을 가진 하위 범주에서 시간 경과 데이터를 더욱 차별화하는 특성은 우리가 오래된 데이터를 유지할 것인지 여부(예: 보존 정책)와/또는 오래된 데이터가 사용자가 애플리케이션을 통해 접근할 수 있는지 여부입니다.

(선택 사항) 시간 경과 데이터의 확장된 정의

부가적으로, 앞서 언급한 정의를 클러스터링 속성을 기반으로 한 데이터의 잘 정의된subset에 대한 접근 패턴으로 확장하면, 우리는 다양한 유형의 데이터에 대해 시간 경과 스케일링 패턴을 사용할 수 있습니다.

예를 들어, 완료되지 않은 To-Dos와 같이 활성으로 레이블이 지정된 경우에만 접근되는 데이터, 병합되지 않은 병합 요청에 대한 파이프라인(또는 시간이 기준이 아닌 제약) 등을 고려해 보십시오.

이 경우, 우리는 붕괴를 정의하기 위해 시간 차원이 아닌 범주적 차원(즉, 유한한 값 집합을 사용하는 차원)을 사용하여 관심 있는 subset을 정의합니다.

해당 subset이 데이터 세트의 전체 크기와 비교하여 작기만 하면 우리는 같은 접근 방식을 사용할 수 있습니다.

유사하게, 우리는 CI 파이프라인과 같이 시간이 지남에 따라 실패한 데이터에 대해 시간 차원과 추가 상태 속성을 기반으로 오래된 데이터를 정의할 수 있습니다.

시간 경과 데이터 전략

파티션 테이블

이는 순수한 데이터베이스 관점에서 시간 경과 데이터를 처리하기 위한 수용 가능한 모범 사례입니다.

PostgreSQL의 테이블 파티셔닝에 대한 자세한 정보는 테이블 파티셔닝에 대한 문서 페이지를 참조하십시오.

날짜 간격(예: 월, 연도)으로 파티셔닝하면 각 날짜 간격에 대해 훨씬 더 작은 테이블(파티션)을 만들 수 있으며 애플리케이션 관련 작업을 위해 최신 파티션만 접근할 수 있습니다.

우리는 관심 있는 날짜 간격에 따라 파티셔닝 키를 설정해야 하며, 이는 두 가지 요소에 따라 달라질 수 있습니다:

  1. 우리가 데이터를 얼마나 뒤에서 접근해야 하는가? 매주 파티셔닝하는 것은 항상 1년 전의 데이터를 접근할 경우 소용이 없습니다. 매번 52개의 다른 파티션(테이블)에서 쿼리를 실행해야 하며, 이는 GitLab 사용자 프로필의 활동 피드를 고려할 때와 같습니다.

    반대로, 우리가 최근 7일 동안 생성된 레코드만 접근하려는 경우, 연도별 파티셔닝은 각 파티션에 너무 많은 불필요한 레코드를 포함하게 됩니다. 이는 web_hook_logs의 경우와 같습니다.

  2. 생성된 파티션의 크기는 얼마나 큰가? 파티셔닝의 주요 목적은 가능한 한 작은 테이블에 접근하는 것입니다.

    만약 파티션 자체가 너무 커지면 쿼리는 저성능이 됩니다. 우리는 파티션을 더 작은 파티션으로 다시 파티셔닝해야 할 수도 있습니다.

완벽한 파티셔닝 계획은 데이터 세트에 대한 모든 쿼리가 거의 항상 단일 파티션에 대해 이루어지며 드물게 두 개의 파티션이나 여러 파티션에서 이루어지는 경우가 허용되는 균형을 유지합니다.

우리는 또한 가능한 한 작은 파티션을 목표로 해야 하며, 각 파티션은 5-10M 레코드 및/또는 최대 10GB 이하여야 합니다.

파티셔닝은 다른 전략과 결합하여 오래된 파티션을 제거하거나, 데이터베이스 내에서 저렴한 저장소로 이동하거나, 데이터베이스 외부(아카이브 또는 다른 유형의 저장 엔진 사용)로 이동할 수 있습니다.

우리가 오래된 레코드를 유지하고 싶지 않고 파티셔닝을 사용하는 한, 오래된 데이터를 제거하는 비용은 일상적으로 제로로 간주됩니다.

단지 배경 작업자가 모든 데이터가 보존 정책 기간을 벗어날 때마다 오래된 파티션을 제거하면 됩니다.

예를 들어, 우리가 6개월 이상 된 레코드만 유지하고 싶고 월별로 파티셔닝하면, 우리는 항상 최신 7개의 파티션(현재 월 및 6개월 전)을 안전하게 유지할 수 있습니다.

즉, 매달 시작할 때 8번째로 오래된 파티션을 삭제하는 작업자를 둘 수 있습니다.

같은 데이터베이스 내에서 파티션을 저렴한 저장소로 이동하는 것은 PostgreSQL에서 테이블 스페이스를 사용하여 비교적 간단합니다.

각 파티션에 대해 별도로 테이블 스페이스 및 저장소 매개변수를 지정할 수 있으며, 이 경우 접근 방식은 다음과 같습니다:

  1. 저렴하고 느린 디스크에 새 테이블 스페이스를 생성합니다.

  2. PostgreSQL 최적화기가 디스크가 느리다는 것을 알 수 있도록 새 테이블 스페이스의 저장소 매개변수를 높게 설정합니다.

  3. 배경 작업자를 사용하여 오래된 파티션을 느린 테이블 스페이스로 자동으로 이동합니다.

마지막으로, 데이터베이스 외부로 파티션을 이동하는 것은 데이터베이스 아카이빙 또는 다른 저장 엔진으로 파티션을 수동으로 내보내는 것을 통해 수행할 수 있습니다(자세한 내용은 전용 하위 섹션에서).

오래된 데이터 정리

우리가 오래된 데이터를 어떤 형태로든 유지하고 싶지 않다면, 정리 전략을 구현하여 오래된 데이터를 삭제할 수 있습니다.

이는 과거 데이터를 삭제하기 위해 정리 작업자를 사용하는 간단한 구현 전략입니다. 아래에서 추가적으로 분석하는 예시로, 90일보다 오래된 web_hook_logs를 정리하고 있습니다.

이러한 솔루션의 단점은 대규모 비분할 테이블에서 수동으로 모든 관련 없는 레코드에 접근하고 삭제해야 하기 때문에 비용이 많이드는 작업이 됩니다. PostgreSQL에서의 멀티버전 동시성 제어로 인해 이는 매우 비싼 작업입니다. 또한, 이로 인해 정리 작업자가 새로 생성되는 레코드를 따라잡지 못하게 되는데, 이 속도가 기준치를 초과하는 경우에 발생하며, 문서를 작성하는 시점의 web_hook_logs에서도 발생하고 있습니다.

위에서 언급한 이유로, 우리의 제안은 데이터 보존 전략의 구현은 반드시 파티셔닝을 기반으로 해야 한다는 것입니다. 강력한 이유가 없는 한 그렇습니다.

데이터베이스 외부로 오래된 데이터 이동

대부분의 경우, 우리는 오래된 데이터를 가치 있다고 여기므로 이를 정리하고 싶지 않습니다. 동시에 이 데이터가 데이터베이스 관련 작업(예: 직접 접근 또는 조인 및 기타 쿼리 타입에서 사용됨)에 필요하지 않다면, 데이터를 데이터베이스 외부로 이동할 수 있습니다.

이것은 사용자가 애플리케이션을 통해 직접 접근할 수 없다는 것을 의미하지는 않으며, 데이터베이스 외부로 데이터를 이동하고 메타데이터 오프로드와 유사하게 다른 스토리지 엔진이나 접근 유형을 사용할 수 있습니다.

가장 간단한 사용 사례에서 우리는 최근 데이터에 빠르고 직접 접근할 수 있도록 제공할 수 있으며, 사용자가 오래된 데이터를 아카이브로 다운로드할 수 있도록 허용합니다. 이는 audit_events 사용 사례에서 평가된 옵션입니다.

국가와 산업에 따라 감사 이벤트는 매우 긴 보존 기간을 가질 수 있으며, GitLab 인터페이스를 통해서는 지난 몇 개월의 데이터만 적극적으로 접근됩니다.

추가적인 사용 사례는 데이터를 데이터 웨어하우스나 다른 데이터 저장소로 내보내는 것일 수 있으며, 이러한 저장소가 이 유형의 데이터 처리에 더 적합할 수 있습니다. 예를 들어, 우리가 때때로 테이블에 저장하는 JSON 로그는 BigQuery나 Redshift와 같은 열 저장소에 로드하는 것이 분석/쿼리하는 데 더 좋을 수 있습니다.

데이터를 데이터베이스 외부로 이동하기 위해 고려할 수 있는 몇 가지 전략은 다음과 같습니다:

  1. 이러한 유형의 데이터를 로그로 스트리밍한 다음, 이를 보조 저장소 옵션으로 이동하거나 다른 유형의 데이터 저장소에 직접 로드하기 (CSV/JSON 데이터로).
  2. 데이터를 CSV로 내보내고, 객체 저장소에 업로드한 후, 데이터베이스에서 이 데이터를 삭제하고, CSV를 다른 데이터 저장소로 로드하는 ETL 프로세스 생성.
  3. 데이터 저장소에서 제공하는 API를 사용하여 백그라운드에서 데이터를 로드하기.

대용량 데이터 세트에는 이 방법이 적합하지 않을 수 있습니다. 파일을 사용한 일괄 업로드 옵션이 존재하는 한, API 호출보다 더 나은 성과를 낼 것입니다.

사용 사례

웹 후크 로그

관련 에픽: 파티셔닝: web_hook_logs 테이블

web_hook_logs의 중요한 특성은 다음과 같습니다:

  1. 데이터 세트 크기: 정말 큰 테이블입니다. 우리가 이 테이블을 파티셔닝하기로 결정한 시점(2021-03-01)에는 대략 5억 2700만 개의 레코드와 총 크기가 약 1TB에 달했습니다.

    • 테이블: web_hook_logs
    • 행 수: 약 5억 2700만
    • 총 크기: 1.02 TiB (10.46%)
    • 테이블 크기: 713.02 GiB (13.37%)
    • 인덱스 크기: 42.26 GiB (1.10%)
    • TOAST 크기: 279.01 GiB (38.56%)
  2. 접근 방법: 우리는 항상 최대 7일 이전의 로그를 요청합니다.

  3. 불변성: created_at을 기준으로 파티셔닝할 수 있습니다. 이 속성은 변하지 않습니다.

  4. 보존: 90일 보존 정책이 설정되어 있습니다.

추가적으로, 우리는 그 시점에 백그라운드 작업자(PruneWebHookLogsWorker)를 사용하여 데이터를 정리하려고 했으나, 삽입 속도를 따라잡지 못했습니다.

결과적으로, 2021년 3월에는 2020년 7월 이후 삭제되지 않은 레코드가 여전히 존재하며, 테이블 크기는 매일 200만 개 이상의 레코드가 증가하고 있었습니다. 이는 어느 정도 안정된 크기를 유지해야 할 필요가 있었습니다.

마지막으로, 2021년 3월 기준으로 삽입 속도가 매달 170GB 이상의 데이터로 증가하고 있으며 계속 증가하고 있으므로, 오래된 데이터를 정리하는 유일한 실행 가능한 솔루션은 파티셔닝을 통한 것이었습니다.

우리의 접근 방식은 해당 테이블을 월별로 파티셔닝하는 것이었으며, 이는 90일 보존 정책과 일치합니다.

필요한 프로세스는 다음과 같습니다:

  1. 파티셔닝 키 결정

    이 경우 created_at 열을 사용하는 것은 간단합니다. 이는 보존 정책이 존재하고 상충되는 접근 패턴이 없을 때 자연스러운 파티셔닝 키입니다.

  2. 파티셔닝 키를 결정한 후, 파티션을 생성하고 기존 테이블에서 데이터를 복사하여 백필을 수행합니다. 우리는 기존 테이블을 단순히 파티셔닝할 수 없으며, 새 파티셔닝된 테이블을 생성해야 합니다.

    따라서, 파티셔닝된 테이블과 모든 관련 파티션을 생성하고, 모든 데이터를 복사한 다음, 새로운 데이터 또는 기존 데이터의 업데이트/삭제를 새 파티셔닝된 테이블에 반영할 수 있도록 동기화 트리거를 추가해야 합니다.

    테이블 파티셔닝을 시작하는 데 필요한 모든 세부정보가 포함된 MR

    이 과정은 15일과 7.6시간이 걸렸습니다.

  3. 초기 파티셔닝이 시작된 후의 일정, 백필을 수행하는 데 사용되는 백그라운드 마이그레이션을 정리하고 나머지 작업을 완료하며, 실패한 작업을 재시도합니다.

    모든 세부정보가 포함된 MR

  4. 파티셔닝된 테이블에 남은 외래 키 및 보조 인덱스를 추가합니다. 이는 다음 이정표에서 기존 비파티셔닝 테이블과 동일한 스키마를 제공하기 위한 것입니다.

    우리는 처음에 외래 키와 인덱스를 추가하지 않으며, 이는 각 삽입에 오버헤드를 추가하고, 이 테이블(5억 개 이상의 레코드)에서 초기 백필을 느리게 할 수 있습니다. 따라서 우리는 가벼운, 바닐라 테이블 버전을 생성하고 모든 데이터를 복사한 후, 나머지 인덱스와 외래 키를 추가합니다.

  5. 기본 테이블을 파티셔닝된 복사본과 교환: 이 시점부터 파티셔닝된 테이블이 애플리케이션에서 적극적으로 사용됩니다.

    원래 테이블을 삭제하는 것은 파괴적인 작업이며, 우리는 과정 중 어떤 문제가 발생하지 않도록 확인하고 싶으므로, 기존 비파티셔닝 테이블을 유지합니다. 또한 동기화 트리거를 반대로 전환하여 비파티셔닝 테이블이 여전히 파티셔닝된 테이블에서 발생하는 모든 작업과 최신 상태를 유지하도록 합니다. 이는 필요시 테이블을 다시 교환할 수 있게 해줍니다.

    모든 세부정보가 포함된 MR

  6. 마지막 단계인 스와핑 후 한 이정표: 비파티셔닝 테이블 삭제

    모든 세부정보가 포함된 Issue

  7. 비파티셔닝 테이블이 삭제된 후, 과거 파티션을 삭제함으로써 정리 전략을 구현하기 위해 작업자를 추가할 수 있습니다.

    이 경우, 작업자는 항상 4개의 파티션만 활성화되도록 보장합니다(보존 정책이 90일이므로). 4개월보다 오래된 파티션은 삭제되며, 현재 월이 아직 활성화된 상태이기 때문에 4개월의 파티션은 유지해야 합니다.

감사 이벤트

관련 에픽: 감사 이벤트에 대한 파티셔닝 전략 설계 및 구현

audit_events 테이블은 이전 서브 섹션에서 논의한 web_hook_logs 테이블과 많은 특성을 공유하므로, 우리는 그들이 다른 점에 집중합니다.

합의는 파티셔닝이 대부분의 성능 문제를 해결할 수 있을 것이라는 것이었습니다.

대부분의 다른 대규모 테이블과는 달리, 주요 충돌하는 접근 패턴이 없습니다: 우리는 접근 패턴을 월별 파티셔닝과 일치시키기 위해 전환할 수 있었습니다. 예를 들어, 다른 테이블에서는 파티셔닝 접근 방식이 정당화될 수 있지만 (예: 네임스페이스별로) 많은 충돌하는 접근 패턴이 있습니다.

또한, audit_events는 매우 적은 읽기(쿼리)와 함께 쓰기 중심의 테이블이며, 매우 간단한 스키마를 가지고 있으며, 데이터베이스의 나머지 부분과 연결되어 있지 않습니다 (수신 또는 송신 FK 제약 조건 없음) 및 두 개의 인덱스만 정의되어 있습니다.

후자는 중요했는데, 외래 키 제약 조건이 없기 때문에 우리가 PostgreSQL 11에 있을 때 파티셔닝을 할 수 있었기 때문입니다. 이제 PostgreSQL 12로 이동했기 때문에 더 이상 우려 사항이 아닙니다. 이는 위의 web_hook_logs 사용 사례에서 볼 수 있습니다.

audit_events의 파티셔닝을 위한 이주 및 단계는 web_hook_logs에 대해 이전 서브 섹션에서 설명된 것과 유사합니다. 현재 audit_events에 대한 보존 전략이 정의되어 있지 않으므로 그에 대한 가지치기 전략이 구현되어 있지 않습니다. 그러나 우리는 미래에 아카이빙 솔루션을 구현할 수 있습니다.

audit_events의 경우 흥미로운 점은 최적의 쿼리를 장려하기 위해 필요한 UI/UX 변경을 구현하기 위해 우리가 따라야 했던 필수 단계에 대한 논의입니다. 파티셔닝된 쿼리를 최적화하기 위해 필요한 변경 사항을 애플리케이션 수준에서 정렬하는 데 시작점으로 사용할 수 있습니다.

CI 테이블

참고: CI 테이블 사용 사례에 대한 요구 사항 및 분석: 아직 진행 중입니다. 분석이 진행됨에 따라 더 많은 세부 정보를 추가할 계획입니다.