Redis는 싱글 스레드
Redis를 사용하다 보면 장애가 발생하거나 성능이 예측한대로 나오지 않는 경우가 종종 발생합니다.
이들은 모두 Redis가 싱글 스레드라는 것을 잊어버리거나 모르고 있기 때문에 발생하는 현상입니다.
싱글 스레드인 Redis는 시간이 오래 걸리는 명령어를 호출하면 그 명령어를 처리하는 동안 다른 클라이언트의 요청을 처리할 수가 없습니다.
이로인한 실무에서 흔히 하는 실수의 사례를 살펴보겠습니다.
1. 서버에서는 Keys 명령어를 사용하지 말자.
Redis 명령어 중에, 현재 서버에 저장된 Key 목록을 볼 수 있는 keys 명령어가 있습니다.
모든 Key를 가져올 때는 "*"를 사용하고 특정한 문자가 들어가 있는 Key를 가져올때는 "*문자*"를 사용합니다.
127.0.0.1:6379> keys *
1) "user_1"
127.0.0.1:6379> keys *user*
1) "user_1"
keys 명령어는 굉장히 좋은 기능을 제공하는 듯 합니다.
하지만 모든 Key를 대상으로 검색하기 때문에 실제 서비스에서 해당 명령어를 사용하면 장애로 이어질 가능성이 높습니다.
저장된 데이터가 적을때는 문제가 없겠지만 데이터가 많아질 수록 성능은 더욱 느려집니다.
Redis 메뉴얼을 보면 아시겠지만, 해당 명령어는 실제 제품에서는 쓰지 말라고 설명하고 있습니다.
Key 목록을 꼭 얻어야하는 경우에는 필요한 데이터 구조를 Key에 맞춰 저장하는 방법이 있겠습니다.
2. flushall/flushdb 명령을 주의하자.
Redis에는 모든 데이터를 삭제하는 "flushall/flushdb"라는 명령어가 있습니다.
Redis는 db라는 가상의 공간을 분리할 수 있는 개념을 제공하고, select 명령으로 이동할 수 있습니다.
이를 통해 같은 Key 이름이라도 "db 0번"이나 "db 1번" 등, db 개수에 따라 여러개를 만들 수 도 있습니다.
이런 db를 통째로 지우는 명령어가 flushdb 명령어이고, db의 모든 내용을 지울 수 있는 명령어는 flushall 입니다.
(select 명령어를 주지 않으면 기본적으로는 0번을 사용합니다.)
마찬가지로 Redis는 싱글스레드이기 때문에 모든 데이터는 지우는 동안 다른 명령에 영향을 받게 되어 장애로 이어질 수 있습니다.
Redis의 Persistent 기능
Redis와 Memcached의 차이는 Persistent 기능을 제공한다는 점입니다.
Memcached의 경우 서버가 장애를 일으켜 문제가 발생하면 모든 데이터가 사라지지만, Redis는 디스크에 저장되어 있는 데이터를 기반으로 다시 복구할 수 있습니다.
그래서 Persistent 기능은 Redis를 데이터 스토어 형태로 사용할 경우 거의 필수적으로 사용해야 합니다.
하지만 이렇게 좋아 보이는 Redis의 디스크 저장 기능이 사실 장애의 주된 원인이 됩니다. 그래서 적절히 알고 사용하는 것이 아주 중요합니다.
Redis에서 제동하는 RDB, AOF 등의 기능에 대해 알아보고 장애를 피할 수 있는 방법을 알아보겠습니다.
1. RDB
Redis에서 현재 메모리에 대한 덤프를 생성하는 기능을 RDB라고 합니다.(RDBMS와는 관계가 1도 없습니다.)
- 그럼 앞에서 Redis는 싱글 스레드라고 설명을 보고 몇가지 의문사항이 들 것입니다.
- 현재의 메모리에 대한 스냅샷을 저장하는 동안에는 Redis는 아무런 요청을 못하는 것이 아닌가?
- 저장하는 동안에도 계속 데이터의 생성, 변경, 삭제 작업이 이루어질텐데 이에대한 정확도에 영향은 없을 것인가?
Redis는 지속적인 서비스와 RDB 저장을 위해 fork를 통하여 자식 프로세스를 생성하여 현재 메모리 상태를 복제합니다.
정리하자면 RDB는 가장 최신 데이터라기 보다는 특정한 시점의 데이터, 즉 스냅샷이라고 생각해야 합니다.
RDB 저장을 위해서는 "SAVE"와 "BGSAVE" 명령어가 있습니다.
- SAVE : 모든 작업을 멈추고 현재 메모리 상태에 대한 RDB 파일 생성(생성 동안에는 아무런 작업도 불가)
- BGSAVE : 자식 프로레스를 생성하는 fork 작업을 통해 자식 프로세스에서 RDB 파일 생성(생성 동안에도 작업 가능)
기본적으로 RDB는 redis.conf에 사용함으로 설정되어 있고, save 설정대로 동작하게 됩니다.
이 설정을 모두 지울 경우, RDB가 자동적으로 생성되지 않습니다.
(redis.conf는 해당 글을 참고해주세요!)
2. AOF
AOF는 "Append Only File"의 약어로, 데이터를 저장하기 전에 AOF 파일에 현재 수행해야 할 명령을 미리 저장해두고, 장애가 발생하면 AOF를 기반으로 복구합니다.
즉, 다음과 같은 순서로 데이터가 저장됩니다.
- 클라이언트가 Redis에 업데이트 관련 명령어를 요청
- Redis는 해당 명령을 AOF에 저장
- 파일쓰기가 완료되면 실제로 해당 명령을 실행해서 메모리의 내용을 변경
AOF는 redis.conf에서 기본적으로 "사용 안 함"으로 설정되어 있기 때문에 사용하려면 아래와 같이 설정을 변경해야 합니다.
(redis.conf는 해당 글을 참고해주세요!)
여기서 주의해야할 설정은 appendfsync 값입니다.
AOF를 파일에 저장할 때, OS가 파일쓰기 시점을 결정하여 파일을 버퍼 캐시(메모리)에 저장하고 적절한 시점에 이 데이터를 디스크로 저장합니다.
옵션 | 내용 |
always | AOF 값을 추가할 때마다 fsync를 호출해서 디스크에 실제 쓰기 |
everysec | 매초마다 fsync를 호출해서 디스크에 실제 쓰기 |
no | OS가 실제 sync를 할 때까지 따로 설정하지 않음 |
디스크 쓰기가 실제 속도에 영향을 주므로 당연히 Redis의 속도는 no > everysec > always 순 입니다.
AOF와 RDB의 우선순위
AOF와 RDB 모두 Redis에서 Persistent를 구현하는 방법인데, 두 개의 파일이 모두 있다면 어떤 것을 읽게 될까?
당연히 최신 데이터를 더 많이 가진 파일을 읽어야 할 텐데 어떤 파일이 최신 데이터를 더 많이 가지고 있는지 알 수 없습니다.
하지만 RDB는 특정한 시점을 기준으로 스냅샷을 저장해 장애 발생 시 다음 저장할 때까지의 데이터는 모두 유실되지만, AOF는 매 작업마다 디스크에 기록을 남기기 때문에 모든 데이터가 남아있기 때문에 AOF를 읽게 됩니다.
3. Redis가 메모리를 두 배로 사용하는 문제
Redis가 운영되는 중에 장애를 일으키는 가장 큰 원인은 RDB를 저장하는 Persistent 기능으로, fork를 사용하기 때문입니다.
fork로 자식 프로세스를 생성하고 Read 작업일때는 부모 프로세스와 자식 프로세스는 같은 메모리를 공유합니다.
하지만 자식 프로세스에서 write가 발생할 때 공유하는 해당 데이터는 자식 프로세스에 복사되기 되기 때문에 자식 프로세스에도 메모리가 부여됩니다.
메로리가 4GB인 장비가 있다고 할 때, 3GB를 사용하다가 fork해서 자식 프로세스에서 wirte 작업이 많이 생긴다면 메모리가 부족하므로 어떤 문제가 발생할지 알 수 없습니다.
그렇다면 Redis 서버에 메모리를 얼마나 할당하는 것이 좋을까?
Redis 자체는 64bit에서 메모리를 다루는 크기에는 한계가 없지만(다만 Key와 Value는 각각 하나당 최대 512MB가 한계), 다음과 같은 기준으로 메모리를 할당하는 것이 좋습니다.
예를들어 Core 4개를 가지고 있으며 메모리가 32GB인 장비를 사용한다면 프로세스 별로 6GB(4x6=24GB) 정도를 할당하는 것이 좋습니다.
하나의 Redis서버를 하나의 장비에서 사용하는 것보다는 멀티코어를 활용하기 위해 여러개의 Redis 서버를 한 서버에 띄우는 것이 성능 면에서 좋습니다.(단 관리비용은 증가)
여러개의 Redis 서버를 하나의 서버에 띄우면, RDB 저장으로 인해 자식 프로세스가 생성됩니다.
즉 프로세스 4개와 RDB용 저장 프로세스를 합쳐 총 5개의 프로세스가 생성되더라도, 30GB만 사용하므로 메모리에 여유가 있게 됩니다.
4. Read는 가능한데 Write만 실패하는 경우
Redis를 운영하면서 RDB를 사용하는 사람이라면 Redis 서버는 동작하지 않는데, 정기적인 health check에는 이상이 없다고 나오는 상황을 99% 경험하게 됩니다.
이는 Redis의 기본 설정 상 RDB 저장이 실패하면 해당 장비에 뭔가 이상이 있다고 생각하여 Write 명령을 더는 처리하지 않으며, 데이터가 변경되지 않도록 관리하기 때문입니다.
일반적으로 health check는 Read 관련 명령을 이용하여 검사하기 때문에 정상으로 인식합니다.
그렇다면 어떠한 이유로 RDB 생성에 실패할까?
- RDB를 저장할 수 있을 정도의 디스크 여유 공간이 없는 경우
- 실제 디스크가 고장난 경우
- 메모리 부족으로 인해서 자식 프로세스를 생성하지 못한 경우
- 누군가 강제적으로 자식 프로세스를 종료시킨 경우
위 이유 등으로 RDB 저장에 실패하면 Redis 내부에 "lastbgsave_status"라는 변수가 REDIS_ERR로 설정됩니다.
(lastbgsave_status 값은 src/rdb.c의 "backgroundSaveDoneHandler"에서 처리)
그러면 "processCommand"라는 함수에서 사용자 요청이 들어왔을 때 Write 관련 요청은 모두 무시되게 됩니다.
이러한 문제는 어떻게 해결해야 할까?
첫번째 방법은 해당 상황이 맞는지를 확인하는 것 입니다.
해당 상황이라면 Write 관련 요청에 대해서 항상 다음과 같은 응답을 받게 됩니다.
$ 127.0.0.1:6379> set test 123
(error) MISCONF Redis is configured to save RDB snapshots, but is currently
not able to persist on disk. Commands that may modify the data set are
disabled. Please check Redis logs for details about the error.
두번째 방법은 info 명령어를 이용해 rdb_last_bgsave_status의 상태를 확인하는 것 입니다.
rdb_last_bgsave_status가 ok 라면 정상적인 상태입니다.
$ info
...
rdb_last_bgsave_status:ok
...
마지막으로 정책상 Write 명령어를 허용해도 되는지 결정해야 합니다.
이런 상황에서 계속 데이터를 변경하는 Write 명령을 허용할지 여부는 서비스의 정책에 따라 다릅니다.
Write를 허용한다고 가정한다면 아래 명령어를 통해 설정할 수 있습니다.
$ 127.0.0.1:6379> config set stop-writes-on-bgsave-error no
OK
redis.conf에도 미리 등록이 가능한데 이는 Redis 2.6.13 버전에서 추가되었습니다. (redis.conf는 해당 글을 참고해주세요!)
'Redis' 카테고리의 다른 글
6. Spring Redis 분산락(Distribute Lock)을 활용한 동시성 처리 (1) | 2024.03.12 |
---|---|
5. Redis HA(High availability)와 Sentinel (0) | 2024.02.21 |
4. Redis 복제 (0) | 2024.02.15 |
2. Docker로 Redis 실행하기 (0) | 2023.09.30 |
1. Redis의 이해 (1) | 2023.09.30 |