Lettuce Pipelining

Redis Developer Course Redis Technical Support Redis Enterprise Server

Lettuce Pipelining


1. Redis Pipelining

Redis 명령을 일괄 처리하여 왕복 시간을 최적화하는 방법
How to optimize round-trip times by batching Redis commands

Redis 파이프라이닝은 개별 명령에 대한 응답을 기다리지 않고 한 번에 여러 명령을 실행하여 성능을 향상시키는 기술입니다. 파이프라이닝은 대부분의 Redis 클라이언트에서 지원됩니다. 이 문서에서는 파이프라이닝이 해결하도록 설계된 문제와 Redis에서 파이프라이닝이 작동하는 방식을 설명합니다.
Redis pipelining is a technique for improving performance by issuing multiple commands at once without waiting for the response to each individual command. Pipelining is supported by most Redis clients. This document describes the problem that pipelining is designed to solve and how pipelining works in Redis.

요청/응답 프로토콜 및 왕복 시간
Request/Response protocols and Round-Trip Time (RTT)

Redis는 클라이언트-서버 모델과 소위 요청/응답 프로토콜을 사용하는 TCP 서버입니다. 이는 일반적으로 요청이 다음 단계를 통해 수행됨을 의미합니다.
Redis is a TCP server using the client-server model and what is called a Request/Response protocol. This means that usually a request is accomplished with the following steps:

  • 클라이언트는 서버에 쿼리를 보내고 일반적으로 차단 방식으로 소켓에서 서버 응답을 읽습니다.
    The client sends a query to the server, and reads from the socket, usually in a blocking way, for the server response.
  • 서버는 명령을 처리하고 클라이언트에 응답을 다시 보냅니다.
    The server processes the command and sends the response back to the client.

예를 들어 4개의 명령 시퀀스는 다음과 같습니다.
So for instance a four commands sequence is something like this:

클라이언트와 서버는 네트워크 링크를 통해 연결됩니다. 이러한 링크는 매우 빠르거나(루프백 인터페이스) 매우 느릴 수 있습니다 (두 호스트 사이에 많은 홉이 있는 인터넷을 통해 설정된 연결). 네트워크 대기 시간이 무엇이든 패킷이 클라이언트에서 서버로 이동하고 응답을 전달하기 위해 서버에서 클라이언트로 다시 이동하는 데는 시간이 걸립니다.
Clients and Servers are connected via a network link. Such a link can be very fast (a loopback interface) or very slow (a connection established over the Internet with many hops between the two hosts). Whatever the network latency is, it takes time for the packets to travel from the client to the server, and back from the server to the client to carry the reply.

이 시간을 RTT(Round Trip Time)이라고 합니다. 클라이언트가 연속적으로 많은 요청을 수행해야 할 때(예를 들어 동일한 목록에 많은 요소를 추가하거나 많은 키로 데이터베이스를 채우는 경우) 이것이 성능에 어떤 영향을 미칠 수 있는지 쉽게 알 수 있습니다. 예를 들어 RTT 시간이 250밀리초인 경우(인터넷을 통한 링크가 매우 느린 경우) 서버가 초당 100,000개의 요청을 처리할 수 있더라도 초당 최대 4개의 요청을 처리할 수 있습니다.
This time is called RTT (Round Trip Time). It's easy to see how this can affect performance when a client needs to perform many requests in a row (for instance adding many elements to the same list, or populating a database with many keys). For instance if the RTT time is 250 milliseconds (in the case of a very slow link over the Internet), even if the server is able to process 100k requests per second, we'll be able to process at max four requests per second.

사용된 인터페이스가 루프백 인터페이스인 경우 RTT는 훨씬 더 짧으며 일반적으로 1밀리초 미만이지만 연속해서 많은 쓰기를 수행해야 하는 경우에는 이 값도 추가됩니다.
If the interface used is a loopback interface, the RTT is much shorter, typically sub-millisecond, but even this will add up to a lot if you need to perform many writes in a row.

다행히도 이 사용 사례를 개선할 수 있는 방법이 있습니다.
Fortunately there is a way to improve this use case.

레디스 파이프라이닝 Redis Pipelining

클라이언트가 이전 응답을 아직 읽지 않은 경우에도 새 요청을 처리할 수 있도록 요청/응답 서버를 구현할 수 있습니다. 이렇게 하면 응답을 전혀 기다리지 않고 서버에 여러 명령을 보내고 마지막으로 한 단계로 응답을 읽을 수 있습니다.
A Request/Response server can be implemented so that it is able to process new requests even if the client hasn't already read the old responses. This way it is possible to send multiple commands to the server without waiting for the replies at all, and finally read the replies in a single step.

이를 파이프라이닝이라고 하며 수십 년 동안 널리 사용되는 기술입니다. 예를 들어 많은 POP3 프로토콜 구현은 이미 이 기능을 지원하여 서버에서 새 이메일을 다운로드하는 프로세스 속도를 극적으로 향상시킵니다.
This is called pipelining, and is a technique widely in use for many decades. For instance many POP3 protocol implementations already support this feature, dramatically speeding up the process of downloading new emails from the server.

Redis는 초기부터 파이프라이닝을 지원해왔으므로 실행 중인 버전에 관계없이 Redis에서 파이프라이닝을 사용할 수 있습니다. 다음은 원시 netcat 유틸리티를 사용하는 예입니다.
Redis has supported pipelining since its early days, so whatever version you are running, you can use pipelining with Redis. This is an example using the raw netcat utility:

이번에는 모든 호출에 대해 RTT 비용을 지불하지 않고 세 가지 명령에 대해 한 번만 지불합니다.
This time we don't pay the cost of RTT for every call, but just once for the three commands.

명확히 말하면 첫 번째 예제의 작업 순서는 파이프라인을 통해 다음과 같습니다.
To be explicit, with pipelining the order of operations of our very first example will be the following:

중요 참고 사항: 클라이언트가 파이프라이닝을 사용하여 명령을 보내는 동안 서버는 메모리를 사용하여 응답을 대기열에 추가해야 합니다. 따라서 파이프라이닝을 사용하여 많은 명령을 보내야 하는 경우 각각 합리적인 수(예: 10k 명령)를 포함하는 배치로 보내고 응답을 읽은 다음 또 다른 10k 명령을 다시 보내는 것이 좋습니다. 속도는 거의 동일하지만 사용되는 추가 메모리는 기껏해야 이러한 10k 명령에 대한 응답을 대기열에 넣는 데 필요한 양입니다.
IMPORTANT NOTE: While the client sends commands using pipelining, the server will be forced to queue the replies, using memory. So if you need to send a lot of commands with pipelining, it is better to send them as batches each containing a reasonable number, for instance 10k commands, read the replies, and then send another 10k commands again, and so forth. The speed will be nearly the same, but the additional memory used will be at most the amount needed to queue the replies for these 10k commands.

RTT만의 문제가 아닙니다. It's not just a matter of RTT

파이프라이닝은 왕복 시간과 관련된 대기 시간 비용을 줄이는 방법일 뿐만 아니라 실제로 특정 Redis 서버에서 초당 수행할 수 있는 작업 수를 크게 향상시킵니다. 파이프라이닝을 사용하지 않으면 각 명령을 제공하는 것이 데이터 구조에 액세스하고 응답을 생성하는 관점에서는 매우 저렴하지만 소켓 I/O를 수행하는 관점에서는 비용이 매우 많이 들기 때문입니다. 여기에는 read() 및 write() 시스템 호출 호출이 포함됩니다. 이는 사용자 영역에서 커널 영역으로 이동하는 것을 의미합니다. 컨텍스트 전환(context switch)은 속도를 크게 떨어뜨립니다.
Pipelining is not just a way to reduce the latency cost associated with the round trip time, it actually greatly improves the number of operations you can perform per second in a given Redis server. This is because without using pipelining, serving each command is very cheap from the point of view of accessing the data structures and producing the reply, but it is very costly from the point of view of doing the socket I/O. This involves calling the read() and write() syscall, that means going from user land to kernel land. The context switch is a huge speed penalty.

파이프라이닝을 사용하면 일반적으로 단일 read() 시스템 호출로 많은 명령을 읽고 단일 write() 시스템 호출로 여러 응답을 전달합니다. 결과적으로, 초당 수행되는 총 쿼리 수는 처음에는 파이프라인이 길어질수록 거의 선형적으로 증가하며, 이 그림에 표시된 대로 결국 파이프라인 없이 얻은 기준의 10배에 도달합니다.
When pipelining is used, many commands are usually read with a single read() system call, and multiple replies are delivered with a single write() system call. Consequently, the number of total queries performed per second initially increases almost linearly with longer pipelines, and eventually reaches 10 times the baseline obtained without pipelining, as shown in this figure.

pipeline_iops

실제 코드 예제 A real world code example

다음 벤치마크에서는 파이프라이닝을 지원하는 Redis Ruby 클라이언트를 사용하여 파이프라이닝으로 인한 속도 향상을 테스트합니다.
In the following benchmark we'll use the Redis Ruby client, supporting pipelining, to test the speed improvement due to pipelining:
위의 간단한 스크립트를 실행하면 루프백 인터페이스를 통해 실행되는 Mac OS X 시스템에서 다음 그림이 생성됩니다. 여기서 파이프라이닝은 RTT가 이미 매우 낮기 때문에 가장 작은 개선을 제공합니다.
Running the above simple script yields the following figures on my Mac OS X system, running over the loopback interface, where pipelining will provide the smallest improvement as the RTT is already pretty low:

without pipelining     1.185238 seconds
with pipelining           0.250783 seconds

보시다시피 파이프라이닝을 사용하여 전송을 5배 향상했습니다.
As you can see, using pipelining, we improved the transfer by a factor of five.

Pipelining vs Scripting

Redis 2.6부터 사용 가능한 Redis 스크립팅을 사용하면 서버 측에서 필요한 많은 작업을 수행하는 스크립트를 사용하여 파이프라이닝에 대한 다양한 사용 사례를 보다 효율적으로 처리할 수 있습니다. 스크립팅의 가장 큰 장점은 최소한의 대기 시간으로 데이터를 읽고 쓸 수 있어 읽기, 계산, 쓰기와 같은 작업이 매우 빠르게 수행된다는 것입니다. 클라이언트는 쓰기 명령을 호출하기 전에 읽기 명령의 응답이 필요하므로 이 시나리오에서는 파이프라인이 도움이 되지 않습니다.
Using Redis scripting, available since Redis 2.6, a number of use cases for pipelining can be addressed more efficiently using scripts that perform a lot of the work needed at the server side. A big advantage of scripting is that it is able to both read and write data with minimal latency, making operations like read, compute, write very fast (pipelining can't help in this scenario since the client needs the reply of the read command before it can call the write command).

때로는 애플리케이션이 파이프라인에서 EVAL 또는 EVALSHA 명령을 보내려고 할 수도 있습니다. 이는 전적으로 가능하며 Redis는 SCRIPT LOAD 명령을 통해 이를 명시적으로 지원합니다 (실패 위험 없이 EVALSHA를 호출할 수 있음을 보장함).
Sometimes the application may also want to send EVAL or EVALSHA commands in a pipeline. This is entirely possible and Redis explicitly supports it with the SCRIPT LOAD command (it guarantees that EVALSHA can be called without the risk of failing).

부록: 루프백 인터페이스에서도 사용 중 루프가 느린 이유는 무엇입니까?
Appendix: Why are busy loops slow even on the loopback interface?

이 페이지에서 다룬 모든 배경 지식에도 불구하고 다음과 같은 Redis 벤치마크(의사 코드)가 서버와 클라이언트가 동일한 물리적 시스템에서 실행 중일 때 루프백 인터페이스에서 실행될 때에도 느린 이유가 여전히 궁금할 수 있습니다.
Even with all the background covered in this page, you may still wonder why a Redis benchmark like the following (in pseudo code), is slow even when executed in the loopback interface, when the server and the client are running in the same physical machine:

결국 Redis 프로세스와 벤치마크가 모두 동일한 상자에서 실행되는 경우 실제 대기 시간이나 네트워킹이 포함되지 않고 메모리의 메시지를 한 위치에서 다른 위치로 복사하는 것 아닌가요?
After all, if both the Redis process and the benchmark are running in the same box, isn't it just copying messages in memory from one place to another without any actual latency or networking involved?

그 이유는 시스템의 프로세스가 항상 실행되는 것은 아니며 실제로 프로세스를 실행하는 것은 커널 스케줄러이기 때문입니다. 예를 들어 벤치마크 실행이 허용되면 Redis 서버에서 (마지막으로 실행된 명령과 관련된) 응답을 읽고 새 명령을 작성합니다. 명령은 이제 루프백 인터페이스 버퍼에 있지만 서버에서 읽으려면 커널이 서버 프로세스(현재 시스템 호출에서 차단됨)가 실행되도록 예약해야 합니다. 따라서 실용적인 측면에서 루프백 인터페이스에는 커널 스케줄러 작동 방식으로 인해 여전히 네트워크와 유사한 대기 시간이 포함됩니다.
The reason is that processes in a system are not always running, actually it is the kernel scheduler that lets the process run. So, for instance, when the benchmark is allowed to run, it reads the reply from the Redis server (related to the last command executed), and writes a new command. The command is now in the loopback interface buffer, but in order to be read by the server, the kernel should schedule the server process (currently blocked in a system call) to run, and so forth. So in practical terms the loopback interface still involves network-like latency, because of how the kernel scheduler works.

기본적으로 비지 루프 벤치마크는 네트워크로 연결된 서버의 성능을 측정할 때 수행할 수 있는 가장 어리석은 작업입니다. 현명한 것은 이런 식으로 벤치마킹을 피하는 것입니다.
Basically a busy loop benchmark is the silliest thing that can be done when metering performances on a networked server. The wise thing is just avoiding benchmarking in this way.


2. Pipelining and command flushing

Redis는 클라이언트-서버 모델과 소위 요청/응답 프로토콜을 사용하는 TCP 서버입니다. 이는 일반적으로 요청이 다음 단계를 통해 수행됨을 의미합니다.
Redis is a TCP server using the client-server model and what is called a Request/Response protocol. This means that usually a request is accomplished with the following steps:

클라이언트는 서버에 쿼리를 보내고 일반적으로 차단 방식으로 소켓에서 서버 응답을 읽습니다. 서버는 명령을 처리하고 클라이언트에 응답을 다시 보냅니다.
The client sends a query to the server and reads from the socket, usually in a blocking way, for the server response. The server processes the command and sends the response back to the client.

클라이언트가 이전 응답을 아직 읽지 않은 경우에도 새 요청을 처리할 수 있도록 요청/응답 서버를 구현할 수 있습니다. 이렇게 하면 응답을 전혀 기다리지 않고 서버에 여러 명령을 보내고 마지막으로 한 단계로 응답을 읽을 수 있습니다.
A request/response server can be implemented so that it is able to process new requests even if the client did not already read the old responses. This way it is possible to send multiple commands to the server without waiting for the replies at all, and finally read the replies in a single step.

동기 API를 사용하면 일반적으로 응답이 완료될 때까지 프로그램 흐름이 차단됩니다. 기본 연결은 요청을 보내고 응답을 받는 중입니다. 이 경우 차단은 전역 관점이 아닌 현재 스레드 관점에서만 적용됩니다.
Using the synchronous API, in general, the program flow is blocked until the response is accomplished. The underlying connection is busy with sending the request and receiving its response. Blocking, in this case, applies only from a current Thread perspective, not from a global perspective.

동기식 API를 사용하면 전역 수준에서 차단되지 않는 이유를 이해하려면 이것이 무엇을 의미하는지 이해해야 합니다. Lettuce는 비차단 비동기 클라이언트입니다. 명령 응답 대기(동기화)를 생성하기 위해 스레드별로 차단 동작을 달성하는 동기 API를 제공합니다. 차단은 다른 스레드 자체에는 영향을 미치지 않습니다. 레터스는 파이프라인 방식으로 작동하도록 설계되었습니다. 여러 스레드가 하나의 연결을 공유할 수 있습니다. 하나의 스레드가 하나의 명령을 처리하는 동안 다른 스레드는 새 명령을 보낼 수 있습니다. 첫 번째 요청이 반환되자마자 첫 번째 스레드의 프로그램 흐름은 계속되고, 두 번째 요청은 Redis에 의해 처리된 후 특정 시점에 다시 돌아옵니다.
To understand why using a synchronous API does not block on a global level we need to understand what this means. Lettuce is a non-blocking and asynchronous client. It provides a synchronous API to achieve a blocking behavior on a per-Thread basis to create await (synchronize) a command response. Blocking does not affect other Threads per se. Lettuce is designed to operate in a pipelining way. Multiple threads can share one connection. While one Thread may process one command, the other Thread can send a new command. As soon as the first request returns, the first Thread’s program flow continues, while the second request is processed by Redis and comes back at a certain point in time.

Lettuce는 쓰기에서 읽기를 읽고 스레드로부터 안전한 연결을 제공하기 위해 netty 디커플링 위에 구축되었습니다. 결과적으로 읽기 및 쓰기는 서로 다른 스레드에서 처리될 수 있으며 명령은 서로 독립적이지만 순서대로 작성되고 읽혀집니다. Wiki에서 메시지 순서에 대한 자세한 내용을 찾아 단일 및 다중 스레드 배열의 명령 순서 규칙에 대해 알아볼 수 있습니다. 전송 및 명령 실행 계층은 명령이 작성되고 처리될 때까지 그리고 해당 응답을 읽는 동안 처리를 차단하지 않습니다. Lettuce는 호출되는 순간 명령을 보냅니다.
Lettuce is built on top of netty decouple reading from writing and to provide thread-safe connections. The result is, that reading and writing can be handled by different threads and commands are written and read independent of each other but in sequence. You can find more details about message ordering in the Wiki to learn about command ordering rules in single- and multi-threaded arrangements. The transport and command execution layer does not block the processing until a command is written, processed and while its response is read. Lettuce sends commands at the moment they are invoked.

좋은 예는 비동기 API입니다. 비동기 API에 대한 모든 호출은 명령이 netty 파이프라인에 작성된 후 Future(응답 핸들)를 반환합니다. 파이프라인에 쓴다고 해서 명령이 기본 전송에 기록된다는 의미는 아닙니다. 응답을 기다리지 않고 여러 명령을 작성할 수 있습니다. API 호출(동기화, 비동기 및 4.0부터 반응형 API까지)은 여러 스레드에서 수행할 수 있습니다.
A good example is the async API. Every invocation on the async API returns a Future (response handle) after the command is written to the netty pipeline. A write to the pipeline does not mean, the command is written to the underlying transport. Multiple commands can be written without awaiting the response. Invocations to the API (sync, async and starting with 4.0 also reactive API) can be performed by multiple threads.

스레드 간 연결을 공유하는 것은 가능하지만 다음 사항에 유의하세요.
Sharing a connection between threads is possible but keep in mind:

처리에 필요한 명령이 길어질수록 다른 호출자는 결과를 기다리는 시간도 길어집니다.
The longer commands need for processing, the longer other invoker wait for their results

공유 연결에서는 트랜잭션 명령(MULTI)을 사용하면 안 됩니다. Redis 차단 명령(예: BLPOP)을 사용하는 경우 공유 연결의 모든 호출은 다른 스레드의 성능에 영향을 미치는 차단 명령이 반환될 때까지 차단됩니다. 차단 명령은 여러 연결을 사용하는 이유가 될 수 있습니다.
You should not use transactional commands (MULTI) on shared connection. If you use Redis-blocking commands (e. g. BLPOP) all invocations of the shared connection will be blocked until the blocking command returns which impacts the performance of other threads. Blocking commands can be a reason to use multiple connections.

Command flushing

명령 플러시는 고급 주제이며 대부분의 경우(예: 사용 사례가 단일 스레드 대량 가져오기 애플리케이션이 아닌 한) Lettuce가 기본적으로 파이프라이닝을 사용하므로 필요하지 않습니다. Lettuce의 일반 작동 모드는 모든 명령을 플러시하는 것입니다. 즉, 모든 명령은 실행된 후 전송에 기록됩니다(보내집니다). 일반 사용자라면 누구나 이 동작을 원합니다. 버전 3.3부터 명령 플러시를 제어할 수 있습니다.
Command flushing is an advanced topic and in most cases (i.e. unless your use-case is a single-threaded mass import application) you won’t need it as Lettuce uses pipelining by default. The normal operation mode of Lettuce is to flush every command which means, that every command is written to the transport after it was issued. Any regular user desires this behavior. You can control command flushing since Version 3.3.

왜 이런 일을 하고 싶나요? 플러시는 비용이 많이 드는 시스템 호출이며 성능에 영향을 미칩니다. 자동 플러시를 비활성화하는 일괄 처리는 특정 조건에서 사용할 수 있으며 다음과 같은 경우에 권장됩니다.
Why would you want to do this? A flush is an expensive system call and impacts performance. Batching, disabling auto-flushing, can be used under certain conditions and is recommended if:

- Redis에 대해 여러 호출을 수행하고 호출 결과에 즉시 의존하지 않는 경우
  You perform multiple calls to Redis and you’re not depending immediately on the result of the call
- 대량 임포트 중입니다
  You’re bulk-importing

플러시 동작 제어는 비동기 API에서만 사용할 수 있습니다. 동기화 API는 차단 호출을 에뮬레이트하며 명령을 호출하자마자 차단 호출이 끝날 때까지 더 이상 연결과 상호 작용할 수 없습니다.
Controlling the flush behavior is only available on the async API. The sync API emulates blocking calls and as soon as you invoke a command, you’re no longer able to interact with the connection until the blocking call ends.

AutoFlushCommands 상태는 연결별로 설정되므로 공유 연결을 사용하는 모든 스레드에 영향을 줍니다. 이 효과를 생략하려면 전용 연결을 사용하십시오. AutoFlushCommands 상태는 Lettuce 연결 풀링을 통해 풀링된 연결에 설정할 수 없습니다.
The AutoFlushCommands state is set per connection and therefore affects all threads using the shared connection. If you want to omit this effect, use dedicated connections. The AutoFlushCommands state cannot be set on pooled connections by the Lettuce connection pooling.

Example 57. Asynchronous Pipelining

Performance impact 성능에 미치는 영향

기본 쓰기 후 플러시 모드에서 호출된 명령은 약 100Kops/초(비동기/다중 스레드 실행) 순서로 수행됩니다. 여러 명령을 일괄적으로 그룹화하면(크기는 환경에 따라 다르지만 성능 테스트 중에는 50~1000개의 일괄 처리가 적합함) 처리량을 최대 5배까지 늘릴 수 있습니다.
Commands invoked in the default flush-after-write mode perform in an order of about 100Kops/sec (async/multithreaded execution). Grouping multiple commands in a batch (size depends on your environment, but batches between 50 and 1000 work nice during performance tests) can increase the throughput up to a factor of 5x.


3. Java Pipelining Source

Java Lettuce를 사용한 파이프라인(Pipelining) 명령 사용법입니다.

Redis9_Pipeline.java


<< Async Pipelining Pub/Sub >>

Email 답글이 올라오면 이메일로 알려드리겠습니다.