From ce1ceeb25768cd402c662973fd1ae3caeee1ab05 Mon Sep 17 00:00:00 2001 From: Brian Goff Date: Fri, 7 Feb 2020 15:55:06 -0800 Subject: [PATCH] Add stats options to not prime the stats Metrics collectors generally don't need the daemon to prime the stats with something to compare since they already have something to compare with. Before this change, the API does 2 collection cycles (which takes roughly 2s) in order to provide comparison for CPU usage over 1s. This was primarily added so that `docker stats --no-stream` had something to compare against. Really the CLI should have just made a 2nd call and done the comparison itself rather than forcing it on all API consumers. That ship has long sailed, though. With this change, clients can set an option to just pull a single stat, which is *at least* a full second faster: Old: ``` time curl --unix-socket /go/src/github.com/docker/docker/bundles/test-integration-shell/docker.sock http://./containers/test/stats?stream=false\&one-shot=false > /dev/null 2>&1 real0m1.864s user0m0.005s sys0m0.007s time curl --unix-socket /go/src/github.com/docker/docker/bundles/test-integration-shell/docker.sock http://./containers/test/stats?stream=false\&one-shot=false > /dev/null 2>&1 real0m1.173s user0m0.010s sys0m0.006s ``` New: ``` time curl --unix-socket /go/src/github.com/docker/docker/bundles/test-integration-shell/docker.sock http://./containers/test/stats?stream=false\&one-shot=true > /dev/null 2>&1 real0m0.680s user0m0.008s sys0m0.004s time curl --unix-socket /go/src/github.com/docker/docker/bundles/test-integration-shell/docker.sock http://./containers/test/stats?stream=false\&one-shot=true > /dev/null 2>&1 real0m0.156s user0m0.007s sys0m0.007s ``` This fixes issues with downstreams ability to use the stats API to collect metrics. Signed-off-by: Brian Goff --- api/server/router/container/container_routes.go | 5 +++++ api/swagger.yaml | 5 +++++ api/types/backend/backend.go | 1 + client/container_stats.go | 16 ++++++++++++++++ client/interface.go | 1 + daemon/stats.go | 7 ++++++- docs/api/version-history.md | 2 ++ integration/container/stats_test.go | 16 +++++++++++++++- 8 files changed, 51 insertions(+), 2 deletions(-) diff --git a/api/server/router/container/container_routes.go b/api/server/router/container/container_routes.go index b41e540ad0..c1db782db8 100644 --- a/api/server/router/container/container_routes.go +++ b/api/server/router/container/container_routes.go @@ -105,9 +105,14 @@ func (s *containerRouter) getContainersStats(ctx context.Context, w http.Respons if !stream { w.Header().Set("Content-Type", "application/json") } + var oneShot bool + if versions.GreaterThanOrEqualTo(httputils.VersionFromContext(ctx), "1.41") { + oneShot = httputils.BoolValueOrDefault(r, "one-shot", false) + } config := &backend.ContainerStatsConfig{ Stream: stream, + OneShot: oneShot, OutStream: w, Version: httputils.VersionFromContext(ctx), } diff --git a/api/swagger.yaml b/api/swagger.yaml index 6933fb1675..2ae9afea29 100644 --- a/api/swagger.yaml +++ b/api/swagger.yaml @@ -5685,6 +5685,11 @@ paths: description: "Stream the output. If false, the stats will be output once and then it will disconnect." type: "boolean" default: true + - name: "one-shot" + in: "query" + description: "Only get a single stat instead of waiting for 2 cycles. Must be used with stream=false" + type: "boolean" + default: false tags: ["Container"] /containers/{id}/resize: post: diff --git a/api/types/backend/backend.go b/api/types/backend/backend.go index 6afdd16dfc..9880c632bd 100644 --- a/api/types/backend/backend.go +++ b/api/types/backend/backend.go @@ -73,6 +73,7 @@ type LogSelector struct { // behavior of a backend.ContainerStats() call. type ContainerStatsConfig struct { Stream bool + OneShot bool OutStream io.Writer Version string } diff --git a/client/container_stats.go b/client/container_stats.go index 6ef44c7748..0a6488dde8 100644 --- a/client/container_stats.go +++ b/client/container_stats.go @@ -24,3 +24,19 @@ func (cli *Client) ContainerStats(ctx context.Context, containerID string, strea osType := getDockerOS(resp.header.Get("Server")) return types.ContainerStats{Body: resp.body, OSType: osType}, err } + +// ContainerStatsOneShot gets a single stat entry from a container. +// It differs from `ContainerStats` in that the API should not wait to prime the stats +func (cli *Client) ContainerStatsOneShot(ctx context.Context, containerID string) (types.ContainerStats, error) { + query := url.Values{} + query.Set("stream", "0") + query.Set("one-shot", "1") + + resp, err := cli.get(ctx, "/containers/"+containerID+"/stats", query, nil) + if err != nil { + return types.ContainerStats{}, err + } + + osType := getDockerOS(resp.header.Get("Server")) + return types.ContainerStats{Body: resp.body, OSType: osType}, err +} diff --git a/client/interface.go b/client/interface.go index cde64be4b5..4f9fd67354 100644 --- a/client/interface.go +++ b/client/interface.go @@ -67,6 +67,7 @@ type ContainerAPIClient interface { ContainerRestart(ctx context.Context, container string, timeout *time.Duration) error ContainerStatPath(ctx context.Context, container, path string) (types.ContainerPathStat, error) ContainerStats(ctx context.Context, container string, stream bool) (types.ContainerStats, error) + ContainerStatsOneShot(ctx context.Context, container string) (types.ContainerStats, error) ContainerStart(ctx context.Context, container string, options types.ContainerStartOptions) error ContainerStop(ctx context.Context, container string, timeout *time.Duration) error ContainerTop(ctx context.Context, container string, arguments []string) (containertypes.ContainerTopOKBody, error) diff --git a/daemon/stats.go b/daemon/stats.go index 006d2223b2..97910dab0b 100644 --- a/daemon/stats.go +++ b/daemon/stats.go @@ -12,6 +12,7 @@ import ( "github.com/docker/docker/api/types/versions" "github.com/docker/docker/api/types/versions/v1p20" "github.com/docker/docker/container" + "github.com/docker/docker/errdefs" "github.com/docker/docker/pkg/ioutils" ) @@ -30,6 +31,10 @@ func (daemon *Daemon) ContainerStats(ctx context.Context, prefixOrName string, c return err } + if config.Stream && config.OneShot { + return errdefs.InvalidParameter(errors.New("cannot have stream=true and one-shot=true")) + } + // If the container is either not running or restarting and requires no stream, return an empty stats. if (!container.IsRunning() || container.IsRestarting()) && !config.Stream { return json.NewEncoder(config.OutStream).Encode(&types.StatsJSON{ @@ -63,7 +68,7 @@ func (daemon *Daemon) ContainerStats(ctx context.Context, prefixOrName string, c updates := daemon.subscribeToContainerStats(container) defer daemon.unsubscribeToContainerStats(container, updates) - noStreamFirstFrame := true + noStreamFirstFrame := !config.OneShot for { select { case v, ok := <-updates: diff --git a/docs/api/version-history.md b/docs/api/version-history.md index 8366ac866f..8a11e4d0e2 100644 --- a/docs/api/version-history.md +++ b/docs/api/version-history.md @@ -57,6 +57,8 @@ keywords: "API, Docker, rcli, REST, documentation" service. * `GET /tasks/{id}` now includes `JobIteration` on the task if spawned from a job-mode service. +* `GET /containers/{id}/stats` now accepts a query param (`one-shot`) which, when used with `stream=false` fetches a + single set of stats instead of waiting for two collection cycles to have 2 CPU stats over a 1 second period. ## v1.40 API changes diff --git a/integration/container/stats_test.go b/integration/container/stats_test.go index a33f8f809c..b4b7aa3b34 100644 --- a/integration/container/stats_test.go +++ b/integration/container/stats_test.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "io" + "reflect" "testing" "time" @@ -33,10 +34,23 @@ func TestStats(t *testing.T) { assert.NilError(t, err) defer resp.Body.Close() - var v *types.Stats + var v types.Stats err = json.NewDecoder(resp.Body).Decode(&v) assert.NilError(t, err) assert.Check(t, is.Equal(int64(v.MemoryStats.Limit), info.MemTotal)) + assert.Check(t, !reflect.DeepEqual(v.PreCPUStats, types.CPUStats{})) + err = json.NewDecoder(resp.Body).Decode(&v) + assert.Assert(t, is.ErrorContains(err, ""), io.EOF) + + resp, err = client.ContainerStatsOneShot(ctx, cID) + assert.NilError(t, err) + defer resp.Body.Close() + + v = types.Stats{} + err = json.NewDecoder(resp.Body).Decode(&v) + assert.NilError(t, err) + assert.Check(t, is.Equal(int64(v.MemoryStats.Limit), info.MemTotal)) + assert.Check(t, is.DeepEqual(v.PreCPUStats, types.CPUStats{})) err = json.NewDecoder(resp.Body).Decode(&v) assert.Assert(t, is.ErrorContains(err, ""), io.EOF) }