Status | Authors | Coach | DRIs | Owning Stage | Created |
---|---|---|---|---|---|
ongoing |
@grzesiek
|
@ayufan
@grzesiek
|
@jreporter
@cheryl.li
| devops verify | 2022-05-31 |
- 어떤 문제를 해결하려고 하는가?
- CI/CD 데이터 분해, 파티셔닝 및 시간 감쇠는 어떤 관련이 있나요?
- 왜 CI/CD 데이터를 파티션해야 하는가?
- 어떻게 CI/CD 데이터를 파티션하고자 하는가?
- 명시적 논리적 파티션 ID를 사용하는 이유
- 파티션 테이블 변경
- 대규모 파티션을 작은 파티션으로 분할
- 데이터베이스에 파티션 메타데이터 저장
- 선언적 파티셔닝을 사용하여 시간의 감쇠 패턴 구현
- 파티션화된 데이터에 접근
- 프로젝트 또는 네임스페이스 ID를 사용하여 파티션하는 이유
- 빌드 대기열 테이블 파티션
- 리스크 감소를 위한 반복
- 반복
- 결론
- 담당자
파이프라인 데이터 파티셔닝 설계
어떤 문제를 해결하려고 하는가?
우리는 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 데이터베이스 관련 프로덕션 환경 사고가 많이 발생했습니다. 예를 들어:
- S1: 2022-03-17
ci_builds
테이블의 쓰기 증가 - S1: 2021-11-22
ci_job_artifacts
의 레플리카에서 과도한 버퍼 읽기 - S2: 2022-04-12 10분 이상 실행된 트랜잭션 탐지
- S2: 2022-04-06 과도한
ci_builds
읽기로 인한 데이터베이스 경합 가능성 - S2: 2022-03-18
ci_builds
에서 외래 키를 제거할 수 없음 - S2: 2022-10-10
queuing_queries_duration
SLI apdex 위반 SLO
우리에게는 대략 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
열을 DEFAULT
값 100
또는 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_id
와 source_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_messages
는 ci_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_builds
와 ci_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_name
을 p_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 쿼리는 대부분 변경되지 않을 것입니다. 그러나 데이터베이스 성능 저하가 발생했을 때 “제로 파티션”을 긴급 분리할 수 있게 만들 것입니다. 이것은 사용자들이 이전 데이터에 접근할 수 없게 할 것입니다. 하지만 애플리케이션은 계속해서 실행되므로 애플리케이션 전체의 중단보다 나은 대안이 될 것입니다.
- Phase 0: CI/CD 데이터 파티션화 전략 구축: 완료. ✅
-
Phase 1: 가장 큰 6개의 CI/CD 데이터베이스 테이블을 파티션화.
- 6개 데이터베이스 테이블에 대해 파티션화 스키마 생성
-
partition_id
를 모든 파티션화된 리소스에 연쇄적으로 적용할 방법 설계 - 라우팅 테이블을 타깃으로 하는 초기 쿼리 분석기 구현
- 파티션화된 데이터베이스 테이블에 0번 파티션 연결
- 애플리케이션을 라우팅 테이블과 파티션화된 테이블을 타깃으로 지정하도록 업데이트
- 이 솔루션의 성능과 효율성 메트릭
되돌림 전략: 라우팅 테이블 대신 구체적인 파티션 사용으로 전환.
-
Phase 2: 파티션화 테이블을 대상으로 SQL 쿼리에 파티션 키 추가
- 파티션화된 테이블을 대상으로 하는 쿼리를 확인하기 위한 쿼리 분석기 구현
- 모든 기존 쿼리가 필터로서 파티션 키를 사용하도록 기존 쿼리 수정
되돌림 전략: 피처 플래그, 쿼리 별로.
-
Phase 3: 새로운 파티션화된 데이터 액세스 패턴 구축
- 새 API 구축하거나 기존 API 확장하여 시간 감쇠 데이터 유지 정책을 기반으로 제외해야 할 파티션에 저장된 데이터에 액세스할 수 있는 기능을 추가
되돌림 전략: 피처 플래그.
-
Phase 4: 파티션화된 메커니즘 위에 시간 감쇠 메커니즘 도입
- 시간 감쇠 정책 메커니즘 구축
- GitLab.com에서 시간 감쇠 전략 활성화
-
Phase 5: 파티션을 자동으로 생성하는 메커니즘 도입
- 파티션을 자동으로 생성할 수 있도록 함
- 새 아키텍처를 Self-Managed형 인스턴스에 전달
아래 다이어그램은 Gantt 차트 상의 이 계획을 시각화합니다. 아래 차트의 날짜는 계획을 더 잘 시각화하기 위한 예상치일 뿐이며, 이는 마감일이 아니며 언제든지 변경될 수 있습니다.
결론
CI/CD 데이터의 파티션화에 대한 견고한 전략을 구축하려고 합니다. 멀티테라바이트의 PostgreSQL 인스턴스의 데이터베이스 스키마를 잘못 관리하면 잠재적인 다운타임 없이는 쉽게 되돌릴 수 없는 실수가 있을 수 있다는 사실을 알고 있습니다. 이것은 우리가 파티션화 전략을 연구하고 정제하는 데 상당한 시간을 투자하고 있는 이유입니다. 또한 이 문서에 기술된 전략 또한 반복의 대상입니다. 더 나은 방법을 찾으면서 리스크를 줄이고 계획을 개선할 수 있는 경우에는 이 문서를 업데이트해야 합니다.
대규모 데이터 이관을 피하는 방법을 찾았으며, CI/CD 데이터 파티션화에 대한 반복적인 전략을 구축하고 있습니다. 이곳에 우리의 전략을 문서화하여 지식을 공유하고 다른 팀 구성원들로부터 피드백을 구하고자 합니다.
담당자
DRIs:
역할 | 담당자 |
---|---|
저자 | Grzegorz Bizon, 주요 엔지니어 |
추천자 | Kamil Trzciński, 시니어 차별화된 엔지니어 |
제품 리더십 | Jackie Porter, 제품 관리 이사 |
엔지니어링 리더십 | Caroline Simpson, 엔지니어링 매니저 / Cheryl Li, 시니어 엔지니어링 매니저 |
선임 엔지니어 | Marius Bobin, 시니어 백엔드 엔지니어 |
선임 엔지니어 | Maxime Orefice, 시니어 백엔드 엔지니어 |
선임 엔지니어 | Tianwen Chen, 시니어 백엔드 엔지니어 |