Add a maximum concurrency option to rps runner
The rps benchmark runner has no throttle for how many total concurrent test threads it will run. For some tests, like nova boot tests, cloud capacity for instances could be reached before a suitable amount of tests. For that reason this introduces a throttle of maximum concurrent scenario threads that are allowed to be live. Much like how tasks are divvied up across workers, concurrency slots are divvied up across workers as well. Each worker gets a max and will wait for a thread to finish before adding a new one once the max has been reached. If no max concurrency is provided, the concurrency is pinned to the times to execute. Co-Authored-By: Jesse Keating <jlk@bluebox.net> Co-Authored-By: Marian Krcmarik <mkrcmari@redhat.com> Implements: blueprint rps-benchmark-with-max-concurrency Change-Id: I488322eb9b50afe58801d28be8ad37f0cc5aeb07
This commit is contained in:
parent
edc7e7d6ec
commit
51a605fde5
@ -252,6 +252,18 @@
|
|||||||
failure_rate:
|
failure_rate:
|
||||||
max: 0
|
max: 0
|
||||||
|
|
||||||
|
-
|
||||||
|
args:
|
||||||
|
sleep: 0.001
|
||||||
|
runner:
|
||||||
|
type: "rps"
|
||||||
|
times: 200
|
||||||
|
rps: 20
|
||||||
|
max_concurrency: 10
|
||||||
|
sla:
|
||||||
|
failure_rate:
|
||||||
|
max: 0
|
||||||
|
|
||||||
-
|
-
|
||||||
args:
|
args:
|
||||||
sleep: 0.1
|
sleep: 0.1
|
||||||
|
@ -24,22 +24,24 @@ from rally.common import log as logging
|
|||||||
from rally.common import utils
|
from rally.common import utils
|
||||||
from rally import consts
|
from rally import consts
|
||||||
|
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def _worker_process(queue, iteration_gen, timeout, rps, times,
|
def _worker_process(queue, iteration_gen, timeout, rps, times,
|
||||||
context, cls, method_name, args, aborted):
|
max_concurrent, context, cls, method_name,
|
||||||
|
args, aborted):
|
||||||
"""Start scenario within threads.
|
"""Start scenario within threads.
|
||||||
|
|
||||||
Spawn N threads per second. Each thread runs the scenario once, and appends
|
Spawn N threads per second. Each thread runs the scenario once, and appends
|
||||||
the result to the queue.
|
result to queue. A maximum of max_concurrent threads will be ran
|
||||||
|
concurrently.
|
||||||
|
|
||||||
:param queue: queue object to append results
|
:param queue: queue object to append results
|
||||||
:param iteration_gen: next iteration number generator
|
:param iteration_gen: next iteration number generator
|
||||||
:param timeout: operation's timeout
|
:param timeout: operation's timeout
|
||||||
:param rps: number of scenario iterations to be run per one second
|
:param rps: number of scenario iterations to be run per one second
|
||||||
:param times: total number of scenario iterations to be run
|
:param times: total number of scenario iterations to be run
|
||||||
|
:param max_concurrent: maximum worker concurrency
|
||||||
:param context: scenario context object
|
:param context: scenario context object
|
||||||
:param cls: scenario class
|
:param cls: scenario class
|
||||||
:param method_name: scenario method name
|
:param method_name: scenario method name
|
||||||
@ -80,8 +82,8 @@ def _worker_process(queue, iteration_gen, timeout, rps, times,
|
|||||||
(i, real_rps, rps))
|
(i, real_rps, rps))
|
||||||
|
|
||||||
# try to join latest thread(s) until it finished, or until time to
|
# try to join latest thread(s) until it finished, or until time to
|
||||||
# start new thread
|
# start new thread (if we have concurrent slots available)
|
||||||
while i / (time.time() - start) > rps:
|
while i / (time.time() - start) > rps or len(pool) >= max_concurrent:
|
||||||
if pool:
|
if pool:
|
||||||
pool[0].join(sleep)
|
pool[0].join(sleep)
|
||||||
if not pool[0].isAlive():
|
if not pool[0].isAlive():
|
||||||
@ -126,6 +128,10 @@ class RPSScenarioRunner(base.ScenarioRunner):
|
|||||||
"timeout": {
|
"timeout": {
|
||||||
"type": "number",
|
"type": "number",
|
||||||
},
|
},
|
||||||
|
"max_concurrency": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1
|
||||||
|
},
|
||||||
},
|
},
|
||||||
"additionalProperties": False
|
"additionalProperties": False
|
||||||
}
|
}
|
||||||
@ -151,27 +157,50 @@ class RPSScenarioRunner(base.ScenarioRunner):
|
|||||||
timeout = self.config.get("timeout", 0) # 0 means no timeout
|
timeout = self.config.get("timeout", 0) # 0 means no timeout
|
||||||
iteration_gen = utils.RAMInt()
|
iteration_gen = utils.RAMInt()
|
||||||
cpu_count = multiprocessing.cpu_count()
|
cpu_count = multiprocessing.cpu_count()
|
||||||
processes_to_start = min(cpu_count, times)
|
processes_to_start = min(cpu_count, times,
|
||||||
|
self.config.get("max_concurrency", times))
|
||||||
rps_per_worker = float(self.config["rps"]) / processes_to_start
|
rps_per_worker = float(self.config["rps"]) / processes_to_start
|
||||||
times_per_worker, times_overhead = divmod(times, processes_to_start)
|
times_per_worker, times_overhead = divmod(times, processes_to_start)
|
||||||
|
|
||||||
|
# Determine concurrency per worker
|
||||||
|
concurrency_per_worker, concurrency_overhead = divmod(
|
||||||
|
self.config.get("max_concurrency", times),
|
||||||
|
processes_to_start)
|
||||||
|
|
||||||
self._log_debug_info(times=times, timeout=timeout, cpu_count=cpu_count,
|
self._log_debug_info(times=times, timeout=timeout, cpu_count=cpu_count,
|
||||||
processes_to_start=processes_to_start,
|
processes_to_start=processes_to_start,
|
||||||
rps_per_worker=rps_per_worker,
|
rps_per_worker=rps_per_worker,
|
||||||
times_per_worker=times_per_worker,
|
times_per_worker=times_per_worker,
|
||||||
times_overhead=times_overhead)
|
times_overhead=times_overhead,
|
||||||
|
concurrency_per_worker=concurrency_per_worker,
|
||||||
|
concurrency_overhead=concurrency_overhead)
|
||||||
|
|
||||||
result_queue = multiprocessing.Queue()
|
result_queue = multiprocessing.Queue()
|
||||||
|
|
||||||
def worker_args_gen(times_overhead):
|
def worker_args_gen(times_overhead, concurrency_overhead):
|
||||||
|
"""Generate arguments for process worker.
|
||||||
|
|
||||||
|
Remainder of threads per process division is distributed to
|
||||||
|
process workers equally - one thread per each process worker
|
||||||
|
until the remainder equals zero. The same logic is applied
|
||||||
|
to concurrency overhead.
|
||||||
|
:param times_overhead: remaining number of threads to be
|
||||||
|
distributed to workers
|
||||||
|
:param concurrency_overhead: remaining number of maximum
|
||||||
|
concurrent threads to be distributed
|
||||||
|
to workers
|
||||||
|
"""
|
||||||
while True:
|
while True:
|
||||||
yield (result_queue, iteration_gen, timeout, rps_per_worker,
|
yield (result_queue, iteration_gen, timeout, rps_per_worker,
|
||||||
times_per_worker + (times_overhead and 1),
|
times_per_worker + (times_overhead and 1),
|
||||||
|
concurrency_per_worker + (concurrency_overhead and 1),
|
||||||
context, cls, method_name, args, self.aborted)
|
context, cls, method_name, args, self.aborted)
|
||||||
if times_overhead:
|
if times_overhead:
|
||||||
times_overhead -= 1
|
times_overhead -= 1
|
||||||
|
if concurrency_overhead:
|
||||||
|
concurrency_overhead -= 1
|
||||||
|
|
||||||
process_pool = self._create_process_pool(
|
process_pool = self._create_process_pool(
|
||||||
processes_to_start, _worker_process,
|
processes_to_start, _worker_process,
|
||||||
worker_args_gen(times_overhead))
|
worker_args_gen(times_overhead, concurrency_overhead))
|
||||||
self._join_processes(process_pool, result_queue)
|
self._join_processes(process_pool, result_queue)
|
||||||
|
@ -37,6 +37,7 @@ class RPSScenarioRunnerTestCase(test.TestCase):
|
|||||||
"type": consts.RunnerType.RPS,
|
"type": consts.RunnerType.RPS,
|
||||||
"times": 1,
|
"times": 1,
|
||||||
"rps": 100,
|
"rps": 100,
|
||||||
|
"max_concurrency": 50,
|
||||||
"timeout": 1
|
"timeout": 1
|
||||||
}
|
}
|
||||||
rps.RPSScenarioRunner.validate(config)
|
rps.RPSScenarioRunner.validate(config)
|
||||||
@ -83,6 +84,7 @@ class RPSScenarioRunnerTestCase(test.TestCase):
|
|||||||
is_set=mock.MagicMock(return_value=False))
|
is_set=mock.MagicMock(return_value=False))
|
||||||
|
|
||||||
times = 4
|
times = 4
|
||||||
|
max_concurrent = 3
|
||||||
|
|
||||||
fake_ram_int = iter(range(10))
|
fake_ram_int = iter(range(10))
|
||||||
|
|
||||||
@ -90,7 +92,8 @@ class RPSScenarioRunnerTestCase(test.TestCase):
|
|||||||
"id": "uuid1"}]}
|
"id": "uuid1"}]}
|
||||||
|
|
||||||
rps._worker_process(mock_queue, fake_ram_int, 1, 10, times,
|
rps._worker_process(mock_queue, fake_ram_int, 1, 10, times,
|
||||||
context, "Dummy", "dummy", (), mock_event)
|
max_concurrent, context, "Dummy", "dummy",
|
||||||
|
(), mock_event)
|
||||||
|
|
||||||
self.assertEqual(times, mock_log.debug.call_count)
|
self.assertEqual(times, mock_log.debug.call_count)
|
||||||
self.assertEqual(times, mock_thread.call_count)
|
self.assertEqual(times, mock_thread.call_count)
|
||||||
@ -127,7 +130,7 @@ class RPSScenarioRunnerTestCase(test.TestCase):
|
|||||||
context = fakes.FakeUserContext({}).context
|
context = fakes.FakeUserContext({}).context
|
||||||
context["task"] = {"uuid": "fake_uuid"}
|
context["task"] = {"uuid": "fake_uuid"}
|
||||||
|
|
||||||
config = {"times": 20, "rps": 20, "timeout": 5}
|
config = {"times": 20, "rps": 20, "timeout": 5, "max_concurrency": 15}
|
||||||
runner = rps.RPSScenarioRunner(self.task, config)
|
runner = rps.RPSScenarioRunner(self.task, config)
|
||||||
|
|
||||||
runner._run_scenario(fakes.FakeScenario, "do_it", context, {})
|
runner._run_scenario(fakes.FakeScenario, "do_it", context, {})
|
||||||
|
Loading…
Reference in New Issue
Block a user