Долгосрочное хранение данных в Elasticsearch

  • Tutorial


Меня зовут Игорь Сидоренко, я техлид в команде админов, поддерживающих в рабочем состоянии всю инфраструктуру Домклик.


Хочу поделиться своим опытом в настройке распределённого хранения данных в Elasticsearch. Мы рассмотрим, какие настройки на нодах отвечают за распределение шардов, как устроен и работает ILM.


Те, кто работают с логами, так или иначе сталкиваются с проблемой долгосрочного хранение для последующего анализа. В Elasticsearch это особенно актуально, потому что с функциональностью куратора всё было прискорбно. В версии 6.6 появился функционал ILM. Он состоит из 4 фаз:


  • Hot — индекс активно обновляется и запрашивается.
  • Warm — индекс больше не обновляется, но всё ещё запрашивается.
  • Cold — индекс больше не обновляется и редко запрашивается. Информация всё ещё должна быть доступна для поиска, но запросы могут выполняться медленнее.
  • Delete — индекс больше не нужен и может быть безопасно удален.

Дано


  • Elasticsearch Data Hot: 24 процессора, 128 Гб памяти, 1,8 Тб SSD RAID 10 (8 нод).
  • Elasticsearch Data Warm: 24 процессора, 64 Гб памяти, 8 Тб NetApp SSD Policy (4 ноды).
  • Elasticsearch Data Cold: 8 процессоров, 32 Гб памяти, 128 Тб HDD RAID 10 (4 ноды).

Цель


Эти настройки индивидуальны, всё зависит от места на нодах, количества индексов, логов и т.д. У нас это 2-3 Тб данных за сутки.


  • 5 дней — фаза Hot (8 основных / 1 реплика).
  • 20 дней — фаза Warm (shrink-индекс 4 основных / 1 реплика).
  • 90 дней — фаза Cold (freeze-индекс 4 основных / 1 реплика).
  • 120 дней — фаза Delete.

Настройка Elasticsearch


Для распределения шард по нодам нужен всего один параметр:


  • Hot-ноды:
    ~]# cat /etc/elasticsearch/elasticsearch.yml | grep attr
    # Add custom attributes to the node:
    node.attr.box_type: hot
  • Warm-ноды:
    ~]# cat /etc/elasticsearch/elasticsearch.yml | grep attr
    # Add custom attributes to the node:
    node.attr.box_type: warm
  • Cold-ноды:
    ~]# cat /etc/elasticsearch/elasticsearch.yml | grep attr
    # Add custom attributes to the node:
    node.attr.box_type: cold

Настройка Logstash


Как это всё работает и как мы реализовали эту функцию? Давайте начнем с попадания логов в Elasticsearch. Есть два способа:


  1. Logstash забирает логи из Kafka. Может забрать чистыми или преобразовать на своей стороне.
  2. Что-то само пишет в Elasticsearch, например, APM-сервер.

Рассмотрим пример управления индексами через Logstash. Он создает индекс и применяет к нему шаблон индекса и соответствующий ILM.


k8s-ingress.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
    }
}

Настройка Kibana


Есть базовый шаблон, который применяется ко всем новым индексам. Он задаёт распределение горячих индексов, количество шардов, реплик и т.д. Вес шаблона определяется опцией order. Шаблоны с более высоким весом переопределяют уже существующие параметры шаблона или добавляют новые.




GET _template/default
{
  "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" : { }
  }
}

Затем применим маппинг к индексам k8s-ingress-* с помощью шаблона с более высоким весом.




GET _template/k8s-ingress
{
  "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" : { }
  }
}

После применения всех шаблонов мы применяем ILM-политику и начинаем следить за жизнью индексов.





GET _ilm/policy/k8s-ingress
{
  "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" : { }
          }
        }
      }
    }
  }
}

Проблемы


Были проблемы на этапе настройки и отладки.


Hot-фаза


Для корректной ротации индексов критично присутствие в конце index_name-date-000026 чисел формата 000001. В коде есть строчки, которые проверяют индексы с помощью регулярного выражения на наличие чисел в конце. Иначе будет ошибка, к индексу не применятся политики и он всегда будет в hot-фазе.


Warm-фаза


Shrink (обрезание) — уменьшение количества шардов, потому что нод в теплой и холодной фазах у нас по 4. В документации есть такие строчки:


  • The index must be read-only.
  • A copy of every shard in the index must reside on the same node.
  • The cluster health status must be green.

Чтобы урезать индекс, Elasticsearch перемещает все основные (primary) шарды на одну ноду, дублирует урезанный индекс с необходимыми параметрами, а потом удаляет старый. Параметр total_shards_per_node должен быть равен или больше количества основных шардов, чтобы уместить их на одной ноде. В противном случае будут уведомления и шарды не переедут на нужные ноды.




GET /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"
          }
        }
      }
    }
  }
}

Cold-фаза


Freeze (заморозка) — мы замораживаем индекс для оптимизации запросов по историческим данным.


Searches performed on frozen indices use the small, dedicated, search_throttled threadpool to control the number of concurrent searches that hit frozen shards on each node. This limits the amount of extra memory required for the transient data structures corresponding to frozen shards, which consequently protects nodes against excessive memory consumption.
Frozen indices are read-only: you cannot index into them.
Searches on frozen indices are expected to execute slowly. Frozen indices are not intended for high search load. It is possible that a search of a frozen index may take seconds or minutes to complete, even if the same searches completed in milliseconds when the indices were not frozen.

Итоги


Мы научились подготавливать ноды для работы с ILM, настроили шаблон для распределения шардов по горячим нодам и настроили ILM на индекс со всеми фазами жизни.


Полезные ссылки


ДомКлик
Место силы

Комментарии 6

    +1
    То чувство, когда понимаешь что пора апгрейдить кластер :) В 6.х к сожалению такого нет, приходится двигать индексы скриптами.
      0
      Та-ак, а сакральное знание-то где? Или это для тех, кто не читает документацию? :)
        +3
        По большей части да. Разжевывание темы ILM и небольшой обзор нюансов, с которыми мы столкнулись в процессе настройки.
        +2
        Добавлю пять копеек:
        Что-бы kibana искала в замороженных индексах, необходимо включить соответствующий параметр «Search in frozen indices».
        В хот фазе для логов по одному дню rollover не особо нужен и, если его убрать, то можно не заморачиваться с нумерацией вида 000001.
        В варм фазе, если индекс не урезать, то он остаётся доступен для записи — что может быть тоже полезно.
          +2
          Спасибо за дополнение, про Kibana упустил.
          +1
          А еще если выгружаете elasticdump ом, из frozen не забудьте добавить ```--params='{«ignore_throttled»: «false»}'```
          Ну и экспорт в CSV Вроде до сих пор не работает с frozen индексами (в 6ке точно)
          так что, хоть штука с замораживанием и годная. Но костылей еще много в ней.

          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

          Самое читаемое