2 # Migration test main engine
4 # Copyright (c) 2016 Red Hat, Inc.
6 # This library is free software; you can redistribute it and/or
7 # modify it under the terms of the GNU Lesser General Public
8 # License as published by the Free Software Foundation; either
9 # version 2.1 of the License, or (at your option) any later version.
11 # This library is distributed in the hope that it will be useful,
12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 # Lesser General Public License for more details.
16 # You should have received a copy of the GNU Lesser General Public
17 # License along with this library; if not, see <http://www.gnu.org/licenses/>.
26 from guestperf
.progress
import Progress
, ProgressStats
27 from guestperf
.report
import Report
28 from guestperf
.timings
import TimingRecord
, Timings
30 sys
.path
.append(os
.path
.join(os
.path
.dirname(__file__
),
31 '..', '..', '..', 'python'))
32 from qemu
.machine
import QEMUMachine
37 def __init__(self
, binary
, dst_host
, kernel
, initrd
, transport
="tcp",
38 sleep
=15, verbose
=False, debug
=False):
40 self
._binary
= binary
# Path to QEMU binary
41 self
._dst
_host
= dst_host
# Hostname of target host
42 self
._kernel
= kernel
# Path to kernel image
43 self
._initrd
= initrd
# Path to stress initrd
44 self
._transport
= transport
# 'unix' or 'tcp' or 'rdma'
46 self
._verbose
= verbose
52 def _vcpu_timing(self
, pid
, tid_list
):
56 jiffies_per_sec
= os
.sysconf(os
.sysconf_names
['SC_CLK_TCK'])
58 statfile
= "/proc/%d/task/%d/stat" % (pid
, tid
)
59 with
open(statfile
, "r") as fh
:
61 fields
= stat
.split(" ")
62 stime
= int(fields
[13])
63 utime
= int(fields
[14])
64 records
.append(TimingRecord(tid
, now
, 1000 * (stime
+ utime
) / jiffies_per_sec
))
67 def _cpu_timing(self
, pid
):
70 jiffies_per_sec
= os
.sysconf(os
.sysconf_names
['SC_CLK_TCK'])
71 statfile
= "/proc/%d/stat" % pid
72 with
open(statfile
, "r") as fh
:
74 fields
= stat
.split(" ")
75 stime
= int(fields
[13])
76 utime
= int(fields
[14])
77 return TimingRecord(pid
, now
, 1000 * (stime
+ utime
) / jiffies_per_sec
)
79 def _migrate_progress(self
, vm
):
80 info
= vm
.cmd("query-migrate")
86 info
.get("status", "active"),
88 info
["ram"].get("transferred", 0),
89 info
["ram"].get("remaining", 0),
90 info
["ram"].get("total", 0),
91 info
["ram"].get("duplicate", 0),
92 info
["ram"].get("skipped", 0),
93 info
["ram"].get("normal", 0),
94 info
["ram"].get("normal-bytes", 0),
95 info
["ram"].get("dirty-pages-rate", 0),
96 info
["ram"].get("mbps", 0),
97 info
["ram"].get("dirty-sync-count", 0)
100 info
.get("total-time", 0),
101 info
.get("downtime", 0),
102 info
.get("expected-downtime", 0),
103 info
.get("setup-time", 0),
104 info
.get("cpu-throttle-percentage", 0),
105 info
.get("dirty-limit-throttle-time-per-round", 0),
106 info
.get("dirty-limit-ring-full-time", 0),
109 def _migrate(self
, hardware
, scenario
, src
, dst
, connect_uri
):
112 src_pid
= src
.get_pid()
114 vcpus
= src
.cmd("query-cpus-fast")
117 src_threads
.append(vcpu
["thread-id"])
119 # XXX how to get dst timings on remote host ?
122 print("Sleeping %d seconds for initial guest workload run" % self
._sleep
)
123 sleep_secs
= self
._sleep
124 while sleep_secs
> 1:
125 src_qemu_time
.append(self
._cpu
_timing
(src_pid
))
126 src_vcpu_time
.extend(self
._vcpu
_timing
(src_pid
, src_threads
))
131 print("Starting migration")
132 if scenario
._auto
_converge
:
133 resp
= src
.cmd("migrate-set-capabilities",
135 { "capability": "auto-converge",
138 resp
= src
.cmd("migrate-set-parameters",
139 cpu_throttle_increment
=scenario
._auto
_converge
_step
)
141 if scenario
._post
_copy
:
142 resp
= src
.cmd("migrate-set-capabilities",
144 { "capability": "postcopy-ram",
147 resp
= dst
.cmd("migrate-set-capabilities",
149 { "capability": "postcopy-ram",
153 resp
= src
.cmd("migrate-set-parameters",
154 max_bandwidth
=scenario
._bandwidth
* 1024 * 1024)
156 resp
= src
.cmd("migrate-set-parameters",
157 downtime_limit
=scenario
._downtime
)
159 if scenario
._compression
_mt
:
160 resp
= src
.cmd("migrate-set-capabilities",
162 { "capability": "compress",
165 resp
= src
.cmd("migrate-set-parameters",
166 compress_threads
=scenario
._compression
_mt
_threads
)
167 resp
= dst
.cmd("migrate-set-capabilities",
169 { "capability": "compress",
172 resp
= dst
.cmd("migrate-set-parameters",
173 decompress_threads
=scenario
._compression
_mt
_threads
)
175 if scenario
._compression
_xbzrle
:
176 resp
= src
.cmd("migrate-set-capabilities",
178 { "capability": "xbzrle",
181 resp
= dst
.cmd("migrate-set-capabilities",
183 { "capability": "xbzrle",
186 resp
= src
.cmd("migrate-set-parameters",
189 1024 * 1024 * 1024 / 100 *
190 scenario
._compression
_xbzrle
_cache
))
192 if scenario
._multifd
:
193 resp
= src
.cmd("migrate-set-capabilities",
195 { "capability": "multifd",
198 resp
= src
.cmd("migrate-set-parameters",
199 multifd_channels
=scenario
._multifd
_channels
)
200 resp
= dst
.cmd("migrate-set-capabilities",
202 { "capability": "multifd",
205 resp
= dst
.cmd("migrate-set-parameters",
206 multifd_channels
=scenario
._multifd
_channels
)
208 if scenario
._dirty
_limit
:
209 if not hardware
._dirty
_ring
_size
:
210 raise Exception("dirty ring size must be configured when "
211 "testing dirty limit migration")
213 resp
= src
.cmd("migrate-set-capabilities",
215 { "capability": "dirty-limit",
218 resp
= src
.cmd("migrate-set-parameters",
219 x_vcpu_dirty_limit_period
=scenario
._x
_vcpu
_dirty
_limit
_period
)
220 resp
= src
.cmd("migrate-set-parameters",
221 vcpu_dirty_limit
=scenario
._vcpu
_dirty
_limit
)
223 resp
= src
.cmd("migrate", uri
=connect_uri
)
228 progress_history
= []
236 progress
= self
._migrate
_progress
(src
)
238 src_qemu_time
.append(self
._cpu
_timing
(src_pid
))
239 src_vcpu_time
.extend(self
._vcpu
_timing
(src_pid
, src_threads
))
241 if (len(progress_history
) == 0 or
242 (progress_history
[-1]._ram
._iterations
<
243 progress
._ram
._iterations
)):
244 progress_history
.append(progress
)
246 if progress
._status
in ("completed", "failed", "cancelled"):
247 if progress
._status
== "completed" and paused
:
249 if progress_history
[-1] != progress
:
250 progress_history
.append(progress
)
252 if progress
._status
== "completed":
254 print("Sleeping %d seconds for final guest workload run" % self
._sleep
)
255 sleep_secs
= self
._sleep
256 while sleep_secs
> 1:
258 src_qemu_time
.append(self
._cpu
_timing
(src_pid
))
259 src_vcpu_time
.extend(self
._vcpu
_timing
(src_pid
, src_threads
))
262 return [progress_history
, src_qemu_time
, src_vcpu_time
]
264 if self
._verbose
and (loop
% 20) == 0:
265 print("Iter %d: remain %5dMB of %5dMB (total %5dMB @ %5dMb/sec)" % (
266 progress
._ram
._iterations
,
267 progress
._ram
._remaining
_bytes
/ (1024 * 1024),
268 progress
._ram
._total
_bytes
/ (1024 * 1024),
269 progress
._ram
._transferred
_bytes
/ (1024 * 1024),
270 progress
._ram
._transfer
_rate
_mbs
,
273 if progress
._ram
._iterations
> scenario
._max
_iters
:
275 print("No completion after %d iterations over RAM" % scenario
._max
_iters
)
276 src
.cmd("migrate_cancel")
279 if time
.time() > (start
+ scenario
._max
_time
):
281 print("No completion after %d seconds" % scenario
._max
_time
)
282 src
.cmd("migrate_cancel")
285 if (scenario
._post
_copy
and
286 progress
._ram
._iterations
>= scenario
._post
_copy
_iters
and
289 print("Switching to post-copy after %d iterations" % scenario
._post
_copy
_iters
)
290 resp
= src
.cmd("migrate-start-postcopy")
293 if (scenario
._pause
and
294 progress
._ram
._iterations
>= scenario
._pause
_iters
and
297 print("Pausing VM after %d iterations" % scenario
._pause
_iters
)
298 resp
= src
.cmd("stop")
301 def _is_ppc64le(self
):
302 _
, _
, _
, _
, machine
= os
.uname()
303 if machine
== "ppc64le":
307 def _get_guest_console_args(self
):
308 if self
._is
_ppc
64le
():
309 return "console=hvc0"
311 return "console=ttyS0"
313 def _get_qemu_serial_args(self
):
314 if self
._is
_ppc
64le
():
315 return ["-chardev", "stdio,id=cdev0",
316 "-device", "spapr-vty,chardev=cdev0"]
318 return ["-chardev", "stdio,id=cdev0",
319 "-device", "isa-serial,chardev=cdev0"]
321 def _get_common_args(self
, hardware
, tunnelled
=False):
327 "cgroup_disable=memory",
331 args
.append(self
._get
_guest
_console
_args
())
338 args
.append("ramsize=%s" % hardware
._mem
)
340 cmdline
= " ".join(args
)
342 cmdline
= "'" + cmdline
+ "'"
346 "-kernel", self
._kernel
,
347 "-initrd", self
._initrd
,
349 "-m", str((hardware
._mem
* 1024) + 512),
350 "-smp", str(hardware
._cpus
),
352 if hardware
._dirty
_ring
_size
:
353 argv
.extend(["-accel", "kvm,dirty-ring-size=%s" %
354 hardware
._dirty
_ring
_size
])
356 argv
.extend(["-accel", "kvm"])
358 argv
.extend(self
._get
_qemu
_serial
_args
())
361 argv
.extend(["-machine", "graphics=off"])
363 if hardware
._prealloc
_pages
:
364 argv_source
+= ["-mem-path", "/dev/shm",
366 if hardware
._locked
_pages
:
367 argv_source
+= ["-overcommit", "mem-lock=on"]
368 if hardware
._huge
_pages
:
373 def _get_src_args(self
, hardware
):
374 return self
._get
_common
_args
(hardware
)
376 def _get_dst_args(self
, hardware
, uri
):
378 if self
._dst
_host
!= "localhost":
380 argv
= self
._get
_common
_args
(hardware
, tunnelled
)
381 return argv
+ ["-incoming", uri
]
384 def _get_common_wrapper(cpu_bind
, mem_bind
):
386 if len(cpu_bind
) > 0 or len(mem_bind
) > 0:
387 wrapper
.append("numactl")
389 wrapper
.append("--physcpubind=%s" % ",".join(cpu_bind
))
391 wrapper
.append("--membind=%s" % ",".join(mem_bind
))
395 def _get_src_wrapper(self
, hardware
):
396 return self
._get
_common
_wrapper
(hardware
._src
_cpu
_bind
, hardware
._src
_mem
_bind
)
398 def _get_dst_wrapper(self
, hardware
):
399 wrapper
= self
._get
_common
_wrapper
(hardware
._dst
_cpu
_bind
, hardware
._dst
_mem
_bind
)
400 if self
._dst
_host
!= "localhost":
402 "-R", "9001:localhost:9001",
403 self
._dst
_host
] + wrapper
407 def _get_timings(self
, vm
):
414 regex
= r
"[^\s]+\s\((\d+)\):\sINFO:\s(\d+)ms\scopied\s\d+\sGB\sin\s(\d+)ms"
415 matcher
= re
.compile(regex
)
417 for line
in log
.split("\n"):
418 match
= matcher
.match(line
)
420 records
.append(TimingRecord(int(match
.group(1)),
421 int(match
.group(2)) / 1000.0,
422 int(match
.group(3))))
425 def run(self
, hardware
, scenario
, result_dir
=os
.getcwd()):
426 abs_result_dir
= os
.path
.join(result_dir
, scenario
._name
)
428 if self
._transport
== "tcp":
429 uri
= "tcp:%s:9000" % self
._dst
_host
430 elif self
._transport
== "rdma":
431 uri
= "rdma:%s:9000" % self
._dst
_host
432 elif self
._transport
== "unix":
433 if self
._dst
_host
!= "localhost":
434 raise Exception("Running use unix migration transport for non-local host")
435 uri
= "unix:/var/tmp/qemu-migrate-%d.migrate" % os
.getpid()
442 if self
._dst
_host
!= "localhost":
443 dstmonaddr
= ("localhost", 9001)
445 dstmonaddr
= "/var/tmp/qemu-dst-%d-monitor.sock" % os
.getpid()
446 srcmonaddr
= "/var/tmp/qemu-src-%d-monitor.sock" % os
.getpid()
448 src
= QEMUMachine(self
._binary
,
449 args
=self
._get
_src
_args
(hardware
),
450 wrapper
=self
._get
_src
_wrapper
(hardware
),
451 name
="qemu-src-%d" % os
.getpid(),
452 monitor_address
=srcmonaddr
)
454 dst
= QEMUMachine(self
._binary
,
455 args
=self
._get
_dst
_args
(hardware
, uri
),
456 wrapper
=self
._get
_dst
_wrapper
(hardware
),
457 name
="qemu-dst-%d" % os
.getpid(),
458 monitor_address
=dstmonaddr
)
464 ret
= self
._migrate
(hardware
, scenario
, src
, dst
, uri
)
465 progress_history
= ret
[0]
466 qemu_timings
= ret
[1]
467 vcpu_timings
= ret
[2]
468 if uri
[0:5] == "unix:" and os
.path
.exists(uri
[5:]):
471 if os
.path
.exists(srcmonaddr
):
472 os
.remove(srcmonaddr
)
474 if self
._dst
_host
== "localhost" and os
.path
.exists(dstmonaddr
):
475 os
.remove(dstmonaddr
)
478 print("Finished migration")
483 return Report(hardware
, scenario
, progress_history
,
484 Timings(self
._get
_timings
(src
) + self
._get
_timings
(dst
)),
485 Timings(qemu_timings
),
486 Timings(vcpu_timings
),
487 self
._binary
, self
._dst
_host
, self
._kernel
,
488 self
._initrd
, self
._transport
, self
._sleep
)
489 except Exception as e
:
491 print("Failed: %s" % str(e
))