Cloud Computing

24 fev, 2017

Escalando aplicações web com Openstack Heat

Publicidade

A computação na nuvem abriu novos caminhos para utilização de infraestrutura computacional de forma mais eficiente. Em vez de ter que comprar toneladas de servidores para que se aguente pequenos picos de acesso em um e-commerce, por exemplo, podemos provisionar novas máquinas em minutos – ou até mesmo segundos -, de acordo com a necessidade.

Sabemos que não é tão simples assim, pois a maioria das aplicações web existentes são aplicações antigas que não foram feitas para a nuvem; muitas vezes são grandes monolitos que só escalam na vertical. Até mesmo em aplicações que estão nascendo neste momento, muitas vezes por decisões de negócio e de aceleração de entrega, a arquitetura de software deixa a desejar e já nasce não preparada para a nuvem.

Neste artigo mostrarei uma forma simples de como usar o openstack para escalar uma aplicação web. Utilizaremos, além da camada básica de computação (nova) e network (nêutron), duas outras stacks: o Ceilometer (telemetria) e o Heat (orquestração).

Conhecendo o Heat

O Heat é o principal projeto de orquestração do OpenStack, ele possibilita fazer o deploy de aplicações complexas na nuvem utilizando templates no formato YAML. O Heat consegue automatizar a criação de instancias, network, storage, praticamente tudo que o openstack oferece.

No Heat temos alguns componentes:

  • heat-engine: faz o papel principal de orquestração através dos templates.
  • heat-api-cfn: fornece REST APIs compatíveis com o CloudFormation da AWS.
  • heat-api: Fornece REST APIs nativas do openstack para interação com o heat-engine.
  • heat: a interface de linha de comando do que comunica com o heat-api.

Estrutura de um template do heat:

 

heat_template_version: 2015-04-30
description: Simple template to deploy a single compute instance

parameters:
  key_name:
    type: string
    label: Key Name
    description: Name of key-pair to be used for compute instance
  image_id:
    type: string
    label: Image ID
    description: Image to be used for compute instance
  instance_type:
    type: string
    label: Instance Type
    description: Type of instance (flavor) to be used

resources:
  my_instance:
    type: OS::Nova::Server
    properties:
      key_name: { get_param: key_name }
      image: { get_param: image_id }
      flavor: { get_param: instance_type }
output:
  server_adress:
    str_replace:
        template: http://host/
        params:
          host: { get_attr: [floating_ip_address] }

Temos, basicamente, três seções: os parâmetros de entrada antes da execução do arquivo, os recursos de infraestrutura que serão orquestrados ou recursos que ajudarão no processo de orquestração, como por exemplo, monitoramento de uso de cpu, e ainda uma saída para dar algum feedback ao usuário ou sistema.

Mão na massa

Neste exemplo, criaremos uma infraestrutura escalável horizontalmente conforme mostrado na figura abaixo:

Teremos um Load Balancer recebendo requisições HTTP e distribuindo as requisições via Round Robin para um pool escalável de máquinas virtuais. Para esse exemplo, seremos simplistas e nosso trigger para autoscaling será utilização de CPU, conseguimos mensurá-lo de forma facilitada através do ceilometer, que nos dá diversas informações dos recursos que estão sendo utilizados no openstack. Com ele, criaremos alarmes que serão ativados quando a utilização de CPU chegar em determinado percentual.

Primeiro definimos a versão do template para que o Heat saiba interpretar. Os parâmetros deste exemplo são a imagem com a aplicação a ser escalada, o par de chaves de acesso a instância, o flavor da instância, network, subnet e a rede externa para deixarmos o loadbalancer disponível para a internet. Todos estes itens já estão pré-configurados no openstack.

heat_template_version: 2016-10-14

description: Sample autoscaling with LBAAS

parameters:
  image:
    type: string
    description: Image used for web servers
    default: CentOS-7 64Bits 
  key:
    type: string
    description: SSH key to connect to the servers
    default: Ubuntu Victor
  web-flavor:
    type: string
    description: flavor used by the web servers
    default: small.2GB
  subnet_id:
    type: string
    description: subnet on which the load balancer will be located
    default: 2a4ddc88-57d3-4b6c-816e-86d2b623eeee

O segundo passo é definir os recursos.

Primeiramente, definimos o autoscaling group. Nesta seção, setamos a quantidade mínima e máxima de instâncias, bem como a configuração da instância propriamente dita; neste caso, deixamos a configuração da máquina em outro arquivo (scaledmachine.yaml). Além disso, apenas para exemplificar o scalling, iniciamos a máquina com um user_data que cria quatro loops infinitos para forçar o uso de cpu e verificarmos o scalling up em funcionamento.

Todo autoscaling deve ser regido por uma política; criamos duas, a de scale up (web_server_scaleup_policy) e scale down (web_server_scaledown_policy). Cada política tem um alarme como trigger; configuramos dois deles (cpu_alarm_high e cpu_alarm_low). Como podem ver, o alarme que escala para cima, ou seja, incrementa uma instância no pool, é ativado caso o uso de CPU seja maior que 60% no intervalo de tempo de um minuto. Caso o uso de CPU seja menor que 30% por 6 minutos, o alarme cpu_alarm_low é ativado, desescalando uma máquina.

O Recurso lb especifica a criação do load balancer na porta 80, o pool tem a informação dos recursos que estão atrelados ao loadbalancer, o monitor por sua vez é um Health Check do Load Balancer.

resources:
  autoscaling-gp:
    type: OS::Heat::AutoScalingGroup
    properties:
      min_size: 1
      max_size: 6
      resource:
        type: scaledmachine.yaml
        properties:
          flavor: {get_param: flavor}
          image: {get_param: image}
          key_name: {get_param: key}
          network: {get_param: network}
          pool_id: {get_resource: pool}
          metadata: {"metering.stack": {get_param: "OS::stack_id"}}
          user_data: |
            #!/bin/bash -v
            for i in 1 2 3 4; do while : ; do : ; done & done 
    
  web_server_scaleup_policy:
    type: OS::Heat::ScalingPolicy
    properties:
      adjustment_type: change_in_capacity
      auto_scaling_group_id: {get_resource: autoscaling-gp}
      cooldown: 60
      scaling_adjustment: 1

  web_server_scaledown_policy:
    type: OS::Heat::ScalingPolicy
    properties:
      adjustment_type: change_in_capacity
      auto_scaling_group_id: {get_resource: autoscaling-gp}
      cooldown: 360
      scaling_adjustment: -1

  cpu_alarm_high:
    type: OS::Ceilometer::Alarm
    properties:
      description: Scale-up if the average CPU > 60% for 1 minute
      meter_name: cpu_util
      statistic: avg
      period: 60
      evaluation_periods: 1
      threshold: 60
      alarm_actions:
        - {get_attr: [web_server_scaleup_policy, alarm_url]}
      matching_metadata: {'metadata.user_metadata.stack': {get_param: "OS::stack_id"}}
      comparison_operator: gt

  cpu_alarm_low:
    type: OS::Ceilometer::Alarm
    properties:
      description: Scale-down if the average CPU < 15% for 360 seconds
      meter_name: cpu_util
      statistic: avg
      period: 360
      evaluation_periods: 1
      threshold: 15
      alarm_actions:
        - {get_attr: [web_server_scaledown_policy, alarm_url]}
      matching_metadata: {'metadata.user_metadata.stack': {get_param: "OS::stack_id"}}
      comparison_operator: lt

  monitor:
    type: OS::Neutron::HealthMonitor
    properties:
      type: TCP
      delay: 5
      max_retries: 5
      timeout: 5

  pool:
    type: OS::Neutron::Pool
    properties:
      protocol: HTTP
      monitors: [{get_resource: monitor}]
      subnet_id: {get_param: subnet_id}
      lb_method: ROUND_ROBIN
      vip:
        protocol_port: 80

  lb:
    type: OS::Neutron::LoadBalancer
    properties:
      protocol_port: 80
      pool_id: {get_resource: pool}

  # assign a floating ip address to the load balancer
  lb_floating:
    type: OS::Neutron::FloatingIP
    properties:
      floating_network_id: {get_param: external_network_id}
      port_id: {get_attr: [pool, vip, port_id]}

Template de descrição da máquina virtual a ser escalada (scaledmachine.yaml):

heat_template_version: 2016-10-14

description: A load-balancer server
parameters:
  image:
    type: string
    description: Image used for servers
  key_name:
    type: string
    description: SSH key to connect to the servers
  flavor:
    type: string
    description: flavor used by the servers
  pool_id:
    type: string
    description: Pool to contact
  user_data:
    type: string
    description: Server user_data
  metadata:
    type: json
  network:
    type: string
    description: Network used by the server

resources:
  server:
    type: OS::Nova::Server
    properties:
      flavor: {get_param: flavor}
      image: {get_param: image}
      key_name: {get_param: key_name}
      metadata: {get_param: metadata}
      user_data: {get_param: user_data}
      user_data_format: RAW
      networks: [{network: {get_param: network} }]
  member:
    type: OS::Neutron::PoolMember
    properties:
      pool_id: {get_param: pool_id}
      address: {get_attr: [server, first_address]}
      protocol_port: 80

outputs:
  server_ip:
    description: IP Address of the load-balanced server.
    value: { get_attr: [server, first_address] }
  lb_member:
    description: LB member details.
    value: { get_attr: [member, show] }

Nesse exemplo, como já iniciamos a instância com grande uso de CPU, a cada minuto uma máquina deveria ser escalada. Contudo isso vai depender das configurações do OpenStack. Por padrão, o período de pooling do ceilometer é de 10 minutos, logo, em um caso como este, a cada 10 minutos uma máquina iria escalar.