Split metricserver.go into multiple files.

PiperOrigin-RevId: 538932080
This commit is contained in:
Etienne Perot
2023-06-08 17:21:04 -07:00
committed by gVisor bot
parent c0a70cd70c
commit 457c1c36d2
6 changed files with 447 additions and 317 deletions
+4
View File
@@ -9,6 +9,10 @@ go_library(
name = "metricserver",
srcs = [
"metricserver.go",
"metricserver_http.go",
"metricserver_lifecycle.go",
"metricserver_metrics.go",
"metricserver_profile.go",
],
visibility = ["//runsc:__subpackages__"],
deps = [
+28 -317
View File
@@ -17,8 +17,6 @@ package metricserver
import (
"context"
"crypto/sha256"
"encoding/binary"
"errors"
"fmt"
"io"
@@ -31,7 +29,6 @@ import (
"regexp"
"runtime"
"runtime/debug"
"runtime/pprof"
"strconv"
"strings"
"syscall"
@@ -50,13 +47,6 @@ import (
)
const (
// verifyLoopInterval is the interval at which we check whether there are any sandboxes we need
// to serve metrics for. If there are none, the server exits.
verifyLoopInterval = 20 * time.Second
// httpTimeout is the timeout used for all connect/read/write operations of the HTTP server.
httpTimeout = 1 * time.Minute
// metricsExportTimeout is the maximum amount of time that the metrics export process should take.
metricsExportTimeout = 30 * time.Second
@@ -71,10 +61,9 @@ const (
// servedSandbox is a sandbox that we serve metrics from.
// A single metrics server will export data about multiple sandboxes.
type servedSandbox struct {
rootContainerID container.FullID
rootDir string
metricServerAddr string
extraLabels map[string]string
rootContainerID container.FullID
server *metricServer
extraLabels map[string]string
// mu protects the fields below.
mu sync.Mutex
@@ -111,66 +100,9 @@ type servedSandbox struct {
// cleanupVerifier holds a reference to the cleanup function of the verifier.
cleanupVerifier func()
}
// SandboxPrometheusLabels returns a set of Prometheus labels that identifies the sandbox running
// the given root container.
func SandboxPrometheusLabels(rootContainer *container.Container) (map[string]string, error) {
s := rootContainer.Sandbox
labels := make(map[string]string, 4)
labels[prometheus.SandboxIDLabel] = s.ID
// Compute iteration ID label in a stable manner.
// This uses sha256(ID + ":" + creation time).
h := sha256.New()
if _, err := io.WriteString(h, s.ID); err != nil {
return nil, err
}
if _, err := io.WriteString(h, ":"); err != nil {
return nil, err
}
if _, err := io.WriteString(h, rootContainer.CreatedAt.UTC().String()); err != nil {
return nil, err
}
labels[prometheus.IterationIDLabel] = strconv.FormatUint(binary.BigEndian.Uint64(h.Sum(nil)[:8]), 36)
if s.PodName != "" {
labels[prometheus.PodNameLabel] = s.PodName
}
if s.Namespace != "" {
labels[prometheus.NamespaceLabel] = s.Namespace
}
return labels, nil
}
// ComputeSpecMetadata returns the labels for the `spec_metadata` metric.
// It merges data from the Specs of multiple containers running within the
// same sandbox.
// This function must support being called with `allContainers` being nil.
// It must return the same set of label keys regardless of how many containers
// are in `allContainers`.
func ComputeSpecMetadata(allContainers []*container.Container) map[string]string {
const (
unknownOCIVersion = "UNKNOWN"
inconsistentOCIVersion = "INCONSISTENT"
)
hasUID0Container := false
ociVersion := unknownOCIVersion
for _, cont := range allContainers {
if cont.RunsAsUID0() {
hasUID0Container = true
}
if ociVersion == unknownOCIVersion {
ociVersion = cont.Spec.Version
} else if ociVersion != cont.Spec.Version {
ociVersion = inconsistentOCIVersion
}
}
return map[string]string{
"hasuid0": strconv.FormatBool(hasUID0Container),
"ociversion": ociVersion,
}
// extra contains additional per-sandbox data.
extra sandboxData
}
// load loads the sandbox being monitored and initializes its metric verifier.
@@ -182,7 +114,7 @@ func (s *servedSandbox) load() (*sandbox.Sandbox, *prometheus.Verifier, error) {
s.mu.Lock()
defer s.mu.Unlock()
if s.sandbox == nil {
allContainers, err := container.LoadSandbox(s.rootDir, s.rootContainerID.SandboxID, container.LoadOpts{
allContainers, err := container.LoadSandbox(s.server.rootDir, s.rootContainerID.SandboxID, container.LoadOpts{
TryLock: container.TryAcquire,
})
if err != nil {
@@ -200,12 +132,12 @@ func (s *servedSandbox) load() (*sandbox.Sandbox, *prometheus.Verifier, error) {
if rootContainer == nil {
return nil, nil, fmt.Errorf("no root container found for sandbox ID %q", s.rootContainerID.SandboxID)
}
sandboxMetricAddr := strings.ReplaceAll(rootContainer.Sandbox.MetricServerAddress, "%RUNTIME_ROOT%", s.rootDir)
sandboxMetricAddr := strings.ReplaceAll(rootContainer.Sandbox.MetricServerAddress, "%RUNTIME_ROOT%", s.server.rootDir)
if sandboxMetricAddr == "" {
return nil, nil, errors.New("sandbox did not request instrumentation")
}
if sandboxMetricAddr != s.metricServerAddr {
return nil, nil, fmt.Errorf("sandbox requested instrumentation by a metric server running at a different address (sandbox wants %q, this metric server serves %q)", sandboxMetricAddr, s.metricServerAddr)
if sandboxMetricAddr != s.server.address {
return nil, nil, fmt.Errorf("sandbox requested instrumentation by a metric server running at a different address (sandbox wants %q, this metric server serves %q)", sandboxMetricAddr, s.server.address)
}
// Update label data as read from the state file.
// Do not store empty labels.
@@ -263,6 +195,9 @@ func (s *servedSandbox) load() (*sandbox.Sandbox, *prometheus.Verifier, error) {
s.verifier = verifier
s.cleanupVerifier = cleanup
}
if err := s.extra.load(s); err != nil {
return nil, nil, err
}
return s.sandbox, s.verifier, nil
}
@@ -333,7 +268,7 @@ type metricServer struct {
// This socket file will be deleted on server shutdown.
// This field is not set if binding to a network port, or when the UDS already existed prior to
// being bound by us (i.e. its ownership isn't ours), such that it isn't deleted in this case.
// The field is unset once the file is succesfully removed.
// The field is unset once the file is successfully removed.
udsPath string
// sandboxes is the list of sandboxes we serve metrics for.
@@ -377,6 +312,9 @@ type metricServer struct {
// shutdownCh is written to when receiving the signal to shut down gracefully.
shutdownCh chan os.Signal
// extraData contains additional server-wide data.
extra serverData
}
// sufficientlyEqualStats returns whether the given FileInfo's are sufficiently
@@ -518,9 +456,8 @@ func (m *metricServer) refreshSandboxesLocked() {
m.numSandboxes++
served := &servedSandbox{
rootContainerID: sid,
rootDir: m.rootDir,
metricServerAddr: m.address,
rootContainerID: sid,
server: m,
extraLabels: map[string]string{
prometheus.SandboxIDLabel: sid.SandboxID,
},
@@ -540,101 +477,6 @@ func (m *metricServer) refreshSandboxesLocked() {
}
}
// httpResult is returned by HTTP handlers.
type httpResult struct {
code int
err error
}
// httpOK is the "everything went fine" HTTP result.
var httpOK = httpResult{code: http.StatusOK}
// serveIndex serves the index page.
func (m *metricServer) serveIndex(w http.ResponseWriter, req *http.Request) httpResult {
if req.URL.Path != "/" {
if strings.HasPrefix(req.URL.Path, "/metrics?") {
// Prometheus's scrape_config.metrics_path takes in a query path and automatically encodes
// all special characters in it to %-form, including the "?" character.
// This can prevent use of query parameters, and we end up here instead.
// To address this, rewrite the URL to undo this transformation.
// This means requesting "/metrics%3Ffoo=bar" is rewritten to "/metrics?foo=bar".
req.URL.RawQuery = strings.TrimPrefix(req.URL.Path, "/metrics?")
req.URL.Path = "/metrics"
return m.serveMetrics(w, req)
}
return httpResult{http.StatusNotFound, errors.New("path not found")}
}
fmt.Fprintf(w, "<html><head><title>runsc metrics</title></head><body>")
fmt.Fprintf(w, "<p>You have reached the runsc metrics server page!</p>")
fmt.Fprintf(w, `<p>To see actual metric data, head over to <a href="/metrics">/metrics</a>.</p>`)
fmt.Fprintf(w, "</body></html>")
return httpOK
}
// Metrics generated by the metrics server itself.
var (
SandboxPresenceMetric = prometheus.Metric{
Name: "sandbox_presence",
Type: prometheus.TypeGauge,
Help: "Boolean metric set to 1 for each known sandbox.",
}
SandboxRunningMetric = prometheus.Metric{
Name: "sandbox_running",
Type: prometheus.TypeGauge,
Help: "Boolean metric set to 1 for each running sandbox.",
}
SandboxMetadataMetric = prometheus.Metric{
Name: "sandbox_metadata",
Type: prometheus.TypeGauge,
Help: "Key-value pairs about per-sandbox metadata.",
}
SandboxCapabilitiesMetric = prometheus.Metric{
Name: "sandbox_capabilities",
Type: prometheus.TypeGauge,
Help: "Linux capabilities added within containers of the sandbox.",
}
SandboxCapabilitiesMetricLabel = "capability"
SpecMetadataMetric = prometheus.Metric{
Name: "spec_metadata",
Type: prometheus.TypeGauge,
Help: "Key-value pairs about OCI spec metadata.",
}
SandboxCreationMetric = prometheus.Metric{
Name: "sandbox_creation_time_seconds",
Type: prometheus.TypeGauge,
Help: "When the sandbox was created, as a unix timestamp in seconds.",
}
NumRunningSandboxesMetric = prometheus.Metric{
Name: "num_sandboxes_running",
Type: prometheus.TypeGauge,
Help: "Number of sandboxes running at present.",
}
NumCannotExportSandboxesMetric = prometheus.Metric{
Name: "num_sandboxes_broken_metrics",
Type: prometheus.TypeGauge,
Help: "Number of sandboxes from which we cannot export metrics.",
}
NumTotalSandboxesMetric = prometheus.Metric{
Name: "num_sandboxes_total",
Type: prometheus.TypeCounter,
Help: "Counter of sandboxes that have ever been started.",
}
)
// Metrics is a list of metrics that the metric server generates.
var Metrics = []prometheus.Metric{
SandboxPresenceMetric,
SandboxRunningMetric,
SandboxMetadataMetric,
SandboxCapabilitiesMetric,
SpecMetadataMetric,
SandboxCreationMetric,
NumRunningSandboxesMetric,
NumCannotExportSandboxesMetric,
NumTotalSandboxesMetric,
prometheus.ProcessStartTimeSeconds,
}
// serveMetrics serves metrics requests.
func (m *metricServer) serveMetrics(w http.ResponseWriter, req *http.Request) httpResult {
ctx, ctxCancel := context.WithTimeout(req.Context(), metricsExportTimeout)
@@ -905,139 +747,6 @@ func (m *metricServer) servePID(w http.ResponseWriter, req *http.Request) httpRe
return httpOK
}
// profileCPU returns a CPU profile over HTTP.
func (m *metricServer) profileCPU(w http.ResponseWriter, req *http.Request) httpResult {
// Time to finish up profiling and flush out the results to the client.
const finishProfilingBuffer = 250 * time.Millisecond
m.mu.Lock()
if m.shuttingDown {
m.mu.Unlock()
return httpResult{http.StatusServiceUnavailable, errors.New("server is shutting down already")}
}
m.mu.Unlock()
w.WriteHeader(http.StatusOK)
if err := pprof.StartCPUProfile(w); err != nil {
// We cannot return this as an error, because we've already sent the HTTP 200 OK status.
log.Warningf("Failed to start recording CPU profile: %v", err)
return httpOK
}
deadline := time.Now().Add(httpTimeout - finishProfilingBuffer)
if seconds, err := strconv.Atoi(req.URL.Query().Get("seconds")); err == nil && time.Duration(seconds)*time.Second < httpTimeout {
deadline = time.Now().Add(time.Duration(seconds) * time.Second)
} else if ctxDeadline, hasDeadline := req.Context().Deadline(); hasDeadline {
deadline = ctxDeadline.Add(-finishProfilingBuffer)
}
log.Infof("Profiling CPU until %v...", deadline)
var wasInterrupted bool
select {
case <-time.After(time.Until(deadline)):
wasInterrupted = false
case <-req.Context().Done():
wasInterrupted = true
}
pprof.StopCPUProfile()
if wasInterrupted {
log.Warningf("Profiling CPU interrupted.")
} else {
log.Infof("Profiling CPU done.")
}
return httpOK
}
// profileHeap returns a heap profile over HTTP.
func (m *metricServer) profileHeap(w http.ResponseWriter, req *http.Request) httpResult {
m.mu.Lock()
if m.shuttingDown {
m.mu.Unlock()
return httpResult{http.StatusServiceUnavailable, errors.New("server is shutting down already")}
}
m.mu.Unlock()
w.WriteHeader(http.StatusOK)
runtime.GC() // Run GC just before looking at the heap to get a clean view.
if err := pprof.Lookup("heap").WriteTo(w, 0); err != nil {
// We cannot return this as an error, because we've already sent the HTTP 200 OK status.
log.Warningf("Failed to record heap profile: %v", err)
}
return httpOK
}
// shutdownLocked shuts down the server. It assumes mu is held.
func (m *metricServer) shutdownLocked(ctx context.Context) {
log.Infof("Server shutting down.")
m.shuttingDown = true
if m.udsPath != "" {
if err := os.Remove(m.udsPath); err != nil {
log.Warningf("Cannot remove UDS at %s: %v", m.udsPath, err)
} else {
m.udsPath = ""
}
}
if m.pidFile != "" {
if err := os.Remove(m.pidFile); err != nil {
log.Warningf("Cannot remove PID file at %s: %v", m.pidFile, err)
}
}
m.srv.Shutdown(ctx)
}
// logRequest wraps an HTTP handler and adds logging to it.
func logRequest(f func(w http.ResponseWriter, req *http.Request) httpResult) func(w http.ResponseWriter, req *http.Request) {
return func(w http.ResponseWriter, req *http.Request) {
log.Infof("Request: %s %s", req.Method, req.URL.Path)
defer func() {
if r := recover(); r != nil {
log.Warningf("Request: %s %s: Panic:\n%v", req.Method, req.URL.Path, r)
}
}()
result := f(w, req)
if result.err != nil {
http.Error(w, result.err.Error(), result.code)
log.Warningf("Request: %s %s: Failed with HTTP code %d: %v", req.Method, req.URL.Path, result.code, result.err)
}
// Run GC after every request to keep memory usage as predictable and as flat as possible.
runtime.GC()
}
}
// verify is one iteration of verifyLoop.
// It runs in a loop in the background which checks all sandboxes for liveness, tries to load
// their metadata if that hasn't been loaded yet, and tries to pick up new sandboxes that
// failed to register for whatever reason.
func (m *metricServer) verify(ctx context.Context) {
_, err := container.ListSandboxes(m.rootDir)
m.mu.Lock()
defer m.mu.Unlock()
if err != nil {
if !m.allowUnknownRoot {
log.Warningf("Cannot list sandboxes in root directory %s, it has likely gone away: %v. Server shutting down.", m.rootDir, err)
m.shutdownLocked(ctx)
}
return
}
m.refreshSandboxesLocked()
}
// verifyLoop runs in the background and periodically calls verify.
func (m *metricServer) verifyLoop(ctx context.Context) {
ticker := time.NewTicker(verifyLoopInterval)
defer ticker.Stop()
for ctx.Err() == nil {
select {
case <-ctx.Done():
return
case <-m.shutdownCh:
log.Infof("Received interrupt signal, shutting down server.")
m.mu.Lock()
m.shutdownLocked(ctx)
m.mu.Unlock()
return
case <-ticker.C:
m.verify(ctx)
}
}
}
// Server is the set of options to run a metric server.
// Initialize this struct and then call Run on it to run the metric server.
type Server struct {
@@ -1081,7 +790,7 @@ func (s *Server) Run(ctx context.Context) error {
}
if _, err := container.ListSandboxes(conf.RootDir); err != nil {
if !m.allowUnknownRoot {
return fmt.Errorf("invalid root directory %q: tried to list sandboxes within it and got: %v", conf.RootDir, err)
return fmt.Errorf("invalid root directory %q: tried to list sandboxes within it and got: %w", conf.RootDir, err)
}
log.Warningf("Invalid root directory %q: tried to list sandboxes within it and got: %v. Continuing anyway, as the server is configured to tolerate this.", conf.RootDir, err)
}
@@ -1089,7 +798,7 @@ func (s *Server) Run(ctx context.Context) error {
// permission errors. Double-check by actually listing the directory.
if _, err := ioutil.ReadDir(conf.RootDir); err != nil {
if !m.allowUnknownRoot {
return fmt.Errorf("invalid root directory %q: tried to list all entries within it and got: %v", conf.RootDir, err)
return fmt.Errorf("invalid root directory %q: tried to list all entries within it and got: %w", conf.RootDir, err)
}
log.Warningf("Invalid root directory %q: tried to list all entries within it and got: %v. Continuing anyway, as the server is configured to tolerate this.", conf.RootDir, err)
}
@@ -1112,11 +821,11 @@ func (s *Server) Run(ctx context.Context) error {
if strings.HasPrefix(conf.MetricServer, fmt.Sprintf("%c", os.PathSeparator)) {
beforeBindSt, beforeBindErr := os.Stat(conf.MetricServer)
if listener, listenErr = (&net.ListenConfig{}).Listen(ctx, "unix", conf.MetricServer); listenErr != nil {
return fmt.Errorf("cannot listen on unix domain socket %q: %v", conf.MetricServer, listenErr)
return fmt.Errorf("cannot listen on unix domain socket %q: %w", conf.MetricServer, listenErr)
}
afterBindSt, afterBindErr := os.Stat(conf.MetricServer)
if afterBindErr != nil {
return fmt.Errorf("cannot stat our own unix domain socket %q: %v", conf.MetricServer, afterBindErr)
return fmt.Errorf("cannot stat our own unix domain socket %q: %w", conf.MetricServer, afterBindErr)
}
ownUDS := true
if beforeBindErr == nil && beforeBindSt.Mode() == afterBindSt.Mode() {
@@ -1144,7 +853,7 @@ func (s *Server) Run(ctx context.Context) error {
log.Warningf("Binding on all interfaces. This will allow anyone to list all containers on your machine!")
}
if listener, listenErr = (&net.ListenConfig{}).Listen(ctx, "tcp", conf.MetricServer); listenErr != nil {
return fmt.Errorf("cannot listen on TCP address %q: %v", conf.MetricServer, listenErr)
return fmt.Errorf("cannot listen on TCP address %q: %w", conf.MetricServer, listenErr)
}
}
@@ -1164,10 +873,12 @@ func (s *Server) Run(ctx context.Context) error {
m.srv.Handler = mux
m.srv.ReadTimeout = httpTimeout
m.srv.WriteTimeout = httpTimeout
go m.verifyLoop(ctx)
if err := m.startVerifyLoop(ctx); err != nil {
return fmt.Errorf("cannot start background loop: %w", err)
}
if m.pidFile != "" {
if err := ioutil.WriteFile(m.pidFile, []byte(fmt.Sprintf("%d", m.pid)), 0644); err != nil {
return fmt.Errorf("cannot write PID to file %q: %v", m.pidFile, err)
return fmt.Errorf("cannot write PID to file %q: %w", m.pidFile, err)
}
defer os.Remove(m.pidFile)
log.Infof("Wrote PID %d to file %v.", m.pid, m.pidFile)
@@ -1193,7 +904,7 @@ func (s *Server) Run(ctx context.Context) error {
if serveErr == http.ErrServerClosed {
return nil
}
return fmt.Errorf("cannot serve on address %s: %v", conf.MetricServer, serveErr)
return fmt.Errorf("cannot serve on address %s: %w", conf.MetricServer, serveErr)
}
// Per documentation, http.Server.Serve can never return a nil error, so this is not a success.
return fmt.Errorf("HTTP server Serve() did not return expected error")
+79
View File
@@ -0,0 +1,79 @@
// Copyright 2023 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 metricserver
import (
"errors"
"fmt"
"net/http"
"runtime"
"strings"
"time"
"gvisor.dev/gvisor/pkg/log"
)
// httpTimeout is the timeout used for all connect/read/write operations of the HTTP server.
const httpTimeout = 1 * time.Minute
// httpResult is returned by HTTP handlers.
type httpResult struct {
code int
err error
}
// httpOK is the "everything went fine" HTTP result.
var httpOK = httpResult{code: http.StatusOK}
// serveIndex serves the index page.
func (m *metricServer) serveIndex(w http.ResponseWriter, req *http.Request) httpResult {
if req.URL.Path != "/" {
if strings.HasPrefix(req.URL.Path, "/metrics?") {
// Prometheus's scrape_config.metrics_path takes in a query path and automatically encodes
// all special characters in it to %-form, including the "?" character.
// This can prevent use of query parameters, and we end up here instead.
// To address this, rewrite the URL to undo this transformation.
// This means requesting "/metrics%3Ffoo=bar" is rewritten to "/metrics?foo=bar".
req.URL.RawQuery = strings.TrimPrefix(req.URL.Path, "/metrics?")
req.URL.Path = "/metrics"
return m.serveMetrics(w, req)
}
return httpResult{http.StatusNotFound, errors.New("path not found")}
}
fmt.Fprintf(w, "<html><head><title>runsc metrics</title></head><body>")
fmt.Fprintf(w, "<p>You have reached the runsc metrics server page!</p>")
fmt.Fprintf(w, `<p>To see actual metric data, head over to <a href="/metrics">/metrics</a>.</p>`)
fmt.Fprintf(w, "</body></html>")
return httpOK
}
// logRequest wraps an HTTP handler and adds logging to it.
func logRequest(f func(w http.ResponseWriter, req *http.Request) httpResult) func(w http.ResponseWriter, req *http.Request) {
return func(w http.ResponseWriter, req *http.Request) {
log.Infof("Request: %s %s", req.Method, req.URL.Path)
defer func() {
if r := recover(); r != nil {
log.Warningf("Request: %s %s: Panic:\n%v", req.Method, req.URL.Path, r)
}
}()
result := f(w, req)
if result.err != nil {
http.Error(w, result.err.Error(), result.code)
log.Warningf("Request: %s %s: Failed with HTTP code %d: %v", req.Method, req.URL.Path, result.code, result.err)
}
// Run GC after every request to keep memory usage as predictable and as flat as possible.
runtime.GC()
}
}
@@ -0,0 +1,104 @@
// Copyright 2023 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.
//go:build go1.1
// +build go1.1
package metricserver
import (
"context"
"os"
"time"
"gvisor.dev/gvisor/pkg/log"
"gvisor.dev/gvisor/runsc/container"
)
// verifyLoopInterval is the interval at which we check whether there are any sandboxes we need
// to serve metrics for.
const verifyLoopInterval = 20 * time.Second
// sandboxData contains additional per-sandbox data.
type sandboxData struct{}
// load loads additional per-sandbox data.
func (s *sandboxData) load(*servedSandbox) error {
return nil
}
// serverData contains additional server-wide data.
type serverData struct{}
// verify is one iteration of verifyLoop.
// It runs in a loop in the background which checks all sandboxes for liveness, tries to load
// their metadata if that hasn't been loaded yet, and tries to pick up new sandboxes that
// failed to register for whatever reason.
func (m *metricServer) verify(ctx context.Context) {
_, err := container.ListSandboxes(m.rootDir)
m.mu.Lock()
defer m.mu.Unlock()
if err != nil {
if !m.allowUnknownRoot {
log.Warningf("Cannot list sandboxes in root directory %s, it has likely gone away: %v. Server shutting down.", m.rootDir, err)
m.shutdownLocked(ctx)
}
return
}
m.refreshSandboxesLocked()
}
// startVerifyLoop runs in the background and periodically calls verify.
func (m *metricServer) startVerifyLoop(ctx context.Context) error {
go func() {
ticker := time.NewTicker(verifyLoopInterval)
defer ticker.Stop()
for ctx.Err() == nil {
select {
case <-ctx.Done():
return
case <-m.shutdownCh:
log.Infof("Received interrupt signal, shutting down server.")
m.mu.Lock()
m.shutdownLocked(ctx)
m.mu.Unlock()
return
case <-ticker.C:
m.verify(ctx)
}
}
}()
return nil
}
// shutdownLocked shuts down the server. It assumes mu is held.
func (m *metricServer) shutdownLocked(ctx context.Context) {
log.Infof("Server shutting down.")
m.shuttingDown = true
if m.udsPath != "" {
if err := os.Remove(m.udsPath); err != nil {
log.Warningf("Cannot remove UDS at %s: %v", m.udsPath, err)
} else {
m.udsPath = ""
}
}
if m.pidFile != "" {
if err := os.Remove(m.pidFile); err != nil {
log.Warningf("Cannot remove PID file at %s: %v", m.pidFile, err)
} else {
m.pidFile = ""
}
}
m.srv.Shutdown(ctx)
}
+149
View File
@@ -0,0 +1,149 @@
// Copyright 2023 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 metricserver
import (
"crypto/sha256"
"encoding/binary"
"io"
"strconv"
"gvisor.dev/gvisor/pkg/prometheus"
"gvisor.dev/gvisor/runsc/container"
)
// Metrics generated by the metrics server itself.
var (
SandboxPresenceMetric = prometheus.Metric{
Name: "sandbox_presence",
Type: prometheus.TypeGauge,
Help: "Boolean metric set to 1 for each known sandbox.",
}
SandboxRunningMetric = prometheus.Metric{
Name: "sandbox_running",
Type: prometheus.TypeGauge,
Help: "Boolean metric set to 1 for each running sandbox.",
}
SandboxMetadataMetric = prometheus.Metric{
Name: "sandbox_metadata",
Type: prometheus.TypeGauge,
Help: "Key-value pairs about per-sandbox metadata.",
}
SandboxCapabilitiesMetric = prometheus.Metric{
Name: "sandbox_capabilities",
Type: prometheus.TypeGauge,
Help: "Linux capabilities added within containers of the sandbox.",
}
SandboxCapabilitiesMetricLabel = "capability"
SpecMetadataMetric = prometheus.Metric{
Name: "spec_metadata",
Type: prometheus.TypeGauge,
Help: "Key-value pairs about OCI spec metadata.",
}
SandboxCreationMetric = prometheus.Metric{
Name: "sandbox_creation_time_seconds",
Type: prometheus.TypeGauge,
Help: "When the sandbox was created, as a unix timestamp in seconds.",
}
NumRunningSandboxesMetric = prometheus.Metric{
Name: "num_sandboxes_running",
Type: prometheus.TypeGauge,
Help: "Number of sandboxes running at present.",
}
NumCannotExportSandboxesMetric = prometheus.Metric{
Name: "num_sandboxes_broken_metrics",
Type: prometheus.TypeGauge,
Help: "Number of sandboxes from which we cannot export metrics.",
}
NumTotalSandboxesMetric = prometheus.Metric{
Name: "num_sandboxes_total",
Type: prometheus.TypeCounter,
Help: "Counter of sandboxes that have ever been started.",
}
)
// Metrics is a list of metrics that the metric server generates.
var Metrics = []*prometheus.Metric{
&SandboxPresenceMetric,
&SandboxRunningMetric,
&SandboxMetadataMetric,
&SandboxCapabilitiesMetric,
&SpecMetadataMetric,
&SandboxCreationMetric,
&NumRunningSandboxesMetric,
&NumCannotExportSandboxesMetric,
&NumTotalSandboxesMetric,
&prometheus.ProcessStartTimeSeconds,
}
// SandboxPrometheusLabels returns a set of Prometheus labels that identifies the sandbox running
// the given root container.
func SandboxPrometheusLabels(rootContainer *container.Container) (map[string]string, error) {
s := rootContainer.Sandbox
labels := make(map[string]string, 4)
labels[prometheus.SandboxIDLabel] = s.ID
// Compute iteration ID label in a stable manner.
// This uses sha256(ID + ":" + creation time).
h := sha256.New()
if _, err := io.WriteString(h, s.ID); err != nil {
return nil, err
}
if _, err := io.WriteString(h, ":"); err != nil {
return nil, err
}
if _, err := io.WriteString(h, rootContainer.CreatedAt.UTC().String()); err != nil {
return nil, err
}
labels[prometheus.IterationIDLabel] = strconv.FormatUint(binary.BigEndian.Uint64(h.Sum(nil)[:8]), 36)
if s.PodName != "" {
labels[prometheus.PodNameLabel] = s.PodName
}
if s.Namespace != "" {
labels[prometheus.NamespaceLabel] = s.Namespace
}
return labels, nil
}
// ComputeSpecMetadata returns the labels for the `spec_metadata` metric.
// It merges data from the Specs of multiple containers running within the
// same sandbox.
// This function must support being called with `allContainers` being nil.
// It must return the same set of label keys regardless of how many containers
// are in `allContainers`.
func ComputeSpecMetadata(allContainers []*container.Container) map[string]string {
const (
unknownOCIVersion = "UNKNOWN"
inconsistentOCIVersion = "INCONSISTENT"
)
hasUID0Container := false
ociVersion := unknownOCIVersion
for _, cont := range allContainers {
if cont.RunsAsUID0() {
hasUID0Container = true
}
if ociVersion == unknownOCIVersion {
ociVersion = cont.Spec.Version
} else if ociVersion != cont.Spec.Version {
ociVersion = inconsistentOCIVersion
}
}
return map[string]string{
"hasuid0": strconv.FormatBool(hasUID0Container),
"ociversion": ociVersion,
}
}
@@ -0,0 +1,83 @@
// Copyright 2023 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 metricserver
import (
"errors"
"net/http"
"runtime"
"runtime/pprof"
"strconv"
"time"
"gvisor.dev/gvisor/pkg/log"
)
// profileCPU returns a CPU profile over HTTP.
func (m *metricServer) profileCPU(w http.ResponseWriter, req *http.Request) httpResult {
// Time to finish up profiling and flush out the results to the client.
const finishProfilingBuffer = 250 * time.Millisecond
m.mu.Lock()
if m.shuttingDown {
m.mu.Unlock()
return httpResult{http.StatusServiceUnavailable, errors.New("server is shutting down already")}
}
m.mu.Unlock()
w.WriteHeader(http.StatusOK)
if err := pprof.StartCPUProfile(w); err != nil {
// We cannot return this as an error, because we've already sent the HTTP 200 OK status.
log.Warningf("Failed to start recording CPU profile: %v", err)
return httpOK
}
deadline := time.Now().Add(httpTimeout - finishProfilingBuffer)
if seconds, err := strconv.Atoi(req.URL.Query().Get("seconds")); err == nil && time.Duration(seconds)*time.Second < httpTimeout {
deadline = time.Now().Add(time.Duration(seconds) * time.Second)
} else if ctxDeadline, hasDeadline := req.Context().Deadline(); hasDeadline {
deadline = ctxDeadline.Add(-finishProfilingBuffer)
}
log.Infof("Profiling CPU until %v...", deadline)
var wasInterrupted bool
select {
case <-time.After(time.Until(deadline)):
wasInterrupted = false
case <-req.Context().Done():
wasInterrupted = true
}
pprof.StopCPUProfile()
if wasInterrupted {
log.Warningf("Profiling CPU interrupted.")
} else {
log.Infof("Profiling CPU done.")
}
return httpOK
}
// profileHeap returns a heap profile over HTTP.
func (m *metricServer) profileHeap(w http.ResponseWriter, req *http.Request) httpResult {
m.mu.Lock()
if m.shuttingDown {
m.mu.Unlock()
return httpResult{http.StatusServiceUnavailable, errors.New("server is shutting down already")}
}
m.mu.Unlock()
w.WriteHeader(http.StatusOK)
runtime.GC() // Run GC just before looking at the heap to get a clean view.
if err := pprof.Lookup("heap").WriteTo(w, 0); err != nil {
// We cannot return this as an error, because we've already sent the HTTP 200 OK status.
log.Warningf("Failed to record heap profile: %v", err)
}
return httpOK
}