In the company I have built search engine based on ElasticSearch and distributed system of data import. I would like to share my experience and speak about the following topics:
– What the search engine is and why it’s needed?
– What platforms exist to choose from?
– ElasticSearch and its capabilities.
– How to ensure continuous data flow?
– Maintenance of data consistency.
– Reduce to a minimum effort needed for search extension.
– Lessons learned.
38. Autosharding
1 2 3 4
Application
Shards
Search though all the
shards
Aggregation of results
Overhead
1 2 3 4
Application
Search though all the
shards
Aggregation of results
Overhead
39. Routing
1 2 3 4
Application
Shards
Execute search on
specified shards
Merge of results takes less
time
Reduce traffic whithin
response
Добрый день. Полагаю, что из многие из нас смотрят сериалы. И часто при просмотре мы слышим фразу «1 икс Бет. Ставки на спорт». Сегодня мы поговорим о ставках, но других!
Я – Андрей Винда. За плечами более 14 лет опыта в разработке ПО. Последние два года я работаю в компании SBTech на позиции ТимЛида.
Давайте поговорим о бизнесе нашей компании.
В каждой стране есть люди, которые хотят заработать деньги, делая ставки на спорт. Мы называем их игроками. А наша компания дает им такую возможность.
Компания работает в двух сферах: B2B и B2C. Своих клиентов компания называет «операторами». Оператор – это владелец сайта ставок.
Существует огромное множество операторов. В нашей компании на текущий момент зарегистрировано боле 250 операторов.
В системе есть два вида пользователей:
Агенты
Игроки
Агенты – это представители операторов, которые имеют возможность просматривать разного вида отчеты, информацию по игрокам, поиск игроков по разнообразным параметрам и их комбинациям.
С другой стороны есть игроки, которые заходят на сайт, просматривают события, делают ставки, изменяют свою информацию и так далее.
Я расскажу вам какой поисковый движок мы выбрали, как организовали постоянный поток данных, с какими трудностями столкнулись и как пришли к рабочей версии.
Как 2 года назад был организован поиск игроков.
Это была грусть-печаль!
А мы имели мы следующее.
Примитивный графический интерфейс, где фильтры были беспорядочно добавлены и размещены.
Весь поиск производился силами MS SQL.
Из-за этого у нас были следующие проблемы при поиске.
Полнотекстовый поиск крайне медлителен (LIKE) и возможен не для всех полей
При указании периода поиска 6 месяцев и более – система работала стабильно и предсказуемо! Она зависала и выдавала ошибку: Timeout.
Это было вследствие того, что система была вынуждена произвести фильтрацию среди более чем 20 миллионов записей.
Скорость поиска низкая и ВСЕГДА зависит от нагрузки на БАЗУ!
Далее.
Главные сложности и ограничения реляционных БД.
Агрегация на лету
JOINS
Масштабируемость (можно, но сложно и дорого)
Такая система была неконкурентоспособна.
Поэтом вместе с бизнесом были сформированы следующие требования.
Одно поле ввода – поиск по многим полям
Полнотекстовый поиск по всем текстовым полям
Ускорить поиск
Возможность задания больших периодов
Что же тут главное.
Первое, Масштабируемость.
Второе, поиск должен быть очень быстрым
Третье, возможность указания больших периодов (более 2-х лет)
И последнее, Не Облачное решение.
Этот пункт связан с особенностью бизнеса. По требованиям многих регуляторов, беттинговые системы должны хоститься в стране, в которой они работают.
Ну ладно, требования у нас есть. С чего начать?
Итак, нам нужен супер быстрый поисковой движок, который из коробки поддерживает полнотекстовый поиск, масштабируемость, простые запросы без JOINS
Мы сразу же остановили свой взгляд на Эластике, т.к. это фактически стандарт в мире поисковых систем.
Даже рейтинг поисковых систем показывает, что Эластик - впереди планеты всей.
Elasticsearch используют такие компании, как Netflix, StackOverflow, LinkedIn, Barclays, Facebook и многие другие
Вот его основные возможности:
Масштабируемость и отказоустойчивостьElasticsearch действительно легко масштабируется. К уже имеющейся системе можно на ходу добавлять новые сервера, и поисковый движок сможет сам распределить на них нагрузку. При этом данные будут распределены таким образом, что при отказе какой-то из нод они не будут утеряны и сама поисковая система продолжит работу без сбоев.
В Elasticsearch есть огромное количество настроек, с помощью которых можно увеличить производительность элатика при поиске, фильтрации и агрегации
Elasticsearch практически полностью управляется по HTTP с помощью запросов в формате JSON
Рассмотрим вкратце структуру Эластика
Есть кластер, состоящий из нод.
Каждая нода – это сервер, на котором запущен Elasticsearch.
Каждая нода состоит из шардов.
Шард – это фактически инвертированный индекс Lucene.
Зная теперь, что за зверь этот Elasticsearch мы начали разработку.
Так как любой продукт встречают по одежке, то мы решили обратить наше внимание на очень важный момент. А именно, на Визуальный интерфейс. Он был ужасен!
Мы взялись за его переделку и вот что вышло!
Для удобства использования наверх были вынесены самые часто используемые фильтры, а ниже все фильтры сгруппированы по категориям.
Также Smart Search – это как раз то самое поле, значение которого ищется сразу по нескольким полям (имеется ввиду вхождение или полное совпадение)
Теперь у пользователя системы есть возможность задания любых фильтров
в любых комбинациях.
Теперь давайте посмотрим, как изменилась структура поисковой системы
Мы использовали денормализированный тип отношений,
всего у нас был 1 индекс, в котором было более 130 полей.
Было оставлено автошардирование,
а индексация проходила в 5 потоков с небольшим временным зазором между каждым.
Конфигурация кластера была следующая.
Это конфигурация Эластика по умолчанию. У нас 3 ноды, каждая имеет 8 Gb оперативной памяти, 100 Gb дискового пространства и 4 ядра.
Шардов всего 15.
Наполнение данных было устроено весьма просто.
Взяли данные из базы и если хотя бы одно из полей в таблице было изменено - отправляли их в Эластик.
Процесс индексации был построен на базе нашего собственного планировщика задач.
При разработке планировщика были использованы следующие технологии.
А используются они следующим образом.
RabbitMQ – канал коммуникации
Quartz.NET – запуск задач по расписанию
Dapper – легковесная ORM для чтения данных из БД
Nest – официальный .NET клиент для работы с Elasticsearch
Давайте рассмотрим схему работы планировщика.
Конфигурацию всех задач храним в отдельной базе
Задачи запускаются по расписанию (Quartz.NET)
Планировщик общается с исполнителями через RabbitMQ
Схема работы перекачки данных выглядит достаточно просто. А именно.
Получаем данные
Разбиваем их на пакеты
Отправляем пакеты в Эластик
И так, пока не перегоним все данные.
Наше решение прошло тестирование. Все работало как положено, и мы решили идти на продакшн
Все работало как часы. Но ровно 2 месяца
Потом случилась беда!
На сервере Эластика закончилось место.
Счет шел на минуты. Мы отключили индексацию на STG, чтобы выиграть немного времени.
Работа системы была под угрозой. В любой момент все могло рухнуть. Слава Богу, что не рухнуло!
Начинался трудный путь к успеху.
Никто из нас не знал, что делать. Единственный выход – освоить теорию.
Чем мы с вами сейчас и займемся.
Так как Эластик основан на Lucene, то давайте рассмотрим следующие его особенности.
Индекс состоит из множества сегментов.
Сегмент – это неизменяемая единица
В Lucene есть всего две операции – создание нового документа и удаление старого
Хотя на самом деле вместо удаления документа соответствующие сегменты помечаются как удаленные. При этом они физически занимают место и память, но не участвуют в поиске!
Когда сегменты занимают в памяти максимально позволенный объем (MergeFactor) – запускается процедура слияния сегментов.
Идеально иметь один сегмент! Таким образом отпадает необходимость объединять и ранжировать результаты с нескольких сегментов.
Основываясь на этих фактах, были определены допущенные нами основные ошибки.
Это:
Огромный размер индекса
Частые обновления данныхПроблема заключается как раз в том, что документы в этом индексе часто изменяются. Это приводит к большому количеству удаленных сегментов и занимаемой при этом памяти. О скорости поиска при таком раскладе говорить не приходиться.Эластик не предназначен для частых изменений данных, но ищет он все равно очень быстро.
Поиск сразу по всем шардамэто вследствие того, что мы использовали автошардирование.
Начался поиск путей решения!
Что же можно сделать с таким большим размером индекса?
Каким-то образом его надо разбить на несколько. Чтобы определить варианты изменения индекса, посмотрим какие же типы отношений между объектами предлагает Эластик.
Денормализированные
Вложенные
Родитель – Ребенок
На уровне приложения
Проанализировав каждый тип, мы остановились на Родитель – Ребенок.
Он давал нам следующие преимущества.
Во-первых, при изменении структуры одного типа необходимо переиндексировать документы только этого типа.
Во-вторых, можно оптимизировать поисковый запрос, указав конкретные типы для поиска.
В-третьих, возможность параллельной индексации всех или нескольких типов.
В-четвертых, при частых изменениях документов имеем меньший размер удаленных сегментов
Используя этот тип отношений, структура поискового индекса изменилась следующим образом
Название нашего индекса PlayerData. И в этом индексе несколько типов: General, Lifetime, Sensitive и Personalization.
Родитель в данном случае – это General, а остальные являются его детьми
Также мы занялись оптимизацией поиска и вот что сделали.
Т.к. у нас сортировка всегда происходит по идентификатора игрока, а не по высчитываемому Elastic’ом рейтингу, то мы перешли от Query к Filter.
Это позволило нам помочь Эластику НЕ делать лишних движений.
Итак, поиск мы немного улучшили. Но можно ли сделать, что-то еще. Оказывается да.
Шардирование как раз и является волшебной пилюлей.
При встроенном шардировании, когда ElasticSearch обрабатывает поисковый запрос, то он не знает на каких шардах стоит искать и потому производит поиск на всех шардах. После этого результаты поиска со всех шардов сливаются, сортируются и выдаются в качестве результата. При таком подходе накладные расходы могут легко повлиять на производительность. Если же при выполнения поискового запроса указать Elasticsearch на каких шардах производить поиск – то можно существенно уменьшить накладные расходы.
Для ускорения работы поиска было решено использовать собственный аргумент шардирования. В нашем случае им стал идентификатор оператора.
Так как при поиске мы всегда указываем конкретных операторов, то данное решение выглядит весьма логичным. При таком подходе Эластик будет производить поиск только на указанных шардах.
При собственном шардировании возникает одна ситуация.
Из-за того, что мы распределяем игроков по их принадлежности к оператору, может так выйти, что на каком-то шарде данных больше, чем на других.
Такое может произойти, если игроки двух или более больших операторов будут помещены в один шард.
Такую возможность надо держать в уме.
И при наступлении критического размера это можно будет решить двумя способами:
Увеличении размера диска
Добавление в кластер новых нод
Нам оставалось решить еще несколько проблем.
Во-первых, что делать, когда игрок меняет оператора? Из-за того, что Эластик не позволяет изменить для созданного документа шард был создан специальный исполнитель, который обнаруживал таких игроков и удалял их из старых шардов, а потом добавлял данные игрока на новые шарды.
Во-вторых, как быстрее избавиться от удаленных сегментов?У Эластика есть специальная команда, когда начинает процесс слияния сегментов.
Не поверите, но она называется optimize
Мы пользуемся ей с завидной регулярностью.
Теперь наполнение Эластика выглядит следующим образом.
Вроде все хорошо, но есть одно НО.
Как уменьшить поток данных и отправлять в Эластик данные, которые действительно изменились?
Надо было решить проблему с частыми изменениями данных.
Совсем избавиться от частых изменений мы не могли.
Хранить данные с учетом временного фактора – мы не могли, т.к. данные изменяются постоянно, но предугадать это невозможно.
Как уменьшить поток данных и отправлять в Эластик данные, которые действительно изменились?
Для этих целей мы создали новый алгоритм, который позволяет нам определять действительно ли изменились данные у игрока, которые мы храним в Эластике.
Для этого мы вычисляем ContentHash на основании полученных данных игрока.
Если вычисленный хеш совпадает с уже отправленными в Эластик данными – то они исключаются из пакета.
После проверки данных по всем игрокам, данные которых изменились, пакет отправляется в Эластик.
Если пакет принят успешно, то вычисленный Хеш сохраняется как последний отправленный по игроку.
Таким образом в Эластик отправляются данные, которые действительно нужно там изменить.
Как же можно понять, что наша система будет в работоспособном состоянии по прошествии нескольких дней, недель, месяцев.
Для этих целей нам подойдет постоянный мониторинг показателей нашей поисковой системы.
Какие же это показатели?
Все показатели мы разделили на несколько уровней, чтобы в случае проблемы понимать, куда бежать и что делать.
Давайте рассмотрим каждый из уровней.
Нижний уровень мониторинга — железо и базовые метрики, такие же, какие собираются с любого сервера. А именно:
Загрузка процессорных ядер;
Использование памяти;
Пинг до сервера и время отклика;
i/o по дисковой подсистеме;
Остаток свободного места на дисках
Уровень повыше, но мониторинг всё такой же стандартный:
Количество запущенных процессов сервиса elasticsearch;
Используемая сервисом память;
Пинг до порта приложения (стандартные порты elasticsearch/kibana — 9200/9300/5601).
Если любая из метрик упала в ноль — это, означает что приложение упало, либо зависло, и немедленно вызывается алерт.
Общие метрики состояния кластера. Самые важные из них это:status — принимает одно из значений: green/yellow/red. Green — всё хорошо; yellow — какие-то шарды отсутствуют/инициализируются, но оставшихся кластеру достаточно, чтобы собраться в консистентное состояние; red — всё плохо, каким-то индексам не хватает шардов до 100% целостности, беда, трагедия, алерт.Общее количество нод в кластере. Полезно мониторить их изменение, потому что иногда случаются ситуации, когда какая-то из нод залипла под нагрузкой и вывалилась из кластера, но потребляет ресурсы и держит порт открытым. Влияет на целостностность кластера.
Количество не назначенных шард. Значение метрики не равное нулю — это очень плохой признак. Либо из кластера выпала нода, либо не хватает места для размещения, либо какая-то другая причина и нужно незамедлительно разбираться.
Общие метрики состояния ноды. Самые важные из них это:
Память. Elasticsearch хранит в оперативной памяти каждой дата-ноды индексную часть каждого шарда, принадлежащего этой ноде для осуществления поиска. Регулярно приходит сборщик и очищает неиспользуемые пулы памяти. Через некоторое время, если данных на ноде много и они перестают помещаться в память, сборщик выполняет очистку всё дольше и дольше, пытаясь найти то, что вообще можно очистить, вплоть до полного stop the world. А из-за того, что кластер Elasticsearch работает со скоростью самой медленной дата-ноды, залипать начинает уже весь кластер. Есть еще одна причина следить за памятью —после 4-5 часов в состоянии jvm.mem.heap_used_percent > 95% падение процесса становится неизбежным.
Файловая система: метрики по дисковому пространству, доступному каждой ноде. Если значение приближается к watermark.low — аларм.
Пулы очередей: стоит особо отметить отказы на добавление данных. Рост этого показателя — очень плохой признак, который показывает, что эластику не хватает ресурсов для приёма новых данных. Бороться, без добавления железа в кластер, сложно
Теперь же, конфигурацию Эластика мы определил сами.Увеличили память и место, что было с запасом.
Количество шардов рассчитали по формуле 1 Шард = 1 Ядро.
Изменения, все вместе взятые, дали заметный прогресс.
Время открывать шампанское и отмечать успех!
Что же обеспечило успешный результат?
Целостность данных
В нашем случае это был набор задач, которые регулярно выполнялись и делали следующие действия:
Непрерывный импорт данных
Удаление данных при изменении параметра шардирования
Индексация или поиск
Поиск важен. Но индексация важнее. Без новых / измененных данных поиск будет приносить больше вреда, чем пользы.
Т.к. задачи по расписанию запускаются каждые 3 минуты, то имеет смысл новые / измененные данные адаптировать для поиска.
Изменение структуры индекса
Шардирование
Регулярный запуск слияний сегментов
Заточка на индексацию
1 Шард на 1 Ядро
Настройка по умолчанию для Elasticsearch может сыграть злую шутку с вами. Когда Elastic настроен по умолчанию – то кажется что все отлично работает. В этом основное отличие от других систем, где сразу видны проблемы при недостаточной настройке. Elastic же работает до поры до времени, а потом приходит боль!
Мониторинг! Это очень важно!
Тренироваться, прогонять настройку системы в течение длительного времени при большой нагрузке, чтобы понимать, готова ли система к таким нагрузкам.
Как не надо делать:
Мы использовали единственный индекс, в котором определили более 130 полей. Документы в индексы часто изменялись. Как следствие – система рухнула. Мораль: Знание основ работы системы – must have.
Мы не управляли настройкой системы и составом кластера.
Даже во время подготовки к презентации были найдены новые пути для улучшения. Так что процесс оптимизации и улучшения бесконечный!
Первым шагом для нас станет добавление, так называемых, balancer nodes в Elasticsearch. Они могут производить агрегирование результатов запросов по другим шардам, у них никогда не будет перегружен IO, так как они не выполняют чтения и записи на диск, и мы разгрузим наши data nodes.
Оптимизация настроек индексов:
Тип хранения данных
Размер буфера памяти
Переход на Elasticsearch 6.0
================================
index.store.type по умолчанию ставится в niofs, а по бенчмаркам производительность ниже чем у mmapfs
indices.memory.index_buffer_sizeувеличить до 30%, а количество RAM под Java Heap наоборот уменьшить до 30%, так как с mmapfs нужно намного больше оперативки для кеша операционной системы
Редкие значения
Когда не все поля в индексе имеют заполненные значения, то место на диске и в кеше будет зарезервировано для таких пробелов. Изменения в Lucene 7 поддерживают такие ситуации и новый формат кодирования уменьшает занимаемое место и увеличивает пропускную способность запросов.
Сортировка при индексации
Lucene 7 также позволяет определить сортировку при индексации. Это повышает производительность, позволяю сортировать индексы при индексации во время записи, а не во время чтения. Индексы записываются на диск в определенном заранее порядке.
Улучшенное восстановление шарда
Новая функция, называемая Sequence IDs, обещает гарантировать более успешное и эффективное восстановление шардов.
Каждая операция индексирования, обновления и удаления получает идентификатор, который регистрируется в журнале транзакций основного шарда. После этого реплика может ссылаться на операции, записанные в этом журнале и использовать их для обновления без необходимости копирования всех файлов, что значительно ускоряет восстановление. Есть возможность настраивать значение того, как долго хранить эти журналы транзакций.
Реплики могут запускать неподтвержденные и разные операции - это означает, что в случае сбоя первичного шарда реплики смогут синхронизироваться с новым основным шардом, не дожидаясь следующего восстановления.