레디스에 대한 간단한 설명과 성능향상시키기

Hwangro Lee
13 min readAug 22, 2021

--

레디스를 사용하면서 알게된 많은 부분들을 공유합니다.

레디스란?

memcached와 같은 인메모리 데이터베이스로 데이터를 메모리에 저장하기 때문에 속도가 빨라 서비스의 성능을 개선시키는 목적으로 많이 사용한다.

데이터가 메모리에서 관리되기 때문에 레디스를 재가동할경우 저장된 데이터는 사라진다. 이러한 문제는 백업, 클러스터, 센티널등을 활용하여 개선할 수 있다.

레디스의 경우 memcached와 많이 비교가 되고 있다. memcached의 경우 key-value로만 데이터를 저장할 수 있지만 레디스는 Lists, Sets, Hashes등 많은 데이터타입을 지원하고 있어서 현업에서 많이 쓰이고 있다.

레디스는 기본적으로 싱글쓰레드로 동작한다. 그렇기 때문에 명령어를 사용할때 최대한 실행복잡도가 O(1) 인 명령어를 사용하도록 노력해야 한다. 특히, keys, flushall, flushdb, smembers, hgetall 과 같은 명령어는 사용을 금지하도록 개발자들끼리 소통을 하면 좋다. 혹은 redis 6.0.0 부터 지원하는 ACL기능을 사용하여 강제로 명령어를 막아도 좋다. (ACL기능은 직접사용해 보진 못했다.)

keys, smembers, hgetall 과 같은 기능은 scan, sscan, hscan 으로 명령어를 대체할 수 있다.

Strings

Key와 Value 가 일 대 일 관계이다. 알파벳, 숫자, 한글등을 저장할 수 있다. Key 와 Value 모두 최대 길이는 512MB 이다.

Lists

데이터를 링크드 리스트 형태로 관리하여 입력된 순서대로 저장되어 큐, 스택으로도 활용이 가능하다. 단 같은 값을 넣었을 때 중복으로 데이터가 들어갈 수 있다.

Redis 3.2 이전 버전에서는 데이터수가 512개이하라면 짚리스트를 사용하고 513개부터를 링크드리스트를 사용한다고 한다.

Redis 3.2 부터는 퀵리스트로 데이터를 저장하도록 확정되었다. 퀵리스트는 레디스를 메인디비로 사용한다고 알려진 트위터에서 발표했으며 Yao yu가 발표했다고 한다. 짚리스트, 링크드 리스트의 성능을 대폭 향상시켰다고 한다.

http://redisgate.kr/redis/configuration/internal_quicklist.php

<주의사항>

  • BRPOP: 데이터가 들어오면 오른쪽에서 데이터를 꺼내온다. 데이터가 있다면 RPOP처럼 오른쪽 데이터를 꺼내오지만 데이터가 없다면 데이터가 들어올때까지 timeout 시간만큼 기다린다
  • BLPOP: 데이터가 들어오면 왼쪽에서 데이터를 꺼내온다. 데이터가 있다면 LPOP처럼 왼쪽 데이터를 꺼내오지만 데이터가 없다면 데이터가 들어올때까지 timeout 시간만큼 기다린다
  • BRPOPLPUSH: 데이터가 들어오면 오른쪽에서 꺼내서 왼쪽으로 넣는다. 데이터가 있다면 RPOPLPUSH처럼 오른쪽에서 꺼내서 왼쪽에 넣지만 데이터가 없다면 데이터가 들어올때까지 timeout 시간만큼 기다린다.

레디스는 싱글쓰레드로 동작하기 때문에 3개의 명령어는 레디스에 락을 유도할 수 있기때문에 사용에 특히 유념해야 한다.

Sets

데이터의 중복을 허용하지 않은 일대 다 관계입니다. Sets의 데이터는 기본적으로 해시테이블로 저장되지만 숫자의 경우 정수배열데이터 구조를 가진다. 정수배열 데이터 구조는 해시테이블보다 메모리를 적게 사용합니다.메모리 용량을 중요시한다면 데이터를 정수 값으로 관리하면 좋을것 같다. <smembers 대신 sscan 명령어를 사용하자.>

Hashes

데이터를 field, value 구조로 저장가능하며 rdb의 테이블 구조와 비슷하다. 1개의 key에 field, value 쌍으로 약 40억개를 저장할 수 있다. 하지만 퍼포먼스 측면에서 1,000개정도의 데이터씩 관리하는걸 추천한다고 한다. hashes에 한번 너무 많은 데이터를 넣은 후 삭제할 경우 이슈가 생길 수 있다. <hgetall 대신 hscan 명령어를 사용하자>

Sorted set

1개의 key에 여러개의 score, value 형태로 저장할 수 있으며 value는 중복된 데이터를 넣을 수 없고 score로 정렬된다. 정렬을 필요로 하는 데이터 관리하면 좋다.

레디스를 운영할때 기억하면 좋은 것들.

만료일을 항상 넣는다.

메모리라는 한정된 데이터를 사용하기 때문에 만료일을 지정하여 데이터가 만료일에 지워질 수 있게 관리하자.

지워지면 안되는 데이터라면 백업을 하자.

지워지면 안되는 데이터라면 RDB + AOF 기능을 사용하여 항상 백업을 하자. RDB 를사용하여 디스크에 바이너리파일로 백업하자.RDB로 백업을 안하는 시간동안에는 AOF로 백업을 하자.

Slowlog로 성능을 확인하자

redis는 상당히 빠른 속도를 자랑하는 데이터베이스이기 때문에 조금만 느려지더라도 사용량에 따라 서비스에 큰 영향을 줄 수 있다. 그래서 레디스에 이슈가 있을때 가장 먼저 보는 것 중 하나는 Slowlog이다.

slowlog를 통해 command, timestamp, duration, ip 등의 정보를 알 수 있어 오래걸리는 명령어를 찾기 편리합니다. 하지만 slowlog는 기본적으로 메모리에 저장된다. enterprise 버전에서는 파일로 기록도 가능하다.

# 설정한 시간(microseconds) 이상인 명령을 기록한다.
# 기본값은 10000(10ms) 이다.
# 0으로 입력하면 모든 명령어를 기록한다.
# 비활성화 하려면 음수로 설정.
slowlog-log-slower-than 10000
# 설정한 수 만큼 보관한다.
# 기본값은 128이다.
# 제한은 없으며 꽉차면 오래된 것부터 지우고 새로운 명령어가 기록된다.
slowlog-max-len 128

AOF로 명령어 백업하기

명령어들을 파일에 저장 후 레디스가 리부트되면 파일에 저장된 명령어를 실행시켜서 데이터를 복구한다. 기본적으로 appendonly.aof 에 기록되며 입력/수정/삭제명령어 실행될때 마다 버퍼에 기록 후 파일에 기록된다.

파일이 너무 커질 것을 대비하여 일정 시간마다 파일을 다시 쓰게 되며 rewrite 시 최종값이 파일에 기록된다.

# AOF 기능 사용 유무
appendonly yes|no
# 파일 이름을 지정한다.
appendfilename [filename]
# 파일에 명령어를 기록하는 시점.
# always: 명령 실행시마다 AOF 파일에 기록.
# everysec: 1초마다 파일에 기록. 1초 사이의 데이터가 유실될 수 있다.
# no: 파일에 기록하는 시점을 os가 지정. 리눅스는 30초 간격으로 기록.
appendfsync [always | everysec | no]
# 이 값을 100으로 했다면 이전 다시 쓰기한 다음 AOF 파일 크기가 100% 증가했으면 다시 쓰기한다.
# 즉 값이 100이고, 이전 rewrite 했을때 1GB 였는데 파일이 2GB 되었다면 (100% 증가) rewrite 한다.
auto-aof-rewrite-percentage [ 0-100 ]
# 64mb 이하면 rewrite를 하지 않는다.
auto-aof-rewrite-min-size [ 64mb ]
# AOF 파일을 rewrite 할 때 disk에 쓰는 fsync를 32mb씩 나누어서 한다.
# 이것은 대량 디스크 쓰기로 발생할 수 있는 문제를 피할 수 있다.
aof-rewrite-incremental-fsync [ yes | no ]
# AOF 파일을 재작성할 때, Redis는 RDB preamble을 이용하여 더 빠르게 쓰고 복구할 수 있다.
# 이 옵션이 활성화되어 있다면, AOF파일을 재작성하는 작업은 다음 두 가지 스탠자(구)로 구성된다.
# 로딩 중, Redis는 AOF 파일이 "REDIS"라는 문자열로 시작한다면 알아차리고, 고정된 앞 부분은 RDB 파일로 로드하고, 계속해서 뒷 부분은 AOF파일로 로드한다.
# Redis 5.0 부터 기본값 yes. 5.0 이전은 no
aof-use-rdb-preamble [ yes | no ]

참고: https://mozi.tistory.com/369

RDB로 백업하기

레디스 데이터를 특정 시점에 바이너리 파일로 저장하는 방식으로 명령어를 저장하는 AOF 파일보다 파일 사이즈가 작기때문에 레디스 시작시 빠르게 데이터를 로드한다.

# rdb 저장 시점을 지정한다.
# time동안 count 만큼의 key변경이 발생하면 파일로 저장
save [time], [count]
# 저장된 파일명을 지정
dbfilename [ filename ]
# yes 일때 파일로 쓰다 실패하면 이 후 모든 쓰기 요청을 막는다.
# no 일때 파일ㄹ을 쓰다 실패하면 이 후 모든 동작으로 정상적으로 처리한다.
# save 명령어일때에만 적용되며 bgsave에서는 적용되지 않는다.
stop-writes-on-bgsave-error [ yes | no ]
# 파일을 쓸때 LZF 방식으로 압축할지 정한다.
# 기본값은 yes 이고 저장공간활용 측면에서 유리하다
# 압축할때 CPU를 사용하는데 부담된다면 no.
rdbcompression [ yes | no ]
# 파일이 정확히 저장되었는지 확인하는 방법으로 checksum을 파일 끝에 추가할 수 있다.
# 이 기능을 사용하면 파일의 정확성을 높일 수 있지만 파일을 저장할때, 로딩할때 10%정도 성능저하를 일으킬 수 있다.
# 기본값 yes
rdbchecksum [ yes | no ]
# RDB 파일을 쓸 때 disk에 쓰는 fsync를 32mb씩 나누어서 한다.
# 이점은 appendfsync가 everysec일 때, 디스크를 나누어서 사용하므로써,
# 1초마다 발생하는 AOF file 디스크 쓰기에 문제가 없도록하기 위함이다.
# 기본값 yes
rdb-save-incremental-fsync [ yes | no ]

http://redisgate.kr/redis/configuration/param_save.php

클러스터, 센티널환경이라면 백업은 선택사항이다.

클러스터와 센티널환경이라면 마스터가 다운되면 자동으로 슬레이브가 마스터로 승격되고 죽었던 이전 마스터노드는 슬레이브로 바뀌기 때문에 데이터 손실이 없다고 봐도 무방하기 때문에 백업을 하지 않아도 된다고 한다.

데이터 삭제는 Lazyfree 기능을 활용하자

데이터를 삭제하는 작업은 레디스에게 있어 큰 부담을 주는 작업이다. 하나의 키에 있는 데이터를 모두 지워야 하게 때문에 키에 담긴 데이터에 따라 수행시간이 얼마나 걸릴지 알 수 없다. 그래서 Redis 4.0 부터는 lazyfree 파라미터가 추가되었다.

  • lazyfree-lazy-eviction: eviction policy 에 의해 데이터가 지워질때 Del 명령어가 아닌 Unlink 명령으로 삭제한다.
  • lazyfree-lazy-expire: 만료된 키를 제거할때 Unlink 명령어를 사용합니다. 기본적으로 Del 명령어를 사용한다.
  • lazyfree-lazy-server-del: 존재하는 키에 SET 으로 다른 값을 넣을때, 키 이름을 바꿀때, SUNIONSTORE 명령에서 destination key와 source key를 같은 key로 지정했을 때 Unlink 명령어를 사용한다.
  • lazyfree-lazy-user-del: Del 명령이 내부적으로 Unlink로 동작한다.
  • slate-lazy-flush: slave 가 master로부터 전체 데이터를 받을 때 기존 데이터를 삭제하는데 이 때 Flushall async 로 데이터를 삭제하기 때문에 보다 빠르게 동기화 할 수 있다. 단, 비동기여도 삭제중에 데이터를 저장하는 것이기 때문에 메모리가 더 필요하다

Maxmemory 를 활용하자

레디스를 캐시로 사용한다면 maxmemory와 eviction policy를 설정하게 활용하자. maxmemory는 32비트 환경에서는 3GB가 기본으로 세팅되어 있고 64비트에서는 무제한으로 설정되어 있다.

머신의 메모리를 넘게 되면 레디스는 죽게되고 서비스에도 큰 장애를 줄 수 있다. maxmemory과 eviction policy를 지정하면 메모리가 maxmemory만큼 찻을때 eviction policy 에 따라 자동으로 기존 키가 제거되고 새로운 키를 저장한다.

Eviction policy의 종류는 아래와 같이 있다.

  • noeviction: maxmemory에 도달하면 쓰기/삭제 작업시 오류를 반환한다.
  • allkeys-lru: 새로 추가된 데이터의 용량을 확보하기 위해 최근에 사용하지 않은 키를 제거한다.
  • volatile-lru: 새로 추가된 데이터의 용량을 확보하기 위해 TTL이 설정된 키들 중 최근에 사용하지 않은 키를 제거한다.
  • allkeys-lfu: 새로 추가된 데이터의 용량을 확보하기 위해 사용빈도수가 가장 적은 키를 제거한다. (최근 저장된 키라도 사용 빈도수가 적다면 대상이 될 수 있다.)
  • volatile-lfu: 새로 추가된 데이터의 용량을 확보하기 위해 TTL이 설정된 키들 중 사용빈도 수가 적은 키를 제거한다 (최근 저장된 키라도 사용 빈도수가 적다면 대상이 될 수 있다.)
  • allkeys-random: 새로 추가된 데이터의 용량을 확보하기 위해 무작위로 키를 제거한다.
  • volatile-random: 새로 추가된 데이터의 용량을 확보하기 위해 TTL이 설정된 키들 중 무작위로 키를 제거한다.
  • volatile-ttl: 새로 추가된 데이터의 용량을 확보하기 위해 TTL이 짧은 키를 제거한다.

운영중인 레디스에 적용할 경우 config set 명령어를 사용하고 redis.conf 에도 업데이트한다.

tcp-keepalive를 조정하자.

TCP 통신시 3-way handshake 가 필요하다. TCP 통신을 위해 반드시 지켜야하는 표준 규약이면 회피할 수 없는 방법이다. TCP 통신할때마다 3-way handshake를 진행한다면 상당한 리소스가 소모되기때이다. 통신이 지속적으로 이루어진다면 한번 열어둔 세션을 없애지 않고 사용하면 효율적이라고 판단이 되었고 이를 위해 TCP keepalive 개념이 생겨나게 되었다.

tcp keepalive 기능은 커넥션을 유지하기 때문에 새로운 요청이 왔을때 기존 커넥션을 사용하여 통신을 할 수 있다. 하지만 반대로 요청을 받아들이지 않는 좀비 커넥션이 생길 수 있다는 단점도 가지고 있다.

좀비 커넥션 생성을 막기 위해 tcp-keepalive에 초를 설정하면 설정한 시간마다 연결이 유지되었는지를 확인하고 응답을 받았다면 연결을 유지하고 응답을 받지 못한다면 소켓을 닫는다.

3.2.1 부터 기본값 300이며 값이 0 이면 계속 열어둔다.

https://jihooyim1.gitbooks.io/linuxbasic/content/contents/08.html

한번에 여러 명령어를 쓸 경우 pipeline 활용하자.

여러 명령어를 한번에 실행할 경우 매번 명령어를 실행하기 보다 pipeline 을 사용하여 명령어를 실행하면 매번 커넥션을 맺어야하는 리소스를 줄 일 수 있다.

Tcp backlog 사이즈 조절

backlog 는 응답을 클라이언트에게 보내고 잘받았는지 체크 후 backlog 에서 제거한다. 동시에 많은 명령어가 몰려올때 backlog에 쌓이는데 backlog 사이즈보다 backlog에 쌓이는 수가 많으면 이슈가 발생할 수 있다.

Info all 로 서버상태 확인하기

Info all로 레디스의 상태를 확인 할 수 있다. commandstats 영역에서는 지금까지 사용된 명령어의 통계를 볼 수 있기때문에 어떤 명령어에서 많은 시간이 걸렸는지 확인이 가능하다.info all 에 보이는 통계는 config resetstat 으로 초기화가 가능하다.

https://iamabhishek-dubey.medium.com/redis-best-practices-and-performance-tuning-c48611388bbc

https://www.linkedin.com/pulse/performance-tuning-redis-lalit-kumar-jain/

https://redis.com/blog/redis-running-slowly-heres-what-you-can-do-about-it/

https://jihooyim1.gitbooks.io/linuxbasic/content/contents/08.html

https://redis.io/topics/clients

--

--