У OpenSearch неоднозначная репутация. Некоторые специалисты считают, что его развитие остановилось на версии Elasticsearch 7.10.2. Но это не сделало OpenSearch динозавром, обреченным на постепенный упадок. Продукт развивается и выдает аналогичный Elasticsearch функционал, и даже больше — возможности, которые вообще недоступны в базовой лицензии Elasticsearch.В этом посте расскажем о том, на что способен OpenSearch и как мы боролись с его недостатками. Всё это — на примере внедрения, где в одном кластере нам пришлось наладить разделение доступов для десятка команд, не теряя при этом отказоустойчивости.

Разграничить доступы, но сохранить один кластер

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

Если к Elasticsearch нет единого доступа, то существуют два способа получить информацию из него. Можно хранить где-то пачку доступов ко всем развернутым Elasticsearch. Это неудобно и небезопасно. Или нужно запрашивать выгрузку логов. Вариант ненадёжный — неудобные логи в собственном хранилище могут и «пропасть». Часть проблем можно решить, загнав все данные в Grafana и визуализировав их там. Но сделать это, не зная структуру данных источника, крайне трудно.

Логично было бы каждой команде работать в своем кластере Elasticsearch. Подобные услуги предлагает и сам Amazon. Рассматривали и мы такой способ. То есть развернуть энное количество заранее согласованных нод, а уже потом эти ноды пилить под кластеры для каждой отдельной команды. Этот вариант не подошёл: нам нужен был доступ ко всем данным без переключения между кластерами.

Вы можете сказать, что решение существует. Ведь в OpenSearch теперь есть кросс-кластерный поиск. Можно создать мастер-кластер, который подключается ко всем остальным кластерам и производит поиск по ним. Одно катастрофичное «но». Этот замечательный способ работает только максимум на 20-ти кластерах. Кроме того, чтобы развернуть отдельные кластеры, нужно было получить пачку согласований от ИБ. А это очень затягивало предоставление услуги и никак не вмещалось в KPI в 20 минут. В нашем кейсе крупного банка с далеко не двумя десятками команд нам пришлось придумывать новое решение старой проблемы.

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

Рис. 1. Общая схема сервиса, признаться, очень нестандартная.
Рис. 1. Общая схема сервиса, признаться, очень нестандартная.

Главным же для нас стал вопрос — как разделить потоки данных от разных пользователей? Подумали, что можно будет разграничить потоки по портам на Logstash, а чтобы ограничить доступ — привязать каждый порт к собственному сертификату. Но этот вариант не был отказоустойчив: Logstash в кластер не собираются, а единственный способ обезопасить себя от потери данных — поднимать несколько Logstash и управлять переключениями между ними, например, через keepalived. Таким образом, мы вешаем общий виртуальный IP, через него идут данные, а оба Logstash’а работают в режиме Active/Passive. Если на первом Logstash падает сервис, виртуальный IP переходит на второй, и данные идут на него, пока поднимается первый. Чаще всего Logstash падает из-за нагрузки и скопления очереди, которая упёрлась в потолок. Поэтому после перезапуска он будет пытаться разгрести накопленную очередь, а в некоторых случаях придётся сбросить её и потерять часть данных.

Не ронять Logstash: отправляем данные в Kafka

Чтобы нивелировать проблему с перегрузкой Logstash, мы установили брокер очередей Kafka. Скопление данных в очереди Kafka возьмёт на себя, а Logstash читает данные из брокера по мере возможности. В периоды, когда нагрузка снижается, данные из очередей разбираются и дальше всё спокойно работает. Эта схема и была реализована, решив задачу отказоустойчивости. Единственное «но» — пользователям, который ранее писали в Logstash или напрямую в Elasticsearch, нужно изменить способ отправки данных, чтобы писать их в Kafka.

А что по производительности?

Вопрос производительности решается опять же стандартно — горизонтальным масштабированием. При накоплении очередей на Kafka добавляется Logstash, на который переводятся несколько последних топиков и все последующие, при нехватке места для хранения в OpenSearch добавляются Data Node.

Ролевая модель и теннанты в OpenSearch

Банк справедливо желал создать централизованную систему сбора и хранения логов, чтобы легко администрировать и управлять ей. Вторым требованием стала отказоустойчивость системы. В ТЗ это отразилось добавлением брокера очередей Kafka. Ну и, естественно, все взаимодействия между узлами — строго с использованием SSL

Управление всей системой и заказ услуг осуществляется через Manage IQ. Разграничение прав на отправку данных в топики Kafka реализовано c помощью ACL: для каждого топика прописан пользователь, указанный в выданном при заказе услуги сертификате. Одновременно с этим добавляется ACL на чтение из соответствующего топика для сертификата, который используется для обмена данными на Logstash. А в Logstash добавляется запись в pipeline и конфиг с одним из зафиксированных в ТЗ форматом приёма данных. На самом же OpenSearch всё вполне стандартно: для группы из AD, в которую входит заказчик, создается отдельный теннант (чтобы избежать нагромождения витрин с надписями no data found, если вы понимаете, о чём я), в нём создается индекс паттерн (так как права по умолчанию выдаются только на чтение), ну и прописываются доступы на чтение из «своего» индекса.

Рис. 2. ELK как сервис. Схема Elasticsearch. Доступ к данным пользователя.
Рис. 2. ELK как сервис. Схема Elasticsearch. Доступ к данным пользователя.

Самописный скрипт: чистим данные по-своему

В процессе мы столкнулись с проблемой: после смены лицензий Elasticsearch и ответвления OpenSearch пропала совместимость большей части вспомогательного софта. Мы «потеряли» актуальные версии Elasticsearch Curator и библиотеки для Python. Мы планировали использовать Curator для контроля объёма хранимых в индексах данных. Но он отказался работать с «неизвестным» ему OpenSearch, как и библиотека Python. Кстати, старые версии библиотеки и Curator вроде завелись, но заказчик увидел потенциальные риски в необновляемом стороннем ПО, поэтому…

Остановились на использовании Index State Management. Он позволяет настраивать ротацию индексов по объёму и удаление по возрасту. В идеальном мире этого достаточно: при стабильном равномерном потоке данных можно легко контролировать суммарный объём хранимых логов, что является главным параметром биллинга услуги для внутренних заказчиков. Заказывая сервис хранения логов, пользователь указывает средний объём за сутки и период хранения данных и платит за рассчитываемый произведением этих значений суммарный занимаемый объём. Но в реальности существуют простои сервиса, перенастройка логирования, дебаг-режим и т. д. и т. п. В таких случаях рассчитанный для ротации раз в сутки индекс может превысить свой объём в десятки раз, а период хранения его не изменится. Это приведёт к перерасходу объёма. Но самое опасное — это переполнение хранилища самого OpenSearch. Потому что пользователи запускаются в систему, исходя из расчётного суммарно занимаемого объёма, и масштабирование системы идёт по той же логике.

Мы решили ротировать индексы за счёт даты и времени из Logstash, а для контроля объёма и срока хранения писать собственный скрипт, чтобы через API контролировать состояние индексов по маске. Также придумали сообщать заказчику услуги, если удаление его данных происходит вне планового времени.

Функционал скрипта:

  • удаление данных, превышающих период хранения;

  • удаление данных при превышении суммарно рассчитанного их объёма;

  • информирование пользователя при удалении данных по объёму раньше расчётного срока.

Работает скрипт так:

  • Из endpoint _stats/store через параметр jq '._all.total.store.size_in_bytes' получаем объём всех индексов по маске.

  • Из _cat/indices получаем список всех индексов по маске. Список сортируем и оцениваем возраст самого старого индекса.

  • Если объём превышен, а возраст — нет, пользователь получает уведомление о преждевременной очистке индекса, чтобы он мог либо увеличить объём заказанной услуги, либо снизить объём отправляемых на хранение данных.

А далее скрипт двумя циклами удаляет сначала всё, что превышает период хранения, а затем — самые старые индексы, пока суммарный объём не приблизится к расчётному.

Недостаток как точка развития

Мы сделали оптимальное решение, лавируя в существовавших требованиях и ограничениях. А теперь честно поговорим о его недостатках. Они в том, что масштабировать Logstash и настраивать объём очередей Kafka приходится эмпирическим путём. А с учётом автоматизации процессов теряется гибкость. Информация о пропускной способности Logstash очень скудная — он способен принимать 100000 событий в секунду от генератора. Но нигде не сказано, что будет происходить, если события при этом ещё и обрабатываются. Одно дело — «получить и отдать» данные как есть, другое — обработать неструктурированный лог. JSON-парсер работает быстро, регулярки для того же SYSLOG — сильно медленнее. В других проектах мы сталкивались с тем, что, когда нужно разобрать большой syslog на составные поля, это давало сильную загрузку CPU. Чаще всего под Logstash добавляют оперативную память, а здесь пришлось добавить ядер: Logstash копил у себя очереди из-за задержек парсинга полей. Для него это оказалось задачкой не по зубам.

Мы упростили этот процесс, согласовав формат передаваемых данных. Вот она — благословенная стандартизация! В шаблонах для заказа услуги мы сразу прописали форматы отправки и регистрации сообщений. «Принимаем данные только JSON и только syslog в стандартизированном формате ISO8601». JSON парсит легко и непринуждённо, а вот syslog должен быть только определённого формата и содержать конкретный набор полей. Да, он всё равно будет парситься регуляркой, но одной и той же. Это позволит хотя бы примерно оценивать создаваемую нагрузку. Тем не менее, вопрос производительности решён не до конца, и мы копим экспертизу по этой системе.

Олег Кириченко, старший инженер-проектировщик по мониторингу «Инфосистемы Джет»

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


  1. chemtech
    30.03.2022 11:33
    +1

    Спасибо за пост. Как вы устанавливаете Zookeeper, Kafka, OpenSearch, Logstash и другие ? Puppet, ansible или может в Kubernetes?


    1. JetHabr Автор
      30.03.2022 12:03

      Устанавливаем компоненты разными путями: всё зависит от ИТ-инфраструктуры и требований заказчика. Где-то всё ставится в Kubernetes/OpenShift, где-то компоненты распределяются частями: что-то в Kuber, что-то на виртуализации, бывает даже на железных серверах. Естественно, все процессы максимально автоматизируются плейбуками.


  1. onegreyonewhite
    30.03.2022 16:50

    Что-то либо я не понял, либо проблема из пальца высосана с logstash.

    Там где надо было поднять ELK стек, лично мы брали отдельный logstash на приёмку данных, если и применяли, то минимальную обработку, затем клали сообщение в очередь (тогда ещё RabbitMQ), а затем уже из очереди брали другим набором экземпляров logstash данные на обработку. В таком варианте так и не удалось положить это на лопатки, хотя поток отправляли знатный, а логи парсили сложными регулярками. При том такое решение масштабировалось довольно легко - добавляли новые инстансы logstash косюмеров. Был написан скрипт, который смотрел на очередь и если она продолжала расти какое-то время, то добавлялся новый консюмер.

    Подозреваю, что проблема у вас была с тем, что разные команды по-разному обрабатывали логи, но как по мне, то в этом случае нужно было им отдать свой logstash, который умеет не только в очереди ложить, но и в другой logstash. Мутно, конечно, но решило бы проблему. Хотя последний абзац и эту идею рушит.

    В общем выглядит так, как будто вы не до конца разобрались с возможностями logstash.


    1. JetHabr Автор
      31.03.2022 11:43

      Почти во всех случаях возникающие проблемы – это результат тех или иных ограничений. Например, в одном проекте, где мы упёрлись в потолок производительности, проблемой было ограниченное число выделяемых на сервис виртуальных машин. А у заказчика из поста выше есть ограничения не только на число ВМ (ограничение продиктовано стоимостью обладания каждой отдельной машиной), но и на возможности команд, которые будут отдавать свои данные. Проблема производительности на тестовой инсталляции не возникала, она обозначена исключительно как потенциальная. И для решения её предложен вариант близкий к тому, какой вы описываете: поднимать дополнительные logstash для натравливания на разные очереди.


  1. AlexGluck
    31.03.2022 13:25

    У ластика и opensearch в частности, есть lifecycle соответственно вопрос в том зачем писать какой-то отдельный скрипт или использовать куратор, когда можно встроенными средствами контролировать объёмы и сроки хранения?


    1. JetHabr Автор
      31.03.2022 17:51

      Про это есть подробно в посте. Повторим:

      Остановились на использовании Index State Management. Он позволяет настраивать ротацию индексов по объёму и удаление по возрасту. В идеальном мире этого достаточно: при стабильном равномерном потоке данных можно легко контролировать суммарный объём хранимых логов, что является главным параметром биллинга услуги для внутренних заказчиков. Заказывая сервис хранения логов, пользователь указывает средний объём за сутки и период хранения данных и платит за рассчитываемый произведением этих значений суммарный занимаемый объём. Но в реальности существуют простои сервиса, перенастройка логирования, дебаг-режим и т. д. и т. п. В таких случаях рассчитанный для ротации раз в сутки индекс может превысить свой объём в десятки раз, а период хранения его не изменится. Это приведёт к перерасходу объёма. Но самое опасное — это переполнение хранилища самого OpenSearch. Потому что пользователи запускаются в систему, исходя из расчётного суммарно занимаемого объёма, и масштабирование системы идёт по той же логике.