Almacenamiento de datos a largo plazo en Elasticsearch

Almacenamiento de datos a largo plazo en Elasticsearch

Mi nombre es Igor Sidorenko, soy un líder técnico en el equipo de administradores que mantienen toda la infraestructura de Domclick.

Quiero compartir mi experiencia en la configuración de almacenamiento de datos distribuidos en Elasticsearch. Veremos qué configuraciones en los nodos son responsables de la distribución de fragmentos, cómo funciona y funciona ILM.

Quienes trabajan con registros, de una forma u otra, enfrentan el problema del almacenamiento a largo plazo para su posterior análisis. En Elasticsearch, esto es especialmente cierto, porque todo fue desafortunado con la funcionalidad del curador. La versión 6.6 introdujo la funcionalidad ILM. Consta de 4 fases:

  • Caliente: el índice se está actualizando y consultando activamente.
  • Templado: el índice ya no se actualiza, pero aún se consulta.
  • Frío: el índice ya no se actualiza y rara vez se consulta. La información aún debe poder buscarse, pero las consultas pueden ser más lentas.
  • Eliminar: el índice ya no es necesario y se puede eliminar de forma segura.

Dado

  • Elasticsearch Data Hot: 24 procesadores, 128 GB de memoria, SSD RAID 1,8 de 10 TB (8 nodos).
  • Elasticsearch Data Warm: 24 procesadores, 64 GB de memoria, política de SSD de NetApp de 8 TB (4 nodos).
  • Elasticsearch Data Cold: 8 procesadores, 32 GB de memoria, 128 TB HDD RAID 10 (4 nodos).

objetivo

Estas configuraciones son individuales, todo depende del lugar en los nodos, la cantidad de índices, registros, etc. Tenemos 2-3 TB de datos por día.

  • 5 días - Fase caliente (8 principales / 1 réplica).
  • 20 días - Fase cálida (índice de contracción 4 principales / 1 réplica).
  • 90 días - Fase fría (índice de congelación 4 principales / 1 réplica).
  • 120 días - Fase de eliminación.

Configuración de Elasticsearch

Para distribuir un fragmento entre nodos, solo necesita un parámetro:

  • Popular-nodos:
    ~]# cat /etc/elasticsearch/elasticsearch.yml | grep attr
    # Add custom attributes to the node:
    node.attr.box_type: hot
  • Templado-nodos:
    ~]# cat /etc/elasticsearch/elasticsearch.yml | grep attr
    # Add custom attributes to the node:
    node.attr.box_type: warm
  • Frío-nodos:
    ~]# cat /etc/elasticsearch/elasticsearch.yml | grep attr
    # Add custom attributes to the node:
    node.attr.box_type: cold

Configuración de Logstash

¿Cómo funciona todo y cómo implementamos esta característica? Comencemos por obtener registros en Elasticsearch. Hay dos maneras:

  1. Logstash obtiene registros de Kafka. Puede recoger limpio o convertir de su lado.
  2. Algo en sí mismo escribe en Elasticsearch, por ejemplo, un servidor APM.

Considere un ejemplo de administración de índices a través de Logstash. Crea un índice y se aplica a él. patrón de índice y correspondiente ILM.

k8s-ingreso.conf

input {
    kafka {
        bootstrap_servers => "node01, node02, node03"
        topics => ["ingress-k8s"]
        decorate_events => false
        codec => "json"
    }
}

filter {
    ruby {
        path => "/etc/logstash/conf.d/k8s-normalize.rb"
    }
    if [log] =~ "[warn]" or [log] =~ "[error]" or [log] =~ "[notice]" or [log] =~ "[alert]" {
        grok {
            match => { "log" => "%{DATA:[nginx][error][time]} [%{DATA:[nginx][error][level]}] %{NUMBER:[nginx][error][pid]}#%{NUMBER:[nginx][error][tid]}: *%{NUMBER:[nginx][error][connection_id]} %{DATA:[nginx][error][message]}, client: %{IPORHOST:[nginx][error][remote_ip]}, server: %{DATA:[nginx][error][server]}, request: "%{WORD:[nginx][error][method]} %{DATA:[nginx][error][url]} HTTP/%{NUMBER:[nginx][error][http_version]}", (?:upstream: "%{DATA:[nginx][error][upstream][proto]}://%{DATA:[nginx][error][upstream][host]}:%{DATA:[nginx][error][upstream][port]}/%{DATA:[nginx][error][upstream][url]}", )?host: "%{DATA:[nginx][error][host]}"(?:, referrer: "%{DATA:[nginx][error][referrer]}")?" }
            remove_field => "log"
        }
    }
    else {
        grok {
            match => { "log" => "%{IPORHOST:[nginx][access][host]} - [%{IPORHOST:[nginx][access][remote_ip]}] - %{DATA:[nginx][access][remote_user]} [%{HTTPDATE:[nginx][access][time]}] "%{WORD:[nginx][access][method]} %{DATA:[nginx][access][url]} HTTP/%{NUMBER:[nginx][access][http_version]}" %{NUMBER:[nginx][access][response_code]} %{NUMBER:[nginx][access][bytes_sent]} "%{DATA:[nginx][access][referrer]}" "%{DATA:[nginx][access][agent]}" %{NUMBER:[nginx][access][request_lenght]} %{NUMBER:[nginx][access][request_time]} [%{DATA:[nginx][access][upstream][name]}] (?:-|%{IPORHOST:[nginx][access][upstream][addr]}:%{NUMBER:[nginx][access][upstream][port]}) (?:-|%{NUMBER:[nginx][access][upstream][response_lenght]}) %{DATA:[nginx][access][upstream][response_time]} %{DATA:[nginx][access][upstream][status]} %{DATA:[nginx][access][request_id]}" }
            remove_field => "log"
        }
    }
}
output {
    elasticsearch {
        id => "k8s-ingress"
        hosts => ["node01", "node02", "node03", "node04", "node05", "node06", "node07", "node08"]
        manage_template => true # включаем управление шаблонами
        template_name => "k8s-ingress" # имя применяемого шаблона
        ilm_enabled => true # включаем управление ILM
        ilm_rollover_alias => "k8s-ingress" # alias для записи в индексы, должен быть уникальным
        ilm_pattern => "{now/d}-000001" # шаблон для создания индексов, может быть как "{now/d}-000001" так и "000001"
        ilm_policy => "k8s-ingress" # политика прикрепляемая к индексу
        index => "k8s-ingress-%{+YYYY.MM.dd}" # название создаваемого индекса, может содержать %{+YYYY.MM.dd}, зависит от ilm_pattern
    }
}

configuración de kibana

Hay un patrón base que se aplica a todos los índices nuevos. Establece la distribución de índices calientes, el número de fragmentos, réplicas, etc. El peso de la plantilla está determinado por la opción order. Las plantillas con un peso más alto anulan los parámetros de plantilla existentes o agregan otros nuevos.

Almacenamiento de datos a largo plazo en Elasticsearch
Almacenamiento de datos a largo plazo en Elasticsearch

GET_plantilla/predeterminado

{
  "default" : {
    "order" : -1, # вес шаблона
    "version" : 1,
    "index_patterns" : [
      "*" # применяем ко всем индексам
    ],
    "settings" : {
      "index" : {
        "codec" : "best_compression", # уровень сжатия
        "routing" : {
          "allocation" : {
            "require" : {
              "box_type" : "hot" # распределяем только по горячим нодам
            },
            "total_shards_per_node" : "8" # максимальное количество шардов на ноду от одного индекса
          }
        },
        "refresh_interval" : "5s", # интервал обновления индекса
        "number_of_shards" : "8", # количество шардов
        "auto_expand_replicas" : "0-1", # количество реплик на ноду от одного индекса
        "number_of_replicas" : "1" # количество реплик
      }
    },
    "mappings" : {
      "_meta" : { },
      "_source" : { },
      "properties" : { }
    },
    "aliases" : { }
  }
}

Luego aplique el mapeo a los índices. k8s-ingress-* usando una plantilla con un peso más alto.

Almacenamiento de datos a largo plazo en Elasticsearch
Almacenamiento de datos a largo plazo en Elasticsearch

OBTENER _template/k8s-ingreso

{
  "k8s-ingress" : {
    "order" : 100,
    "index_patterns" : [
      "k8s-ingress-*"
    ],
    "settings" : {
      "index" : {
        "lifecycle" : {
          "name" : "k8s-ingress",
          "rollover_alias" : "k8s-ingress"
        },
        "codec" : "best_compression",
        "routing" : {
          "allocation" : {
            "require" : {
              "box_type" : "hot"
            }
          }
        },
        "number_of_shards" : "8",
        "number_of_replicas" : "1"
      }
    },
    "mappings" : {
      "numeric_detection" : false,
      "_meta" : { },
      "_source" : { },
      "dynamic_templates" : [
        {
          "all_fields" : {
            "mapping" : {
              "index" : false,
              "type" : "text"
            },
            "match" : "*"
          }
        }
      ],
      "date_detection" : false,
      "properties" : {
        "kubernetes" : {
          "type" : "object",
          "properties" : {
            "container_name" : {
              "type" : "keyword"
            },
            "container_hash" : {
              "index" : false,
              "type" : "keyword"
            },
            "host" : {
              "type" : "keyword"
            },
            "annotations" : {
              "type" : "object",
              "properties" : {
                "value" : {
                  "index" : false,
                  "type" : "text"
                },
                "key" : {
                  "index" : false,
                  "type" : "keyword"
                }
              }
            },
            "docker_id" : {
              "index" : false,
              "type" : "keyword"
            },
            "pod_id" : {
              "type" : "keyword"
            },
            "labels" : {
              "type" : "object",
              "properties" : {
                "value" : {
                  "type" : "keyword"
                },
                "key" : {
                  "type" : "keyword"
                }
              }
            },
            "namespace_name" : {
              "type" : "keyword"
            },
            "pod_name" : {
              "type" : "keyword"
            }
          }
        },
        "@timestamp" : {
          "type" : "date"
        },
        "nginx" : {
          "type" : "object",
          "properties" : {
            "access" : {
              "type" : "object",
              "properties" : {
                "agent" : {
                  "type" : "text"
                },
                "response_code" : {
                  "type" : "integer"
                },
                "upstream" : {
                  "type" : "object",
                  "properties" : {
                    "port" : {
                      "type" : "keyword"
                    },
                    "name" : {
                      "type" : "keyword"
                    },
                    "response_lenght" : {
                      "type" : "integer"
                    },
                    "response_time" : {
                      "index" : false,
                      "type" : "text"
                    },
                    "addr" : {
                      "type" : "keyword"
                    },
                    "status" : {
                      "index" : false,
                      "type" : "text"
                    }
                  }
                },
                "method" : {
                  "type" : "keyword"
                },
                "http_version" : {
                  "type" : "keyword"
                },
                "bytes_sent" : {
                  "type" : "integer"
                },
                "request_lenght" : {
                  "type" : "integer"
                },
                "url" : {
                  "type" : "text",
                  "fields" : {
                    "keyword" : {
                      "type" : "keyword"
                    }
                  }
                },
                "remote_user" : {
                  "type" : "text"
                },
                "referrer" : {
                  "type" : "text"
                },
                "remote_ip" : {
                  "type" : "ip"
                },
                "request_time" : {
                  "format" : "yyyy/MM/dd HH:mm:ss||yyyy/MM/dd||epoch_millis||dd/MMM/YYYY:H:m:s Z",
                  "type" : "date"
                },
                "host" : {
                  "type" : "keyword"
                },
                "time" : {
                  "format" : "yyyy/MM/dd HH:mm:ss||yyyy/MM/dd||epoch_millis||dd/MMM/YYYY:H:m:s Z",
                  "type" : "date"
                }
              }
            },
            "error" : {
              "type" : "object",
              "properties" : {
                "server" : {
                  "type" : "keyword"
                },
                "upstream" : {
                  "type" : "object",
                  "properties" : {
                    "port" : {
                      "type" : "keyword"
                    },
                    "proto" : {
                      "type" : "keyword"
                    },
                    "host" : {
                      "type" : "keyword"
                    },
                    "url" : {
                      "type" : "text",
                      "fields" : {
                        "keyword" : {
                          "type" : "keyword"
                        }
                      }
                    }
                  }
                },
                "method" : {
                  "type" : "keyword"
                },
                "level" : {
                  "type" : "keyword"
                },
                "http_version" : {
                  "type" : "keyword"
                },
                "pid" : {
                  "index" : false,
                  "type" : "integer"
                },
                "message" : {
                  "type" : "text"
                },
                "tid" : {
                  "index" : false,
                  "type" : "keyword"
                },
                "url" : {
                  "type" : "text",
                  "fields" : {
                    "keyword" : {
                      "type" : "keyword"
                    }
                  }
                },
                "referrer" : {
                  "type" : "text"
                },
                "remote_ip" : {
                  "type" : "ip"
                },
                "connection_id" : {
                  "index" : false,
                  "type" : "keyword"
                },
                "host" : {
                  "type" : "keyword"
                },
                "time" : {
                  "format" : "yyyy/MM/dd HH:mm:ss||yyyy/MM/dd||epoch_millis||dd/MMM/YYYY:H:m:s Z",
                  "type" : "date"
                }
              }
            }
          }
        },
        "log" : {
          "type" : "text"
        },
        "@version" : {
          "type" : "text",
          "fields" : {
            "keyword" : {
              "ignore_above" : 256,
              "type" : "keyword"
            }
          }
        },
        "eventtime" : {
          "type" : "float"
        }
      }
    },
    "aliases" : { }
  }
}

Después de aplicar todas las plantillas, aplicamos la política ILM y comenzamos a monitorear la vida de los índices.

Almacenamiento de datos a largo plazo en Elasticsearch

Almacenamiento de datos a largo plazo en Elasticsearch

Almacenamiento de datos a largo plazo en Elasticsearch

OBTENER _ilm/policy/k8s-ingreso

{
  "k8s-ingress" : {
    "version" : 14,
    "modified_date" : "2020-06-11T10:27:01.448Z",
    "policy" : {
      "phases" : {
        "warm" : { # теплая фаза
          "min_age" : "5d", # срок жизни индекса после ротации до наступления теплой фазы
          "actions" : {
            "allocate" : {
              "include" : { },
              "exclude" : { },
              "require" : {
                "box_type" : "warm" # куда перемещаем индекс
              }
            },
            "shrink" : {
              "number_of_shards" : 4 # обрезание индексов, т.к. у нас 4 ноды
            }
          }
        },
        "cold" : { # холодная фаза
          "min_age" : "25d", # срок жизни индекса после ротации до наступления холодной фазы
          "actions" : {
            "allocate" : {
              "include" : { },
              "exclude" : { },
              "require" : {
                "box_type" : "cold" # куда перемещаем индекс
              }
            },
            "freeze" : { } # замораживаем для оптимизации
          }
        },
        "hot" : { # горячая фаза
          "min_age" : "0ms",
          "actions" : {
            "rollover" : {
              "max_size" : "50gb", # максимальный размер индекса до ротации (будет х2, т.к. есть 1 реплика)
              "max_age" : "1d" # максимальный срок жизни индекса до ротации
            },
            "set_priority" : {
              "priority" : 100
            }
          }
        },
        "delete" : { # фаза удаления
          "min_age" : "120d", # максимальный срок жизни после ротации перед удалением
          "actions" : {
            "delete" : { }
          }
        }
      }
    }
  }
}

Problemas

Hubo problemas en la etapa de configuración y depuración.

Fase caliente

Para la correcta rotación de índices, la presencia al final es crítica index_name-date-000026 números de formato 000001. Hay líneas en el código que verifican los índices usando una expresión regular para detectar la presencia de números al final. De lo contrario, habrá un error, no se aplicarán políticas al índice y siempre estará en la fase activa.

Fase cálida

Encogimiento (cutoff) — reduciendo el número de fragmentos, porque tenemos 4 nodos en las fases cálida y fría. La documentación contiene las siguientes líneas:

  • El índice debe ser de solo lectura.
  • Una copia de cada fragmento del índice debe residir en el mismo nodo.
  • El estado de salud del clúster debe ser verde.

Para podar un índice, Elasticsearch mueve todos los fragmentos primarios a un nodo, duplica el índice truncado con los parámetros necesarios y luego elimina el anterior. Parámetro total_shards_per_node debe ser igual o mayor que el número de fragmentos principales para caber en un nodo. De lo contrario, habrá notificaciones y los fragmentos no se moverán a los nodos correctos.

Almacenamiento de datos a largo plazo en Elasticsearch
Almacenamiento de datos a largo plazo en Elasticsearch

OBTENER /shrink-k8s-ingress-2020.06.06-000025/_settings

{
  "shrink-k8s-ingress-2020.06.06-000025" : {
    "settings" : {
      "index" : {
        "refresh_interval" : "5s",
        "auto_expand_replicas" : "0-1",
        "blocks" : {
          "write" : "true"
        },
        "provided_name" : "shrink-k8s-ingress-2020.06.06-000025",
        "creation_date" : "1592225525569",
        "priority" : "100",
        "number_of_replicas" : "1",
        "uuid" : "psF4MiFGQRmi8EstYUQS4w",
        "version" : {
          "created" : "7060299",
          "upgraded" : "7060299"
        },
        "lifecycle" : {
          "name" : "k8s-ingress",
          "rollover_alias" : "k8s-ingress",
          "indexing_complete" : "true"
        },
        "codec" : "best_compression",
        "routing" : {
          "allocation" : {
            "initial_recovery" : {
              "_id" : "_Le0Ww96RZ-o76bEPAWWag"
            },
            "require" : {
              "_id" : null,
              "box_type" : "cold"
            },
            "total_shards_per_node" : "8"
          }
        },
        "number_of_shards" : "4",
        "routing_partition_size" : "1",
        "resize" : {
          "source" : {
            "name" : "k8s-ingress-2020.06.06-000025",
            "uuid" : "gNhYixO6Skqi54lBjg5bpQ"
          }
        }
      }
    }
  }
}

Fase fría

Congelar (congelar) - Congelamos el índice para optimizar las consultas sobre datos históricos.

Las búsquedas realizadas en índices congelados usan el grupo de subprocesos de búsqueda limitado, pequeño y dedicado para controlar la cantidad de búsquedas simultáneas que alcanzan fragmentos congelados en cada nodo. Esto limita la cantidad de memoria adicional requerida para las estructuras de datos transitorios correspondientes a fragmentos congelados, lo que, en consecuencia, protege a los nodos contra el consumo excesivo de memoria.
Los índices congelados son de solo lectura: no puede indexarlos.
Se espera que las búsquedas en índices congelados se ejecuten lentamente. Los índices congelados no están destinados a una alta carga de búsqueda. Es posible que la búsqueda de un índice congelado tarde segundos o minutos en completarse, incluso si las mismas búsquedas se completaron en milisegundos cuando los índices no estaban congelados.

resultados

Aprendimos a preparar nodos para trabajar con ILM, configurar una plantilla para distribuir fragmentos entre nodos calientes y configurar ILM para un índice con todas las fases de la vida.

Enlaces de interés

Fuente: habr.com