瀏覽代碼

Merge pull request #40478 from cpuguy83/dont-prime-the-stats

Add stats options to not prime the stats
Sebastiaan van Stijn 5 年之前
父節點
當前提交
54d88a7cd3

+ 5 - 0
api/server/router/container/container_routes.go

@@ -105,9 +105,14 @@ func (s *containerRouter) getContainersStats(ctx context.Context, w http.Respons
 	if !stream {
 	if !stream {
 		w.Header().Set("Content-Type", "application/json")
 		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{
 	config := &backend.ContainerStatsConfig{
 		Stream:    stream,
 		Stream:    stream,
+		OneShot:   oneShot,
 		OutStream: w,
 		OutStream: w,
 		Version:   httputils.VersionFromContext(ctx),
 		Version:   httputils.VersionFromContext(ctx),
 	}
 	}

+ 5 - 0
api/swagger.yaml

@@ -5691,6 +5691,11 @@ paths:
           description: "Stream the output. If false, the stats will be output once and then it will disconnect."
           description: "Stream the output. If false, the stats will be output once and then it will disconnect."
           type: "boolean"
           type: "boolean"
           default: true
           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"]
       tags: ["Container"]
   /containers/{id}/resize:
   /containers/{id}/resize:
     post:
     post:

+ 1 - 0
api/types/backend/backend.go

@@ -73,6 +73,7 @@ type LogSelector struct {
 // behavior of a backend.ContainerStats() call.
 // behavior of a backend.ContainerStats() call.
 type ContainerStatsConfig struct {
 type ContainerStatsConfig struct {
 	Stream    bool
 	Stream    bool
+	OneShot   bool
 	OutStream io.Writer
 	OutStream io.Writer
 	Version   string
 	Version   string
 }
 }

+ 16 - 0
client/container_stats.go

@@ -24,3 +24,19 @@ func (cli *Client) ContainerStats(ctx context.Context, containerID string, strea
 	osType := getDockerOS(resp.header.Get("Server"))
 	osType := getDockerOS(resp.header.Get("Server"))
 	return types.ContainerStats{Body: resp.body, OSType: osType}, err
 	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
+}

+ 1 - 0
client/interface.go

@@ -67,6 +67,7 @@ type ContainerAPIClient interface {
 	ContainerRestart(ctx context.Context, container string, timeout *time.Duration) error
 	ContainerRestart(ctx context.Context, container string, timeout *time.Duration) error
 	ContainerStatPath(ctx context.Context, container, path string) (types.ContainerPathStat, error)
 	ContainerStatPath(ctx context.Context, container, path string) (types.ContainerPathStat, error)
 	ContainerStats(ctx context.Context, container string, stream bool) (types.ContainerStats, 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
 	ContainerStart(ctx context.Context, container string, options types.ContainerStartOptions) error
 	ContainerStop(ctx context.Context, container string, timeout *time.Duration) error
 	ContainerStop(ctx context.Context, container string, timeout *time.Duration) error
 	ContainerTop(ctx context.Context, container string, arguments []string) (containertypes.ContainerTopOKBody, error)
 	ContainerTop(ctx context.Context, container string, arguments []string) (containertypes.ContainerTopOKBody, error)

+ 6 - 1
daemon/stats.go

@@ -12,6 +12,7 @@ import (
 	"github.com/docker/docker/api/types/versions"
 	"github.com/docker/docker/api/types/versions"
 	"github.com/docker/docker/api/types/versions/v1p20"
 	"github.com/docker/docker/api/types/versions/v1p20"
 	"github.com/docker/docker/container"
 	"github.com/docker/docker/container"
+	"github.com/docker/docker/errdefs"
 	"github.com/docker/docker/pkg/ioutils"
 	"github.com/docker/docker/pkg/ioutils"
 )
 )
 
 
@@ -30,6 +31,10 @@ func (daemon *Daemon) ContainerStats(ctx context.Context, prefixOrName string, c
 		return err
 		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 the container is either not running or restarting and requires no stream, return an empty stats.
 	if (!ctr.IsRunning() || ctr.IsRestarting()) && !config.Stream {
 	if (!ctr.IsRunning() || ctr.IsRestarting()) && !config.Stream {
 		return json.NewEncoder(config.OutStream).Encode(&types.StatsJSON{
 		return json.NewEncoder(config.OutStream).Encode(&types.StatsJSON{
@@ -64,7 +69,7 @@ func (daemon *Daemon) ContainerStats(ctx context.Context, prefixOrName string, c
 	updates := daemon.subscribeToContainerStats(ctr)
 	updates := daemon.subscribeToContainerStats(ctr)
 	defer daemon.unsubscribeToContainerStats(ctr, updates)
 	defer daemon.unsubscribeToContainerStats(ctr, updates)
 
 
-	noStreamFirstFrame := true
+	noStreamFirstFrame := !config.OneShot
 	for {
 	for {
 		select {
 		select {
 		case v, ok := <-updates:
 		case v, ok := <-updates:

+ 2 - 0
docs/api/version-history.md

@@ -61,6 +61,8 @@ keywords: "API, Docker, rcli, REST, documentation"
   service.
   service.
 * `GET /tasks/{id}` now includes `JobIteration` on the task if spawned from a
 * `GET /tasks/{id}` now includes `JobIteration` on the task if spawned from a
   job-mode service.
   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
 ## v1.40 API changes
 
 

+ 15 - 1
integration/container/stats_test.go

@@ -4,6 +4,7 @@ import (
 	"context"
 	"context"
 	"encoding/json"
 	"encoding/json"
 	"io"
 	"io"
+	"reflect"
 	"testing"
 	"testing"
 	"time"
 	"time"
 
 
@@ -34,10 +35,23 @@ func TestStats(t *testing.T) {
 	assert.NilError(t, err)
 	assert.NilError(t, err)
 	defer resp.Body.Close()
 	defer resp.Body.Close()
 
 
-	var v *types.Stats
+	var v types.Stats
 	err = json.NewDecoder(resp.Body).Decode(&v)
 	err = json.NewDecoder(resp.Body).Decode(&v)
 	assert.NilError(t, err)
 	assert.NilError(t, err)
 	assert.Check(t, is.Equal(int64(v.MemoryStats.Limit), info.MemTotal))
 	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)
 	err = json.NewDecoder(resp.Body).Decode(&v)
 	assert.Assert(t, is.ErrorContains(err, ""), io.EOF)
 	assert.Assert(t, is.ErrorContains(err, ""), io.EOF)
 }
 }