Files
Etienne PerotandgVisor bot d210be2f2b Add more debug logging for host settings.
This ensures that:
- If not checked, a message about that is still printed.
- If checked but already optimal, a message about that is also printed.

Put together, this ensures that host settings behavior always has non-zero
output in the logs, so that what it is doing is always explicit rather than
having to infer what is happening from the lack of logs.

PiperOrigin-RevId: 744029122
2025-04-04 12:21:19 -07:00

331 lines
12 KiB
Go

// Copyright 2024 The gVisor Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package hostsettings provides suggestions or adjustments for host kernel
// settings to improve runsc performance, stability, or security.
package hostsettings
import (
"fmt"
"os"
"strconv"
"strings"
"gvisor.dev/gvisor/pkg/log"
"gvisor.dev/gvisor/pkg/sentry/hostmm"
"gvisor.dev/gvisor/runsc/config"
)
// Handle deals with host settings according to the given config.
func Handle(conf *config.Config) error {
switch conf.HostSettings {
case config.HostSettingsIgnore:
log.Infof("Host settings not checked per config.")
return nil
case config.HostSettingsCheck, config.HostSettingsCheckMandatory:
deltas, errs := check(conf)
for _, err := range errs {
log.Warningf("Checking host settings: error: %v", err)
}
for _, delta := range deltas {
if delta.Mandatory {
return fmt.Errorf("host setting %q (currently: %q) is not supported (%s); must change it to %q for runsc to work", delta.Name, delta.FromValue, delta.Purpose, delta.ToValue)
}
if conf.HostSettings == config.HostSettingsCheck {
log.Warningf("Host setting %q (currently: %q) is not optimal (%s); it is recommended to change it to %q", delta.Name, delta.FromValue, delta.Purpose, delta.ToValue)
}
}
case config.HostSettingsAdjust, config.HostSettingsEnforce:
deltas, errs := check(conf)
for _, err := range errs {
log.Warningf("Host settings: error: %v", err)
}
for _, delta := range deltas {
log.Warningf("Host setting %q (currently: %q) is not optimal (%s); attempting to change it to %q...", delta.Name, delta.FromValue, delta.Purpose, delta.ToValue)
err := delta.Apply()
// We handle the nil error case first here as it simplifies control flow.
if err == nil {
log.Warningf("Host setting %q changed from %q to %q (%s)", delta.Name, delta.FromValue, delta.ToValue, delta.Purpose)
continue
}
if delta.Mandatory {
return fmt.Errorf("host setting %q (currently: %q) is not supported (%s), and trying to change it to %q failed (%w); bailing out as this is necessary for runsc to work", delta.Name, delta.FromValue, delta.Purpose, delta.ToValue, err)
}
if conf.HostSettings == config.HostSettingsEnforce {
return fmt.Errorf("failed to adjust %s to %q: %w", delta.Name, delta.ToValue, err)
}
log.Warningf("Host settings: failed to adjust %s to %q: %v; continuing anyway.", delta.Name, delta.ToValue, err)
}
default:
return fmt.Errorf("invalid host settings policy %d", conf.HostSettings)
}
return nil
}
// Setting is a host setting to check or adjust.
type Setting interface {
// Name is the name of the setting. In most cases this should be the
// full path of the setting, either under /sys or /proc/sys.
// This can be a non-path for settings that are not controlled via files,
// such as kernel command-line parameters or SELinux labels.
Name() string
// Delta checks whether the current value of the setting is optimal.
// If already optimal, it returns a nil Delta.
Delta() (*Delta, error)
}
// Delta is a change to make to a host setting.
type Delta struct {
// Name of the setting being changed.
Name string
// FromValue is the value of the setting before applying the delta.
FromValue string
// ToValue is the value of the setting after applying the delta.
ToValue string
// Mandatory indicates whether the delta *must* be applied.
// This should be set in cases where `runsc` will fail completely
// if the delta is not applied.
Mandatory bool
// Purpose achieved by applying the delta.
Purpose string
// Apply applies the delta.
Apply func() error
}
// pathSetting implements `Setting` for kernel settings controlled via files.
type pathSetting struct {
// path is the full path of the setting, either under /sys or /proc/sys.
path string
// mightNotExist indicates that the file may not exist on all systems.
// If the path does not exist, no delta is returned for this setting.
mightNotExist bool
// mightLackReadPerm indicates that the file may not be readable.
// If the file cannot be read with a permission error, and we are running
// in rootless mode, a message is logged but no delta is returned for this
// setting.
mightLackReadPerm bool
// purpose describes the intent behind setting this value.
purpose string
// `delta` returns an empty string if the current value is optimal,
// or a value to write to `path` if not optimal.
// It also returns whether the delta is mandatory.
delta func(conf *config.Config, current string) (string, bool, error)
}
// Name implements `Setting.Name`.
func (s pathSetting) Name() string {
return s.path
}
// Delta implements `Setting.Delta`.
func (s pathSetting) Delta(conf *config.Config) (*Delta, error) {
currentBytes, err := os.ReadFile(s.path)
if err != nil {
if s.mightNotExist && os.IsNotExist(err) {
return nil, nil
}
if s.mightLackReadPerm && os.IsPermission(err) {
log.Infof("Host settings: Cannot check if %q is optimal (%s): %v; continuing anyway.", s.path, s.purpose, err)
return nil, nil
}
return nil, fmt.Errorf("cannot read %q: %w", s.path, err)
}
currentValue := strings.TrimSpace(string(currentBytes))
log.Debugf("Host settings: Current value of %q: %q", s.path, currentValue)
newValue, mandatory, err := s.delta(conf, currentValue)
if err != nil {
return nil, err
}
if newValue == "" {
return nil, nil
}
return &Delta{
Name: s.path,
FromValue: currentValue,
ToValue: newValue,
Mandatory: mandatory,
Purpose: s.purpose,
Apply: func() error {
if err := os.WriteFile(s.path, []byte(newValue), 0644); err != nil {
return fmt.Errorf("cannot write %q: %w", s.path, err)
}
// Double-check that the value has been written properly.
newDelta, err := s.Delta(conf)
if err != nil {
return fmt.Errorf("cannot re-read %q after writing: %w", s.path, err)
}
if newDelta != nil {
return fmt.Errorf("writing to %q did not change the observed value: wrote %q but read back %q", s.path, newValue, newDelta.FromValue)
}
return nil
},
}, nil
}
// checks checks the host settings and returns any deltas to do or errors
// occurred while checking.
func check(conf *config.Config) ([]*Delta, []error) {
log.Debugf("Checking host settings")
settings := []pathSetting{
{
path: "/sys/kernel/mm/transparent_hugepage/shmem_enabled",
purpose: "turning on transparent hugepages support in shmem increases memory allocation performance",
delta: func(conf *config.Config, current string) (string, bool, error) {
cur, err := hostmm.GetTransparentHugepageEnum(current)
if err != nil {
return "", false, err
}
switch cur {
case "always", "advise", "force", "within_size":
return "", false, nil
case "never":
return "advise", false, nil
case "deny":
// This is a non-default setting, so assume that it's
// admin-intended and don't try to change it.
return "", false, nil
default:
// Linux can reasonably add new options, so this shouldn't
// be fatal.
log.Warningf("Unknown value for /sys/kernel/mm/transparent_hugepage/shmem_enabled: %s", cur)
return "", false, nil
}
},
},
{
path: "/sys/kernel/mm/transparent_hugepage/defrag",
purpose: "disabling direct compaction improves page fault latency when hugepages are not immediately available",
delta: func(conf *config.Config, current string) (string, bool, error) {
cur, err := hostmm.GetTransparentHugepageEnum(current)
if err != nil {
return "", false, err
}
switch cur {
case "always", "defer", "never":
return "", false, nil
case "defer+madvise":
return "defer", false, nil
case "madvise":
return "never", false, nil
default:
// Linux can reasonably add new options, so this shouldn't
// be fatal.
log.Warningf("Unknown value for /sys/kernel/mm/transparent_hugepage/defrag: %s", cur)
return "", false, nil
}
},
},
{
path: "/proc/sys/vm/max_map_count",
purpose: "increasing max_map_count decreases the likelihood of host VMA exhaustion",
delta: func(conf *config.Config, current string) (string, bool, error) {
const recommendedMaxMapCount = 4194304
currentVal, err := strconv.Atoi(strings.TrimSpace(current))
if err != nil {
return "", false, fmt.Errorf("failed to parse %q as an integer: %v", current, err)
}
if currentVal >= recommendedMaxMapCount {
return "", false, nil
}
return strconv.Itoa(recommendedMaxMapCount), false, nil
},
},
{
path: "/proc/sys/user/max_user_namespaces",
purpose: "runsc requires creating at least 2 new user namespaces and may run into the limit when creating multiple containers",
delta: func(conf *config.Config, current string) (string, bool, error) {
const recommendedMaxUserNamespaces = 256
currentVal, err := strconv.Atoi(strings.TrimSpace(current))
if err != nil {
return "", false, fmt.Errorf("failed to parse %q as an integer: %v", current, err)
}
if currentVal >= recommendedMaxUserNamespaces {
return "", false, nil
}
if currentVal < 2 {
// If less than two, runsc will definitely fail (it needs one for
// the gofer and one for the sandbox), so mark as mandatory.
return strconv.Itoa(recommendedMaxUserNamespaces), true, nil
}
return strconv.Itoa(recommendedMaxUserNamespaces), false, nil
},
},
{
path: "/proc/sys/kernel/unprivileged_userns_clone",
mightNotExist: true,
purpose: "in rootless mode, runsc requires the ability to create new user namespaces without privileges",
delta: func(conf *config.Config, current string) (string, bool, error) {
if !conf.Rootless {
// Setting not required.
return "", false, nil
}
currentVal, err := strconv.Atoi(strings.TrimSpace(current))
if err != nil {
return "", false, fmt.Errorf("failed to parse %q as an integer: %v", current, err)
}
if currentVal != 0 {
return "", false, nil
}
return "1", true, nil
},
},
{
path: "/proc/sys/kernel/unprivileged_userns_apparmor_policy",
mightNotExist: true,
mightLackReadPerm: true,
purpose: "in rootless mode, runsc requires the ability to create new user namespaces without privileges",
delta: func(conf *config.Config, current string) (string, bool, error) {
if !conf.Rootless {
// Setting not required.
return "", false, nil
}
currentVal, err := strconv.Atoi(strings.TrimSpace(current))
if err != nil {
return "", false, fmt.Errorf("failed to parse %q as an integer: %v", current, err)
}
if currentVal != 0 {
return "", false, nil
}
return "1", true, nil
},
},
}
var deltas []*Delta
var errs []error
for _, setting := range settings {
log.Debugf("Checking host setting: %s", setting.Name())
delta, err := setting.Delta(conf)
if err != nil {
errs = append(errs, fmt.Errorf("failed to check %s: %v", setting.Name(), err))
continue
}
if delta != nil {
deltas = append(deltas, delta)
} else {
log.Infof("Host setting %q is optimal", setting.Name())
}
}
return deltas, errs
}