From 97334b0193a6b7f85d4765a4c1bf0611465fac56 Mon Sep 17 00:00:00 2001 From: Andrea Luzzardi Date: Tue, 10 Feb 2015 11:09:45 -0800 Subject: [PATCH] Dependency Filter: co-schedule dependent containers on the same node. Supported dependencies: - Shared volumes: `--volumes-from=dependency` - Links: `--link=dependency:alias` - Shared network stack: `--net=container:dependency` Fixes #251 Signed-off-by: Andrea Luzzardi --- flags.go | 4 +- scheduler/filter/README.md | 19 +++ scheduler/filter/dependency.go | 71 ++++++++++ scheduler/filter/dependency_test.go | 202 ++++++++++++++++++++++++++++ scheduler/filter/filter.go | 1 + 5 files changed, 295 insertions(+), 2 deletions(-) create mode 100644 scheduler/filter/dependency.go create mode 100644 scheduler/filter/dependency_test.go diff --git a/flags.go b/flags.go index 0a9df506dc..2ce6272914 100644 --- a/flags.go +++ b/flags.go @@ -86,12 +86,12 @@ var ( } // hack for go vet - flFilterValue = cli.StringSlice([]string{"constraint", "affinity", "health", "port"}) + flFilterValue = cli.StringSlice([]string{"constraint", "affinity", "health", "port", "dependency"}) DEFAULT_FILTER_NUMBER = len(flFilterValue) flFilter = cli.StringSliceFlag{ Name: "filter, f", - Usage: "filter to use [constraint, affinity, health, port]", + Usage: "filter to use [constraint, affinity, health, port, dependency]", Value: &flFilterValue, } ) diff --git a/scheduler/filter/README.md b/scheduler/filter/README.md index 6a51cfda69..37f454101a 100644 --- a/scheduler/filter/README.md +++ b/scheduler/filter/README.md @@ -236,6 +236,25 @@ $ docker run -d -p 80:80 nginx 2014/10/29 00:33:20 Error response from daemon: no resources available to schedule container ``` +## Dependency Filter + +This filter co-schedules dependent containers on the same node. + +Currently, dependencies are declared as follows: + +- Shared volumes: `--volumes-from=dependency` +- Links: `--link=dependency:alias` +- Shared network stack: `--net=container:dependency` + +Swarm will attempt to co-locate the dependent container on the same node. If it +cannot be done (because the dependent container doesn't exist, or because the +node doesn't have enough resources), it will prevent the container creation. + +The combination of multiple dependencies will be honored if possible. For +instance, `--volumes-from=A --net=container:B` will attempt to co-locate the +container on the same node as `A` and `B`. If those containers are running on +different nodes, Swarm will prevent you from scheduling the container. + ## Health Filter This filter will prevent scheduling containers on unhealthy nodes. diff --git a/scheduler/filter/dependency.go b/scheduler/filter/dependency.go new file mode 100644 index 0000000000..df3dff1f3d --- /dev/null +++ b/scheduler/filter/dependency.go @@ -0,0 +1,71 @@ +package filter + +import ( + "fmt" + "strings" + + "github.com/docker/swarm/cluster" + "github.com/samalba/dockerclient" +) + +// DependencyFilter co-schedules dependent containers on the same node. +type DependencyFilter struct { +} + +func (f *DependencyFilter) Filter(config *dockerclient.ContainerConfig, nodes []*cluster.Node) ([]*cluster.Node, error) { + if len(nodes) == 0 { + return nodes, nil + } + + // Extract containers from links. + links := []string{} + for _, link := range config.HostConfig.Links { + links = append(links, strings.SplitN(link, ":", 2)[0]) + } + + // Check if --net points to a container. + net := []string{} + if strings.HasPrefix(config.HostConfig.NetworkMode, "container:") { + net = append(net, strings.TrimPrefix(config.HostConfig.NetworkMode, "container:")) + } + + candidates := []*cluster.Node{} + for _, node := range nodes { + if f.check(config.HostConfig.VolumesFrom, node) && + f.check(links, node) && + f.check(net, node) { + candidates = append(candidates, node) + } + } + + if len(candidates) == 0 { + return nil, fmt.Errorf("Unable to find a node fulfilling all dependencies: %s", f.String(config)) + } + + return candidates, nil +} + +// Get a string representation of the dependencies found in the container config. +func (f *DependencyFilter) String(config *dockerclient.ContainerConfig) string { + dependencies := []string{} + for _, volume := range config.HostConfig.VolumesFrom { + dependencies = append(dependencies, fmt.Sprintf("--volumes-from=%s", volume)) + } + for _, link := range config.HostConfig.Links { + dependencies = append(dependencies, fmt.Sprintf("--link=%s", link)) + } + if strings.HasPrefix(config.HostConfig.NetworkMode, "container:") { + dependencies = append(dependencies, fmt.Sprintf("--net=%s", config.HostConfig.NetworkMode)) + } + return strings.Join(dependencies, " ") +} + +// Ensure that the node contains all dependent containers. +func (f *DependencyFilter) check(dependencies []string, node *cluster.Node) bool { + for _, dependency := range dependencies { + if node.Container(dependency) == nil { + return false + } + } + return true +} diff --git a/scheduler/filter/dependency_test.go b/scheduler/filter/dependency_test.go new file mode 100644 index 0000000000..e6843b84b7 --- /dev/null +++ b/scheduler/filter/dependency_test.go @@ -0,0 +1,202 @@ +package filter + +import ( + "testing" + + "github.com/docker/swarm/cluster" + "github.com/samalba/dockerclient" + "github.com/stretchr/testify/assert" +) + +func TestDependencyFilterSimple(t *testing.T) { + var ( + f = DependencyFilter{} + nodes = []*cluster.Node{ + cluster.NewNode("node-1", 0), + cluster.NewNode("node-2", 0), + cluster.NewNode("node-3", 0), + } + result []*cluster.Node + err error + container *cluster.Container + config *dockerclient.ContainerConfig + ) + + container = &cluster.Container{Container: dockerclient.Container{Id: "c0"}} + assert.NoError(t, nodes[0].AddContainer(container)) + + container = &cluster.Container{Container: dockerclient.Container{Id: "c1"}} + assert.NoError(t, nodes[1].AddContainer(container)) + + container = &cluster.Container{Container: dockerclient.Container{Id: "c2"}} + assert.NoError(t, nodes[2].AddContainer(container)) + + // No dependencies - make sure we don't filter anything out. + config = &dockerclient.ContainerConfig{} + result, err = f.Filter(config, nodes) + assert.NoError(t, err) + assert.Equal(t, result, nodes) + + // volumes-from. + config = &dockerclient.ContainerConfig{ + HostConfig: dockerclient.HostConfig{ + VolumesFrom: []string{"c0"}, + }, + } + result, err = f.Filter(config, nodes) + assert.NoError(t, err) + assert.Len(t, result, 1) + assert.Equal(t, result[0], nodes[0]) + + // link. + config = &dockerclient.ContainerConfig{ + HostConfig: dockerclient.HostConfig{ + Links: []string{"c1:foobar"}, + }, + } + result, err = f.Filter(config, nodes) + assert.NoError(t, err) + assert.Len(t, result, 1) + assert.Equal(t, result[0], nodes[1]) + + // net. + config = &dockerclient.ContainerConfig{ + HostConfig: dockerclient.HostConfig{ + NetworkMode: "container:c2", + }, + } + result, err = f.Filter(config, nodes) + assert.NoError(t, err) + assert.Len(t, result, 1) + assert.Equal(t, result[0], nodes[2]) + + // net not prefixed by "container:" should be ignored. + config = &dockerclient.ContainerConfig{ + HostConfig: dockerclient.HostConfig{ + NetworkMode: "bridge", + }, + } + result, err = f.Filter(config, nodes) + assert.NoError(t, err) + assert.Equal(t, result, nodes) +} + +func TestDependencyFilterMulti(t *testing.T) { + var ( + f = DependencyFilter{} + nodes = []*cluster.Node{ + cluster.NewNode("node-1", 0), + cluster.NewNode("node-2", 0), + cluster.NewNode("node-3", 0), + } + result []*cluster.Node + err error + container *cluster.Container + config *dockerclient.ContainerConfig + ) + + // nodes[0] has c0 and c1 + container = &cluster.Container{Container: dockerclient.Container{Id: "c0"}} + assert.NoError(t, nodes[0].AddContainer(container)) + container = &cluster.Container{Container: dockerclient.Container{Id: "c1"}} + assert.NoError(t, nodes[0].AddContainer(container)) + + // nodes[1] has c2 + container = &cluster.Container{Container: dockerclient.Container{Id: "c2"}} + assert.NoError(t, nodes[1].AddContainer(container)) + + // nodes[2] has nothing + + // Depend on c0 which is on nodes[0] + config = &dockerclient.ContainerConfig{ + HostConfig: dockerclient.HostConfig{ + VolumesFrom: []string{"c0"}, + }, + } + result, err = f.Filter(config, nodes) + assert.NoError(t, err) + assert.Len(t, result, 1) + assert.Equal(t, result[0], nodes[0]) + + // Depend on c1 which is on nodes[0] + config = &dockerclient.ContainerConfig{ + HostConfig: dockerclient.HostConfig{ + VolumesFrom: []string{"c1"}, + }, + } + result, err = f.Filter(config, nodes) + assert.NoError(t, err) + assert.Len(t, result, 1) + assert.Equal(t, result[0], nodes[0]) + + // Depend on c0 AND c1 which are both on nodes[0] + config = &dockerclient.ContainerConfig{ + HostConfig: dockerclient.HostConfig{ + VolumesFrom: []string{"c0", "c1"}, + }, + } + result, err = f.Filter(config, nodes) + assert.NoError(t, err) + assert.Len(t, result, 1) + assert.Equal(t, result[0], nodes[0]) + + // Depend on c0 AND c2 which are on different nodes. + config = &dockerclient.ContainerConfig{ + HostConfig: dockerclient.HostConfig{ + VolumesFrom: []string{"c0", "c2"}, + }, + } + result, err = f.Filter(config, nodes) + assert.Error(t, err) +} + +func TestDependencyFilterChaining(t *testing.T) { + var ( + f = DependencyFilter{} + nodes = []*cluster.Node{ + cluster.NewNode("node-1", 0), + cluster.NewNode("node-2", 0), + cluster.NewNode("node-3", 0), + } + result []*cluster.Node + err error + container *cluster.Container + config *dockerclient.ContainerConfig + ) + + // nodes[0] has c0 and c1 + container = &cluster.Container{Container: dockerclient.Container{Id: "c0"}} + assert.NoError(t, nodes[0].AddContainer(container)) + container = &cluster.Container{Container: dockerclient.Container{Id: "c1"}} + assert.NoError(t, nodes[0].AddContainer(container)) + + // nodes[1] has c2 + container = &cluster.Container{Container: dockerclient.Container{Id: "c2"}} + assert.NoError(t, nodes[1].AddContainer(container)) + + // nodes[2] has nothing + + // Different dependencies on c0 and c1 + config = &dockerclient.ContainerConfig{ + HostConfig: dockerclient.HostConfig{ + VolumesFrom: []string{"c0"}, + Links: []string{"c1"}, + NetworkMode: "container:c1", + }, + } + result, err = f.Filter(config, nodes) + assert.NoError(t, err) + assert.Len(t, result, 1) + assert.Equal(t, result[0], nodes[0]) + + // Different dependencies on c0 and c2 + config = &dockerclient.ContainerConfig{ + HostConfig: dockerclient.HostConfig{ + VolumesFrom: []string{"c0"}, + Links: []string{"c2"}, + NetworkMode: "container:c1", + }, + } + result, err = f.Filter(config, nodes) + assert.Error(t, err) +} diff --git a/scheduler/filter/filter.go b/scheduler/filter/filter.go index 8bfaafeb22..3f762820bc 100644 --- a/scheduler/filter/filter.go +++ b/scheduler/filter/filter.go @@ -24,6 +24,7 @@ func init() { "health": &HealthFilter{}, "constraint": &ConstraintFilter{}, "port": &PortFilter{}, + "dependency": &DependencyFilter{}, } }