runsc create: Record metric information when instrumentation is requested.

This changes the `runsc` sandbox creation process to gather metric
registration data before any container is started, and saves this in the state
file.

This change has no tests, but coverage is provided in a later change that
provides an end-to-end container tests that the metric server works and
exports data faithfully.

This change is part of a series of changes to support Prometheus-style metrics
in `runsc`. Doing so requires making several seemingly-odd design decisions,
due to the following architectural constraints:

- Prometheus requires an HTTP server serving the `/metrics` endpoint.
- For performance reasons, the `runsc boot` process cannot run the `netpoller`
  goroutine.
  - Since we don't want to write our own HTTP server implementation, this
    means the HTTP endpoint has to be served by a separate process that
    remains running during the lifetime of the container.
- The `runsc boot` process is untrusted.
  - This means we cannot trust metrics data that comes out of the Sentry.
    Therefore, there needs to be an elaborate dance where we pre-register
    metric metadata before starting any untrusted workload. Then, the server
    relaying the metric data must verify the validity of metric values against
    this metric metadata. This avoids leaking metrics, cardinality blow-ups,
    and other such DoS vectors.
- This feature needs to be easy-to-use in a typical Docker setting.
  - This means having the ability to just say
    `--metrics-server=localhost:1337` in the `runsc` runtime entry in
    `/etc/docker/daemon.json` and have that Just Work(TM), even when multiple
    containers are running.
  - Since only one process may listen on a port at a given time, this means
    the metric server needs to be able to multiplex requests out to multiple
    running sandboxes, and remain alive for the entire duration of either of
    these sandboxes.
  - For this reason, the metrics server runs *outside* of the usual
    per-container cgroups.
  - This also saves system resources by not running one server per sandbox.
- The metrics server must be exposed to the outside world, and cannot assume
  that its clients are trustworthy.
  - For this reason, a metrics server is bound to a runtime root directory,
    and double-checks all that the sandboxes it is asked to follow actually
    exist in this root directory.

PiperOrigin-RevId: 503539630
This commit is contained in:
Etienne Perot
2023-01-20 15:53:12 -08:00
committed by gVisor bot
parent b331253f04
commit 8a63501720
2 changed files with 16 additions and 11 deletions
+1 -1
View File
@@ -15,6 +15,7 @@ go_library(
],
visibility = [
"//runsc:__subpackages__",
"//test:__subpackages__",
],
deps = [
"//pkg/atomicbitops",
@@ -45,7 +46,6 @@ go_library(
"@com_github_opencontainers_runtime_spec//specs-go:go_default_library",
"@com_github_syndtr_gocapability//capability:go_default_library",
"@com_github_vishvananda_netlink//:go_default_library",
"@org_golang_google_protobuf//encoding/prototext:go_default_library",
"@org_golang_x_sys//unix:go_default_library",
],
)
+15 -10
View File
@@ -33,7 +33,6 @@ import (
specs "github.com/opencontainers/runtime-spec/specs-go"
"github.com/syndtr/gocapability/capability"
"golang.org/x/sys/unix"
"google.golang.org/protobuf/encoding/prototext"
"gvisor.dev/gvisor/pkg/atomicbitops"
"gvisor.dev/gvisor/pkg/cleanup"
"gvisor.dev/gvisor/pkg/control/client"
@@ -137,8 +136,7 @@ type Sandbox struct {
// Used for verifying metric data integrity after containers are started.
// Only populated if exporting metrics was requested when the sandbox was
// created.
// This is a textproto string of metricpb.MetricRegistration.
RegisteredMetrics string `json:"registeredMetrics"`
RegisteredMetrics *metricpb.MetricRegistration `json:"registeredMetrics"`
// MetricServerAddress is the address of the metric server that this sandbox
// intends to export metrics for.
@@ -281,6 +279,17 @@ func New(conf *config.Config, args *Args) (*Sandbox, error) {
return nil, fmt.Errorf("cannot read client sync file: %w", err)
}
if conf.MetricServer != "" {
// The control server is up and the sandbox was configured to export metrics.
// We must gather data about registered metrics prior to any process starting in the sandbox.
log.Debugf("Getting metric registration information from sandbox %q", s.ID)
var registeredMetrics control.MetricsRegistrationResponse
if err := s.call(boot.MetricsGetRegistered, nil, &registeredMetrics); err != nil {
return nil, fmt.Errorf("cannot get registered metrics: %v", err)
}
s.RegisteredMetrics = registeredMetrics.RegisteredMetrics
}
c.Release()
return s, nil
}
@@ -990,7 +999,7 @@ func (s *Sandbox) IsRootContainer(cid string) bool {
// Destroy frees all resources associated with the sandbox. It fails fast and
// is idempotent.
func (s *Sandbox) destroy() error {
log.Debugf("Destroy sandbox %q", s.ID)
log.Debugf("Destroying sandbox %q", s.ID)
pid := s.Pid.load()
if pid != 0 {
log.Debugf("Killing sandbox %q", s.ID)
@@ -1116,14 +1125,10 @@ func (s *Sandbox) UsageFD() (*control.MemoryUsageRecord, error) {
// bogus metrics.
// This returns an error if the sandbox has not requested instrumentation during creation time.
func (s *Sandbox) GetRegisteredMetrics() (*metricpb.MetricRegistration, error) {
if s.RegisteredMetrics == "" {
if s.RegisteredMetrics == nil {
return nil, errors.New("sandbox did not request instrumentation when it was created")
}
registeredMetrics := &metricpb.MetricRegistration{}
if err := prototext.Unmarshal([]byte(s.RegisteredMetrics), registeredMetrics); err != nil {
return nil, err
}
return registeredMetrics, nil
return s.RegisteredMetrics, nil
}
// ExportMetrics returns a snapshot of metric values from the sandbox in Prometheus format.