본문 바로가기
IT

레디스(Redis) 파이프라이닝(Pipelining)이란?

by eddy's warehouse 2024. 3. 14.

Redis는 명령을 일괄 처리하여 왕복 시간을 최적화하는 방법인 파이프라이닝을 제공합니다.

Redis 파이프라이닝은 개별 명령에 대한 응답을 기다리지 않고 여러 명령을 한 번에 실행하여 성능을 개선하는 기술입니다. 파이프라이닝은 대부분의 Redis 클라이언트에서 지원됩니다.

이 문서에서는 파이프라이닝이 어떤 문제를 해결할 수 있는지와 Redis에서 파이프라이닝이 작동하는 방식에 대해 설명해 드리도록 하겠습니다.

레디스(Redis) 파이프라이닝(Pipelining)이란? 썸네일

요청/응답 프로토콜 및 왕복 시간(RTT: round-trip time)

Redis는 클라이언트-서버 모델과 요청/응답 프로토콜이라고 하는 것을 사용하는 TCP 서버입니다.

즉, 일반적으로 요청은 다음 단계로 이루어집니다:

  • 클라이언트는 서버에 쿼리를 보내고, 보통 블로킹 방식으로 소켓에서 서버 응답을 읽습니다.
  • 서버는 명령을 처리하고 응답을 클라이언트로 다시 보냅니다.

예를 들어 네 개의 명령 시퀀스는 다음과 같습니다. 여기서 INCR은 레디스 명령어로 X의 키를 갖는 값에 1을 증가시키는 명령입니다.

클라이언트: INCR X
서버: 1
클라이언트: INCR X
서버: 2
클라이언트: INCR X
서버: 3
클라이언트: INCR X
서버 4

 

클라이언트와 서버는 네트워크 링크를 통해 연결됩니다. 이러한 링크는 매우 빠르거나(루프백 인터페이스) 매우 느릴 수 있습니다(두 호스트 사이에 많은 홉이 있는 인터넷을 통해 설정된 연결). 네트워크 지연 시간에 상관없이 패킷이 클라이언트에서 서버로 이동하고 서버에서 클라이언트로 다시 돌아와 응답을 전달하는 데는 시간이 걸립니다.

이 시간을 RTT(왕복 시간)라고 합니다. 클라이언트가 연속으로 많은 요청을 수행해야 할 때(예: 동일한 목록에 많은 요소를 추가하거나 많은 키로 데이터베이스를 채우는 경우) 이것이 성능에 어떤 영향을 미칠 수 있는지 쉽게 알 수 있습니다. 예를 들어 RTT 시간이 250밀리 초인 경우(인터넷을 통한 매우 느린 링크의 경우) 서버가 초당 10만 건의 요청을 처리할 수 있더라도 초당 최대 4건의 요청만 처리할 수 있습니다.

사용되는 인터페이스가 루프백 인터페이스인 경우 RTT는 일반적으로 밀리초 미만으로 훨씬 짧아지지만, 연속으로 많은 쓰기를 수행해야 하는 경우 이마저도 시간이 많이 늘어날 수 있습니다.

다행히도 이 사용 사례를 개선할 수 있는 방법이 있습니다.

 

Redis Pipelining

요청/응답 서버는 클라이언트가 이전 응답을 아직 읽지 않았더라도 새 요청을 처리할 수 있도록 구현할 수 있습니다. 이렇게 하면 응답을 기다릴 필요 없이 여러 명령을 서버로 전송하고 한 번에 응답을 읽을 수 있습니다.

이를 파이프라이닝이라고 하며 수십 년 동안 널리 사용되고 있는 기술입니다. 예를 들어 많은 POP3 프로토콜 구현은 이미 이 기능을 지원하여 서버에서 새 이메일을 다운로드하는 프로세스의 속도를 획기적으로 높여줍니다.

Redis는 초기부터 파이프라이닝을 지원해 왔기 때문에 실행 중인 버전에 관계없이 Redis에서 파이프라이닝을 사용할 수 있습니다. 다음은 netcat(nc) 유틸리티를 사용하여 레디스에 PING 명령 3개를 한번에 보내는 예시입니다.

$  (printf "PING\r\nPING\r\nPING\r\n"; sleep 1) | nc localhost 6379
+PONG
+PONG
+PONG

 

이번에는 모든 통화에 대해 RTT 비용을 지불하는 것이 아니라 세 가지 명령에 대해 한 번만 지불합니다.

명확하게 설명하자면 파이프라이닝을 사용하면 첫 번째 예제의 작업 순서는 다음과 같습니다:

클라이언트 : INCR X
클라이언트 : INCR X
클라이언트 : INCR X
클라이언트 : INCR X
서버 : 1
서버 : 2
서버 : 3
서버 : 4

 

중요 참고: 클라이언트가 파이프라이닝을 사용하여 명령을 전송하는 동안 서버는 메모리를 사용하여 응답을 강제로 대기열에 넣어야 합니다. 따라서 파이프라이닝을 사용하여 많은 명령을 보내야 하는 경우에는 각각 적절한 수(예: 10,000개의 명령)를 포함하는 일괄 처리로 보내고, 응답을 읽은 다음 다시 10,000개의 명령을 보내는 식으로 보내는 것이 좋습니다. 속도는 거의 동일하지만 추가로 사용되는 메모리는 이 10,000개의 명령에 대한 응답을 대기열에 대기시키는 데 필요한 양만큼만 늘어납니다.

 

파이프라이닝이 중요한 이유

 

파이프라이닝은 단순히 왕복 시간(RTT: round trip time)과 관련된 지연 비용을 줄이는 방법일 뿐만 아니라, 실제로 특정 Redis 서버에서 초당 수행할 수 있는 작업의 수를 크게 향상시킵니다. 파이프라이닝을 사용하지 않으면 데이터 구조에 액세스 하고 응답을 생성하는 관점에서는 각 명령을 제공하는 것이 매우 저렴하지만, 소켓 I/O를 수행하는 관점에서는 매우 비용이 많이 들기 때문입니다. 여기에는 read() 및 write() 시스템 호출이 포함되며, 이는 사용자 영역에서 커널 영역으로 이동하는 것을 의미합니다. 컨텍스트 전환은 엄청난 속도 저하를 초래합니다.

파이프라이닝을 사용하면 일반적으로 한 번의 read() 시스템 호출로 많은 명령을 읽고, 한 번의 write() 시스템 호출로 여러 개의 응답을 전달합니다. 따라서 초당 수행되는 총 쿼리 수는 처음에는 파이프라인이 길어질수록 거의 선형적으로 증가하여 결국 이 그림과 같이 파이프라이닝 사용하지 않은 성능의 10배에 도달하게 됩니다.

파이프라이닝 사용시 성능증가
파이프라이닝 사용시 성능증가

 

Redis 파이프라이닝 성능 측정을 통한 실제 성능

 

다음 벤치마크에서는 파이프라이닝을 지원하는 Redis 파이썬 클라이언트를 사용하여 파이프라이닝으로 인한 속도 향상을 테스트해 보겠습니다.

아래는 파이썬으로 작성한 파이프라이능을 쓰는 함수와 그렇지 않은 함수를 보여줍니다.

import redis
import time

def bench(descr):
    def decorator(func):
        def wrapper():
            start = time.time()
            func()
            print(f"{descr} {time.time() - start} seconds")
        return wrapper
    return decorator

@bench('without pipelining')
def without_pipelining():
    r = redis.Redis()
    for _ in range(10000):
        r.ping()

@bench('with pipelining')
def with_pipelining():
    r = redis.Redis()
    with r.pipeline() as pipe:
        for _ in range(10000):
            pipe.ping()
        pipe.execute()

if __name__ == "__main__":
    without_pipelining()
    with_pipelining()

 

결과는.. 로컬 루프백에서 10배 정도 성능 차이가 나타납니다.

로컬 루프백이므로 성능 차이가 가장 작게 나타나는데요, RTT가 매우 낮은 상태에서도 파이프라이닝은 그렇지 않은 것에 비해 아래 정도의 성능 개선을 보여주고 있습니다.

$ python3 pipe.py
without pipelining 1.1742589473724365 seconds
with pipelining 0.14603471755981445 seconds

 

오늘은 레디스에서 파이프라이닝이 무엇이고 왜 써야하는지에 대해서 알아보았습니다. 레디스를 사용한다면 파이프라이닝은 기본적으로 사용해야 하는 필수 요소로 보이며, 사용하지 않은 경우에 비해서 성능차가 10배가 나타나니 반드시 파이프라이닝을 사용하시기 바랍니다.

다음에는 레디스 프로그래밍 패턴에 대해서 알아보겠습니다.

facebook twitter kakaoTalk kakaostory naver band shareLink