Нулевой даунтайм API на ElasticSearch

Kate

Administrator
Команда форума
В статье я хочу поделиться своим опытом достижения нулевого даунтайма ответа одного из API, использующего большой индекс ElasticSearch. Эта статья будет полезна тем, кто уже использует ElasticSearch и не может позволить себе ни минуты простоя, а один из ваших индексов продолжает расти. Также будет полезна тем, кто работает над ускорением наполнения индекса или пытается сформировать группу индексов, например несколько индексов для разных городов или стран.

Столкнулась наша команда с двумя из описанных выше проблем: ускорение наполнения и обеспечение минимального даунтайма ответа API. Все это случилось довольно быстро, когда количество потенциальных документов для индексации стало превышать 1 млн записей. До этого момента проблема решалась оптимизацией кода и запросов в БД. Конечно, подготовка такого объема данных и формирование индекса занимает не один час. Усложнило задачу то, что индекс использовался в нескольких ключевых API системы, которыми пользуется мобильное приложение, компании, имеющие интеграцию с нашим сервисом, внутренние инструменты технической поддержки. Над индексом постоянно выполняются CRUD-операции.

В статье будут приводиться команды для управления индексами на примере версии ElasticSearch 5.6. В целом описанный подход не ограничивается версиями, но приведенные curl запросы могут отличаться в зависимости от версии.

Часть 1. Ускорение наполнения индекса​

Итак, если ваш индекс неумолимо растет, и вы поняли, что есть потребность ускорения наполнения:

1. Постарайтесь максимально оптимизировать запросы в БД и получать их порциями для обработки сразу нескольких документов и записи батчами (Bulk API)

2. Если у вас есть один синхронный скрипт, который итерационно добавляет записи батчами по 10000 штук, то возможно стоит реализовать запуск параллельных скриптов обработчиков. Вы можете создать один основной job, который будет порождать n других. В них мы можем передавать в виде аргументов необходимые данные, например текущие параметры offset и limit. Тем самым вы ускорите подготовку данных, но будьте осторожны, если все n job будут приходить с батчами на запись одновременно, то ElasticSearch может начать медленно обрабатывать данные. Это связано с ресурсами, выделенными под ElasticSearch, с настройками индекса и самого ElasticSearch. Необходимо подбирать оптимальное значение количества документов в одном батче, мониторить состояние ElasticSearch и JVM. Возможным решением будет запуск каждой из джоб с рандомной задержкой, чтобы избежать одновременных запросов на сохранение большого объема данных сразу несколькими обработчиками

3. Если вы используете репликацию индексов (параметр репликации number_of_replicas больше нуля) и refresh_interval не равный -1, то индексация документов будет проходить существенно медленнее, чем без реплик из-за необходимости синхронизировать данные. refresh_interval - параметр настройки индекса, отвечающий за интервал обновления данных в индексе для их видимости. Его можно отключить, поставив в -1 или же увеличить со значения по умолчанию равное 1s до оптимального в вашей системе. Пример запроса на изменение настроек индекса:

curl -X PUT "elasticsearch:9200/products-20210606?pretty" -H 'Content-Type: application/json' -d'
{
"settings": {
"index": {
"number_of_replicas": 0,
"refresh_interval": -1
}
}
}'
Проверим, что настройки индекса корректны:

curl -X GET "elasticsearch:9200/products-20210606?pretty" -H 'Content-Type: application/json'
Затем на индекс проходит скрипт наполнения. После успешного наполнения настройки необходимо переключить в ваши целевые конфигурации. Например:

curl -X PUT "elasticsearch:9200/products/_settings?pretty" -H 'Content-Type: application/json' -d'
{
"index": {
"number_of_replicas": 3,
"refresh_interval": "1s"
}
}'
4. Еще один пункт, который разработчики ElasticSearch рекомендуют для ускорения – это использование автогенерируемых ID документов. Сразу скажу, что в нашем случае ощутимого прироста на этом пункте мы не увидели.

Все описанное выше более подробно есть в документации в статье про ускорение индексации

Но все это позволяет лишь сократить скорость первичного наполнения документов. Я бы не рискнула вот так легко и непринужденно менять настройки существующего индекса, на который идут обращения из АПИ на продакшн.

Часть 2. API Aliases в ElasticSearch​

В этом разделе статьи рассмотрим aliases и проведем пару экспериментов по работе с ними. Алиас (англ. Alias) в ElasticSearch (API Aliases) - это псевдоним индекса или его второе имя. Вы можете продолжать обращаться к индексу, на который создан alias как по имени, так и по alias. В примерах выше мы создали индекс с именем products-20210606 (<имя индекса>-<дата создания>). Теперь создадим на него alias. Обратите внимание, что имя индекса и название alias не могут совпадать.

curl -X PUT "elasticsearch:9200/products-20210606/_alias/products?pretty" -H 'Content-Type: application/json'
Проверим список существующих индексов и убедимся, что он есть:

curl -X GET "elasticsearch:9200/_aliases?pretty" -H 'Content-Type: application/json'
На выходе получаем:

{
"products-20210606": {}
"aliases": {
"products": {}
}
}
}
Итак, у нас есть один индекс и один alias, конечно, на данном этапе это бесполезно. Но у нас есть наполненный индекс, который отдает данные по псевдониму products. Он работает и на запись, и на чтение. Важный момент, что для указания на какой именно индекс из группы одного alias будет идти запись можно использовать параметр is_write_index. Он позволяет явно указать какой из индексов работает на запись, но к сожалению в нашей версии ElasticSearch он не поддерживается, да и так как работа на запись новых документов пока идет сборка нового индекса должна быть сразу в два индекса, чтобы избежать ситуации, когда новые товары попали в старый индекс и не попали в новый или попали в новый и будут доступны только когда индексация пройдет до конца, то такое решение нам не подходило, поэтому новые товары пишутся сразу в два индекса, а наполнение нового индекса получает предельный ID товара в самом начале, чтобы не обрабатывать документы дважды. Конечно, в зависимости от ситуации этот подход может и должен меняться.

Приступим к наполнению второго индекса. Создадим его с именем products-20210607

curl -X PUT "elasticsearch:9200/products-20210607?pretty" -H 'Content-Type: application/json' -d'
{
"settings": {
"index": {
"number_of_shards": 3,
"number_of_replicas": 0,
"refresh_interval": -1
}
}
}'
Теперь добавим два поля в оба индекса, чтобы проверить как себя будут вести выборки данных.

curl -X PUT "elasticsearch:9200/products-20210606,products-20210607/_mapping/general?pretty" -H 'Content-Type: application/json' -d'
{
"properties": {
"product_name": {
"type": "text"
}
"description": {
"type": "text"
}
}
}'
Добавляем по одному товару в каждый индекс и затем убеждаемся, что товар из первого индекса приходит и при указании alias products, и при указании названия индекса

// пример запроса на создание товара в индексе products-20210606
curl -X PUT "elasticsearch:9200/products-20210606/general/1?pretty" -H 'Content-Type: application/json' -d'
{
"product_name": "Test product products-20210606",
"description": "Test desc products-20210606"
}'<o:p>
// пример запроса на получение товара в индексе products-20210606
curl -X GET "elasticsearch:9200/products-20210606/general/_search?pretty" -H 'Content-Type: application/json' -d'
{
"query": {
"match_all" : {}
}
}'
// пример запроса на получение товара по alias products
curl -X GET "elasticsearch:9200/products/general/_search?pretty" -H 'Content-Type: application/json' -d'
{
"query": {
"match_all" : {}
}
}'
Результат запроса в обоих случаях одинаков:

{
"took" : 2,
"timed_out" : false,
"_shards" : {
"total" : 3,
"successful" : 3,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : 1,
"max_score" : 1.0,
"hits" : [
{
"_index" : "products-20210606",
"_type" : "general",
"_id" : "1",
"_score" : 1.0,
"_source" : {
"product_name" : "Test product products-20210606",
"description" : "Test desc products-20210606"
}
}
]
}
}
Но как только мы добавим такой же alias на второй индекс, то данные начнут приходить из обоих индексов:

curl -X PUT "elasticsearch:9200/products-20210607/_alias/products?pretty" -H 'Content-Type: application/json'
Результат запроса match_all:

{
"took" : 6,
"timed_out" : false,
"_shards" : {
"total" : 6,
"successful" : 6,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : 2,
"max_score" : 1.0,
"hits" : [
{
"_index" : "products-20210606",
"_type" : "general",
"_id" : "1",
"_score" : 1.0,
"_source" : {
"product_name" : "Test product products-20210606",
"description" : "Test desc products-20210606"
}
},
{
"_index" : "products-20210607",
"_type" : "general",
"_id" : "1",
"_score" : 1.0,
"_source" : {
"product_name" : "Test product products-20210607",
"description" : "Test desc products-20210607"
}
}
]
}
}
На основе проделанных запросов видно:

  1. aliases позволяют использовать одно название индекса для хранения данных сразу в нескольких индексах, формируя подобие группы
  2. alias нам позволит явно указать с какого индекса сейчас должно идти чтение данных, но только если мы условно гарантируем, что сколько бы не было индексов в группе – alias стоит только на одном из них
  3. Также видно, что если будут идентичные данные в двух разных индексах, то мы получим проблему дубликатов в результатах выборок. Здесь огромное спасибо нашим QA, так как багу выявили в тестировании, а наша ошибка была в том, что alias на второй индекс мы ставили в самом начале.

Часть 3. Нулевой даунтайм API​

Вернемся к нулевому даунтайму API при построении нового индекса. У нас есть наполненный индекс, в который поступают текущие запросы API. Мы его ускорили по пунктам первой части статьи. Что же делать дальше?

1. Создаем alias на текущий индекс и переключаем API на получение данных по названию индекса -> API продолжает работать;

2. Создаем новый индекс и запускаем наполнение данными нового индекса;

3. Не забываем подумать о том, как в момент индексации будут поступать новые документы, обновляться и удаляться старые, ведь наполнение нового индекса может идти не один час. В моем случае при поступлении нового сообщения в очередь на обработку товара подошло получение всех индексов по маски из второго примера, так как alias на второй индекс ставится после всех изменений. Пример из кода метода получения по alias всех его индексов (c использованием PHP библиотеки ElasticSearch https://packagist.org/packages/elasticsearch/elasticsearch) :

/**
* @param string $aliasName
* @return array
*/
public function getIndexesByAlias(string $aliasName): array
{
return array_keys($this->client->indices()->getAlias(['name' => aliasName]));
}
Пример метода получения списка индексов со схожими наименованиями

/**
* @param string $name
* @return array
*/
public function getIndexesByNameMatch(string $name): array
{
return array_keys($this->client->indices()->get(['index' => $name . '*']));
}
Возможно, вам и не нужно будет прорабатывать такой сценарий, так как никаких новых документов и обновления старых не поступает, а просто есть необходимость освежить старые. Например, обновить адреса и почтовые индексы на основе данных из внешнего источника;

4. После успешного наполнения по описанному алгоритму ускорения индексации, необходимо переключить реплики и только после того, как реплики будут «здоровые» можно устанавливать alias на новый индекс. Проверить, что реплики создались и назначены успешно, можно командой:

curl GET elasticsearch:9200/_cat/shards?h=index,shard,prirep,state,unassigned.reason | grep UNASSIGNED
В нашем случае эта операция занимает около 30 минут на данный момент. Если сразу назначить alias и удалить старый индекс, то можно получить даунтайм времени формирования шардов реплики;

5. После того как реплики были назначены, можно устанавливать alias на новый индекс;

6. Удаляем старый индекс.

Какие проблемы удалось решить с помощью описанного выше механизма?

  • Добавление нового поля или изменение типа поля (с text на keyword) теперь не является проблемой, так как новый индекс формируется с нуля;
  • В случае баги и поломки большого количества данных в индексе, мы можем его просто наполнить с нуля;
  • Для внешних систем API остается работоспособным даже если появилась необходимость переиндексации данных, которое длиться несколько часов.

Источник статьи: https://habr.com/ru/
 
Сверху