Node.js

Redis Deep Dive

이경찬 :) 2024. 1. 11. 15:35

 

Redis란?

key, value 구조의 비정형 데이터를 저장하고 관리하기 위한 오픈 소스 기반의 비관계형 데이터베이스 관리 시스템입니다.

Redis는 DBMS와 다르게 데이터를 메모리에 저장하여 더 빠른 응답이 가능합니다.

한마디로 캐시서버 기능이 가능합니다.

 

- key, value 구조이기 때문에 쿼리를 사용할 필요가 없습니다.

- String, List, Set, Sorted Set, Hash 자료 구조를 지원합니다

캐시서버란?

데이터베이스가 있는데도 Redis라는 인메모리 데이터 구조 저장소를 사용하는 이유는 무엇일까요?

 

데이터베이스는 데이터를 물리 디스크에 직접 쓰기 때문에 서버에 문제가 발생하여 다운되더라도 데이터가 손실되지 않습니다. 하지만 매번 디스크에 접근해야 하기 때문에 사용자가 많아질수록 부하가 많아져서 느려 질 수 있습니다.

일반적으로 운영 초기이거나 규모가 작은, 사용자가 많지 않은 서비스의 경우에는 WEB - WAS - DB의 구조로도 데이터베이스에 무리가 가지 않습니다.

하지만 사용자가 늘어난다면 데이터베이스가 과부하 될 수 있기 때문에 이때 캐시 서버를 도입하여 사용합니다.

그리고 이 캐시 서버로 이용할 수 있는게 Redis입니다.

 

같은 요청이 들어오면 매번 데이터베이스를 거치는게 아니라 캐시 서버에서 첫 번째 요청 이후 저장된 결과값을 바로 내려주기 때문에 DB의 부하를 줄이고 서비스의 속도도 더 개선되는 장점이 있습니다.

 

캐시 서버는 Look aside cache 패턴과 Write Back 패턴이 존재합니다.

 

- Look aside cache

1. 클라이언트가 데이터를 요청

2. 웹서버는 데이터가 존재하는지 Cache 서버에 먼저 확인

3. Cache 서버에 데이터가 있으면 DB에 데이터를 조회하지 않고 Cache 서버에 있는 결과값을 클라이언트에게 바로 반환 (Cache Hit)

4. Cache 서버에 데이터가 없으면 DB에 데이터를 조회하여 Cache 서버에 저장하고 결과값을 클라이언트에게 반환 (Cache Miss)

 

- Write Back 

1. 웹서버는 모든 데이터를 Cache 서버에 저장

2. Cache 서버에 특정 시간 동안 데이터가 저장됨

3. Cache 서버에 있는 데이터를 DB에 저장

4. DB에 저장된 Cache 서버의 데이터를 삭제

 

* insert 쿼리를 한 번씩 500번 날리는 것보다 insert 쿼리 500개를 붙여서 한 번에 날리는 것이 더 효율적이라는 원리입니다.

* 이 방식은 들어오는 데이터들이 저장되기 전에 메모리 공간에 머무르는데 이때 서버에 장애가 발생하여 다운된다면 데이터가 손실될 수 있다는 단점이 있습니다.

 

싱글스레드인 Redis에서 발생 할 수 있는 문제점과 해결방안

Redis는 인메모리 데이터 저장 방식으로 빠르게 데이터를 가져온다는 장점이 있습니다. 하지만 서비스가 갑자기 잘되어 트래픽이 몰리면 어떻게 될까요? 

서버의 한계점을 넘어간다면, 인스턴스 장애가 발생할 수 있습니다. 이 경우에 대한 대비책들을 마련해야 합니다.

 

크게 이런 대첵에는 복제/샤딩 전략이 존재합니다.

 

먼저 복제 전략부터 알아보도록 하겠습니다.

 

1. Master/ Replica 구조

 

 

위 그림은 Master /Replica의 구조를 나타냅니다.

최초 Master / Replica 구성시

Master의 데이터는 모든 Replica에 복제됩니다.

 

만약 데이터의 변경이 발생한다면, 변경 작업은 Master에서만 가능합니다. 이후 변경된 데이터는 비동기적으로 모든 Replica에게 전달되어 반영됩니다.

 

2. Master/Replica의 동기화 과정

 

1. Master와 Replica 인스턴스를 별도로 구성합니다.

2. Replica 인스턴스에서 ReplicaOf 명령어를 통해 Master 인스턴스와의 동기화 명령을 수행합니다.

3. Master에서는 fork를 통해 자식 프로세스를 생성합니다.

4. 자식 프로세스에서는 Master 메모리에 있는 모든 데이터를 Disk로 dump 합니다.

5. dump가 완료되면, 이를 Replica에 전달하여 반영합니다.

6. Master에서는 복제가 진행되는 동안 변경 데이터를 Replication Buffer에 저장합니다.

7. Dump 전송이 완료되면, Replication Buffer의 내용을 Replica에게 전달하여 데이터를 최신상태로 만듭니다.

8. 작업이 완료되면, 이후에는 데이터 변경발생분만 비동기방식으로 전달됩니다.

 

최초 구성시에는 전체 동기화가 발생하고, 이때 fork가 발생하므로 메모리 사용량이 증가할 수 있습니다.

 

위의 구성 완료 후 네트워크 지연이 발생하면 동기화는 어떻게 처리가 될까?

 

 

 

Master 인스턴스에서 내부적으로 Backlog Buffer가 만들어집니다. 이후 Replica와의 단절이 발생하게 되면, Master 인스턴스에서는 변경 데이터를 Buffer에 저장합니다. 이때 Buffer는 유한한 크기를 지녔으므로, 지연이 오랫동안 발생한다면 Buffer가 넘칠 수 있습니다.

 

단절 이후 다시 재연결 되었을 때 과정은 다음과 같습니다.

 

1. Replica에서 Master와 동기화를 위해 부분 동기화를 시도합니다.

2. 만약 Backlog Buffer에 네트워크 단절 이후의 데이터가 모두 존재하면, Buffer에있는 데이터를 전달받아 최신 상태를 만듭니다.

3. 만약 오랜 시간 네트워크 단절로 인해 Backlog Buffer에 데이터가 유실되었을 경우에는 전체 동기화 과정을 진행합니다. 

 

ADB, RDB를 설명할때도 살펴봤지만, 프로세스 fork가 일어나게되면 메모리 사용율이 급격하게 증가할 수 있으므로 전체 동기화 작업 혹은 Replica 추가 작업시에는 모니터링과 메모리 조정등이 필요합니다.

 

3. Master / Replica 장단점

장점:

- 우선 위의 구조의 장점은 어느 인스턴스가 다운되더라도 애플리케이션의 영향을 최소화 할 수 있습니다.

- 데이터 조회를 위해 굳이 Master에게 요청하지 않아도 되므로 Read 작업에 대한 부하를 여러 인스턴스로 분산시킬 수 있는 장점이 있습니다.

 

문제점

- 만약 Master 인스턴스에 문제가 발생하면 쓰기 작업 및 Replica 인스턴스의 동기화 문제가 발생

이 문제를 해결하기 위해 관리자가 모니터링을 통해 장애 여부를 감지하고, 수동으로 Replica 인스턴스 중 하나를 Master로 선정하고, 나머지 Replica에서 새로 변경한 Master를 바라보도록 설정을 변경해야 합니다.

 

이렇게 관리자가 수동으로 이슈를 해결하면 운영자 입장에서 많이 불편하고 문제를 발견하지 못하면 그동안 애플리케이션이 작동하지 않는 심각한 문제로 이루어 질 수 있습니다.
따라서 이러한 이슈를 해결하기 위해 Redis Sentinel 기능을 제공하였습니다. Sentinel은 별도의 프로세스로 Master 인스턴스 다운시, 이를 감지하여 Replica 중 하나를 Master 인스턴스로 Failover 및 이를 애플리케이션에게 통지하는 기능을 포함하고 있습니다.

 

4. Master/ Replica/ Sentinel 구조

 

 

Master/ Replica/ Sentinel 구조 도식화

 

위 그림은 Master/ Replica/ Sentinel의 구조입니다. 녹색선이 Sentinel과 연결된 네트워크 Path를 의미합니다.

 

Sentinel은 다른 Sentinel을 포함한 모든 Redis 인스턴스와 연결합니다. 이후 1초마다 HeartBeat 통신을 통해 Master 및 Replica 서버가 정상적으로 작동중인지 여부를 확인하고 이상 발생시 자동으로 Failover 및 애플리케이션에 알림을 전송합니다.

 

Sentinel은 주관적다운, 객관적다운의 단계를 거쳐서 Master를 교체해줍니다.

 

1. 연결된 노드 중 HeartBeat에 일정 시간동안 응답하지 않는 경우 해당 Sentinel은 장애가 발생한 것으로 간주하고 해당 노드를 주관적 다운으로 인지합니다.

2. 만약 장애가 발생한 인스턴스가 Master일 경우에느 모든 Sentinel에게 Master Down 여부를 묻습니다. Master Down여부를 전달받은 Sentinel들은 실제 Master 인스턴스가 죽었는지를 확인 후 이를 응답합니다.

3. 이떄 Master 인스턴스에 장애가 발생했다고 응답하는 비율이 정족수를 넘게되면 이를 객관적 다운이라고 인지하게 됩니다.

4. 객관적 다운이 발생하게 되면 다른 Sentinel 인스턴스와 통신하여 장애 조치 작업을 시행합니다.

5. Replica중 하나를 Master로 승격을 합니다. 이때 기존의 Master가 원래대로 정상처리되면 Replica로 처리합니다.

 

샤딩

위의 복제방법은 Master의 데이터를 Replica에 모두 저장하여 가용성과 읽기 작업의 성능을 높일 수 있습니다.

 

데이터 양이 폭발적으로 늘어난다면?

 

DB의 모든데이터를 Redis에 복제하면 메모리에서 이를 버틸 수가 없을 수 있습니다.

이를 보완하기위해 데이터 분산을 통해 고가용성을 확보할 수 있는 파티셔닝 개념과 Redis에서 사용되는 샤딩전략에 대해 알아보겠습니다.

 

1. 파티셔닝 개념

파티셔닝은 DB의 관리 용이성 및 읽기 최적화를 위해 논리적인 테이블의 물리 구조를 여러개의 파티션으로 분할하여 분산 저장하는 기법을 말합니다.

 

 

데이터베이스 예시

 

예를들어 위와같은 회원 테이블이 존재한다고 가정을 해 보겠습니다. 

 

만약에 위와같이 모든데이터를 같은 테이블에 저장을 하면 해당 테이블에 조회 성능을 높이기 위해 인덱스 추가등의 작업이 쉽지 않을 뿐더러 점차 조회 성능도 떨어지게 될 것입니다.

 

수십억건의 데이터가 존재하는 테이블에서 매월마다 가입일이 5년지난 데이터를 삭제해야 하는 경우에는 어떻게 해야할까요?

 

데이터를 지우기위해 수십억건의 테이블을 탐색하면서 조건에 해당하는 데이터를 삭제해야 합니다. 이렇게 되면 오랜시간 동안 Lock으로 인해 동시성 저하가 발생할 수 있으며, 테이블 크기가 점점 더 커질수록 해당 작업은 어려워질 것입니다.

 

이러한 경우 사용자에게 논리적으로 보여지는 테이블은 하나지만 물리적으로는 여러 파티션에 데이터를 나누어 저장한다면, 조회 성능 향상 및 관리가 용이해질 것입니다.

 

 

 

위의 예는 가입일을 기준으로 파티셔닝을 하였습니다.

 

이렇게 가입일 기준으로 파티션을 구성하면 다음과 같은 이점이 있습니다.

 

만약 매월 강비일 기준으로 사용자를 삭제한다면, 기존에는 테이블내 모든 데이터를 탐색해야 했지만 지금은 특정 월에 해당하는 파티션만 Drop하면 되므로 작업 부담이 줄어듭니다.

 

그리고 특정 월에 해당하느 데이터를 조회할 때, 해당 파티션에 속한 데이터에 대해서만 Multi block I/O를 실시할 수 있기 때문에 인덱스를 사용하는 방법보다 빠른 조회가 가능할 수 있습니다.

 

=> 파티셔닝은 대용량의 논리적 구조를 여러 물리적인 파티션으로 분할하여 조회 및 관리 용이성을 위해 사용됩니다.

 

2. 파티셔닝 종류

수평적 파티셔닝, 수직적 파티셔닝이 존재합니다.

 

수평적 파티셔닝은 위의 사례와 같이 특정 데이텅(가입일)기준으로 데이터를 다른 파티션에 저장하는 방법을 말합니다.

수직 파티셔닝

 

반면, 수직적 파티셔닝은 특정 컬럼을 기준으로 데이터를 분할하는 방법을 말합니다. 위 그림과 같이 기존 회원 테이블을 특정 컬럼을 기준으로 2개의 파티션으로 분할한 경우가 이에 해당됩니다.

 

수직적 파티셔닝을 하는 이유?

 

- 수직적 파티셔닝을해서 얻는 이점은 한쪽 세그먼트에서 발생하는 DML이 다른쪽에 영향을 끼치지 않습니다. 반면, 레코드 전체 데이터를 읽어야할 경우에는 데이터가 물리적으로 분산되었으므로 I/O에서 다소 비효율이 발생합니다.

 

그렇다면 샤딩은 무엇일까요?

 

샤딩은 수평적 파티셔닝의 한 종류입니다. 수평적 파티셔닝과 비교하여 다른점은 파티셔닝은 단일 DBMS내에서의 데이터 분할 정책이고, 샤딩은 분할된 여러 데이터베이스 서버로 데이터를 분할하는 방법입니다.

 

샤딩을 구성하게되면 샤드 수만큼 노드가 존재하며, 서버가 여러대 존재하므로 부하를 적절히 분산할 수 있는 장점이 있습니다.

 

3. 파티셔닝 전략

위와같이 특정 월(범위)를 통해 나누는 파티션을 Range파티션 이라고 합니다.

 

이런 방식은 파티션별로 데이터 편차가 크게 나는 경우에 비효율적입니다.

가장 큰 문제점은. 데이터 분포도가 고르지 못할 경우 데이터 배분을 균등하게 할 수 없습니다.

 

- 해시 파티셔닝

 

위의 문제를 해결하기위해 데이터를 서버별로 균등하게 분포해야 부하를 고르게 분산할 수 있습니다.

 

해시 파티셔닝은 Redis의 key값에 대하여 해시함수를 적용한 결과를 Redis Master의 개수만큼 나머지 연산을 토대로 데이터를 저장할 Master 서버르 지정하는 방법입니다.

 

- ReBalancing 문제

 

만약 해시 파티셔닝이 적용된 상황에서 데이터 용량이 더욱 커져 Master 서버 추가를 해야한다면 기존 데이터를 재분배 해야하는 문제가 있습니다.

 

이 과정에서 기존의 해시 파티셔닝을 사용하게되면 대부분의 데이터를 리밸런싱을 해줘야 하는 문제가 발생합니다.

 

- Consistent Hashing

 

Consistent Hashing 기법은 데이터와 더불어 Master 서버에 대하여 동일한 해시 함수를 적용하고, Master 서버 해시값 구간에 해당되는 데이터를 저장하는 방법입니다.

 

Consistent Hashing 기법에서는 서버를 추가하면 해시 함수를 적용하여 Hash Ring 형태로 만듭니다. 이후 입력되는 데이터는 해시 값 결과에 따라 저장소가 결정됩니다.

 

그래도 노드를 제거할 때 해당 노드가 가지고 있는 데이터를 재분배 해줘야 합니다. 만약 Master1 노드를 제거해서 Master2 노드에 데이터를 전부 이관해야 하는 경우이면, 이는 효율적으로 데이터 재분배가 일어났다고 보기 힘듭니다.

 

- 가상 노드 추가

 

Hash Ring에 단일 노드만 배치하니까 발생한 문제는, 노드를 제거했을 때, 인접해있는 다른 노드 하나에 모든 데이터를 이관해야하는 문제점이 있습니다. 따라서 이를 해소하는 방법은 Hash Ring에 여러 가상 노드를 배치하는 것입니다.

 

여러 가상노드를 추가하면 해당 노드를 제거할 때 이관해야 하는 데이터 양이 작아지므로 부하가 적게 일어납니다.

 

정리하자면, 해시 파티셔닝을 사용함으로 인하여 데이터를 균등하게 분포하면서, 데이터 재분배에 대한 영향도를 최소화 하기 위해서 Consistent Hashing 알고리즘을 이용할 수 있습니다. Consistent Hashing 알고리즘을 사용시 고려 사항은 Hash Ring에 가상 노드를 촘촘하게 그리고 노드가 간격을 균일하게 배치할 수 있도록 Hash 알고리즘이 적용되야 합니다. 특정 노드간의 범위가 벌어지게된다면, 그만큼 재분배해야할 데이터 양이 많아지므로 이를 유의해야 합니다.