Browse code

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>

Brian Goff authored on 2020/02/08 08:55:06
Showing 8 changed files
... ...
@@ -105,9 +105,14 @@ func (s *containerRouter) getContainersStats(ctx context.Context, w http.Respons
105 105
 	if !stream {
106 106
 		w.Header().Set("Content-Type", "application/json")
107 107
 	}
108
+	var oneShot bool
109
+	if versions.GreaterThanOrEqualTo(httputils.VersionFromContext(ctx), "1.41") {
110
+		oneShot = httputils.BoolValueOrDefault(r, "one-shot", false)
111
+	}
108 112
 
109 113
 	config := &backend.ContainerStatsConfig{
110 114
 		Stream:    stream,
115
+		OneShot:   oneShot,
111 116
 		OutStream: w,
112 117
 		Version:   httputils.VersionFromContext(ctx),
113 118
 	}
... ...
@@ -5685,6 +5685,11 @@ paths:
5685 5685
           description: "Stream the output. If false, the stats will be output once and then it will disconnect."
5686 5686
           type: "boolean"
5687 5687
           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
5688 5693
       tags: ["Container"]
5689 5694
   /containers/{id}/resize:
5690 5695
     post:
... ...
@@ -73,6 +73,7 @@ type LogSelector struct {
73 73
 // behavior of a backend.ContainerStats() call.
74 74
 type ContainerStatsConfig struct {
75 75
 	Stream    bool
76
+	OneShot   bool
76 77
 	OutStream io.Writer
77 78
 	Version   string
78 79
 }
... ...
@@ -24,3 +24,19 @@ func (cli *Client) ContainerStats(ctx context.Context, containerID string, strea
24 24
 	osType := getDockerOS(resp.header.Get("Server"))
25 25
 	return types.ContainerStats{Body: resp.body, OSType: osType}, err
26 26
 }
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
+}
... ...
@@ -67,6 +67,7 @@ type ContainerAPIClient interface {
67 67
 	ContainerRestart(ctx context.Context, container string, timeout *time.Duration) error
68 68
 	ContainerStatPath(ctx context.Context, container, path string) (types.ContainerPathStat, error)
69 69
 	ContainerStats(ctx context.Context, container string, stream bool) (types.ContainerStats, error)
70
+	ContainerStatsOneShot(ctx context.Context, container string) (types.ContainerStats, error)
70 71
 	ContainerStart(ctx context.Context, container string, options types.ContainerStartOptions) error
71 72
 	ContainerStop(ctx context.Context, container string, timeout *time.Duration) error
72 73
 	ContainerTop(ctx context.Context, container string, arguments []string) (containertypes.ContainerTopOKBody, error)
... ...
@@ -12,6 +12,7 @@ import (
12 12
 	"github.com/docker/docker/api/types/versions"
13 13
 	"github.com/docker/docker/api/types/versions/v1p20"
14 14
 	"github.com/docker/docker/container"
15
+	"github.com/docker/docker/errdefs"
15 16
 	"github.com/docker/docker/pkg/ioutils"
16 17
 )
17 18
 
... ...
@@ -30,6 +31,10 @@ func (daemon *Daemon) ContainerStats(ctx context.Context, prefixOrName string, c
30 30
 		return err
31 31
 	}
32 32
 
33
+	if config.Stream && config.OneShot {
34
+		return errdefs.InvalidParameter(errors.New("cannot have stream=true and one-shot=true"))
35
+	}
36
+
33 37
 	// If the container is either not running or restarting and requires no stream, return an empty stats.
34 38
 	if (!container.IsRunning() || container.IsRestarting()) && !config.Stream {
35 39
 		return json.NewEncoder(config.OutStream).Encode(&types.StatsJSON{
... ...
@@ -63,7 +68,7 @@ func (daemon *Daemon) ContainerStats(ctx context.Context, prefixOrName string, c
63 63
 	updates := daemon.subscribeToContainerStats(container)
64 64
 	defer daemon.unsubscribeToContainerStats(container, updates)
65 65
 
66
-	noStreamFirstFrame := true
66
+	noStreamFirstFrame := !config.OneShot
67 67
 	for {
68 68
 		select {
69 69
 		case v, ok := <-updates:
... ...
@@ -57,6 +57,8 @@ keywords: "API, Docker, rcli, REST, documentation"
57 57
   service.
58 58
 * `GET /tasks/{id}` now includes `JobIteration` on the task if spawned from a
59 59
   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.
60 62
 
61 63
 ## v1.40 API changes
62 64
 
... ...
@@ -4,6 +4,7 @@ import (
4 4
 	"context"
5 5
 	"encoding/json"
6 6
 	"io"
7
+	"reflect"
7 8
 	"testing"
8 9
 	"time"
9 10
 
... ...
@@ -33,10 +34,23 @@ func TestStats(t *testing.T) {
33 33
 	assert.NilError(t, err)
34 34
 	defer resp.Body.Close()
35 35
 
36
-	var v *types.Stats
36
+	var v types.Stats
37 37
 	err = json.NewDecoder(resp.Body).Decode(&v)
38 38
 	assert.NilError(t, err)
39 39
 	assert.Check(t, is.Equal(int64(v.MemoryStats.Limit), info.MemTotal))
40
+	assert.Check(t, !reflect.DeepEqual(v.PreCPUStats, types.CPUStats{}))
41
+	err = json.NewDecoder(resp.Body).Decode(&v)
42
+	assert.Assert(t, is.ErrorContains(err, ""), io.EOF)
43
+
44
+	resp, err = client.ContainerStatsOneShot(ctx, cID)
45
+	assert.NilError(t, err)
46
+	defer resp.Body.Close()
47
+
48
+	v = types.Stats{}
49
+	err = json.NewDecoder(resp.Body).Decode(&v)
50
+	assert.NilError(t, err)
51
+	assert.Check(t, is.Equal(int64(v.MemoryStats.Limit), info.MemTotal))
52
+	assert.Check(t, is.DeepEqual(v.PreCPUStats, types.CPUStats{}))
40 53
 	err = json.NewDecoder(resp.Body).Decode(&v)
41 54
 	assert.Assert(t, is.ErrorContains(err, ""), io.EOF)
42 55
 }