이것이 레디스다
참고문헌
정경석. 이것이 레디스다. 서울:한빛미디어. 2013.
1장 들어가며
빅데이터란 무엇인가?
일반적으로는 대략 10테라바이트에서 10페타바이트 정도의 데이터를 일컫는다.
빅데이터에 대한 정의는 크게 두 가지다. 하나는 2011년도에 MGI에서 정의한 데이터베이스의 규모(데이터의 크기)에 기반한 분류 방법이고 다른 하나는 같은 해 EMC에서 정의한 데이터의 처리 방법에 기반한 분류 방법이다.
빅데이터의 두 번째 정의는 데이터의 처리방식에 기반하여 분류하는 것을 말하며, 정형화된 데이터가 아닌 비정형 데이터를 처리하는 기술 및 아키텍처를 말한다.
NoSQL의 탄생 배경
전통적인 데이터 처리 방법인 SQL 데이터베이스 또는 전통적인 데이터 처리 애플리케이션만으로는 처리가 불가능한 크기의 데이터를 처리하기 위해 나온 기술들의 총칭이 NoSQL 이다.
각 단일 서버(하드웨어)의 성능을 증가시켜서 더 많은 요청을 처리하는 방법을 스케일 업(scale up)이라고 한다. 동일한 사양의 새로운 서버(하드웨어)를 추가하는 방법을 스케일 아웃(scale out)이라고 한다. 통상적으로 스케일 업을 적용하면 서비스의 중단이나 추가 하드웨어 비용이 발생한다.
스케일 업은 명확한 한계가 존재한다. 하나의 장비에 설치할 수 있는 CPU 및 메모리와 디스크 수에는 한계가 있기 때문이다. 또한 소프트웨어 구조상 아무리 스케일 업을 한다고 해도 상승 가능한 물리적인 한계가 존재한다.
이에 반해서 대부분의 NoSQL은 처음부터 스케일 아웃을 염두에 두고 설계되었기 때문에 데이터의 증가나 요청량이 증가하더라도 동일하거나 비슷한 사양의 새로운 하드웨어를 추가하면 대응이 가능하다.
레디스와 멤캐시드
멤캐시드(Memcached)와 레디스를 같은 캐시 시스템으로서 동등한 위치에서 비교하게 된 것은 레디스가 멤캐시드와 동일한 기능을 제공하면서 영속성, 다양한 데이터 구조와 같은 부가적인 기능을 지원하기 때문이다. 또한 특정한 조건에서는 멤캐시드에 비해서 더 나은 성능을 보여주기도 한다. 이와 같은 이유로 레디스는 맴캐시드를 대체하는 솔루션으로서 각광받고 있다.
레디스의 개요
레디스는 모든 데이터를 메모리에 저장하고 조회한다. 즉, 인메모리 데이터베이스 솔루션이다. 다른 인메모리 솔루션들과의 차이점 중 가장 특별한 점은 레디스의 ‘다양한 자료구조’다.
- 영속성을 지원하는 인메모리 데이터 저장소다.
- 읽기 성능 증대를 위한 서버 측 복제를 지원한다.
- 쓰기 성능 증대를 위한 클라이언트 측 샤딩(Sharding)을 지원한다.
- ANSI C로 작성됐다. 따라서 ANSI C 컴파일러가 동작하는 곳이면 어디든 설치 및 실행이 가능하다.
- 레디스 클라이언트는 대부분의 언어로 포팅되어 있다.
- 네이버, 라인, 깃허브, 블리자드, 트웻덱, 야후 플리커, 중국 시나웨이보 등 많은 서비스에서 사용되고 있으며 성능적으로 검증된 솔루션이다.
- 문자열, 리스트, 해시, 셋, 정렬된 셋과 같은 다양한 데이터형을 지원한다. 메모리 저장소임에도 불구하고 많은 데이터형을 지원하므로 다양한 기능을 구현할 수 있다.
레디스는 고성능 키-값 저장소로서 문자열, 리스트, 해시, 셋, 정렬된 셋 형식의 데이터를 지원하는 NoSQL 이다.
2장 빨리 시작해보기
- 리눅스 환경에 설치하기
- 윈도우 환경에 설치하기
간단한 레디스 명령 실행하기
- 레디스 성능 측정
redis-benchmark 명령을 실행하면 아래와 같은 로그와 함께 성능 측정 결과가 출력된다.
redis-benchmark
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238C:\Program Files\Redis>redis-benchmark
====== PING_INLINE ======
100000 requests completed in 1.80 seconds
50 parallel clients
3 bytes payload
keep alive: 1
88.10% <= 1 milliseconds
95.23% <= 2 milliseconds
99.86% <= 3 milliseconds
99.94% <= 4 milliseconds
99.98% <= 5 milliseconds
100.00% <= 5 milliseconds
55432.37 requests per second
====== PING_BULK ======
100000 requests completed in 1.66 seconds
50 parallel clients
3 bytes payload
keep alive: 1
89.51% <= 1 milliseconds
99.51% <= 2 milliseconds
99.93% <= 3 milliseconds
99.98% <= 4 milliseconds
100.00% <= 5 milliseconds
100.00% <= 5 milliseconds
60096.15 requests per second
====== SET ======
100000 requests completed in 1.75 seconds
50 parallel clients
3 bytes payload
keep alive: 1
86.08% <= 1 milliseconds
98.35% <= 2 milliseconds
99.44% <= 3 milliseconds
99.61% <= 4 milliseconds
99.74% <= 5 milliseconds
99.82% <= 6 milliseconds
99.88% <= 7 milliseconds
99.95% <= 8 milliseconds
99.98% <= 9 milliseconds
99.99% <= 10 milliseconds
100.00% <= 10 milliseconds
57110.22 requests per second
====== GET ======
100000 requests completed in 1.61 seconds
50 parallel clients
3 bytes payload
keep alive: 1
91.87% <= 1 milliseconds
99.94% <= 2 milliseconds
99.97% <= 3 milliseconds
100.00% <= 4 milliseconds
100.00% <= 4 milliseconds
62034.74 requests per second
====== INCR ======
100000 requests completed in 1.67 seconds
50 parallel clients
3 bytes payload
keep alive: 1
88.67% <= 1 milliseconds
99.91% <= 2 milliseconds
99.96% <= 3 milliseconds
99.99% <= 4 milliseconds
100.00% <= 4 milliseconds
59808.61 requests per second
====== LPUSH ======
100000 requests completed in 1.65 seconds
50 parallel clients
3 bytes payload
keep alive: 1
88.91% <= 1 milliseconds
99.89% <= 2 milliseconds
99.96% <= 3 milliseconds
100.00% <= 4 milliseconds
100.00% <= 4 milliseconds
60642.81 requests per second
====== RPUSH ======
100000 requests completed in 1.69 seconds
50 parallel clients
3 bytes payload
keep alive: 1
90.43% <= 1 milliseconds
99.90% <= 2 milliseconds
99.99% <= 3 milliseconds
100.00% <= 3 milliseconds
59171.59 requests per second
====== LPOP ======
100000 requests completed in 1.65 seconds
50 parallel clients
3 bytes payload
keep alive: 1
91.41% <= 1 milliseconds
99.91% <= 2 milliseconds
99.98% <= 3 milliseconds
100.00% <= 3 milliseconds
60569.35 requests per second
====== RPOP ======
100000 requests completed in 1.71 seconds
50 parallel clients
3 bytes payload
keep alive: 1
87.03% <= 1 milliseconds
99.75% <= 2 milliseconds
99.98% <= 3 milliseconds
100.00% <= 3 milliseconds
58479.53 requests per second
====== SADD ======
100000 requests completed in 1.91 seconds
50 parallel clients
3 bytes payload
keep alive: 1
78.23% <= 1 milliseconds
98.87% <= 2 milliseconds
99.82% <= 3 milliseconds
99.97% <= 4 milliseconds
100.00% <= 4 milliseconds
52219.32 requests per second
====== SPOP ======
100000 requests completed in 1.90 seconds
50 parallel clients
3 bytes payload
keep alive: 1
80.69% <= 1 milliseconds
99.49% <= 2 milliseconds
99.89% <= 3 milliseconds
99.96% <= 4 milliseconds
99.99% <= 5 milliseconds
100.00% <= 5 milliseconds
52631.58 requests per second
====== LPUSH (needed to benchmark LRANGE) ======
100000 requests completed in 1.72 seconds
50 parallel clients
3 bytes payload
keep alive: 1
88.08% <= 1 milliseconds
99.85% <= 2 milliseconds
99.97% <= 3 milliseconds
100.00% <= 3 milliseconds
58207.21 requests per second
====== LRANGE_100 (first 100 elements) ======
100000 requests completed in 3.01 seconds
50 parallel clients
3 bytes payload
keep alive: 1
77.33% <= 1 milliseconds
99.44% <= 2 milliseconds
100.00% <= 3 milliseconds
100.00% <= 3 milliseconds
33200.53 requests per second
====== LRANGE_300 (first 300 elements) ======
100000 requests completed in 6.60 seconds
50 parallel clients
3 bytes payload
keep alive: 1
0.00% <= 1 milliseconds
93.36% <= 2 milliseconds
99.79% <= 3 milliseconds
99.99% <= 4 milliseconds
100.00% <= 5 milliseconds
15142.34 requests per second
====== LRANGE_500 (first 450 elements) ======
100000 requests completed in 8.73 seconds
50 parallel clients
3 bytes payload
keep alive: 1
0.00% <= 1 milliseconds
7.93% <= 2 milliseconds
99.13% <= 3 milliseconds
99.90% <= 4 milliseconds
99.97% <= 5 milliseconds
99.99% <= 6 milliseconds
99.99% <= 7 milliseconds
100.00% <= 8 milliseconds
100.00% <= 8 milliseconds
11458.69 requests per second
====== LRANGE_600 (first 600 elements) ======
100000 requests completed in 10.70 seconds
50 parallel clients
3 bytes payload
keep alive: 1
0.00% <= 1 milliseconds
0.01% <= 2 milliseconds
94.34% <= 3 milliseconds
99.92% <= 4 milliseconds
99.96% <= 5 milliseconds
99.98% <= 6 milliseconds
99.99% <= 7 milliseconds
100.00% <= 8 milliseconds
100.00% <= 9 milliseconds
9344.05 requests per second
====== MSET (10 keys) ======
100000 requests completed in 1.77 seconds
50 parallel clients
3 bytes payload
keep alive: 1
85.43% <= 1 milliseconds
99.74% <= 2 milliseconds
99.93% <= 3 milliseconds
99.97% <= 4 milliseconds
100.00% <= 5 milliseconds
56529.11 requests per second
C:\Program Files\Redis>
3장 NoSQL
NoSQL의 개념과 정의
- 대용량 웹 서비스를 위하여 만들어진 데이터 저장소
- 관계형 데이터 모델을 지양하며 대량의 분산된 데이터를 저장하고 조회하는 데 특화된 저장소
- 스키마 없이 사용 가능하거나 느슨한 스키마를 제공하는 저장소
구글의 빅에티블, 아마존의 다이나모와 같은 논문 및 자료는 데이터를 실시간으로 분산 처리하는 개념을 세상에 알려주었다. 시간이 지남에 따라 이 개념들을 토대로 실시간 분산 처리를 위한 오픈소스 솔루션들이 속속 개발되었고, 이와 같은 노력의 결과물이 NoSQL이다.
CAP 정리
CAP 정리란, 이론 컴퓨터 과학 분야에서 분산 컴퓨터 시스템을 설명하는 데 사용되는 이론이다. ‘일관성(Consistency), 가용성(Availability), 분할 허용성(Partition Tolerance) 모두를 동시에 지원하는 분산 컴퓨터 시스템은 없다’라고 정의되어 있다.
일관성
일관성은 동시성 또는 동일성이라고도 하며 ‘다중 클라이언트에서 같은 시간에 조회하는 데이터는 항상 동일한 데이터임을 보증하는 것’을 의미한다.
가용성
가용성이란 ‘모든 클라이너트의 읽기와 쓰기 요청에 대하여 항상 응답이 가능해야 함을 보증하는 것’이며 내고장성이라고도 한다. 시스템 아키텍처 설계에서 말하는 고가용성(High Availability)와 유사한 개념으로 볼 수 있다.
가용성을 설명하면서 빠지지 않는 단어가 단일 고장점 SPOF(Single Point Of Failure)이다. 단일 고장점이란 시스템을 구성하는 개별 요소 중에서 하나의 요소가 망가졌을 때 시스템 전체를 멈추게 만드는 요소를 말한다.
네트워크 분할 허용성
지역적으로 분할된 네트워크 환경에서 동작하는 시스템에서 두 지역 간의 네트워크가 단절되거나 네트워크 데이터의 유실이 일어나더라도 각 지역 내의 시스템은 정상적으로 동작해야 함을 의미한다.
NoSQL 분류와 저장구조
저장소는 크게 키-값 모델, 문서 모델, 컬럼 모델, 그래프 모델로 분류할 수 있다.
키-값 모델
대부분의 키-값 모델 NoSQL은 단순한 저장구조로 인하여 복잡한 조회 연산을 지원하지 않는다. 또한 고속 읽기와 쓰기에 최적화된 경우가 많다.
레디스, 리악(Riak), 다이나모, 볼드모트 등이 있다.
문서 모델
하나의 키에 하나의 구조화된 문서(JSON, BSON, XML 등)를 저장하고 조회한다.
키-값 및 컬럼모델 NoSQL에 비하여 상대적으로 많은 종류의 기능을 제공.
B트리의 특성으로 인하여 한 번 작성되면 자주 변하지 않는 정보를 저장하고 조회하는데 적합하다. 예를 들어 중앙 집중식 로그 저장, 타임라인 저장, 통계 정보 저장 등이 이에 해당된다.
MongoDB, 카우치베이스, 테라스토어, 레이븐DB 등이 있다.
컬럼 모델
하나의 키에 여러 개의 컬럼 이름과 컬럼 값의 쌍으로 이루어진 데이터를 저장하고 조회한다. 단일 키에 의한 단일 컬럼 및 범위 조회도 가능하다. 모든 컬럼은 항상 타임스탬프 값과 함께 저장된다. 컬럼 모델 NoSQL에서 키는 로우키라 불린다.
HBase, 카산드라, 하이퍼 테이블 등이 있다.
그래프 모델
그래프모델 NoSQL은 노드와 관계(또는 버텍스와 엣지라고 한다)를 사용하여 데이터를 저장하고 조회하는데 관계는 속성이라는 부가 정보를 가진다.
NoSQL을 언제 어떻게 사용해야 하는가?
대량의 단순 정보를 빠르게 저장하고 조회할 때, 관계형 데이터베이스가 처리하지 못하는 대량의 데이터를 입력할 때(보통 수십 기가바이트의 데이터), 스키마가 고정되지 않은 데이터를 저장하고 조회할 때 등이 해당된다.
관계형 데이터베이스에서 NoSQL로 데이터 저장소를 변경할 때, 가장 먼저 고려해야 할 사항은 일관성 모델이다. 구현된 서비스에 강한 일관성 모델이 필요한지, 느슨한 일관성 모델을 사용해도 큰 문제가 되지 않는지에 대한 판단이 우선되어야 한다.
고려해야 할 사항들 : 일관성 모델, 데이터 모델, 읽기 쓰기 성능, 단일 고장점, 원자성 지원, 하드웨어 구성, 무중단 시스템
4장 레디스 시작
레디스 자료구조
문자열
저장 가능한 문자열의 크기는 최대 512MB이다.
해시 데이터
해시 데이터는 키 하나가 여러 개의 필드-값 쌍으로 이루어진다. 일반적인 프로그래밍 언어의 맵 자료구조와 동일하다.
셋 데이터
셋 데이터는 중복을 허용하지 않는 집합 형태의 자료구조이며, 정렬되어 있지 않다. 셋 데이터에는 2^32-1개의 값을 저장할 수 있다. 셋 데이터의 내부 구현은 해시 구조이기 때문에 저장된 요소의 개수에 상관없이 동일한 시간 복잡도를 가진다.
정렬된 셋 데이터
셋 데이터와 유사한 구조를 가지고 있기 때문에 셋 데이터와 동일한 특징을 가지면서 요소 정렬이라는 부가적인 특징을 가진다. 정렬된 셋 데이터는 셋 데이터 구조에 요소의 가중치 값이 추가되어 있다. 가중치에 따라서 각 요소의 정렬이 수행되는데, 기본 정렬 순서는 오름차순이다. 정렬된 데이터 셋 구조는 셋 데이터와 동일하게 2^32-1개의 요소를 저장할 수 있으며, 가중치에 입력할 수 있는 값은 정수 똔느 배정밀도 부동소수점이다. 정렬된 셋 데이터는 주로 실시간 순위를 계산하는 데 사용된다.
리스트 데이터
저장순서를 기억하는 데이터 구조로 중복을 허용한다. 저장 가능한 최대 요소 수는 2^32-1개이다. 레디스의 리스트 데이터는 이중 연결 리스트로 구현되어 있기 때문에 첫 번째 요소와 마지막 요소의 조회 시간이 가장 빠르며 리스트의 중간에 저장된 데이터를 조회하는 시간이 가장 오래 걸린다.
주요 명령 예제
키 관리
삭제, 만료, 목록 조회, 직렬화, 역 직렬화, 데이터형 조회, 키 이동 등이 있다.
키의 만료
저장된 키에 대하여 만료시간을 설정할 수 있다(레디스가 멤캐시드와 같은 캐시 시스템과 비교되는 이유가 바로 이 기능 때문이다). 키의 만료란 지정된 시간이 되면 키와 키에 저장된 데이터가 사라지게 되어 조회할 수 없는 상태로 변하는 것을 말하는데, 레디스가 제공하는 만료 처리 방법은 ‘지정된 시간에 만료’와 명령이 수행된 이후부터 일정 시간 이후의 만료’를 지원한다.
5장 레디스 클라이언트
자바 클라이언트
- Jedis
- JRedis
- JDBC-Redis
- RJC
- rdis-protocol
- lettuce
예제 5-1 Hello Jedis
1 | package net.sf.redisbook.ch5; |
예제 5-1은 매번 실행될 때마다 새로운 레디스 연결을 생성한다. 이와같이 데이터의 요청마다 새로운 커넥션을 만들 수도 있지만 서버 관점에서는 좋지 않은 사용 패턴이다. 한 번 생성된 커넥션은 재사용하는 것이 리소스 관리 측면에서 유리하다. 제디스는 이와 같은 커넥션 재사용을 위한 JedisPool 이라는 연결 풀 객체를 제공한다. 제디스의 연결 풀을 사용한 [예제5-2]를 살펴보자. [예제5-2]는 연결 풀을 사용하여 해시 데이터에 사용자의 정보를 저장하고 조회한다.
예제 5-2 제디스 연결 풀 사용예제
1 | package net.sf.redisbook.ch5; |
제디스는 아파치의 ObjectPool 라이브러리를 사용하여 연결 풀을 제공한다.
- ObjectPool의 최대 개수를 설정한다.
- ObjectPool에 등록된 연결이 설정한 최대 개수에 도달했을 때, 새로운 요청에 대한 처리 방법을 지정한다. 여기서는 가용 연결이 생길 때까지 대기하도록 설정했다.
- 의 설정값을 기초로 하여 IP 192.168.56.102의 6379포트에서 동작하는 레디스 서버의 제디스 연결풀을 생성한다.
- 생성된 제디스 풀에서 첫 번째 커넥션을 가져온다.
- 첫 번째 커넥션을 사용하여 info:자린고비 키에 hset 메서드를 사용하여 자린고비 사용자의 정보를 입력한다.
- 생성된 제디스 풀에서 두 번째 커넥션을 가져온다.
- info: 자린고비 키에 저장된 정보를 hmget 메서드를 사용하여 조회한다.
- 첫 번째 커넥션을 풀로 돌려준다.
- 두 번째 커넥션을 풀로 돌려준다.
- 생성된 제디스 연결 풀을 제거한다.
[예제5-2]는 단일 자바 애플리케이션이므로 연결 풀을 제거하는 10. 을 수행했다. 통상적으로 WAS에서 위와 같은 프로그램을 만든다면 연결 풀을 전역상수(static final 상수)에 등록하여 사용한다.
레디스 명령행 클라이언트는 영문이 아닌 키와 인코딩된 데이터를 출력하지만 제디스는 인코딩되지 않은 데이터를 출력한다.
예제 5-10 제디스를 사용한 입력
1 | package net.sf.redisbook.ch5; |
1.에서는 입력을 위한 전체 데이터 건수를 지정했다. 2.는 레디스에 저장할 키와 값을 12자리로 고정하기 위한 코드다. [예제 5-10]에서 생성된 키의 범위는 key100000001부터 key109999999까지 전부 천만 개고 필자의 테스트 장비에서 실행했다.
예제 5-10이 실행된 결과는 아래와 같다.1
2초당 처리 건수 7803.965
소요 시간 1281.4초
예제 5-11 천만 건의 데이터를 다중 스레드로 전송하는 예제
1 | package net.sf.redisbook.ch5; |
[예제 5-11]은 [예제 5-10]을 다중 스레드로 구현했다. 1.에 설정된 스레드의 개수를 조절하면서 테스트해보자.
- 프로그램 실행 시 시작할 스레드 개수를 지정한다. 예제에서는 5로 지정했다.
- 자바 프로그램이 종료될 때 실행되는 이벤트 스레드를 등록한다.
- 전체 실행 시간과 초당 전송 수를 출력한다.
- 지정된 스레드 개수만큼 스레드를 생성한다. 생성하는 스레드에 연결 풀과 인덱스를 인자로 지정한다.
- 실제 데이터를 전송할 스레드를 생성한다.
- 지정된 스레드 인덱스에 해당하는 키를 생성하고 아니면 실행하지 않는다. 이 부분은 각 스레드가 천만개의 요청을 공평하게 나누어 전송하게 한다.
아래는 [예제 5-11]에서 5개의 스레드를 설정하고 실행한 결과다.1
2
3스레드 개수 5개
초당 처리 건수 12174.04
소요 시간 821.41초
단일 스레드에 비해 성능이 약 60% 정도 향상됐다.
…
스레드의 개수가 증가하면서 성능이 지속적으로 증가하다가 100개를 정점으로 성능이 하락한다. 즉, 다중 스레드에 대한 임계치가 존재한다.
레디스 파이프라인
불필요한 네트워크 왕복 시간을 줄여 더 빠른 성능을 얻고자 한다면 레디스 파이프라인을 사용하자. 파이프라인은 레디스 명령행 클라이언트와 제디스 모두에서 실행 가능하다.
레디스 명령행 클라이언트에서 파이트라인을 사용하려면 데이터가 저장된 파일이 필요하다. 이 텍스트 파일은 두 가지 구조를 가질 수 있다. 첫 번째는 순수한 레디스 명령 집합으로 이루어진 텍스트 파일이고, 두 번째는 5.2절에서 설명한 레디스 프로토콜로 이루어진 텍스트 파일이다.
파이프라인을 운영 중인 시스템에 사용하면 파이프라인 명령이 레디스의 모든 리소스를 점유하게 되어 서비스의 응답 속도가 저하되기도 한다. 이럴 때는 제디스와 같은 클라이언트 라이브러리를 사용하여 데이터를 입력하는 편이 더 낫다.
6장 레디스 내부구조
레디스 객체 구조
레디스는 저장된 데이터를 관리하기 위하여 redisObject 객체를 사용한다.
레디스 인코딩 종류
레디스는 데이터의 주 저장소로 메모리를 사용한다. 운영체제의 관점에서 메모리는 한정적인 자원이며 매우 비싼 리소스에 해당한다. 레디스의 개발자는 이와 같이 제한적인 환경에서 데이터를 효율적으로 저장하기 위해서 인코딩을 사용한다. 레디스에서 사용하는 인코딩의 종류는 앞에서 설명한 바와 같이 모두 8가지이다.
문자열 데이터 인코딩
- REDIS_ENCODING_RAW : 전혀 가공되지 않은 원본 데이터
- REDIS_ENCODING_INT : 숫자 데이터 인코딩
레디스는 10000보다 작은 숫자를 미리 공유객체 상수로 등록해 두어 같은 객체를 재사용한다. 이렇게 하면 동일한 값을 가지는 데이터를 한 번만 저장하므로 메모리 낭비를 막을 수 있다.리스트 데이터 인코딩
- REDIS_ENCODING_ZIPLIST : 리스트 데이터를 저장할 때 더 적은 메모리를 사용할 수 있음. 더 많은 CPU를 사용
- REDIS_ENCODING_LINKEDLIST : 일반 연결 리스트의 구조와 동일
셋 데이터 인코딩
셋 데이터는 O(1)의 시간 복잡도를 제공하기 위하여 내부적으로 해시 테이블 구조로 구현되어 있다. - REDIS_ENCODING_INTSET : 메모리를 절약하기 위한 특별한 인코딩
- REDIS_ENCODING_HT : 셋 데이터의 기본 인코딩
레디스 문자열
레디스는 문자열을 메모리에 저장할 때 C언어의 char*를 사용하여 저장한다. 전체 메모리의 크기를 확인할 필요 없이 sdshdr 구조체의 len 필드를 조회하여 문자열의 길이를 확인할 수 있다.
예제 6-10 문자열 표현을 위한 SDS 구조체
1 | struct sdshdr { |
레디스 공유 객체
레디스는 자주 사용되는 값을 전역 변수인 공유 객체에 저장해 두고 사용한다. 이 공유객체에 포함되는 값의 종류는 에러 메시지, 프로토콜을 위한 문자열, 자주 사용되는 문자열, 0부터 9999까지의 숫자가 해당된다. 특이하게도 이 모든 값은 redisObject 구조체를 사용하여 표현된다.
7장 레디스 활용 사례
- 레디스로 구현할 수 있는 것
- 레디스 활용 사례와 구현
- 구현의 문제점과 해결책
- 사례의 기능 확장
- 기능 테스트
8장 확장과 분산 기법
레디스에 스케일 아웃을 적용하기 위해서 동일한 사양의 하드웨어를 3대로 늘리는 방법을 생각해보자. 레드스는 스케일 아웃을 처리하기 위한 방법으로 두 가지 기법을 제공한다. 읽기 분산을 위한 복제(Replication)와 쓰기 분산을 위한 샤딩(Sharding)이 이에 해당된다.
레디스 클러스터에 포함되는 설명에서는 노드로 표현하며 클러스터가 아닌 별도의 단일 서버에 대한 설명에서는 레디스 인스턴스로 표현한다.
- 복제 : 클라이언트가 어느 노드에 접근하더라도 동일한 데이터를 읽을 수 있도록 데이터를 각 노드에 복제하여 저장하는 것을 말한다.
- 샤딩 : 데이터를 특정 조건에 따라 나누어 저장하는 것을 말한다. 예를 들어 1부터 10까지 10개의 데이터를 두 대의 노드를 사용하여 샤딩을 수행한다고 가정하자. 이 때 첫 번쨰 노드에 1부터 5까지의 데이터를 저장하고 두 번째 노드에 6부터 10까지의 데이터를 저장하여 데이터를 분할 저장하는 방법을 샤딩이라 말한다.
- 샤드(shard) : 두 개의 노드를 사용하여 데이터를 분할 저장하였을 때 각 노드를 샤드라고 부른다. 각 샤드가 복제를 사용하여 여러 개의 노드로 구성될 때에도 하나의 샤드로 취급한다.
읽기 성능 증대를 위한 복제 기법
복제는 동일한 데이터를 다중의 노드에 중복하여 저장하는 것을 말하는데, 레디스는 복제를 위해서 마스터와 슬레이브의 복제의 개념을 사용한다. 마스터는 복제를 위한 데이터의 원본 역할을 한다. 슬레이브는 마스터 노드에 데이터 복제 요청을 하고 데이터를 수신하여 데이터를 동기화한다.
레디스의 복제는 읽기 성능의 증대를 위해서 사용된다. 통상적으로 레디스 인스턴스 하나가 처리할 수 있는 TPS(Transaction Per Second)는 1만에서 2만이다. 또한 대부분의 레디스 명령은 읽기와 쓰기에 따른 성능 편차가 없다. 그러므로 레디스의 읽기 성능을 증대시키기 위해서 복제를 사용해여 클러스터를 구성한다.
레디스의 복제는 기본적으로 마스터 노드에서 쓰기 연산을 수행하고 슬레이브 노드에서 읽기 연산을 수행하도록 하여 읽기와 쓰기를 완전히 분리한다. 읽기 연산과 쓰기 연산을 완전히 분리하여 확장 구조의 유연성을 가질 수 있다. 반대로 읽기와 쓰기를 분리하기 위해서 애플리케이션 레벨에서 사용할 수 있는 명령의 한계와 애플리케이션의 복잡도 증가가 발생하게 된다.
복제는 슬레이브 노드가 마스터 노드의 데이터를 실시간으로 복제하여 데이터의 동기화를 유지한다. 슬레이브 노드가 시작할 때 마스터 노드에게 복제를 요청하고 최초 복제가 완료된 이후는 변경 사항만 업데이트한다. 슬레이브 노드가 복제를 요청하므로 마스터 노드의 위치 정보는 슬레이브 노드가 가지고 있다. 그러므로 레디스 클러스터에 노드를 추가할 때 마스터 노드의 설정 변경이나 마스터 노드의 재시작 등이 필요하지 않다.
이와 같이 레디스의 복제는 마스터 노드가 슬레이브의 정보를 가지지 않기 때문에 다양한 구조의 복제 구성이 가능하다.
단일 복제
예제 8-1 마스터 노드를 위한 레디스 설정 파일
1 | redis configration |
예제 8-2 슬레이브 노드를 위한 레디스 설정 파일 -slave.conf
1 | redis configration |
1.은 마스터 노드의 위치 정보를 나타내는데 슬레이브 노드의 설정 파일에 마스터 노드의 위치 정보가 포함된다. 2.는 레디스 2.6에서 추가된 새로운 기능으로서 슬레이브 노드의 동작 방식을 결정한다. 단순 읽기 전용으로 사용할 것인지 읽기와 쓰기를 모두 허용할 것인지를 결정한다.
다중 복제
단일 복제 상태에서 여러 대의 슬레이브 노드를 추가한 것을 다중 복제라고 한다. 새로운 슬레이브 노드를 추가하는 방법은 단일 복제에서 첫 번째 슬레이브 노드를 추가하는 방법과 완전히 동일하다.
마스터 노드에서 데이터를 슬레이브 노드로 복제하기 위해서 필요한 리소스는 마스터 노드의 CPU와 네트워크다. 그중에서도 네트워크 리소스의 비용이 가장 크다. 하나의 마스터 노드에 너무 많은 슬레이브 노드가 접속되면 마스터 노드가 가진 대부분의 리소스가 복제를 위한 데이터 동기화에 사용되어 전체적인 성능이 저하된다.
계층형 복제
계층형 복제는 마스터 노드에 너무 많은 슬레이브가 접속되어 쓰기 성능이 저하되는 문제점을 해결하기 위한 복제 방법이다.
마스터 노드와 슬레이브 노드 1이 1차로 복제되어 동기화되고, 그 이후에 슬레이브 노드 2,3,4가 복제된다. 슬레이브 노드 1은 1차 복제를 수행하기 위해서 읽기와 쓰기 연산을 수행하지 않도록 구성되어 있다. 다중 복제에서 설명한 리소스 부족이 그 이유인데, 리소스가 충분하다면 슬레이브 1에서 읽기 연산을 수행하여도 무방하다.
레디스의 클러스터 구성에서는 듀얼 마스터(Dual Master) 또는 다중 마스터(Multi Master) 복제를 지원하지 않는다.
쓰기 성능 증대를 위한 샤딩 기법
샤딩은 다른 용어로 파티셔닝(Partitioning)이라 부른다. 데이터 파티셔닝은 두 가지 관점에서 유용하다. 첫째, 더 많은 데이터를 레디스에 저장할 수 있다. 예를 들어 복제를 사용했을 때 레디스에 저장 가능한 데이터의 전체 크기는 마스터 노드의 메모리 크기와 동일하거나 더 작다. 즉, 마스터 노드의 물리 메모리 크기보다 많은 데이터를 저장하면 성능이 급격하게 저하되기 때문에 마스터 노드의 메모리 크기에 의해서 저장 가능한 전체 데이터 크기가 결정된다. 그러므로 샤딩은 동일한 개수의 하드웨어를 사용할 떄 복제에 비해서 더 많은 데이터를 저장할 수 있다.
샤딩을 통해서 쓰기 성능의 증대를 이룰 수 있다. 하나의 물리적인 하드웨어가 처리할 수 있는트랜잭션 수를 100이라고 했을 때 다중의 하드웨어에 복제를 수행하여 쓰기 성능을 증대 하려는 시도를 생각해보자. 마스터 하드웨어에 쓰기 연산이 발생하면 나머지 두 하드웨어에도 동일한 쓰기 연산이 발생해야 동기화가 유지된다. 즉 모든 노드가 동일한 쓰기 연산을 수행하게 되므로 복제를 통해서는 쓰기 성능을 증대시킬 수 없다. 반대로 샤딩을 사용하면 하나의 샤드에 하나의 데이터만 존재하므로 쓰기 연산은 해당 샤드에서만 발생하여 더 많은 쓰기 연산을 처리할 수 있다.
레디스 2.6버전에서는 서버 측 샤딩을 지원하지 않으므로 클라이언트 측 샤딩을 사용하여야 한다.
- 수직 샤딩(Vertical Sharding) : 관계형 데이터베이스의 테이블에 해당하는 정보를 노드별로 분할하는 방법이다. 예를 들어 사용자 정보는 첫 번째 노드, 친구 정보는 두 번째 노드에 저장하는 방식을 말한다.
- 범위 지정 샤딩(Range Sharding) : 키를 특정 범위를 기준으로 분할하여 저장하는 방법이다. 예를 들어 1부터 1,000까지의 키가 있을 때 1부터 500까지의 키는 첫 번째 노드, 501부터 1,000까지의 키는 두 번째 노드에 저장하게 된다.
- 해시 기반 샤딩(Hash Based Sharding) : 키를 해시 함수에 대입하여 결과값에 특정 연산을 가해 데이터의 위치를 결정하는 방법을 말하는데, 일관된 해싱(Consistent Hashing)이라고도 한다.
마치며
레디스 클러스터 버전이 나오지 않았기 때문에 클러스터의 정보를 클라이언트에서 가지고 있어야 하는 문제가 남아있다. 이와 같은 문제점을 해결하기 위하여 클러스터의 정보를 원격 서버에 저장하고 클라이언트에서 원격 서버로부터 클러스터 정보를 업데이트하도록 구성하기도 한다.
레디스의 샤딩 구성에서 가장 중요한 부분은 해시 함수의 선택이다. 데이터의 저장 위치는 해시 함수의 결과에 따라서 정해지기 때문에 운영 중간에 해시 함수의 변경으로 전체 데이터의 위치를 다시 정해야 하는 상황이 발생할 수 있다는 것이다.
9장 레디스 운영 시 고려사항
임계점의 정의
논리적으로 스케일 아웃의 한계는 없다. 하지만 현실적으로 네트워크 대역폭 등의 의한 한계가 있다.
CPU, 메모리, 네트워크에 대한 임계점
레디스는 데이터 저장과 조회에 단일 스레드를 사용한다. 바꾸어 말하면 멀티코어 시스템에서 실행되더라도 하나의 코어를 사용하기 때문에 단일 코어의 성능이 낮은 32코어 장치보다 단일 코어 성능이 높은 4코어 장치에서 더 빠른 성능을 보인다.
레디스는 메모리에 데이터를 저장하기 때문에 레디스가 사용할 메모리의 크기를 지정하는 것은 성능과 가장 밀접한 관련이 있다.
복제를 위한 데이터는 모두 네트워크 허브 2를 통하여 전송되고, 데이터의 읽기 연산과 쓰기 연산은 모두 네트워크 허브 1을 사용하여 전송된다. 즉, 이와 같이 네트워크를 구성하면 서비스를 위한 네트워크 대역폭을 분리하여 네트워크 병목 현상을 피할 수 있다.
임계점 테스트 벤치마크
임계점의 한계를 극복하기 위한 하드웨어 구성
레디스의 복제를 구성할 때 하나의 마스터에 너무 많은 슬레이브를 구성하지 않도록 한다. 이와 같은 상황을 방지하기 위해서 복제를 위한 별도의 네트워크 카드를 설정하는 방법과 복제를 위한 별도의 스위치 허브를 사용하는 구성을 고려해야 한다.
레디스 클러스터 구성에서 가장 쉽게 만날 수 있는 임계점은 네트워크다. 레디스 클러스터 구성에서 네트워크 임계점에 도달하게 되면 클라이언트로 전송되는 명령의 응답시간이 늘어나게 되므로 빠른 응답시간이 필요한 서비스에서는 치명적인 단점이 된다는 사실을 잊지 말자.
10장 레디스 튜닝
레디스의 환경 설정 - redis.config
레디스의 스냅샷 - RDB
레디스는 데이터의 영구 저장을 위하여 메모리에 저장된 모든 데이터를 디스크로 저장하며 이것을 스냅샷이라고 부른다. 스냅샷이라고 불리는 이유는 특정 시점의 메모리를 사진을 찍듯이 그대로 디스크에 저장하기 때문이다. 스냅샷의 결과로 dump.rdb 파일이 생성되는데 이 파일을 사용하여 해당 시점의 데이터로 복원할 수 있다.
1 | 전체 메모리 = 자식 프로세스의 메모리 크기 + 부모 프로세스가 변경한 페이지 개수 * 4KB |
fork 함수가 호출되어 자식 프로세스가 종료되기 전에 모든 페이지가 변경된다면 레디스는 메모리를 최대 2배만큼 사용하게 된다. 이와 같은 상황은 쓰기 연산이 매우 많은 시스템에서 발생할 수 있다. 그러므로 스냅샷을 사용할 때는 자신의 서비스가 쓰기 위즈의 서비스인지를 먼저 확인하여 메모리 사용 정책을 정의해야 한다.
Append Only File - AOF
AOF(Append Only File)는 레디스에서 데이터의 영구 저장을 위하여 지원하는 두 번째 기능이다. 레디스가 수신하는 모든 쓰기 명령을 aof 파일에 기록하여 데이터를 보관한다.
AOF는 스냅샷에 비해 더 나은 데이터 정합성을 보장한다. 즉, 스냅샷은 각 스냅샷 사이의 데이터를 잃어버리지만 AOF는 최악의 상황에서 마지막 1초의 데이터만을 잃어버린다.
각 설정에 따른 장단점
aof 파일은 레디스 명령어를 레디스 프로토콜 형식으로 저장하고 레디스가 재시작할 때 다시 동일한 명령어를 실행하여 데이터의 정합성을 유지한다. 또한 모든 명령어를 다시 실행하기 때문에 스냅샷에 비하여 데이터 로드 시간이 오래 걸린다.
11장 루아 스크립트
루아(lua)는 ‘달’을 의미하는 포르투갈어며, 1993년 브라질에서 처음 개발됐다. 루아 공식홈페이지에 따르면 ‘루아는 강력하고 빠르며 가벼우면서 다른 언어에 이식하기 좋은 스크립트 언어’다. 또한 대소문자를 구분한다.
루아 스크립트는 매우 강력한 문자열 조작함수와 수학 함수들을 가지고 있으며 쉬운 문법으로 프로그래머가 아닌 사람들도 쉽게 익히고 사용할 수 있다. 루아는 그래픽 시뮬레이션을 위한 스크립트 언어로 개발되었기 때문에 타 스크립트 언어에 비하여 빠른 성능을 제공한다. 루아 스크립트는 자바 언어와 유사한 가비지 컬렉션을 지원한다. 그러므로 사용하지 않는 변수를 메모리에서 제거하기 위해서 별도의 처리를 할 필요가 없다. 만약 사용이 완료된 변수를 즉시 가비지 컬렉션의 대상으로 만들고 싶다면 변수에 nil을 할당하면 된다.
레디스에서 루아를 사용하기 위해서는 몇 가지 제약사항이 따르는데, 첫 번째는 지역 변수의 사용이다. 즉, 레디스에서 루아를 사용할 때는 전역 변수를 사용하지 말아야 한다. 두 번째로 루아의 배열인 테이블을 사용할 때 첨자는 숫자로만 사용하여야 한다.
루아 IDE 설치와 테스트
이클립스 환경에서 사용 가능한 루아 개발환경은 크게 두 가지 종류가 있다. LuaEclipse와 이클립스 공식 루아 개발환경인 Koneki가 그것이다.
루아 스크립트 언어 기본
변수와 지역성
루아 스크립트는 다른 스크립트 언어와 유사하게 사용하는 변수형을 지정하지 않는다. 즉, 동적 변수형을 사용하며 입력된 값에 따라서 변수형이 자동으로 지정된다. 그렇기 때문에 루아에서 저장된 변수형을 확인하고자 할 때는 type 함수를 사용한다.
루아에서 변수를 선언할 때는 반드시 영문이나 언더스코어(_)로 시작해야 하며 예약어를 변수로 사용할 수 없다.1
and / break / do / else / elseif / end / false / for / function / if / in / local / nil / not / or / repeat / return / then / true / until / while
루아 스크립트는 전역 변수와 지역 변수를 구분한다. 전역 변수는 프로그램의 어느 위치에서나 참조할 수 있는 반면, 지역 변수는 변수가 선언된 블록 범위 안에서만 참조할 수 있다. 루아는 전역 변수와 지역 변수의 구분이 특이한데, 특별한 지정을 하지 않으면 모두 전역 변수로 인식된다. 심지어 변수를 선언하지 않고 사용할 때에도 전역 변수로 인식한다. 또한 함수도 지역 함수와 전역 함수로 구분된다.
루아 스크립트에서 지역 변수나 지역 함수를 선언하기 위해서는 local 키워드를 사용한다. 레디스 공식 문서에 따르면 레디스에서 루아 스크립트를 사용할 때는 전역 변수나 전역 함수를 등록하지 말 것을 권장하고 있다. 루아 스크립트에서 사용한 전역 변수와 전역 함수가 같은 이름의 레디스 내부의 전역 변수나 전역 함수와 충돌을 일으킬 수 있기 때문이다.
레디스에서 루아 사용하기
레디스 서버에서 수행되는 루아 스크립트에 대한 특징
- 레디스에서 루아 스크립트를 실행할 때 스크립트에 대한 인수를 입력할 수 있다.
- 루아 스크립트에서 레디스 명령을 사용할 수 있다.
- 레디스 서버에서 실행되는 루아 스크립트는 원자적으로 처리된다.
레디스에서 루아 스크립트를 실행하는 방법은 두 가지이다.
첫 번째는 eval 명령을 사용하여 수행하고자 하는 루아 스크립트를 매번 레디스로 전송하는 것이다.
두 번째는 script load 명령을 사용하여 수행할 스크립트를 레디스 서버에 미리 등록해두는 방법이다.
