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: 1sscrape_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 = <<EOFuser 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 = <<EOFstream { 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 exampleID = exampleName = exampleSubmit Date = 2020-11-02T16:28:52ZType = servicePriority = 50Datacenters = dc1Namespace = defaultStatus = runningPeriodic = falseParameterized = falseSummaryTask Group Queued Starting Running Failed Complete Lostcache 0 0 3 0 3 0cache-lb 0 0 1 0 1 0Latest DeploymentID = c3ee5e5dStatus = successfulDescription = Deployment completed successfullyDeployedTask Group Desired Placed Healthy Unhealthy Progress Deadlinecache 3 3 3 0 2020-11-02T16:39:30Zcache-lb 1 1 1 0 2020-11-02T16:39:06ZAllocationsID Node ID Task Group Version Desired Status Created Modified5a35ffec c442fcaa cache 2 run running 4m49s ago 4m30s ago8ceec492 c442fcaa cache-lb 2 run running 5m7s ago 4m35s agoceb84c32 c442fcaa cache 2 run running 5m7s ago 4m50s ago2de4ff81 c442fcaa cache 2 run running 5m16s ago 4m57s ago2ffa9be6 c442fcaa cache-lb 1 stop complete 37m9s ago 5m7s ago528156b1 c442fcaa cache 0 stop complete 37m9s ago 5m15s ago04645e48 c442fcaa cache 0 stop complete 37m9s ago 5m7s ago2d9fc1f2 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 5a35ffecID = 5a35ffec-2af3-d36f-6dd8-d8453407d6a5Eval ID = 564f00dfName = example.cache[2]Node ID = c442fcaaNode Name = ubuntu-focalJob ID = exampleJob Version = 2Client Status = runningClient Description = Tasks are runningDesired Status = runDesired Description = <none>Created = 6m55s agoModified = 6m36s agoDeployment ID = c3ee5e5dDeployment Health = healthyAllocation AddressesLabel Dynamic Address*db yes 10.0.2.15:25465 -> 6379Task "redis" is "running"Task ResourcesCPU Memory Disk Addresses3/57 MHz 992 KiB/10 MiB 300 MiBTask Events:Started At = 2020-11-02T16:29:11ZFinished At = N/ATotal Restarts = 0Last Restart = N/ARecent Events:Time Type Description2020-11-02T16:29:11Z Started Task started by client2020-11-02T16:29:11Z Task Setup Building Task Directory2020-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 8ceec492ID = 8ceec492-9549-e563-40d9-bf76a47940f2Eval ID = f0c24365Name = example.cache-lb[0]Node ID = c442fcaaNode Name = ubuntu-focalJob ID = exampleJob Version = 2Client Status = runningClient Description = Tasks are runningDesired Status = runDesired Description = <none>Created = 7m44s agoModified = 7m12s agoDeployment ID = c3ee5e5dDeployment Health = healthyAllocation AddressesLabel Dynamic Address*lb yes 10.0.2.15:29363 -> 6379Task "nginx" is "running"Task ResourcesCPU Memory Disk Addresses0/57 MHz 1.5 MiB/10 MiB 300 MiBTask Events:Started At = 2020-11-02T16:28:54ZFinished At = N/ATotal Restarts = 0Last Restart = N/ARecent Events:Time Type Description2020-11-02T16:29:24Z Signaling Template re-rendered2020-11-02T16:29:16Z Signaling Template re-rendered2020-11-02T16:29:13Z Signaling Template re-rendered2020-11-02T16:29:04Z Signaling Template re-rendered2020-11-02T16:28:57Z Signaling Template re-rendered2020-11-02T16:28:55Z Signaling Template re-rendered2020-11-02T16:28:54Z Started Task started by client2020-11-02T16:28:53Z Driver Downloading image2020-11-02T16:28:53Z Task Setup Building Task Directory2020-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.nomadJob registration successful
Now, dispatch instances of the load-generation task by running the following:
$ nomad job dispatch das-load-testDispatched Job ID = das-load-test/dispatch-1604336299-70a3923eEvaluation 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).