Как мы Elasticsearch в порядок приводили: разделение данных, очистка, бэкапы

Эта статья — практическая история о том, как мы столкнулись с проблемой разделения логов, хранимых в Elasticsearch, из-за которой пришлось поменять подход к бэкапам и управлению индексами.

5swow2bqxoliv36e1r3eaboz-ym.png

Всё началось вскоре после того, как было поднято production-окружение. У нас был «боевой» кластер Kubernetes, все логи из которого собирал fluentd и направлял их напрямую в индексы logstash-yyy.mm.dd

rmuccezpyhbhaalkmv-n4dfc4xc.png

Однако появился запрос хранить некоторые логи приложений для поиска до 90 дней. На тот момент мы не могли себе этого позволить: хранение текущих индексов за такой период превысило бы все разумные меры. Поэтому было принято решение создать отдельный индекс-паттерн для таких приложений и настроить на него отдельный retention.

Разделение логов


Чтобы выполнить такую задачу и отделить «нужные» логи от «ненужных», мы воспользовались параметром match у fluentd и создали еще одну секцию в output.conf для поиска совпадений по нужным сервисам в пространстве имён production согласно тегированию, указанному для input-плагина (а также изменив @id и logstash_prefix, чтобы направить запись в разные места).

Фрагменты получившегося output.conf:


      @id elasticsearch1
      @type elasticsearch
      logstash_format true
      logstash_prefix log-prod
     ...


      @id elasticsearch
      @type elasticsearch
      logstash_format true
      logstash_prefix logstash
     ...


Итог — у нас два вида индексов (log-prod-yyyy.mm.dd и logstash-yyyy.mm.dd):

pdewn2hdkj-d0rzxnn1g0tc9bpo.png

Очистка индексов


На тот момент в кластере уже был настроен curator, который очищал индексы старше 14 дней. Он описывался как объект CronJob для разворачивания прямо в кластер Kubernetes.

Иллюстрация в action_file.yml:

-
    actions:
      1:
        action: delete_indices
        description: >-
          Delete indices older than 14 days (based on index name), for logstash-
          prefixed indices. Ignore the error if the filter does not result in an
          actionable list of indices (ignore_empty_list) and exit cleanly.
        options:
          ignore_empty_list: True
          timeout_override:
          continue_if_exception: False
          disable_action: False
          allow_ilm_indices: True
        filters:
        - filtertype: pattern
          kind: prefix
          value: logstash
        - filtertype: age
          source: name
          direction: older
          timestring: '%Y.%m.%d'
          unit: days
          unit_count: 14


Однако мы решили, что вариант с куратором избыточен (запускается как отдельное ПО, требует отдельной настройки и запуска по крону) — ведь можно переделать очистку с помощью index lifecycle policy.

В Elasticsearch с версии 6.6 (вышла в январе 2019 года) появилась возможность прикрепления к index template политики, которая будет отслеживать время хранения для индекса. Политики можно использовать не только для управления очисткой, но и других задач, которые позволят упростить взаимодействие с индексами (например, выравнивание индексов по размеру, а не по дням).

Для этого требовалось лишь создать две политики такого вида:

PUT _ilm/policy/prod_retention
{
    "policy" : {
      "phases" : {
        "delete" : {
          "min_age" : "90d",
          "actions" : {
            "delete" : { }
          }
        }
      }
    }
  }

PUT _ilm/policy/default_retention
{
    "policy" : {
      "phases" : {
        "delete" : {
          "min_age" : "14d",
          "actions" : {
            "delete" : { }
          }
        }
      }
    }
  }


… и прикрепить их к требуемым index templates:

PUT _template/log_prod_template
{
  "index_patterns": ["log-prod-*"],                 
  "settings": {
    "index.lifecycle.name": "prod_retention",        
  }
}
PUT _template/default_template
{
  "index_patterns": ["logstash-*"],                 
  "settings": {
    "index.lifecycle.name": "default_retention",        
  }
}


Теперь кластер Elasticsearch будет самостоятельно управлять хранением данных. Это означает, что все индексы, которые попадут под шаблон в index_patterns, указанный выше, будут очищаться после заданного в политике количества дней.

Но тут мы сталкиваемся с другой проблемой…

Реиндексирование внутри кластера и из удаленных кластеров


Созданные нами политики, шаблоны и те изменения, что были применены во fluentd, будут иметь эффект только для новых создаваемых индексов. Чтобы привести в порядок то, что уже имеем, придется запустить процесс переиндексации, обратившись к Reindex API.

Требуется выделить «нужные» логи из уже закрытых индексов и реиндексировать их самих, чтобы были применены политики.

Для этого достаточно двух простых запросов:

POST _reindex
{
  "source": {
    "index": "logstash-2019.10.24",
    "query": {
      "match": {
        "kubernetes.namespace_name": "production"
      }
    }
  },
  "dest": {
    "index": "log-prod-2019.10.24"
  }
}

POST _reindex
{
  "source": {
    "index": "logstash-2019.10.24"
    "query": {
      "bool": { 
        "must_not": {
          "match": {
            "kubernetes.namespace_name": "production"
           }
        }
      }
    }
  },
  "dest": {
    "index": "logstash-2019.10.24-ri"
  }
}


Теперь старые индексы можно удалить!

Однако есть и другие трудности. Как уже известно, ранее был настроен curator, который чистил кластер с retention в 14 дней. Таким образом, для актуальных для нас сервисов потребуется восстановить данные и из того, что уже было когда-то удалено. Здесь помогут бэкапы.

В простейшем случае, что применимо и к нашему, бэкапы делаются посредством вызова elasticdump для всех индексов разом:

/usr/bin/elasticdump --all $OPTIONS --input=http://localhost:9200 --output=$


Развернуть все, что было от запуска production, не представлялось возможным, т.к. в кластере банально не было столько места. К тому же, доступ к логам из бэкапа требовался уже сейчас.

Решение — развернуть временный кластер, в который восстанавливается бэкап, откуда уже и достаются нужные нам логи (способом, аналогичным уже описанному). А параллельно мы начали думать, как сделать снятие бэкапов более удобным способом — подробнее об этом см. ниже.

Итак, следующие шаги (по временному кластеру):

  1. Установить еще один Elasticsearch на отдельный сервер с достаточно большим диском.
  2. Развернуть бэкап из имеющегося у нас dump-файла:
    /usr/bin/elasticdump --bulk --input=dump.json --output=http://127.0.0.1:9200/
  3. Обратите внимание, что индексы в кластере не приобретут состояние green, т.к. мы переносим дамп в 1-узловую конфигурацию. Но переживать из-за этого не стоит: главное — вытащить только primal-шарды.
  4. Не нужно забывать, что для разрешения реиндексирования из удаленных кластеров нужно добавить их в whitelist в elasticsearch.yaml:
    reindex.remote.whitelist: 192.168.3.221:9200
  5. Далее произведем запрос на реиндексацию из удаленного кластера в текущем production-кластере:
    POST _reindex
    {
      "source": {
        "remote": {
          "host": "http://1.2.3.4:9200"
        },
        "size": 10000,
        "index": "logstash-2019.10.24",
        "query": {
          "match": {
            "kubernetes.namespace_name": "production"
          }
        }
      },
      "dest": {
        "index": "log-prod-2019.10.24"
      }
    }


Этим запросом мы получаем из индексов в удаленном кластере документы, которые будут передаваться в production-кластер по сети. На стороне временного кластера все документы, не подходящие под запрос, будут отфильтрованы.

Параметры size и slice используются для ускорения процесса реиндексации:

  • size — для увеличения количества документов для индексации, передаваемых в пакете;
  • slice — для деления этой задачи на 6 подзадач, которые будут параллельно заниматься реиндексированием. (Параметр не работает в реиндексации из удаленных кластеров).


На принимающем Elasticsearch мы настроили шаблоны индексов на максимальную производительность.

Когда все нужные логи оказались в одном месте полностью разделенными так, как требовалось, временный кластер можно удалять:

4m9yxo7bcko3dslumlqgfeg0ufa.png

Бэкапы Elasticsearch


Самое время вернуться к бэкапам: запуск elasticdump на все индексы — далеко не самое оптимальное решение. Хотя бы по той причине, что восстановление может занимать неприлично много времени, а бывают случаи, когда важен каждый час.

Такую задачу можно «отдать» самому Elasticsearch — Snapshot Repository на базе S3. И вот основные причины, по которым мы сами выбрали такой подход:

  • Удобство создания и восстановления из таких бэкапов, поскольку используется родной синтаксис запросов к Elasticsearch.
  • Снапшоты накатываются инкрементально, т.е. добавляя новые данные к уже имеющимся.
  • Возможность восстановить из снапшота любой индекс и даже восстановить глобальное состояние кластера.
  • S3 кажется более надежным местом для хранения бэкапов, чем просто файл (в том числе и по той причине, что обычно мы используем S3 в режимах HA).
  • Переносимость S3: мы не привязаны к конкретным провайдерам и можем развернуть S3 на новом месте самостоятельно.


Настройка бэкапа в S3 требует дополнительной установки плагина в Elasticsearch, чтобы Snapshot Repository мог общаться c S3. Вся конфигурация сводится к следующим шагам:

  1. Устанавливаем плагин для S3 на все узлы:
    bin/elasticsearch-plugin install repository-s3

    … и параллельно добавляем S3 в whitelist в elasticsearch.yml:
        Repositories.url.allowed_urls:
    - "https://example.com/*"
  2. Добавляем ключи SECRET и ACCESS в Elasticsearch keystore. По умолчанию для подключения к S3 используется пользователь default:
    bin/elasticsearch-keystore add s3.client.default.access_key
    bin/elasticsearch-keystore add s3.client.default.secret_key

    … и после этого поочередно перезапускаем сервис Elasticsearch на всех узлах.
  3. Создаем репозиторий для снапшотов:
    PUT /_snapshot/es_s3_repository
    {
      "type": "s3",
      "settings": {
        "bucket": "es-snapshot",
        "region": "us-east-1",
        "endpoint": "https://example.com"
      }
    }

    В данном случае для создания снапшота хватит примеров из документации, где имя снапшота будет в формате: snapshot-2018.05.11, — т.е.:
    PUT /_snapshot/my_backup/%3Csnapshot-%7Bnow%2Fd%7D%3E
  4. Осталось лишь протестировать восстановление индекса:
    POST /_snapshot/es_s3_repository/snapshot-2019.12.30/_restore
    {
      "indices": "logstash-2019.12.29",
      "rename_pattern": "logstash-(.+)",
      "rename_replacement": "restored_index_$1"
    }

    Так мы восстанавливаем индекс «рядом», просто переименовав его. Однако можно восстановить его и в тот же индекс — только для этого индекс в кластере должен быть закрыт и иметь такое же количество шардов, что и индекс в снапшоте.


Статус снятия снапшота можно проверять по имени через API, вызвав информацию о снапшоте и посмотрев в поле state:

GET /_snapshot/es_s3_repository/snapshot_2019.12.30


Статус восстановления же можно проверять из самого кластера: в начале восстановления кластер перейдет в статус red, т.к. будут восстанавливаться primal-шарды ваших индексов. Как только процесс будет завершен, индексы и кластер перейдут в статус yellow — до того момента, как будет создано указанное количество реплик.

Также отслеживать статус можно с помощью wait_for_completion=true, указанном прямо в строке запроса.

Результат — получаем точную копию индекса из сделанного снапшота:

green open restored_index_2019.12.29    eJ4wXqg9RAebo1Su7PftIg 1 1 1836257 0   1.9gb 1000.1mb
green open logstash-2019.12.29          vE8Fyb40QiedcW0k7vNQZQ 1 1 1836257 0   1.9gb 1000.1mb


Итоги и недостатки


Оптимальными для нас решениям в кластере Elasticsearch оказались:

  • Настройка output-плагина в fluentd, с которой можно разделять логи прямо на выходе из кластера.
  • Политика index lifecycle, позволяющая не заботиться о проблемах с местом, занятым индексами.
  • Новый вид бэкапов через snapshot repository (вместо бэкапа целого кластера), с которым появилась возможность восстанавливать отдельные индексы из S3, что стало гораздо удобнее.


Впрочем, полезно будет предостеречь, что изменения в политиках и шаблонах из-за изменяющихся потребностей в дальнейшем приведут к необходимости реиндексировать все индексы в кластере. А с бэкапами картина ещё дальше от идеала…

Несмотря на то, что мы «передали» выполнение бэкапов самому Elasticsearch, запуск снятия снапшота и наблюдение за его выполнением всё еще остается нашей задачей: обращение к API, отслеживание статусов (через wait_for_completion) и по статусу мы будем производить сами с помощью скриптов. При всей удобности backup repository есть у него и другая проблема: слишком мало плагинов. Например, нет возможности работать через WebDAV, если вместо S3 понадобится что-то совсем простое, но такое же мобильное.

В целом, такая обособленность системы плохо ложится на использование централизованного подхода к бэкапам и мы пока не нашли (среди Open Source-инструментов) универсальное средство, которое бы это позволило.

P.S.


Читайте также в нашем блоге:

© Habrahabr.ru