This page contains information related to upcoming products, features, and functionality. It is important to note that the information presented is for informational purposes only. Please do not rely on this information for purchasing or planning purposes. The development, release, and timing of any products, features, or functionality may be subject to change or delay and remain at the sole discretion of GitLab Inc.
Status Authors Coach DRIs Owning Stage Created
ongoing @grzesiek @ayufan @grzesiek @jreporter @cheryl.li devops verify 2022-05-31

파이프라인 데이터 파티셔닝 설계

어떤 문제를 해결하려고 하는가?

우리는 CI/CD 데이터셋을 파티셔닝하고자 합니다. 왜냐하면 일부 데이터베이스 테이블이 매우 크기 때문에 CI/CD 데이터베이스 분해를 배포한 후에도 단일 노드 읽기의 확장 측면에서 어려움을 겪을 수 있기 때문입니다.

우리는 가장 큰 데이터베이스 테이블 중 일부를 PostgreSQL 선언적 파티셔닝을 사용하여 더 작은 테이블로 변환하여 데이터베이스 성능 하락의 위험을 줄이고자 합니다.

이 노력에 대한 자세한 내용은 상위 청사진에서 더 자세히 살펴볼 수 있습니다.

파이프라인 데이터 시간 감쇠

CI/CD 데이터 분해, 파티셔닝 및 시간 감쇠는 어떤 관련이 있나요?

CI/CD 분해는 CI/CD 데이터베이스 클러스터를 “주요” 데이터베이스 클러스터에서 분리하여 다른 주 데이터베이스가 쓰기를받을 수 있도록 하는 것입니다. 주요 이점은 쓰기 및 데이터 저장용 용량을 두 배로 늘릴 수 있다는 것입니다. 새 데이터베이스 클러스터는 비-CI/CD 데이터베이스 테이블에 대한 읽기/쓰기를 제공할 필요가 없으므로 읽기에 대한 추가 용량도 제공됩니다.

CI/CD 파티셔닝은 큰 CI/CD 데이터베이스 테이블을 더 작은 테이블로 분할하는 것입니다. 이는 각 CI/CD 데이터베이스 노드에서 읽기 용량을 향상시킬 것이며, 작은 테이블에서 데이터를 읽는 것이 거대한 테이블에서 데이터를 읽는 것보다 훨씬 저렴하기 때문입니다. 우리는 데이터를 읽는 SQL 쿼리의 증가를 더 잘 처리하기 위해 더 많은 CI/CD 데이터베이스 복제본을 추가할 수 있지만, 효율적으로 단일 읽기를 수행하려면 파티셔닝이 필요합니다. 다른 측면에서의 성능도 향상될 것이며, PostgreSQL은 매우 큰 데이터베이스 테이블을 유지보수하는 것보다 여러 개의 작은 테이블을 유지하는 것이 더 효율적일 것입니다.

CI/CD 시간 감쇠는 파이프라인 데이터의 강력한 시간 감쇠 특성을 활용할 수 있도록 합니다. 이를 구현하는 다양한 방법이 있지만, 파티셔닝을 사용하여 시간 감쇠를 구현하는 것이 특히 유익할 수 있습니다. 시간 감쇠를 구현할 때 보통 데이터를 아카이브로 표시하고 데이터가 더 이상 관련이 없거나 필요하지 않을 때 데이터베이스에서 다른 위치로 마이그레이션합니다. 우리 데이터셋은 매우 크기 때문에(수십 테라바이트), 이러한 대량의 데이터를 이동하는 것은 어려운 일입니다. 파티셔닝을 사용하여 시간 감쇠를 구현하면 하나의 데이터베이스 테이블에서 단일 레코드를 업데이트하여 전체 파티션(또는 파티션 세트)을 아카이브할 수 있습니다. 데이터베이스 수준에서 시간 감쇠 패턴을 구현하기에 가장 적은 비용이 드는 방법 중 하나입니다.

분해와 파티셔닝 비교

왜 CI/CD 데이터를 파티션해야 하는가?

우리는 파이프라인, 빌드 및 아티팩트를 저장하는 데이터베이스 테이블이 너무 크기 때문에 CI/CD 데이터를 파티션해야 합니다. ci_builds 데이터베이스 테이블 크기는 현재 약 2.5 TB이며 인덱스 크기는 약 1.4 GB입니다. 이것은 너무 크고 저희의 100GB 크기 제한 원칙을 위반합니다. 또한, 이 숫자가 초과되면 경고를 생성하여 우리에게 알림을 보내고 싶습니다.

큰 SQL 테이블은 인덱스 유지 시간을 증가시키는데, 이는 최신 삭제된 튜플들이 자동 진공에 의해 정리되지 못하는 시간을 강조합니다. 이는 작은 테이블이 필요함을 보여줍니다. 우리는 거대한 테이블을 (다시) 인덱싱할 때 얼마나 많은 불룻이 축적되는지 메트릭할 것입니다. 이 분석을 기반으로 (다시) 인덱싱과 관련된 SLO(죽은 튜플/불룻)를 설정할 수 있을 것입니다.

지난 몇 달 동안 전반적으로 S1 및 S2 데이터베이스 관련 프로덕션 환경 사고가 많이 발생했습니다. 예를 들어:

우리에게는 대략 50개의 ci_* 접두사가 붙은 데이터베이스 테이블이 있으며, 그 중 일부는 파티션을 준다면 이점을 얻을 수 있을 것입니다.

이 데이터를 가져오기 위한 간단한 SQL 쿼리:

WITH tables AS (SELECT table_name FROM information_schema.tables WHERE table_name LIKE 'ci_%')
  SELECT table_name,
    pg_size_pretty(pg_total_relation_size(quote_ident(table_name))) AS total_size,
    pg_size_pretty(pg_relation_size(quote_ident(table_name))) AS table_size,
    pg_size_pretty(pg_indexes_size(quote_ident(table_name))) AS index_size,
    pg_total_relation_size(quote_ident(table_name)) AS total_size_bytes
  FROM tables ORDER BY total_size_bytes DESC;

2022년 3월 데이터:

테이블명 총 크기 인덱스 크기
ci_builds 3.5 TB 1 TB
ci_builds_metadata 1.8 TB 150 GB
ci_job_artifacts 600 GB 300 GB
ci_pipelines 400 GB 300 GB
ci_stages 200 GB 120 GB
ci_pipeline_variables 100 GB 20 GB
(…약 40개 더 있음)    

위의 테이블에서 분명히 많은 데이터가 저장되어 있음이 명확합니다.

우리가 거의 50개의 CI/CD 관련 데이터베이스 테이블이 있음에도, 우리는 초기에는 6개의 테이블에만 파티션하려고 합니다. 우리는 시작할 때 가장 흥미로운 테이블을 순환적으로 파티션하겠으나, 필요에 따라 나머지 테이블을 파티션할 방법도 가져야 합니다. 이 문서는 이러한 전략을 포착하고 가능한 많은 세부 사항을 설명하여 이 지식을 엔지니어링 팀 사이에서 공유하고자 하는 시도입니다.

어떻게 CI/CD 데이터를 파티션하고자 하는가?

우리는 CI/CD 테이블을 순환적으로 파티션하고자 합니다. 초기 6개의 테이블을 동시에 모두 파티션하는 것은 현실적이지 않을 수 있으므로 순환적인 전략이 필요할 수 있습니다. 또한 나중에 필요해졌을 때 나머지 데이터베이스 테이블을 파티션하는 전략도 가져야 합니다.

또한 대규모 데이터 마이그레이션을 피하는 것이 중요합니다. 우리는 가장 큰 6개의 CI/CD 테이블에 거의 6테라바이트의 데이터를 다양한 열과 인덱스로 저장하고 있습니다. 이러한 대량의 데이터를 마이그레이션하는 것은 어려울 수 있으며 프로덕션 환경에서 불안정성을 유발할 수 있습니다. 이와 같은 우려로 인해 우리는 기존 데이터베이스 테이블을 다운타임없이 파티션 제로로 첨부하는 방법을 개발했습니다. 이것이 가능한 것은 증명되었습니다. 이를 통해 파티션된 스키마를 만들 수 있지만 (예: 라우팅 테이블 p_ci_pipelines 사용), 이전의 ci_pipelines 테이블을 배타적 잠금 없이 파티션 제로로 첨부할 수 있습니다. 레거시 테이블은 평소와 같이 사용할 수 있지만, 우리가 필요에 따라 새 파티션을 생성할 수 있으며 p_ci_pipelines 테이블을 라우팅 쿼리에 사용할 것입니다. 라우팅 테이블을 사용하기 위해서는 좋은 파티션 키를 찾아야 합니다.

우리의 계획은 논리적 파티션 ID를 사용하는 것입니다. 우리는 ci_pipelines 테이블로 시작하여 partition_id 열을 DEFAULT100 또는 1000으로 만들고자 합니다. DEFAULT 값을 추가함으로써 매 행에 대해 이 값을 완전히 뒷받침할 필요가 없게 됩니다. 첫 번째 파티션을 첨부하기 전에 CHECK 제약 조건을 추가함으로써 PostgreSQL에게 이미 일관성을 보증했음을 알려주고 이 테이블을 파티션된 스키마 정의에 배타적 테이블 잠금을 보유하는 동안 첨부할 때 이를 확인할 필요가 없음을 알립니다. 우리는 p_ci_pipelines를 위한 두 번째 파티션을 생성할 때마다 이 값을 증가시키고, 파티셔닝 전략은 LIST 파티셔닝일 것입니다.

마찬가지로, 순환적으로 파티션화하고자 하는 초기 6개의 데이터베이스 테이블에도 partition_id 열을 만들고자 합니다. 새로운 파이프라인이 생성되면 partition_id가 할당되고 빌드 및 아티팩트와 같은 관련 리소스는 동일한 값을 사용할 것입니다. 우리는 이러한 문제가 발생할 때 partition_id 열을 6개의 문제가 있는 테이블에 추가하고자 합니다.

우리는 CI/CD 데이터를 순환적으로 파티션하기를 원합니다. 빠르게 성장하는 ci_builds_metadata 테이블로 시작하고자 합니다. 기존 테이블에서 빌드 메타데이터를 사용할 때 읽지만, 다른 액세스 패턴도 비교적 간단할 것입니다. p_ci_builds_metadata로 시작함으로써 우리는 조직적이고 양적인 결과를 조기에 달성할 수 있을 것이며, 최대 테이블을 파티션화하는 새로운 패턴이 될 것입니다. 우리는 LIST 파티셔닝 전략을 사용하여 빌드 메타데이터를 파티션화할 것입니다.

많은 파티션이 p_ci_builds_metadata에 첨부된 상태에서 많은 partition_id를 가지게 되면 다음에 파티션화할 다른 CI 테이블을 선택할 것입니다. 이 경우에는 p_ci_builds_metadata를 이미 많은 물리적인 파티션으로 가지고 있을 것이므로 다음 테이블에 대해 RANGE 파티셔닝을 사용할 수도 있을 것입니다. 예를 들어, ci_builds를 다음 파티션화 후보로 선택한다면, p_ci_builds_metadata를 이미 많은 물리적 파티션이 있기 때문에 여러 다른 값이 ci_builds.partition_id에 저장되어 있을 것입니다. 이 경우 RANGE 파티셔닝을 사용하는 것이 더 쉬울 수 있습니다.

물리적 파티셔닝과 논리적 파티셔

다중 프로젝트 파이프라인

부모-자식 파이프라인은 항상 동일한 파티션의 일부가 될 것입니다. 왜냐하면 자식 파이프라인은 부모 파이프라인의 리소스로 간주되기 때문입니다. 이들은 프로젝트 파이프라인 디렉터리 페이지에서 개별적으로 볼 수 없습니다.

반면에, 다중 프로젝트 파이프라인은 파이프라인 디렉터리 페이지에서 확인할 수 있습니다. 그리고 trigger 토큰이나 작업 토큰을 사용하여 API에서 생성된 경우에는 파이프라인 그래프에서 하류/상류 링크로도 액세스할 수 있습니다. 또한, 트리거 토큰을 사용하여 다른 파이프라인에서 생성할 수도 있지만, 이 경우에는 원본 파이프라인을 저장하지는 않습니다.

ci_builds를 분할함에 따라, 외래 키를 ci_sources_pipelines 테이블로 업데이트해야 합니다:

외래키 제약 조건:
    "fk_be5624bf37" 외래 키 (source_job_id) 참조: ci_builds(id) ON DELETE CASCADE
    "fk_d4e29af7d7" 외래 키 (source_pipeline_id) 참조: ci_pipelines(id) ON DELETE CASCADE
    "fk_e1bad85861" 외래 키 (pipeline_id) 참조: ci_pipelines(id) ON DELETE CASCADE

ci_sources_pipelines 레코드는 두 개의 ci_pipelines 행(부모 및 자식)을 참조합니다. 보통의 전략은 테이블에 partition_id를 추가하는 것인데, 여기서는 다중 프로젝트 파이프라인이 모두 동일한 파티션의 일부가 되도록 강제하지 않으려면 partition_id를 추가하면 됩니다.

따라서 이 테이블에는 두 개의 partition_id, 즉 partition_idsource_partition_id를 추가해야 합니다:

외래키 제약 조건:
    "fk_be5624bf37" 외래 키 (source_job_id, source_partition_id) 참조: ci_builds(id, source_partition_id) ON DELETE CASCADE
    "fk_d4e29af7d7" 외래 키 (source_pipeline_id, source_partition_id) 참조: ci_pipelines(id, source_partition_id) ON DELETE CASCADE
    "fk_e1bad85861" 외래 키 (pipeline_id, partition_id) 참조: ci_pipelines(id, partition_id) ON DELETE CASCADE

이 해결책은 이중 특정 문제의 가장 가까운 해결책입니다.

  • 다른 파티션의 파이프라인을 참조하는 능력을 유지합니다.
  • 나중에 다중 프로젝트 파이프라인을 동일한 파티션의 일부가 되도록 강제하려고 결정할 경우 두 열이 동일한 값을 갖는지 확인하는 제약 조건을 추가할 수 있습니다.

명시적 논리적 파티션 ID를 사용하는 이유

논리적 partition_id를 사용하여 CI/CD 데이터를 파티셔닝하면 여러 가지 이점이 있습니다. 주 키를 기준으로 파티셔닝할 수도 있지만, 이렇게 하면 데이터의 구조화와 저장 방식을 이해하는 데 훨씬 더 많은 복잡성과 추가 인지적 부담이 동반됩니다.

CI/CD 데이터는 계층적 데이터입니다. 단계는 파이프라인에 속하고, 빌드는 단계에 속하며, 아티팩트는 빌드에 속합니다(드물게는 예외도 있음). 이러한 계층 구조를 반영하는 파티셔닝 전략을 설계하여 기여자들의 복잡성 및 그에 따른 인지적 부담을 감소시킵니다. 파이프라인과 연관된 모든 리소스를 검색할 때 논리적 파티션 ID가 파이프라인과 연결되어 있으므로 파티션 ID 번호가 있으면 해당 파이프라인과 관련된 자원을 항상 찾을 수 있습니다. 이를 통해 어떤 파티션 테이블에 있는지 PostgreSQL이 알 수 있습니다.

파이프라인 데이터에 대해 단일 및 일관된 partition_id 값을 사용하는 것은 후에 주요 키를 기반으로 파티셔닝하는 것보다 더 많은 선택지를 제공합니다.

파티션 테이블 변경

파티션된 테이블에 대해 여전히 ALTER TABLE 문을 실행할 수 있을 것이며, 테이블이 파티셔닝되기 전과 유사하게 작동할 것입니다. PostgreSQL이 부모 파티셔닝된 테이블에 ALTER TABLE 문을 실행하면 모든 자식 파티션에 대해 동일한 잠금을 획들하고 그들을 동기화시킬 것입니다. 이는 비파티션 테이블에 대해 ALTER TABLE을 실행하는 것과는 몇 가지 중요한 차이가 있습니다:

  • PostgreSQL은 더 많은 테이블에 대해 ACCESS EXCLUSIVE 잠금을 획득하지만, 데이터 양은 증가하지 않을 것입니다. 각 파티션은 부모 테이블과 유사하게 잠길 것이며, 모든 파티션은 하나의 트랜잭션 내에서 동시에 업데이트될 것입니다.
  • 잠금 기간은 관련 파티션의 수에 따라 증가할 것입니다. 모든 ALTER TABLE 문은 작성된 테이블당 작은 상수 시간을 소비합니다. PostgreSQL은 각 파티션을 순차적으로 수정해야 하므로 잠금 시간이 증가할 것입니다. 그러나 많은 파티션이 있을 때까지 이 시간은 매우 작아질 것입니다.
  • 수천 개의 파티션이 ALTER TABLE에 참여하면 작업 중 잡아야 하는 잠금의 수를 지원할 max_locks_per_transaction의 값이 충분한지 확인해야 할 것입니다.

대규모 파티션을 작은 파티션으로 분할

초기 partition_id 번호는 100(또는 계산 및 예상에 따라 더 높은 수, 예를 들어 1000)로 시작하고자 합니다. 기존 테이블이 이미 크기 때문에 1부터 시작하는 것이 아니라 더 작은 파티션으로 테이블을 분할하고자 할 경우를 위해서입니다. 만약 100부터 시작하면 1, 20, 45에 대한 파티션을 만들고 기존 레코드를 partition_id를 더 작은 숫자로 업데이트하여 이동시킬 수 있을 것입니다.

PostgreSQL은 이러한 레코드를 일관된 방식으로 해당 파티션으로 이동시킬 것이며, 같은 시간에 동시에 이루어진 트랜잭션을 통해 모든 파이프라인 자원에 대해 이 작업을 실행합니다. 만약 대규모 파티션을 작은 파티션으로 분할해야 할지에 대한 결정은 아직 명확하지 않지만, 파티션 ID를 업데이트하기 위해 백그라운드 마이그레이션을 사용하고 PostgreSQL이 자체적으로 파티션 간의 행을 이동할 수 있을 것입니다.

명명 규칙

파티션된 테이블은 라우팅 테이블이라고 하며 p_ 접두사를 사용할 것입니다. 이것은 쿼리 분석을 자동화하기 위한 도구 구축에 도움이 될 것입니다.

테이블 파티션은 파티션으로 불릴 것이며 ci_builds_101과 같이 물리적 파티션 ID를 접미사로 사용할 수 있습니다. 기존 CI 테이블들은 새로운 라우팅 테이블의 제로 파티션이 될 것입니다. 주어진 테이블의 선택된 파티셔닝 전략에 따라 물리적 파티션 당 여러 개의 논리적 파티션을 가질 수 있습니다.

첫 번째 파티션을 연결하고 잠금 획득

저희는 테이블을 파티셔닝하는 첫 테이블을 파티셔닝할 때 PostgreSQL이 외래 키를 통해 참조하는 테이블에 AccessExclusiveLock을 요구한다는 것을 배웠습니다. 애플리케이션 비즈니스 로직에서 다른 순서로 잠금을 획득하려는 마이그레이션이 데드락을 발생시킬 수 있습니다.

이 문제를 해결하기 위해 우선순위 잠금 전략을 도입하여 더이상의 데드락 오류를 피할 수 있도록 하였습니다. 이를 통해 잠금 순서를 정의하고 잠금을 획득할 때까지 계속하여 재시도할 수 있도록 하였습니다. 이 프로세스는 최대 40분이 걸릴 수 있습니다.

우리는 이 전략을 사용하여 파티션 도구)를 통해 유지 규모의 낮은 트래픽 기간 (00:00 UTC 이후)에 시도하여 ci_builds 테이블의 잠금을 15회의 재시도 끝에 성공적으로 획득하였습니다.

이전에 이 전략에 대한 예시를 확인하세요.

파티션 단계

데이터베이스 파티션 도구 문서에는 테이블을 파티션으로 나누는 단계 디렉터리이 있지만, 이는 우리의 반복적인 전략에는 충분하지 않습니다. 데이터셋이 계속 성장함에 따라 우리는 모든 테이블이 파티션으로 나누어질 때까지 기다리지 않고 즉시 파티션화 성능을 활용하고 싶습니다. 예를 들어, ci_builds_metadata 테이블을 파티션화한 후에는 새로운 파티션으로 데이터를 쓰고 읽기를 시작하고 싶습니다. 이는 partition_id 값을 기본 값인 100에서 101로 증가시키는 것을 의미합니다. 이제 파이프라인 계층의 새로운 리소스는 모두 partition_id = 101로 유지됩니다. 다음으로 파티션화될 테이블에 대해 데이터베이스 도구 지침을 계속 따를 수 있지만 몇 가지 추가 단계가 필요합니다.

  • FK 참조에 partition_id 열을 추가하고 대부분의 레코드에는 기본 값인 100을 설정합니다.
  • 응용프로그램 논리를 변경하여 partition_id 값을 연쇄시킵니다.
  • 최근 레코드에 대한 partition_id 값을 수정하는 백그라운드 마이그레이션과 같은 후배포 마이그레이션을 수행합니다. 예를들어:

    UPDATE ci_pipeline_metadata
           SET partition_id = ci_pipelines.partition_id
           FROM ci_pipelines
                WHERE ci_pipelines.id = ci_pipeline_metadata.pipeline_id
                  AND ci_pipelines.partition_id in (101, 102);
    
  • 외래 키 정의 변경

데이터베이스에 파티션 메타데이터 저장

새로운 파티션을 생성하고 시간의 감쇠를 구현할 책임이 있는 효율적인 메커니즘을 구축하기 위해 파티션 메타데이터 테이블인 ci_partitions을 도입하고 싶습니다. 이 테이블에는 많은 파이프라인이 있는 모든 논리적인 파티션에 대한 메타데이터가 저장됩니다. 우리는 각 논리적 파티션에 대한 파이프라인 ID 범위를 저장해야 할 수도 있습니다. 이를 통해 특정 파이프라인 ID에 대한 partition_id 번호를 찾을 수 있고 “활성” 또는 “보관” 상태에 따른 논리적 파티션에 대한 정보를 얻을 수 있습니다. 이를 통해 데이터베이스 선언적 파티셔닝을 사용하여 시간의 감쇠 패턴을 구현하는 데 도움이 될 것입니다.

이렇게 함으로써, 파티션화된 리소스에 대한 통합된 리소스 식별자를 사용하여 파티션화된 리소스를 효율적으로 조회할 수 있는 것이 가능할 것입니다. 이는 UI 또는 API에서 URL로 직접 참조될 수 있는 경우 중요할 수 있습니다. 예를들어, 파이프라인 123456, 빌드 23456에 대해 1e240-5ba0와 같은 ID를 사용할 수 있을 것입니다. 하이픈 -을 사용하면 식별자가 마우스 더블 클릭으로 하이라이트되고 복사되는 것을 방지할 수 있습니다. 이 문제를 피하고 싶다면, 16진수 체계에 없는 문자로 쓰여진 표현 중 어떤 것이나 사용할 수 있습니다. 예를들어 라틴 문자 알파벳에서 g에서 z까지의 아무 글자를 사용할 수 있습니다. 이 경우, URI의 예는 1e240x5ba0과 같을 것입니다. 파티션화된 리소스의 기본 식별자를 업데이트하기로 결정한다면 (현재 이는 큰 정수일 뿐입니다) 리밸런싱이 발생할 때 식별자를 변경하지 않도록 하는 중요한 시스템을 설계하는 것이 중요합니다.

ci_partitions 테이블은 파티션 식별자, 해당 논리적 파티션에 유효한 파이프라인 ID 범위 및 파티션이 보관되었는지 여부에 대한 정보를 저장할 것입니다. 타임스탬프가 있는 추가 열도 도움이 될 수 있습니다.

선언적 파티셔닝을 사용하여 시간의 감쇠 패턴 구현

ci_partitions를 사용하여 선언적 파티셔닝을 사용하여 시간의 감쇠 패턴을 구현할 수 있습니다. PostgreSQL에게 보관된 논리적 파티션을 알려줌으로써 우리는 다음과 같은 SQL 쿼리를 사용하여 이러한 파티션에서 읽기를 중지할 수 있을 것입니다.

SELECT * FROM ci_builds WHERE partition_id IN (
  SELECT id FROM ci_partitions WHERE active = true
);

이 쿼리를 사용함으로써 우리는 읽을 파티션의 수를 제한할 수 있게 되며, 따라서 CI/CD 데이터의 데이터 보관 정책에 따라 “보관된” 파이프라인 데이터에 대한 접근을 제한할 수 있을 것입니다. 이상적으로 우리는 한 번에 두 개 이상의 파티션에서 읽지 않기를 원하기 때문에 자동 파티셔닝 메커니즘을 시간의 감쇠 정책과 일치시키는 필요가 있습니다. 보관된 데이터에 대한 새로운 접근 방식을 구현해야 할 필요가 있지만, PostgreSQL에 보관된 데이터를 저장하는 비용은 크게 줄일 수 있습니다.

여기에는 여기에는 이 설명의 범위를 벗어나는 몇 가지 기술적인 세부 정보가 있습니다. 그러나 이러한 전략을 사용함으로써 우리는 데이터를 “보관”함으로써 PostgreSQL 클러스터에 머무르는 비용을 크게 줄일 수 있게 될 것입니다.

파티션화된 데이터에 접근

대부분의 경우에는 보관되었는지 여부에 관계없이 파티션화된 데이터에 접근할 수 있을 것입니다. Merge Request 페이지에서는 Merge Request이 여러 년 전에 만들어졌더라도 항상 파이프라인 세부 정보를 표시할 것입니다. 이는 ci_partitions가 파이프라인 ID와 해당 partition_id를 연결하는 조회 테이블로 사용될 것이며, 파이프라인 데이터가 저장된 파티션을 찾을 수 있게 되는 것입니다.

파이프라인, 빌드, 아티팩트 등을 검색하는 것은 모든 파티션을 통해 수행할 수 없기 때문에 특정 제약이 필요하며 보관된 파이프라인 데이터를 검색하는 더 나은 방법을 찾아야 합니다. UI 및 API에서 보관된 데이터에 액세스하는 다른 액세스 패턴이 필요할 것입니다.

PostgreSQL에서 partition_id 파티셔닝 키의 사용을 강제하는 데 일부 기술적인 도전 과제가 있습니다. 이에 대한 우리의 응용프로그램을 업데이트하는 것을 더 쉽게하기 위해 우리는 우리의 증명된 컨셉 Merge Request에서 새로운 쿼리 분석기를 설계했습니다.

별도의 증명된 컨셉 Merge Request관련 이슈을 통해 일치하는 partition_id를 사용하면 추가적인 스코프 수정자를 사용하여 Rails 연결을 확장할 수 있게 되는데, 이로써 SQL 쿼리에 파티션 키를 제공할 수 있게 됩니다.

이 접근법의 문제점은 사전로드를 훨씬 더 어렵게 만든다는 것입니다. 이에서 인스턴스 의존적인 연결은 사전로드와 함께 사용될 수 없기 때문에 사전로드가 훨씬 더 어려워집니다.

쿼리 분석기

우리는 파티션된 테이블과 함께 잘 작동하도록 수정되어야 하는 쿼리를 감지하기 위해 2개의 쿼리 분석기를 구현했습니다.

  • 라우팅 테이블을 거치지 않고 직접 쿼리하는 것을 감지하는 분석기
  • WHERE 절에서 partition_id를 지정하지 않고 라우팅 테이블을 사용하는 쿼리를 감지하는 분석기

우리는 먼저 기존에 문제가 있는 쿼리를 감지하기 위해 첫 번째 분석기를 test 환경에서 활성화했습니다. 확장 가능성과 관련하여 production 환경에서는 일부 트래픽(0.1%)을 위해서만 가능합니다.

두 번째 분석기는 나중에 활성화될 것입니다.

기본 키

테이블을 파티션화하려면 기본 키에 파티션 키 열을 포함해야 합니다.

먼저 (id, partition_id)를 포함하는 고유 인덱스를 생성합니다. 그런 다음 기본 키 제약을 삭제하고 새로 생성된 인덱스를 사용하여 새로운 기본 키 제약을 설정합니다.

ActiveRecord composite primary keys를 지원하지 않습니다 따라서 ‘id’ 열을 기본 키로써 대우하도록 강제해야 합니다.

class Model < ApplicationRecord
  self.primary_key = 'id'
end

응용프로그램 레이어는 이제 데이터베이스 구조를 알지 못하게 되며 ActiveRecord에서 가져온 모든 기존 쿼리는 여전히 데이터에 액세스하기 위해 id 열을 사용합니다. 이러한 접근법에는 id 값을 채우기 위해 데이터베이스 시퀀스를 사용하도록 보장하고 액세스 패턴을 partition_id 값을 포함하도록 다시 쓰는 것을 포함하여 partition_id 값을 포함하는 것으로 다시 쓴 액세스 패턴을 보장하기 위한 일부 위험이 있습니다. 입력 중 id를 직접 할당하는 것은 피해야 합니다.

외부 키

외부 키는 주요 키이거나 고유 제약 조건을 형성하는 열을 참조해야 합니다. 이러한 전략을 사용하여 그것들을 정의할 수 있습니다.

파티션 ID를 공유하는 라우팅 테이블 사이의 관계

동일한 파이프라인 계층 구조의 관계에서는 partition_id 열을 공유하여 외래 키 제약 조건을 정의할 수 있습니다.

p_ci_pipelines:
 - id
 - partition_id

p_ci_builds:
 - id
 - partition_id
 - pipeline_id

이 경우, p_ci_builds.partition_id는 빌드와 파이프라인의 파티션을 나타내며, 라우팅 테이블에 FK를 추가할 수 있습니다.

ALTER TABLE ONLY p_ci_builds
    ADD CONSTRAINT fk_on_pipeline_and_partition
    FOREIGN KEY (pipeline_id, partition_id)
    REFERENCES p_ci_pipelines(id, partition_id) ON DELETE CASCADE;

다른 파티션 ID를 가진 라우팅 테이블 사이의 관계

모든 CI 도메인의 관계에 partition_id를 재사용하는 것은 불가능하므로, 이 경우 값을 다른 속성으로 저장해야 합니다. 예를 들어, 중복 파이프라인을 취소할 때 우리는 취소된 파이프라인의 ID를 auto_canceled_by_id로 저장합니다.

p_ci_pipelines:
 - id
 - partition_id
 - auto_canceled_by_id
 - auto_canceled_by_partition_id

이 경우에는 취소하는 파이프라인이 취소된 파이프라인과 동일한 계층에 속해 있는지를 보증할 수 없으므로, 해당 파티션을 저장하기 위해 추가적인 속성이 필요하며, FK는 다음과 같이 됩니다.

ALTER TABLE ONLY p_ci_pipelines
    ADD CONSTRAINT fk_cancel_redundant_pipelines
    FOREIGN KEY (auto_canceled_by_id, auto_canceled_by_partition_id)
    REFERENCES p_ci_pipelines(id, partition_id) ON DELETE SET NULL;

라우팅 테이블 및 일반 테이블 간의 관계

CI 도메인의 모든 테이블이 파티션화되는 것은 아니기 때문에 라우팅 테이블을 가져와야 할 것입니다. 예를 들어, ci_pipelines에서 external_pull_requests를 참조할 수 있습니다.

FOREIGN KEY (external_pull_request_id)
REFERENCES external_pull_requests(id)
ON DELETE SET NULL

이 경우 새로운 파이프라인 파티션에서 사용할 수 있도록 파티션 수준에서 FK 정의만 이동하면 됩니다.

ALTER TABLE p_ci_pipelines
  ADD CONSTRAINT fk_external_request
  FOREIGN KEY (external_pull_request_id)
  REFERENCES external_pull_requests(id) ON DELETE SET NULL;

일반 테이블 및 라우팅 테이블 간의 관계

CI 도메인의 대부분의 테이블은 최소한 하나의 라우팅 테이블을 참조할 것입니다. 예를 들어, ci_pipeline_messagesci_pipelines를 참조할 것입니다. 이러한 정의들은 라우팅 테이블을 사용하도록 업데이트해야 하며, 이를 위해서는 partition_id 열이 필요합니다.

p_ci_pipelines:
 - id
 - partition_id

ci_pipeline_messages:
 - id
 - pipeline_id
 - pipeline_partition_id

다음을 사용하여 외래 키를 정의할 수 있습니다.

ALTER TABLE ci_pipeline_messages ADD CONSTRAINT fk_pipeline_partitioned
  FOREIGN KEY (pipeline_id, pipeline_partition_id)
  REFERENCES p_ci_pipelines(id, partition_id) ON DELETE CASCADE;

구식 FK 정의를 제거해야 하며, 그렇지 않으면 ci_pipeline_messages에 새로운 삽입을 시도할 때 파티션이 0이 아닌 파이프라인 ID를 사용하면 참조 오류가 발생합니다.

인덱스

우리는 PostgreSQL을 통해 단일 인덱스(고유 또는 그 외)를 테이블의 모든 파티션에 걸쳐 생성할 수 없다는 것을 배웠습니다.

이 문제를 해결하는 한 가지 방법은 고유성 제약 조건 내에 파티션 키를 포함하는 것입니다.

이는 앞으로 더 많은 파티션을 위해 충분한 수의 선행 바이트를 예약해야 함을 의미하는데, 16 진수의 형태에서 최대 파티션 번호에 대한 고려대상을 수용하기 위한 것입니다. 이 방법으로 인코딩할 수 있는 최대 숫자는 16진법으로 65535인 FFFF이라는 것입니다.

이는 전역적으로 고유한 제약 조건을 제공하며 충분합니다.

또한 우리는 직접적으로 제로 파티션을 사용하지 않는 쿼리, 라우팅 테이블에 첫 번째 파티션으로 첨부된 구식 테이블 등을 감지하도록 하는 쿼리 분석기를 설계했습니다. 이를 통해 모든 쿼리가 파티션화된 스키마나 파티션화된 라우팅 테이블(예: p_ci_pipelines)을 타겟팅하고 있는지 확인할 수 있습니다.

프로젝트 또는 네임스페이스 ID를 사용하여 파티션하는 이유

우리는 project_id 또는 namespace_id를 사용하여 파티션을 나누고 싶어하지 않습니다. 샤딩 및 pod팅은 응용 프로그램의 다른 계층에서 해결해야 할 다른 문제입니다. 이는 문제 상 성능이 시간이 지남에 따라 점점 나빠지는 원래 문제 설명을 해결하지 않습니다. 우리는 미래에 pod들을 도입하고, 그것이 데이터가 연관된 그룹이나 프로젝트를 기준으로 데이터를 분리하는 주요 메커니즘이 될 수 있다.

이론적으로 project_id 또는 namespace_id 중 하나를 두 번째 파티션 차원으로 사용할 수 있지만, 이미 매우 복잡한 문제에 더 많은 복잡성을 추가할 수 있습니다.

빌드 대기열 테이블 파티션

빌드 대기열 테이블도 파티션화하고 싶습니다. 현재 우리에게는 ci_pending_buildsci_running_builds가 있습니다. 이러한 테이블은 다른 CI/CD 데이터 테이블과 다릅니다. 왜냐하면 제품에서 이러한 테이블에 저장된 모든 데이터가 24시간 후에 무효화되도록 하는 비즈니스 규칙이 있기 때문입니다.

결과적으로 이러한 데이터 테이블을 파티션하기 위한 다른 전략이 필요할 것입니다. 이를 위해서는 이러한 파티션을 24시간 후에 완전히 제거하고, 항상 라우팅 테이블을 통해 두 파티션에서 항상 읽을 수 있어야 합니다. 이러한 테이블을 파티션하기 위한 전략은 잘 이해되어 있지만, 이러한 파티션의 생성 및 삭제를 관리하기 위한 견고한 Ruby 기반 자동화가 필요합니다. 이를 달성하기 위해 기존 데이터베이스 파티셔닝 도구들을 CI/CD 데이터 파티셔닝을 지원하도록 조정하기 위해 데이터베이스 팀과 협력할 예정입니다.

리스크 감소를 위한 반복

이러한 전략은 CI/CD 파티셔닝을 실행하는 리스크를 수용할 수 있는 수준으로 감소시키는데 도움이 될 것입니다. 우리는 초기에 두 파티션에서만 읽는 것에 중점을 두어 이 문제를 해결가능하도록 할 것입니다. 만약 우리의 프로덕션 환경에서 문제가 발생할 경우 제로 파티션을 분리할 수 있도록 하기 위해 각 반복 단계에는 되돌리기 전략이 있으며, 데이터베이스 변경 사항을 배포하기 전에 벤치마킹 환경에서 테스트하고자 합니다.

이러한 노력의 경우 리스크를 줄이는 주요 방법은 반복과 되돌리기가 가능하도록 하는 것입니다. 이 문서에 설명된 변경 내용은 안전하고 신뢰성 있는 방식으로 배포되는 것이 우리의 우선 순위입니다.

이러한 구현을 전개함에 따라 더 많은 디자인 상의 반복, 점진적인 배포를 지원하고 문제가 발생할 경우 변경을 되돌리는 더 나은 방법을 찾아야 할 것입니다. 때로는 데이터베이스 스키마 변경을 반복적으로 배포하는 것은 리스크를 줄이는 방법입니다. 또한 프로덕션 환경에 점진적인 배포를 지원하기는 더 어려우며, 때로는 추가적인 창의력이 필요할 수 있지만, 여기에서 그것이 분명히 필요할 것입니다.

파티션화된 스키마의 점진적 배포

최초의 파티션화된 라우팅 테이블(아마도 p_ci_pipelines)을 도입하고 그것의 제로 파티션(ci_pipelines)을 연결한 후, 우리는 일반적으로 새 라우팅 테이블과 상호작용을 시작해야 합니다. 보통은 Ci::Pipeline 레일 모델이 후 24시간이 지난 구식 데이터를 무효화되도록 하는 가장 빠른 방법으로 self.table_name = 'p_ci_pipelines'와 같이 뭔가를 오버라이드할 것입니다. 불행히도 이 접근 방식은 대개 애플리케이션 부팅시 self.table_name이 읽히며 이후에 앱을 재시작하지 않고는 이러한 변화를 되돌릴 수 없다는 점에서 점진적인 배포를 지원하지 못할 수 있습니다.

이를 해결하는 한 가지 방법은 Ci::Partitioned::Pipeline 모델을 도입하는 것입니다. 이 모델은 Ci::Pipeline을 상속받을 것이며 self.table_namep_ci_pipeline로 설정하고 메타 클래스를 Ci::Pipeline.partitioned로 반환할 것입니다. 이를 통해 ci_pipelines에서 p_ci_pipelines로의 읽기를 피처 플래그를 사용하여 경로의 방법을 간단하게 돌릴 수 있게 됩니다.

파티션화된 읽기로 점진적인 실험

다른 예로는 다른 파티션을 추가하기로 결정하는 시점과 관련된 시간일 수 있습니다. Phase 1의 목표는 파티션화된 스키마/라우팅 테이블 당 두 개의 파티션을 갖도록 하는 것입니다. 즉, p_ci_pipelines의 경우 ci_pipelines는 0번 파티션에 연결되고 새로운 데이터를 위해 ci_pipelines_p1 파티션이 생성됩니다. p_ci_pipelines의 모든 읽기는 p1 파티션에서의 데이터도 읽어야 하며 파티션을 여러 개 타깃으로 하는 읽기에 대한 성능과 부담을 평가하기 위해 반복적으로 실험해야 합니다.

이를 위해 우리는 이전 데이터를 반복적으로 ci_pipelines_m1 (마이너스 1) 파티션으로 이동시킬 수 있습니다. 아마도 우리는 partition_id = 1을 생성하고 매우 오래된 파이프라인을 거기에 이동시킬 것입니다. 그런 다음 m1 파티션으로 데이터를 반복적으로 이전하여 영향과 성능을 메트릭하고 새 파티션 p1을 만들기 전에 우리의 확신을 더 높일 수 있을 것입니다.

반복

우리는 먼저 Phase 1 반복에 중점을 두고 싶습니다. 이 반복의 목표와 주요 목적은 가장 큰 6개의 CI/CD 데이터베이스 테이블을 6개의 라우팅 테이블(파티션화된 스키마)과 12개의 파티션으로 나누는 것입니다. 이로써 우리의 Rails SQL 쿼리는 대부분 변경되지 않을 것입니다. 그러나 데이터베이스 성능 저하가 발생했을 때 “제로 파티션”을 긴급 분리할 수 있게 만들 것입니다. 이것은 사용자들이 이전 데이터에 접근할 수 없게 할 것입니다. 하지만 애플리케이션은 계속해서 실행되므로 애플리케이션 전체의 중단보다 나은 대안이 될 것입니다.

  1. Phase 0: CI/CD 데이터 파티션화 전략 구축: 완료. ✅
  2. Phase 1: 가장 큰 6개의 CI/CD 데이터베이스 테이블을 파티션화.
    1. 6개 데이터베이스 테이블에 대해 파티션화 스키마 생성
    2. partition_id를 모든 파티션화된 리소스에 연쇄적으로 적용할 방법 설계
    3. 라우팅 테이블을 타깃으로 하는 초기 쿼리 분석기 구현
    4. 파티션화된 데이터베이스 테이블에 0번 파티션 연결
    5. 애플리케이션을 라우팅 테이블과 파티션화된 테이블을 타깃으로 지정하도록 업데이트
    6. 이 솔루션의 성능과 효율성 메트릭

    되돌림 전략: 라우팅 테이블 대신 구체적인 파티션 사용으로 전환.

  3. Phase 2: 파티션화 테이블을 대상으로 SQL 쿼리에 파티션 키 추가
    1. 파티션화된 테이블을 대상으로 하는 쿼리를 확인하기 위한 쿼리 분석기 구현
    2. 모든 기존 쿼리가 필터로서 파티션 키를 사용하도록 기존 쿼리 수정

    되돌림 전략: 피처 플래그, 쿼리 별로.

  4. Phase 3: 새로운 파티션화된 데이터 액세스 패턴 구축
    1. 새 API 구축하거나 기존 API 확장하여 시간 감쇠 데이터 유지 정책을 기반으로 제외해야 할 파티션에 저장된 데이터에 액세스할 수 있는 기능을 추가

    되돌림 전략: 피처 플래그.

  5. Phase 4: 파티션화된 메커니즘 위에 시간 감쇠 메커니즘 도입
    1. 시간 감쇠 정책 메커니즘 구축
    2. GitLab.com에서 시간 감쇠 전략 활성화
  6. Phase 5: 파티션을 자동으로 생성하는 메커니즘 도입
    1. 파티션을 자동으로 생성할 수 있도록 함
    2. 새 아키텍처를 Self-Managed형 인스턴스에 전달

아래 다이어그램은 Gantt 차트 상의 이 계획을 시각화합니다. 아래 차트의 날짜는 계획을 더 잘 시각화하기 위한 예상치일 뿐이며, 이는 마감일이 아니며 언제든지 변경될 수 있습니다.

gantt title CI 데이터 파티셔닝 시간표 dateFormat YYYY-MM-DD axisFormat %m-%y section Phase 0 데이터 파티셔닝 전략 구축 :done, 0_1, 2022-06-01, 90일 section Phase 1 가장 큰 CI 테이블 파티션화 :1_1, after 0_1, 200일 가장 큰 테이블 파티션화 :milestone, metadata, 2023-03-01, 1일 100GB 이상의 테이블 파티션화 :milestone, 100gb, after 1_1, 1일 section Phase 2 SQL 쿼리에 파티션 키 추가 :2_1, 2023-01-01, 120일 긴급 파티션 분리 가능 :milestone, detachment, 2023-04-01, 1일 모든 SQL 쿼리가 파티션으로 라우팅 :milestone, routing, after 2_1, 1일 section Phase 3 새로운 데이터 액세스 패턴 구축 :3_1, 2023-05-01, 120일 기존 API 엔드포인트에 비활성 데이터용 새 API 엔드포인트 생성 :milestone, api1, 2023-07-01, 1일 기존 API 엔드포인트에 필터 추가 :milestone, api2, 2023-09-01, 1일 section Phase 4 시간 감쇠 메커니즘 도입 :4_1, 2023-08-01, 120일 비활성 파티션은 읽히지 않음 :milestone, part1, 2023-10-01, 1일 데이터베이스 클러스터 성능 향상 :milestone, part2, 2023-11-01, 1일 section Phase 5 자동 파티셔닝 메커니즘 도입 :5_1, 2023-09-01, 120일 파티션 자동 생성 기능 도입 :milestone, part3, 2023-12-01, 1일 파티셔닝이 Self-Managed형에 이용 가능하게 됨 :milestone, part4, 2024-01-01, 1일

결론

CI/CD 데이터의 파티션화에 대한 견고한 전략을 구축하려고 합니다. 멀티테라바이트의 PostgreSQL 인스턴스의 데이터베이스 스키마를 잘못 관리하면 잠재적인 다운타임 없이는 쉽게 되돌릴 수 없는 실수가 있을 수 있다는 사실을 알고 있습니다. 이것은 우리가 파티션화 전략을 연구하고 정제하는 데 상당한 시간을 투자하고 있는 이유입니다. 또한 이 문서에 기술된 전략 또한 반복의 대상입니다. 더 나은 방법을 찾으면서 리스크를 줄이고 계획을 개선할 수 있는 경우에는 이 문서를 업데이트해야 합니다.

대규모 데이터 이관을 피하는 방법을 찾았으며, CI/CD 데이터 파티션화에 대한 반복적인 전략을 구축하고 있습니다. 이곳에 우리의 전략을 문서화하여 지식을 공유하고 다른 팀 구성원들로부터 피드백을 구하고자 합니다.

담당자

DRIs:

역할 담당자
저자 Grzegorz Bizon, 주요 엔지니어
추천자 Kamil Trzciński, 시니어 차별화된 엔지니어
제품 리더십 Jackie Porter, 제품 관리 이사
엔지니어링 리더십 Caroline Simpson, 엔지니어링 매니저 / Cheryl Li, 시니어 엔지니어링 매니저
선임 엔지니어 Marius Bobin, 시니어 백엔드 엔지니어
선임 엔지니어 Maxime Orefice, 시니어 백엔드 엔지니어
선임 엔지니어 Tianwen Chen, 시니어 백엔드 엔지니어