Skip to content

Commit 7422c36

Browse files
authored
perf: lessen memory consumption for emulated mget command (#357)
1 parent d6fc855 commit 7422c36

File tree

5 files changed

+97
-5
lines changed

5 files changed

+97
-5
lines changed

.github/workflows/test.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,8 @@ jobs:
246246
- single
247247
- excessive_pipelining
248248
- pipelining_in_moderation
249+
- original_mget
250+
- emulated_mget
249251
env:
250252
REDIS_VERSION: '7.2'
251253
DOCKER_COMPOSE_FILE: 'compose.yaml'

lib/redis_client/cluster/router.rb

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -334,16 +334,31 @@ def send_watch_command(command)
334334
end
335335
end
336336

337-
def send_multiple_keys_command(cmd, method, command, args, &block) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
338-
if command.size < 3 || !::RedisClient::Cluster::KeySlotConverter.extract_hash_tag(command[1]).empty? # rubocop:disable Style/IfUnlessModifier
337+
def send_multiple_keys_command(cmd, method, command, args, &block) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
338+
key_step = @command.determine_key_step(cmd)
339+
if command.size <= key_step + 1 || !::RedisClient::Cluster::KeySlotConverter.extract_hash_tag(command[1]).empty? # rubocop:disable Style/IfUnlessModifier
339340
return try_send(assign_node(command), method, command, args, &block)
340341
end
341342

342-
single_key_cmd = MULTIPLE_KEYS_COMMAND_TO_PIPELINE[cmd]
343-
key_step = @command.determine_key_step(cmd)
344343
seed = @config.use_replica? && @config.replica_affinity == :random ? nil : Random.new_seed
345344
pipeline = ::RedisClient::Cluster::Pipeline.new(self, @command_builder, @concurrent_worker, exception: true, seed: seed)
346-
command[1..].each_slice(key_step) { |*v| pipeline.call(single_key_cmd, *v) }
345+
346+
# This implementation is prioritized to lessen memory consumption rather than readability.
347+
single_key_cmd = MULTIPLE_KEYS_COMMAND_TO_PIPELINE[cmd]
348+
single_command = Array.new(key_step + 1)
349+
single_command[0] = single_key_cmd
350+
if key_step == 1
351+
command[1..].each do |key|
352+
single_command[1] = key
353+
pipeline.call_v(single_command)
354+
end
355+
else
356+
command[1..].each_slice(key_step) do |v|
357+
key_step.times { |i| single_command[i + 1] = v[i] }
358+
pipeline.call_v(single_command)
359+
end
360+
end
361+
347362
replies = pipeline.execute
348363
result = case cmd
349364
when 'mset' then replies.first

test/ips_mget.rb

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# frozen_string_literal: true
2+
3+
require 'benchmark/ips'
4+
require 'redis_cluster_client'
5+
require 'testing_constants'
6+
7+
module IpsMget
8+
module_function
9+
10+
ATTEMPTS = 40
11+
12+
def run
13+
cli = make_client
14+
prepare(cli)
15+
print_letter('MGET')
16+
bench('MGET', cli)
17+
end
18+
19+
def make_client
20+
::RedisClient.cluster(
21+
nodes: TEST_NODE_URIS,
22+
replica: true,
23+
replica_affinity: :random,
24+
fixed_hostname: TEST_FIXED_HOSTNAME,
25+
concurrency: { model: :on_demand },
26+
**TEST_GENERIC_OPTIONS
27+
).new_client
28+
end
29+
30+
def print_letter(title)
31+
print "################################################################################\n"
32+
print "# #{title}\n"
33+
print "################################################################################\n"
34+
print "\n"
35+
end
36+
37+
def prepare(client)
38+
ATTEMPTS.times do |i|
39+
client.call('SET', "{key}#{i}", "val#{i}")
40+
client.call('SET', "key#{i}", "val#{i}")
41+
end
42+
end
43+
44+
def bench(cmd, client)
45+
original = [cmd] + Array.new(ATTEMPTS) { |i| "{key}#{i}" }
46+
emulated = [cmd] + Array.new(ATTEMPTS) { |i| "key#{i}" }
47+
48+
Benchmark.ips do |x|
49+
x.time = 5
50+
x.warmup = 1
51+
x.report("#{cmd}: original") { client.call_v(original) }
52+
x.report("#{cmd}: emulated") { client.call_v(emulated) }
53+
x.compare!
54+
end
55+
end
56+
end
57+
58+
IpsMget.run

test/prof_mem.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ module ProfMem
1010
ATTEMPT_COUNT = 1000
1111
MAX_PIPELINE_SIZE = 100
1212
SLICED_NUMBERS = (1..ATTEMPT_COUNT).each_slice(MAX_PIPELINE_SIZE).freeze
13+
ORIGINAL_MGET = (%w[MGET] + Array.new(40) { |i| "{key}#{i}" }).freeze
14+
EMULATED_MGET = (%w[MGET] + Array.new(40) { |i| "key#{i}" }).freeze
1315
CLI_TYPES = %w[primary_only scale_read_random scale_read_latency pooled].freeze
1416
MODES = {
1517
single: lambda do |client_builder_method|
@@ -38,6 +40,14 @@ module ProfMem
3840
numbers.each { |i| pi.call('GET', i) }
3941
end
4042
end
43+
end,
44+
original_mget: lambda do |client_builder_method|
45+
cli = send(client_builder_method)
46+
ATTEMPT_COUNT.times { cli.call_v(ORIGINAL_MGET) }
47+
end,
48+
emulated_mget: lambda do |client_builder_method|
49+
cli = send(client_builder_method)
50+
ATTEMPT_COUNT.times { cli.call_v(EMULATED_MGET) }
4151
end
4252
}.freeze
4353

test/prof_stack.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
module ProfStack
1010
SIZE = 40
1111
ATTEMPTS = 1000
12+
ORIGINAL_MGET = (%w[MGET] + Array.new(SIZE) { |i| "{key}#{i}" }).freeze
13+
EMULATED_MGET = (%w[MGET] + Array.new(SIZE) { |i| "key#{i}" }).freeze
1214

1315
module_function
1416

@@ -36,6 +38,7 @@ def prepare(client)
3638
SIZE.times do |j|
3739
n = SIZE * i + j
3840
pi.call('SET', "key#{n}", "val#{n}")
41+
pi.call('SET', "{key}#{n}", "val#{n}")
3942
end
4043
end
4144
end
@@ -58,6 +61,10 @@ def execute(client, mode)
5861
end
5962
end
6063
end
64+
when :original_mget
65+
ATTEMPTS.times { client.call_v(ORIGINAL_MGET) }
66+
when :emulated_mget
67+
ATTEMPTS.times { client.call_v(EMULATED_MGET) }
6168
else raise ArgumentError, mode
6269
end
6370
end

0 commit comments

Comments
 (0)