Ola. Nesse post, vamos tratar como fazer o HPA do Kubernetes conseguir identificar a quantidade de requisições http que o POD esta recebendo e assim escalar a quantidade de PODs de acordo com a demanda.
Essa é uma ótima alternativa do que utilizar HPA por CPU ou memória, principalmente se for aplicações Spring Boot (Java) em que muitas vezes o consumo de CPU ou memória não são indicativos de capacidade de resposta.
Você pode acompanhar e pegar o fonte de tudo o que eu criei nesse post no github:
https://github.com/escovabit-tec-br/spring-boot-kubernetes-hpa-prometheus
Esse post, vai ter uma lista de pré-requisitos, infelizmente não tenho post falando de cada um deles, mas vou ir colocando as documentações oficiais.
Pré requisitos
- Prometheus, Operator e Grafana: https://github.com/prometheus-community/helm-charts/tree/main/charts/kube-prometheus-stack
- Prometheus Adapter: https://github.com/prometheus-community/helm-charts/tree/main/charts/prometheus-adapter
- Kube State Metrics: https://github.com/prometheus-community/helm-charts/tree/main/charts/kube-state-metrics
- Helm: https://helm.sh/docs/
- Minikube (Opcional, de for rodar local): https://minikube.sigs.k8s.io/docs/start/
Arquitetura da solução
Nossa solução depende de um conjunto de “pequenos” fatores para funcionar.

Vamos fazer um breve resumo de cada um deles, e depois tratar cada elemento em sequencia.
- Spring Boot Actuator – Prometheus:
Precisamos fazer nossa aplicação exportar as métricas de utilização no formato que o Prometheus espera. É assim que conseguimos os dados de “estado” da aplicação, para saber quantas requisições ela esta recebendo.
- Prometheus – Service Monitor:
Depois, devemos instruir o Prometheus, que ele precisa fazer a coleta das informações de nossa aplicação. Com isso vamos ter uma “base de dados” com as métricas para consultar.
- Prometheus Adapter – Custom Query e Custom Metrics:
Agora que temos a informação em “banco” com o Prometheus, devemos criar uma “Custom Query” que ira fazer a consulta no Prometheus e exportar a informação diretamente para o Kubernetes em uma “Custom Metrics”.
- Kubernetes HPA
Por fim, devemos criar um HPA no Kubernetes que consulte a “Custom Metrics” e calcule o estado do HPA.
Configurando a aplicação Spring Boot
Fiz um Controller (PingController) somente para servir de rebatedor e simular a operação da aplicação.
package br.tec.escovabit.apphpa.controller;
import java.time.OffsetDateTime;
import lombok.SneakyThrows;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import br.tec.escovabit.apphpa.controller.model.response.ThreadWaitModel;
import lombok.extern.slf4j.Slf4j;
@RestController
@RequestMapping("/ping")
@Slf4j
public class PingController {
@SneakyThrows
@GetMapping("/thread-wait/{time}")
public ResponseEntity<ThreadWaitModel> threadWait(@PathVariable("time") Long time) {
ThreadWaitModel model = new ThreadWaitModel();
model.setStartDate(OffsetDateTime.now());
log.info("Thread wait start: {}", model.getStartDate());
try {
Thread.sleep(time);
} catch (InterruptedException e) {
log.error("Thread InterruptedException", e);
throw e;
}
model.setEndDate(OffsetDateTime.now());
log.info("Thread wait end: {}", model.getEndDate());
return ResponseEntity.ok().body(model);
}
}
Já falamos sobre o Actuator dentro do Spring Boot em outros posts:
Vamos tratar aqui, somente o que não foi passado nos post anteriores.
Começamos criando a classe de configuração MicrometerConfiguration.
Essa classe, basicamente configura o Micrometer.io para exportar o nome da aplicação junto com os dados para o Prometheus. Isso é importante para conseguimos distinguir qual aplicação esta recebendo as requisições, mesmo sendo o mesmo contexto na URL.
package br.tec.escovabit.apphpa.configuration;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.Arrays;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.actuate.autoconfigure.metrics.MeterRegistryCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import io.micrometer.core.instrument.MeterRegistry;
import io.micrometer.core.instrument.config.MeterFilter;
@Configuration
public class MicrometerConfiguration {
private static final String UNKNOW = "unknow";
private static final List<String> NON_APPLICATION_ENDPOINTS = Arrays.asList(
"/swagger", "/**", "/v2/api-docs", "/webjars");
private static final Logger LOGGER = Logger.getLogger(MicrometerConfiguration.class.getName());
private static final String TAG_URI = "uri";
@Bean
public static MeterRegistryCustomizer<MeterRegistry> metricsCommonTags(
@Value("${spring.application.name}") String applicationName) {
return registry -> registry.config()
.commonTags(
"host", getHostName(),
"instance", getHostName(),
"ip", getHostAddress(),
"application", applicationName)
.meterFilter(denyFrameworkURIsFilter());
}
private static MeterFilter denyFrameworkURIsFilter() {
return MeterFilter.deny(id -> isNonApplicationEndpoint(id.getTag(TAG_URI)));
}
private static boolean isNonApplicationEndpoint(String uri) {
return uri != null
&& NON_APPLICATION_ENDPOINTS.stream()
.map(uri::startsWith)
.filter(i -> i)
.findFirst()
.orElse(false);
}
private static String getHostName() {
try {
return InetAddress.getLocalHost().getHostName();
} catch (UnknownHostException e) {
LOGGER.log(Level.INFO, e.getMessage(), e);
return UNKNOW;
}
}
private static String getHostAddress() {
try {
return InetAddress.getLocalHost().getHostAddress();
} catch (UnknownHostException e) {
LOGGER.log(Level.INFO, e.getMessage(), e);
return UNKNOW;
}
}
}
Outra pequena configuração, é no arquivo application.yml, definir o nome da aplicação:
spring:
application:
name: app-hpa
Você pode comparar a resposta da url /actuator/prometheus no post anterior com essa que acabamos de fazer:

Configurando o Prometheus Adapter
Estou considerando que o seu ambientes já tem o Prometheus rodando, só vou tratar da configuração especifica da “Custom Metrics”.
Fiz um deve README.md sobre a instalação do Prometheus: https://github.com/escovabit-tec-br/spring-boot-kubernetes-hpa-prometheus/tree/main/helm/prometheus
A customização que precisamos fazer no Prometheus Adaptar esta em criar uma “Custom Metrics” conforme mostrada a baixo.
rules:
default: true
custom:
- seriesQuery: '{__name__=~"^http_server_requests_seconds_.*",container!="POD",namespace!="",pod!=""}'
seriesFilters: []
resources:
overrides:
namespace:
resource: namespace
pod:
resource: pod
name:
matches: '^http_server_requests_seconds_count$'
as: 'sum_http_server_requests_seconds_count_filted'
metricsQuery: sum(rate(<<.Series>>{<<.LabelMatchers>>,uri!~"UNKNOWN|^/actuator/.*"}[5m])) by (<<.GroupBy>>)
Os principais elementos dessa configuração que precisamos conhecer:
- seriesQuery
Corresponde a query que sera executada no Prometheus. O http_server_requests_seconds_* é o nome do atributo que o Spring Boot expõem as informações.
- name.as
Indica o nome que definimos para está “Custom Metrics”, é ele que vamos utilizar no HPA.
- metricsQuery
Representa como o Prometheus Adapter filtra a query por aplicação, para que o HPA calcule o resultado por POD.
Também é importante a configuração de uri que remove o actuator do calculo, para que somente uri de “negocio” sejam calculadas.
Publicando nossa aplicação
Por fim, vamos fazer a publicação da nossa aplicação.
Fiz um helm para facilitar a publicação a nossa aplicação de exemplo. Nos arquivos de publicação, não tem nenhuma peculiaridade, o que temos que tratar são dois yamls que vou mostrar a baixo. Ele são publicados em conjunto com a aplicação.
O primeiro é o ServiceMonitor, este yaml tem como objetivo configura o Prometheus a identificar e coletar as informações da nossa aplicação.
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
labels:
app.kubernetes.io/instance: app-hpa
app.kubernetes.io/name: app-hpa
name: app-hpa
namespace: l2-app-exemplos
spec:
endpoints:
- honorLabels: true
interval: 1m
path: /actuator/prometheus
port: http
scheme: http
scrapeTimeout: 30s
jobLabel: app-hpa-l2-app-exemplos
namespaceSelector:
matchNames:
- l2-app-exemplos
selector:
matchLabels:
app.kubernetes.io/instance: app-hpa
app.kubernetes.io/name: app-hpa
O que se precisa de atenção nessa configuração são os itens:
- spec.endpoints.path
Que representa o endeço que o prometheus ira chamar para obter os dados para a coleta
- spec.endpoints.port
Que representa o nome da porta do Service/POD estão exportando.
Precisa que o Service e o POD estejam usando portas com o atributo “name“, porque o Prometheus localiza a porta pelo nome atribuído a ela.
Em sequencia, temos as configuração do HPA, nela devemos configura o uso da nossa “Custom Metrics”
apiVersion: autoscaling/v2beta2
kind: HorizontalPodAutoscaler
metadata:
name: app-hpa
namespace: l2-app-exemplos
labels:
app.kubernetes.io/instance: app-hpa
app.kubernetes.io/name: app-hpa
spec:
scaleTargetRef:
kind: Deployment
name: app-hpa
apiVersion: apps/v1
minReplicas: 1
maxReplicas: 100
metrics:
- type: Pods
# Dependendo da versão do K8S pode ser assim
pods:
metric:
name: sum_http_server_requests_seconds_count_filted
target:
type: AverageValue
averageValue: 20m
# Ou assim
# pods:
# metricName: sum_http_server_requests_seconds_count_filted
# targetAverageValue: 20m
behavior:
scaleDown:
stabilizationWindowSeconds: 10
policies:
- type: Pods
value: 2
periodSeconds: 2
scaleUp:
stabilizationWindowSeconds: 10
policies:
- type: Pods
value: 2
periodSeconds: 2
selectPolicy: Max
A configuração fica sobre o elemento:
- spec.metrics.pods.metricName: sum_http_server_requests_seconds_count_filted
Que representa o nome da Custom Metrics que criamos no Prometheus Adapter.
Com tudo pronto, podemos ver nosso HPA em ação.


Para simular as requisições, basta rodar um loop como este:
while true; do curl http://$NODE_IP:$NODE_PORT/ping/thread-wait/1000; done;
Para saber como o Kubernetes esta “enxergando” a métrica, execute o comando:
kubectl get --raw "/apis/custom.metrics.k8s.io/v1beta1/namespaces/l2-app-exemplos/pods/*/sum_http_server_requests_seconds_count_filted?labelSelector=app.kubernetes.io%2Finstance%3Dapp-hpa%2Capp.kubernetes.io%2Fname%3Dapp-hpa" | jq .
O resultado, deve ser parecido com este:
{
"kind": "MetricValueList",
"apiVersion": "custom.metrics.k8s.io/v1beta1",
"metadata": {
"selfLink": "/apis/custom.metrics.k8s.io/v1beta1/namespaces/l2-app-exemplos/pods/%2A/sum_http_server_requests_seconds_count_filted"
},
"items": [
{
"describedObject": {
"kind": "Pod",
"namespace": "l2-app-exemplos",
"name": "app-hpa-688b6958bd-2zwv2",
"apiVersion": "/v1"
},
"metricName": "sum_http_server_requests_seconds_count_filted",
"timestamp": "2021-11-25T09:32:57Z",
"value": "62m",
"selector": null
},
{
"describedObject": {
"kind": "Pod",
"namespace": "l2-app-exemplos",
"name": "app-hpa-688b6958bd-mkszm",
"apiVersion": "/v1"
},
"metricName": "sum_http_server_requests_seconds_count_filted",
"timestamp": "2021-11-25T09:32:57Z",
"value": "79m",
"selector": null
},
{
"describedObject": {
"kind": "Pod",
"namespace": "l2-app-exemplos",
"name": "app-hpa-688b6958bd-v6kts",
"apiVersion": "/v1"
},
"metricName": "sum_http_server_requests_seconds_count_filted",
"timestamp": "2021-11-25T09:32:57Z",
"value": "50m",
"selector": null
}
]
}
Para o valor do HPA, o Kubernetes soma todos os values e divide pela quantidade de PODs
Conclusão
Fazendo essa estrutura, conseguimos ter uma forma de crescer o numero de PODs com base em uma informação mais calculável do que CPU e memória.
Pois conseguimos fazer testes de carga para saber quanto um único POD consegue responder de forma aceitável e escalar para a estimativa de requisições que precisamos responde.
Nos vemos no próximo post. SYL