Merge pull request #2205 from dongluochen/containerslots

Containerslots
This commit is contained in:
Dongluo Chen 2016-05-03 15:11:39 -07:00
commit b2de64872b
6 changed files with 315 additions and 1 deletions

View File

@ -25,6 +25,7 @@ Each filter has a name that identifies it. The node filters are:
* `constraint`
* `health`
* `containerslots`
The container configuration filters are:
@ -48,6 +49,8 @@ $ swarm manage --filter=health --filter=dependency
When creating a container or building an image, you use a `constraint` or
`health` filter to select a subset of nodes to consider for scheduling.
If a node in Swarm cluster has a label with key `containerslots`
and a number-value, Swarm will not launch more containers than the given number.
### Use a constraint filter
@ -175,6 +178,19 @@ The node `health` filter prevents the scheduler form running containers
on unhealthy nodes. A node is considered unhealthy if the node is down or it
can't communicate with the cluster store.
### Use the containerslots filter
You may give your Docker nodes the containerslots label
```bash
$ docker daemon --label containerslots=3
```
Swarm will run up to 3 containers at this node, if all nodes are "full",
an error is thrown indicating no suitable node can be found.
If the value is not castable to an integer number or is not present,
there will be no limit on container number.
## Container filters
When creating a container, you can use three types of container filters:

View File

@ -30,6 +30,7 @@ func init() {
filters = []Filter{
&HealthFilter{},
&PortFilter{},
&SlotsFilter{},
&DependencyFilter{},
&AffinityFilter{},
&ConstraintFilter{},

52
scheduler/filter/slots.go Normal file
View File

@ -0,0 +1,52 @@
package filter
import (
"errors"
"strconv"
"github.com/docker/swarm/cluster"
"github.com/docker/swarm/scheduler/node"
)
var (
// ErrNoNodeWithFreeSlotsAvailable is exported
ErrNoNodeWithFreeSlotsAvailable = errors.New("No node with enough open slots available in the cluster")
)
//SlotsFilter only schedules containers with open slots.
type SlotsFilter struct {
}
// Name returns the name of the filter
func (f *SlotsFilter) Name() string {
return "containerslots"
}
// Filter is exported
func (f *SlotsFilter) Filter(_ *cluster.ContainerConfig, nodes []*node.Node, _ bool) ([]*node.Node, error) {
result := []*node.Node{}
for _, node := range nodes {
if slotsString, ok := node.Labels["containerslots"]; ok {
slots, err := strconv.Atoi(slotsString) //if err => cannot cast to int, so ignore the label
if err != nil || len(node.Containers) < slots {
result = append(result, node)
}
} else {
//no limit if there is no containerslots label
result = append(result, node)
}
}
if len(result) == 0 {
return nil, ErrNoNodeWithFreeSlotsAvailable
}
return result, nil
}
// GetFilters returns just the info that this node failed, because there where no free slots
func (f *SlotsFilter) GetFilters(config *cluster.ContainerConfig) ([]string, error) {
return []string{"available container slots"}, nil
}

View File

@ -0,0 +1,167 @@
package filter
import (
"testing"
"github.com/docker/engine-api/types"
"github.com/docker/swarm/cluster"
"github.com/docker/swarm/scheduler/node"
"github.com/stretchr/testify/assert"
)
var labelsWithSlots = make(map[string]string)
var labelsWithoutSlots = make(map[string]string)
var labelsWithStringSlot = make(map[string]string)
func testFixturesAllFreeNode() []*node.Node {
return []*node.Node{
{
ID: "node-0-id",
Name: "node-0-name",
Labels: labelsWithSlots,
Containers: []*cluster.Container{
{Container: types.Container{}},
},
},
{
ID: "node-1-id",
Name: "node-1-name",
Labels: labelsWithSlots,
Containers: []*cluster.Container{},
},
}
}
func testFixturesPartlyFreeNode() []*node.Node {
return []*node.Node{
{
ID: "node-0-id",
Name: "node-0-name",
Labels: labelsWithSlots,
Containers: []*cluster.Container{
{Container: types.Container{}},
{Container: types.Container{}},
{Container: types.Container{}},
},
},
{
ID: "node-1-id",
Name: "node-1-name",
Labels: labelsWithSlots,
Containers: []*cluster.Container{},
},
}
}
func testFixturesAllNoLabelNode() []*node.Node {
return []*node.Node{
{
ID: "node-0-id",
Name: "node-0-name",
Labels: labelsWithoutSlots,
Containers: []*cluster.Container{
{Container: types.Container{}},
{Container: types.Container{}},
{Container: types.Container{}},
},
},
{
ID: "node-1-id",
Name: "node-1-name",
Labels: labelsWithoutSlots,
Containers: []*cluster.Container{},
},
}
}
func testFixturesNoFreeNode() []*node.Node {
return []*node.Node{
{
ID: "node-0-id",
Name: "node-0-name",
Labels: labelsWithSlots,
Containers: []*cluster.Container{
{Container: types.Container{}},
{Container: types.Container{}},
{Container: types.Container{}},
},
},
{
ID: "node-1-id",
Name: "node-1-name",
Labels: labelsWithSlots,
Containers: []*cluster.Container{
{Container: types.Container{}},
{Container: types.Container{}},
{Container: types.Container{}},
},
},
}
}
func testFixturesNoFreeNodeButStringLabel() []*node.Node {
return []*node.Node{
{
ID: "node-0-id",
Name: "node-0-name",
Labels: labelsWithSlots,
Containers: []*cluster.Container{
{Container: types.Container{}},
{Container: types.Container{}},
{Container: types.Container{}},
},
},
{
ID: "node-1-id",
Name: "node-1-name",
Labels: labelsWithStringSlot,
Containers: []*cluster.Container{
{Container: types.Container{}},
{Container: types.Container{}},
{Container: types.Container{}},
},
},
}
}
func TestSlotsFilter(t *testing.T) {
labelsWithSlots["containerslots"] = "3"
labelsWithStringSlot["containerslots"] = "foo"
var (
f = SlotsFilter{}
nodesAllFree = testFixturesAllFreeNode()
nodesPartlyFree = testFixturesPartlyFreeNode()
nodesAllNoLabel = testFixturesAllNoLabelNode()
nodesNoFree = testFixturesNoFreeNode()
nodesNoFreeButStringLabel = testFixturesNoFreeNodeButStringLabel()
result []*node.Node
err error
)
result, err = f.Filter(&cluster.ContainerConfig{}, nodesAllFree, true)
assert.NoError(t, err)
assert.Equal(t, result, nodesAllFree)
result, err = f.Filter(&cluster.ContainerConfig{}, nodesPartlyFree, true)
assert.NoError(t, err)
assert.Len(t, result, 1)
assert.Equal(t, result[0], nodesPartlyFree[1])
result, err = f.Filter(&cluster.ContainerConfig{}, nodesAllNoLabel, true)
assert.NoError(t, err)
assert.Equal(t, result, nodesAllNoLabel)
result, err = f.Filter(&cluster.ContainerConfig{}, nodesNoFree, true)
assert.Equal(t, err, ErrNoNodeWithFreeSlotsAvailable)
assert.Nil(t, result)
result, err = f.Filter(&cluster.ContainerConfig{}, nodesNoFreeButStringLabel, true)
assert.NoError(t, err)
assert.Len(t, result, 1)
assert.Equal(t, result[0], nodesNoFreeButStringLabel[1])
}

View File

@ -18,7 +18,7 @@ function teardown() {
run docker_swarm build --build-arg="constraint:node==node-9" $TESTDATA/build
[ "$status" -eq 1 ]
[[ "${lines[1]}" == *"Unable to find a node that satisfies the following conditions"* ]]
[[ "${lines[2]}" == *"[node==node-9]"* ]]
[[ "${lines[3]}" == *"[node==node-9]"* ]]
run docker_swarm images -q
[ "$status" -eq 0 ]

View File

@ -0,0 +1,78 @@
#!/usr/bin/env bats
load helpers
function teardown() {
swarm_manage_cleanup
stop_docker
}
@test "containerslots filter" {
start_docker_with_busybox 2 --label containerslots=2
swarm_manage
# Use busybox to save image pulling time for integration test.
# Running the first 4 containers, it should be fine.
run docker_swarm run -d -t busybox sh
[ "$status" -eq 0 ]
run docker_swarm run -d -t busybox sh
[ "$status" -eq 0 ]
run docker_swarm run -d -t busybox sh
[ "$status" -eq 0 ]
run docker_swarm run -d -t busybox sh
[ "$status" -eq 0 ]
# When trying to start the 5th one, it should be error finding a node with free slots.
run docker_swarm run -d -t busybox sh
[ "$status" -ne 0 ]
[[ "${output}" == *"Unable to find a node that satisfies the following conditions"* ]]
[[ "${output}" == *"available container slots"* ]]
# And the number of running containers should be still 4.
run docker_swarm ps
[ "${#lines[@]}" -eq 5 ]
}
@test "containerslots without existing label" {
start_docker_with_busybox 2
swarm_manage
# Use busybox to save image pulling time for integration test.
# Running more than 5 containers, it should be fine.
run docker_swarm run -d -t busybox sh
[ "$status" -eq 0 ]
run docker_swarm run -d -t busybox sh
[ "$status" -eq 0 ]
run docker_swarm run -d -t busybox sh
[ "$status" -eq 0 ]
run docker_swarm run -d -t busybox sh
[ "$status" -eq 0 ]
run docker_swarm run -d -t busybox sh
[ "$status" -eq 0 ]
# And the number of running containers should be 5.
run docker_swarm ps
[ "${#lines[@]}" -eq 6 ]
}
@test "containerslots with invalid label" {
start_docker_with_busybox 2 --label containerslots="foo"
swarm_manage
# Use busybox to save image pulling time for integration test.
# Running more than 5 containers, it should be fine.
run docker_swarm run -d -t busybox sh
[ "$status" -eq 0 ]
run docker_swarm run -d -t busybox sh
[ "$status" -eq 0 ]
run docker_swarm run -d -t busybox sh
[ "$status" -eq 0 ]
run docker_swarm run -d -t busybox sh
[ "$status" -eq 0 ]
run docker_swarm run -d -t busybox sh
[ "$status" -eq 0 ]
# And the number of running containers should be 5.
run docker_swarm ps
[ "${#lines[@]}" -eq 6 ]
}