Skip to content

Commit ce1ceeb

Browse files
committed
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 <cpuguy83@gmail.com>
1 parent 40b2b4b commit ce1ceeb

File tree

8 files changed

+51
-2
lines changed

8 files changed

+51
-2
lines changed

api/server/router/container/container_routes.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,9 +105,14 @@ func (s *containerRouter) getContainersStats(ctx context.Context, w http.Respons
105105
if !stream {
106106
w.Header().Set("Content-Type", "application/json")
107107
}
108+
var oneShot bool
109+
if versions.GreaterThanOrEqualTo(httputils.VersionFromContext(ctx), "1.41") {
110+
oneShot = httputils.BoolValueOrDefault(r, "one-shot", false)
111+
}
108112

109113
config := &backend.ContainerStatsConfig{
110114
Stream: stream,
115+
OneShot: oneShot,
111116
OutStream: w,
112117
Version: httputils.VersionFromContext(ctx),
113118
}

api/swagger.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5685,6 +5685,11 @@ paths:
56855685
description: "Stream the output. If false, the stats will be output once and then it will disconnect."
56865686
type: "boolean"
56875687
default: true
5688+
- name: "one-shot"
5689+
in: "query"
5690+
description: "Only get a single stat instead of waiting for 2 cycles. Must be used with stream=false"
5691+
type: "boolean"
5692+
default: false
56885693
tags: ["Container"]
56895694
/containers/{id}/resize:
56905695
post:

api/types/backend/backend.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ type LogSelector struct {
7373
// behavior of a backend.ContainerStats() call.
7474
type ContainerStatsConfig struct {
7575
Stream bool
76+
OneShot bool
7677
OutStream io.Writer
7778
Version string
7879
}

client/container_stats.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,19 @@ func (cli *Client) ContainerStats(ctx context.Context, containerID string, strea
2424
osType := getDockerOS(resp.header.Get("Server"))
2525
return types.ContainerStats{Body: resp.body, OSType: osType}, err
2626
}
27+
28+
// ContainerStatsOneShot gets a single stat entry from a container.
29+
// It differs from `ContainerStats` in that the API should not wait to prime the stats
30+
func (cli *Client) ContainerStatsOneShot(ctx context.Context, containerID string) (types.ContainerStats, error) {
31+
query := url.Values{}
32+
query.Set("stream", "0")
33+
query.Set("one-shot", "1")
34+
35+
resp, err := cli.get(ctx, "/containers/"+containerID+"/stats", query, nil)
36+
if err != nil {
37+
return types.ContainerStats{}, err
38+
}
39+
40+
osType := getDockerOS(resp.header.Get("Server"))
41+
return types.ContainerStats{Body: resp.body, OSType: osType}, err
42+
}

client/interface.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ type ContainerAPIClient interface {
6767
ContainerRestart(ctx context.Context, container string, timeout *time.Duration) error
6868
ContainerStatPath(ctx context.Context, container, path string) (types.ContainerPathStat, error)
6969
ContainerStats(ctx context.Context, container string, stream bool) (types.ContainerStats, error)
70+
ContainerStatsOneShot(ctx context.Context, container string) (types.ContainerStats, error)
7071
ContainerStart(ctx context.Context, container string, options types.ContainerStartOptions) error
7172
ContainerStop(ctx context.Context, container string, timeout *time.Duration) error
7273
ContainerTop(ctx context.Context, container string, arguments []string) (containertypes.ContainerTopOKBody, error)

daemon/stats.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"github.com/docker/docker/api/types/versions"
1313
"github.com/docker/docker/api/types/versions/v1p20"
1414
"github.com/docker/docker/container"
15+
"github.com/docker/docker/errdefs"
1516
"github.com/docker/docker/pkg/ioutils"
1617
)
1718

@@ -30,6 +31,10 @@ func (daemon *Daemon) ContainerStats(ctx context.Context, prefixOrName string, c
3031
return err
3132
}
3233

34+
if config.Stream && config.OneShot {
35+
return errdefs.InvalidParameter(errors.New("cannot have stream=true and one-shot=true"))
36+
}
37+
3338
// If the container is either not running or restarting and requires no stream, return an empty stats.
3439
if (!container.IsRunning() || container.IsRestarting()) && !config.Stream {
3540
return json.NewEncoder(config.OutStream).Encode(&types.StatsJSON{
@@ -63,7 +68,7 @@ func (daemon *Daemon) ContainerStats(ctx context.Context, prefixOrName string, c
6368
updates := daemon.subscribeToContainerStats(container)
6469
defer daemon.unsubscribeToContainerStats(container, updates)
6570

66-
noStreamFirstFrame := true
71+
noStreamFirstFrame := !config.OneShot
6772
for {
6873
select {
6974
case v, ok := <-updates:

docs/api/version-history.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ keywords: "API, Docker, rcli, REST, documentation"
5757
service.
5858
* `GET /tasks/{id}` now includes `JobIteration` on the task if spawned from a
5959
job-mode service.
60+
* `GET /containers/{id}/stats` now accepts a query param (`one-shot`) which, when used with `stream=false` fetches a
61+
single set of stats instead of waiting for two collection cycles to have 2 CPU stats over a 1 second period.
6062

6163
## v1.40 API changes
6264

integration/container/stats_test.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"encoding/json"
66
"io"
7+
"reflect"
78
"testing"
89
"time"
910

@@ -33,10 +34,23 @@ func TestStats(t *testing.T) {
3334
assert.NilError(t, err)
3435
defer resp.Body.Close()
3536

36-
var v *types.Stats
37+
var v types.Stats
3738
err = json.NewDecoder(resp.Body).Decode(&v)
3839
assert.NilError(t, err)
3940
assert.Check(t, is.Equal(int64(v.MemoryStats.Limit), info.MemTotal))
41+
assert.Check(t, !reflect.DeepEqual(v.PreCPUStats, types.CPUStats{}))
42+
err = json.NewDecoder(resp.Body).Decode(&v)
43+
assert.Assert(t, is.ErrorContains(err, ""), io.EOF)
44+
45+
resp, err = client.ContainerStatsOneShot(ctx, cID)
46+
assert.NilError(t, err)
47+
defer resp.Body.Close()
48+
49+
v = types.Stats{}
50+
err = json.NewDecoder(resp.Body).Decode(&v)
51+
assert.NilError(t, err)
52+
assert.Check(t, is.Equal(int64(v.MemoryStats.Limit), info.MemTotal))
53+
assert.Check(t, is.DeepEqual(v.PreCPUStats, types.CPUStats{}))
4054
err = json.NewDecoder(resp.Body).Decode(&v)
4155
assert.Assert(t, is.ErrorContains(err, ""), io.EOF)
4256
}

0 commit comments

Comments
 (0)