InfoGrab DocsInfoGrab Docs

시간 감쇠 데이터

요약

이 문서는 데이터베이스 확장성 워킹 그룹에서 도입된 *시간 감쇠 패턴(time-decay pattern)*을 설명합니다. 일부 데이터셋은 강한 시간 감쇠 효과의 영향을 받습니다. 이러한 효과는 일반적으로 제품 또는 애플리케이션의 의미론(semantics)과 연관됩니다.

이 문서는 데이터베이스 확장성 워킹 그룹에서 도입된 *시간 감쇠 패턴(time-decay pattern)*을 설명합니다. 시간 감쇠 데이터의 특성을 살펴보고, 이 맥락에서 GitLab 개발 시 고려해야 할 모범 사례를 제안합니다.

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

이러한 효과는 일반적으로 제품 또는 애플리케이션의 의미론(semantics)과 연관됩니다. 오래된 데이터에 얼마나 자주 접근하는지, 그리고 오래된 데이터가 사용자나 애플리케이션에 얼마나 유용하거나 필요한지에 따라 정도가 달라질 수 있습니다.

먼저 데이터에 있어서 시간과 관련된 고유한 편향이 없는 엔티티를 생각해 봅시다.

사용자나 프로젝트의 레코드는 생성 시점과 무관하게 동일하게 중요하고 자주 접근될 수 있습니다. 사용자의 idcreated_at을 사용하는 것만으로는 관련 레코드가 얼마나 자주 접근되거나 업데이트되는지 예측할 수 없습니다.

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

이러한 유형의 데이터는 대부분 며칠 또는 몇 주 후에 비즈니스적 용도가 없어지며, 데이터 분석 관점에서도 빠르게 덜 중요해집니다. 어느 시점에 실질적인 가치가 없어질 때까지 애플리케이션의 현재 상태와 점점 더 관련이 없어지는 스냅샷을 나타냅니다.

두 극단의 중간에는, 보존하고 싶은 유용한 정보를 가진 데이터셋이 있지만, 생성 후 초기(짧은) 기간이 지나면 오래된 레코드에 거의 접근하지 않게 됩니다.

시간 감쇠 데이터의 특성#

우리가 관심을 갖는 데이터셋은 다음과 같은 특성을 보입니다:

  • 데이터셋의 크기: 상당히 큰 규모입니다.

  • 접근 방법: 데이터셋에 접근하는 대다수의 쿼리를 시간 관련 차원 또는 시간 감쇠 효과를 가진 범주형 차원으로 필터링할 수 있습니다.

  • 불변성: 시간 감쇠 상태가 변하지 않습니다.

  • 보존: 오래된 데이터를 유지할 것인지 여부, 또는 오래된 데이터가 애플리케이션을 통해 사용자에게 계속 접근 가능해야 하는지 여부입니다.

데이터셋의 크기#

강한 시간 감쇠 효과를 보이는 다양한 크기의 데이터셋이 있을 수 있지만, 이 블루프린트의 맥락에서는 상당히 큰 데이터셋을 가진 엔티티에 집중하려 합니다.

소규모 데이터셋은 데이터베이스 관련 리소스 사용에 크게 기여하지 않으며, 쿼리에 상당한 성능 저하를 일으키지도 않습니다.

반면, 약 5천만 건 이상의 레코드 또는 100 GB 이상의 크기를 가진 대규모 데이터셋은 정말 작은 데이터 부분집합에 지속적으로 접근하는 데 상당한 오버헤드를 추가합니다. 이러한 경우, 시간 감쇠 효과를 활용하여 활발하게 접근되는 데이터셋을 줄이고자 합니다.

데이터 접근 방법#

시간 감쇠 데이터의 두 번째이자 가장 중요한 특성은, 대부분의 경우 날짜 필터를 사용하여 데이터에 암묵적으로 또는 명시적으로 접근할 수 있으며 시간 관련 차원을 기반으로 결과를 제한한다는 것입니다.

이러한 차원에는 여러 가지가 있을 수 있지만, 여기서는 생성 날짜만을 중점적으로 다룹니다. 생성 날짜는 가장 일반적으로 사용되는 것이면서, 우리가 제어하고 최적화할 수 있는 것이기 때문입니다. 생성 날짜는:

  • 불변입니다.

  • 레코드가 생성될 때 설정됩니다.

  • 레코드를 이동하지 않고도 레코드를 물리적으로 클러스터링하는 것과 연결될 수 있습니다.

시간 감쇠 데이터가 기본적으로 애플리케이션에서 그러한 방식으로 접근되지 않더라도, 대다수의 쿼리가 데이터를 명시적으로 그런 방식으로 필터링하도록 만들 수 있다는 점을 추가하는 것이 중요합니다. 이러한 시간 감쇠 관련 접근 방법이 없는 시간 감쇠 데이터는 최적화 관점에서 무용지물입니다. 확장 패턴을 설정하고 따를 방법이 없기 때문입니다.

일부 예외적인 작업이 있을 수 있으므로, 항상 시간 감쇠 관련 접근 방법을 사용하는 데이터만으로 정의를 제한하지는 않습니다. 이러한 예외 작업들은 필요할 수 있으며, 나머지 접근 방법이 확장될 수 있다면 확장되지 않아도 허용할 수 있습니다. 예를 들어, 관리자가 특정 유형의 모든 과거 이벤트에 접근하는 경우, 다른 모든 작업은 최대 한 달의 이벤트에만 접근하며 과거 6개월로 제한될 수 있습니다.

불변성#

시간 감쇠 데이터의 세 번째 특성은 시간 감쇠 상태가 변하지 않는다는 것입니다. 한번 "오래된" 것으로 간주되면, 다시 "새로운" 것이나 관련 있는 상태로 전환될 수 없습니다.

이 정의는 당연해 보일 수 있지만, "오래된" 데이터에 대한 작업을 비싸게 만들 수 있어야 합니다 (예: 아카이빙하거나 저렴한 스토리지로 이동하는 것). 이 때 다시 관련성이 생겨 중요한 애플리케이션 작업이 저성능이 되는 것에 대한 걱정 없이 이를 수행할 수 있어야 합니다.

시간 감쇠 데이터 접근 패턴의 반례로, 이슈를 업데이트된 시점 기준으로 표시하는 애플리케이션 뷰를 고려해 봅시다. 우리는 "업데이트" 관점에서도 가장 최근 데이터에 관심이 있지만, 이 정의는 변동적이며 실행하기 어렵습니다.

보존#

마지막으로, 약간 다른 접근법을 사용하는 하위 범주로 시간 감쇠 데이터를 더욱 구분하는 특성은 오래된 데이터를 유지할 것인지 여부 (예: 보존 정책)와/또는 애플리케이션을 통해 사용자가 오래된 데이터에 접근 가능한지 여부입니다.

(선택 사항) 시간 감쇠 데이터의 확장된 정의#

부가 설명으로, 앞서 언급한 정의를 클러스터링 속성을 기반으로 데이터의 잘 정의된 부분집합에 접근을 제한하는 접근 패턴으로 확장한다면, 다른 많은 유형의 데이터에도 시간 감쇠 확장 패턴을 사용할 수 있습니다.

예를 들어, 완료로 표시되지 않은 To-Do, 병합되지 않은 머지 리퀘스트의 파이프라인(또는 유사한 시간 기반이 아닌 제약)처럼 활성 상태로 라벨링된 동안에만 접근되는 데이터를 생각해 봅시다. 이 경우, 감쇠를 정의하기 위해 시간 차원 대신 범주형 차원(즉, 유한한 값 집합을 사용하는 것)을 사용하여 관심 부분집합을 정의합니다. 그 부분집합이 데이터셋의 전체 크기에 비해 작은 한, 동일한 접근법을 사용할 수 있습니다.

마찬가지로, 6개월 이상 전에 실패한 CI 파이프라인과 같이 시간 차원과 추가 상태 속성을 모두 기반으로 데이터를 오래된 것으로 정의할 수도 있습니다.

시간 감쇠 데이터 전략#

테이블 파티셔닝#

이것은 순수한 데이터베이스 관점에서 시간 감쇠 데이터를 처리하는 허용 가능한 모범 사례입니다. PostgreSQL의 테이블 파티셔닝에 대한 자세한 정보는 테이블 파티셔닝 문서 페이지에서 확인할 수 있습니다.

날짜 간격(예: 월, 년)별로 파티셔닝하면 각 날짜 간격에 대해 훨씬 더 작은 테이블(파티션)을 생성하고 애플리케이션 관련 작업에 대해 가장 최근 파티션에만 접근할 수 있습니다.

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

얼마나 과거까지의 데이터에 접근해야 하는가? 52개의 서로 다른 파티션(테이블)에 걸쳐 매번 쿼리를 실행해야 한다면, 1년 전 데이터에 항상 접근하는 경우에 주별 파티셔닝은 의미가 없습니다. 예를 들어, GitLab 사용자 프로필의 활동 피드를 생각해 보십시오.

반면에, 마지막 7일의 생성 레코드에만 접근하고 싶다면, web_hook_logs의 경우처럼 연별 파티셔닝은 각 파티션에 너무 많은 불필요한 레코드를 포함하게 됩니다.

생성된 파티션은 얼마나 큰가? 파티셔닝의 주요 목적은 가능한 한 작은 테이블에 접근하는 것입니다. 파티션 자체가 너무 커지면 쿼리 성능이 저하되기 시작합니다. 더 작은 파티션으로 다시 파티셔닝(분할)해야 할 수도 있습니다.

완벽한 파티셔닝 방식은 데이터셋에 대한 모든 쿼리가 거의 항상 단일 파티션에만 적용되도록 하며, 일부 경우는 두 파티션에 걸쳐 있고 여러 파티션에 걸치는 경우는 드문 것이 허용 가능한 균형입니다. 또한 파티션은 가능한 한 작게 유지해야 하며, 각 파티션당 최대 500만~1천만 건의 레코드 및/또는 10 GB 이하를 목표로 해야 합니다.

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

오래된 레코드를 보관하고 싶지 않고 파티셔닝을 사용한다면, 오래된 데이터를 정리하는 것은 사실상 상수의, 거의 제로에 가까운 비용이 듭니다. 이는 거대한 테이블에서 데이터를 삭제하는 것(다음 하위 섹션 참조)에 비해 훨씬 효율적입니다. 해당 파티션 안의 모든 데이터가 보존 정책의 기간을 벗어나면 오래된 파티션을 삭제하는 백그라운드 워커만 있으면 됩니다.

예를 들어, 6개월 이하의 레코드만 보관하고 싶고 월별로 파티셔닝하는 경우, 항상 최신 7개의 파티션(현재 월과 과거 6개월)을 안전하게 보관할 수 있습니다. 즉, 매월 초에 8번째로 오래된 파티션을 삭제하는 워커를 가질 수 있습니다.

파티션을 동일한 데이터베이스 내의 더 저렴한 스토리지로 이동하는 것은 테이블스페이스를 사용하여 PostgreSQL에서 상대적으로 간단합니다. 각 파티션에 대해 테이블스페이스와 스토리지 파라미터를 별도로 지정할 수 있으므로, 이 경우의 접근법은 다음과 같습니다:

  • 더 저렴한 저속 디스크에 새 테이블스페이스를 만듭니다.

  • 해당 새 테이블스페이스에 스토리지 파라미터를 높게 설정하여 PostgreSQL 옵티마이저가 디스크가 느리다는 것을 알 수 있게 합니다.

  • 백그라운드 워커를 사용하여 오래된 파티션을 자동으로 느린 테이블스페이스로 이동합니다.

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

오래된 데이터 정리#

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

이것은 오래된 데이터를 삭제하는 정리 워커를 사용하는 구현이 간단한 전략입니다. 아래에서 더 자세히 분석할 예로, 90일보다 오래된 web_hook_logs를 정리하고 있습니다.

대규모 비파티션 테이블에 대한 이 솔루션의 단점은, 더 이상 관련 없다고 간주되는 모든 레코드에 수동으로 접근하여 삭제해야 한다는 것입니다. 이것은 PostgreSQL의 다중 버전 동시성 제어(multi-version concurrency control)로 인해 매우 비용이 많이 드는 작업입니다. 또한 새로운 레코드가 생성되는 비율이 임계값을 초과하면 정리 워커가 따라잡지 못하는 상황이 발생하는데, 이 문서를 작성할 당시 web_hook_logs의 경우가 그러했습니다.

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

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

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

이것이 사용자가 애플리케이션을 통해 직접 접근할 수 없다는 의미는 아닙니다. 오래된 데이터에만 적용되는 경우처럼, 메타데이터를 오프로딩하는 것과 유사하게 데이터를 데이터베이스 외부로 이동하고 다른 스토리지 엔진이나 접근 유형을 사용할 수 있습니다.

가장 간단한 사용 사례에서는 최근 데이터에 빠르고 직접적인 접근을 제공하면서, 사용자가 오래된 데이터가 담긴 아카이브를 다운로드할 수 있도록 할 수 있습니다. 이것은 audit_events 사용 사례에서 평가되는 옵션입니다. 국가와 산업에 따라, 감사 이벤트는 매우 긴 보존 기간을 가질 수 있지만, GitLab 인터페이스를 통해 활발하게 접근되는 것은 최근 몇 달의 데이터뿐입니다.

추가적인 사용 사례에는 해당 유형의 데이터를 처리하는 데 더 적합할 수 있는 데이터 웨어하우스나 다른 유형의 데이터 스토어로 데이터를 내보내는 것이 포함될 수 있습니다. 예를 들어, 때때로 테이블에 저장하는 JSON 로그를 BigQuery나 Redshift와 같은 칼럼형 스토어에 로드하면 데이터 분석/쿼리에 더 적합할 수 있습니다.

데이터를 데이터베이스 외부로 이동하기 위한 여러 전략을 고려할 수 있습니다:

  • 이 유형의 데이터를 로그로 스트리밍한 다음 보조 스토리지 옵션으로 이동하거나 다른 유형의 데이터 스토어로 직접(CSV/JSON 데이터로) 로드합니다.

  • 데이터를 CSV로 내보내고, 객체 스토리지에 업로드하고, 데이터베이스에서 이 데이터를 삭제한 다음, 다른 데이터 스토어에 CSV를 로드하는 ETL 프로세스를 생성합니다.

  • 데이터 스토어에서 제공하는 API를 사용하여 백그라운드에서 데이터를 로드합니다.

이는 대규모 데이터셋에 대해 실행 가능하지 않은 솔루션일 수 있습니다. 파일을 사용한 대량 업로드가 가능한 경우, API 호출보다 성능이 뛰어나야 합니다.

사용 사례#

웹 훅 로그#

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

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

데이터셋의 크기: 매우 큰 테이블입니다. 파티셔닝을 결정했을 때(2021-03-01), 약 5억 2700만 건의 레코드와 대략 1 TB의 총 크기를 가지고 있었습니다.

테이블: 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%)

접근 방법: 최대 최근 7일의 로그를 항상 요청합니다.

불변성: 변경되지 않는 속성인 created_at으로 파티셔닝할 수 있습니다.

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

추가로, 당시 우리는 백그라운드 워커(PruneWebHookLogsWorker)를 사용하여 데이터를 정리하려 했지만, 삽입 속도를 따라잡지 못했습니다.

그 결과, 2021년 3월에는 2020년 7월부터 삭제되지 않은 레코드가 여전히 남아 있었고, 테이블은 다소 안정적인 크기를 유지하는 대신 하루에 200만 건 이상의 레코드씩 증가하고 있었습니다.

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

우리의 접근 방식은 90일 보존 정책과 일치하는 월별 파티셔닝이었습니다.

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

파티셔닝 키 결정

이 경우 created_at 칼럼을 사용하는 것이 간단합니다: 보존 정책이 있고 충돌하는 접근 패턴이 없었을 때 자연스러운 파티셔닝 키입니다.

파티셔닝 키를 결정한 후, 파티션을 생성하고 백필(기존 테이블에서 데이터 복사)할 수 있습니다. 기존 테이블을 그냥 파티셔닝할 수는 없습니다. 새로운 파티션 테이블을 만들어야 합니다.

따라서 파티션 테이블과 관련된 모든 파티션을 생성하고, 모든 것을 복사하기 시작하며, 기존 데이터에 대한 새로운 데이터나 업데이트/삭제가 새 파티션 테이블에 미러링될 수 있도록 동기화 트리거도 추가해야 합니다.

테이블 파티셔닝 시작에 필요한 모든 세부 정보가 담긴 MR

이 프로세스를 완료하는 데 15일 7시간 36분이 걸렸습니다.

초기 파티셔닝 시작 후 한 마일스톤 뒤, 백필에 사용된 백그라운드 마이그레이션을 정리하고 남아 있는 job을 마저 실행하고, 실패한 job을 재시도하는 등의 작업을 합니다.

필요한 모든 세부 정보가 담긴 MR

파티션 테이블에 남아 있는 외래 키와 보조 인덱스를 추가합니다. 이렇게 하면 다음 마일스톤에서 교체하기 전에 원래 비파티션 테이블과 스키마가 동일해집니다.

각 삽입에 오버헤드가 추가되고 테이블의 초기 백필을 느리게 만들기 때문에 처음에는 추가하지 않습니다(이 경우 5억 건 이상의 레코드에 대해 상당히 시간이 누적될 수 있습니다). 따라서 경량화된 기본 버전의 테이블을 생성하고, 모든 데이터를 복사한 다음 남아 있는 인덱스와 외래 키를 추가합니다.

기본 테이블과 파티션 복사본 교체: 이때부터 파티션 테이블이 애플리케이션에서 활발하게 사용되기 시작합니다.

원래 테이블을 삭제하는 것은 파괴적인 작업이며 프로세스 중에 문제가 없었는지 확인하고 싶으므로, 오래된 비파티션 테이블을 보관합니다. 또한 파티션 테이블에서 발생하는 작업으로 비파티션 테이블이 여전히 최신 상태로 유지되도록 동기화 트리거를 반대 방향으로 전환합니다. 이를 통해 필요한 경우 테이블을 다시 교체할 수 있습니다.

필요한 모든 세부 정보가 담긴 MR

마지막 단계, 교체 후 한 마일스톤: 비파티션 테이블 삭제

필요한 모든 세부 정보가 담긴 이슈

비파티션 테이블이 삭제된 후, 과거 파티션을 삭제하는 정리 전략을 구현하는 워커를 추가할 수 있습니다.

이 경우, 워커는 항상 4개의 파티션만 활성 상태로 유지하고(보존 정책이 90일이므로) 네 달 이상 된 파티션을 삭제합니다. 현재 달이 아직 활성 상태인 동안 4개월의 파티션을 보관해야 합니다. 90일 전으로 거슬러 올라가면 네 번째로 오래된 파티션에 이르기 때문입니다.

감사 이벤트#

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

audit_events 테이블은 이전 하위 섹션에서 논의한 web_hook_logs 테이블과 많은 특성을 공유하므로, 차이점에 초점을 맞춥니다.

파티셔닝이 대부분의 성능 문제를 해결할 수 있다는 것이 공통적인 의견이었습니다.

대부분의 다른 대형 테이블과 달리, 충돌하는 주요 접근 패턴이 없습니다. 월별 파티셔닝에 맞게 접근 패턴을 전환할 수 있었습니다. 이것은 예를 들어, 파티셔닝 접근 방식을 정당화할 수 있지만(예: 네임스페이스별) 충돌하는 접근 패턴이 많은 다른 테이블의 경우와는 다릅니다.

또한, audit_events는 매우 적은 읽기(쿼리)를 가진 쓰기 집약적 테이블이며, 나머지 데이터베이스와 연결되지 않은(들어오거나 나가는 FK 제약 없음) 매우 단순한 스키마를 가지고 있으며, 두 개의 인덱스만 정의되어 있습니다.

후자는 당시 중요했는데, 외래 키 제약이 없다는 것은 PostgreSQL 11에서도 파티셔닝할 수 있다는 것을 의미했기 때문입니다. 이것은 이제 위의 web_hook_logs 사용 사례에서 볼 수 있듯이, PostgreSQL 12를 필수 기본값으로 이동했으므로 더 이상 우려 사항이 아닙니다.

audit_events 파티셔닝에 필요한 마이그레이션과 단계는 web_hook_logs에 대해 이전 하위 섹션에서 설명한 것과 유사합니다. 현재 audit_events에 대한 보존 전략이 정의되어 있지 않으므로, 정리 전략도 구현되어 있지 않지만 향후 아카이빙 솔루션을 구현할 수도 있습니다.

audit_events의 경우에서 흥미로운 것은, 파티셔닝된 데이터의 최적 쿼리를 장려하기 위해 필요한 UI/UX 변경 사항을 구현하기 위해 따라야 했던 필요한 단계에 대한 논의입니다. 이것은 특정 시간 감쇠 관련 접근 방법에 모든 접근 패턴을 맞추기 위해 애플리케이션 수준에서 필요한 변경 사항에 대한 시작점으로 사용될 수 있습니다.

CI 테이블#

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

시간 감쇠 데이터

GitLab v19.1
원문 보기
요약

이 문서는 데이터베이스 확장성 워킹 그룹에서 도입된 *시간 감쇠 패턴(time-decay pattern)*을 설명합니다. 일부 데이터셋은 강한 시간 감쇠 효과의 영향을 받습니다. 이러한 효과는 일반적으로 제품 또는 애플리케이션의 의미론(semantics)과 연관됩니다.

이 문서는 데이터베이스 확장성 워킹 그룹에서 도입된 *시간 감쇠 패턴(time-decay pattern)*을 설명합니다. 시간 감쇠 데이터의 특성을 살펴보고, 이 맥락에서 GitLab 개발 시 고려해야 할 모범 사례를 제안합니다.

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

이러한 효과는 일반적으로 제품 또는 애플리케이션의 의미론(semantics)과 연관됩니다. 오래된 데이터에 얼마나 자주 접근하는지, 그리고 오래된 데이터가 사용자나 애플리케이션에 얼마나 유용하거나 필요한지에 따라 정도가 달라질 수 있습니다.

먼저 데이터에 있어서 시간과 관련된 고유한 편향이 없는 엔티티를 생각해 봅시다.

사용자나 프로젝트의 레코드는 생성 시점과 무관하게 동일하게 중요하고 자주 접근될 수 있습니다. 사용자의 idcreated_at을 사용하는 것만으로는 관련 레코드가 얼마나 자주 접근되거나 업데이트되는지 예측할 수 없습니다.

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

이러한 유형의 데이터는 대부분 며칠 또는 몇 주 후에 비즈니스적 용도가 없어지며, 데이터 분석 관점에서도 빠르게 덜 중요해집니다. 어느 시점에 실질적인 가치가 없어질 때까지 애플리케이션의 현재 상태와 점점 더 관련이 없어지는 스냅샷을 나타냅니다.

두 극단의 중간에는, 보존하고 싶은 유용한 정보를 가진 데이터셋이 있지만, 생성 후 초기(짧은) 기간이 지나면 오래된 레코드에 거의 접근하지 않게 됩니다.

시간 감쇠 데이터의 특성#

우리가 관심을 갖는 데이터셋은 다음과 같은 특성을 보입니다:

  • 데이터셋의 크기: 상당히 큰 규모입니다.

  • 접근 방법: 데이터셋에 접근하는 대다수의 쿼리를 시간 관련 차원 또는 시간 감쇠 효과를 가진 범주형 차원으로 필터링할 수 있습니다.

  • 불변성: 시간 감쇠 상태가 변하지 않습니다.

  • 보존: 오래된 데이터를 유지할 것인지 여부, 또는 오래된 데이터가 애플리케이션을 통해 사용자에게 계속 접근 가능해야 하는지 여부입니다.

데이터셋의 크기#

강한 시간 감쇠 효과를 보이는 다양한 크기의 데이터셋이 있을 수 있지만, 이 블루프린트의 맥락에서는 상당히 큰 데이터셋을 가진 엔티티에 집중하려 합니다.

소규모 데이터셋은 데이터베이스 관련 리소스 사용에 크게 기여하지 않으며, 쿼리에 상당한 성능 저하를 일으키지도 않습니다.

반면, 약 5천만 건 이상의 레코드 또는 100 GB 이상의 크기를 가진 대규모 데이터셋은 정말 작은 데이터 부분집합에 지속적으로 접근하는 데 상당한 오버헤드를 추가합니다. 이러한 경우, 시간 감쇠 효과를 활용하여 활발하게 접근되는 데이터셋을 줄이고자 합니다.

데이터 접근 방법#

시간 감쇠 데이터의 두 번째이자 가장 중요한 특성은, 대부분의 경우 날짜 필터를 사용하여 데이터에 암묵적으로 또는 명시적으로 접근할 수 있으며 시간 관련 차원을 기반으로 결과를 제한한다는 것입니다.

이러한 차원에는 여러 가지가 있을 수 있지만, 여기서는 생성 날짜만을 중점적으로 다룹니다. 생성 날짜는 가장 일반적으로 사용되는 것이면서, 우리가 제어하고 최적화할 수 있는 것이기 때문입니다. 생성 날짜는:

  • 불변입니다.

  • 레코드가 생성될 때 설정됩니다.

  • 레코드를 이동하지 않고도 레코드를 물리적으로 클러스터링하는 것과 연결될 수 있습니다.

시간 감쇠 데이터가 기본적으로 애플리케이션에서 그러한 방식으로 접근되지 않더라도, 대다수의 쿼리가 데이터를 명시적으로 그런 방식으로 필터링하도록 만들 수 있다는 점을 추가하는 것이 중요합니다. 이러한 시간 감쇠 관련 접근 방법이 없는 시간 감쇠 데이터는 최적화 관점에서 무용지물입니다. 확장 패턴을 설정하고 따를 방법이 없기 때문입니다.

일부 예외적인 작업이 있을 수 있으므로, 항상 시간 감쇠 관련 접근 방법을 사용하는 데이터만으로 정의를 제한하지는 않습니다. 이러한 예외 작업들은 필요할 수 있으며, 나머지 접근 방법이 확장될 수 있다면 확장되지 않아도 허용할 수 있습니다. 예를 들어, 관리자가 특정 유형의 모든 과거 이벤트에 접근하는 경우, 다른 모든 작업은 최대 한 달의 이벤트에만 접근하며 과거 6개월로 제한될 수 있습니다.

불변성#

시간 감쇠 데이터의 세 번째 특성은 시간 감쇠 상태가 변하지 않는다는 것입니다. 한번 "오래된" 것으로 간주되면, 다시 "새로운" 것이나 관련 있는 상태로 전환될 수 없습니다.

이 정의는 당연해 보일 수 있지만, "오래된" 데이터에 대한 작업을 비싸게 만들 수 있어야 합니다 (예: 아카이빙하거나 저렴한 스토리지로 이동하는 것). 이 때 다시 관련성이 생겨 중요한 애플리케이션 작업이 저성능이 되는 것에 대한 걱정 없이 이를 수행할 수 있어야 합니다.

시간 감쇠 데이터 접근 패턴의 반례로, 이슈를 업데이트된 시점 기준으로 표시하는 애플리케이션 뷰를 고려해 봅시다. 우리는 "업데이트" 관점에서도 가장 최근 데이터에 관심이 있지만, 이 정의는 변동적이며 실행하기 어렵습니다.

보존#

마지막으로, 약간 다른 접근법을 사용하는 하위 범주로 시간 감쇠 데이터를 더욱 구분하는 특성은 오래된 데이터를 유지할 것인지 여부 (예: 보존 정책)와/또는 애플리케이션을 통해 사용자가 오래된 데이터에 접근 가능한지 여부입니다.

(선택 사항) 시간 감쇠 데이터의 확장된 정의#

부가 설명으로, 앞서 언급한 정의를 클러스터링 속성을 기반으로 데이터의 잘 정의된 부분집합에 접근을 제한하는 접근 패턴으로 확장한다면, 다른 많은 유형의 데이터에도 시간 감쇠 확장 패턴을 사용할 수 있습니다.

예를 들어, 완료로 표시되지 않은 To-Do, 병합되지 않은 머지 리퀘스트의 파이프라인(또는 유사한 시간 기반이 아닌 제약)처럼 활성 상태로 라벨링된 동안에만 접근되는 데이터를 생각해 봅시다. 이 경우, 감쇠를 정의하기 위해 시간 차원 대신 범주형 차원(즉, 유한한 값 집합을 사용하는 것)을 사용하여 관심 부분집합을 정의합니다. 그 부분집합이 데이터셋의 전체 크기에 비해 작은 한, 동일한 접근법을 사용할 수 있습니다.

마찬가지로, 6개월 이상 전에 실패한 CI 파이프라인과 같이 시간 차원과 추가 상태 속성을 모두 기반으로 데이터를 오래된 것으로 정의할 수도 있습니다.

시간 감쇠 데이터 전략#

테이블 파티셔닝#

이것은 순수한 데이터베이스 관점에서 시간 감쇠 데이터를 처리하는 허용 가능한 모범 사례입니다. PostgreSQL의 테이블 파티셔닝에 대한 자세한 정보는 테이블 파티셔닝 문서 페이지에서 확인할 수 있습니다.

날짜 간격(예: 월, 년)별로 파티셔닝하면 각 날짜 간격에 대해 훨씬 더 작은 테이블(파티션)을 생성하고 애플리케이션 관련 작업에 대해 가장 최근 파티션에만 접근할 수 있습니다.

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

얼마나 과거까지의 데이터에 접근해야 하는가? 52개의 서로 다른 파티션(테이블)에 걸쳐 매번 쿼리를 실행해야 한다면, 1년 전 데이터에 항상 접근하는 경우에 주별 파티셔닝은 의미가 없습니다. 예를 들어, GitLab 사용자 프로필의 활동 피드를 생각해 보십시오.

반면에, 마지막 7일의 생성 레코드에만 접근하고 싶다면, web_hook_logs의 경우처럼 연별 파티셔닝은 각 파티션에 너무 많은 불필요한 레코드를 포함하게 됩니다.

생성된 파티션은 얼마나 큰가? 파티셔닝의 주요 목적은 가능한 한 작은 테이블에 접근하는 것입니다. 파티션 자체가 너무 커지면 쿼리 성능이 저하되기 시작합니다. 더 작은 파티션으로 다시 파티셔닝(분할)해야 할 수도 있습니다.

완벽한 파티셔닝 방식은 데이터셋에 대한 모든 쿼리가 거의 항상 단일 파티션에만 적용되도록 하며, 일부 경우는 두 파티션에 걸쳐 있고 여러 파티션에 걸치는 경우는 드문 것이 허용 가능한 균형입니다. 또한 파티션은 가능한 한 작게 유지해야 하며, 각 파티션당 최대 500만~1천만 건의 레코드 및/또는 10 GB 이하를 목표로 해야 합니다.

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

오래된 레코드를 보관하고 싶지 않고 파티셔닝을 사용한다면, 오래된 데이터를 정리하는 것은 사실상 상수의, 거의 제로에 가까운 비용이 듭니다. 이는 거대한 테이블에서 데이터를 삭제하는 것(다음 하위 섹션 참조)에 비해 훨씬 효율적입니다. 해당 파티션 안의 모든 데이터가 보존 정책의 기간을 벗어나면 오래된 파티션을 삭제하는 백그라운드 워커만 있으면 됩니다.

예를 들어, 6개월 이하의 레코드만 보관하고 싶고 월별로 파티셔닝하는 경우, 항상 최신 7개의 파티션(현재 월과 과거 6개월)을 안전하게 보관할 수 있습니다. 즉, 매월 초에 8번째로 오래된 파티션을 삭제하는 워커를 가질 수 있습니다.

파티션을 동일한 데이터베이스 내의 더 저렴한 스토리지로 이동하는 것은 테이블스페이스를 사용하여 PostgreSQL에서 상대적으로 간단합니다. 각 파티션에 대해 테이블스페이스와 스토리지 파라미터를 별도로 지정할 수 있으므로, 이 경우의 접근법은 다음과 같습니다:

  • 더 저렴한 저속 디스크에 새 테이블스페이스를 만듭니다.

  • 해당 새 테이블스페이스에 스토리지 파라미터를 높게 설정하여 PostgreSQL 옵티마이저가 디스크가 느리다는 것을 알 수 있게 합니다.

  • 백그라운드 워커를 사용하여 오래된 파티션을 자동으로 느린 테이블스페이스로 이동합니다.

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

오래된 데이터 정리#

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

이것은 오래된 데이터를 삭제하는 정리 워커를 사용하는 구현이 간단한 전략입니다. 아래에서 더 자세히 분석할 예로, 90일보다 오래된 web_hook_logs를 정리하고 있습니다.

대규모 비파티션 테이블에 대한 이 솔루션의 단점은, 더 이상 관련 없다고 간주되는 모든 레코드에 수동으로 접근하여 삭제해야 한다는 것입니다. 이것은 PostgreSQL의 다중 버전 동시성 제어(multi-version concurrency control)로 인해 매우 비용이 많이 드는 작업입니다. 또한 새로운 레코드가 생성되는 비율이 임계값을 초과하면 정리 워커가 따라잡지 못하는 상황이 발생하는데, 이 문서를 작성할 당시 web_hook_logs의 경우가 그러했습니다.

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

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

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

이것이 사용자가 애플리케이션을 통해 직접 접근할 수 없다는 의미는 아닙니다. 오래된 데이터에만 적용되는 경우처럼, 메타데이터를 오프로딩하는 것과 유사하게 데이터를 데이터베이스 외부로 이동하고 다른 스토리지 엔진이나 접근 유형을 사용할 수 있습니다.

가장 간단한 사용 사례에서는 최근 데이터에 빠르고 직접적인 접근을 제공하면서, 사용자가 오래된 데이터가 담긴 아카이브를 다운로드할 수 있도록 할 수 있습니다. 이것은 audit_events 사용 사례에서 평가되는 옵션입니다. 국가와 산업에 따라, 감사 이벤트는 매우 긴 보존 기간을 가질 수 있지만, GitLab 인터페이스를 통해 활발하게 접근되는 것은 최근 몇 달의 데이터뿐입니다.

추가적인 사용 사례에는 해당 유형의 데이터를 처리하는 데 더 적합할 수 있는 데이터 웨어하우스나 다른 유형의 데이터 스토어로 데이터를 내보내는 것이 포함될 수 있습니다. 예를 들어, 때때로 테이블에 저장하는 JSON 로그를 BigQuery나 Redshift와 같은 칼럼형 스토어에 로드하면 데이터 분석/쿼리에 더 적합할 수 있습니다.

데이터를 데이터베이스 외부로 이동하기 위한 여러 전략을 고려할 수 있습니다:

  • 이 유형의 데이터를 로그로 스트리밍한 다음 보조 스토리지 옵션으로 이동하거나 다른 유형의 데이터 스토어로 직접(CSV/JSON 데이터로) 로드합니다.

  • 데이터를 CSV로 내보내고, 객체 스토리지에 업로드하고, 데이터베이스에서 이 데이터를 삭제한 다음, 다른 데이터 스토어에 CSV를 로드하는 ETL 프로세스를 생성합니다.

  • 데이터 스토어에서 제공하는 API를 사용하여 백그라운드에서 데이터를 로드합니다.

이는 대규모 데이터셋에 대해 실행 가능하지 않은 솔루션일 수 있습니다. 파일을 사용한 대량 업로드가 가능한 경우, API 호출보다 성능이 뛰어나야 합니다.

사용 사례#

웹 훅 로그#

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

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

데이터셋의 크기: 매우 큰 테이블입니다. 파티셔닝을 결정했을 때(2021-03-01), 약 5억 2700만 건의 레코드와 대략 1 TB의 총 크기를 가지고 있었습니다.

테이블: 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%)

접근 방법: 최대 최근 7일의 로그를 항상 요청합니다.

불변성: 변경되지 않는 속성인 created_at으로 파티셔닝할 수 있습니다.

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

추가로, 당시 우리는 백그라운드 워커(PruneWebHookLogsWorker)를 사용하여 데이터를 정리하려 했지만, 삽입 속도를 따라잡지 못했습니다.

그 결과, 2021년 3월에는 2020년 7월부터 삭제되지 않은 레코드가 여전히 남아 있었고, 테이블은 다소 안정적인 크기를 유지하는 대신 하루에 200만 건 이상의 레코드씩 증가하고 있었습니다.

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

우리의 접근 방식은 90일 보존 정책과 일치하는 월별 파티셔닝이었습니다.

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

파티셔닝 키 결정

이 경우 created_at 칼럼을 사용하는 것이 간단합니다: 보존 정책이 있고 충돌하는 접근 패턴이 없었을 때 자연스러운 파티셔닝 키입니다.

파티셔닝 키를 결정한 후, 파티션을 생성하고 백필(기존 테이블에서 데이터 복사)할 수 있습니다. 기존 테이블을 그냥 파티셔닝할 수는 없습니다. 새로운 파티션 테이블을 만들어야 합니다.

따라서 파티션 테이블과 관련된 모든 파티션을 생성하고, 모든 것을 복사하기 시작하며, 기존 데이터에 대한 새로운 데이터나 업데이트/삭제가 새 파티션 테이블에 미러링될 수 있도록 동기화 트리거도 추가해야 합니다.

테이블 파티셔닝 시작에 필요한 모든 세부 정보가 담긴 MR

이 프로세스를 완료하는 데 15일 7시간 36분이 걸렸습니다.

초기 파티셔닝 시작 후 한 마일스톤 뒤, 백필에 사용된 백그라운드 마이그레이션을 정리하고 남아 있는 job을 마저 실행하고, 실패한 job을 재시도하는 등의 작업을 합니다.

필요한 모든 세부 정보가 담긴 MR

파티션 테이블에 남아 있는 외래 키와 보조 인덱스를 추가합니다. 이렇게 하면 다음 마일스톤에서 교체하기 전에 원래 비파티션 테이블과 스키마가 동일해집니다.

각 삽입에 오버헤드가 추가되고 테이블의 초기 백필을 느리게 만들기 때문에 처음에는 추가하지 않습니다(이 경우 5억 건 이상의 레코드에 대해 상당히 시간이 누적될 수 있습니다). 따라서 경량화된 기본 버전의 테이블을 생성하고, 모든 데이터를 복사한 다음 남아 있는 인덱스와 외래 키를 추가합니다.

기본 테이블과 파티션 복사본 교체: 이때부터 파티션 테이블이 애플리케이션에서 활발하게 사용되기 시작합니다.

원래 테이블을 삭제하는 것은 파괴적인 작업이며 프로세스 중에 문제가 없었는지 확인하고 싶으므로, 오래된 비파티션 테이블을 보관합니다. 또한 파티션 테이블에서 발생하는 작업으로 비파티션 테이블이 여전히 최신 상태로 유지되도록 동기화 트리거를 반대 방향으로 전환합니다. 이를 통해 필요한 경우 테이블을 다시 교체할 수 있습니다.

필요한 모든 세부 정보가 담긴 MR

마지막 단계, 교체 후 한 마일스톤: 비파티션 테이블 삭제

필요한 모든 세부 정보가 담긴 이슈

비파티션 테이블이 삭제된 후, 과거 파티션을 삭제하는 정리 전략을 구현하는 워커를 추가할 수 있습니다.

이 경우, 워커는 항상 4개의 파티션만 활성 상태로 유지하고(보존 정책이 90일이므로) 네 달 이상 된 파티션을 삭제합니다. 현재 달이 아직 활성 상태인 동안 4개월의 파티션을 보관해야 합니다. 90일 전으로 거슬러 올라가면 네 번째로 오래된 파티션에 이르기 때문입니다.

감사 이벤트#

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

audit_events 테이블은 이전 하위 섹션에서 논의한 web_hook_logs 테이블과 많은 특성을 공유하므로, 차이점에 초점을 맞춥니다.

파티셔닝이 대부분의 성능 문제를 해결할 수 있다는 것이 공통적인 의견이었습니다.

대부분의 다른 대형 테이블과 달리, 충돌하는 주요 접근 패턴이 없습니다. 월별 파티셔닝에 맞게 접근 패턴을 전환할 수 있었습니다. 이것은 예를 들어, 파티셔닝 접근 방식을 정당화할 수 있지만(예: 네임스페이스별) 충돌하는 접근 패턴이 많은 다른 테이블의 경우와는 다릅니다.

또한, audit_events는 매우 적은 읽기(쿼리)를 가진 쓰기 집약적 테이블이며, 나머지 데이터베이스와 연결되지 않은(들어오거나 나가는 FK 제약 없음) 매우 단순한 스키마를 가지고 있으며, 두 개의 인덱스만 정의되어 있습니다.

후자는 당시 중요했는데, 외래 키 제약이 없다는 것은 PostgreSQL 11에서도 파티셔닝할 수 있다는 것을 의미했기 때문입니다. 이것은 이제 위의 web_hook_logs 사용 사례에서 볼 수 있듯이, PostgreSQL 12를 필수 기본값으로 이동했으므로 더 이상 우려 사항이 아닙니다.

audit_events 파티셔닝에 필요한 마이그레이션과 단계는 web_hook_logs에 대해 이전 하위 섹션에서 설명한 것과 유사합니다. 현재 audit_events에 대한 보존 전략이 정의되어 있지 않으므로, 정리 전략도 구현되어 있지 않지만 향후 아카이빙 솔루션을 구현할 수도 있습니다.

audit_events의 경우에서 흥미로운 것은, 파티셔닝된 데이터의 최적 쿼리를 장려하기 위해 필요한 UI/UX 변경 사항을 구현하기 위해 따라야 했던 필요한 단계에 대한 논의입니다. 이것은 특정 시간 감쇠 관련 접근 방법에 모든 접근 패턴을 맞추기 위해 애플리케이션 수준에서 필요한 변경 사항에 대한 시작점으로 사용될 수 있습니다.

CI 테이블#

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