qemu/tests/migration/guestperf/shell.py

256 lines
10 KiB
Python
Raw Normal View History

tests: introduce a framework for testing migration performance This introduces a moderately general purpose framework for testing performance of migration. The initial guest workload is provided by the included 'stress' program, which is configured to spawn one thread per guest CPU and run a maximally memory intensive workload. It will loop over GB of memory, xor'ing each byte with data from a 4k array of random bytes. This ensures heavy read and write load across all of guest memory to stress the migration performance. While running the 'stress' program will record how long it takes to xor each GB of memory and print this data for later reporting. The test engine will spawn a pair of QEMU processes, either on the same host, or with the target on a remote host via ssh, using the host kernel and a custom initrd built with 'stress' as the /init binary. Kernel command line args are set to ensure a fast kernel boot time (< 1 second) between launching QEMU and the stress program starting execution. None the less, the test engine will initially wait N seconds for the guest workload to stablize, before starting the migration operation. When migration is running, the engine will use pause, post-copy, autoconverge, xbzrle compression and multithread compression features, as well as downtime & bandwidth tuning to encourage completion. If migration completes, the test engine will wait N seconds again for the guest workooad to stablize on the target host. If migration does not complete after a preset number of iterations, it will be aborted. While the QEMU process is running on the source host, the test engine will sample the host CPU usage of QEMU as a whole, and each vCPU thread. While migration is running, it will record all the stats reported by 'query-migration'. Finally, it will capture the output of the stress program running in the guest. All the data produced from a single test execution is recorded in a structured JSON file. A separate program is then able to create interactive charts using the "plotly" python + javascript libraries, showing the characteristics of the migration. The data output provides visualization of the effect on guest vCPU workloads from the migration process, the corresponding vCPU utilization on the host, and the overall CPU hit from QEMU on the host. This is correlated from statistics from the migration process, such as downtime, vCPU throttling and iteration number. While the tests can be run individually with arbitrary parameters, there is also a facility for producing batch reports for a number of pre-defined scenarios / comparisons, in order to be able to get standardized results across different hardware configurations (eg TCP vs RDMA, or comparing different VCPU counts / memory sizes, etc). To use this, first you must build the initrd image $ make tests/migration/initrd-stress.img To run a a one-shot test with all default parameters $ ./tests/migration/guestperf.py > result.json This has many command line args for varying its behaviour. For example, to increase the RAM size and CPU count and bind it to specific host NUMA nodes $ ./tests/migration/guestperf.py \ --mem 4 --cpus 2 \ --src-mem-bind 0 --src-cpu-bind 0,1 \ --dst-mem-bind 1 --dst-cpu-bind 2,3 \ > result.json Using mem + cpu binding is strongly recommended on NUMA machines, otherwise the guest performance results will vary wildly between runs of the test due to lucky/unlucky NUMA placement, making sensible data analysis impossible. To make it run across separate hosts: $ ./tests/migration/guestperf.py \ --dst-host somehostname > result.json To request that post-copy is enabled, with switchover after 5 iterations $ ./tests/migration/guestperf.py \ --post-copy --post-copy-iters 5 > result.json Once a result.json file is created, a graph of the data can be generated, showing guest workload performance per thread and the migration iteration points: $ ./tests/migration/guestperf-plot.py --output result.html \ --migration-iters --split-guest-cpu result.json To further include host vCPU utilization and overall QEMU utilization $ ./tests/migration/guestperf-plot.py --output result.html \ --migration-iters --split-guest-cpu \ --qemu-cpu --vcpu-cpu result.json NB, the 'guestperf-plot.py' command requires that you have the plotly python library installed. eg you must do $ pip install --user plotly Viewing the result.html file requires that you have the plotly.min.js file in the same directory as the HTML output. This js file is installed as part of the plotly python library, so can be found in $HOME/.local/lib/python2.7/site-packages/plotly/offline/plotly.min.js The guestperf-plot.py program can accept multiple json files to plot, enabling results from different configurations to be compared. Finally, to run the entire standardized set of comparisons $ ./tests/migration/guestperf-batch.py \ --dst-host somehost \ --mem 4 --cpus 2 \ --src-mem-bind 0 --src-cpu-bind 0,1 \ --dst-mem-bind 1 --dst-cpu-bind 2,3 --output tcp-somehost-4gb-2cpu will store JSON files from all scenarios in the directory named tcp-somehost-4gb-2cpu Signed-off-by: Daniel P. Berrange <berrange@redhat.com> Message-Id: <1469020993-29426-7-git-send-email-berrange@redhat.com> Signed-off-by: Amit Shah <amit.shah@redhat.com>
2016-07-20 16:23:13 +03:00
#
# Migration test command line shell integration
#
# Copyright (c) 2016 Red Hat, Inc.
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library; if not, see <http://www.gnu.org/licenses/>.
#
import argparse
import fnmatch
import os
import os.path
import platform
import sys
from guestperf.hardware import Hardware
from guestperf.engine import Engine
from guestperf.scenario import Scenario
from guestperf.comparison import COMPARISONS
from guestperf.plot import Plot
from guestperf.report import Report
class BaseShell(object):
def __init__(self):
parser = argparse.ArgumentParser(description="Migration Test Tool")
# Test args
parser.add_argument("--debug", dest="debug", default=False, action="store_true")
parser.add_argument("--verbose", dest="verbose", default=False, action="store_true")
parser.add_argument("--sleep", dest="sleep", default=15, type=int)
parser.add_argument("--binary", dest="binary", default="/usr/bin/qemu-system-x86_64")
parser.add_argument("--dst-host", dest="dst_host", default="localhost")
parser.add_argument("--kernel", dest="kernel", default="/boot/vmlinuz-%s" % platform.release())
parser.add_argument("--initrd", dest="initrd", default="tests/migration/initrd-stress.img")
parser.add_argument("--transport", dest="transport", default="unix")
# Hardware args
parser.add_argument("--cpus", dest="cpus", default=1, type=int)
parser.add_argument("--mem", dest="mem", default=1, type=int)
parser.add_argument("--src-cpu-bind", dest="src_cpu_bind", default="")
parser.add_argument("--src-mem-bind", dest="src_mem_bind", default="")
parser.add_argument("--dst-cpu-bind", dest="dst_cpu_bind", default="")
parser.add_argument("--dst-mem-bind", dest="dst_mem_bind", default="")
parser.add_argument("--prealloc-pages", dest="prealloc_pages", default=False)
parser.add_argument("--huge-pages", dest="huge_pages", default=False)
parser.add_argument("--locked-pages", dest="locked_pages", default=False)
self._parser = parser
def get_engine(self, args):
return Engine(binary=args.binary,
dst_host=args.dst_host,
kernel=args.kernel,
initrd=args.initrd,
transport=args.transport,
sleep=args.sleep,
debug=args.debug,
verbose=args.verbose)
def get_hardware(self, args):
def split_map(value):
if value == "":
return []
return value.split(",")
return Hardware(cpus=args.cpus,
mem=args.mem,
src_cpu_bind=split_map(args.src_cpu_bind),
src_mem_bind=split_map(args.src_mem_bind),
dst_cpu_bind=split_map(args.dst_cpu_bind),
dst_mem_bind=split_map(args.dst_mem_bind),
locked_pages=args.locked_pages,
huge_pages=args.huge_pages,
prealloc_pages=args.prealloc_pages)
class Shell(BaseShell):
def __init__(self):
super(Shell, self).__init__()
parser = self._parser
parser.add_argument("--output", dest="output", default=None)
# Scenario args
parser.add_argument("--max-iters", dest="max_iters", default=30, type=int)
parser.add_argument("--max-time", dest="max_time", default=300, type=int)
parser.add_argument("--bandwidth", dest="bandwidth", default=125000, type=int)
parser.add_argument("--downtime", dest="downtime", default=500, type=int)
parser.add_argument("--pause", dest="pause", default=False, action="store_true")
parser.add_argument("--pause-iters", dest="pause_iters", default=5, type=int)
parser.add_argument("--post-copy", dest="post_copy", default=False, action="store_true")
parser.add_argument("--post-copy-iters", dest="post_copy_iters", default=5, type=int)
parser.add_argument("--auto-converge", dest="auto_converge", default=False, action="store_true")
parser.add_argument("--auto-converge-step", dest="auto_converge_step", default=10, type=int)
parser.add_argument("--compression-mt", dest="compression_mt", default=False, action="store_true")
parser.add_argument("--compression-mt-threads", dest="compression_mt_threads", default=1, type=int)
parser.add_argument("--compression-xbzrle", dest="compression_xbzrle", default=False, action="store_true")
parser.add_argument("--compression-xbzrle-cache", dest="compression_xbzrle_cache", default=10, type=int)
def get_scenario(self, args):
return Scenario(name="perfreport",
downtime=args.downtime,
bandwidth=args.bandwidth,
max_iters=args.max_iters,
max_time=args.max_time,
pause=args.pause,
pause_iters=args.pause_iters,
post_copy=args.post_copy,
post_copy_iters=args.post_copy_iters,
auto_converge=args.auto_converge,
auto_converge_step=args.auto_converge_step,
compression_mt=args.compression_mt,
compression_mt_threads=args.compression_mt_threads,
compression_xbzrle=args.compression_xbzrle,
compression_xbzrle_cache=args.compression_xbzrle_cache)
def run(self, argv):
args = self._parser.parse_args(argv)
engine = self.get_engine(args)
hardware = self.get_hardware(args)
scenario = self.get_scenario(args)
try:
report = engine.run(hardware, scenario)
if args.output is None:
print report.to_json()
else:
with open(args.output, "w") as fh:
print >>fh, report.to_json()
return 0
except Exception as e:
print >>sys.stderr, "Error: %s" % str(e)
if args.debug:
raise
return 1
class BatchShell(BaseShell):
def __init__(self):
super(BatchShell, self).__init__()
parser = self._parser
parser.add_argument("--filter", dest="filter", default="*")
parser.add_argument("--output", dest="output", default=os.getcwd())
def run(self, argv):
args = self._parser.parse_args(argv)
engine = self.get_engine(args)
hardware = self.get_hardware(args)
try:
for comparison in COMPARISONS:
compdir = os.path.join(args.output, comparison._name)
for scenario in comparison._scenarios:
name = os.path.join(comparison._name, scenario._name)
if not fnmatch.fnmatch(name, args.filter):
if args.verbose:
print "Skipping %s" % name
continue
if args.verbose:
print "Running %s" % name
dirname = os.path.join(args.output, comparison._name)
filename = os.path.join(dirname, scenario._name + ".json")
if not os.path.exists(dirname):
os.makedirs(dirname)
report = engine.run(hardware, scenario)
with open(filename, "w") as fh:
print >>fh, report.to_json()
except Exception as e:
print >>sys.stderr, "Error: %s" % str(e)
if args.debug:
raise
class PlotShell(object):
def __init__(self):
super(PlotShell, self).__init__()
self._parser = argparse.ArgumentParser(description="Migration Test Tool")
self._parser.add_argument("--output", dest="output", default=None)
self._parser.add_argument("--debug", dest="debug", default=False, action="store_true")
self._parser.add_argument("--verbose", dest="verbose", default=False, action="store_true")
self._parser.add_argument("--migration-iters", dest="migration_iters", default=False, action="store_true")
self._parser.add_argument("--total-guest-cpu", dest="total_guest_cpu", default=False, action="store_true")
self._parser.add_argument("--split-guest-cpu", dest="split_guest_cpu", default=False, action="store_true")
self._parser.add_argument("--qemu-cpu", dest="qemu_cpu", default=False, action="store_true")
self._parser.add_argument("--vcpu-cpu", dest="vcpu_cpu", default=False, action="store_true")
self._parser.add_argument("reports", nargs='*')
def run(self, argv):
args = self._parser.parse_args(argv)
if len(args.reports) == 0:
print >>sys.stderr, "At least one report required"
return 1
if not (args.qemu_cpu or
args.vcpu_cpu or
args.total_guest_cpu or
args.split_guest_cpu):
print >>sys.stderr, "At least one chart type is required"
return 1
reports = []
for report in args.reports:
reports.append(Report.from_json_file(report))
plot = Plot(reports,
args.migration_iters,
args.total_guest_cpu,
args.split_guest_cpu,
args.qemu_cpu,
args.vcpu_cpu)
plot.generate(args.output)