Computer >> 컴퓨터 >  >> 프로그래밍 >> 데이터베이스

Redis 성능 최적화 전략: 핵심은 '느리게 만들지 않는 것'

Redis 성능 최적화 전략: 핵심은  느리게 만들지 않는 것

간단한 캐싱부터 수 테라바이트 규모의 대형 시스템까지 다양한 용도로 Redis를 사용하는 많은 개발자와 기업들과 이야기를 나누다 보면, 가장 자주 받는 질문이 바로 성능에 관한 것입니다.

Redis는 성능에 접근하는 방식 자체가 다릅니다. 대부분의 데이터베이스 서버에서는 성능을 높이는 방법을 고민하지만, Redis에서는 성능을 떨어뜨리지 않는 것이 목표입니다. 이는 완전히 다른 접근 방식이며, 이를 제대로 활용하려면 다른 사고방식이 필요합니다.

성능 지표 – 지연 시간(Latency)이 왕이다?

Redis를 사용할 때 우리가 주목해야 할 성능 지표는 본질적으로 두 가지입니다. 초당 몇 개의 명령(또는 트랜잭션)을 실행할 수 있는가, 그리고 각 명령이 얼마나 걸리는가입니다. 그런데 잘 살펴보면 전자는 후자의 파생 결과일 뿐입니다.

Redis는 단일 스레드(single-threaded)로 동작하기 때문에, 초당 처리할 수 있는 연산 수(ops/sec)는 곧 각 명령의 소요 시간과 직결됩니다. 따라서 궁극적으로 중요한 것은 제가 '명령 지연 시간(command latency)'이라고 부르는 값입니다.

Redis의 장점 중 하나는 단순함입니다. 쿼리(query)가 아닌 명령(command)을 발행하는 구조죠. 명령은 데이터를 가져오는 훨씬 간단한 경로이며, 이 단순함이 곧 속도로 이어집니다. 또한 개발자가 다양한 쿼리 패턴에 맞춰 일반적으로 최적화하는 대신, 특정 작업에 특화된 명령을 직접 제공할 수 있다는 점도 큰 장점입니다. 이 우아한 단순함은 그대로 Redis를 사용하는 프로그래머에게 전달됩니다. 즉, 세트 필터링 같은 '쿼리' 유형의 작업은 서버가 아니라 클라이언트 측에서 처리하는 경우가 많습니다.

이런 종류의 조회 작업을 서버에서 일관되게 처리하는 것이 더 낫다고 생각하는 사람도 있지만, 저는 이제 그 방식이 선호되지 않는다고 봅니다. 그 이유는 흔히 말하는 확장성(scalability) 때문입니다.

예를 들어 '수평 확장 가능(horizontally scalable)'한 웹 서비스나 사이트를 운영하기 시작하면, 초기에는 이 패턴이 잘 작동하는 것처럼 보입니다. 하지만 감당하기 어려울 정도의 대규모 트래픽을 처리하게 되면 데이터베이스가 심각한 병목 지점이라는 사실을 금방 깨닫게 됩니다. 그리고 곧이어, 보통 MySQL 같은 SQL 스토어인 그 데이터베이스는 수평으로 확장할 수 없다는 것, 즉 단순히 인스턴스를 추가하는 방식으로 해결할 수 없다는 것을 알게 됩니다.

명령 지연 시간(Command Latency)

물론 Redis 역시 수평 확장이 자유롭지 않습니다. 하지만 결정적인 차이가 있습니다. 필터링 로직, 정렬, 그리고 하나의 Redis 명령(혹은 몇 개 안 되는 명령)으로 실행할 수 없는 모든 작업을 클라이언트에 두면, 데이터베이스에 불필요한 로직과 '처리해야 할 것들'을 떠넘기지 않게 됩니다.

Redis는 전통적인 '데이터베이스 서버'가 아니라 '데이터 스토어'로 사용해야 합니다. 이것이 'Redis를 느리게 만들지 마라' 원칙의 첫 번째 핵심입니다. 덧붙여 말하자면, Lua 스크립팅의 길로 들어서는 순간 오히려 전체 성능이 하락할 위험이 있습니다. 트래픽이 낮거나 보통 수준인 개발 환경에서는 눈치채지 못할 수 있지만, 대규모 트래픽 상황에 도달하면 문제가 드러납니다. 그 시점에는 이미 해당 로직이 코드에 깊이 박혀 있어서 애플리케이션 코드로 옮기기 위해 상당한 기술 부채(technical debt)를 감수해야 하는 경우가 많습니다. 기술 부채 정리에 얼마나 낮은 우선순위가 부여되는지는 우리 모두 잘 알고 있죠.

그렇다고 Lua 스크립트가 쓸모없다는 뜻은 아닙니다. 다만 철저히 검토해서 사용해야 한다는 의미입니다. Lua 스크립트가 순성능 손실인지 아닌지 판단하는 좋은 기준은, 그 로직을 클라이언트 측에서 처리할 경우 추가로 발생하는 네트워크 왕복(round trip) 비용과 비교하는 것입니다. 단, 클라이언트 코드가 얼마나 빨리 처리할 수 있는지가 아니라 왕복 횟수의 비용을 기준으로 삼아야 합니다.

이렇게 생각해 보세요. 왕복 비용을 2ms 줄였는데 스크립트 실행 시간으로 3ms가 추가되었다면, 방향을 잘못 잡은 것입니다.

여기서 단일 스레드 서버의 특성을 예리하게 인식해야 합니다. 그 3ms짜리 스크립트가 실행되는 동안 수백, 어쩌면 수천 개의 명령이 블로킹됩니다. 반면 이를 (조회) → (로직) → (조회) 순서로 분리하면, '(로직)' 단계 동안 서버는 다른 요청들을 계속 처리할 수 있습니다. 로직을 클라이언트 코드에 두면 다중 클라이언트 기반 시스템이 본래 가진 동시성(concurrency)을 그대로 보존할 수 있습니다.

물론 트랜잭션이나 Lua 스크립팅이 꼭 필요하다면 사용하세요. 다만 코드가 '더 편해 보인다'는 이유만으로 사용해서는 안 됩니다. 항상 동시성 성능에 미치는 영향을 인식하고, 실제로 측정한 뒤, 의식적으로 선택해야 합니다.

여기서 도출되는 'Redis를 느리게 만들지 마라'의 두 번째 원칙은 다음과 같습니다: 스크립트를 통한 서버 측 로직을 피하고 동시성을 보존하라. 덤으로 얻는 이점도 있습니다. 여러 키에 걸쳐 동작하는 Lua 스크립트를 재작성하거나 폐기하지 않고도 Redis Cluster 환경으로 전환할 수 있습니다.

Redis를 느리게 만드는 다른 요인들

Redis 서버 운영에는 시스템 수준 혹은 운영적인 측면에서 성능을 떨어뜨릴 수 있는 요소들이 몇 가지 더 있습니다. I/O 자원 제한이 대표적입니다. 예컨대 8GB의 네트워크 대역폭이 필요한데 1GB만 사용 가능하다면, 당연히 '느리게' 느껴질 것입니다. 데몬 프로세스에 허용된 열린 소켓 수가 필요한 동시 연결 수보다 적으면, 명령을 실행하는 대신 연결이 닫히기를 기다리는 시간이 소모됩니다.

운영 선택 때문에 Redis가 느려지는 가장 흔한 두 가지 경우는 다음과 같습니다:

  1. 가상 머신 위에서 구동하는 것 — 특히 Xen 하이퍼바이저 기반 VM
  2. 무거운 디스크 영속성(persistence) 설정

첫 번째는 Redis 공식 문서 등 표준 문헌에서 이미 충분히 다뤄집니다. Xen 하이퍼바이저 기반 VM에 올리지 말라는 것입니다. 두 번째인 영속성 문제 역시 다뤄지긴 하지만, 제 생각에는 그 내용이 충분히 깊지 않습니다.

물론 표준적인 권장 사항들이 있습니다. 로컬 고속 디스크 사용, 영속화 시 CoW(copy-on-write)를 감당할 충분한 메모리 확보, 아예 슬레이브에서만 영속화 수행 등입니다. 이런 요소들이 실제로 명령 지연 시간에 영향을 줄 수 있습니다. 그런데 빠져 있는 부분은, 그 영향이 실제로 있는지 어떻게 제대로 판단하느냐입니다.

이를 테스트하는 표준 방법 중 하나는 Redis에 내장된 지연 시간(latency) 진단 도구를 활용하는 것입니다. 기술적인 세부 방법에 들어가기 전에, 더 큰 물음인 '어떻게'에 대해 짚고 넘어가겠습니다. 핵심은 언제 이 테스트를 실행하느냐입니다. 우선, 데이터셋이 작다면 이 테스트는 무의미할 가능성이 높습니다. 얼마나 작아야 할까요? 데이터를 디스크에 덤프하는 데 걸리는 시간을 확인하세요. 1~2초, 길어야 10초 수준이라면 의미 있는 데이터를 얻기 어렵습니다. 물론 이 모든 전제는 SAVE가 아닌 BGSAVE를 사용한다는 것입니다.

영속성 지연(Persistence Latency)

여기서 제가 '영속성 지연(persistence latency)'이라 부르는 개념의 첫 번째 원칙이 나옵니다. 영속성 지연이란 명령이 서버에 도달한 시점부터 그 결과가 어떤 형태로든 영속 저장소에 기록되기까지 걸리는 시간을 말합니다. 원칙은 간단합니다: 지연을 최소한으로 추가하는 영속화 옵션을 찾으세요.

네트워크와 디스크 환경에 따라 다르지만, 일반적으로 AoF(Append Only File)나 슬레이브 서버가 지연 증가폭이 가장 작은 영속화 옵션이며, RDB는 가장 뒤에 위치합니다.

RDB가 마지막인 주된 이유는 'T초 동안 N번 변경'이라는 내장된 지연 구조 때문입니다. 최소 변경 횟수 N이 해당 최소 기간 내에 충족된다고 가정하면(기본 최소 윈도우는 60초), 영속성 지연은 간격 T에 RDB를 디스크에 기록하는 시간을 더한 값이 됩니다. 예를 들어 기본 60초 간격에서 메모리를 디스크에 덤프하는 데 30초가 걸린다면, 영속성 지연은 (60+30) 90초입니다.

그렇다면 이 영속성 지연이 과연 문제인지 묻게 됩니다. 이 질문에는 두 가지 측면이 있습니다. 첫째, 비즈니스 요구 사항에 충분한가? 둘째, Redis를 느리게 만드는가?

첫 번째 질문은 모두에게 통하는 일반적인 답을 드릴 수 없습니다. 다만 영속성 지연에 대한 요구 사항이 위 공식보다 엄격하다면, 답은 RDB가 아니라 '슬레이브 및/또는 AoF 사용' 쪽으로 기울 가능성이 높습니다. 물론 이는 Redis를 구동하는 플랫폼에서 명백한 실수를 저지르지 않는다는 전제입니다. 이것이 두 번째 측면, 즉 Redis를 느리게 만드는가로 이어집니다.

어떤 사람들은 'RDB 파일을 저장하는 데 얼마나 걸리는가'라는 질문에 집착합니다. 제 견해로는 이것이 오류입니다. 저장 시간이 정말 중요할까요? 이 질문의 답은 '저장 작업이 Redis를 느리게 만드는가'에 달려 있습니다. 느려지지 않는다면 1초가 걸리든 1시간이 걸리든 신경 쓸 필요가 없습니다.

예를 들어, 서버 A가 RDB 저장에 1,000초가 걸린다고 해봅시다. 저장하지 않을 때의 명령 지연 범위는 30~100마이크로초입니다. 그런데 저장 중에도 지연은 여전히 30~100마이크로초 범위를 유지합니다. 이 경우, Redis 성능이 나쁘다는 명목으로 RDB 저장 시간을 줄이는 작업에 나서는 것은 이른 최적화(premature optimization)입니다.

반면 서버 B는 겨우 10초 만에 저장하지만, 명령 지연이 30~100us에서 130~250us로 뛰어오릅니다. 이제야 RDB 파일 생성 시간을 신경 써야 할 이유가 생깁니다. 저장 작업이 Redis를 느리게 만들고 있으므로 그 영향을 최소화해야 하기 때문입니다. 더 빠른 디스크가 해결책이라면, 이제야 그 투자를 정당화할 근거가 생기는 것입니다. 물론 100~150마이크로초 정도의 증가가 애플리케이션에 실질적인 성능 부담이 아니라면, 다시 이른 최적화의 영역으로 돌아갑니다.

이 지연 시간을 실제로 측정하는 방법에 대해서는, 이 글이 이미 상당히 길어졌으므로 다음 파트에서 더 자세히 다루겠습니다.