Nomad
Test drive Dynamic Application Sizing
Enterprise Only
The functionality described here is available only in Nomad Enterprise with the Multi-Cluster & Efficiency module. To explore Nomad Enterprise features, you can sign up for a free 30-day trial from here.
Prometheus Required
Currently, Prometheus is the only APM supported for Dynamic Application Sizing
Using a Vagrant virtual machine, you will deploy a simple environment containing:
- An APM, specifically Prometheus, to collect metric data.
- Nomad Autoscaler Enterprise.
- A sample job, which will be configured to enable DAS recommendations with:
- one NGINX instance used as a TCP load balancer.
- three Redis instances to service requests.
- A sample dispatch job to create load on the Redis nodes.
Prerequisites
Familiarity with the Dynamic application scaling concepts tutorial.
This Vagrantfile to create a suitable environment to run the demonstration.
This Vagrantfile provisions:
one Ubuntu 20.04 VM preinstalled with:
- Nomad Enterprise v1.0.0 beta 2
- The current version of Consul installable via package
- The current version of Docker installable via package
Start and connect to the Vagrant environment
Download the Vagrantfile. Start the test-drive environment by running
vagrant up
.
$ vagrant up
Once the environment is provisioned and you are returned to your command prompt, connect to the Vagrant instance.
$ vagrant ssh
Once you are at the vagrant@ubuntu-focal:~$
prompt, you are ready to continue.
Verify Nomad telemetry configuration
Nomad needs to be configured to enable telemetry publishing. You need to enable
allocation and node metrics. Since this tutorial also uses Prometheus as its APM,
you need to set prometheus_metrics
to true.
The configuration for the Nomad inside the test-drive already has the
appropriate telemetry configuration. View the configuration using
cat /etc/nomad.d/nomad.hcl
file and note the following stanza is included.
telemetry {
publish_allocation_metrics = true
publish_node_metrics = true
prometheus_metrics = true
}
Given this configuration, Nomad generates node and allocation metrics and make them available in a format that Prometheus can consume. If you are using this test-drive with your own Nomad cluster, add this telemetry block to the configuration for every Nomad node in your cluster and restart them to load the new configuration.
Return to the vagrant user's home directory if you changed away from it.
$ cd /home/vagrant
Start Prometheus
The autoscaler configuration in this test-drive uses Prometheus to retrieve historical metrics when starting to track a new target. In this beta, Prometheus is also used for ongoing monitoring metrics, but this is currently being shifted to using Nomad's metrics API. The first step is to run an instance of Prometheus for the Nomad Autoscaler to use. The simplest way to do this is to run Prometheus as a Nomad job. The environment contains a complete Prometheus job file to get started with.
You can create a file called prometheus.nomad
with the following content, or
you can copy prometheus.nomad
from the ~/nomad-autoscaler/jobs
folder when
logged into a vagrant user's shell inside the VM.
job "prometheus" {
datacenters = ["dc1"]
group "prometheus" {
count = 1
network {
port "prometheus_ui" {
static = 9090
}
}
task "prometheus" {
driver = "docker"
config {
image = "prom/prometheus:v2.18.1"
args = [
"--config.file=/etc/prometheus/config/prometheus.yml",
"--storage.tsdb.path=/prometheus",
"--web.console.libraries=/usr/share/prometheus/console_libraries",
"--web.console.templates=/usr/share/prometheus/consoles",
]
volumes = [
"local/config:/etc/prometheus/config",
]
ports = ["prometheus_ui"]
}
template {
data = <<EOH
---
global:
scrape_interval: 1s
evaluation_interval: 1s
scrape_configs:
- job_name: nomad
metrics_path: /v1/metrics
params:
format: ['prometheus']
static_configs:
- targets: ['{{ env "attr.unique.network.ip-address" }}:4646']
- job_name: consul
metrics_path: /v1/agent/metrics
params:
format: ['prometheus']
static_configs:
- targets: ['{{ env "attr.unique.network.ip-address" }}:8500']
EOH
change_mode = "signal"
change_signal = "SIGHUP"
destination = "local/config/prometheus.yml"
}
resources {
cpu = 100
memory = 256
}
service {
name = "prometheus"
port = "prometheus_ui"
check {
type = "http"
path = "/-/healthy"
interval = "10s"
timeout = "2s"
}
}
}
}
}
Run the job in Nomad.
$ nomad job run prometheus.nomad
Start the autoscaler
The next step is to run the Nomad Autoscaler. For the beta, an enterprise version of the Nomad Autoscaler is provided that includes the DAS plugins. The simplest approach is to run the autoscaler as a Nomad job; however, you can download the Nomad Autoscaler and run it as a standalone process.
This test-drive Vagrant environment comes with Consul. The supplied Nomad job specifications uses this Consul to discover the Nomad and Prometheus URLs. Should you want to use this specification in a cluster without Consul, You can supply the URLs yourself and remove the checks.
You can create a file called das-autoscaler.nomad
with the following content, or
you can copy das-autoscaler.nomad
from the ~/nomad-autoscaler/jobs
folder when
logged into a vagrant user's shell inside the VM.
job "das-autoscaler" {
datacenters = ["dc1"]
group "autoscaler" {
count = 1
task "autoscaler" {
driver = "docker"
config {
image = "hashicorp/nomad-autoscaler-enterprise:0.2.0-beta2"
command = "bin/nomad-autoscaler"
args = [
"agent",
"-config",
"${NOMAD_TASK_DIR}/autoscaler.hcl",
"-http-bind-address",
"0.0.0.0",
]
ports = ["http"]
}
template {
destination = "${NOMAD_TASK_DIR}/autoscaler.hcl"
data = <<EOH
// Set the log level so we can see some more interesting output at the expense
// of chattiness.
log_level = "debug"
// Set the address of the Nomad agent. This can be omitted and in this example
// is set to the default for clarity.
nomad {
// Use Consul service discovery for the Nomad client IP and Port.
address = "{{ with service "nomad-client" }}{{ with index . 0 }}http://{{.Address}}:{{.Port}}{{ end }}{{ end }}"
// Use the splat operator so the autoscaler monitors scaling policies from
// all Nomad namespaces. If you wish to have it only monitor a single
// namespace, update this param to match the desired name.
namespace = "*"
// If Nomad ACLs are in use, the following line should be uncommented and
// updated to include an ACL token.
// token = ""
}
// Setup the Prometheus APM so that the autoscaler can pull historical and
// point-in-time metrics regarding task resource usage.
apm "prometheus" {
driver = "prometheus"
config = {
// Use Consul service discovery for the Prometheus IP and Port.
address = "{{ with service "prometheus" }}{{ with index . 0 }}http://{{.Address}}:{{.Port}}{{ end }}{{ end }}"
}
}
policy_eval {
// Lower the evaluate interval so we can reproduce recommendations after only
// 5 minutes, rather than having to wait for 24hrs as is the default.
evaluate_after = "5m"
// Disable the horizontal application and horizontal cluster workers. This
// helps reduce log noise during the demo.
workers = {
cluster = 0
horizontal = 0
}
}
EOH
}
resources {
cpu = 1024
memory = 512
}
}
network {
port "http" {
to = 8080
}
}
service {
name = "nomad-autoscaler"
port = "http"
check {
type = "http"
path = "/v1/health"
interval = "5s"
timeout = "2s"
}
}
}
}
Run the job in Nomad.
$ nomad job run das-autoscaler.nomad
Upon starting, the autoscaler loads the DAS-specific plugin and launches workers
to evaluate vertical policies. You can see the logs using the Nomad UI or nomad alloc logs ...
command:
[INFO] agent.plugin_manager: successfully launched and dispensed plugin: plugin_name=app-sizing-percentile
[INFO] agent.plugin_manager: successfully launched and dispensed plugin: plugin_name=nomad-target
[INFO] agent.plugin_manager: successfully launched and dispensed plugin: plugin_name=app-sizing-nomad
[INFO] agent.plugin_manager: successfully launched and dispensed plugin: plugin_name=prometheus
[INFO] agent.plugin_manager: successfully launched and dispensed plugin: plugin_name=app-sizing-avg
[INFO] agent.plugin_manager: successfully launched and dispensed plugin: plugin_name=app-sizing-max
[INFO] policy_eval.worker: starting worker: id=f6d205b3-9e48-ba9d-a230-9d3e8f2bdf81 queue=vertical_cpu
[INFO] policy_eval.worker: starting worker: id=750bcea7-47af-94b3-820c-1770c757ed07 queue=vertical_mem
If there are already jobs configured with vertical policies, the autoscaler begins dispatching policy evaluations from the broker to the workers; otherwise, this occurs when vertical policies are added to a job specification:
[DEBUG] policy_eval.broker: dequeue eval: queue=vertical_mem
Note
The autoscaler does not immediately register recommendations.
The evaluate_after
field in the autoscaler configuration indicates the
amount of historical metrics that must be available before a recommendation
is made for a task. The purpose is to prevent recommendations with
insufficient historical information; without representative data,
appropriate recommendations cannot be made, which could result in
under-provisioning a task. For the purpose of evaluating the feature, this
can be reduced. For more production-like environments, this interval should
be long enough to capture a representative sample of metrics. The default
interval is 24 hours.
Deploy the sample job
Create a job named example.nomad.hcl with the following content.
job "example" {
datacenters = ["dc1"]
group "cache-lb" {
count = 1
network {
port "lb" {
to = 6379
}
}
service {
name = "redis-lb"
port = "lb"
address_mode = "host"
check {
type = "tcp"
port = "lb"
interval = "10s"
timeout = "2s"
}
}
task "nginx" {
driver = "docker"
config {
image = "nginx"
ports = ["lb"]
volumes = [
# It's safe to mount this path as a file because it won't re-render.
"local/nginx.conf:/etc/nginx/nginx.conf",
# This path hosts files that will re-render with Consul Template.
"local/nginx:/etc/nginx/conf.d"
]
}
# This template overwrites the embedded nginx.conf file so it loads
# conf.d/*.conf files outside of the `http` block.
template {
data = <<EOF
user nginx;
worker_processes 1;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
include /etc/nginx/conf.d/*.conf;
EOF
destination = "local/nginx.conf"
}
# This template creates a TCP proxy to Redis.
template {
data = <<EOF
stream {
server {
listen 6379;
proxy_pass backend;
}
upstream backend {
{{ range service "redis" }}
server {{ .Address }}:{{ .Port }};
{{ else }}server 127.0.0.1:65535; # force a 502
{{ end }}
}
}
EOF
destination = "local/nginx/nginx.conf"
change_mode = "signal"
change_signal = "SIGHUP"
}
resources {
cpu = 50
memory = 10
}
}
}
group "cache" {
count = 3
network {
port "db" {
to = 6379
}
}
service {
name = "redis"
port = "db"
address_mode = "host"
check {
type = "tcp"
port = "db"
interval = "10s"
timeout = "2s"
}
}
task "redis" {
driver = "docker"
config {
image = "redis:3.2"
ports = ["db"]
}
resources {
cpu = 500
memory = 256
}
}
}
}
Add DAS to the sample job
In order to enable a Nomad job task for sizing recommendations, the following job specification contains a task scaling stanza for CPU and one for memory. These stanzas, when placed within a job specification's task stanza, configure the task for both CPU and memory recommendations.
To enable application-sizing for multiple tasks with DAS, you need to add this
scaling block to every new or additional task in the job spec. Inside both the
cache-lb
and the cache
tasks, add the following scaling policies. You can
verify your changes against the completed example.nomad.hcl
file in the
~/nomad-autoscaler/jobs
directory.
scaling "cpu" {
policy {
cooldown = "1m"
evaluation_interval = "1m"
check "95pct" {
strategy "app-sizing-percentile" {
percentile = "95"
}
}
}
}
scaling "mem" {
policy {
cooldown = "1m"
evaluation_interval = "1m"
check "max" {
strategy "app-sizing-max" {}
}
}
}
Note
These scaling policies are extremely aggressive and provide
"flappy" recommendations, making them unsuitable for production. They are
set with low cooldown
and evaluation_interval
values in order to
quickly generate recommendations for this test drive. Consult the
Dynamic Application Sizing Concepts tutorial for how to determine
suggested production values.
Reregister the example.nomad.hcl file by running the nomad job run example.nomad.hcl
command.
$ nomad job run example.nomad.hcl
Once the job has been registered with its updated specification, the Nomad autoscaler automatically detects the new scaling policies and start the required internal processes.
Further details on the individual parameters and available strategies can be found in the Nomad documentation, including information on how you can further customize the application-sizing block to your needs (percentile, cooldown periods, sizing strategies).
Review DAS recommendations
Once the autoscaler has generated recommendations, you can review them in the Nomad UI or using the Nomad API and accept or dismiss the recommendations.
Select the Optimize option in the Workload section of the sidebar. When there are DAS recommendations they appear here.
Clicking Accept applies the recommendation, updating the job with resized tasks. Dismissing the recommendation causes it to disappear. However, the autoscaler continues to monitor and eventually makes additional recommendations for the job until the vertical scaling policy is removed from the job specification.
Click the Accept button to accept the suggestion.
You also receive a suggestion for the cache-lb
task.
Click the Accept button to accept the suggestion.
Use curl to access the List Recommendations API.
$ curl 'http://127.0.0.1:4646/v1/recommendations?pretty'
You should receive two recommendations: one for the cache task and one for the cache-lb task.
[
{
"ID": "1308e937-63b1-fa43-67e9-3187c954e417",
"Region": "global",
"Namespace": "default",
"JobID": "example",
"JobVersion": 0,
"Group": "cache-lb",
"Task": "nginx",
"Resource": "CPU",
"Value": 57,
"Current": 50,
"Meta": {
"window_size": 300000000000.0,
"nomad_policy_id": "dd393d4b-99d7-7b72-132c-7e70f1b6b2dc",
"num_evaluated_windows": 11.0
},
"Stats": {
"max": 20.258468627929688,
"mean": 0.21294420193006963,
"min": 0.0,
"p99": 20.258468627929688
},
"EnforceVersion": false,
"SubmitTime": 1604353860521108002,
"CreateIndex": 350,
"ModifyIndex": 350
},
{
"ID": "b9331de3-299f-cd74-bf6d-77aa36a3e147",
"Region": "global",
"Namespace": "default",
"JobID": "example",
"JobVersion": 0,
"Group": "cache",
"Task": "redis",
"Resource": "CPU",
"Value": 57,
"Current": 500,
"Meta": {
"window_size": 300000000000.0,
"nomad_policy_id": "1b63f7bd-c995-d61e-cf4f-b49a8d777b65",
"num_evaluated_windows": 12.0
},
"Stats": {
"p99": 32.138671875,
"max": 32.138671875,
"mean": 2.5897381649120943,
"min": 0.06250959634780884
},
"EnforceVersion": false,
"SubmitTime": 1604353860521659719,
"CreateIndex": 352,
"ModifyIndex": 352
},
{
"ID": "f91454d6-8df8-ce64-696b-b21c758cfb3b",
"Region": "global",
"Namespace": "default",
"JobID": "example",
"JobVersion": 0,
"Group": "cache",
"Task": "redis",
"Resource": "MemoryMB",
"Value": 10,
"Current": 256,
"Meta": {
"nomad_policy_id": "9153e45b-618c-a7e4-6aa3-c720fd20184f",
"num_evaluated_windows": 12.0,
"window_size": 300000000000.0,
"nomad_autoscaler.count.capped": true,
"nomad_autoscaler.count.original": 2.0,
"nomad_autoscaler.reason_history": []
},
"Stats": {
"max": 2.01171875,
"mean": 1.9451913759689923,
"min": 1.9375,
"p99": 1.984375
},
"EnforceVersion": false,
"SubmitTime": 1604353860521511567,
"CreateIndex": 351,
"ModifyIndex": 351
}
]
You can accept them by using the Apply and Dismiss Recommendations API endpoint. Replace the Recommendation IDs in the command with the recommendation IDs received when you queried the List Recommendations API.
$ curl 'http://127.0.0.1:4646/v1/recommendations/apply?pretty' \
--request POST \
--data '{"Apply":["1308e937-63b1-fa43-67e9-3187c954e417",
"b9331de3-299f-cd74-bf6d-77aa36a3e147"]}'
"Errors": [],
"LastIndex": 0,
"RequestTime": 0,
"UpdatedJobs": [
{
"EvalCreateIndex": 403,
"EvalID": "5a1c5f5e-6a82-17a9-ca2d-b053e4f418f2",
"JobID": "example",
"JobModifyIndex": 403,
"Namespace": "default",
"Recommendations": [
"1308e937-63b1-fa43-67e9-3187c954e417",
"b9331de3-299f-cd74-bf6d-77aa36a3e147"
],
"Warnings": ""
}
]
}
Verify recommendation is applied
Watch for the deployment to complete and then verify that the job is now using
the recommended values instead of the ones initially supplied. You can do this
with in the Nomad UI or using the nomad alloc status
command for a cache
and a
cache-lb
allocation listed from the nomad job status example
command.
Navigate to the example job's detail screen in the Nomad UI
Note that the Task Groups section shows the updated values for Reserved CPU and Reserved Memory given by the autoscaler.
List out the allocations for the example job by running nomad job status example
.
$ nomad job status example
ID = example
Name = example
Submit Date = 2020-11-02T16:28:52Z
Type = service
Priority = 50
Datacenters = dc1
Namespace = default
Status = running
Periodic = false
Parameterized = false
Summary
Task Group Queued Starting Running Failed Complete Lost
cache 0 0 3 0 3 0
cache-lb 0 0 1 0 1 0
Latest Deployment
ID = c3ee5e5d
Status = successful
Description = Deployment completed successfully
Deployed
Task Group Desired Placed Healthy Unhealthy Progress Deadline
cache 3 3 3 0 2020-11-02T16:39:30Z
cache-lb 1 1 1 0 2020-11-02T16:39:06Z
Allocations
ID Node ID Task Group Version Desired Status Created Modified
5a35ffec c442fcaa cache 2 run running 4m49s ago 4m30s ago
8ceec492 c442fcaa cache-lb 2 run running 5m7s ago 4m35s ago
ceb84c32 c442fcaa cache 2 run running 5m7s ago 4m50s ago
2de4ff81 c442fcaa cache 2 run running 5m16s ago 4m57s ago
2ffa9be6 c442fcaa cache-lb 1 stop complete 37m9s ago 5m7s ago
528156b1 c442fcaa cache 0 stop complete 37m9s ago 5m15s ago
04645e48 c442fcaa cache 0 stop complete 37m9s ago 5m7s ago
2d9fc1f2 c442fcaa cache 0 stop complete 37m9s ago 4m48s ago
From the job status output, a cache
allocation has allocation ID 5a35ffec.
Run the nomad alloc status 5a35ffec
command to get the Task Resources
information about this allocation.
$ nomad alloc status 5a35ffec
ID = 5a35ffec-2af3-d36f-6dd8-d8453407d6a5
Eval ID = 564f00df
Name = example.cache[2]
Node ID = c442fcaa
Node Name = ubuntu-focal
Job ID = example
Job Version = 2
Client Status = running
Client Description = Tasks are running
Desired Status = run
Desired Description = <none>
Created = 6m55s ago
Modified = 6m36s ago
Deployment ID = c3ee5e5d
Deployment Health = healthy
Allocation Addresses
Label Dynamic Address
*db yes 10.0.2.15:25465 -> 6379
Task "redis" is "running"
Task Resources
CPU Memory Disk Addresses
3/57 MHz 992 KiB/10 MiB 300 MiB
Task Events:
Started At = 2020-11-02T16:29:11Z
Finished At = N/A
Total Restarts = 0
Last Restart = N/A
Recent Events:
Time Type Description
2020-11-02T16:29:11Z Started Task started by client
2020-11-02T16:29:11Z Task Setup Building Task Directory
2020-11-02T16:29:11Z Received Task received by client
Note that the Task Resources section shows the updated values for memory and CPU given by the autoscaler.
From the earlier job status output, a cache-lb
allocation has allocation ID
8ceec492. Run the nomad alloc status 8ceec492
command to get the Task
Resources information about this allocation.
$ nomad alloc status 8ceec492
ID = 8ceec492-9549-e563-40d9-bf76a47940f2
Eval ID = f0c24365
Name = example.cache-lb[0]
Node ID = c442fcaa
Node Name = ubuntu-focal
Job ID = example
Job Version = 2
Client Status = running
Client Description = Tasks are running
Desired Status = run
Desired Description = <none>
Created = 7m44s ago
Modified = 7m12s ago
Deployment ID = c3ee5e5d
Deployment Health = healthy
Allocation Addresses
Label Dynamic Address
*lb yes 10.0.2.15:29363 -> 6379
Task "nginx" is "running"
Task Resources
CPU Memory Disk Addresses
0/57 MHz 1.5 MiB/10 MiB 300 MiB
Task Events:
Started At = 2020-11-02T16:28:54Z
Finished At = N/A
Total Restarts = 0
Last Restart = N/A
Recent Events:
Time Type Description
2020-11-02T16:29:24Z Signaling Template re-rendered
2020-11-02T16:29:16Z Signaling Template re-rendered
2020-11-02T16:29:13Z Signaling Template re-rendered
2020-11-02T16:29:04Z Signaling Template re-rendered
2020-11-02T16:28:57Z Signaling Template re-rendered
2020-11-02T16:28:55Z Signaling Template re-rendered
2020-11-02T16:28:54Z Started Task started by client
2020-11-02T16:28:53Z Driver Downloading image
2020-11-02T16:28:53Z Task Setup Building Task Directory
2020-11-02T16:28:52Z Received Task received by client
Here, also, the Task Resources section shows the updated values for memory and CPU given by the autoscaler.
Generate load to create new recommendations
Create a parameterized dispatch job to generate load in your cluster. Create a
file named das-load-test.nomad
with the following content. You can also copy
this file from the ~/nomad-autoscaler/jobs
folder in the Vagrant instance.
job "das-load-test" {
datacenters = ["dc1"]
type = "batch"
parameterized {
payload = "optional"
meta_optional = ["requests", "clients"]
}
group "redis-benchmark" {
task "redis-benchmark" {
driver = "docker"
config {
image = "redis:3.2"
command = "redis-benchmark"
args = [
"-h","${HOST}",
"-p","${PORT}",
"-n","${REQUESTS}",
"-c","${CLIENTS}",
]
}
template {
destination = "secrets/env.txt"
env = true
data = <<EOF
{{ with service "redis-lb" }}{{ with index . 0 -}}
HOST={{.Address}}
PORT={{.Port}}
{{- end }}{{ end }}
REQUESTS={{ or (env "NOMAD_META_requests") "100000" }}
CLIENTS={{ or (env "NOMAD_META_clients") "50" }}
EOF
}
resources {
cpu = 100
memory = 128
}
}
}
}
Register the dispatch job with the nomad job run das-load-test.nomad
command.
$ nomad job run das-load-test.nomad
Job registration successful
Now, dispatch instances of the load-generation task by running the following:
$ nomad job dispatch das-load-test
Dispatched Job ID = das-load-test/dispatch-1604336299-70a3923e
Evaluation ID = 1793fe23
==> Monitoring evaluation "1793fe23"
Evaluation triggered by job "das-load-test/dispatch-1604336299-70a3923e"
Allocation "589a1825" created: node "c442fcaa", group "redis-benchmark"
Evaluation status changed: "pending" -> "complete"
==> Evaluation "1793fe23" finished with status "complete
Each run of this job creates 100,000 requests against your Redis cluster using 50 Redis clients.
Once you have run the job, watch the Optimize view for new suggestions based on the latest activity.
Exit and clean up
Exit the shell session on the Vagrant VM by typing exit
. Run the vagrant destroy
command to stop and remove the virtual box instance. Delete the Vagrantfile once
you no longer want to use the test-drive environment.
Learn more
If you have not already, review the Dynamic Application Sizing Concepts tutorial for more information about the individual parameters and available strategies.
You can also find more information in the Nomad Autoscaler Scaling Policies documentation, including how you can further customize the application-sizing block to your needs (percentile, cooldown periods, and sizing strategies).