Clarifications about Redis and Memcached

Salvatore Sanfilippo

Redis와 Memcached에 대한 해명

저를 아시는 분이라면 제가 경쟁 제품을 나쁘게 보는 사람이 아니라는 걸 아실 겁니다. 오히려 사용자에게 선택지가 있는 걸 좋아해서 Redis를 다른 기술과 비교하는 일은 거의 하지 않습니다.

하지만 올바른 솔루션을 고르려면 사용자가 정확하게 알고 있어야 한다는 것도 사실입니다.

이 글은 Mike Perham이 쓴 블로그 글을 읽고 쓰게 되었습니다. Sidekiq이라는 유명한 라이브러리의 저자로 아실 텐데, 그 라이브러리는 백엔드로 Redis를 사용합니다. 그래서 Mike가 Redis에 “반대”하는 사람이라고는 전혀 생각하지 않습니다. 그럼에도 그는 해당 글(http://www.mikeperham.com/2015/09/24/storing-data-with-redis/에서 보실 수 있습니다)에서 캐싱 용도라면 “[Redis] 대신 Memcached를 쓰는 게 아마 더 낫다”고 말합니다. 즉 Mike는 Redis가 캐싱에 좋지 않다고 진심으로 믿고 있으며, 그 근거를 다음과 같이 듭니다.

  1. Memcached는 캐싱을 위해 설계되었다.
  2. 디스크 I/O를 전혀 수행하지 않는다.
  3. 멀티스레드이며 멀티코어를 활용해 수십만 건의 요청을 처리할 수 있다.

위 주장들을 하나씩 짚어보고, 이후에는 위 문장들에는 담기지 않았지만 대부분의 캐싱 사용자와 사용 사례에 있어 더 중요하다고 생각되는 정보들을 추가로 다루겠습니다.

Memcached는 캐싱을 위해 설계되었다: 이건 논거가 되지 않으므로 넘어가겠습니다. “Redis는 캐싱을 위해 설계되었다”라고 말할 수도 있으니까요. 이 점에서는 두 시스템이 완전히 같습니다. 다음으로 넘어가겠습니다.

디스크 I/O를 전혀 하지 않는다: Redis에서도 원한다면 디스크 I/O를 완전히 끌 수 있어 순수한 인메모리 경험을 제공합니다. 다만 정말 필요하다면 재부팅할 때만 데이터베이스를 저장하도록 할 수도 있습니다. 예를 들어 “SHUTDOWN SAVE” 명령이 그렇습니다. 요점은 Redis의 영속성이 전혀 사용하지 않을 때조차 부가 가치라는 점입니다.

멀티스레드다: 이건 사실입니다. 제 목표 중에도 Redis의 I/O를 스레드로 처리하는 것이 있습니다(Memcached처럼, 사실 데이터 접근 자체는 스레드로 처리되지 않습니다). 하지만 Redis는 특히 파이프라이닝을 사용하면 스레드당 초당 처리량이 매우 놀랍습니다(파이프라이닝을 적극 사용하면 50만 건이 흔한 수치이며, 파이프라이닝 없이도 약 100,000 ops/sec 정도입니다). 각 Redis 인스턴스가 동일하고 모두 마스터로 동작하며 디스크 연산을 끄고 “memcached 샤딩 모델”처럼 샤딩을 클라이언트에 맡기는 전형적인 캐싱 시나리오에서는 시스템당 여러 Redis 프로세스를 띄우는 것도 나쁘지 않습니다. 이렇게 하면 shared-nothing 방식의 멀티스레드 구성이 되므로 중요한 것은 단일 스레드당 처리할 수 있는 연산량입니다. 마지막으로 확인했을 때 Redis는 스레드당 성능이 적어도 Memcached만큼은 빨랐습니다. 구현은 시간이 지나며 바뀌므로 오늘날 우위가 어느 쪽에 있을지는 모르지만, 둘 다 가용한 자원을 극대화하려는 만큼 성능은 비슷할 거라고 봅니다. Memcached의 멀티스레딩은 사용과 관리를 더 단순하게 만든다는 점에서 여전히 장점이지만, 결정적인 요소는 아니라고 생각합니다.

더 할 이야기가 있습니다. Mike는 연산의 *질*에 대해서는 언급하지 않고 초당 연산 수만 이야기합니다. Redis나 Memcached 같은 시스템에서는 명령 디스패치와 I/O 비용이 실제 인메모리 자료구조를 건드리는 비용보다 압도적으로 큽니다. 그래서 Redis에서는 단순한 GET이나 SET을 실행하는 비용과 ZRANK 같은 복잡한 연산을 실행하는 비용이 거의 같습니다. 하지만 복잡한 연산으로 애플리케이션 관점에서 훨씬 더 많은 일을 해낼 수 있습니다. 캐시된 값 다섯 개를 가져오는 대신 작은 Lua 스크립트 하나를 보내는 식이죠. 따라서 두 시스템의 실질적인 “확장성”은 여러 차원을 가지고 있으며, 무엇을 달성할 수 있는가도 그중 하나입니다.

Mike가 제기한 우려 중 제가 보기에 유효한 것은 멀티스레딩뿐이며, Redis를 Memcached 대체라는 특수한 경우로 본다면 이는 여러 프로세스를 실행하거나, 혹은 그냥 하나만 실행하는 것으로도 해결할 수 있습니다. Memcached와 유사한 연산을 수행하면서 단일 스레드를 포화시키는 것 자체가 매우 어렵기 때문입니다.

진짜 차이점

이제 두 시스템 간의 *진짜* 차이점에 대해 이야기할 때입니다.

메모리 효율성

이 부분은 예전에 Memcached가 Redis보다 나았던 지점입니다. 단순한 문자열 대 문자열 사전을 표현하도록 설계된 시스템에서는 메모리를 더 효율적으로 쓰기가 더 쉽습니다. 이 차이는 극적이지 않고 제가 확인하지 않은 지도 5년 정도 되었지만, 예전에는 눈에 띄는 정도였습니다.

하지만 장시간 구동되는 프로세스의 메모리 효율성을 따지면 이야기가 조금 달라집니다. 다음 섹션을 읽어 보시기 바랍니다.

다만 메모리 효율성을 제대로 평가하려면 Redis에서 작게 집계된 값들이 특수하게 인코딩되어 매우 메모리 효율적으로 저장된다는 점도 고려해야 합니다. 예를 들어 작은 정수들로 이루어진 집합은 내부적으로 8, 16, 32 또는 64비트 정수 배열로 표현되며, 정렬되어 있어 이진 탐색을 사용할 수 있으므로 존재 여부를 확인할 때 로그 시간에 접근할 수 있습니다.

JSON을 쓰는 대신 해시를 이용해 객체를 표현할 때도 마찬가지입니다. 따라서 실제 메모리 효율성은 구체적인 사용 사례를 놓고 평가해야 합니다.

Redis LRU vs 슬랩 할당자

메모리 활용 측면에서도 Memcached가 완벽하지는 않습니다. 시간이 지나며 캐시 값의 크기가 크게 바뀌는 애플리케이션을 운영한다면 심각한 단편화를 겪을 가능성이 높고, 유일한 해결책은 재시작뿐입니다. 이 관점에서 Redis는 훨씬 더 결정적입니다.

게다가 Redis LRU는 최근 크게 개선되어 이제는 실제 LRU에 매우 가깝게 근사합니다. 자세한 정보는 여기에서 확인할 수 있습니다: http://redis.io/topics/lru-cache. 제가 이해한 바로는 Memcached의 LRU는 여전히 슬랩 할당자에 따라 만료가 이루어지므로 때로는 실제 LRU와 동떨어진 동작을 보일 수 있지만, 이 부분에 대해서는 전문가들의 의견을 듣고 싶습니다. Redis LRU를 테스트해 보고 싶다면 최근 버전의 Redis에서 제공하는 redis-cli LRU 테스트 모드를 사용할 수 있습니다.

스마트 캐싱

Redis를 캐싱 용도로 쓰면서 Memcached처럼만 사용한다면 정말 큰 기회를 놓치는 겁니다. 제 생각에 이게 Mike 글에서 가장 큰 오류입니다. 사람들이 점점 더 Redis로 옮겨가는 이유는 캐시 데이터를 훨씬 더 유용한 형태로 표현할 수 있다는 걸 알게 되었기 때문입니다. 어떤 항목의 최신 N개를 유지하고 싶나요? capped list를 쓰면 됩니다. 캐시된 인기 지수를 관리하고 싶나요? sorted set을 쓰면 됩니다. 이런 식입니다.

영속성과 레플리케이션

이런 기능이 필요하다면 매우 중요한 자산이 됩니다. 예를 들어 이 모델을 사용하면 막대한 읽기 부하를 확장하는 것이 매우 간단합니다. 영속성을 활용한 재시작, 시간에 따른 캐시 스냅샷 생성 등도 마찬가지입니다. 물론 두 기능이 전혀 중요하지 않은 사용 사례가 있는 것도 당연합니다. 여기서 말하고 싶은 것은 “순수 캐싱” 사용 사례 중에서도 영속성과 레플리케이션이 중요한 경우가 있다는 점입니다.

가시성

Redis는 가시성이 매우 뛰어납니다. 수많은 내부 지표에 대한 상세한 보고를 제공하며, 데이터셋을 SCAN하고 객체의 만료를 관찰하며, LRU 알고리즘을 튜닝할 수 있습니다. 클라이언트에 이름을 붙여 CLIENT LIST에서 확인할 수도 있고, “MONITOR”로 애플리케이션을 디버깅하는 등 여러 고급 기능을 제공합니다. 저는 이것이 장점이라고 생각합니다.

Lua 스크립팅

Lua 스크립팅이 많은 캐싱 사용 사례에서 큰 도움이 된다고 생각합니다. 예를 들어 캐시된 JSON blob이 있을 때 Lua 명령으로 단일 필드만 추출해 클라이언트에 반환함으로써 전체를 전송하지 않아도 됩니다(개념적으로는 Redis 해시를 직접 사용해 객체를 표현함으로써 같은 일을 할 수도 있습니다).

결론

Memcached는 훌륭한 소프트웨어입니다. 소스 코드를 여러 번 읽어 봤고, 우리 업계에 혁명을 가져온 작품이며, 여러분에게 Redis보다 더 나은 선택인지 직접 확인해 보셔야 합니다. 하지만 사물은 있는 그대로 평가되어야 하고, 결국 저는 Mike의 글과 수년간 봐 온 비슷한 글들을 읽으며 조금 답답함을 느꼈습니다. 그래서 제 관점을 보여드리기로 했습니다. 사실과 다른 부분이 있다면 알려 주시면 “EDIT” 섹션을 추가해 글을 수정하겠습니다.

원문은 Salvatore Sanfilippo님이 에 게재했습니다.

이 글은 muse-spark-1.2-contributor 모델을 사용해 번역했습니다.