Status | Authors | Coach | DRIs | Owning Stage | Created |
---|---|---|---|---|---|
proposed | - |
읽기 전용 데이터
이 문서는 Database Scalability Working Group에서 소개된 read-mostly 패턴에 대해 설명합니다. read-mostly 데이터의 특성과 GitLab 개발 시 고려해야 할 모범 사례에 대해 논의합니다.
읽기 전용 데이터의 특성
이름에서 이미 암시하듯이 read-mostly 데이터는 갱신보다 훨씬 자주 읽히는 데이터에 관한 것입니다. 이 데이터를 갱신, 삽입 또는 삭제로 작성하는 것은 읽기에 비해 매우 드물다는 것입니다.
또한, 이 맥락에서의 read-mostly 데이터는 일반적으로 소규모 데이터 세트입니다. 여기에서는 대규모 데이터 세트를 다루지 않지만, 대규모 데이터 세트도 “한 번 씩 작성하고 자주 읽기” 특성을 갖고 있을 수 있습니다.
예: 라이선스 데이터
라이선스 데이터를 전형적인 예시로 들어볼까요? GitLab의 라이선스는 GitLab 엔터프라이즈 기능을 사용하기 위한 라이선스가 포함될 수 있습니다. 이 라이선스 데이터는 인스턴스 전역에 보관되며 관련 레코드는 일반적으로 몇 개뿐입니다. 이 정보는 매우 소규모인 licenses
테이블에 저장됩니다.
우리는 이것을 read-mostly 데이터로 간주합니다. 왜냐하면 위에서 설명한 특성을 따르기 때문입니다:
- 드물게 쓰기: 라이선스 데이터는 라이선스를 추가한 후 거의 쓰여지지 않습니다.
- 자주 읽기: 라이선스 데이터는 엔터프라이즈 기능을 사용할 수 있는지 확인하기 위해 매우 자주 읽힙니다.
- 소규모: 이 데이터 세트는 매우 소형입니다. GitLab.com에서는 총 관련 크기가 50kB 이하인 5개의 레코드를 보유하고 있습니다.
규모별 read-mostly 데이터의 영향
이 데이터 세트가 소규모이며 자주 읽힌다고 가정할 때, 데이터가 거의 항상 데이터베이스 캐시나/또는 데이터베이스 디스크 캐시에 남아있을 것으로 기대할 수 있습니다. 따라서 read-mostly 데이터에 대한 우려는 일반적으로 데이터베이스 I/O 오버헤드 주변에 있지 않습니다. 왜냐하면 우리는 일반적으로 디스크에서 데이터를 읽지 않기 때문입니다.
그러나 높은 빈도의 읽기를 고려할 때, 이는 데이터베이스 CPU 부하와 데이터베이스 컨텍스트 전환 면에서 부하를 초래할 수 있습니다. 게다가, 이러한 빈도가 높은 쿼리는 전체 데이터베이스 스택을 통과합니다. 이러한 쿼리는 또한 데이터베이스 연결 다중화 컴포넌트와 로드 밸런서에 부하를 주게 됩니다. 또한, 응용 프로그램은 데이터를 검색하고, 결과를 역직렬화하며, 정보를 나타내는 새로운 객체를 할당하는 데 주기적으로 시간을 소비하게 됩니다.
위 라이선스 데이터 예시에서 라이선스 데이터를 읽는 쿼리는 최고 피크 시에 클러스터에서 초당 약 6,000개의 쿼리(QPS)를 확인했습니다. 그 당시 클러스터 크기에 따라, 각 복제본에서 약 1,000개의 QPS, 주 본 복제본에서 피크 시에 400개 미만의 QPS를 관측했습니다. 이 차이는 읽기 전용 트랜잭션의 확장을 위한 데이터베이스 로드 밸런싱 때문입니다.
당시 데이터베이스 주 본에서의 전반적인 트랜잭션 처리량은 50,000에서 70,000 TPS(초당 트랜잭션) 범위 내에서 변동했습니다. 비교적으로, 이 쿼리 빈도는 전체 쿼리 빈도의 일부에 불과했습니다. 그러나 우리는 이것이 여전히 컨텍스트 전환 면에서 상당한 부하를 가질 것으로 예상합니다. 우리가 할 수 있다면, 이러한 부하를 제거하는 것이 가치가 있습니다.
읽기 전용 데이터 인식 방법
예시와 같이 명확한 경우를 제외하고는 read-mostly 데이터를 인식하는 것이 어려울 수 있습니다.
한 가지 방법은 예를 들어 주 본에서의 읽기/쓰기 비율 및 통계를 살펴보는 것입니다. 여기서 우리는 쓰기/읽기 비율이 60분 동안 TOP20 테이블을 살펴봅니다(피크 트래픽 시간에 메트릭됨):
bottomk(20,
avg by (relname, fqdn) (
(
rate(pg_stat_user_tables_seq_tup_read{env="gprd"}[1h])
+
rate(pg_stat_user_tables_idx_tup_fetch{env="gprd"}[1h])
) /
(
rate(pg_stat_user_tables_seq_tup_read{env="gprd"}[1h])
+ rate(pg_stat_user_tables_idx_tup_fetch{env="gprd"}[1h])
+ rate(pg_stat_user_tables_n_tup_ins{env="gprd"}[1h])
+ rate(pg_stat_user_tables_n_tup_upd{env="gprd"}[1h])
+ rate(pg_stat_user_tables_n_tup_del{env="gprd"}[1h])
)
) and on (fqdn) (pg_replication_is_replica == 0)
)
이것은 주 본 데이터베이스에서 쓰기보다 훨씬 자주 읽히는 테이블을 잘 보여줍니다:
여기서 예를 들어 gitlab_subscriptions
를 확대하여 인덱스 읽기가 전체적으로 10,000개 이상의 튜플이 피크를 기록했음을 알 수 있습니다(시퀀스 스캔이 없습니다):
우리는 아주 드물게 테이블에 쓰기를 합니다(시퀀스 스캔이 없습니다):
게다가, 테이블 크기가 400 MB에 불과하기 때문에 이것 또한 이런 패턴을 고려하고 싶은 후보 중 하나일 수 있습니다(see #327483).
규모에 맞는 읽기 전용 데이터 처리를 위한 모범 사례
읽기 전용 데이터 캐시
데이터베이스 부하를 줄이기 위해 데이터에 대한 캐시를 구현하여 데이터베이스에서 쿼리 빈도를 크게 줄입니다. 사용 가능한 캐싱의 다양한 범위가 있습니다:
-
RequestStore
: 요청별 인메모리 캐시 (based onrequest_store
gem) -
ProcessMemoryCache
: 프로세스별 인메모리 캐시 (aActiveSupport::Cache::MemoryStore
) -
Gitlab::Redis::Cache
및Rails.cache
: Redis에서 완전한 캐시
위의 예시를 계속해서, 우리는 라이선스 정보를 요청당 캐시하는 RequestStore
가 있었습니다. 그러나 그것은 여전히 요청당 하나의 쿼리로 이끄는 결과를 가져왔습니다. 라이선스 정보를 캐시하기 시작했을 때, 즉 프로세스별 인메모리 캐시를 사용하여 1초간 캐시하면 쿼리 빈도가 극적으로 감소했습니다.
여기서 캐싱의 선택은 해당 데이터의 특성에 크게 의존합니다. 거의 갱신되지 않는 아주 작은 데이터 세트인 라이선스 데이터는 인메모리 캐싱에 적합한 후보입니다. 여기서 프로세스별 캐시가 유리합니다. 왜냐하면 이것은 요청률로부터 캐시 갱신 속도를 분리하기 때문입니다.
여기에 주의해야 할 점은 현재 우리의 Redis 설정이 Redis 보조를 사용하지 않고 있으며 캐싱에 대해 단일 노드에 의존하고 있다는 것입니다. 즉, 우리는 Redis가 압력으로 인해 다운되는 것을 피하기 위해 균형을 맞추어야 합니다. 반면, PostgreSQL 복제본에서 데이터를 읽으면 부하가 여러 읽기 전용 복제본에 분산됩니다. 데이터베이스 쿼리가 더 비쌌더라도 부하가 여러 노드에 분산됩니다.
읽기 전용 데이터를 레플리카에서 읽기
캐싱이 구현되었든 구현되지 않았든, 우리는 데이터베이스 레플리카에서 데이터를 읽을 수 있다면 그것을 확실히 해야 합니다. 이는 많은 데이터베이스 레플리카에 걸쳐 읽기를 확장하는 데 도움이 되며, 데이터베이스 주 서버로부터 불필요한 작업을 제거합니다.
GitLab 데이터베이스 읽기를 위한 로드 밸런싱은 첫 번째 쓰기 후나 명시적 트랜잭션을 열 때에는 주 서버에 고수합니다. 읽기 중심 데이터의 경우, 우리는 이러한 데이터를 트랜잭션 범위 외에서 읽으려고 노력하고 어떠한 쓰기도 하기 전에 이 데이터를 읽으려 합니다. 이는 이 데이터가 드물게 업데이트되기 때문에 종종 약간 오래된 데이터를 읽는 데 크게 신경쓰지 않아도 되기 때문에 종종 가능합니다. 그러나 이 쿼리가 이전 쓰기나 트랜잭션으로 인해 레플리카로 보내질 수 없다는 것은 명확하게 표현되지 않을 수 있습니다. 따라서 읽기 중심 데이터를 만나면, 이 데이터가 레플리카에서 읽힐 수 있는지 확인하고 더 넓은 맥락을 검토하는 것이 좋은 실천입니다.