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

ievlckzw7qhudvltrzgdvm-vv6s.png

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

Хочу поделиться своим опытом в настройке распределённого хранения данных в 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.

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


  • 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

Как это всё работает и как мы реализовали эту функцию? Давайте начнем с попадания логов в 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
    }
}

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


wsr07tvfx9vip38regk_4-o8ys4.jpeg


rojrlgtpymp_gzjv_b5-mqfni1o.jpeg


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-* с помощью шаблона с более высоким весом.


lyk0gedbtw2mpnnfhmhscl66r30.jpeg


urzn9hlr47wlubv8rxne7m3t8uu.jpeg


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-политику и начинаем следить за жизнью индексов.

d-pwrkrxqdcxrnu7xfkiz1jf7ek.jpeg

3y4zgfjk7delfybib7kviwcgfgi.jpeg

ltwiwybdhgkdpq1hovlyay6-go8.jpeg


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 должен быть равен или больше количества основных шардов, чтобы уместить их на одной ноде. В противном случае будут уведомления и шарды не переедут на нужные ноды.


3u7i9_tzlfkx6uo2wxbs67abkzi.jpeg


edyl0hz3rjlwno_ui6ysjkiazpe.jpeg


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 на индекс со всеми фазами жизни.


© Habrahabr.ru