Redis에 심플한 key-value 로 수 억개의 데이터 저장하기

해당 글은 http://instagram-engineering.tumblr.com/post/12202313862/storing-hundreds-of-millions-of-simple-key-value-pairs 라는 instaram에서 redis를 사용한 경험에 대한 글을 “발번역”하고 제 의견을 추가한 것입니다.

과도기 적인 시스템을 운영중에는, 때때로 작은 기반 시스템을 구축해야 할 경우가 생깁니다. 인스타그램에서도,  최근에 그런 작업을 진행해야만 했습니다. 3억 개의 사진을 각각 어떤 userID 와 매핑되고, 샤딩 서버로 쿼리를 보낼 지에 대한 정보를 유지하고 만들어야 했습니다. (관련 정보는 http://instagram-engineering.tumblr.com/post/10853187575/sharding-ids-at-instagram 를 보면 됩니다.)

클라이언트와 API 어플리케이션이 업데이트되고, 이에 대한 정보를 전달하는 동안에는, 여전히 이전 정보를 캐쉬하고 있었습니다. 그래서 다음과 같은 요구사항에 대한 해결책이 필요했습니다.

1. 키를 빨리 찾고, 빨리 값을 리턴해야 한다.

2. 데이터는 메모리에 저장되어야 하고, 이상적으로 EC2 high-memory types( 68GB 보다는, 17GB나 34GB   ) 이내에 들어가야 한다.

(역자 주: EC2에서 가격이 34GB는 17GB 의 2배, 68GB는 17GB의 4배 입니다.)

3. 기존 구조에 적합해야 한다.

4. 서버가 죽어도 다시 데이터를 새로 생성하지 않도록, persistent 해야 한다. (여기의 의미는, 다른 DB나 로그에서 데이터를 다시 재생성할 필요 없이, 저장된 데이터를 그대로 나중에 다시 복원할 수 있어야 한다라는 느낌입니다.)

간단한 해결책은 “Media ID”  와   “User ID” 라는 컬럼을 가지는 형태로 데이터베이스에 저장하는 것이었습니다.  그러나  SQL Database 는  insert 만 제공한다거나, 트랙잰션이 필요없을  경우,  그리고 다른 테이블과도 아무런 연관관계가 없는 이런 데이터에서는 너무 과도한 방법으로 보였습니다.

그래서 우리는 Redis 로 방향을 돌렸습니다.  Redis는  인스타그램에서 널리 사용하는 고성능 Key-Value 저장소입니다.( 예를 들어,  Redis는 인스타그램의 메인 저장소입니다.) Redis 는 일반적인 memcached에 비해서 스위스 군용 나이프 같습니다.  Redis는 sorted sets 과 list 와 같은 파워풀한 Collection을 제공합니다.

(역자 주: memcached는 단순한 키에 대한 set, get 정도만 제공합니다. 그리고 memcached는 store 라기 보다는 Cache로 생각하는 것이 좋습니다.)

그리고 Redis 는 persistence 를 설정으로 사용할 수 있습니다. 백그라운드로 지정된 시간마다, 메모리 상태를  저장합니다. 그리고 Master-Slave 형태로 동작이 가능합니다.(  역자 주: redis 에서는 현재 메모리 상태를 disk로 snapshot을 만드는 기능이 있습니다.  그리고 성능은 떨어지지만, 메모리가 부족할 때 Virtual Memory로 동작할 수 있는 기능이 존재합니다.  자세한 정보는 http://redis.io/documentation 를 참고하세요.) 우리의 모든 redis 서버는 master-slave 형태로 동작하고, Slave는 매분마다 메모리의 Snapshot을 디스크에 저장합니다.

At first, we decided to use Redis in the simplest way possible: for each ID, the key would be the media ID, and the value would be the user ID:

처음에 우리는 Redis를 최대한 간단한 방법으로 사용하기로 했습니다. 각각의 아이디는 media ID로 구성되고, 그 값은 userID가 되는 것입니다.

SET media:1155315 939 
GET media:1155315 
> 939

이 방법에 대한 프로토타이핑 중에, 우리는 redis 가 백만개의 키들을 저장하는 70MB 정도가 필요하다는 것을 알았습니다. 3억개로 확장하면, Amazon EC2의 17GB 모델 보다 큰 21GB 정도의 메모리가 필요했습니다.우리는 언제나 Redis의 코어 개발자중의 한명인 Pieter Noordhuis 문의했습니다. 그는 Redis hashes 를 제안했습니다. Redis의 Hashes는 메모리에 매우 효과적으로 인코딩된 사전 구조로 되어있습니다. Redis의 hash-zipmap-max-entries’ Setting은 효과적으로 인코딩된 해쉬를 저장하는 엔트리의 최대 개수를 설정합니다. 우리는 이 값을 대략 1000 으로 저정하는 것이 HSET 에서 가장 CPU 효율이 좋은 것을 알았습니다.
자세한 정보는 https://github.com/antirez/redis/blob/unstable/src/zipmap.c 를 보시면 됩니다.)

hash type 의 장점을 얻기 위해서, 우리는 Media ID를 1000개의 bucket 으로 나누었습니다.( 단순히 ID를 1000으로 나누고, 나머지는 버렸습니다.)hash 는 Media ID를 키로 가지고, User ID를 값으로 가집니다.예를 들어, 1155315 라는 Media ID가 주어지면, bucket 1155로 할당됩니다.( 1155315 / 1000 = 1155 ):

HSET "mediabucket:1155" "1155315" "939" 
HGET "mediabucket:1155" "1155315" 
> "939"

메모리 사이즈 차이는 꽤 충격적이었습니다. 백만개의 키의 경우( 1000 hashes 가 1000개의 subkey를 가질 때)  Redis는 오직 16MB 만 사용하였습니다. 3억개로 확장한다면, 최대 5GB 이하였습니다.  사실 상 더 싼 아마존 ec2의 m1.large type 에 맞출 수 있습니다. (역자 주:  m1.large는 7.5GB의 메모리를 제공하고 한시간에 $0.34 입니다. 아까 말한 17GB m2.xlarge는 한시간에 $0.5 입니다. 대략 30% 정도 이득입니다.) 거기다가, 우리가 필요했던 장비의(34GB 모델 기준) 1/3 정도 가격입니다. 더 좋은 것은 hashes 는 O(1)의 속도로 검색이 매우 빠릅니다. 이 조합에 대해서 관심이 있다면,  available as a Gist on GitHub( Memcached 역시 비교군으로 들어가 있습니다. memcached는 백만개에 52MB 정도를 사용합니다.) 그리고 이런 종류의 문제에 관심이 있다면, 우리는 고용중입니다.( http://instagram.com/about/jobs/)

(역자 주: 최초에는 단순 Set이 HSet 으로 바뀌면서 Key에 들어가야 하는 부분이 공통 버켓이름으로 대체되어서 데이터가 줄어든 거라고 착각하고 있었습니다. 그런데, 이런 것은 아니라  zipmap 을 통해서 실제로 해시에 저장되는 방식이, 메모리를 덜 낭비하도록 만드는 것입니다. 이 때, key 나 value 가 압축되거나 하지는 않습니다. 다만, 저장에 필요한 다른 값들을 최대한으로 적게 사용하는 것으로 보입니다.)