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