はじめに
こんにちは。マイクロアドでソフトウェアエンジニアをしている飛田と申します。
最近、業務でストリーム処理アプリケーションのスループットを改善する必要がありました。特に、一つのリクエストに対して複数回Redisへの参照や書き込みを行う必要があるアプリケーションだったため、Redis周りの処理時間がボトルネックとなっていました。そこで、Redis Pipelineを導入したところ、パフォーマンスの向上を実感しました。
リリース後、Pipelineがどのように効率化を実現しているかをさらに理解したくなり、通信の仕組みを解析することにしました。
本記事では、Redis Pipelineの基本的な動作および実際の通信内容について解説していきます。
Redis Pipelineの基本的な動作
Redis Pipelineは、Redisサーバーとの通信を効率化するための機能です。 通常、Redisにコマンドを送信するときは、クライアントからサーバーにコマンドを送り、その結果を受け取るまで待機します。この通信の往復により、特に大量のコマンドを送信する場合には大きなオーバーヘッドが発生します。
従来の通信とパイプラインの違い
通常のRedis通信では、クライアントが1つのコマンドを送信し、その結果を受信する動作を繰り返します。 この方式では各コマンド送信ごとにネットワーク遅延が発生します。
例えば、100個のコマンドを送る場合、100回の送受信が行われ、それぞれに通信のラウンドトリップタイム(RTT)が加わります。
一方、Redis Pipelineは、複数のコマンドをまとめてサーバーに送信しその結果をまとめて受け取ることで、通信回数とネットワーク遅延を削減します。 クライアントはサーバーに連続してコマンドを送り、それらが全て処理された後にまとめて結果を受け取ります。これにより、送信と受信の回数が1回ずつで済むため、大量のコマンドを効率的に処理できます。
実験
通常のRedis通信とRedis Pipelineによる通信の違いを調査するため、それぞれのパケットをキャプチャして解析します。両者がどのように通信しているのか、具体的な違いを明らかにしていきます。
この実験では、SETコマンドを3回送った後、GETコマンドを3回送信します。
まずは準備として、以下のdocker-compose.ymlを使用してRedisサーバーを起動します。
version: '3.8' services: redis: image: "redis:latest" ports: - "6379:6379"
以降、実験結果について示しますが、 ローカルPCのDocker上でRedisサーバーを起動し、ローカルPCのPythonのコードからRedisと通信しています。
通常のRedis通信を解析
まず、通常のRedis通信のパケットを解析してみます。 以下は、実験に使用するRedisと通信するPythonスクリプトです。
import redis # redis-pyを使用: https://github.com/redis/redis-py client = redis.Redis(host='localhost', port=6379) print(client.set('key1', 'value1')) print(client.set('key2', 'value2')) print(client.set('key3', 'value3')) print(client.get('key1')) # b'value1' print(client.get('key2')) # b'value2' print(client.get('key3')) # b'value3'
以下の表はRedisへのSETコマンドの送信をパケットキャプチャした結果です。
各SETコマンドが個別に送信され、それぞれ応答を受け取るため、RESP(Redis Serialization Protocol)によるリクエスト/レスポンス間にACKパケットが存在しています。
No. | Time | Source | Destination | Protocol | Length | Info |
15 | 0.087359 | ::1 | ::1 | RESP | 111 | Request: SET key1 value1 |
16 | 0.087374 | ::1 | ::1 | TCP | 76 | 6379 > 52440 [ACK] Seq=11 Ack=146 Win=407616 Len=0 TSval=4233421079 TSecr=3046015615 |
17 | 0.087696 | ::1 | ::1 | RESP | 81 | Response: OK |
18 | 0.087706 | ::1 | ::1 | TCP | 76 | 52440 > 6379 [ACK] Seq=146 Ack=16 Win=407744 Len=0 TSval=3046015615 TSecr=4233421079 |
19 | 0.087819 | ::1 | ::1 | RESP | 111 | Request: SET key2 value2 |
20 | 0.087827 | ::1 | ::1 | TCP | 76 | 6379 > 52440 [ACK] Seq=16 Ack=181 Win=407616 Len=0 TSval=4233421079 TSecr=3046015615 |
21 | 0.088063 | ::1 | ::1 | RESP | 81 | Response: OK |
22 | 0.088076 | ::1 | ::1 | TCP | 76 | 52440 > 6379 [ACK] Seq=181 Ack=21 Win=407744 Len=0 TSval=3046015617 TSecr=4233421081 |
23 | 0.088279 | ::1 | ::1 | RESP | 111 | Request: SET key3 value3 |
24 | 0.088289 | ::1 | ::1 | TCP | 76 | 6379 > 52440 [ACK] Seq=21 Ack=216 Win=407552 Len=0 TSval=4233421081 TSecr=3046015617 |
25 | 0.088558 | ::1 | ::1 | RESP | 81 | Response: OK |
26 | 0.088569 | ::1 | ::1 | TCP | 76 | 52440 > 6379 [ACK] Seq=216 Ack=26 Win=407744 Len=0 TSval=3046015617 TSecr=4233421081 |
Redis Pipelineによる通信を解析(1)
次にRedis Pipelineの通信のパケットを解析してみます。 以下は、実験に使用するRedisと通信するPythonスクリプトです。
import redis # redis-pyを使用: https://github.com/redis/redis-py client = redis.Redis(host='localhost', port=6379) pipeline = client.pipeline() pipeline.set('key1', 'value1') pipeline.set('key2', 'value2') pipeline.set('key3', 'value3') results = pipeline.execute() for result in results: print(result) print(client.get('key1')) # b'value1' print(client.get('key2')) # b'value2' print(client.get('key3')) # b'value3'
以下の表はRedisへのSETコマンドの送信をパケットキャプチャした結果です。 MULTIコマンドを使って複数のSETコマンドを一度に送信し、まとめて実行されています。
複数のSETコマンドを一度のリクエストで送信し、それに対する応答も一度に返されるため、通常の単発リクエスト/応答の通信に比べて、ネットワーク上の往復回数が減少します。
No. | Time | Source | Destination | Protocol | Length | Info |
13 | 0.002088 | ::1 | ::1 | RESP | 210 | Request: MULTI SET key1 value1 SET key2 value2 SET key3 value3 EXEC |
14 | 0.002100 | ::1 | ::1 | TCP | 76 | 6379 > 54520 [ACK] Seq=11 Ack=245 Win=407552 Len=0 TSval=2625827618 TSecr=1789556526 |
15 | 0.002449 | ::1 | ::1 | RESP | 127 | Response: OK QUEUED QUEUED QUEUED Array(3) |
16 | 0.002460 | ::1 | ::1 | TCP | 76 | 54520 > 6379 [ACK] Seq=245 Ack=62 Win=407680 Len=0 TSval=1789556527 TSecr=2625827619 |
Redis Pipelineによる通信を解析(2)
「Redis Pipelineによる通信を解析(1)」では、 SETコマンドを3回送信し、3つのコマンドが1つのパケットとして送信されることが確認できました。
実際のユースケースではもっと大きなデータを投入することが想定されますので、 次に1000件のSETコマンドを送信する実験をしてみました。
実験に使用するPythonのスクリプト:
import redis client = redis.Redis(host='localhost', port=6379) pipeline = client.pipeline() for i in range(0, 1000): pipeline.set(f'key{i}', f'value{i}') results = pipeline.execute()
以下は、パケットキャプチャ結果です。
すべてのSETコマンドが、MULTI
-EXEC
で1つのトランザクションになっていることが確認できます。
また、「Redis Pipelineによる通信を解析(1)」で行った実験とは異なり、SETコマンド群が複数パケットに分かれていることも確認できます。
No. | Time | Source | Destination | Protocol | Length | Info |
15 | 2.052071 | ::1 | ::1 | RESP | 6111 | Request: MULTI SET key0 value0 SET key1 value1 ... SET key159 value159 |
16 | 2.052077 | ::1 | ::1 | RESP | 6082 | Request: SET key160 value160 SET key161 value161 ... SET key313 value313 |
17 | 2.052080 | ::1 | ::1 | TCP | 76 | 6379 > 55103 [ACK] Seq=11 Ack=6146 Win=401600 Len=0 TSval=2425214130 TSecr=1791217777 |
18 | 2.052081 | ::1 | ::1 | RESP | 6082 | Request: SET key314 value314 SET key315 value315 ... SET key467 value467 |
19 | 2.052084 | ::1 | ::1 | TCP | 76 | 6379 > 55103 [ACK] Seq=11 Ack=12152 Win=395648 Len=0 TSval=2425214130 TSecr=1791217777 |
20 | 2.052088 | ::1 | ::1 | TCP | 76 | 6379 > 55103 [ACK] Seq=11 Ack=18158 Win=389632 Len=0 TSval=2425214130 TSecr=1791217777 |
21 | 2.052089 | ::1 | ::1 | RESP | 6082 | Request: SET key468 value468 SET key469 value469 ... SET key621 value621 |
22 | 2.052091 | ::1 | ::1 | TCP | 76 | 6379 > 55103 [ACK] Seq=11 Ack=24164 Win=383616 Len=0 TSval=2425214130 TSecr=1791217777 |
23 | 2.052093 | ::1 | ::1 | RESP | 6082 | Request: SET key622 value622 SET key623 value623 ... SET key775 value775 |
24 | 2.052100 | ::1 | ::1 | RESP | 6082 | Request: SET key776 value776 SET key777 value777 ... SET key929 value929 |
25 | 2.052101 | ::1 | ::1 | TCP | 76 | 6379 > 55103 [ACK] Seq=11 Ack=30170 Win=377600 Len=0 TSval=2425214130 TSecr=1791217777 |
26 | 2.052103 | ::1 | ::1 | RESP | 2820 | Request: SET key930 value930 SET key931 value931 ... SET key999 value999 EXEC |
27 | 2.052104 | ::1 | ::1 | TCP | 76 | 6379 > 55103 [ACK] Seq=11 Ack=36176 Win=371584 Len=0 TSval=2425214130 TSecr=1791217777 |
28 | 2.052108 | ::1 | ::1 | TCP | 76 | 6379 > 55103 [ACK] Seq=11 Ack=38920 Win=398912 Len=0 TSval=2425214130 TSecr=1791217777 |
29 | 2.053092 | ::1 | ::1 | RESP | 4131 | Response: OK QUEUED QUEUED ... QUEUED |
30 | 2.053108 | ::1 | ::1 | TCP | 76 | 55103 > 6379 [ACK] Seq=38920 Ack=4066 Win=403712 Len=0 TSval=1791217778 TSecr=2425214131 |
31 | 2.053460 | ::1 | ::1 | RESP | 4801 | Response: QUEUED QUEUED ... QUEUED |
32 | 2.053473 | ::1 | ::1 | TCP | 76 | 55103 > 6379 [ACK] Seq=38920 Ack=8791 Win=398976 Len=0 TSval=1791217778 TSecr=2425214131 |
33 | 2.054312 | ::1 | ::1 | RESP | 5308 | Response: QUEUED QUEUED QUEUED QUEUED QUEUED QUEUED QUEUED QUEUED QUEUED QUEUED QUEUED QUEUED QUEUED QUEUED QUEUED QUEUED QUEUED QUEUED QUEUED QUEUED QUEUED QUEUED QUEUED QUEUED QUEUED Array(1000) |
※ 大量のSETコマンドが投げられているパケットやそのレスポンスのパケットにつきましては、Request: MULTI SET key0 value0 SET key1 value1 ... SET key159 value159
のように...
で略記しています
(参考) 通常のRedis通信とRedis Pipelineで実行時間を比較する
上記で行った複数の実験により、Redis Pipelineは送受信するパケット数が少ないということが判明しました。 そのため、ラウンドトリップ時間が節約できて、バルクインサートを高速に完了できることが期待されます。
ここでは、荒い実験として1万件のSETコマンドにかかる実行時間を、通常のRedis通信とRedis Pipelineによる通信で比較してみました。
以下は比較のためのPythonスクリプトです。
import redis import time NUM_ITER = 10000 client = redis.Redis(host='localhost', port=6379) # Redis PipelineのSETの実行時間を計測 start_time = time.time() pipeline = client.pipeline() for i in range(0, NUM_ITER): pipeline.set(f'key{i}', f'value{i}') results = pipeline.execute() end_time = time.time() execution_time = end_time - start_time print(f"Redis Pipeline 実行時間: {execution_time} 秒") # 通常のRedisのSETの実行時間を計測 start_time = time.time() for i in range(0, NUM_ITER): client.set(f'key{i}', f'value{i}') end_time = time.time() execution_time = end_time - start_time print(f"通常 Redis 実行時間: {execution_time} 秒")
実行結果:
Redis Pipeline 実行時間: 0.0724639892578125 秒 通常 Redis 実行時間: 3.4237308502197266 秒
荒い実験の結果、Redis Pipelineによる通信にかかった時間は、通常のRedis通信と比較して、2%程度になりました。 このことから、バルクインサートについてはRedis Pipelineの方が速度面で優れているということが分かります。
おわりに
今回の実験を通じて、Redis Pipelineを用いることで、複数のコマンドをまとめて処理し、通信の往復回数を減らすことができるため、パフォーマンスが大幅に向上することが確認できました。
特に、大量のデータを書き込む場合においては、通常の個別リクエストよりもPipelineを使用することで、ネットワーク遅延やオーバーヘッドを最小限に抑えられそうですね。