레이턴시 문제 진단
레이턴시 문제 진단 (Diagnosing latency issues)
Redis에서 레이턴시 문제를 겪고 있다면 무엇이 문제일 수 있는지 이해하도록 도와주는 문서예요.
여기서 레이턴시(latency) 란 클라이언트가 명령을 보내고 그 명령에 대한 응답을 받기까지의 최대 지연을 의미해요. 보통 Redis 처리 시간은 마이크로초 이하로 매우 낮지만, 레이턴시 수치를 높이는 특정 조건들이 있어요.
출처: 공식문서
시간이 없으니 체크리스트부터
이 문서의 내용은 Redis를 저지연으로 운영하기 위해 중요해요. 하지만 다들 바쁘다는 걸 이해하니 먼저 빠른 체크리스트부터 볼게요. 아래 단계를 따라도 해결되지 않으면 돌아와서 전체 문서를 읽어보세요.
- 서버를 차단하는 느린 명령(slow command) 을 실행하고 있지 않은지 확인하세요. Redis Slow Log 기능으로 확인할 수 있어요.
- EC2 사용자라면 m3.medium 같은 HVM 기반 최신 EC2 인스턴스를 사용하세요. 그렇지 않으면 fork()가 너무 느려요.
- 커널에서 transparent huge pages를 비활성화해야 해요.
echo never > /sys/kernel/mm/transparent_hugepage/enabled로 비활성화하고 Redis 프로세스를 재시작하세요. - 가상 머신을 쓴다면 Redis와 무관한 고유 레이턴시(intrinsic latency) 가 있을 수 있어요.
./redis-cli --intrinsic-latency 100으로 런타임 환경에서 기대할 수 있는 최소 레이턴시를 확인하세요. 주의: 이 명령은 클라이언트가 아니라 서버에서 실행해야 해요. - Redis의 Latency monitor 기능을 활성화·사용해 인스턴스의 레이턴시 이벤트와 원인에 대한 사람이 읽을 수 있는 설명을 얻으세요.
일반적으로 내구성과 레이턴시/성능 간의 트레이드오프 테이블을, 더 강한 안전성에서 더 나은 레이턴시 순서로 정리하면:
- AOF + fsync always: 매우 느려요. 무엇을 하는지 알 때만 쓰세요.
- AOF + fsync every second: 좋은 절충이에요.
- AOF + fsync every second + no-appendfsync-on-rewrite 옵션을 yes로: 위와 같지만 rewrite 중 fsync를 피해 디스크 부담을 낮춰요.
- AOF + fsync never: 이 구성에선 fsync를 커널에 맡겨요. 디스크 부담과 레이턴시 스파이크 위험이 더 줄어요.
- RDB: 구성한 save 트리거에 따라 매우 다양한 트레이드오프를 가져요.
이제 15분 시간이 있는 분들을 위해 자세한 내용을 설명할게요.
레이턴시 측정
레이턴시 문제를 겪고 있다면 애플리케이션 맥락에서 측정하는 법을 알거나, 문제가 거시적으로도 명확할 거예요. 하지만 redis-cli로 Redis 서버의 레이턴시를 밀리초 단위로 측정할 수 있어요:
redis-cli --latency -h `host` -p `port`
내장 Redis 레이턴시 모니터링 하위 시스템 사용
Redis 2.8.13부터 Redis는 서버가 어디서 차단되고 있는지 이해하기 위해 서로 다른 실행 경로를 샘플링할 수 있는 레이턴시 모니터링 기능을 제공해요. 이 문서에 설명된 문제 디버깅이 훨씬 단순해지므로 레이턴시 모니터링을 ASAP 활성화할 것을 권장해요. 자세한 내용은 Latency monitor 문서를 참고하세요.
레이턴시 모니터링의 샘플링·보고 기능이 Redis 시스템의 레이턴시 원인을 이해하기 쉽게 해주지만, 여전히 이 문서를 충분히 읽고 Redis와 레이턴시 스파이크 주제를 더 잘 이해할 것을 권장해요.
레이턴시 기준선 (Latency baseline)
Redis를 실행하는 환경에 본질적으로 포함된 레이턴시가 있어요. 운영체제 커널이 제공하는 레이턴시이고, 가상화를 쓴다면 사용하는 하이퍼바이저가 제공하는 레이턴시죠.
이 레이턴시는 제거할 수 없지만, 기준선(baseline) 이기 때문에 연구하는 게 중요해요. 다른 말로, 커널이나 하이퍼바이저의 구현·설정 때문에 환경에서 실행되는 모든 프로세스가 겪는 레이턴시보다 더 좋은 Redis 레이턴시를 얻을 수 없어요.
우리는 이 레이턴시를 intrinsic latency(고유 레이턴시) 라 부르고, redis-cli는 Redis 2.8.7부터 이를 측정할 수 있어요. Linux 3.11.0을 실행하는 엔트리 레벨 서버에서의 실행 예시예요.
참고: 인자 100은 테스트가 실행될 초(秒) 수예요. 테스트를 오래 실행할수록 레이턴시 스파이크를 발견할 가능성이 커져요. 100초면 보통 적절하지만, 다른 시간에 몇 번 실행해 보는 게 좋을 수 있어요. 테스트는 CPU 집약적이라 시스템에서 단일 코어를 포화시킬 가능성이 있어요.
$ ./redis-cli --intrinsic-latency 100
Max latency so far: 1 microseconds.
Max latency so far: 16 microseconds.
Max latency so far: 50 microseconds.
Max latency so far: 53 microseconds.
Max latency so far: 83 microseconds.
Max latency so far: 115 microseconds.
참고: redis-cli는 이 특수한 경우 Redis를 실행(또는 실행 예정)하는 서버에서 실행해야 해요. 이 특수 모드에서 redis-cli는 Redis 서버에 전혀 연결하지 않아요. 커널이 redis-cli 프로세스 자체에 CPU 시간을 제공하지 않는 가장 큰 시간을 측정하려 할 뿐이에요.
위 예시에서 시스템의 intrinsic latency는 단지 0.115 밀리초(115 마이크로초)로 좋은 편이에요. 하지만 intrinsic latency는 시스템 부하에 따라 시간이 지나며 바뀔 수 있다는 점을 기억하세요.
가상화 환경은 특히 높은 부하나 시끄러운 이웃(noisy neighbor)이 있을 때 그렇게 좋은 수치를 보여주지 않아요. 다음은 Redis와 Apache를 실행하는 Linode 4096 인스턴스에서의 실행 예시예요:
$ ./redis-cli --intrinsic-latency 100
Max latency so far: 573 microseconds.
Max latency so far: 695 microseconds.
Max latency so far: 919 microseconds.
Max latency so far: 1606 microseconds.
Max latency so far: 3191 microseconds.
Max latency so far: 9243 microseconds.
Max latency so far: 9671 microseconds.
여기선 intrinsic latency가 9.7 밀리초예요. 즉 Redis에 그것보다 더 좋은 걸 요구할 수 없어요. 하지만 더 높은 부하나 시끄러운 이웃이 있는 다른 가상화 환경의 다른 시간대 실행은 훨씬 더 나쁜 값을 쉽게 보여줄 수 있어요. 겉보기엔 정상적으로 돌아가는 시스템에서 최대 40 밀리초까지 측정한 적도 있어요.
네트워크와 통신으로 인한 레이턴시
클라이언트는 TCP/IP 연결이나 Unix 도메인 연결로 Redis에 접속해요. 1 Gbit/s 네트워크의 일반적인 레이턴시는 약 200us이고, Unix 도메인 소켓의 레이턴시는 30us까지 낮을 수 있어요. 실제로 네트워크와 시스템 하드웨어에 따라 달라요. 통신 자체에 더해, 시스템은 (스레드 스케줄링, CPU 캐시, NUMA 배치 등으로) 레이턴시를 더 추가해요. 시스템 유발 레이턴시는 물리 머신보다 가상화 환경에서 훨씬 높아요.
결과적으로 Redis가 대부분의 명령을 마이크로초 이하로 처리해도, 서버에 많은 왕복(roundtrip)을 수행하는 클라이언트는 이 네트워크·시스템 레이턴시를 지불해야 해요.
따라서 효율적인 클라이언트는 여러 명령을 함께 파이프라이닝해 왕복 횟수를 제한하려 해요. 서버와 대부분의 클라이언트가 완전히 지원해요. MSET/MGET 같은 집계 명령도 이 목적으로 쓸 수 있어요. Redis 2.4부터는 여러 명령이 모든 데이터 타입에 대해 가변 인자(variadic)를 지원하기도 해요.
몇 가지 가이드라인이에요:
- 여유가 된다면 VM보다 물리 머신을 서버 호스팅에 선호하세요.
- 서버에 체계적으로 연결/해제하지 마세요(웹 기반 애플리케이션에서 특히 중요). 연결을 최대한 오래 유지하세요.
- 클라이언트가 서버와 같은 호스트에 있다면 Unix 도메인 소켓을 쓰세요.
- 파이프라이닝보다 집계 명령(MSET/MGET)이나 (가능하면) 가변 인자 명령을 선호하세요.
- 일련의 왕복보다 (가능하면) 파이프라이닝을 선호하세요.
- Redis는 원시 파이프라이닝에 적합하지 않은 경우(예: 명령 결과가 다음 명령의 입력이 되는 경우)를 위해 Lua 서버 측 스크립팅을 지원해요.
Linux에서 일부 사람들은 프로세스 배치(taskset), cgroups, 실시간 우선순위(chrt), NUMA 구성(numactl), 저지연 커널을 써서 더 나은 레이턴시를 얻을 수 있어요. 바닐라 Redis는 단일 CPU 코어에 바인딩하는 것은 실질적으로 적합하지 않다는 점을 주의하세요. Redis는 BGSAVE나 BGREWRITEAOF처럼 매우 CPU 집약적일 수 있는 백그라운드 작업을 fork할 수 있어요. 이 작업들은 절대 메인 이벤트 루프와 같은 코어에서 실행되면 안 돼요.
대부분의 상황에서 이런 시스템 레벨 최적화는 필요 없어요. 필요할 때, 그리고 익숙할 때만 하세요.
Redis의 단일 스레드 특성
Redis는 대부분 단일 스레드 설계를 사용해요. 이는 멀티플렉싱(multiplexing) 이라는 기술을 사용해 단일 프로세스가 모든 클라이언트 요청을 서비스한다는 뜻이에요. Redis는 주어진 순간에 하나의 요청만 서비스할 수 있으므로, 모든 요청은 순차적으로 처리돼요. 이는 Node.js가 동작하는 방식과 매우 유사해요. 하지만 두 제품 모두 느리다고 인식되진 않아요. 부분적으로는 단일 요청을 완료하는 시간이 적기 때문이고, 주로는 소켓에서 데이터를 읽거나 쓰는 같은 시스템 콜에서 차단되지 않도록 설계됐기 때문이에요.
Redis가 대부분 단일 스레드라고 말한 이유는 실제로 Redis 2.4부터 일부 느린 I/O 연산(주로 디스크 I/O 관련)을 백그라운드에서 수행하는 데 스레드를 사용하기 때문이에요. 그래도 Redis가 모든 요청을 단일 스레드로 서비스한다는 사실은 바뀌지 않아요.
느린 명령으로 인한 레이턴시
단일 스레드의 결과로, 요청 하나를 처리하는 게 느리면 다른 모든 클라이언트가 이 요청이 처리될 때까지 기다려요. GET, SET, LPUSH 같은 일반 명령을 실행할 때는 전혀 문제가 없어요. 이런 명령들은 상수(그리고 아주 작은) 시간에 실행되니까요. 하지만 SORT, LREM, SUNION처럼 많은 요소에 작동하는 명령이 있어요. 예를 들어 두 개의 큰 set의 교집합을 구하는 건 상당한 시간이 걸릴 수 있어요.
모든 명령의 알고리즘 복잡도는 문서화되어 있어요. 익숙하지 않은 명령을 쓸 때 체계적으로 확인하는 게 좋은 습관이에요.
레이턴시 우려가 있다면 많은 요소로 구성된 값에 느린 명령을 쓰지 말거나, 모든 느린 쿼리를 실행하는 Redis 복제를 사용한 replica를 운영하세요.
느린 명령은 Redis Slow Log 기능으로 모니터링할 수 있어요.
또한 좋아하는 프로세스당 모니터링 프로그램(top, htop, prstat 등)으로 메인 Redis 프로세스의 CPU 소비를 빠르게 확인할 수 있어요. 트래픽이 없는데 높다면 보통 느린 명령이 쓰이고 있다는 신호예요.
중요 참고: 느린 명령 실행으로 인한 매우 흔한 레이턴시 원인은 프로덕션 환경에서 KEYS 명령을 쓰는 거예요. KEYS는 Redis 문서에 명시된 대로 디버깅 목적으로만 써야 해요. Redis 2.8부터 키 공간과 기타 큰 컬렉션을 점진적으로 반복하기 위한 새 명령이 도입됐어요. SCAN, SSCAN, HSCAN, ZSCAN 명령을 확인하세요.
fork로 인한 레이턴시
RDB 파일을 백그라운드로 생성하거나 AOF 영속화가 활성화된 경우 Append Only File을 다시 쓰려면 Redis가 백그라운드 프로세스를 fork해야 해요. (메인 스레드에서 실행되는) fork 연산 자체가 레이턴시를 유발할 수 있어요.
Forking은 대부분의 Unix 계열 시스템에서 값비싼 연산이에요. 프로세스에 연결된 많은 객체를 복사하는 작업을 포함하기 때문이죠. 특히 가상 메모리 메커니즘과 연관된 페이지 테이블이 그래요.
예를 들어 Linux/AMD64 시스템에서 메모리는 4kB 페이지로 나뉘어요. 가상 주소를 물리 주소로 변환하기 위해 각 프로세스는 주소 공간의 페이지마다 최소 하나의 포인터를 담은 페이지 테이블(실제로는 트리로 표현)을 저장해요. 그래서 큰 24GB Redis 인스턴스는 24GB / 4kB * 8 = 48MB 페이지 테이블이 필요해요.
백그라운드 저장을 수행하면 이 인스턴스를 fork해야 하므로 48MB 메모리를 할당·복사해야 해요. 특히 큰 메모리 청크의 할당·초기화가 비쌀 수 있는 가상 머신에서는 시간과 CPU가 걸려요.
시스템별 fork 시간
최신 하드웨어는 페이지 테이블 복사가 빠르지만, Xen은 그렇지 않아요. Xen의 문제는 가상화 특유가 아니라 Xen 특유예요. 예를 들어 VMware나 Virtual Box는 fork 시간이 느리지 않아요. 다음은 Redis 인스턴스 크기별 fork 시간을 비교한 표예요. 데이터는 BGSAVE를 수행하고 INFO 명령 출력의 latest_fork_usec 필드를 봐서 얻었어요.
좋은 소식은 새로운 유형의 EC2 HVM 기반 인스턴스는 fork 시간이 훨씬 나아져 물리 서버와 거의 맞먹는다는 거예요. 예를 들어 m3.medium(이상) 인스턴스를 쓰면 좋은 결과를 얻을 수 있어요.
- Linux beefy VM on VMware 6.0GB RSS가 77ms에 fork (GB당 12.8ms)
- 물리 머신의 Linux (Unknown HW) 6.1GB RSS가 80ms에 fork (GB당 13.1ms)
- 물리 머신의 Linux (Xeon @ 2.27Ghz) 6.9GB RSS가 62ms에 fork (GB당 9ms)
- 6sync의 Linux VM (KVM) 360MB RSS가 8.2ms에 fork (GB당 23.3ms)
- EC2의 Linux VM, 구형 인스턴스 (Xen) 6.1GB RSS가 1460ms에 fork (GB당 239.3ms)
- EC2의 Linux VM, 신형 인스턴스 (Xen) 1GB RSS가 10ms에 fork (GB당 10ms)
- Linode의 Linux VM (Xen) 0.9GB RSS가 382ms에 fork (GB당 424ms)
보시다시피 Xen에서 실행되는 일부 VM은 한 자릿수에서 두 자릿수 규모의 성능 저하를 보여요. EC2 사용자에게 제안은 간단해요: 최신 HVM 기반 인스턴스를 사용하세요.
transparent huge pages로 인한 레이턴시
안타깝게도 Linux 커널에 transparent huge pages가 활성화돼 있으면, Redis는 디스크에 영속화하기 위해 fork 호출을 쓴 뒤 큰 레이턴시 페널티를 입어요. Huge pages가 다음 문제의 원인이에요:
- fork가 호출되면 공유 huge pages를 가진 두 프로세스가 생성돼요.
- 바쁜 인스턴스에서 몇 번의 이벤트 루프 실행이 명령으로 하여금 수천 개의 페이지를 목표로 하게 만들어, 거의 전체 프로세스 메모리의 copy-on-write를 유발해요.
- 이는 큰 레이턴시와 큰 메모리 사용을 초래해요.
다음 명령으로 transparent huge pages를 비활성화하세요:
echo never > /sys/kernel/mm/transparent_hugepage/enabled
스와핑(운영체제 페이징)으로 인한 레이턴시
Linux(및 많은 다른 현대 OS)는 시스템 메모리를 효율적으로 쓰기 위해 메모리 페이지를 메모리와 디스크 사이로 옮길 수 있어요.
Redis 페이지가 커널에 의해 메모리에서 스왑 파일로 옮겨졌다면, 그 메모리 페이지에 저장된 데이터를 Redis가 쓸 때(예: 그 페이지에 저장된 키에 접근) 커널은 페이지를 메인 메모리로 다시 옮기기 위해 Redis 프로세스를 멈춰요. 이미 메모리에 있는 페이지에 접근하는 것에 비해 무작위 I/O를 수반하는 느린 연산이고, Redis 클라이언트가 겪는 비정상적 레이턴시로 이어져요.
커널이 Redis 메모리 페이지를 디스크로 옮기는 주된 이유는 세 가지예요:
- 실행 중인 프로세스가 가용 물리 메모리보다 더 요구해서 시스템이 메모리 압박을 받을 때. 이 문제의 가장 단순한 예는 Redis가 가용 메모리보다 더 많이 쓰는 거예요.
- Redis 인스턴스 데이터셋, 또는 데이터셋 일부가 대부분 완전히 유휴(클라이언트가 전혀 접근 안 함) 상태라, 커널이 유휴 메모리 페이지를 디스크로 스왑할 수 있을 때. 이 문제는 매우 드물어요. 적당히 느린 인스턴스조차 모든 메모리 페이지를 자주 건드려 커널이 모든 페이지를 메모리에 유지하도록 강제하거든요.
- 어떤 프로세스가 시스템에 대량의 읽기/쓰기 I/O를 생성할 때. 파일은 일반적으로 캐시되므로, 커널이 파일 시스템 캐시를 늘리도록 압박해 스와핑 활동을 생성하죠. 여기엔 큰 파일을 만들 수 있는 Redis RDB 및/또는 AOF 백그라운드 스레드도 포함돼요.
다행히 Linux는 문제를 조사할 좋은 도구를 제공해요. 스와핑으로 인한 레이턴시가 의심될 때 가장 간단한 건 실제로 그런지 확인하는 거예요.
첫 번째로 할 일은 디스크에 스왑된 Redis 메모리 양을 확인하는 거예요. 이렇게 하려면 Redis 인스턴스 pid를 얻어야 해요:
$ redis-cli info | grep process_id
process_id:5454
이제 이 프로세스의 /proc 파일 시스템 디렉토리로 들어가요:
$ cd /proc/5454
여기서 Redis 프로세스의 메모리 레이아웃을 설명하는 smaps라는 파일을 찾을 수 있어요(Linux 2.6.16 이상을 가정). 이 파일에는 프로세스 메모리 맵에 대한 매우 상세한 정보가 있고, Swap이라는 필드가 정확히 우리가 찾는 거예요. 하지만 Swap 필드가 하나만 있는 건 아니에요. smaps 파일에는 Redis 프로세스의 서로 다른 메모리 맵이 들어 있으니까요(프로세스 메모리 레이아웃은 단순한 페이지의 선형 배열보다 복잡해요).
프로세스에 의해 스왑된 모든 메모리에 관심이 있으므로, 먼저 전체 파일에서 Swap 필드를 grep하는 거예요:
$ cat smaps | grep 'Swap:'
Swap: 0 kB
Swap: 0 kB
Swap: 0 kB
Swap: 0 kB
Swap: 0 kB
Swap: 12 kB
Swap: 156 kB
Swap: 8 kB
...
모든 값이 0 kB이거나 가끔 4k 항목만 있다면 완전히 정상이에요. 실제 우리 예시 인스턴스(매초 수백 명의 사용자를 서비스하며 Redis를 실행하는 실제 웹사이트)에는 더 많은 스왑 페이지를 보여주는 항목이 몇 개 있어요. 심각한 문제인지 확인하려면 메모리 맵의 크기도 함께 출력하도록 명령을 바꿔요:
$ cat smaps | egrep '^(Swap|Size)'
Size: 316 kB
Swap: 0 kB
Size: 720896 kB
Swap: 12 kB
Size: 4096 kB
Swap: 156 kB
...
출력에서 720896 kB 맵(12 kB만 스왑)과 다른 맵에서 156 kB 더 스왑된 것을 볼 수 있어요. 기본적으로 아주 적은 양의 메모리만 스왑됐으니 전혀 문제가 되지 않아요.
반면 프로세스 메모리의 사소하지 않은 양이 디스크에 스왑됐다면 레이턴시 문제는 스와핑과 관련이 있을 가능성이 커요. 이 경우 vmstat 명령으로 추가 확인할 수 있어요:
$ vmstat 1
procs -----------memory---------- ---swap-- -----io---- -system-- ----cpu----
r b swpd free buff cache si so bi bo in cs us sy id wa
0 0 3980 697932 147180 1406456 0 0 2 2 2 0 4 4 91 0
0 0 3980 697428 147180 1406580 0 0 0 0 19088 16104 9 6 84 0
0 0 3980 697048 147180 1406640 0 0 0 0 18613 15987 6 6 88 0
우리에게 흥미로운 출력 부분은 스왑 파일로/로부터 스왑되는 메모리 양을 세는 si와 so 두 컬럼이에요. 이 두 컬럼에 0이 아닌 값이 보이면 시스템에 스와핑 활동이 있다는 뜻이에요.
마지막으로 iostat 명령으로 시스템의 전반적인 I/O 활동을 확인할 수 있어요.
$ iostat -xk 1
avg-cpu: %user %nice %system %iowait %steal %idle
13.55 0.04 2.92 0.53 0.00 82.95
Device: rrqm/s wrqm/s r/s w/s rkB/s wkB/s avgrq-sz avgqu-sz await svctm %util
sda 0.77 0.00 0.01 0.00 0.40 0.00 73.65 0.00 3.62 2.58 0.00
sdb 1.27 4.75 0.82 3.54 38.00 32.32 32.19 0.11 24.80 4.24 1.85
레이턴시 문제가 Redis 메모리가 디스크에 스왑된 것 때문이라면 시스템의 메모리 압박을 낮춰야 해요. Redis가 가용 메모리보다 더 많이 쓴다면 RAM을 추가하거나, 같은 시스템에서 다른 메모리 집약적 프로세스를 실행하지 않도록 하세요.
AOF와 디스크 I/O로 인한 레이턴시
또 다른 레이턴시 원인은 Redis의 Append Only File 지원 때문이에요. AOF는 기본적으로 두 개의 시스템 콜을 사용해 작업을 수행해요. 하나는 append only file에 데이터를 쓰는 write(2) 이고, 다른 하나는 사용자가 지정한 내구성 수준을 보장하기 위해 커널 파일 버퍼를 디스크로 flush하는 fdatasync(2) 예요.
write(2)와 fdatasync(2) 모두 레이턴시의 원인이 될 수 있어요. 예를 들어 write(2)는 시스템 전체 동기화가 진행 중이거나, 출력 버퍼가 가득 차서 새 쓰기를 받으려면 커널이 디스크로 flush해야 할 때 차단될 수 있어요.
fdatasync(2)는 더 나쁜 레이턴시 원인이에요. 커널과 파일 시스템의 많은 조합에서 완료까지 수 밀리초에서 수 초가 걸릴 수 있기 때문이에요. 특히 다른 프로세스가 I/O를 할 때 그렇죠. 이 때문에 Redis는 가능할 때 Redis 2.4부터 fdatasync(2) 호출을 별도 스레드에서 수행해요.
AOF 파일을 쓸 때 설정이 레이턴시의 양과 원인에 어떻게 영향을 주는지 볼게요.
AOF는 appendfsync 구성 옵션으로 세 가지 방식으로 디스크에 fsync를 수행하도록 구성할 수 있어요(이 설정은 CONFIG SET 명령으로 런타임에 수정 가능).
- appendfsync가 no 값이면 Redis는 fsync를 수행하지 않아요. 이 구성에서 레이턴시의 유일한 원인은 write(2)일 수 있어요. 이 경우 보통 해결책이 없어요. 단순히 디스크가 Redis가 데이터를 받는 속도를 따라잡지 못하는 거니까요. 하지만 다른 프로세스가 I/O로 디스크를 심각하게 느리게 하지 않았다면 흔치 않아요.
- appendfsync가 everysec 값이면 Redis는 1초마다 fsync를 수행해요. 별도 스레드를 쓰고, fsync가 아직 진행 중이면 Redis는 버퍼를 사용해 write(2) 호출을 최대 2초까지 지연시켜요(같은 파일에 fsync가 진행 중이면 Linux에서 write가 차단되므로). 하지만 fsync가 너무 오래 걸리면 Redis는 결국 fsync가 진행 중이어도 write(2) 호출을 수행하고, 이는 레이턴시 원인이 될 수 있어요.
- appendfsync가 always 값이면 모든 쓰기 연산마다 클라이언트에 OK 코드로 응답하기 전에 fsync를 수행해요(실제로 Redis는 동시에 실행된 많은 명령을 단일 fsync로 묶으려 해요). 이 모드에서 성능은 일반적으로 매우 낮고, 빨리 fsync를 수행할 수 있는 빠른 디스크와 파일 시스템 구현을 강력히 권장해요.
대부분의 Redis 사용자는 appendfsync 구성 지시어에 no 또는 everysec 설정을 써요. 최소 레이턴시를 위한 제안은 같은 시스템에서 다른 프로세스가 I/O를 하지 않도록 하는 거예요. SSD 디스크를 쓰는 것도 도움이 되지만, 보통 Redis가 seek 없이 append only file에 쓰기 때문에 디스크가 남아있다면 SSD가 아닌 디스크도 잘 작동해요.
append only file과 관련된 레이턴시 문제를 조사하고 싶다면 Linux에서 strace 명령을 쓸 수 있어요:
sudo strace -p $(pidof redis-server) -T -e trace=fdatasync
위 명령은 Redis가 메인 스레드에서 수행하는 모든 fdatasync(2) 시스템 콜을 보여줘요. appendfsync 설정이 everysec일 때 백그라운드 스레드가 수행하는 fdatasync 시스템 콜은 위 명령으로 볼 수 없어요. 그걸 보려면 strace에 -f 스위치를 추가하세요.
원한다면 fdatasync와 write 시스템 콜을 모두 다음 명령으로 볼 수도 있어요:
sudo strace -p $(pidof redis-server) -T -e trace=fdatasync,write
하지만 write(2)가 클라이언트 소켓에 데이터를 쓰는 데도 쓰이므로, 디스크 I/O와 무관한 너무 많은 것들이 보일 거예요. strace에 "느린 시스템 콜만 보여달라"고 하는 방법은 없는 것 같아서 다음 명령을 사용해요:
sudo strace -f -p $(pidof redis-server) -T -e trace=fdatasync,write 2>&1 | grep -v '0.0' | grep -v unfinished
만료(expires)로 인한 레이턴시
Redis는 만료된 키를 두 가지 방식으로 내보내요(evict):
- 하나는 lazy 방식으로, 명령이 키를 요청했는데 이미 만료된 것으로 발견되면 만료시켜요.
- 하나는 active 방식으로, 100밀리초마다 몇 개의 키를 만료시켜요.
Active 만료는 적응형(adaptive)으로 설계됐어요. 만료 사이클은 100밀리초마다(초당 10회) 시작되고 다음을 수행해요:
ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP개의 키를 샘플링해 이미 만료된 키를 모두 내보내요.- 만료된 키가 25%를 넘으면 반복해요.
ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP가 기본 20으로 설정되고 프로세스가 초당 10회 수행되므로, 보통 초당 200개의 키만 active하게 만료돼요. 이는 이미 만료된 키가 오랫동안 접근되지 않아 lazy 알고리즘이 도움이 되지 않을 때도 DB를 충분히 빠르게 청소하기에 충분해요. 동시에 초당 200개만 만료하는 것은 Redis 인스턴스의 레이턴시에 영향을 주지 않아요.
하지만 알고리즘은 적응형이라, 샘플링된 키 집합에서 이미 만료된 키가 25%를 넘으면 루프를 돌아요. 그런데 알고리즘을 초당 10회 실행하므로, 이는 우리의 무작위 샘플에서 25% 이상의 키가 적어도 같은 초에 만료되는 불운한 사건이 있다는 뜻이에요.
기본적으로 이는 데이터베이스에 같은 초에 만료되는 키가 아주 많고, 그 키들이 만료가 설정된 현재 키 인구의 25% 이상을 차지하면, Redis가 이미 만료된 키의 비율을 25% 미만으로 낮추기 위해 차단할 수 있다는 뜻이에요.
이 접근은 이미 만료된 키에 너무 많은 메모리를 쓰지 않기 위해 필요하고, 보통 완전히 무해해요. 정확히 같은 초에 많은 키가 만료되는 건 이상하니까요. 하지만 사용자가 같은 Unix 시간으로 EXPIREAT를 광범위하게 사용했다면 불가능한 일은 아니에요.
요약하면, 같은 순간에 많은 키가 만료되는 것은 레이턴시의 원인이 될 수 있다는 걸 명심하세요.
Redis 소프트웨어 워치독 (Redis Software Watchdog)
Redis 2.6은 Redis Software Watchdog을 도입했어요. 이는 어떤 이유로든 일반 도구를 사용한 분석을 빠져나간 레이턴시 문제를 추적하도록 설계된 디버깅 도구예요.
소프트웨어 워치독은 실험적 기능이에요. 프로덕션 환경에서 쓰도록 설계됐지만, Redis 서버의 정상 실행과 예상치 못한 상호작용을 할 수 있으므로 진행 전 데이터베이스를 백업해야 해요.
다른 수단으로는 문제를 추적할 방법이 없을 때 최후의 수단(last resort) 으로만 사용하는 게 중요해요.
이 기능의 작동 방식이에요:
- 사용자가
CONFIG SET명령으로 소프트웨어 워치독을 활성화해요. - Redis가 자신을 지속적으로 모니터링하기 시작해요.
- Redis가 서버가 충분히 빨리 반환하지 않는 어떤 연산에 차단됐고 그게 레이턴시 문제의 원인일 수 있다고 감지하면, 서버가 어디서 차단됐는지에 대한 저수준 보고서를 로그 파일에 덤프해요.
- 사용자는 워치독 보고서를 포함해 Redis Google Group에 메시지를 써서 개발자에게 연락해요.
이 기능은 redis.conf 파일로 활성화할 수 없어요. 이미 실행 중인 인스턴스에서 디버깅 목적으로만 활성화되도록 설계됐기 때문이에요.
기능을 활성화하려면 다음을 쓰면 돼요:
CONFIG SET watchdog-period 500
기간은 밀리초 단위로 지정해요. 위 예시에서는 서버가 500밀리초 이상의 지연을 감지한 경우에만 레이턴시 문제를 기록하도록 지정했어요. 구성 가능한 최소 기간은 200밀리초예요.
소프트웨어 워치독 사용이 끝나면 watchdog-period 파라미터를 0으로 설정해 끌 수 있어요. 중요: 꼭 이렇게 하세요. 필요 이상으로 오래 워치독을 켜둔 채 인스턴스를 유지하는 건 일반적으로 좋지 않으니까요.
소프트웨어 워치독이 구성된 값보다 긴 지연을 감지하면 로그 파일에 이렇게 인쇄되는 예시예요:
[8547 | signal handler] (1333114359)
--- WATCHDOG TIMER EXPIRED ---
/lib/libc.so.6(nanosleep+0x2d) [0x7f16b5c2d39d]
...
./redis-server(main+0x2b6) [0x418556]
참고: 예시에서 서버를 차단하는 데 DEBUG SLEEP 명령이 사용됐어요. 서버가 다른 맥락에서 차단되면 스택 트레이스가 달라져요.
워치독 스택 트레이스를 여러 개 수집하게 되면 모든 것을 Redis Google Group에 보내는 걸 권장해요. 트레이스를 많이 얻을수록 인스턴스의 문제가 무엇인지 이해하기 쉬워지니까요.