-
Notifications
You must be signed in to change notification settings - Fork 2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Qemu driver: graceful shutdown feature #3411
Qemu driver: graceful shutdown feature #3411
Conversation
216285c
to
2129de2
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks great! Thanks for submitting this! The only blocker is around the Kill()
method logic.. I'm not sure it will block up to kill_timeout
waiting for the VM to actually shutdown. See relevant comments on that code and the docs.
client/driver/qemu.go
Outdated
@@ -239,7 +288,7 @@ func (d *QemuDriver) Start(ctx *ExecContext, task *structs.Task) (*StartResponse | |||
) | |||
} | |||
|
|||
d.logger.Printf("[DEBUG] Starting QemuVM command: %q", strings.Join(args, " ")) | |||
d.logger.Printf("[DEBUG] driver.qemu - starting QemuVM command: %q", strings.Join(args, " ")) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you standardize on driver.qemu:
as you logging prefix? That's the convention we use elsewhere.
client/driver/qemu.go
Outdated
@@ -317,7 +367,7 @@ func (d *QemuDriver) Open(ctx *ExecContext, handleID string) (DriverHandle, erro | |||
|
|||
exec, pluginClient, err := createExecutorWithConfig(pluginConfig, d.config.LogOutput) | |||
if err != nil { | |||
d.logger.Println("[ERR] driver.qemu: error connecting to plugin so destroying plugin pid and user pid") | |||
d.logger.Println("[ERR] driver.qemu - error connecting to plugin so destroying plugin pid and user pid") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While you're changing this could you switch to Printf and include at least the pids. There's not really enough information in this log line to be useful.
client/driver/qemu.go
Outdated
defer monitorSocket.Close() | ||
h.logger.Printf("[DEBUG] driver.qemu - sending graceful shutdown command to qemu monitor socket at %s", h.monitorPath) | ||
_, err = monitorSocket.Write([]byte(qemuGracefulShutdownMsg)) | ||
if err == nil { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there any way to block until either the vm exits or the kill timeout is reached and make sure to always call h.executor.ShutDown()
to kill Nomad's sidecar process?
I'm afraid this could leak VMs and Nomad executor processes.
client/driver/qemu.go
Outdated
if err == nil { | ||
return nil | ||
} | ||
h.logger.Printf("[WARN] driver.qemu - failed to send '%s' to monitor socket '%s': %s", qemuGracefulShutdownMsg, h.monitorPath, err) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Using %q
instead of '%s'
will properly escape and quote strings for what it's worth. Not a big deal to change as we're not consistent either, but %q
is pretty handy.
client/driver/qemu.go
Outdated
@@ -395,13 +462,13 @@ func (h *qemuHandle) Kill() error { | |||
case <-h.doneCh: | |||
return nil | |||
case <-time.After(h.killTimeout): | |||
h.logger.Printf("[DEBUG] driver.qemu - kill timeout exceeded") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you include monitorPath and/or userPid in log messages so users have some way of connectin gthem to a particular VM?
client/driver/qemu.go
Outdated
@@ -414,7 +481,7 @@ func (h *qemuHandle) run() { | |||
ps, werr := h.executor.Wait() | |||
if ps.ExitCode == 0 && werr != nil { | |||
if e := killProcess(h.userPid); e != nil { | |||
h.logger.Printf("[ERR] driver.qemu: error killing user process: %v", e) | |||
h.logger.Printf("[ERR] driver.qemu - error killing user process: %v", e) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Include userPID
client/driver/qemu_test.go
Outdated
@@ -14,6 +14,14 @@ import ( | |||
ctestutils "github.com/hashicorp/nomad/client/testutil" | |||
) | |||
|
|||
func generateString(length int) string { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Replace with strings.Repeat("x", n)
Config: map[string]interface{}{ | ||
"image_path": "linux-0.2.img", | ||
"accelerator": "tcg", | ||
"graceful_shutdown": false, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Adding a graceful_shutdown test would be nice. Maybe copy StartOpen_Wait, but don't worry about the Open part (that's the restore path)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(Done! Thanks!)
@@ -47,6 +48,8 @@ The `qemu` driver supports the following configuration in the job spec: | |||
If the host machine has `qemu` installed with KVM support, users can specify | |||
`kvm` for the `accelerator`. Default is `tcg`. | |||
|
|||
* `graceful_shutdown` `(bool: false)` - Using the [qemu monitor](https://en.wikibooks.org/wiki/QEMU/Monitor), send an ACPI shutdown signal to virtual machines rather than simply terminating them. This emulates a physical power button press, and gives instances a chance to shut down cleanly. If the VM is still running after ``kill_timeout``, it will be forcefully terminated. (Note that [prior to qemu 2.10.1](https://github.com/qemu/qemu/commit/ad9579aaa16d5b385922d49edac2c96c79bcfb6), the monitor socket path is limited to 108 characters. Graceful shutdown will be disabled if qemu is < 2.10.1 and the generated monitor path exceeds this length. You may encounter this issue if you set long [data_dir](https://www.nomadproject.io/docs/agent/configuration/index.html#data_dir) or [alloc_dir](https://www.nomadproject.io/docs/agent/configuration/client.html#alloc_dir) paths.) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wrap at 80 characters and indent lines with 2 spaces (we're considering loosening this style, but now we're trying to stay consistent).
If the VM is still running after
kill_timeout
, it will be forcefully terminated.
Huh, I thought if you wrote the shutdown message to the monitor's socket properly the Kill()
method returned and Nomad would consider the VM dead even if it wasn't? Perhaps my comment on that code above is wrong?
35176dc
to
95e07da
Compare
95e07da
to
60030d8
Compare
Hi @schmichael - thank you for all the feedback! I've made a number of improvements in response, including more actionable logging, fixes to Please let me know if I can perform any additional changes and/or cleanup. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks great! Thanks for all of the hard work @cheeseprocedure.
I left a few more comments, but I don't think any are blockers. I'll merge soon unless someone else spots something.
client/driver/qemu.go
Outdated
var err error | ||
if monitorPath == "" { | ||
logger.Printf("[DEBUG] driver.qemu: monitorPath not set; will not attempt graceful shutdown for user process pid %d", userPid) | ||
err = errors.New("monitorPath not set") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You could return the error here and skip the else
block and var err error
declaration.
Not a blocker.
client/driver/qemu_test.go
Outdated
monitorPathExists := false | ||
for i := 0; i < 5; i++ { | ||
if _, err := os.Stat(monitorPath); !os.IsNotExist(err) { | ||
fmt.Printf("Monitor socket exists at %q\n", monitorPath) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
d.logger.Printf
(or a testLogger()
) is preferred but not a blocker.
client/driver/qemu_test.go
Outdated
monitorPathExists = true | ||
break | ||
} | ||
time.Sleep(1 * time.Second) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We test on fast developer machines and very slow VMs, so testing more often (eg every 200ms) and more times (eg 50 times) would be preferred.
client/driver/qemu_test.go
Outdated
} | ||
|
||
// Clean up | ||
if err := resp.Handle.Kill(); err != nil { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Put this in a defer
client/driver/qemu_test.go
Outdated
|
||
// Clean up | ||
if err := resp.Handle.Kill(); err != nil { | ||
fmt.Printf("\nError killing Qemu test: %s", err) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as above (use a testLogger)
Thanks for the additional feedback! I've made some small changes in response. |
GNUmakefile
Outdated
@echo "==> Spell checking website..." | ||
@misspell -error -source=text website/source/ | ||
# @misspell -error -source=text website/source/ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ha, I don't think you meant to commit this. :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
D'oh! Fixed.
client/driver/qemu_test.go
Outdated
} | ||
|
||
// Clean up | ||
defer func() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh sorry, meant to do this block in a defer right after you check the error from d.Start
. That way you cleanup even if a fatal error is hit.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Double d'oh. Fixed!
b4fc5e7
to
1856585
Compare
client/driver/qemu.go
Outdated
// The key populated in Node Attributes to indicate presence of the Qemu driver | ||
qemuDriverAttr = "driver.qemu" | ||
qemuDriverVersionAttr = "driver.qemu.version" | ||
qemuDriverLongMonitorPathAttr = "driver.qemu.longsocketpaths" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am not a fan of this being an attribute. I think we can just check in the start method
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For scheduling purposes, constraining on this can be replicated by: https://www.nomadproject.io/docs/job-specification/constraint.html#quot-version-quot-
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks - I've removed this attribute as part of the changes to getMonitorPath
!
client/driver/qemu.go
Outdated
// Relevant fix is here: | ||
// https://github.com/qemu/qemu/commit/ad9579aaa16d5b385922d49edac2c96c79bcfb6 | ||
currentQemuSemver := semver.New(currentQemuVersion) | ||
fixedSocketPathLenVer := semver.New("2.10.1") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you make this a variable with a comment. Can be a singleton.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done!
client/driver/qemu.go
Outdated
@@ -64,6 +75,13 @@ type qemuHandle struct { | |||
doneCh chan struct{} | |||
} | |||
|
|||
func getMonitorPath(dir string, longPathSupport string) (string, error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Comment on this method.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done!
client/driver/qemu.go
Outdated
@@ -64,6 +75,13 @@ type qemuHandle struct { | |||
doneCh chan struct{} | |||
} | |||
|
|||
func getMonitorPath(dir string, longPathSupport string) (string, error) { | |||
if len(dir) > legacyMaxMonitorPathLen && longPathSupport != "1" { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of taking the longPathSupport
string make this a function on the QemuDriver
and use the node that is in the embedded DriverCtx
to check the version of the qemu driver and determine if there is support.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done!
client/driver/qemu.go
Outdated
d.logger.Printf("[DEBUG] driver.qemu: got monitor path OK: %s", monitorPath) | ||
args = append(args, "-monitor", fmt.Sprintf("unix:%s,server,nowait", monitorPath)) | ||
} else { | ||
d.logger.Printf("[WARN] driver.qemu: %s", err) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this should fail the task.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done! This will now return (and log) an error.
client/driver/qemu.go
Outdated
if h.pluginClient.Exited() { | ||
return nil | ||
// First, try sending a graceful shutdown command via the qemu monitor | ||
err := sendQemuShutdown(h.logger, h.monitorPath, h.userPid) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wrap this in a if check seeing if graceful shutdown is enabled and log the error
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done!
client/driver/qemu.go
Outdated
if monitorPath == "" { | ||
logger.Printf("[DEBUG] driver.qemu: monitorPath not set; will not attempt graceful shutdown for user process pid %d", userPid) | ||
return errors.New("monitorPath not set") | ||
} else { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can remove the else statement since the if returns.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done!
client/driver/qemu.go
Outdated
return errors.New("monitorPath not set") | ||
} else { | ||
monitorSocket, err := net.Dial("unix", monitorPath) | ||
if err == nil { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Swap the checking. if err != nil { log && return err}
and de-indent the block.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done!
client/driver/qemu.go
Outdated
// Reference: https://en.wikibooks.org/wiki/QEMU/Monitor | ||
qemuGracefulShutdownMsg = "system_powerdown\n" | ||
legacyMaxMonitorPathLen = 108 | ||
qemuMonitorSocketName = "qemu-monitor.sock" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about windows?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good question! Until today, I didn't realize QEMU was available on Windows hosts. I would need to spin up an EC2 instance and experiment a bit with QEMU monitor interaction on that platform.
It appears an existing QEMU driver option can cause trouble on Windows hosts (accelerator = "kvm"
). As a first pass, would it be acceptable to fail a task if graceful shutdown has been enabled for the job and runtime.GOOS == "windows"
(ensuring it's called out in documentation, of course)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I think that is acceptable. Would you mind adding error cases for both graceful and kvm on windows.
@dadgar thank you very much for your feedback! I believe I've addressed the comments, with the exception of one outstanding question about Windows support. |
101dba8
to
80495d7
Compare
Remove attribute for long qemu monitor path; misc cleanup; update tests
80495d7
to
66f9840
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Awesome work so far!
client/driver/qemu.go
Outdated
d.logger.Printf("[WARN] driver.qemu: %s", err) | ||
monitorPath, err := d.getMonitorPath(ctx.TaskDir.Dir) | ||
if err != nil { | ||
d.logger.Printf("[ERR] driver.qemu: could not get qemu monitor path - error: %s", err) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"[ERR] driver.qemu: could not get qemu monitor path: %s"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done!
client/driver/qemu.go
Outdated
// If we did not send a graceful shutdown via the monitor socket, we'll | ||
// issue an interrupt to the qemu process as a last resort | ||
if err != nil { | ||
if err := sendQemuShutdown(h.logger, h.monitorPath, h.userPid); err != nil { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will log several debug messages relating to graceful shutdown even if the user didn't set the option. We should only attempt the graceful shutdown if the user asked for it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done!
client/driver/qemu.go
Outdated
// Reference: https://en.wikibooks.org/wiki/QEMU/Monitor | ||
qemuGracefulShutdownMsg = "system_powerdown\n" | ||
legacyMaxMonitorPathLen = 108 | ||
qemuMonitorSocketName = "qemu-monitor.sock" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I think that is acceptable. Would you mind adding error cases for both graceful and kvm on windows.
@dadgar thank you for the review! In addition to the fixes above, I've added simple checks for the graceful-shutdown feature and use of the KVM accelerator which return errors if run on a Windows host. |
c3121f1
to
046b774
Compare
046b774
to
f734d84
Compare
@cheeseprocedure Awesome work! I am going to merge and add some documentation that the graceful shutdown is only supported on Linux for now! |
I'm going to lock this pull request because it has been closed for 120 days ⏳. This helps our maintainers find and focus on the active contributions. |
This PR enables the Qemu driver to attempt a graceful shutdown of guest VMs by passing an ACPI-shutdown via the Qemu monitor.
This is a best-effort mechanism for notifying guests of their impending termination. The driver will still forcibly terminate the process if the kill timeout expires.
I think the integration tests can be improved; any suggestions in that area are especially appreciated.
Thank you!