mirror of
https://github.com/moby/moby.git
synced 2026-01-11 18:51:37 +00:00
Improve performance of function daemon.Containers() (used by docker ps) to mitigate a latency increase when running large number of containers using the containerd image store. We do this by refactoring daemon.Containers() to collect info for containers in parallel, rather than sequentially, using up to log2(N) worker threads. This improves the performance from O(N) to O(log2(N)), where N is the number of containers. To verify correctness, this commits adds unit and integration tests. Signed-off-by: Cesar Talledo <cesar.talledo@docker.com>
229 lines
6.5 KiB
Go
229 lines
6.5 KiB
Go
package daemon
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
containertypes "github.com/docker/docker/api/types/container"
|
|
"github.com/docker/docker/api/types/filters"
|
|
"github.com/docker/docker/container"
|
|
"github.com/docker/docker/image"
|
|
"github.com/google/uuid"
|
|
"github.com/opencontainers/go-digest"
|
|
"gotest.tools/v3/assert"
|
|
is "gotest.tools/v3/assert/cmp"
|
|
)
|
|
|
|
var root string
|
|
|
|
func TestMain(m *testing.M) {
|
|
var err error
|
|
root, err = os.MkdirTemp("", "docker-container-test-")
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
defer os.RemoveAll(root)
|
|
os.Exit(m.Run())
|
|
}
|
|
|
|
// This sets up a container with a name so that name filters
|
|
// work against it. It takes in a pointer to Daemon so that
|
|
// minor operations are not repeated by the caller
|
|
func setupContainerWithName(t *testing.T, name string, daemon *Daemon) *container.Container {
|
|
t.Helper()
|
|
var (
|
|
id = uuid.New().String()
|
|
computedImageID = image.ID(digest.FromString(id))
|
|
cRoot = filepath.Join(root, id)
|
|
)
|
|
if err := os.MkdirAll(cRoot, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
c := container.NewBaseContainer(id, cRoot)
|
|
// these are for passing includeContainerInList
|
|
if name[0] != '/' {
|
|
name = "/" + name
|
|
}
|
|
c.Name = name
|
|
c.Running = true
|
|
c.HostConfig = &containertypes.HostConfig{}
|
|
c.Created = time.Now()
|
|
|
|
// these are for passing the refreshImage reducer
|
|
c.ImageID = computedImageID
|
|
c.Config = &containertypes.Config{
|
|
Image: computedImageID.String(),
|
|
}
|
|
|
|
// this is done here to avoid requiring these
|
|
// operations n x number of containers in the
|
|
// calling function
|
|
daemon.containersReplica.Save(c)
|
|
daemon.reserveName(id, name)
|
|
|
|
return c
|
|
}
|
|
|
|
func containerListContainsName(containers []*containertypes.Summary, name string) bool {
|
|
for _, ctr := range containers {
|
|
for _, containerName := range ctr.Names {
|
|
if containerName == name {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func TestContainerList(t *testing.T) {
|
|
db, err := container.NewViewDB()
|
|
assert.NilError(t, err)
|
|
d := &Daemon{
|
|
containersReplica: db,
|
|
}
|
|
|
|
// test list with different number of containers
|
|
for _, num := range []int{0, 1, 2, 4, 8, 16, 32, 64, 100} {
|
|
t.Run(fmt.Sprintf("%d containers", num), func(t *testing.T) {
|
|
db, err := container.NewViewDB() // new DB to ignore prior containers
|
|
assert.NilError(t, err)
|
|
d = &Daemon{
|
|
containersReplica: db,
|
|
}
|
|
|
|
// create the containers
|
|
containers := make([]*container.Container, num)
|
|
for i := range num {
|
|
name := fmt.Sprintf("cont-%d", i)
|
|
containers[i] = setupContainerWithName(t, name, d)
|
|
// ensure container timestamps are separated enough so the
|
|
// sort used by d.Containers() can deterministically sort them.
|
|
if i > 0 {
|
|
containers[i].Created = containers[i-1].Created.Add(time.Millisecond)
|
|
}
|
|
}
|
|
|
|
// list them and verify correctness
|
|
containerList, err := d.Containers(context.Background(), &containertypes.ListOptions{All: true})
|
|
assert.NilError(t, err)
|
|
assert.Assert(t, is.Len(containerList, num))
|
|
|
|
for i := range num {
|
|
// container list should be ordered in descending creation order
|
|
assert.Assert(t, is.Equal(containerList[i].Names[0], containers[num-1-i].Name))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestContainerList_InvalidFilter(t *testing.T) {
|
|
db, err := container.NewViewDB()
|
|
assert.NilError(t, err)
|
|
d := &Daemon{
|
|
containersReplica: db,
|
|
}
|
|
|
|
_, err = d.Containers(context.Background(), &containertypes.ListOptions{
|
|
Filters: filters.NewArgs(filters.Arg("invalid", "foo")),
|
|
})
|
|
assert.Assert(t, is.Error(err, "invalid filter 'invalid'"))
|
|
}
|
|
|
|
func TestContainerList_NameFilter(t *testing.T) {
|
|
db, err := container.NewViewDB()
|
|
assert.NilError(t, err)
|
|
d := &Daemon{
|
|
containersReplica: db,
|
|
}
|
|
|
|
var (
|
|
one = setupContainerWithName(t, "a1", d)
|
|
two = setupContainerWithName(t, "a2", d)
|
|
three = setupContainerWithName(t, "b1", d)
|
|
)
|
|
|
|
// moby/moby #37453 - ^ regex not working due to prefix slash
|
|
// not being stripped
|
|
containerList, err := d.Containers(context.Background(), &containertypes.ListOptions{
|
|
Filters: filters.NewArgs(filters.Arg("name", "^a")),
|
|
})
|
|
assert.NilError(t, err)
|
|
assert.Assert(t, is.Len(containerList, 2))
|
|
assert.Assert(t, containerListContainsName(containerList, one.Name))
|
|
assert.Assert(t, containerListContainsName(containerList, two.Name))
|
|
|
|
// Same as above but with slash prefix should produce the same result
|
|
containerListWithPrefix, err := d.Containers(context.Background(), &containertypes.ListOptions{
|
|
Filters: filters.NewArgs(filters.Arg("name", "^/a")),
|
|
})
|
|
assert.NilError(t, err)
|
|
assert.Assert(t, is.Len(containerListWithPrefix, 2))
|
|
assert.Assert(t, containerListContainsName(containerListWithPrefix, one.Name))
|
|
assert.Assert(t, containerListContainsName(containerListWithPrefix, two.Name))
|
|
|
|
// Same as above but make sure it works for exact names
|
|
containerList, err = d.Containers(context.Background(), &containertypes.ListOptions{
|
|
Filters: filters.NewArgs(filters.Arg("name", "b1")),
|
|
})
|
|
assert.NilError(t, err)
|
|
assert.Assert(t, is.Len(containerList, 1))
|
|
assert.Assert(t, containerListContainsName(containerList, three.Name))
|
|
|
|
// Same as above but with slash prefix should produce the same result
|
|
containerListWithPrefix, err = d.Containers(context.Background(), &containertypes.ListOptions{
|
|
Filters: filters.NewArgs(filters.Arg("name", "/b1")),
|
|
})
|
|
assert.NilError(t, err)
|
|
assert.Assert(t, is.Len(containerListWithPrefix, 1))
|
|
assert.Assert(t, containerListContainsName(containerListWithPrefix, three.Name))
|
|
}
|
|
|
|
func TestContainerList_LimitFilter(t *testing.T) {
|
|
db, err := container.NewViewDB()
|
|
assert.NilError(t, err)
|
|
d := &Daemon{
|
|
containersReplica: db,
|
|
}
|
|
|
|
// start containers
|
|
num := 32
|
|
for i := range num {
|
|
name := fmt.Sprintf("cont-%d", i)
|
|
setupContainerWithName(t, name, d)
|
|
}
|
|
|
|
containers, err := db.Snapshot().All()
|
|
assert.NilError(t, err)
|
|
assert.Assert(t, is.Len(containers, num))
|
|
|
|
tests := []struct {
|
|
limit int
|
|
doc string
|
|
}{
|
|
{limit: 0, doc: "no limit"},
|
|
{limit: -1, doc: "negative limit doesn't limit"},
|
|
{limit: 1, doc: "limit 1 container"},
|
|
{limit: 20, doc: "limit less than num containers"},
|
|
{limit: 32, doc: "limit equal num containers"},
|
|
{limit: 40, doc: "limit greater than num containers"},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
t.Run(tc.doc, func(t *testing.T) {
|
|
containerList, err := d.Containers(context.Background(), &containertypes.ListOptions{Limit: tc.limit})
|
|
assert.NilError(t, err)
|
|
expectedListLen := num
|
|
if tc.limit > 0 {
|
|
expectedListLen = min(num, tc.limit)
|
|
}
|
|
assert.Assert(t, is.Len(containerList, expectedListLen))
|
|
})
|
|
}
|
|
}
|