Computer >> 컴퓨터 >  >> 프로그래밍 >> Ruby

프로젝트 성장에 대비하는 데이터베이스 확장 전략: UUID부터 파티셔닝까지

젊은 프로젝트를 진행하다 보면 나중에 확장을 쉽게 만들거나 어렵게 만드는 결정을 끊임없이 내리게 됩니다. 때로는 기술 부채(technical debt)를 일부러 안고 가는 것이 좋습니다. 그래야 더 빨리 출시할 수 있으니까요. 하지만 다른 경우에는 대안이 있다는 사실조차 몰랐기 때문에 어쩔 수 없이 기술 부채를 떠안게 되기도 합니다.

Honeybadger에서도 우리 스스로 삶을 불필요하게 힘들게 만든 일들이 있었습니다. 몇 가지 핵심 포인트만 미리 알았더라면 확장 과정이 훨씬 덜 고통스러웠을 것입니다.

UUID 사용하기

"기본 키(primary key)"라고 하면 대부분 자동 증가(auto-increment) 숫자를 떠올립니다. 작은 시스템에서는 잘 동작하지만, 규모가 커지면 큰 문제가 발생합니다.

어느 순간이든 오직 하나의 데이터베이스 서버만 기본 키를 생성할 수 있습니다. 즉, 모든 쓰기(write) 작업이 단일 서버를 거쳐야 한다는 뜻입니다. 초당 수천 건의 쓰기를 처리하고 싶다면 이는 좋지 않은 소식입니다.

UUID를 기본 키로 사용하면 이 문제를 우회할 수 있습니다. UUID는 123e4567-e89b-12d3-a456-426655440000처럼 생긴 고유 식별자입니다.

위키백과는 UUID를 이렇게 설명합니다:

표준 방식으로 생성된 UUID는 중앙 등록 기관이나 생성 당사자 간의 조율 없이도 실질적으로 고유합니다. UUID가 중복될 확률은 0은 아니지만, 무시할 수 있을 정도로 낮습니다.

따라서 누구나 UUID를 생성해 어떤 것을 식별하는 데 사용할 수 있으며, 이미 생성된 다른 식별자와 중복되지 않을 거라고 거의 확신할 수 있습니다. 독립적인 주체들이 UUID로 라벨링한 정보는 식별자 충돌을 해결할 필요 없이 하나의 데이터베이스로 통합하거나 같은 채널로 전송할 수 있습니다.

UUID를 기본 키로 사용하면 모든 쓰기 작업이 단일 데이터베이스를 거칠 필요가 없어집니다. 대신 여러 서버에 분산시킬 수 있습니다.

또한 레코드가 데이터베이스에 저장되기 전에 ID를 미리 생성하는 것 같은 유연성도 얻게 됩니다. 데이터베이스 트랜잭션이 완료되길 기다리지 않고 레코드를 캐시나 검색 서버로 먼저 보내고 싶을 때 유용합니다.

Rails 앱에서 UUID를 기본값으로 활성화하는 것은 간단합니다. 설정 파일만 수정하면 됩니다:

# config/application.rb
config.active_record.primary_key = :uuid

Rails 마이그레이션으로 개별 테이블에 UUID를 적용할 수도 있습니다:

create_table :users, id: :uuid do |t|
  t.string :name
end

개발을 시작할 때 활성화해두면 나중에 확장할 때 수많은 번거로움을 덜어주는 간단한 설정 옵션입니다. 추가 보너스로, 봇이나 악의적인 사용자가 비공개 URL을 추측하기 더 어렵게 만들어 줍니다.

카운트와 카운터

한번 살펴보면 카운트와 카운터는 어디에나 있습니다. 이메일 클라이언트는 읽지 않은 이메일 수를 표시하고, 블로그는 전체 게시물 수로 페이지 수를 계산해 페이지네이션 푸터를 만듭니다.

카운터에는 두 가지 확장성 문제가 있습니다:

  1. select count(*) from users 같은 데이터베이스 쿼리는 본질적으로 느립니다. 결과를 생성하기 위해 레코드셋의 각 레코드를 실제로 순회하기 때문입니다. 백만 건의 레코드가 있다면 상당한 시간이 걸립니다.
  2. "카운터 캐시(counter cache)"로 카운터 속도를 높이려는 시도는 효과가 있지만, 여러 데이터베이스 서버에 쓰기를 분산하는 능력을 제한합니다.

가장 쉬운 해결책은 가능한 한 카운터를 사용하지 않는 것입니다. 초기 설계 단계에서 이렇게 결정하면 훨씬 수월합니다.

예를 들어, 카운트 대신 날짜 범위로 페이지를 나눌 수 있습니다. 나중에 생성하기 지옥처럼 힘들어질, 크게 유용하지 않은 통계는 아예 보여주지 않기로 선택할 수도 있습니다. 요점은 그렇다는 겁니다.

데이터 만료 및 웨어하우징

주어진 RAM, CPU, 디스크 IO 용량에서 단일 PostgreSQL 테이블에 저장할 수 있는 데이터 양에는 상한이 있습니다. 즉, 언젠가 오래된 데이터를 메인 테이블에서 옮겨야 할 시점이 반드시 옵니다.

간단한 사례를 살펴보겠습니다. 몇 기가바이트 크기의 데이터베이스에서 1년 이상 된 레코드를 삭제하고 싶다고 가정해봅시다.

이런 문제를 다뤄본 적이 없다면 이렇게 하고 싶어질 수 있습니다:

MyRecords.where("created_at < ?", 1.year.ago).destroy

문제는 이 쿼리가 며칠 또는 몇 주가 걸릴 수 있다는 점입니다. 데이터베이스가 너무 크기 때문입니다.

특히 고통스러운 문제인데, 너무 늦어서야 문제가 있다는 걸 깨닫는 경우가 많기 때문입니다. 회사가 어리고 데이터베이스에 레코드가 1,000건밖에 없을 때 데이터 삭제 전략을 고민하는 사람은 거의 없습니다.

미리 계획할 수 있다면 쉬운 해결책이 있습니다. 바로 테이블을 파티셔닝(partitioning)하는 것입니다. 모든 데이터를 my_records에 쓰는 대신, 이번 주 데이터는 my_records_1에, 다음 주 데이터는 my_records_2에 씁니다. 지난주 데이터를 삭제할 때가 되면 그냥 drop table my_records_1을 실행하면 됩니다. DELETE와 달리 이 쿼리는 매우 빠르게 완료됩니다.

날짜 외의 필드로도 파티셔닝할 수 있습니다. 사용 사례에 맞는 방식을 선택하면 됩니다.

pg_partman이라는 PostgreSQL 확장은 모든 세부 사항을 처리해 코드 한 줄도 변경하지 않고 데이터베이스를 파티셔닝할 수 있게 해줍니다. Ruby에서 파티셔닝을 관리하고 싶다면 partitionable이라는 유용한 젬(gem)도 있습니다.

마치며

다음에 처음부터 프로젝트를 구축하게 된다면, 잠시 확장에 대해 생각해보시길 권합니다. 집착할 필요는 없습니다. HAML을 쓸지 ERB를 쓸지 며칠씩 고민하지 말고요. 대신 미리 계획함으로써 쉽게 얻을 수 있는 승리(easy wins)가 없는지 스스로에게 물어보세요.