Skip to content
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

Merged
merged 12 commits into from
Nov 3, 2017

Conversation

cheeseprocedure
Copy link
Contributor

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!

Copy link
Member

@schmichael schmichael left a 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.

@@ -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, " "))
Copy link
Member

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.

@@ -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")
Copy link
Member

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.

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 {
Copy link
Member

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.

if err == nil {
return nil
}
h.logger.Printf("[WARN] driver.qemu - failed to send '%s' to monitor socket '%s': %s", qemuGracefulShutdownMsg, h.monitorPath, err)
Copy link
Member

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.

@@ -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")
Copy link
Member

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?

@@ -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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Include userPID

@@ -14,6 +14,14 @@ import (
ctestutils "github.com/hashicorp/nomad/client/testutil"
)

func generateString(length int) string {
Copy link
Member

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,
Copy link
Member

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)?

Copy link
Member

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.)
Copy link
Member

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?

@cheeseprocedure cheeseprocedure force-pushed the f-qemu-graceful-shutdown branch 7 times, most recently from 35176dc to 95e07da Compare November 1, 2017 22:15
@cheeseprocedure
Copy link
Contributor Author

Hi @schmichael - thank you for all the feedback!

I've made a number of improvements in response, including more actionable logging, fixes to Kill() to ensure we don't leak processes, and additional tests of graceful shutdown logic.

Please let me know if I can perform any additional changes and/or cleanup.

Copy link
Member

@schmichael schmichael left a 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.

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")
Copy link
Member

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.

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)
Copy link
Member

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.

monitorPathExists = true
break
}
time.Sleep(1 * time.Second)
Copy link
Member

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.

}

// Clean up
if err := resp.Handle.Kill(); err != nil {
Copy link
Member

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


// Clean up
if err := resp.Handle.Kill(); err != nil {
fmt.Printf("\nError killing Qemu test: %s", err)
Copy link
Member

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)

@cheeseprocedure
Copy link
Contributor Author

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/
Copy link
Member

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. :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

D'oh! Fixed.

}

// Clean up
defer func() {
Copy link
Member

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Double d'oh. Fixed!

// The key populated in Node Attributes to indicate presence of the Qemu driver
qemuDriverAttr = "driver.qemu"
qemuDriverVersionAttr = "driver.qemu.version"
qemuDriverLongMonitorPathAttr = "driver.qemu.longsocketpaths"
Copy link
Contributor

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

Copy link
Contributor

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-

Copy link
Contributor Author

@cheeseprocedure cheeseprocedure Nov 3, 2017

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!

// Relevant fix is here:
// https://github.com/qemu/qemu/commit/ad9579aaa16d5b385922d49edac2c96c79bcfb6
currentQemuSemver := semver.New(currentQemuVersion)
fixedSocketPathLenVer := semver.New("2.10.1")
Copy link
Contributor

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

@@ -64,6 +75,13 @@ type qemuHandle struct {
doneCh chan struct{}
}

func getMonitorPath(dir string, longPathSupport string) (string, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment on this method.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

@@ -64,6 +75,13 @@ type qemuHandle struct {
doneCh chan struct{}
}

func getMonitorPath(dir string, longPathSupport string) (string, error) {
if len(dir) > legacyMaxMonitorPathLen && longPathSupport != "1" {
Copy link
Contributor

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

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)
Copy link
Contributor

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.

Copy link
Contributor Author

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.

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)
Copy link
Contributor

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

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

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 {
Copy link
Contributor

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

return errors.New("monitorPath not set")
} else {
monitorSocket, err := net.Dial("unix", monitorPath)
if err == nil {
Copy link
Contributor

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

// Reference: https://en.wikibooks.org/wiki/QEMU/Monitor
qemuGracefulShutdownMsg = "system_powerdown\n"
legacyMaxMonitorPathLen = 108
qemuMonitorSocketName = "qemu-monitor.sock"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about windows?

Copy link
Contributor Author

@cheeseprocedure cheeseprocedure Nov 2, 2017

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)?

Copy link
Contributor

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.

@cheeseprocedure
Copy link
Contributor Author

@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.

@cheeseprocedure cheeseprocedure force-pushed the f-qemu-graceful-shutdown branch 2 times, most recently from 101dba8 to 80495d7 Compare November 3, 2017 02:25
Remove attribute for long qemu monitor path; misc cleanup; update tests
Copy link
Contributor

@dadgar dadgar left a 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!

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)
Copy link
Contributor

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"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

// 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 {
Copy link
Contributor

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

// Reference: https://en.wikibooks.org/wiki/QEMU/Monitor
qemuGracefulShutdownMsg = "system_powerdown\n"
legacyMaxMonitorPathLen = 108
qemuMonitorSocketName = "qemu-monitor.sock"
Copy link
Contributor

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.

@cheeseprocedure
Copy link
Contributor Author

@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.

@dadgar
Copy link
Contributor

dadgar commented Nov 3, 2017

@cheeseprocedure Awesome work! I am going to merge and add some documentation that the graceful shutdown is only supported on Linux for now!

@dadgar dadgar merged commit 80b434d into hashicorp:master Nov 3, 2017
@github-actions
Copy link

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.
If you have found a problem that seems related to this change, please open a new issue and complete the issue template so we can capture all the details necessary to investigate further.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators Mar 18, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants