Merge pull request #186 from vieux/improve_constraints

Improve constraints to add matching
This commit is contained in:
Andrea Luzzardi 2014-12-19 14:00:16 -08:00
commit 13e42a9d0b
7 changed files with 182 additions and 20 deletions

View File

@ -51,6 +51,10 @@ http://<node_ip:2375>
See [here](https://github.com/docker/swarm/discovery) for more information about See [here](https://github.com/docker/swarm/discovery) for more information about
other discovery services. other discovery services.
### Advanced Scheduling
See [filters](scheduler/filter) and [strategies](scheduler/strategy) to learn more about advanced scheduling.
### TLS ### TLS
Swarm supports TLS authentication between the CLI and Swarm but also between Swarm and the Docker nodes. Swarm supports TLS authentication between the CLI and Swarm but also between Swarm and the Docker nodes.

View File

@ -43,7 +43,7 @@ docker run -d -p 80:80 nginx
# clean up cluster # clean up cluster
docker rm -f `docker ps -aq` docker rm -f `docker ps -aq`
docker run -d -e constraint:operatingsystem=fedora redis docker run -d -e "constraint:operatingsystem=fedora*" redis
docker ps docker ps
docker run -d -e constraint:storagedriver=devicemapper redis docker run -d -e constraint:storagedriver=devicemapper redis

View File

@ -57,7 +57,7 @@ var (
} }
flFilter = cli.StringSliceFlag{ flFilter = cli.StringSliceFlag{
Name: "filter, f", Name: "filter, f",
Usage: "Filter to use [health, label, port]", Usage: "Filter to use [constraint, health, port]",
Value: &cli.StringSlice{"health", "label", "port"}, Value: &cli.StringSlice{"constraint", "health", "port"},
} }
) )

128
scheduler/filter/README.md Normal file
View File

@ -0,0 +1,128 @@
Filters
=======
The `Docker Swarm` scheduler comes with multiple filters.
Thoses filters are used to schedule containers on a subset of nodes.
`Docker Swarm` currently supports 3 filters:
* [Constraint](README.md#constraint-filter)
* [Port](README.md#port-filter)
* [Healty](README.md#healthy-filter)
You can choose the filter(s) you want to use with the `--filter` flag of `swarm manage`
## Constraint Filter
Constraints are key/value pairs associated to particular nodes. You can see them as *node tags*.
When creating a container, the user can select a subset of nodes that should be considered for scheduling by specifying one or more sets of matching key/value pairs.
This approach has several practical use cases such as:
* Selecting specific host properties (such as `storage=ssd`, in order to schedule containers on specific hardware).
* Tagging nodes based on their physical location (`region=us-east`, to force containers to run on a given location).
* Logical cluster partioning (`environment=production`, to split a cluster into sub-clusters with different properties).
To tag a node with a specific set of key/value pairs, one must pass a list of `--label` options at docker startup time.
For instance, let's start `node-1` with the `storage=ssd` label:
```bash
$ docker -d --label storage=ssd
$ swarm join --discovery token://XXXXXXXXXXXXXXXXXX --addr=192.168.0.42:2375
```
Again, but this time `node-2` with `storage=disk`:
```bash
$ docker -d --label storage=disk
$ swarm join --discovery token://XXXXXXXXXXXXXXXXXX --addr=192.168.0.43:2375
```
Once the nodes are registered with the cluster, the master pulls their respective tags and will take them into account when scheduling new containers.
Let's start a MySQL server and make sure it gets good I/O performance by selecting nodes with flash drives:
```
$ docker run -d -P -e constraint:storage=ssd --name db mysql
f8b693db9cd6
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NODE NAMES
f8b693db9cd6 mysql:latest "mysqld" Less than a second ago running 192.168.0.42:49178->3306/tcp node-1 db
```
In this case, the master selected all nodes that met the `storage=ssd` constraint and applied resource management on top of them, as discussed earlier.
`node-1` was selected in this example since it's the only host running flash.
Now we want to run an `nginx` frontend in our cluster. However, we don't want *flash* drives since we'll mostly write logs to disk.
```
$ docker run -d -P -e constraint:storage=disk --name frontend nginx
f8b693db9cd6
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NODE NAMES
963841b138d8 nginx:latest "nginx" Less than a second ago running 192.168.0.43:49177->80/tcp node-2 frontend
f8b693db9cd6 mysql:latest "mysqld" Up About a minute running 192.168.0.42:49178->3306/tcp node-1 db
```
The scheduler selected `node-2` since it was started with the `storage=disk` label.
#### Standard Constraints
Additionally, a standard set of constraints can be used when scheduling containers without specifying them when starting the node.
Those tags are sourced from `docker info` and currently include:
* storagedriver
* executiondriver
* kernelversion
* operatingsystem
## Port Filter
With this filter, `ports` are considered as a unique resource.
```
$ docker run -d -p 80:80 nginx
87c4376856a8
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NODE NAMES
87c4376856a8 nginx:latest "nginx" Less than a second ago running 192.168.0.42:80->80/tcp node-1 prickly_engelbart
```
Docker cluster selects a node where the public `80` port is available and schedules a container on it, in this case `node-1`.
Attempting to run another container with the public `80` port will result in clustering selecting a different node, since that port is already occupied on `node-1`:
```
$ docker run -d -p 80:80 nginx
963841b138d8
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NODE NAMES
963841b138d8 nginx:latest "nginx" Less than a second ago running 192.168.0.43:80->80/tcp node-2 dreamy_turing
87c4376856a8 nginx:latest "nginx" Up About a minute running 192.168.0.42:80->80/tcp node-1 prickly_engelbart
```
Again, repeating the same command will result in the selection of `node-3`, since port `80` is neither available on `node-1` nor `node-2`:
```
$ docker run -d -p 80:80 nginx
963841b138d8
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NODE NAMES
f8b693db9cd6 nginx:latest "nginx" Less than a second ago running 192.168.0.44:80->80/tcp node-3 stoic_albattani
963841b138d8 nginx:latest "nginx" Up About a minute running 192.168.0.43:80->80/tcp node-2 dreamy_turing
87c4376856a8 nginx:latest "nginx" Up About a minute running 192.168.0.42:80->80/tcp node-1 prickly_engelbart
```
Finally, Docker Cluster will refuse to run another container that requires port `80` since not a single node in the cluster has it available:
```
$ docker run -d -p 80:80 nginx
2014/10/29 00:33:20 Error response from daemon: no resources availalble to schedule container
```
## Health Filter
This filter will prevent scheduling containers on unhealthy nodes.

View File

@ -2,17 +2,19 @@ package filter
import ( import (
"fmt" "fmt"
"regexp"
"strings" "strings"
log "github.com/Sirupsen/logrus"
"github.com/docker/swarm/cluster" "github.com/docker/swarm/cluster"
"github.com/samalba/dockerclient" "github.com/samalba/dockerclient"
) )
// LabelFilter selects only nodes that match certain labels. // ConstraintFilter selects only nodes that match certain labels.
type LabelFilter struct { type ConstraintFilter struct {
} }
func (f *LabelFilter) extractConstraints(env []string) map[string]string { func (f *ConstraintFilter) extractConstraints(env []string) map[string]string {
constraints := make(map[string]string) constraints := make(map[string]string)
for _, e := range env { for _, e := range env {
if strings.HasPrefix(e, "constraint:") { if strings.HasPrefix(e, "constraint:") {
@ -24,21 +26,33 @@ func (f *LabelFilter) extractConstraints(env []string) map[string]string {
return constraints return constraints
} }
func (f *LabelFilter) Filter(config *dockerclient.ContainerConfig, nodes []*cluster.Node) ([]*cluster.Node, error) { // Create the regex for globbing (ex: ub*t* -> ^ub.*t.*$)
// and match.
func (f *ConstraintFilter) match(pattern, s string) bool {
regex := "^" + strings.Replace(pattern, "*", ".*", -1) + "$"
matched, err := regexp.MatchString(regex, strings.ToLower(s))
if err != nil {
log.Error(err)
}
return matched
}
func (f *ConstraintFilter) Filter(config *dockerclient.ContainerConfig, nodes []*cluster.Node) ([]*cluster.Node, error) {
constraints := f.extractConstraints(config.Env) constraints := f.extractConstraints(config.Env)
for k, v := range constraints { for k, v := range constraints {
log.Debugf("matching constraint: %s=%s", k, v)
candidates := []*cluster.Node{} candidates := []*cluster.Node{}
for _, node := range nodes { for _, node := range nodes {
switch k { switch k {
case "node": case "node":
// "node" label is a special case pinning a container to a specific node. // "node" label is a special case pinning a container to a specific node.
if strings.ToLower(node.ID) == v || strings.ToLower(node.Name) == v { if f.match(v, node.ID) || f.match(v, node.Name) {
candidates = append(candidates, node) candidates = append(candidates, node)
} }
default: default:
// By default match the node labels. // By default match the node labels.
if label, ok := node.Labels[k]; ok { if label, ok := node.Labels[k]; ok {
if strings.Contains(strings.ToLower(label), v) { if f.match(v, label) {
candidates = append(candidates, node) candidates = append(candidates, node)
} }
} }

View File

@ -8,9 +8,9 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
func TestLabeleFilter(t *testing.T) { func TestConstrainteFilter(t *testing.T) {
var ( var (
f = LabelFilter{} f = ConstraintFilter{}
nodes = []*cluster.Node{ nodes = []*cluster.Node{
cluster.NewNode("node-0"), cluster.NewNode("node-0"),
cluster.NewNode("node-1"), cluster.NewNode("node-1"),
@ -25,6 +25,7 @@ func TestLabeleFilter(t *testing.T) {
nodes[0].Labels = map[string]string{ nodes[0].Labels = map[string]string{
"name": "node0", "name": "node0",
"group": "1", "group": "1",
"region": "us-west",
} }
nodes[1].ID = "node-1-id" nodes[1].ID = "node-1-id"
@ -32,6 +33,7 @@ func TestLabeleFilter(t *testing.T) {
nodes[1].Labels = map[string]string{ nodes[1].Labels = map[string]string{
"name": "node1", "name": "node1",
"group": "1", "group": "1",
"region": "us-east",
} }
nodes[2].ID = "node-2-id" nodes[2].ID = "node-2-id"
@ -39,6 +41,7 @@ func TestLabeleFilter(t *testing.T) {
nodes[2].Labels = map[string]string{ nodes[2].Labels = map[string]string{
"name": "node2", "name": "node2",
"group": "2", "group": "2",
"region": "eu",
} }
// Without constraints we should get the unfiltered list of nodes back. // Without constraints we should get the unfiltered list of nodes back.
@ -91,4 +94,17 @@ func TestLabeleFilter(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
assert.Len(t, result, 1) assert.Len(t, result, 1)
assert.Equal(t, result[0], nodes[0]) assert.Equal(t, result[0], nodes[0])
// Check matching
result, err = f.Filter(&dockerclient.ContainerConfig{
Env: []string{"constraint:region=us"},
}, nodes)
assert.Error(t, err)
assert.Len(t, result, 0)
result, err = f.Filter(&dockerclient.ContainerConfig{
Env: []string{"constraint:region=us*"},
}, nodes)
assert.NoError(t, err)
assert.Len(t, result, 2)
} }

View File

@ -21,7 +21,7 @@ var (
func init() { func init() {
filters = map[string]Filter{ filters = map[string]Filter{
"health": &HealthFilter{}, "health": &HealthFilter{},
"label": &LabelFilter{}, "constraint": &ConstraintFilter{},
"port": &PortFilter{}, "port": &PortFilter{},
} }
} }