Redis의 응답이 느릴때 (메모리 사용량)

2025. 4. 30. 21:07개발

상황

현재 서비스에서, 레디스에는 많은 종류의 데이터가 저장되어있다.

  • 각 게시글에 대한 락정보
  • 인기 게시글 정보
  • 유저-좋아요 정보
  • 게시글의 좋아요 카운팅

하나의 레디스 서버에서 4개의 역할을 수행하기 때문에 서로간의 영향력이 존재하며 한꺼번에 많은 요청을 부담해야 하기 때문에 응답이 느려질 수 있다.

 

각각의 서비스를 서로 다른 레디스 서버로 분리하여 담당하면 좋겠지만, 스케일 아웃을 하기 이전에 최대한 많은 트래픽을 견뎌보고 싶었다.

 

가정한 상황에 대한 문제 원인 분석 : 현재 상황에서 Latency의 원인이 될 수 있는 것들

1. 메모리 사용량의 증가

메모리 사용량이 증가하면 latency의 원인이 될 수 있다.

  1. 메모리 초과시, 페이지 스왑으로 디스크 I/O 발생
  2. RDB, AOF의 백그라운드 스레드가 fork() 등으로 인한 메모리 부족으로 페이지 스왑 발생

즉 메모리 사용량을 최적화 시켜야 한다.

 

위의 4가지 종류의 데이터중 요청에 비례하여 데이터 수가 늘어나는 데이터는 유저-좋아요 정보다.

N명의 유저가, M개의 게시글에 대해 좋아요를 하면 최대 N * M 개까지 증가한다 O(NM)

 

따라서 이 데이터를 최적화하는 것을 목표로 고민해보았다. 결국 수를 줄일 수는 없으니, 자료구조의 변경을 통해 메모리 사용량을 줄일 수 있다고 생각했는데 그 이유는 Key 값에 대해서 오버헤드가 발생할 수 있기 때문이다.

 

이 데이터는

1. 유저가 좋아요 요청시, 이미 존재하는지 확인

2. 삽입

의 로직을 진행하므로 String 자료구조를 활용, "userId" 라는 key에 "postId" 라는 value로 저장한다.

 

 

즉 100만명의 유저가 100만개의 게시글에 대해 요청한다면 결국 1조개의 key를 필요로 한다. 

 

다음 예시를 보자

2명의 유저 1과 2가 게시글 1에 대해 좋아요를 눌렀을 때, 112바이트를 차지한다.

 

따라서 기존에는 String type으로 저장을 하여 N명의 유저가 각각 M개의 게시글에 대해 좋아요를 했다면, N * M * 56 바이트를 차지한다.

=> String type으로 저장시 key의 개수의 증가 rate은 O(NM) 이다.

 

만약 Set type으로 저장을 한다면 (key - "userId", value - "postId"), 각각의 유저만큼 key가 필요하게 된다. 즉 key는 O(N)만큼 필요하다. 다음 예시를 보자.

유저 1이 4개의 게시글에 대해 좋아요를 하였으나 메모리 사용량은 72바이트를 유지하고 5개 부터 88로 증가한다. 메모리 사용률이 1명의 유저가 좋아요한 게시글의 수에 비례하였던 String type에 비하면 메모리 사용률이 더 낮다.

 

=> Set Type으로 저장시 key의 개수 증가 비율은 O(N) 이다.

 

만약 "postId" 라는 key에 "userId"라는 value로 저장해도 다름이 없다. 유저 1이 게시글 1,2에 좋아요를 눌러도 결과는 같을 것이다.

 

하지만 아직 100만 명의 유저가 요청시 key를 100만개나 만들어야 된다. Key를 아예 1개로 만들어 버리기.

 

"likes" 라는 하나의 key에서 "userId:postId" 라는 field를 삽입하기

한명의 유저가 M개의 게시글에 좋아요 시에도, N명의 유저가 게시글에 좋아요 시에도, key는 1개만 필요하다. 위의 사례들에 비해 메모리 사용량이 현저히 줄어든다.

 

=>  key의 개수 증가 비율은 O(1) 이다.

 

Comparison : User 1,2가 게시글 1,2에 대해 좋아요시,

  • String type : 224B
  • Set type 1안 : 144B
  • Set type 2안 : 88B

Summary : 100만명의 유저가 10개의 게시글에 좋아요 했을 때, 메모리 사용량 비교

  • String type : 651.21 M
  • Set Type 1안 : 101.02M
  • Set type 2안 : 391.18 M

예상과는 다르게 Set Type : 1안이 메모리 사용량이 가장 적었다. 1은 100만개의 key, 2안은 1개의 key로 2안이 더 메모리 사용량이 적을 거라고 예측 했으나 내가 간과했던 것은 단순히 key의 개수 뿐만 아니라 각 key에 배정된 value의 개수가 다르다는 것이다.

 

Redis의 Set은 적은 수의 정수를 저장하면 intset으로 압축해서 저장한다. 단 set의 멤버개수가 512를 넘어가면 해시테이블로 전환된다.

2안은 하나의 키에 1000만개의 멤버가 있으므로 hashtable로 전환한다.

(intset은 내부적으로 오름차순 정렬된 배열로 이분탐색, 삽입 + 쉬프트를 수행한다)

 

hashtable로 전환시,

1. 2의 배수씩 버킷 예약을한다. 1000만개라면, 버킷 수가  2^24 까지 확장이 되어있을 것이다.

2. value별 dictEntry는 next pointer, value, key 등에 관한 포인터가 있어 24바이트 정도 필요하다.

 

 

결과적으로 Set Type 1안을 선택함으로 써 가정했던 상황에 대해 메모리 사용량을 651.21 MB에서 101.02 MB로 감소시켰다.