Redis
레디스(Redis)는 Remote Dictionary Server의 약자로서 ‘키-값’ 구조의 비관계형 데이터를 저장하고 관리하기 위한 NoSQL의 일종이다. 2009년 Salvatore Sanfilippo가 처음 개발했다. 2015년부터 Redis Labs가 지원하고 있다. 모든 데이터를 메모리로 불러와서 처리하는 메모리 기반 DBMS이다. BSD 라이선스를 따른다.
출처: 위키피디아
설치 및 실행하기
다음은 레디스 서버를 설치하고 실행해보는 단순한 방법이다.
실제 서비스에 적합한 설정들을 적용시키는 좀 더 복잡한 실행방법에 대해서는 별도의 포스트에서 다루도록 하겠다.
터미널에서 원하는 경로로 이동하여 아래 명령어들을 차례로 입력한다.
wget http://download.redis.io/redis-stable.tar.gz
tar xvzf redis-stable.tar.gz
cd redis-stable
make
설치가 끝나면 잘 설치되었는지 확인하기 위해 아래 명령을 입력해본다.
make test
설치가 완료되었다면, 다음의 프로그램들이 실행 가능하게 된다.
redis-server
: 레디스 서버redis-cli
: 레디스 CLI 인터페이스redis-sentinel
: 레디스 모니터링 툴redis-benchmark
: 레디스의 성능 테스트를 위한 벤치마킹 툴redis-check-aof
: 데이터 파일 손상 확인
레디스 서버를 실행시키려면 다음과 같이 입력한다.
redis-server
그러면 아래와 같이 뜨면서 요청 대기 상태로 들어가게 된다.
여러가지 경고 메세지가 뜨는데 이에 관해서는 천천히 알아볼 예정이다.
9321:C 30 Apr 22:47:19.755 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
9321:C 30 Apr 22:47:19.755 # Redis version=4.0.9, bits=64, commit=00000000, modified=0, pid=9321, just started
9321:C 30 Apr 22:47:19.755 # Warning: no config file specified, using the default config. In order to specify a config file use redis-server /path/to/redis.conf
9321:M 30 Apr 22:47:19.757 * Increased maximum number of open files to 10032 (it was originally set to 1024).
_._
_.-``__ ''-._
_.-`` `. `_. ''-._ Redis 4.0.9 (00000000/0) 64 bit
.-`` .-```. ```\/ _.,_ ''-._
( ' , .-` | `, ) Running in standalone mode
|`-._`-...-` __...-.``-._|'` _.-'| Port: 6379
| `-._ `._ / _.-' | PID: 9321
`-._ `-._ `-./ _.-' _.-'
|`-._`-._ `-.__.-' _.-'_.-'|
| `-._`-._ _.-'_.-' | http://redis.io
`-._ `-._`-.__.-'_.-' _.-'
|`-._`-._ `-.__.-' _.-'_.-'|
| `-._`-._ _.-'_.-' |
`-._ `-._`-.__.-'_.-' _.-'
`-._ `-.__.-' _.-'
`-._ _.-'
`-.__.-'
9321:M 30 Apr 22:47:19.760 # WARNING: The TCP backlog setting of 511 cannot be enforced because /proc/sys/net/core/somaxconn is set to the lower value of 128.
9321:M 30 Apr 22:47:19.760 # Server initialized
9321:M 30 Apr 22:47:19.760 # WARNING overcommit_memory is set to 0! Background save may fail under low memory condition. To fix this issue add 'vm.overcommit_memory = 1' to /etc/sysctl.conf and then reboot or run the command 'sysctl vm.overcommit_memory=1' for this to take effect.
9321:M 30 Apr 22:47:19.760 # WARNING you have Transparent Huge Pages (THP) support enabled in your kernel. This will create latency and memory usage issues with Redis. To fix this issue run the command 'echo never > /sys/kernel/mm/transparent_hugepage/enabled' as root, and add it to your /etc/rc.local in order to retain the setting after a reboot. Redis must be restarted after THP is disabled.
9321:M 30 Apr 22:47:19.760 * Ready to accept connections
이 상태로 두고 새로운 터미널을 켜서 다음과 같이 입력해보면 레디스 서버가 잘 작동하는지 확인할 수 있다.
redis-cli ping
위와 같이 입력했을 때 아래와 같이 응답이 오면 서버가 잘 작동하고 있는 것임을 알 수 있다.
PONG
레디스 서버와 직접적으로 통신하려면 redis-cli
를 사용하면 된다. 터미널에서 아래와 같이 입력하여 실행할 수 있다.
로컬에서 실행하게되면 127.0.0.1
의 주소로 접속하게 되고 포트는 레디스의 기본 포트번호인 6379
를 사용함을 알 수 있다.
redis-cli
127.0.0.1:6379>
기본적인 사용법
레디스는 키-값
의 형태로 데이터를 저장한다. 다음의 기본적인 명령어를 통해 레디스에 값을 저장하고 불러올 수 있다.
SET
키
와 값
을 인자로 받으며 키-값의 형태로 데이터를 저장한다.
SET 키 값
SET a 1
OK
GET
키
를 인자로 받으며 해당 키의 값을 리턴한다.
GET 키 값
GET a
1
DEL
키
를 인자로 받으며 해당 키와 키의 값을 삭제한다. 여러 개의 키를 삭제할 수도 있다.
키가 없다면 아무 일도 일어나지 않는다.
성공적으로 삭제되었을 시 삭제된 키의 수를 리턴한다.
DEL 키
DEL a b
(integer) 1
SELECT
하나의 레디스 서버는 여러 개의 데이터베이스를 가지고 있을 수 있으며 각각의 번호를 SELECT 문에 전달하여 접속할 수 있다.
각각의 데이터베이스는 서로 독립적으로 데이터를 저장하므로 다른 데이터베이스에 저장된 데이터에 접근하기 위해서는 해당 데이터베이스에 먼저 접속해야한다.
SELECT 데이터베이스번호
1번 데이터베이스에 접속하려면,
SELECT 1
레디스의 모든 명령어는 여기에서 확인할 수 있다.
레디스는 문자, 정수뿐만 아니라 리스트나 해시 등의 데이터도 값으로 저장할 수 있다. 간단한 튜토리얼을 통해 레디스가 지원하는 데이터 타입들에 대해 알아 볼 수 있다.
Django 어플리케이션에 연동시키기
레디스는 데이터들을 메모리에 저장시켜서 사용하는 인메모리 데이터베이스이므로 Django와 연동할 때는 주로 데이터를 캐싱하는데 사용한다.
간단한 장고 어플리케이션을 만들어서 캐싱을 사용하기 전과 후의 차이를 비교해보도록 하자.
간단한 Django 프로젝트 생성
장고 프로젝트를 시작한 뒤 post
라는 앱을 만들고, 다음과 같이 대량의 테스트용 포스트를 만들 수 있는 커스텀 매니지먼트 커맨드를 추가하여 1000개의 포스트를 추가하였다.
# post/management/commands/addpost.py
from django.core.management import BaseCommand
from post.models import Post
class Command(BaseCommand):
help = 'creates given number of test posts'
def add_arguments(self, parser):
parser.add_argument('num_posts', type=int)
def handle(self, *args, **options):
num_posts = options['num_posts']
if num_posts > 0:
Post.objects.bulk_create(
[Post(title=f'Test Post{i}', content=f'This is the test post {i}')
for i in range(num_posts)]
)
self.stdout.write(self.style.SUCCESS(f'Successfully added {num_posts} posts'))
./manage.py addpost 1000
Successfully added 1000 posts
다음으로 /posts
로 접속하면 모든 포스트들의 리스트를 json 타입으로 리턴하도록 views.py
와 urls.py
를 작성해주었다.
# post/views.py
class PostListView(ListView):
model = Post
def get(self, request, *args, **kwargs):
posts = Post.objects.all().values('id', 'title', 'content')
context = {}
for i in posts:
context[f'post_{i["id"]}'] = i
return JsonResponse(context)
# urls.py
from django.conf.urls import url
from django.contrib import admin
from django.urls import path
from post.views import PostListView
urlpatterns = [
url(r'^admin/', admin.site.urls),
path('posts/', PostListView.as_view()),
]
http://localhost:8000/posts/
으로 접속해서 아래와 같이 모든 포스트의 리스트가 리턴되는지 확인한다.
{"post_1": {"id": 1, "title": "Test Post0", "content": "This is the test post 0"}, "post_2": {"id": 2, "title": "Test Post1", "content": "This is the test post 1"}, "post_3": {"id": 3, "title": "Test Post2", "content": "This is the test post 2"}, "post_4": {"id": 4, "title": "Test Post3", "content": "This is the test post 3"}, "post_5": {"id": 5, "title": "Test Post4", "content": "This is the test post 4"}, "post_6": {"id": 6, "title": "Test Post5", "content": "This is the test post 5"}, "post_7": {"id": 7, "title": "Test Post6", "content": "This is the test post 6"}, "post_8": {"id": 8, "title": "Test Post7", "content": "This is the test post 7"}, "post_9": {"id": 9, "title": "Test Post8", "content": "This is the test post 8"}, "post_10": {"id": 10, "title": "Test Post9", "content": "This is the test post 9"}, "post_11": {"id": 11, "title": "Test Post10", "content": "This is the test post 10"}, "post_12": {"id": 12, "title": "Test Post11", "content": "This is the test post 11"}, "post_13": .
.
.
Loadtest로 서버 테스트 해보기
특정 주소로 여러 번의 요청을 보내서 서버가 얼마나 잘 요청을 처리해내는지를 테스트하는 loadtest
라는 프로그램을 설치하여 캐싱하기 전과 후의 성능 차이를 확인해 볼 것이다.
loadtest를 설치하려면 먼저 npm
이 설치되어 있어야 한다.
npm
은 이곳에서 설치할 수 있다.
npm을 설치하였다면 다음과 같이 입력하여 loadtest를 설치한다.
npm install -g loadtest
이제 다음과 같이 입력하여 /post
엔드포인트로 100개의 요청을 보내보자.
loadtest -n 100 http://localhost:8000/posts/
Requests: 0 (0%), requests per second: 0, mean latency: 0 ms
Target URL: http://localhost:8000/posts/
Max requests: 100
Concurrency level: 1
Agent: none
Completed requests: 100
Total errors: 0
Total time: 1.0241663699999999 s
Requests per second: 98
Mean latency: 10.2 ms
Percentage of the requests served within a certain time
50% 10 ms
90% 11 ms
95% 11 ms
99% 21 ms
100% 21 ms (longest request)
대충 보면 100개의 요청이 에러없이 총 1.024초 만에 처리되었고 각 요청의 평균 처리시간이 약 10.2 ms 인 것으로 나타났고,
이는 초당 약 98개의 요청을 처리할 수 있는 성능인 것이다.
이제 레디스 서버를 붙여서 캐싱 기능을 적용하여 성능이 얼마나 좋아지는지 비교해보자.
Redis 를 연동하여 캐싱하기
Django에서 레디스 서버를 사용하려면 django-redis
라는 인터페이스를 설치해주어야 한다.
pip install django-redis
그 다음 settings.py
에 다음과 같이 추가해주어 레디스를 캐싱 서버로 사용하도록 해준다.
# settings.py
CACHES = {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": "redis://127.0.0.1:6379/1", # 1번 DB 사용
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
}
}
}
다음으로 views.py
에서 캐시된 데이터를 불러와 리턴하도록 변경해준다.
# views.py
class PostListView(ListView):
model = Post
def get(self, request, *args, **kwargs):
context = cache.get('posts')
if not context:
posts = Post.objects.all().values('id', 'title', 'content')
context = {}
for i in posts:
context[f'post_{i["id"]}'] = i
cache.set('posts', context)
return JsonResponse(context)
cache.set()
은 키
, 값
, 만료시간
을 인자로 받는다. 만료시간은 옵션으로 지정하지 않으면 캐시 데이터가 영구히 지속된다.
cache.get()
은 키
를 진자로 받으며 주어진 키에 해당하는 값을 리턴한다.
처음에 cache.get 으로 캐시 데이터를 레디스로부터 불러온 후, 결과가 없다면 데이터베이스에서 꺼내와서 cache.set 으로 레디스에 저장한 다음 리턴해주는 뷰이다.
이렇게 한 다음 다시 loadtest 를 통해 100개의 요청을 보내본 결과는 다음과 같다.
Requests: 0 (0%), requests per second: 0, mean latency: 0 ms
Target URL: http://localhost:8000/posts/
Max requests: 100
Concurrency level: 1
Agent: none
Completed requests: 100
Total errors: 0
Total time: 0.5189698610000001 s
Requests per second: 193
Mean latency: 5.1 ms
Percentage of the requests served within a certain time
50% 4 ms
90% 5 ms
95% 5 ms
99% 14 ms
100% 14 ms (longest request)
100개의 요청을 처리하는데 걸린 총 시간이 약 0.519 초로 캐싱을 하기 전과 비교하여 대략 반 정도로 줄어들었다.
초당 처리 요청수도 98개에서 193개로 거의 두 배로 늘어났고 평균 레이턴시도 5.1ms로 이전의 10.2ms 에 비해 반으로 줄었다.
단순한 데이터 처리에도 거의 두 배에 가까운 성능향상이 이루어졌는데 이보다 더 크고 복잡한 처리의 경우에는 더 큰 성능향상을 볼 수 있을 것 같다.
저장된 캐시 데이터를 레디스 서버에서 확인하기
참고로 Django에 의해 캐시 데이터가 레디스에 저장되는 경우 키는 KEY_PREFIX:DBINDEX:KEY
의 형태로 저장된다.
KEY_PREFIX
는 settings.py
에서 지정할 수 있으며, 각 서버마다 고유의 접두사를 가진 키를 생성하도록 해주는 기능이다. 지정하지 않으면 빈 문자열이 된다.
DBINDEX
는 데이터베이스 번호이고, KEY
는 cache.set() 에 첫 번째 인자로 넘겨준 그 값이 된다.
따라서 위의 cache.set('post', context)
명령에 의해 만들어진 캐시 데이터는 레디스 서버의 1번 데이터베이스에 :1:posts
라는 키에 저장된다.
만약 KEY_PREFIX 를 지정하고 싶다면, 다음과 같이 settings.py
에 지정해주면 된다.
# settings.py
CACHES = {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": "redis://127.0.0.1:6379/1", # 1번 DB
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
},
"KEY_PREFIX": "example" # 접두사 설정
}
}
이렇게 해준 경우 캐시 데이터는 1번 데이터베이스의 example:1:posts
라는 키에 저장된다.
캐시 데이터 갱신하기
지금까지 설정해준 캐싱 기능에는 문제가 하나 있다.
만약 새로운 데이터가 생기거나 또는 기존의 데이터가 삭제되는 등의 변경이 이루어질 경우, 이것이 레디스 서버에 저장되어있는 데이터와 달라지게 된다.
따라서 요청이 들어오면 변경된 데이터가 아닌 캐시된 데이터를 리턴하므로 문제가 된다.
이 문제를 해결하기 위해 데이터가 생성되거나 삭제될 시 캐시 데이터 또한 업데이트를 해주어야 한다.
한가지 단순한 방법은 모델의 save
와 delete
메서드를 오버라이드하여 메서드가 실행될 때마다 캐시 데이터를 삭제해주는 방법이다.
# post/models.py
class Post(models.Model):
...
def save(self, *args, **kwargs):
cache.delete('posts')
print('saved')
super().save(*args, **kwargs)
def delete(self, *args, **kwargs):
cache.delete('posts')
super().delete(*args, **kwargs)
cache.delete()
는 키를 인자로 받아서 해당 키와 값을 삭제한다.
이렇게 해주면 동일한 데이터에 대해 반복된 요청만 레디스로부터 가져오고 데이터가 변경이 될 경우 새로운 캐시 데이터를 레디스에 저장하게 된다.