낮은 검색 성능 문제를 해결하기 위한 방법 중 인덱싱에 대해 학습합니다.
데이터베이스의 핵심적인 기능을 말하자면 다음 두가지를 이야기 할 수 있습니다. 첫번째는 데이터를 저장하는 것이고, 두번째는 요청에 왔을 때 저장되어있는 데이터 중에 요청에 맞는 데이터를 찾아서 제공하는 것입니다.
데이터베이스 기능의 두번째 경우에서, 낮은 검색 성능이 나타날 때, 좀 더 효율적인 방법으로 특정 키의 값을 확인하고 제공하기 위해서 인덱스(색인)를 이용합니다. 인덱스를 사용하지 않는다면 요청받은 데이터를 찾기 위해 전체 데이터베이스를 스캔해야할 수도 있기 때문입니다.
인덱스는 데이터베이스에 저장된 기본데이터(primary data)에서 파생된 부가적인 메타데이터(meta-data : 데이터에 관한 구조화된 데이터)입니다. 이 메타데이터인 인덱스가 우리가 원하는 데이터의 위치를 찾는데 도움을 주는 이정표가 됩니다. 우리가 책에서 어떠한 내용(data)을 찾을 때 앞부분에 따로 구성된 목차(index)를 통해서 해당 데이터의 위치를 빠르게 찾아 나가는 것에 비유할 수 있습니다.
데이터베이스에 데이터가 추가되고 삭제될 수 있듯이, 인덱스 역시 추가되고 삭제될 수 있습니다. 하지만 인덱스의 편집사항은 데이터베이스의 내용에는 영향을 주지 않습니다. 단지 질의(Query) 성능에만 영향을 줍니다. 책의 목차에서 목차 제목을 추가하거나 삭제해도 책의 내용이 변하지 않습니다. 단지 목차가 더 효율적으로 구성된다면 내용에 접근하는 시간이 빨라질 수 있는 것과 같은 이치입니다.
많은 사용자가 발생함에 따른 문제를 해결하기 위한 방법 중 레플리카에 대해 학습합니다.
많은 사용자가 하나의 데이터베이스에 접근하는 경우에 성능 저하가 발생할 수 있습니다. 이러한 경우에 데이터베이스의 복사본을 저장하는 각각의 노드인 레플리카(replica : 복제서버)를 활용하여 문제를 해결할 수 있습니다. 레플리카는 원본이 되는 데이터베이스와 같은 데이터를 다른 위치에 존재하는 여러 노드에 유지하는 방식입니다. 그렇기 때문에 데이터의 중복성이 발생하는데, 이 중복성으로 인해서 얻을 수 있는 장점이 존재하며, 그에 따라 해결해야하는 문제점들 역시 존재합니다.
시스템 장애 발생시에도 동작할 수 있도록 가용성 확보
레플리카를 활용한 데이터베이스 구조를 구현할 경우 일부 노드가 사용불가능 상태라면 해당 데이터는 남은 다른 노드를 통해 여전히 제공할 수 있다는 장점이 있습니다. 리더가 되는 데이터베이스와 동일한 데이터를 레플리카들이 가지고 있기 때문에, 리더 데이터베이스에 화재, 지진, 네트워크 오류 등의 장애 상황이 발생하더라도 레플리카 중 하나를 새로운 리더로 지정하고 사용자의 요청을 새로운 리더로 연결하여, 서비스가 중단되는 시간을 최소화 할 수 있습니다. 이후 기존 리더 데이터베이스의 복구가 완료되면 정상상태로 사용자 요청 연결을 전환합니다.
사용자의 요청이 하나의 데이터베이스에 집중 될 경우, 데이터베이스 성능에 영향을 미칠 수 있습니다. 특히 웹 기반 애플리케이션의 경우 쓰기 요청보다 읽기 요청의 비율이 높다는 특징이 있는데, 모든 읽기 처리 요청이 하나의 데이터베이스에 쏠리게 되면, 성능저하로 이어질 가능성이 높습니다. 데이터베이스 자체의 성능을 높여서 대처할 수 있지만, 이는 일반적으로 높은 비용을 수반합니다.
이러한 경우에 레플리카를 읽기전용 데이터베이스로 활용할 수 있습니다. 리더에서는 읽기와 쓰기 처리를 한번에 처리하면서, 쓰기 처리가 발생할 경우 각 레플리카에도 데이터를 저장해서, 동기화를 진행합니다. 레플리카는 최종적으로 리더와 같은 데이터를 가지고 있기 때문에 사용자들이 읽기 요청을 보낼 때 해당사항을 처리할 수 있습니다. 이렇게 되면 사용자 트래픽이 각 데이터베이스로 분산되기 때문에 성능향상에 도움이 됩니다.
데이터를 저장하고 요청에 따라 제공해야 하는 데이터베이스가 데이터를 요청하는 곳과 지리적으로 멀 경우 응답시간이 늦어질 수 있습니다. 이러한 지연시간을 감소시키기 위해서 레플리카의 위치를 각 지역에 분산시켜 배치할 수 있습니다. 각 지역에 분산 배치된 레플리카는 해당 지역에서 가까운 사용자의 요청을 처리하여 지연시간을 감소시킬 수 있습니다.
레플리카를 활용한 데이터베이스 구조를 유지할 때 가장 중요한 것은, 모든 데이터베이스가 정확히 같은 데이터를 가지고 있게 하는 것 입니다. 기본적으로 리더가 되는 데이터베이스에 쓰기 요청이 들어왔다면, 해당 데이터는 리더 데이터베이스에 저장되어야 함은 물론, 모든 레플리카에도 똑같이 저장되어야 합니다. 다른 데이터베이스로 데이터를 복제하는 방식에는, 동기식 복제(synchronous)과 비동기식 복제(Asynchronous)이 사용될 수 있습니다.
동기식 복제(synchronous)
동기식으로 복제가 진행되는 레플리카 구조에서는 리더의 데이터 처리와 별개로 레플리카에서의 데이터 처리까지 모두 완료되어야만 프로세스가 진행됩니다. 이 경우에 레플리카와 리더 데이터베이스가 일관성 있게 최신 데이터 복사본을 가지고 있는 것을 보장할 수 있습니다.
그러나 동기식 복제의 경우 네트워크 문제나, 다른 이유로 레플리카가 정상적으로 데이터처리 작업을 완료할 수 없는 경우 응답을 받지 못한 리더 데이터베이스 역시 프로세스를 진행하지 못합니다. 리더는 모든 쓰기를 차단하고 동기 레플리카가 회복되기를 기다리기 때문에 시스템 운영이 멈출수 있는 위험이 있습니다.
비동기식 복제 (Asynchronous)
반면 비동기식 복제는 동기식 복제와 다르게 리더가 레플리카의 처리 응답을 기다리지 않습니다. 리더는 레플리카에 데이터 처리를 요청한 후 자신의 작업을 완료하면 사용자의 요청에 바로 응답합니다. 이 경우에는 연결된 모든 레플리카가 어떠한 이유로, 처리를 지속할 수 없더라도 리더는 쓰기 처리를 계속할 수 있다는 장점이 있습니다. 리더 데이터베이스에 문제가 없다면, 레플리카의 상태와 무관하게 사용자에게 서비스를 계속해서 제공할 수 있습니다.
하지만 레플리카가 읽기 전용으로 이용되고 있을 경우, 사용자에게 리더와 같은 응답을 주지 못하는 경우가 발생할 수 있습니다. 데이터의 불일치가 발생하기 때문에 불일치 상태가 길어지는 경우 큰 문제가 될 수 있습니다. 또한 리더가 잘못되고 복구할 수 없는 상황이 발생 시, 팔로워에게 복제되지 못한 모든 처리가 유실될 수 있으며, 클라이언트에게는 정상 작동을 알린 이후임에도 불구하고 지속성을 보장하지 못하는 문제가 발생할 수 있습니다.
반동기식 복제(semi-synchronous)
앞서 설명한 두 복제 방식의 장단점을 보완하기 위해서, 하나의 레플리카는 동기식 복제를 사용하고, 다른 레플리카들은 비동기식으로 사용하는 반동기식 복제도 활용됩니다.
다중 리더 복제에는 각각 데이터의 하위 집합 관리를 담당하는 리더 역할을 하는 여러 노드가 포함됩니다. 이 접근 방식에서는 모든 리더에서 쓰기를 수행할 수 있으며 변경 사항은 시스템의 다른 리더 및 복제본으로 전파됩니다. 이것은 높은 쓰기 가용성과 낮은 쓰기 대기 시간을 허용합니다. 그러나 여러 리더가 동일한 데이터를 동시에 업데이트할 때 충돌이 발생할 수 있으므로 충돌 해결 메커니즘을 마련해야 합니다.
반면에 리더 없는 복제에는 지정된 리더 노드가 없습니다. 대신 시스템의 모든 노드는 동일하며 읽기 및 쓰기 요청을 모두 수락할 수 있습니다. 각 쓰기는 일반적으로 여러 복제본으로 전송되며 성공 메시지로 응답하는 첫 번째 복제본은 신뢰할 수 있는 버전으로 간주됩니다. 이 접근 방식은 리더 노드를 조정할 필요가 없기 때문에 읽기 및 쓰기 모두에 대해 고가용성을 제공하고 시스템 설계를 단순화합니다. 그러나 쓰기를 여러 노드에 복제해야 할 필요성과 여러 쓰기가 동시에 발생하는 경우 충돌 가능성으로 인해 쓰기 대기 시간이 증가할 수 있습니다.
전반적으로 다중 리더 복제와 리더 없는 복제 간의 선택은 시스템의 특정 요구 사항과 가용성, 일관성 및 대기 시간 간의 균형에 따라 달라집니다.
데이터베이스에 들어가는 데이터셋이 매우 크거나, 쿼리 처리량이 매우 높은 경우에는 단순히 복제하는 것만으로는 부족할 수 있습니다. 이에 따라 큰 데이터베이스를 파티션이라는 작은 단위로 쪼개서 활용하는 방법이 제시되었습니다. 이러한 방식은 샤딩(shardIng)이라고도 표현합니다. 파티션은 그 자체로 작은 데이터베이스가 됩니다.
서비스에 따라서 파티션을 의미하는 용어가 다를 수 있습니다.
몽고DB, Opensearch 등의 서비스에서는 샤드(shard)에 해당
데이터 파티셔닝을 필요로 하는 주된 이유는 확장성 때문입니다. 데이터베이스가 확장되면서 점점 대용량의 데이터베이스가 되고, 그러한 환경에 맞게 프로세스를 처리할 필요성이 생기기 때문입니다. 데이터셋을 여러 디스크에 분산하의 요청에 따른 질의 부하를 여러 곳으로 분산하는 것에 그 목적이 있습니다.
많은 정보를 가진 데이터베이스에는 많은 정보에 대한 요청이 발생합니다. 이때 각각의 요청을 처리하기 위한 지점이 하나뿐이라면, 데이터베이스 성능에 영향을 줄 수 있습니다.
파티셔닝을 통해서 데이터베이스를 작은 범위로 나누고, 요청받은 데이터에 해당하는 파티션에만 접근해서 정보를 조회할 수 있다면 위와 같은 성능 저하를 피할 수 있습니다.
일반적으로 파티셔닝과 복제(Replica)는 함께 사용됩니다. 파티션 내부를 더 자세히 살펴보면 각 파티션의 복사본을 여러 노드에 저장하고 있는 것을 알 수 있습니다. 이러한 방식을 사용하는 이유는 요청의 쏠림현상(skewed)을 방지하기 위해서입니다.
4개의 노드가 사용된다면, 이론적으로 하나의 노드 사용할 때보다 4배의 데이터를 저장하고, 4배의 읽기, 쓰기 요청을 처리할 수 있을 것입니다. 그러나 데이터를 분산시켰음에도 불구하고, 특정 패턴을 가진 요청에 의해서 한 곳으로 요청이 쏠리는 현상(skewed)이 발생할 수 있습니다. 이 같은 경우에는 파티셔닝의 효과가 떨어지게 되고, 극단적인 경우 모든 부하가 한 파티션에 몰려 4개 중 3개 노드가 사용되지 않는 것과 같은 효과가 날 수 있습니다. 이렇게 불균형하게 부하가 높아진 파티션을 핫스팟이라고 부릅니다.
이러한 문제를 피하기 위해서 파티션을 구성할때는 데이터의 쿼리(질의) 부하를 노드 사이에 고르게 분산시킬 수 있도록 전략적으로 배치해야 합니다.
첫번째 방법은 레코드를 할당할 노드를 무작위로 선택하는 것입니다. 이 경우 기계적으로 무작위 선택을 통해 분산효과를 얻을 수 있지만, 데이터를 읽어내야할 때는 특별한 기준으로 찾을 수 없기 때문에 성능저하를 가져올 수 있습니다.
두번째 방법은 키 범위를 기준으로 한 파티셔닝을 진행하는 것입니다. 이 방식은 마치 백과사전처럼 데이터에 접근하기 위한 키를 일정한 기준(키이름에 대해서 a to z를 확인하거나 또는 저장 날짜를 기준으로 키를 분류)에 따라 배치해서 파티션을 구성하는 것입니다.
세번째 방법은 키의 해시값 기준 파티셔닝입니다. 쏠림 현상이 일어날 수 있는 데이터라도 해시함수를 통해 처리를 거쳐 균일하게 분산시킬 수 있습니다. 이 경우에 핫스팟 발생을 막을 수 있지만, 역시 범위 쿼리 효율성이 높은 키 범위 파티셔닝의 장점을 잃어버린다는 단점이 있습니다.
결과적으로 파티셔닝을 진행할 때는 각 서비스의 목적에 따라 검색 효율성을 높이는 것이 좋을지, 핫스팟 발생을 방지하는 것이 좋을지, 균형을 유지할 수 있는 적절한 방법을 구현해야 합니다.
동일 데이터의 잦은 조회에 따른 문제를 해결하기 위한 방법 중 캐싱에 대해 학습합니다.
특정 데이터에 대한 반복된 요청을 효과적으로 처리하기 위한 시도
캐시(Cache)는 임시로 복제된 데이터를 저장하는 장소로 사용자가 더 효율적이고 빠르게 원하는 데이터에 접근할 수 있도록 하기위해 설정됩니다. 캐싱을 이용하면, 원본 데이터베이스가 제공할 수 있는 것보다 짧은 대기 시간을 제공하면서 웹 애플리케이션의 성능을 향상시킬 수 있으며, 데이터베이스의 비용을 절감할 수 있습니다.
성능향상
데이터 베이스는 기본적으로 속도 보다는 데이터의 저장과 안정성에 초점을 맞추게 됩니다.(디스크 기반 데이터 저장소) 반면 캐시의 경우에는 임시로 데이터가 저장되는 장소이기 때문에, 저장의 기능보다는 정보를 제공하는 처리 속도에 더 집중 할 수 있습니다(인 메모리 캐시). 따라서 사용자의 요청이 반복되는 데이터를 빠르게 제공하기 위해서 캐시를 활용하게 되면, 원본 데이터가 존재하는 데이터베이스에 액세스 하는 것보다 훨씬 빠른 속도로 데이터를 제공하면서 전반적인 애플리케이션 환경이 개선됩니다.
비용감소
데이터 베이스의 성능을 높이거나, 많은 쿼리(질의) 요청을 처리하는 것은 모두 비용상승을 수반하는 작업입니다. 캐시를 사용하게 됨으로서 원본 데이터베이스에 대한 쿼리 수를 줄이고, 데이터베이스 자체를 스케일링 할 필요성을 낮추면, 성능 향상과 더불어 비용을 절감하는 효과를 낼 수 있습니다.
Cache-aside
레플리카 콘텐츠에서 언급했던 것처럼, 일반적으로 웹 애플리케이션에서는 읽기 작업량이 많습니다. 이 경우에 애플리케이션을 설계할 때 캐시 보관 패턴을 사용합니다. 애플리케이션은 우선적으로 캐시에서 원하는 데이터를 검색합니다. 데이터가 캐시에 존재하지 않는다면 데이터베이스에 직접 연결하도록 코드를 구성합니다. 데이터베이스에서 직접 데이터를 확보했다면 애플리케이션은 해당 데이터를 캐시에 복사합니다.
Read-through/Write-through Cache
Read-through/Write-through 캐시 모두 데이터베이스와 일렬로 배치되며, 애플리케이션은 뒤에 있는 데이터베이스가 아닌 캐시를 주 데이터 저장소처럼 취급합니다.
Read-through를 통해서 애플리케이션이 데이터를 읽으려 한다면, 최초 데이터를 로드할 때만 캐시가 데이터베이스에 접근합니다. 이후 동일 데이터는 캐시에서 처리됩니다. Cache-Aside 방식과 달리 애플리케이션이 캐시에 기록하지 않기 때문에, 애플리케이션 자체의 부담을 줄일 수 있습니다. 읽기처리가 많은 워크로드에 적합한 캐시 방법입니다.
Write-through를 통해서 에플리케이션에서 쓰기 요청이 발생한다면, 우선적으로 캐시에 데이터를 추가한 뒤 데이터베이스에도 데이터를 추가하게 됩니다. 이로 인해서 항상 최신 상태를 유지하면서 데이터 일관성을 보장 받을 수 있습니다.
결과적으로 Read-through/Write-through를 함께 사용하면 데이터의 일관성을 보장하면서, 애플리케이션의 코드를 단순화 하고 원본 데이터베이스에 전달되는 요청을 최소화 할 수 있습니다.
Write-behind/write-back Cache
Write-behind(back) 방식을 사용하면, 애플리케이션은 일단 캐시에 데이터를 저장합니다. 그 후 캐시가 백그라운드에서 비동기적인 방식으로 데이터베이스에 데이터를 기록합니다. 이러한 방식은 쓰기처리가 많은 워크로드에 적합한 캐시 방법입니다. 애플리케이션은 데이터베이스에 쓰기가 완료되는 것을 기다릴 필요없이 다음 작업을 진행할 수 있으므로 사용자에게 좀 더 쾌적한 사용환경을 제공할 수 있습니다.