거의 1년만에 아티클을 작성하게 되었는데, 오랜만에 작성하게된 아티클의 주제는 제가 직접 겪어본 Redis OOM으로 인한 장애에 대해서 공유를 해보고자합니다.
해당 아티클의 순서는 아래의 순서로 전개될 예정입니다.
- Redis OOM이 일어나게 된 배경
- Redis 아키텍처 돌아보기
- 단기적인 장애 대응 방법
- 최종적인 장애 대응 방법, 그리고 회고
- 글 마무리
Redis OOM 장애가 일어나게 된 배경
제가 운영하던 백엔드의 API 일부 중에는 Redis를 적극 활용하여 대용량 데이터를 프로세싱하여 클라이언트에게 추천 데이터를 반환해주는 로직이 하나 있었습니다.
그리고 API 서버는 ECS 환경에서 운영되어 트래픽이 늘어나거나, 혹은 CPU, MEM 사용량을 관측하여 일정 임계치를 넘어가면 태스크의 개수를 스케일 아웃하도록 설정이 되어있었습니다.
따라서 트래픽이 늘어나더라도 API 서버의 대수가 증가하여 해당 트래픽들을 분산 처리를 해주었기 때문에 어느 수준의 트래픽까지는 오토스케일링 만으로 해결이 가능했었습니다.
그러나.... 갑자기 어느날 오후 10시 40분 정도부터 아래의 에러가 레포트로 날아오기 시작했습니다.
OOM command not allowed when used memory than 'maxmemory'
해당 레포트가 날아오고, Redis를 사용하는 모든 API에 장애가 발생하기 시작하였습니다.
장애가 발생하였을 때에는 머리가 새하얘지면서, 아래의 생각만이 들었습니다.
- Redis에 캐시 추가 SET을 할 일이 별로 없는데 도대체 왜...?
- 누가 Redis에 고의적으로 이상한 커맨드를 날린거 아니야?
결론적으로 말씀드리면, 위의 두가지는 실제 장애의 원인과는 거리가 먼 상상들이었고, 실제 Redis의 장애 원인은 Redis에 들어간 과도하고, 그리고 하나하나가 헤비한 요청들이 쌓여서 문제를 일으켰던 것이었습니다. 정확하게 말씀드리면
- API 서버에 트래픽이 몰리면서 서버의 응답 자체가 많이 느려졌다.
- Redis와 커넥션을 맺은 클라이언트의 성능이 낮아지다보니, 클라이언트가 Redis로부터 데이터를 수신받는데 지연이 발생하기 시작하였다.
- 추천 데이터 프로세싱을 위한 Redis 데이터 하나하나의 크기는 어느 정도 큰 편인데 (매우 큰 정도는 또 아님), 해당 데이터가 수신 버퍼에 쌓이고 쌓여 OOM을 일으켰다.
이런 순서가 될 것 같습니다. 천천히 설명드리도록 하겠습니다.
Redis 아키텍처 간단하게 돌아보기
Redis가 데이터를 프로세싱 하는 구조를 돌아보기 보다는, 데이터가 Redis 내부에서 프로세싱된 이후의 절차를 위주로 돌아보려고합니다.Redis는 클라이언트와 커넥션을 맺게되면 커넥션마다 네트워크 송수신의 성능을 향상시키기 위해 송수신 버퍼라는 것을 두게됩니다.
그리고 Redis는 클라이언트로부터 GET 요청 등의 조회 요청이 날아오면 아래의 절차를 밟아서 클라이언트에게 데이터를 송신합니다.
- Redis가 Key를 통해서 내부의 테이블로부터 데이터를 가져옵니다.
- Redis는 응답 데이터를 준비하여 Connection에 물려있는 송신 버퍼에 데이터를 저장합니다.
- Redis는 네트워크를 통해서 클라이언트에게 송신 버퍼에 있는 데이터를 전송합니다.
- Redis는 클라이언트가 송신 버퍼의 데이터를 수신 받게되면 송신 버퍼로부터 해당 데이터를 제거합니다.
저희는 두번째 절차에 집중할 필요가 있어보입니다. 흔히 Redis에 대해서 생각한다면, 데이터가 저장되는 영역만을 생각하기 쉬운데, 사실 클라이언트에게 데이터를 빠르게 전송하기 위한 클라이언트 버퍼 영역도 생각을 해봐야한다는 것입니다. (Redis는 HA를 위해서 모든 데이터 공간을 Memory로 처리하기 떄문에, 클라이언트 버퍼 또한 Memory를 할당하여 데이터를 보관합니다)
단기적인 장애 대응
장애 대응에는 단기적인 장애 대응과 장기적인 장애 대응 이렇게 두 가지로 나뉩니다. 우선 장애가 발생하였다고 인지된 시점부터는 고객이 느끼는 해당 서비스의 DownTIme을 어떻게든 줄이는 것이 고객 경험 및 물리적 손해를 최대한 줄일 수 있기 때문에, 우선 단기적인 장애 대응부터 하는것이 먼저일 것입니다.
일단 장애를 인지한 순간에는 정확한 원인을 알 수는 없기 때문에, 에러 메시지를 보고 단순하게 Redis에 OOM이 발생하였구나 정도만 알 수 있었습니다.
따라서 서비스 다운타임을 최대한 줄이기 위해 레디스 노드들의 스펙을 스케일업하고, 클라이언트의 응답 속도를 어떻게든 향상시켜야했기 때문에 ECS 테스크들의 스펙들 또한 스케일업을 하였습니다.
장기적인 장애 대응
위의 방법을 통해서 하루 정도의 시간은 벌게되었습니다.
단기적으로 장애 대응을 하였다고는 하지만, 해당 내용이 완벽한 장애 대응이라고 볼 수는 없을 것입니다.
따라서 단기적으로 장애에 대응하였다면 최종적으로 정확히 무엇이 원인이었으며, 어떻게 해결할것이며, 해당 해결방법이 정말로 앞으로의 장애를 방지할 수 있는지 여부또한 보장이 되어야 비로소 장기적으로 장애에 대응하였다고 볼 수 있을것입니다.
원인은 정말 명확했습니다. 일단, Redis로부터 클라이언트로 송신하는 데이터가 최적화 되지 않은 상태였기에 하나하나의 데이터 크기가 비교적 큰 편이기도 했고, 또한 해당 추천 데이터를 호출하는 빈도가 너무 잦았던 것도 문제였습니다.
원인을 찾았기 때문에, 해당 문제점을 해결할 때 까지 서비스를 그 이전 시점으로 롤백하였고, API를 더 최적화 한 다음에 재배포 하여 문제를 해결하게 되었습니다.
회고
해당 장애가 제가 겪어본 서비스 장애 중에 다운타임이 제일 길었던 것 같습니다. 다운타임이 긴 만큼 그 당시의 제 마음또한 엄청 초조하였었는데요, 해당 장애를 겪으면서 시스템의 가시성(Observability)이 얼마나 중요한지 또한 깨닫게 되었던 것 같습니다.
저는 Redis가 알아서 잘 동작해주겠지 라는 마인드로 안일하게 Redis는 굳이 모니터링을 하지 않았었는데요, 해당 장애를 기점으로 Redis또한 모니터링의 대상이 되어야함을 깨달았고, Redis의 어떤 지표를 모니터링 해야하는지를 고민하게 되었습니다.
해당 장애에서 제일 아쉬웠던 것은, ElastiCache에는 네트워크 송수신 네트워크의 크기가 시계열성으로 얼마나 되는지를 보여주는 지표가 존재하였는데요, 해당 지표를 일찍 관측하였더라면 어땠을까라는 생각도 해보게 되었습니다.
최종 정리하면, Redis를 운영하면서 관측해야할 지표는 아래와 같을겁니다.
- Redis 가용 메모리 크기
- Redis 송수신 네트워크 크기
- Redis 송수신 네트워크 패킷 개수
- Redis 클라이언트 Max connections
- Redis Read/Write Operations 개수
- Redis CPU 사용률
- Redis 메모리 단편화 비율
글 마무리
작성하다보니 글이 조금 길어졌습니다. 이 글을 보시는 여러분들도 저와 같은 장애를 겪지 않도록, 저의 글이 여러분에게 장애를 예방하는 계기가 되었으면 좋겠습니다.
긴 글 읽어주셔서 감사합니다!
참고 자료
https://redis.io/blog/top-redis-headaches-for-devops-client-buffers/
https://redis.io/docs/latest/develop/reference/clients/
'devOps' 카테고리의 다른 글
서비스 장애 잘 이해하고 대비하기 (0) | 2024.07.06 |
---|---|
Docker File System (Overlay2) (0) | 2023.08.06 |
Kubernetes 워커노드의 OOM에 의한 클러스터 장애 (0) | 2023.04.11 |
Jenkins on K8S를 설정하며 겪은 일들 (0) | 2023.03.04 |
내가 쿠버네티스 설정하며 겪은 삽질들 (alb-controller, jenkins, monitoring) (0) | 2023.02.27 |