Redis ‐ Redis Pipelining & RTT(Round Trip Time) - thought-corner/Backend-PlayGround GitHub Wiki

Redis - Redis Pipelining & RTT(Round Trip Time)

1. 일반 실행(Request-Response)

  • 동작 : 명령어 하나 보내고 서버가 처리해서 응답을 줄 때까지 기다렸다가 응답을 받으면 다음 명령어를 보낸다.
  • 병목의 원인(RTT) : 서버 처리속도가 아니라 네트워크 왕복 시간(RTT, Round Trip Time)이 핵심이다. 명령어가 100개라면 네트워크를 타고 왔다 갔따 하는 시간만 100배가 소요된다.

2. 파이프라이닝(Pipelining)

  • 동작 : 클라이언트가 응답을 기다리지 않고 여러 개의 명령어를 한꺼번에 묶어서 전송한다.
  • 네트워크 오버헤드 감소로 RTT를 단 1회로 줄여버린다. 그렇게 되면 명령어가 많을수록 일반 방식보다 수십배에서 수백배까지 빠른 성능을 낼 수 있다.

1. 전송 단계 : 일단 한 번에 다 보낸다

  • 동작 : 클라이언트가 명령어 1, 2, 3을 각각 따로 보내고 응답을 기다리는게 아니라, 하나의 패킷에 실어서 네트워크로 한꺼번에 쏜다.
  • 해당 과정에서 RTT(왕복 시간)의 절반(Request Latency)이 딱 한 번만 발생한다.

2. 서버 실행 단계 : 순차적으로, 하지만 쉼 없이

  • 동작 : Redis 서버는 도착한 명령 리스트를 풀어서 순서대로 실행한다.
  • Redis는 싱글 쓰레드이기 때문에 한 번에 하나씩 처리한다. 하지만 명령어 사이사이에 네트워크 응답을 기다리는 대기 시간이 없기 때문에 CPU를 100% 효율적으로 사용하며 아주 빠르게 루프를 돈다.

3. 응답 단계 : 결과도 묶어서 한 번에

  • 동작 : 모든 명령 실행이 끝나면 서버는 결괏값들을 하나고 묶어서 클라이언트에게 보낸다.
  • 여기서도 RTT의 나머지 절반(Response Latency)이 한 번만 발생한다.

1. 원자적 실행 : 트랜잭션 그 이상의 보장

  • 동작 : Redis 서버는 Lua 스크립트를 실행하는 동안 다른 어떤 클라이언트 명령도 처리하지 않는다.
  • 일반적인 MULTI-EXEC 트랜잭션은 명령어들을 묶어서 던지는 것뿐이지만 Lua 스크립트는 스크립트 전체가 하나의 거대한 원자적 명령처럼 동작한다.

2. 복잡한 로직 : 서버에서 계산까지 끝낸다

  • 동작 : 클라이언트가 데이터를 가져와서 자바 코드에서 if문을 돌리고 다시 저장하는 개념이 아니라 if문 로직 자체를 Redis 서버로 보낸다.

3. 네트워크 절감 : RTT의 최소화

  • 파이프라이닝과의 공통점 : 여러 명령을 한 번의 네트워크 왕복(RTT)으로 처리한다.
  • 차이점 : 파이프라이닝은 그냥 명령 리스트를 쏘는 거지만 Lua 스크립트는 로직을 포함한 자체를 쏜다.
  • 애플리케이션과 Redis 사이를 왔다갔다 할 필요가 없으므로 네트워크 지연이 심한 환경에서 성능을 비약적으로 높인다.

1. EVAL 명령과 스크립트 전송

  • 동작 : 클라이언트가 EVAL 명령과 함꼐 Lua 소스 코드를 서버에 보낸다.
  • 전송 : 스크립트 본체와 그 안에서 사용할 Key 개수, 실제 인자들이 네트워크를 타고 Redis 서버에 도착한다.

2. Lua 인터프리터의 역할

  • Redis 서버 내부는 Lua 5.1 인터프리터가 내장되어 있다.
  • 서버에 전송된 스크립트는 이 인터프리터에 로드되어 해석된다. 이 때, Redis는 스크립트를 컴파일하여 실행 준비 상태로 만든다.

3. 핵심: 원자적 실행과 다른 명령 차단

  • 배타적 실행 : Redis는 싱글 쓰레드로 동작한다. Lua 스크립트가 실행되는 순간, Redis 서버는 해당 스크립트 처리에만 전념한다.
  • 블로킹 처리 : 스크립트가 수행되는 동안 외부에서 들어오는 다른 모든 명령은 큐에서 대기해야 한다.
  • 결과 : 조회하고 계산하고 저장하는 과정 사이클이 다른 클라이언트가 절대 비집고 들어올 수 없는 완벽한 격리성이 보장된다.

4. 스크립트 내 Redis 명령 호출

  • 스크립트 내부에서 redis.call() 또는 redis.pcall() 함수를 통해 실제 Redis 명령을 호출한다.
  • 해당 과정은 서버 내부 메모리 안에서 일어나므로 네트워크 지연(RTT)이 전혀 발생하지 않는다.

스크립트 등록(Registration)

  • SHA1 해시 : Redis는 입력받은 스크립트 코드 전체를 SHA1 알고리즘을 통해 고유한 40글자 해시값으로 변환한다.
  • SCRIPT LOAD : 클라이언트가 SCRIPT LOAD 명령을 실행하면 Redis 서버는 코드를 실행하지 않고 내부 메모리 캐시에 저장한 뒤, 방금 만든 해시값을 반환한다.
  • 서버에 캐싱 : 이제 Redis 서버는 해당 해시값은 해당 코드라는 매핑 정보를 기억하게 된다.

해시로 실행(Execution via EVALSHA)

  • EVALSHA : 클라이언트는 이제 긴 스크립트 전체를 보낼 필요가 없고 서버로부터 받은 해시값만 EVALSHA [해시값] 1 [키] [인자] 형태로 보낸다.
  • 해시로 실행 : 서버는 받은 해시값이 캐시에 있는지 확인ㄹ하고 있다면 저장된 코드를 즉시 꺼내서 실행한다.
  • 네트워크 절감 : 스크립트 코드가 10KB 정도로 길더라도, 네트워크로는 단 40글자의 해시만 전송하면 된다. 대역폭을 획기적으로 아낄 수 있다.

1. 명령 최소화(Minimize Commands)

  • Redis 호출 횟수 자체가 성능 결정타. MSET으로 묶거나 파이프라이닝을 활용한다.

2. 테이블 효율(Memory Efficiency)

  • Redis는 메모리 기반. 메모리는 비싼 자원임을 인지해야 한다.
  • 데이터 구조를 짤 때 무조건 String보다 필드가 많다면 Hash를 쓰는 등 메모리 사용량을 최적화하는 설계도 필요하다.
  • 키 이름을 너무 길게 짓지 않는 것도 작은 시작이 된다.

3. 조기 반환(Early Return)

  • Redis는 싱글 쓰레드. CPU를 오래 붙잡고 있게 되면 서비스 전체가 느려진다.
  • Lua 스크립트를 짤 때 중요하다. 처리할 조건이 안 맞으면 즉시 Return해서 Redis 서버가 불필요한 연산을 하지 않고 바로 다음 명령을 처리할 수 있게 비켜줘야 한다.

4. 캐시 활용(Cache Utilization - EVALSHA)

  • 네트워크 대역폭도 자원이다.
  • 무거운 스크립트 코드를 매번 전송하지 않고, 서버에 캐싱된 해시값만 보내서 네트워크 부하와 스크립트 해석 시간을 최소화하는 EVALSHA를 사용한다.