mirror of https://github.com/docker/docs.git
Merge pull request #186 from vieux/improve_constraints
Improve constraints to add matching
This commit is contained in:
commit
13e42a9d0b
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
4
flags.go
4
flags.go
|
|
@ -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"},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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"),
|
||||||
|
|
@ -23,22 +23,25 @@ func TestLabeleFilter(t *testing.T) {
|
||||||
nodes[0].ID = "node-0-id"
|
nodes[0].ID = "node-0-id"
|
||||||
nodes[0].Name = "node-0-name"
|
nodes[0].Name = "node-0-name"
|
||||||
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"
|
||||||
nodes[1].Name = "node-1-name"
|
nodes[1].Name = "node-1-name"
|
||||||
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"
|
||||||
nodes[2].Name = "node-2-name"
|
nodes[2].Name = "node-2-name"
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
|
@ -20,9 +20,9 @@ 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{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue