Русский
Русский
English
Статистика
Реклама

Elasticsearch

Новые возможности анализа табличных данных с алгоритмами машинного обучения в Elastic

11.03.2021 12:11:09 | Автор: admin


Elastic stack, также известный как ELK Stack (аббревиатура из программных компонентов: Elasticsearch, Kibana и Logstash), это платформа построения озера данных с возможностью аналитики по ним в реальном масштабе времени. В настоящее время широко применяется для обеспечения информационной безопасности, мониторинга бесперебойности и производительности работы ИТ-среды и оборудования, анализа рабочих процессов, бизнес-аналитики.


В соответствии со стратегией компании, исходный код всех продуктов Elastic является открытым, разработка ведётся публично, а базовые функции доступны бесплатно, что выгодно отличает платформу от конкурентов.


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


В текущей версии v7.11 доступны следующие наборы инструментов анализа данных с использованием технологий машинного обучения:


  1. Обнаружение аномалий на потоке данных (Anomaly detection)
  2. Аналитика табличных данных (Data frame analytics)

Примечание: Попрактиковаться в использовании перечисленных выше наборов инструментов анализа данных можно будет 24 марта. Совместно с коллегами Elastic мы продемонстрируем, как применять Anomaly detection и Data frame analytics для выявления инцидентов информационной безопасности. Ссылка на регистрацию.

Об инструментах обнаружения аномалий Elastic ранее уже писали на Хабре. С того времени (Elastic версии 7.1) они продолжали активно развиваться, было улучшено качество алгоритмов и удобство их применения для прикладных задач. Но в этой статье мы решили осветить совершенно новый набор функций анализа табличных данных, появившийся в версиях с 7.2 до 7.11.


Data frame analytics это набор функций Elasticsearch и визуализаций Kibana, позволяющих проводить анализ данных без их привязки к временным меткам. В отличии от Anomaly detection, где предполагается временная последовательность анализируемых данных.


Работа с Data frame analytics осуществляется через графический интерфейс с пошаговым мастером настройки. При этом, за счёт автоматической оптимизации параметров обучения (hyperparameters) пользователю не требуются глубокие знания стоящих за ними математических алгоритмов.


Возможности Data frame analytics Elastic версии 7.11 включают в себя:


  1. Выявление отклонений в значениях параметров (outlier detection) с использованием алгоритмов машинного обучения без учителя (unsupervised)
  2. Построение моделей машинного обучения с учителем (supervised) для решения задач:


    a) Регрессии (regression), как определение зависимости одного значения от одного или нескольких других значений


    b) Классификации (classification), как определение принадлежности произвольного объекта к одному из заданных классов



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


Выявление отклонений с использованием алгоритмов машинного обучения без учителя (Outlier detection)


Функция Outlier detection, как и ранее существовавшая в Elastic anomaly detection, предназначена для выявления аномальных значений (выбросов) каких-либо параметров и не предполагает обучение с учителем модель строится каждый раз при запуске. Но в отличие от anomaly detection, значения признаков (фич, особенностей, характерных черт анализируемых объектов) в ней анализируются без учета временной последовательности.


Исходными данными для функции выступает массив записей (JSON документов), в котором данные представлены в виде полей и их значений (поддерживаются числовые и логические типы значений). Каждая запись характеризует поведение какого-либо объекта. Поля в записи включают в себя поле, идентифицирующее объект, и поля со значениями, указывающими на аномальное поведение объекта, их называют признаками (feature).


Результатом функции является оценка отклонения для каждого признака и документа в диапазоне от 0 до 1, где 1 указывает на максимальную вероятность того, что соответствующий признак/документ является выбросом по сравнению с другими документами в анализируемых данных.


В качестве алгоритмов поиска выбросов по умолчанию в Elastic используется ансамбль следующих методов:


  • Метод ближайшего соседа (distance of Kth nearest neighbor)
  • Метод K ближайших соседей (distance of K-nearest neighbors)
  • Локальный уровень выброса (local outlier factor)
  • Локальный уровень выброса на основе расстояния (local distance-based outlier factor)

Подробнее про алгоритмы можно почитать здесь и здесь.


Для оценки и интерпретации результатов выявления отклонений Elastic рассчитывает степень влияния признаков на искомое значение.


В качестве иллюстрации приведём несколько примеров применения Outlier detection:


  • анализ метрик производительности для выявления отклонений в нагрузке на сервера со стороны сразу нескольких приложений (здесь);
  • анализ отпечатков бинарных гистограмм исполняемых файлов для обнаружения обфусцированных и вредоносных файлов (здесь);
  • анализ публичных объявлений airnbnb.com (insideairbnb.com) для поиска необычных предложений (здесь).

Ниже рассмотрим функцию анализа выбросов на примере из документации Elastic.



Цель анализа в этом примере выявить необычное поведение пользователей интернет-магазина. Из магазина в систему поступают события оформления заказа продукции, каждое из которых содержит полное имя заказчика (customer_full_name.keyword), количество покупок в заказе (products.quantity), стоимость заказа (products.taxful_price), id заказа (order_id).


Документы в исходном индексе выглядят так
{  "_index": "kibana_sample_data_ecommerce",  "_type": "_doc",  "_id": "_mXypHcB6S7rlhJIFCvB",  "_version": 1,  "_score": null,  "_source": {    "category": [      "Women's Clothing",      "Women's Accessories"    ],    "currency": "EUR",    "customer_first_name": "Sonya",    "customer_full_name": "Sonya Smith",    "customer_gender": "FEMALE",    "customer_id": 28,    "customer_last_name": "Smith",    "customer_phone": "",    "day_of_week": "Saturday",    "day_of_week_i": 5,    "email": "sonya@smith-family.zzz",    "manufacturer": [      "Oceanavigations",      "Pyramidustries"    ],    "order_date": "2021-03-06T23:31:12+00:00",    "order_id": 592097,    "products": [      {        "base_price": 28.99,        "discount_percentage": 0,        "quantity": 1,        "manufacturer": "Oceanavigations",        "tax_amount": 0,        "product_id": 19238,        "category": "Women's Clothing",        "sku": "ZO0265502655",        "taxless_price": 28.99,        "unit_discount_amount": 0,        "min_price": 13.05,        "_id": "sold_product_592097_19238",        "discount_amount": 0,        "created_on": "2016-12-31T23:31:12+00:00",        "product_name": "Vest - red/white",        "price": 28.99,        "taxful_price": 28.99,        "base_unit_price": 28.99      },      {        "base_price": 13.99,        "discount_percentage": 0,        "quantity": 1,        "manufacturer": "Pyramidustries",        "tax_amount": 0,        "product_id": 13328,        "category": "Women's Accessories",        "sku": "ZO0201102011",        "taxless_price": 13.99,        "unit_discount_amount": 0,        "min_price": 6.86,        "_id": "sold_product_592097_13328",        "discount_amount": 0,        "created_on": "2016-12-31T23:31:12+00:00",        "product_name": "Across body bag - aqua ",        "price": 13.99,        "taxful_price": 13.99,        "base_unit_price": 13.99      }    ],    "sku": [      "ZO0265502655",      "ZO0201102011"    ],    "taxful_total_price": 42.98,    "taxless_total_price": 42.98,    "total_quantity": 2,    "total_unique_products": 2,    "type": "order",    "user": "sonya",    "geoip": {      "country_iso_code": "CO",      "location": {        "lon": -74.1,        "lat": 4.6      },      "region_name": "Bogota D.C.",      "continent_name": "South America",      "city_name": "Bogotu00e1"    },    "event": {      "dataset": "sample_ecommerce"    }  },  "fields": {    "order_date": [      "2021-03-06T23:31:12.000Z"    ],    "products.created_on": [      "2016-12-31T23:31:12.000Z",      "2016-12-31T23:31:12.000Z"    ]  },  "sort": [    1615073472000  ]}

Начнём с подготовки данных для анализа. Так как мы сравниваем между собой заказчиков, то исходные события необходимо группировать по полю customer_full_name.keyword. Признаками поведения заказчиков будут выступать суммарные значения количества заказов, количества покупок, стоимость заказов.


Чтобы сгруппировать события, используем функцию трансформации transforms, которая позволяет на лету формировать и сохранять в Elasticsearch результаты агрегации ранее собранных данных.


Примечание: Для расчёта нужных признаков используем функции Elastic по агрегации: числовые значения считаем через products.quantity.sum и products.taxful_price.sum, а количество заказов order_id.value_count.



Результат агрегации, представленный в табличном виде, выглядит следующим образом:



Эти данные будем записывать в индекс ecommerce_customers_outlier_detection. Документы в нём соответствуют строкам указанной выше таблицы, а столбцы полям в этих документах.



Чтобы просматривать индекс встроенными средствами Kibana, а не только через API, можно включить опцию "Create index template". Тогда соответствующий шаблон отображения индекса будет доступен в Kibana Discovery и при создании дашбордов в Kibana.


Опция "Continuous mode" включит выполнение трансформации в непрерывном режиме. Это обеспечит автоматическое обновление данных в индексе ecommerce_customers_outlier_detection с заданной периодичностью.


Примечание: Функцию транформации можно заменить правилом обработки данных, самостоятельно написанным для корреляционного движка watcher, также являющимся частью Elasticsearch.


Документы в результирующем индексе ecommerce_customers_outlier_detection выглядят следующим образом
{  "_index": "ecommerce_customers_outlier_detection",  "_type": "_doc",  "_id": "QWoH6FA5JvsfN4JjO_LF32QAAAAAAAAA",  "_version": 1,  "_score": 0,  "_source": {    "customer_full_name": {      "keyword": "Abd Adams"    },    "order_id": {      "value_count": 2    },    "products": {      "taxful_price": {        "sum": 98.9765625      },      "quantity": {        "sum": 4      }    }  }}

Теперь самое интересное запускаем задачу машинного обучения (job в терминах Elastic ML). Мастер создания задачи позволяет отфильтровать данные для анализа с использованием встроенных в Elasticsearch языков запросов KQL (Kibana query language) или Lucene.



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



С помощью Advanced configuration параметров создания модели мы можем включить/выключить расчёт степени влияния признаков на модель (Compute feature influence), задать минимальное значение выброса для расчёта этого влияния (Feature influence threshold), а также объём выделенной под модель оперативной памяти (Model memory limit) и количество используемых заданием потоков процессора (Maximum number of threads), тем самым снижая или увеличивая нагрузку на кластер Elasticsearch.



Блок Hyperparameters используется для управления параметрами обучения. Здесь можно выбрать:


  • математический алгоритм (Method) обнаружения отклонений (lof, ldof, distance_kth_nn, distance_knn) или использования ансамбля алгоритмов (ensemble), когда значения отклонений определяются путём комбинации и оценки их результатов;
  • количество используемых в расчётах соседей (N neighbors);
  • переключатель использования предварительной стандартизации значений (Standardization enabled).

Подробнее о параметрах и их значениях можно почитать в референсах на соответствующий эндпоинт API.


На завершающем шаге мастера указываем название и описание для задания, имя целевого индекса для сохранения результатов, имя поля с результатами анализа и необходимость создания шаблона в kibana.



После запуска, задание проходит через этапы:


  • создание результирующего индекса и добавление в него исходных данных (reindexing);
  • загрузка данных из индекса (loading data);
  • анализ данных и расчёт оценок выбросов (analyzing);
  • запись значений оценок выбросов в результирующий индекс (writing results).


Результаты работы задания можно подробно рассмотреть через интерфейс Kibana Data frame analytics.



На таблице с результатами анализа видим значение совокупной оценки выброса для каждого заказчика (ml.outlier_score) и оценку влияния признаков по насыщенности цвета соответствующей ячейки. Соответствующее числовое значение оценки сохраняется в служебном индексе с результатами анализа в поле ml.feature_influence.


В итоговом индексе результат работы алгоритма выглядит следующим образом
 {  "_index": "ecommerce_customers_outlier_detection_results",  "_type": "_doc",  "_id": "RQzr5SwrcdFVVAr9s9NEOwAAAAAAAAAA",  "_version": 2,  "_score": 1,  "_source": {    "customer_full_name": {      "keyword": "Elyssa Tran"    },    "order_id": {      "value_count": 4    },    "products": {      "taxful_price": {        "sum": 223.9921875      },      "quantity": {        "sum": 5      }    },    "ml__incremental_id": 926,    "ml": {      "outlier_score": 0.9770675301551819,      "feature_influence": [        {          "feature_name": "order_id.value_count",          "influence": 0.9383776783943176        },        {          "feature_name": "products.quantity.sum",          "influence": 0.05973121151328087        },        {          "feature_name": "products.taxful_price.sum",          "influence": 0.0018910884391516447        }      ]    }  },  "sort": [    1,    0.9770675301551819  ]}

Графически выбросы отображаются на диаграммах рассеяния. Здесь можно подсвечивать значения выбросов по выбранному порогу оценки.



И включить отображение размера точек в соответствии со значением отклонения.



Примечание: Данные диаграммы разработаны с использованием языка Vega, доступного любому пользователю в конструкторе визуализаций Kibana.


Регрессия и классификация с учителем


Рассмотрим следующий набор функций Data frame analytics контролируемое обучение (обучение с учителем). Они предоставляют пользователю возможность обучить в Elastic свою собственную модель и использовать её для автоматического предсказания значений. Модель можно загружать и выгружать из системы, а также оценивать её качество. Кроме того, через Eland поддерживается импорт в Elastic моделей, обученных с помощью библиотек scikit-learn, XGBoost или LightGBM. Вместе с возможностями платформы по сбору и обработке данных эти функции помогают реализовать в ней подход CRISP-DM (Cross-Industry Standard Process for Data Mining).


В Elastic две функции обучения с учителем регрессия и классификация. Если говорить проще, то цель функций определение неизвестного значения объекта по значениям влияющих на него признаков. В случае регрессии речь идёт о предсказании числового значения, как характеристики объекта, а в случае классификации об определении класса (группы), к которой относится объект.


Исходными данными (обучающим датасетом) для функции служит массив из уникальных записей (JSON документов). В каждой записи присутствует искомое поле (числовое для регрессии или категориальное для классификации) и поля признаков, влияющих на искомое поле. Значения искомого поля здесь используются для обучения модели и проверки её результатов. Результатом работы функции выступает обученная модель и новый массив данных, включающий исходный датасет, предсказанные значения и данными о степени влияния каждого признака. Датасет при этом можно использовать для оценки качества модели, а саму модель для анализа потока данных, поступающих в Elastic или ранее сохранённых в системе.


Схема реализации функций в Elastic


В качестве показателей поддерживаются числовые, логические и категориальные типы данных.


Обе функции используют собственные алгоритмы Elastic, построенные на базе градиентного бустинга деревьев решений XGBoost. Также возможно определение важности признаков по методу SHapley Additive exPlanations (SHAP).


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


Далее разберем примеры использования регрессии и классификации и возможности платформы по интерпретации результатов их работы.


Регрессия


С помощью регрессионного анализа попробуем предсказать продолжительность задержки рейса, на примере из документации Elastic. Для анализа используем метеоданные, данные с табло вылета/прилёта и стоимость билетов.


Документы в исходном индексе выглядят так
{  "_index": "kibana_sample_data_flights",  "_type": "_doc",  "_id": "lmX0pHcB6S7rlhJI2kVr",  "_version": 1,  "_score": null,  "_source": {    "FlightNum": "6GZQTCH",    "DestCountry": "IT",    "OriginWeather": "Thunder & Lightning",    "OriginCityName": "Tokyo",    "AvgTicketPrice": 849.1194483923543,    "DistanceMiles": 6127.633563869634,    "FlightDelay": false,    "DestWeather": "Rain",    "Dest": "Pisa International Airport",    "FlightDelayType": "No Delay",    "OriginCountry": "JP",    "dayOfWeek": 2,    "DistanceKilometers": 9861.470310212213,    "timestamp": "2021-02-17T15:41:38",    "DestLocation": {      "lat": "43.683899",      "lon": "10.3927"    },    "DestAirportID": "PI05",    "Carrier": "ES-Air",    "Cancelled": false,    "FlightTimeMin": 493.07351551061066,    "Origin": "Narita International Airport",    "OriginLocation": {      "lat": "35.76470184",      "lon": "140.3860016"    },    "DestRegion": "IT-52",    "OriginAirportID": "NRT",    "OriginRegion": "SE-BD",    "DestCityName": "Pisa",    "FlightTimeHour": 8.217891925176845,    "FlightDelayMin": 0  },  "fields": {    "hour_of_day": [      15    ],    "timestamp": [      "2021-02-17T15:41:38.000Z"    ]  },  "sort": [    1613576498000  ]}

В данном случае функцию transforms применять не нужно, так как каждый документ датасета содержит данные только об одном полёте, и цель анализа предсказать задержку в отношении каждого полёта. Но подготовить данные можно исключим ошибочные записи с расстоянием перелёта 0 километров. В качестве предсказываемого (проверочного) поля указываем FlightDelayMin.



В таблице included Fields выбираем поля признаков длительности задержки рейса, которые будут учтены при построении модели.



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



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


Следующий шаг после выбора признаков настройка параметров.



Advanced configuration:


  • количество признаков, для которых в результирующий индекс будет записано значение влияния соответствующего признака на результат предсказания (Feature importance values);
  • имя поля, в которое будет записан результат предсказания (Prediction field name);
  • лимит на объём используемой заданием оперативной памяти (Model memory limit);
  • максимальное количество используемых заданием потоков процессора (Maximum numbers of threads).

Hyperparameters:


  • коэффициент регуляризации Лямбда (Lambda);
  • максимальное количество деревьев для бустинга (Max trees);
  • коэффициент регуляризации Гамма (Gamma);
  • размер градиентного шага Эта (Eta);
  • доля выборки (Feature bag fraction);
  • псевдослучайное число, используемое при выборе из датасета документов для обучения модели (Randomize seed).

Примечание: Описание применения в Elastic этих и дополнительных гиперпараметров приведено в документации к API.


Теперь посмотрим на результаты работы алгоритма. Они записываются в итоговый индекс Elasticsearch и представлены в отчёте в табличном виде.



Здесь можно увидеть новые, по сравнению с датасетом, поля:


  1. Отметка об использовании данных для обучения (ml.is_training)
  2. Результаты предсказания задержки рейса (ml.FlightDelayMin_prediction)

Если при создании задания было указано количество признаков, для которых будет рассчитано значение их влияния на результат (Feature importance values), в таблице отобразятся соответствующие значения для каждого признака (ml.feature_importance), а в результатах бар-чарт средних значений по всему датасету.



При клике на поле ml.feature_importance увидим наглядный график принятия решения алгоритмом SHAP.



Точкой обозначено среднее значение прогнозов для всего датасета. Линией соединены значения степени влияния признаков, от меньшего к большему. Верхний признак сильнее всего влияет на решение, в нашем примере это причина задержки рейса (FlightDelayType).


Результаты доступны для просмотра и другими средствами визуализации Kibana

Например, в Kibana Lens. Ниже построен бар-чарт фактических и предсказанных задержек рейсов с разбивкой по погоде в пункте назначения.



Кроме индекса с предсказанными моделью значениями, основным результатом выполнения задания также является обученная модель. Выгрузить модель из Elastic в JSON файл позволяет эндпоинт trained_models, как указано здесь.


В этом же отчете можно увидеть метрики качества модели:


  • среднеквадратическая ошибка (Mean Squared Error, MSE);
  • коэффициент детерминации (R-squared, R2 );
  • псевдо-функция потерь Хьюбера (Pseudo-Huber loss);
  • среднеквадратичная логарифмическая ошибка (Mean squared logarithmic error, MSLE).


Метрики качества считаются отдельно, как для используемых при обучении данных, так и для тестовой выборки, что позволяет оценить применимость модели для новых, не используемых при обучении данных.


После того как модель сформирована, она может применяться для предсказания значений по новым данным. Для этого в Elasticsearch есть две возможности:


  • анализ поступающих в Elasticsearch данных (Inference processor);
  • анализ ранее сохранённых в Elasticsearch данных (Inference aggregation).

Пример c Inference processor
POST /_ingest/pipeline/_simulate{  "pipeline" : {    "description" : "Flight delay prophecy",    "processors" : [       {          "inference": {           "model_id":"flights_delay_prophecy-1613827670191",           "inference_config": {             "regression": {               "results_field": "FlightDelayMin_prediction"             }           }         }       }    ]  },  "docs": [    {      "_index": "index",      "_id": "id",      "_source": {          "FlightNum" : "9HY9SWR",          "Origin" : "Frankfurt am Main Airport",          "OriginLocation" : {            "lon" : "8.570556",            "lat" : "50.033333"          },          "DestLocation" : {            "lon" : "151.177002",            "lat" : "-33.94609833"          },          "DistanceMiles" : 10247.856675613455,          "FlightTimeMin" : 1030.7704158599038,          "OriginWeather" : "Sunny",          "dayOfWeek" : 0,          "AvgTicketPrice" : 841.2656419677076,          "Carrier" : "Kibana Airlines",          "FlightDelayMin" : 0,          "OriginRegion" : "DE-HE",          "FlightDelayType" : "No Delay",          "DestAirportID" : "SYD",          "timestamp" : "2021-02-08T00:00:00",          "Dest" : "Sydney Kingsford Smith International Airport",          "FlightTimeHour" : 17.179506930998397,          "DistanceKilometers" : 16492.32665375846,          "OriginCityName" : "Frankfurt am Main",          "DestWeather" : "Rain",          "OriginCountry" : "DE",          "DestCountry" : "AU",          "DestRegion" : "SE-BD",          "OriginAirportID" : "FRA",          "DestCityName" : "Sydney"        }    }  ]}

Пример c Inference aggregation
GET kibana_sample_data_flights/_search{  "size":0,  "query": {    "term": {      "FlightNum": {        "value": "00HGV4F"      }    }  },   "aggs": {    "res": {       "composite": {         "size": 1,        "sources": [          {            "FlightNum": {              "terms": {                "field": "FlightNum"                              }            }          }        ]      },      "aggs" : {        "AvgTicketPrice": {          "max": {            "field": "AvgTicketPrice"          }        },       "Dest": {           "scripted_metric": {             "init_script": "state.Dest = ''",             "map_script": "state.Dest = params._source.Dest",             "combine_script": "return state.Dest",             "reduce_script": "for (d in states) if (d != null) return d"           }         },         "DestAirportID": {           "scripted_metric": {             "init_script": "state.DestAirportID = ''",             "map_script": "state.DestAirportID = params._source.DestAirportID",             "combine_script": "return state.DestAirportID",             "reduce_script": "for (d in states) if (d != null) return d"           }         },         "DestRegion": {           "scripted_metric": {             "init_script": "state.DestRegion = ''",             "map_script": "state.DestRegion = params._source.DestRegion",             "combine_script": "return state.DestRegion",             "reduce_script": "for (d in states) if (d != null) return d"           }         },         "DistanceKilometers": {          "max": {            "field": "DistanceKilometers"          }        },        "DistanceMiles": {          "max": {            "field": "DistanceMiles"          }        },        "FlightDelayType": {           "scripted_metric": {             "init_script": "state.FlightDelayType = ''",             "map_script": "state.FlightDelayType = params._source.FlightDelayType",             "combine_script": "return state.FlightDelayType",             "reduce_script": "for (d in states) if (d != null) return d"           }         },         "FlightTimeMin": {          "max": {            "field": "FlightTimeMin"          }        },        "Origin": {           "scripted_metric": {             "init_script": "state.Origin = ''",             "map_script": "state.Origin = params._source.Origin",             "combine_script": "return state.Origin",             "reduce_script": "for (d in states) if (d != null) return d"           }         },         "OriginAirportID": {           "scripted_metric": {             "init_script": "state.OriginAirportID = ''",             "map_script": "state.OriginAirportID = params._source.OriginAirportID",             "combine_script": "return state.OriginAirportID",             "reduce_script": "for (d in states) if (d != null) return d"           }         },        "OriginRegion": {           "scripted_metric": {             "init_script": "state.OriginRegion = ''",             "map_script": "state.OriginRegion = params._source.OriginRegion",             "combine_script": "return state.OriginRegion",             "reduce_script": "for (d in states) if (d != null) return d"           }         },        "FlightDelayMin_prediction" : {          "inference": {            "model_id": "flights_delay_prophecy-1613827670191",             "buckets_path": {               "AvgTicketPrice": "AvgTicketPrice",               "Dest": "Dest.value",               "DestAirportID": "DestAirportID.value",               "DestRegion": "DestRegion.value",               "DistanceKilometers": "DistanceKilometers",               "DistanceMiles": "DistanceMiles",               "FlightDelayType": "FlightDelayType.value",               "FlightTimeMin": "FlightTimeMin",               "Origin": "Origin.value",               "OriginAirportID": "OriginAirportID.value",               "OriginRegion": "OriginRegion.value"             }          }        }      }     }  }}

Классификация


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


Порядок действий по созданию задания классификации совпадает с регрессионным анализом, только проверочным полем будет Cancelled.



Как и в предыдущих типах анализа, для числовых полей можно построить диаграммы рассеяния, при этом соответствующие значениям классы, в нашем случае true и false, выделяются цветом.



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


После завершения задачи можно оценить результаты предсказания отмены рейса.



В таблице результатов данные датасета дополнены полями:


  • отметка об использовании данных для обучения (ml.is_training);
  • предсказанный статус отмены рейса (ml.Cancelled_prediction);
  • вероятность предсказания (ml.prediction_probability);
  • влияние признаков на результат (ml.feature_importance);
  • оценка предсказания для всех искомых классов (ml.prediction_score).

Пример сохраненных результатов расчётов ml.prediction_score
"top_classes": [        {          "class_probability": 0.9617513617353418,          "class_score": 0.24080996012552122,          "class_name": false        },        {          "class_probability": 0.03824863826465814,          "class_score": 0.03824863826465814,          "class_name": true        }      ]

В понимании параметров оценки поможет документация Elastic.


Как и в регрессии, в результатах доступен график усреднённых значений важности признаков.



Для проверки результатов в классификации доступны следующие метрики:


  • матрица ошибок (Confusion matrix);
  • площадь кривой ошибок (area under receiver operating characteristic curve, AUC ROC).

Матрица ошибок визуализируется на дашборде с результатами выполнения задания и отображает доли верных, ложноположительных и ложноотрицательных предсказаний для каждого класса.



Кривая ошибок и значение площади AUC ROC в версии 7.11 не отображается, но значения можно получить через API. В 7.12 такая возможность уже будет добавлена.


Пример запроса AUC ROC
POST _ml/data_frame/_evaluate{   "index": "cancelled_flights_prediction_2",   "evaluation": {      "classification": {          "actual_field": "Cancelled",          "predicted_field": "ml.Cancelled_prediction",          "metrics": {           "auc_roc": {             "class_name": "true"           }          }      }   }}

и ответа
{  "classification" : {    "auc_roc" : {      "value" : 0.8240223547611558    }  }}   

Сохранённая модель доступна для использования в Inference processor и Inference aggregation, как в предыдущем примере.


Заключение


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


В будущем переход от бета-тестирования к релизным версиям описанных алгоритмов должен позволить анализировать большие потоки данных и может стать полезным инструментом для аналитиков центров мониторинга кибербезопасности, ИТ-инфраструктуры, приложений и бизнес-процессов.


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

Подробнее..

Мониторинг бизнес-процессов Camunda

07.01.2021 18:21:18 | Автор: admin

Привет, Хабр.

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

Это продолжение цикла статей о нашем опыте использования движка для работы с диаграммами бизнес-процессов Camunda. Предыдущая статья была посвящена разработке плагина для Bitbucket позволяющего просматривать изменения BPMN-схем. Сегодня я расскажу о мониторинге проектов, в которых используется Camunda, как с помощью сторонних инструментов (в нашем случае это стек Elasticsearch из Kibana и Grafana), так и родного для Camunda Cockpit. Опишу сложности, возникшие при использовании Cockpit, и наши решения.

Когда у тебя много микросервисов, то хочется знать об их работе и текущем статусе всё: чем больше мониторинга, тем увереннее ты себя чувствуешь как в штатных, так и внештатных ситуациях, во время релиза и так далее. В качестве средств мониторинга мы используем стек Elasticsearch: Kibana и Grafana. В Kibana смотрим логи, а в Grafana метрики. Также в БД имеются исторические данные по процессам Camunda. Казалось бы, этого должно хватать для понимания, работает ли сервис штатно, и если нет, то почему. Загвоздка в том, что данные приходится смотреть в трёх разных местах, и они далеко не всегда имеют четкую связь друг с другом. На разбор и анализ инцидента может уходить много времени. В частности, на анализ данных из БД: Camunda имеет далеко не очевидную схему данных, некоторые переменные хранит в сериализованном виде. По идее, облегчить задачу может Cockpit инструмент Camunda для мониторинга бизнес-процессов.


Интерфейс Cockpit.

Главная проблема в том, что Cockpit не может работать по кастомному URL. Об этом на их форуме есть множество реквестов, но пока такой функциональности из коробки нет. Единственный выход: сделать это самим. У Cockpit есть Sring Boot-автоконфигурация CamundaBpmWebappAutoConfiguration, вот её-то и надо заменить на свою. Нас интересует CamundaBpmWebappInitializer основной бин, который инициализирует веб-фильтры и сервлеты Cockpit.

Нам необходимо передать в основной фильтр (LazyProcessEnginesFilter) информацию об URL, по которому он будет работать, а в ResourceLoadingProcessEnginesFilter информацию о том, по каким URL он будет отдавать JS- и CSS-ресурсы.

Для этого в нашей реализации CamundaBpmWebappInitializer меняем строчку:

registerFilter("Engines Filter", LazyProcessEnginesFilter::class.java, "/api/*", "/app/*")

на:

registerFilter("Engines Filter", CustomLazyProcessEnginesFilter::class.java, singletonMap("servicePath", servicePath), *urlPatterns)

servicePath это наш кастомный URL. В самом же CustomLazyProcessEnginesFilter указываем нашу реализацию ResourceLoadingProcessEnginesFilter:

class CustomLazyProcessEnginesFilter:       LazyDelegateFilter<ResourceLoaderDependingFilter>       (CustomResourceLoadingProcessEnginesFilter::class.java)

В CustomResourceLoadingProcessEnginesFilter добавляем servicePath ко всем ссылкам на ресурсы, которые мы планируем отдавать клиентской стороне:

override fun replacePlaceholder(       data: String,       appName: String,       engineName: String,       contextPath: String,       request: HttpServletRequest,       response: HttpServletResponse) = data.replace(APP_ROOT_PLACEHOLDER, "$contextPath$servicePath")           .replace(BASE_PLACEHOLDER,                   String.format("%s$servicePath/app/%s/%s/", contextPath, appName, engineName))           .replace(PLUGIN_PACKAGES_PLACEHOLDER,                   createPluginPackagesString(appName, contextPath))           .replace(PLUGIN_DEPENDENCIES_PLACEHOLDER,                   createPluginDependenciesString(appName))

Теперь мы можем указывать нашему Cockpit, по какому URL он должен слушать запросы и отдавать ресурсы.

Но ведь не может быть всё так просто? В нашем случае Cockpit не способен работать из коробки на нескольких экземплярах приложения (например, в подах Kubernetes), так как вместо OAuth2 и JWT используется старый добрый jsessionid, который хранится в локальном кэше. Это значит, что если попытаться залогиниться в Cockpit, подключенный к Camunda, запущенной сразу в нескольких экземплярах, имея на руках ей же выданный jsessionid, то при каждом запросе ресурсов от клиента можно получить ошибку 401 с вероятностью х, где х = (1 1/количество_под). Что с этим можно сделать? У Cockpit во всё том же CamundaBpmWebappInitializer объявлен свой Authentication Filter, в котором и происходит вся работа с токенами; надо заменить его на свой. В нём из кеша сессии берём jsessionid, сохраняем его в базу данных, если это запрос на авторизацию, либо проверяем его валидность по базе данных в остальных случаях. Готово, теперь мы можем смотреть инциденты по бизнес-процессам через удобный графический интерфейс Cockpit, где сразу видно stacktrace-ошибки и переменные, которые были у процесса на момент инцидента.

И в тех случаях, когда причина инцидента ясна по stacktrace исключения, Cockpit позволяет сократить время разбора инцидента до 3-5 минут: зашел, посмотрел, какие есть инциденты по процессу, глянул stacktrace, переменные, и вуаля инцидент разобран, заводим баг в JIRA и погнали дальше. Но что если ситуация немного сложнее, stacktrace является лишь следствием более ранней ошибки или процесс вообще завершился без создания инцидента (то есть технически всё прошло хорошо, но, с точки зрения бизнес-логики, передались не те данные, либо процесс пошел не по той ветке схемы). В этом случае надо снова идти в Kibana, смотреть логи и пытаться связать их с процессами Camunda, на что опять-таки уходит много времени. Конечно, можно добавлять к каждому логу UUID текущего процесса и ID текущего элемента BPMN-схемы (activityId), но это требует много ручной работы, захламляет кодовую базу, усложняет рецензирование кода. Весь этот процесс можно автоматизировать.

Проект Sleuth позволяет трейсить логи уникальным идентификатором (в нашем случае UUID процесса). Настройка Sleuth-контекста подробно описана в документации, здесь я покажу лишь, как запустить его в Camunda.

Во-первых, необходимо зарегистрировать customPreBPMNParseListeners в текущем processEngine Camunda. В слушателе переопределить методы parseStartEvent (добавление слушателя на событие запуска верхнеуровневого процесса) и parseServiceTask (добавление слушателя на событие запуска ServiceTask).

В первом случае мы создаем Sleuth-контекст:

customContext[X_B_3_TRACE_ID] = businessKeycustomContext[X_B_3_SPAN_ID] = businessKeyHalfcustomContext[X_B_3_PARENT_SPAN_ID] = businessKeyHalfcustomContext[X_B_3_SAMPLED] = "0" val contextFlags: TraceContextOrSamplingFlags = tracing.propagation()       .extractor(OrcGetter())       .extract(customContext)val newSpan: Span = tracing.tracer().nextSpan(contextFlags)tracing.currentTraceContext().newScope(newSpan.context())

и сохраняем его в переменную бизнес-процесса:

execution.setVariable(TRACING_CONTEXT, sleuthService.tracingContextHeaders)

Во втором случае мы его из этой переменной восстанавливаем:

val storedContext = execution       .getVariableTyped<ObjectValue>(TRACING_CONTEXT)       .getValue(HashMap::class.java) as HashMap<String?, String?>val contextFlags: TraceContextOrSamplingFlags = tracing.propagation()       .extractor(OrcGetter())       .extract(storedContext)val newSpan: Span = tracing.tracer().nextSpan(contextFlags)tracing.currentTraceContext().newScope(newSpan.context())

Нам нужно трейсить логи вместе с дополнительными параметрами, такими как activityId (ID текущего BPMN-элемента), activityName (его бизнес-название) и scenarioId (ID схемы бизнес-процесса). Такая возможность появилась только с выходом Sleuth 3.

Для каждого параметра нужно объявить BaggageField:

companion object {   val HEADER_BUSINESS_KEY = BaggageField.create("HEADER_BUSINESS_KEY")   val HEADER_SCENARIO_ID = BaggageField.create("HEADER_SCENARIO_ID")   val HEADER_ACTIVITY_NAME = BaggageField.create("HEADER_ACTIVITY_NAME")   val HEADER_ACTIVITY_ID = BaggageField.create("HEADER_ACTIVITY_ID")}

Затем объявить три бина для обработки этих полей:

@Beanopen fun propagateBusinessProcessLocally(): BaggagePropagationCustomizer =       BaggagePropagationCustomizer { fb ->           fb.add(SingleBaggageField.local(HEADER_BUSINESS_KEY))           fb.add(SingleBaggageField.local(HEADER_SCENARIO_ID))           fb.add(SingleBaggageField.local(HEADER_ACTIVITY_NAME))           fb.add(SingleBaggageField.local(HEADER_ACTIVITY_ID))       }/** [BaggageField.updateValue] now flushes to MDC  */@Beanopen fun flushBusinessProcessToMDCOnUpdate(): CorrelationScopeCustomizer =       CorrelationScopeCustomizer { builder ->           builder.add(SingleCorrelationField.newBuilder(HEADER_BUSINESS_KEY).flushOnUpdate().build())           builder.add(SingleCorrelationField.newBuilder(HEADER_SCENARIO_ID).flushOnUpdate().build())           builder.add(SingleCorrelationField.newBuilder(HEADER_ACTIVITY_NAME).flushOnUpdate().build())           builder.add(SingleCorrelationField.newBuilder(HEADER_ACTIVITY_ID).flushOnUpdate().build())       }/** [.BUSINESS_PROCESS] is added as a tag only in the first span.  */@Beanopen fun tagBusinessProcessOncePerProcess(): SpanHandler =       object : SpanHandler() {           override fun end(context: TraceContext, span: MutableSpan, cause: Cause): Boolean {               if (context.isLocalRoot && cause == Cause.FINISHED) {                   Tags.BAGGAGE_FIELD.tag(HEADER_BUSINESS_KEY, context, span)                   Tags.BAGGAGE_FIELD.tag(HEADER_SCENARIO_ID, context, span)                   Tags.BAGGAGE_FIELD.tag(HEADER_ACTIVITY_NAME, context, span)                   Tags.BAGGAGE_FIELD.tag(HEADER_ACTIVITY_ID, context, span)               }               return true           }       }

После чего мы можем сохранять дополнительные поля в контекст Sleuth:

HEADER_BUSINESS_KEY.updateValue(businessKey)HEADER_SCENARIO_ID.updateValue(scenarioId)HEADER_ACTIVITY_NAME.updateValue(activityName)HEADER_ACTIVITY_ID.updateValue(activityId)

Когда мы можем видеть логи отдельно по каждому бизнес-процессу по его ключу, разбор инцидентов проходит гораздо быстрее. Правда, всё равно приходится переключаться между Kibana и Cockpit, вот бы их объединить в рамках одного UI.

И такая возможность имеется. Cockpit поддерживает пользовательские расширения плагины, в Kibana есть Rest API и две клиентские библиотеки для работы с ним: elasticsearch-rest-low-level-client и elasticsearch-rest-high-level-client.

Плагин представляет из себя проект на Maven, наследуемый от артефакта camunda-release-parent, с бэкендом на Jax-RS и фронтендом на AngularJS. Да-да, AngularJS, не Angular.

У Cockpit есть подробная документация о том, как писать для него плагины.

Уточню лишь, что для вывода логов на фронтенде нас интересует tab-панель на странице просмотра информации о Process Definition (cockpit.processDefinition.runtime.tab) и странице просмотра Process Instance (cockpit.processInstance.runtime.tab). Для них регистрируем наши компоненты:

ViewsProvider.registerDefaultView('cockpit.processDefinition.runtime.tab', {   id: 'process-definition-runtime-tab-log',   priority: 20,   label: 'Logs',   url: 'plugin://log-plugin/static/app/components/process-definition/processDefinitionTabView.html'});ViewsProvider.registerDefaultView('cockpit.processInstance.runtime.tab', {   id: 'process-instance-runtime-tab-log',   priority: 20,   label: 'Logs',   url: 'plugin://log-plugin/static/app/components/process-instance/processInstanceTabView.html'});

У Cockpit есть UI-компонент для вывода информации в табличном виде, однако ни в одной документации про него не сказано, информацию о нем и о его использовании можно найти, только читая исходники Cockpit. Если вкратце, то использование компонента выглядит следующим образом:

<div cam-searchable-area (1)    config="searchConfig" (2)    on-search-change="onSearchChange(query, pages)" (3)    loading-state="Loading..." (4)    text-empty="Not found"(5)    storage-group="'ANU'"    blocked="blocked">   <div class="col-lg-12 col-md-12 col-sm-12">       <table class="table table-hover cam-table">           <thead cam-sortable-table-header (6)                  default-sort-by="time"                  default-sort-order="asc" (7)                  sorting-id="admin-sorting-logs"                  on-sort-change="onSortChanged(sorting)"                  on-sort-initialized="onSortInitialized(sorting)" (8)>           <tr>               <!-- headers -->           </tr>           </thead>           <tbody>           <!-- table content -->           </tbody>       </table>   </div></div>

  1. Атрибут для объявления компонента поиска.
  2. Конфигурация компонента. Здесь имеем такую структуру:

    tooltips = { //здесь мы объявляем плейсхолдеры и сообщения,                    //которые будут выводиться в поле поиска в зависимости от результата   'inputPlaceholder': 'Add criteria',   'invalid': 'This search query is not valid',   'deleteSearch': 'Remove search',   'type': 'Type',   'name': 'Property',   'operator': 'Operator',   'value': 'Value'},operators =  { //операторы, используемые для поиска, нас интересует сравнение строк     'string': [       {'key': 'eq',  'value': '='},       {'key': 'like','value': 'like'}   ]},types = [// поля, по которым будет производится поиск, нас интересует поле businessKey   {       'id': {           'key': 'businessKey',           'value': 'Business Key'       },       'operators': [           {'key': 'eq', 'value': '='}       ],       enforceString: true   }]
    

  3. Функция поиска данных используется как при изменении параметров поиска, так и при первоначальной загрузке.
  4. Какое сообщение отображать во время загрузки данных.
  5. Какое сообщение отображать, если ничего не найдено.
  6. Атрибут для объявления таблицы отображения данных поиска.
  7. Поле и тип сортировки по умолчанию.
  8. Функции сортировок.

На бэкенде нужно настроить клиент для работы с Kibana API. Для этого достаточно воспользоваться RestHighLevelClient из библиотеки elasticsearch-rest-high-level-client. Там указать путь до Kibana, данные для аутентификации: логин и пароль, а если используется протокол шифрования, то надо указать подходящую реализацию X509TrustManager.

Для формирования запроса поиска используем QueryBuilders.boolQuery(), он позволяет составлять сложные запросы вида:

val boolQueryBuilder = QueryBuilders.boolQuery();KibanaConfiguration.ADDITIONAL_QUERY_PARAMS.forEach((key, value) ->       boolQueryBuilder.filter()               .add(QueryBuilders.matchPhraseQuery(key, value)));if (!StringUtils.isEmpty(businessKey)) {   boolQueryBuilder.filter()           .add(QueryBuilders.matchPhraseQuery(KibanaConfiguration.BUSINESS_KEY, businessKey));}if (!StringUtils.isEmpty(procDefKey)) {   boolQueryBuilder.filter()           .add(QueryBuilders.matchPhraseQuery(KibanaConfiguration.SCENARIO_ID, procDefKey));}if (!StringUtils.isEmpty(activityId)) {   boolQueryBuilder.filter()           .add(QueryBuilders.matchPhraseQuery(KibanaConfiguration.ACTIVITY_ID, activityId));}

Теперь мы прямо из Cockpit можем просматривать логи отдельно по каждому процессу и по каждой activity. Выглядит это так:


Таб для просмотра логов в интерфейсе Cockpit.

Но нельзя останавливаться на достигнутом, в планах идеи о развитии проекта. Во-первых, расширить возможности поиска. Зачастую в начале разбора инцидента business key процесса на руках отсутствует, но имеется информация о других ключевых параметрах, и было бы неплохо добавить возможность настройки поиска по ним. Также таблица, в которую выводится информация о логах, не интерактивна: нет возможности перехода в нужный Process Instance по клику в соответствующей ему строке таблицы. Словом, развиваться есть куда. (Как только закончатся выходные, я опубликую ссылку на Github проекта, и приглашаю туда всех заинтересовавшихся.)
Подробнее..

Как лицензируется и чем отличаются лицензии Elastic Stack (Elasticsearch)

05.11.2020 02:15:38 | Автор: admin
В этой статье расскажем как лицензируется Elastic Stack, какие бывают лицензии, что туда входит (ключевые возможности), немножечко сравним Elastic с OpenDistro от AWS и другими известными дистрибутивами.



Как видно на картинке выше, существует 5 типов, условно говоря, подписок, по которым можно пользоваться системой. Подробности по тому, что написано ниже, вы можете узнать на специальной странице Elastic. Всё написанное в этой статье применимо к Elastic Stack, размещаемому на собственной инфраструктуре (on-premise).

Open Source. Это версия Elastic Stack, которая находится в свободном доступе в репозитории Elastic на Github. В принципе, вы можете взять ее и сделать убийцу Arcsight, QRadar, Splunk и других прямых конкурентов Elastic. Платить за это ничего не нужно.

Basic. Этот тип лицензии включает в себя возможности предыдущей лицензии, но дополнен функционалом, который не имеет открытого кода, но, тем не менее, доступен на бесплатной основе. Это, например, SIEM, доступ к ролевой модели, некоторые виды визуализаций в Kibana, Index Lifecycle Management, некоторые встроенные интеграции и другие возможности.

На этом бесплатные лицензии закончились и пришло время разобраться с платными лицензиями. Elastic Stack лицензируется по количеству нод Elasticsearch. Рядом может стоять хоть миллион Kibana и Logstash (или Fluentd, если угодно), но лицензии будут считаться именно по хостам, на которых развернут Elasticsearch. В расчет лицензий также не входят ноды с ролями Ingest, Client/Coordinating. На попадающее в расчет количество нод напрямую влияет объем входящего трафика и требования к хранению данных. Напомним, что для обеспечения надежности работы кластера, в нем должно быть минимум 3 ноды. Мы проводим расчет сайзинга исходя из методики, которую описывали в одной из предыдущих статей. При покупке лицензий Elasticsearch доступен только формат подписки длительностью от 1 года с шагом в 1 год (2, 3 и так далее). Теперь вернемся к типам лицензий.

Gold. В лицензии Elasticsearch уровня Gold появляется поддержка авторизации через LDAP/AD, расширеное логирование для внутреннего аудита, расширяются возможности алертинга и техподдержка вендора в рабочие часы. Именно подписка уровня Gold очень похожа на AWS OpenDistro.

Platinum. Наиболее популярный тип подписки. кроме возможностей уровня Gold, тут появляется встроенное в Elastic машинное обучение, кросс-кластерная репликация, поддержка клиентов ODBC/JDBC, возможность гранулярного управления доступом до уровня документов, поддержка вендора 24/7/365 и некоторые другие возможности. Ещё в рамках этой подписки они могут выпускать Emergency patches.

Enterprise. Самый выскоий уровень подписки. Кроме всех возможностей уровня Platinum, сюда входят оркестратор Elastic Cloud Enterprise, Elastic Cloud on Kubernetes, решение по безопасности для конечных устройств Endgame (со всеми его возможностями), поддержка вендором неограниченного количества проектов на базе Elastic и другие возможности. Обычно используется в крупных и очень крупных инсталляциях.

У Elastic появилось уже немало форков, самый известный из которых OpenDistro от AWS. Его ключевым преимуществом является поддержка некоторых возможностей оригинального Elastic, доступных на платных подписках. Основные это интеграция с LDAP/AD (а еще SAML, Kerberos и другими), встроенный алертинг (на бесплатном Elastic это реализуется через Elast Alert), логирование действий пользователей и поддержка JDBC-драйверов.

Упомянем также про HELK и Logz.io. Первый проект на Github, который обвешивает Elasticsearch дополнительным ПО для аналитики угроз (пишут, что пока это всё находится в альфе), а второй облачный сервис, основанный на Elastic и добавляющий некоторые приятные фичи. В комментариях можно поделиться другими форками, о которых вам известно.

Для получения дополнительной информации о сайзинге Elasticsearch, лицензировании и по другим вопросам, вы можете оставить заявку в форме обратной связи на сайте или другим удобным способом.

А ещё можно почитать:

Сайзинг Elasticsearch

Разбираемся с Machine Learning в Elastic Stack (он же Elasticsearch, он же ELK)

Elastic под замком: включаем опции безопасности кластера Elasticsearch для доступа изнутри и снаружи

Что полезного можно вытащить из логов рабочей станции на базе ОС Windows
Подробнее..

Изучаем ELK. Часть I Установка Elasticsearch

23.01.2021 14:23:52 | Автор: admin

Вступительное слово

Эта статья является первой в серии статей по стеку Elasticsearch, Logstash, Kibana (ELK). Цикл статей ориентирован на тех, кто только начинает знакомится со стеком ELK, и содержит минимально необходимый набор знаний, чтобы успешно запустить свой первый кластер ELK.

В рамках данного цикла будут рассмотрены такие темы, как:

  • установка и настройка компонентов ELK,

  • безопасность кластера, репликация данных и шардирование,

  • конфигурирование Logstash и Beat для сборки и отправки данных в Elasticsearch,

  • визуализация в Kibana

  • запуск стека в Docker.

В данной статье будет рассмотрена процедура установки Elasticsearch и конфигурирование кластера.

План действий:

  1. Скачиваем и устанавливаем Elasticsearch.

  2. Настраиваем кластер.

  3. Запускаем и проверяем работоспособность кластера.

  4. Делаем важные настройки.

Скачиваем и устанавливаем Elasticsearch

Существует множество вариантов установки Elasticsearch, под любые нужды и желания. С перечнем можно ознакомится на официальном сайте. Не смотря на то, что на своем стенде я использовал установку из Deb пакетов, считаю правильным так же описать установку из RPM пакетов и архива tar.gz для Linux системы.

Установка из Deb пакетов

  • Импортируем Elasticsearch PGP ключ:

wget -qO - https://artifacts.elastic.co/GPG-KEY-elasticsearch | sudo apt-key add -
  • Устанавливаем apt-transport-https пакет:

sudo apt-get install apt-transport-https
  • Перед установкой пакета необходимо добавить репозиторий Elastic:

echo "deb https://artifacts.elastic.co/packages/7.x/apt stable main" | sudo tee /etc/apt/sources.list.d/elastic-7.x.list
  • Устанавливаем Elasticsearch из пакета:

sudo apt-get update && sudo apt-get install elasticsearch
  • Настраиваем Elasticsearch для автоматического запуска при запуске системы:

sudo /bin/systemctl daemon-reload && sudo /bin/systemctl enable elasticsearch.service

Установка из RPM пакетов

  • Импортируем Elasticsearch PGP ключ:

rpm --import https://artifacts.elastic.co/GPG-KEY-elasticsearch
  • В директории /etc/yum.repos.d/ создаем файл репозитория Elasticsearch elasticsearch.repo:

[elasticsearch]name=Elasticsearch repository for 7.x packagesbaseurl=https://artifacts.elastic.co/packages/7.x/yumgpgcheck=1gpgkey=https://artifacts.elastic.co/GPG-KEY-elasticsearchenabled=0autorefresh=1type=rpm-md
  • Устанавливаем Elasticsearch c помощью пакетного менеджера в зависимости от операционной системы, yum или dnf для CentOS, Red Hat, Fedora или zypper для OpenSUSE:

# Yumsudo yum install --enablerepo=elasticsearch elasticsearch # Dnfsudo dnf install --enablerepo=elasticsearch elasticsearch # Zyppersudo zypper modifyrepo --enable elasticsearch && \  sudo zypper install elasticsearch; \  sudo zypper modifyrepo --disable elasticsearch

Установка из архива tar.gz

  • Скачиваем архив с Elasticsearch

wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-7.10.1-linux-x86_64.tar.gz
  • Извлекаем данные из архива и переходим в каталог с Elasticsearch:

tar -xzf elasticsearch-7.10.1-linux-x86_64.tar.gzcd elasticsearch-7.10.1/

Текущий каталог считается, как $ES_HOME.

Конфигурационные файлы лежать в каталоге $ES_HOME/config/.

На этом шаге установка из архива считается завершенной.

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

Настраиваем кластер

На текущем этапе у нас есть три хоста с установленным Elasticsearch. Следующим и самым важным этапом является настройка каждого узла для объединения их в единый кластер.

Для настройки Elasticsearch используется YAML файл, который лежит по следующему пути /etc/elasticsearch/elasticsearch.yml при установке из Deb или RPM пакетов или $ES_HOME/config/elasticsearch.yml - при установке из архива.

Ниже будут приведены пример конфигурации для узла es-node01. Аналогичные настройки необходимо сделать на каждом узле, указав соответствующие имя и адрес.

  • Указываем имя узла и определяем роли. Укажем для узла роли master и data:

# ------------------------------------ Node ------------------------------------node.name: es-node01        # Имя нодыnode.roles: [ master, data ]  # Роли узла

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

data узел с данной ролью содержит данные и выполняет операции с этими данными

Со списком всех ролей узла можно ознакомится тут.

  • Настраиваем адрес и порт, на которых узел будет принимать запросы:

# ---------------------------------- Network -----------------------------------network.host: 10.0.3.11# Адрес узлаhttp.port: 9200# Порт

Если указать 0.0.0.0 или просто 0, то Elasticsearch будет принимать запросы на всех интерфейсах.

  • Определяем имя кластера и начальный список узлов в голосовании по выбору master узла:

# ---------------------------------- Cluster -----------------------------------cluster.name: es_cluster                                             # Имя кластераcluster.initial_master_nodes: ["es-node01","es-node02","es-node03"]  # Начальный набор мастер узлов

cluster.initial_master_nodes

При начальной загрузке кластера определяются узлы, участвующие в голосовании по выбору мастера, поэтому в новом кластере мы должны указать перечень этих узлов. Если данный параметр оставить пустым (по умолчанию), то узел будет ожидать подключения к уже созданному кластеру.

Данный параметр следует удалить после создания кластера и не использовать при добавлении новых узлов в существующий кластер.

  • Указываем список master узлов кластера:

# --------------------------------- Discovery ----------------------------------discovery.seed_hosts: ["10.0.3.11", "10.0.3.12", "10.0.3.13"] # Узлы кластера

До версии 7.0 в конфигурации Elasticsearch также использовался параметр discovery.zen.minimum_master_nodes, который можно увидеть и сейчас в некоторых конфигурациях. Этот параметр был необходим, чтобы защитится от Split Brain, и определял минимальное количество master узлов для голосования. Начиная с версии 7.0 данный параметр игнорируется, так как кластер автоматически защищает себя. Более подробно о том, как это работает, можно прочитать в данной статье.

  • Определяем, где будем хранить данные и логи

# ----------------------------------- Paths ------------------------------------path.data: /var/lib/elasticsearch # Директория с даннымиpath.logs: /var/log/elasticsearch # Директория с логами

На выходе мы должны получить следующий конфигурационный файл:

# ------------------------------------ Node ------------------------------------node.name: es-node01        # Имя нодыnode.roles: [ master, data ]  # Роли узла## ---------------------------------- Network -----------------------------------network.host: 10.0.3.11# Адрес узлаhttp.port: 9200# Порт## ---------------------------------- Cluster -----------------------------------cluster.name: es_cluster                                             # Имя кластераcluster.initial_master_nodes: ["es-node01","es-node02","es-node03"]  # Начальный набор мастер узлов## --------------------------------- Discovery ----------------------------------discovery.seed_hosts: ["10.0.3.11", "10.0.3.12", "10.0.3.13"] # Узлы кластера## ----------------------------------- Paths ------------------------------------path.data: /var/lib/elasticsearch # Директория с даннымиpath.logs: /var/log/elasticsearch # Директория с логами
  • При необходимости настраиваем межсетевой экран на хосте:

    • 9200 - прием HTTP запросов (согласно конфигурации выше http.port). По умолчанию Elasticsearch использует диапазон 9200-9300 и выбирает первый свободный.

    • 9300-9400 - порты (по умолчанию) для коммуникации между узлами кластера. Elasticsearch будет использовать первый свободный из данного диапазона (для настройки данного порта в Elasticsearch можно использовать параметр transport.port).

Запускаем и проверяем

Запускаем на каждом узле службу elasticsearch:

sudo systemctl start elasticsearch.service

Для установки из архива используем:

$ES_HOME/bin/elasticsearch

или если мы хотим запустить Elasticsearch как демон, то:

$ES_HOME/bin/elasticsearch -d -p pid

Для выключения службы используйте Ctrl-C для первого варианта запуска (из архива) или pkill -F pid для второго варианта.

После запуска первого узла в логах можно увидеть, что узел ожидает подключение других узлов, чтобы определить, кто возьмет роль master:

[es-node01] master not discovered yet, this node has not previously joined a bootstrapped (v7+) cluster, and this node must discover master-eligible nodes [es-node01, es-node02, es-node03] to bootstrap a cluster: have discovered [{es-node01}{olhmN6eCSuGxF4yH0Q-cgA}{CHniuFCYS-u67R5mfysg8w}{10.0.3.11}{10.0.3.11:9300}{dm}{xpack.installed=true, transform.node=false}]; discovery will continue using [10.0.3.12:9300, 10.0.3.13:9300] from hosts providers and [{es-node01}{olhmN6eCSuGxF4yH0Q-cgA}{CHniuFCYS-u67R5mfysg8w}{10.0.3.11}{10.0.3.11:9300}{dm}{xpack.installed=true, transform.node=false}] from last-known cluster state; node term 0, last-accepted version 0 in term 0

После включения остальных узлов в логах появится запись о том, что кластер собрался:

[es-node01] master node changed {previous [], current [{es-node02}{VIGgr6_aS-C39yrnmoZQKw}{pye7sBQUTz6EFh7Pqn7CJA}{10.0.3.12}{10.0.3.12:9300}{dm}{xpack.installed=true, transform.node=false}]}, added {{es-node02}{VIGgr6_aS-C39yrnmoZQKw}{pye7sBQUTz6EFh7Pqn7CJA}{10.0.3.12}{10.0.3.12:9300}{dm}{xpack.installed=true, transform.node=false}}, term: 1, version: 1, reason: ApplyCommitRequest{term=1, version=1, sourceNode={es-node02}{VIGgr6_aS-C39yrnmoZQKw}{pye7sBQUTz6EFh7Pqn7CJA}{10.0.3.12}{10.0.3.12:9300}{dm}{xpack.installed=true, transform.node=false}}

Проверяем состояние кластера, обратившись к любому его узлу:

curl -X GET "http://10.0.3.11:9200/_cluster/health?pretty"{  "cluster_name" : "es_cluster",  "status" : "green",  "timed_out" : false,  "number_of_nodes" : 3,  "number_of_data_nodes" : 3,  "active_primary_shards" : 0,  "active_shards" : 0,  "relocating_shards" : 0,  "initializing_shards" : 0,  "unassigned_shards" : 0,  "delayed_unassigned_shards" : 0,  "number_of_pending_tasks" : 0,  "number_of_in_flight_fetch" : 0,  "task_max_waiting_in_queue_millis" : 0,  "active_shards_percent_as_number" : 100.0}

Узнаем, какой узел взял на себя роль master. В нашем примере это узел es-node02:

curl -X GET "http://10.0.3.11:9200/_cat/master?pretty"VIGgr6_aS-C39yrnmoZQKw 10.0.3.12 10.0.3.12 es-node02

Делаем важные настройки

Для нормальной работы кластера необходимо произвести еще некоторые настройки.

Heap size

Так как Elasticsearch написан на Java, то для работы необходимо настроить размер кучи (heap size). В каталоге установки Elasticsearch имеется файл jvm.options, в котором уже указаны размеры, по умолчанию - это 1 Гб. Однако, для настройки рекомендуется указать новые параметры в файле каталога jvm.options.d, который находится там же.

-Xms16g-Xmx16g

Пример выше использует минимальный Xms и максимальный Xmx размер heap size, равный 16 Гб. Для настройки данных параметров следует использовать следующие рекомендации:

  • установите значения Xmx и Xms не более 50% от имеющейся физической памяти узла. Оставшуюся память Elasticsearch будет использовать для других целей. Чем больше heap size, тем меньше памяти используется под системные кеш;

  • устанавите значение не более того значения, которое использует JVM для сжатия указателей, он же compressed object pointers. Данное значение составляет около 32 Гб. Стоит также отметить, что рекомендуется ограничивать heap size еще одним параметром JVM, а именно zero-based compressed oops (обычно размер около 26 Гб). Узнать подробнее об этих параметрах можно тут.

Отключаем подкачку

Подкачка негативно сказывается на производительности и стабильности работы Elasticsearch, ведь система может выгрузить страницы JVM на диск. Есть несколько вариантов работы с подкачкой:

  1. Полное отключение подкачки. Перезапуск Elasticseach при этом не требуется.

    sudo swapoff -a
    
  2. Ограничение подкачки через значение vm.swappiness=1 для sysctl.

  3. Использование mlockall для блокировки адресного пространства в оперативной памяти.

Чтобы включить mlockall в конфигурации Elasticseach elasticsearch.yml указываем для параметра bootstrap.memory_lock значение true.

bootstrap.memory_lock: true

Перезапускаем Elasticsearch и проверяем настройки через запрос к любому узлу:

curl -X GET "http://10.0.3.12:9200/_nodes?filter_path=**.mlockall&pretty"{  "nodes" : {    "olhmN6eCSuGxF4yH0Q-cgA" : {      "process" : {        "mlockall" : true      }    },    "VIGgr6_aS-C39yrnmoZQKw" : {      "process" : {        "mlockall" : true      }    },    "hyfhcEtyQMK3kKmvYQdtZg" : {      "process" : {        "mlockall" : true      }    }  }}

Если перезапуск Elasticsearch завершился ошибкой вида:

[1] bootstrap checks failed[1]: memory locking requested for elasticsearch process but memory is not locked

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

  • Установка из архива

Установитеulimit -l unlimitedперед запуском Elasticsearch или значениюmemlockустанвоитеunlimitedв файле/etc/security/limits.conf.

  • Установка из пакета RPM или Deb

Установите значение параметраMAX_LOCKED_MEMORYкакunlimitedв /etc/sysconfig/elasticsearch для rpm или /etc/default/elasticsearch для dep.

Если вы используете systemd для запуска Elasticsearch, то лимиты должны быть указаны через настройку параметра LimitMEMLOCK. Для этого выполните команду:

sudo systemctl edit elasticsearch

и укажите следующее значение:

[Service]LimitMEMLOCK=infinity

Настройка файловых дескрипторов

Elasticsearch работает с большим количеством файловых дескрипторов, и их нехватка может катастрофически сказаться на его работе. Согласно официальной документации необходимо указать значение файловых дескрипторов не ниже 65 536.

  • Если Elasticsearch установлен из RPM или Deb пакетов, то настройка не требуется.

  • Для установки из архива необходимо в файле /etc/security/limits.conf установить параметр nofile для пользователя, который осуществляет запуск Elasticsearch. В примере ниже таким пользователем является elasticsearch:

elasticsearch - nofile 65536

Проверить установленные значения можно следующим образом:

curl -X GET "http://10.0.3.11:9200/_nodes/stats/process?filter_path=**.max_file_descriptors&pretty"{  "nodes" : {    "olhmN6eCSuGxF4yH0Q-cgA" : {      "process" : {        "max_file_descriptors" : 65535      }    },    "VIGgr6_aS-C39yrnmoZQKw" : {      "process" : {        "max_file_descriptors" : 65535      }    },    "hyfhcEtyQMK3kKmvYQdtZg" : {      "process" : {        "max_file_descriptors" : 65535      }    }  }}

Настройка виртуальной памяти

Elasticsearch по умолчанию использует каталог mmapfs для хранения индексов, и ограничение операционной системы по умолчанию для счетчиков mmap может привести к нехватки памяти. Для настройки запустите из-под root следующую команду:

sysctl -w vm.max_map_count=262144

Чтобы настройка сохранилась после перезапуска системы, необходимо указать параметр vm.max_map_count в файле /etc/sysctl.conf.

Если Elasticsearch установлен из RPM или Deb пакетов, то настройка не требуется.

Настройка потоков

Необходимо убедиться, что количество потоков, которые Elasticsearch может создавать, было не менее 4096.

Это можно сделать, установив ulimit -u 4096 или установив значение nproc равным 4096 в файле /etc/security/limits.conf.

Если Elasticsearch работает под управлением systemd, то настройка не требуется.

DNS кеширование

По умолчанию Elasticsearch кеширует результат DNS на 60 и 10 секунд для положительных и отрицательных запросов. В случае необходимости можно настроить эти параметры, а именно es.networkaddress.cache.ttl и es.networkaddress.cache.negative.ttl, как JVM опции в каталоге /etc/elasticsearch/jvm.options.d/ для RPM и Deb или в $ES_HOME/config/jvm.options.d/ для установки из архива.

Временный каталог для JNA

Elasticsearch использует Java Native Access (JNA) для запуска кода, необходимого в его работе, и извлекает этот код в свой временный каталог директории /tmp. Следовательно, если каталог смонтирован с опцией noexec, то данный код невозможно будет выполнить.

В данном случае необходимо или перемонтировать каталог /tmp без опции noexec, или изменить настройку JVM, указав другой путь через опцию -Djna.tmpdir=<new_path>.

На этом шаге дополнительные настройки Elasticsearch окончены.

Заключение

Дойдя до этого места, вы должны иметь работоспособный кластер Elasticsearch.

В следующей статье будут описаны процедуры установки и настройки Kibana и Logstash. А в качестве проверки работоспособности кластера соберём данные из файла и посмотрим на них с помощью Kibana.

Полезные ссылки:

Подробнее..

Сборка логов в kubernetes. Установка EFK стека с LDAP интеграцией. (Bitnami, opendistro)

26.01.2021 10:19:42 | Автор: admin

Постепенно эволюционируя, каждая организация переходит от ручного grep логов к более современным инструментам для сбора, анализа логов. Если вы работаете с kubernetes, где приложение может масштабироваться горизонтально и вертикально, вас просто вынуждают отказаться от старой парадигмы сбора логов. В текущее время существует большое количество вариантов систем централизованного логирования, причем каждый выбирает наиболее приемлемый вариант для себя. В данной статье пойдет речь о наиболее популярном и зарекомендовавшем себя стэке Elasticsearch + Kibana + Fluentd в связке с плагином OpenDistro Security. Данный стэк полностью open source, что придает ему особую популярность.

Проблематика

Нам необходимо было наладить сборку логов с кластера kubernetes. Из требований:

  • Использовать только открытые решения.

  • Сделать очистку логов.

  • Необходимо прикрутить LDAP, для разграничения прав.

Пререквизиты

Данный материал предполагает:

  • Имеется установленный kuberenetes 1.18 или выше (ниже версию не проверяли)

  • Установленный пакетный менеджер helm 3

Немного о helm chart

Наиболее популярным способом установки приложения в kubernetes является helm. Helm это пакетный менеджер, с помощью которого можно подготовить набор компонентов для установки и связать их вместe, дополнив необходимыми метриками и конфигурацией.

В своей практике мы используем helm chart от компании bitnami(подразделение vmware)

  • собраны open source продукты на все случаи жизни.

  • корпоративные стандарты по разработке чатов и докер образов

  • красивая документация

  • проект живой и активно поддерживается сообществом

Выбор стека

Во многом выбор стека технологий определило время. С большой долей вероятностью два года назад мы бы деплоили ELK стек вместо EFK и не использовали helm chart.

Fluentd часто упоминается в документации, широко распространен, имеет большое количество плагинов, на все случаи жизни. К тому же у нас есть человек, который после обучение в rebrain и очень хотел внедрить fluentd.

Elasticsearch и kibana поставляются с открытой лицензией, однако плагины для security и других вкусностей идут под иной лицензией. Однако компания Amazon выпустила плагины Open Distro, которые покрывают оставшийся функционал под открытой лицензией.

Схема выглядит примерно так

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

Минимальный деплой EFK стека (без Security)

Сборка EFK стека была произведена по статье Collect and Analyze Log Data for a Kubernetes Cluster with Bitnami's Elasticsearch, Fluentd and Kibana Charts. Компоменты упакованы в отдельный чат. Исходники можно взять здесь и произвести командой

helm dependency updatehelm upgrade --install efk . -f values-minimal.yaml

Из исходников values-minimal.yaml

elasticsearch:  volumePermissions:    enabled: truekibana:  volumePermissions:    enabled: true  elasticsearch:    hosts:    - "efk-elasticsearch-coordinating-only"    port: 9200# Пропишите свой хост, если используете ингресс  ingress:    enabled: true    hostname: kibana.local# Либо   service:    type: NodePort    port: 30010fluentd:  aggregator:    enabled: true    configMap: elasticsearch-output    extraEnv:    - name: ELASTICSEARCH_HOST      value: "efk-elasticsearch-coordinating-only"    - name: ELASTICSEARCH_PORT      value: "9200"  forwarder:#    Чтение логов с диска /var/log/containers/* отключено    enabled: false    configMap: apache-log-parser    extraEnv:    - name: FLUENTD_DAEMON_USER      value: root    - name: FLUENTD_DAEMON_GROUP      value: root

Кибана будет доступна по адресу http://minikube_host:30010/, либо http://kibana.local.

Как видим компонент fluentd forwarder не включен. Перед включением парсинга, я рекомендовал бы настроить на определенные логи, иначе еластику может быть послано слишком большое количество логов. Правила для парсинга описаны в файле apache-log-parser.yaml.

Как отправить логи в EFK

Существует много способов, для начала предлагаем либо включить fluentd forwarder, либо воспользоваться простейшим приложением на python. Ниже пример для bare metal. Исправьте FLUENT_HOST FLUENT_PORT на ваши значения.

cat <<EOF | kubectl apply -f -apiVersion: apps/v1kind: Deploymentmetadata:  name: helloworld  labels:    app: helloworldspec:  replicas: 1  template:    metadata:      name: helloworld      labels:        app: helloworld    spec:      containers:        - name: helloworld          image: sergbs/django-hello-world:1          imagePullPolicy: Always          ports:            - containerPort: 8000          env:            - name: FLUENT_HOST              value: "efk-fluentd-headless"            - name: FLUENT_PORT              value: "24224"  selector:    matchLabels:      app: helloworld---apiVersion: v1kind: Servicemetadata:  name: helloworldspec:  selector:    app: helloworld  ports:    - port: 8000      nodePort: 30011  type: NodePortEOF

По ссылке http://minikube_host:30011/ Будет выведено "Hello, world!" И лог уйдет в elastic

Пример

Включить fluentd forwarder, он создаст daemon set, т.е. запустится на каждой ноде вашего кубернетеса и будет читать логи docker container-ов.

Добавить в ваш докер контейнер драйвер fluentd, тем более можно добавлять более одного драйвера

Добавить в ваше приложение библиотеку, которая будет напрямую логировать. Пример приложения на python. Используйте его, как отладочное средство при установке EFK.

И как показывает практика, логировать напрямую эффективный, но далеко не самый надежный способ. Даже если вы логируете сразу в fluentd и в консоль. В случае потери конекта, во fluentd часть логов просто не смогут попасть и будут потеряны для него навсегда. Поэтому наиболее надежный способ, это считывать логи с диска для отправки в EFK.

Очистка логов.

Для очистки логов используется curator. Его можно включить, добавив в yaml файл:

elasticsearch:  curator:    enabled: true

По умолчанию его конфигурация уже предусматривает удаление через индекса старше 90 дней это можно увидеть внутри подчата efk/charts/elasticsearch-12.6.1.tgz!/elasticsearch/values.yaml

configMaps:  # Delete indices older than 90 days  action_file_yml: |-      ... unit: daysunit_count: 90

Security

Как обычно security доставляет основную боль при настройке и использовании. Но если ваша организация чуть подросла, это необходимый шаг. Стандартом де факто при настройке безопасности является интеграция с LDAP. Официальные плагины от еластика выходят не под открытой лицензией, поэтому приходится использовать плагин Open Distro. Далее продемонстрируем, как его можно запустить.

Сборка elasticsearch c плагином opendistro

Вот проект в котором собирали docker images.

Для установки плагина, необходимо, чтобы версия elasticsearch соответствовала версии плагина.

В quickstart плагина рекомендуется установить install_demo_configuration.sh с демо сертификатами.

FROM bitnami/elasticsearch:7.10.0RUN elasticsearch-plugin install -b https://d3g5vo6xdbdb9a.cloudfront.net/downloads/elasticsearch-plugins/opendistro-security/opendistro_security-1.12.0.0.zipRUN touch /opt/bitnami/elasticsearch/config/elasticsearch.ymlUSER rootRUN /bin/bash /opt/bitnami/elasticsearch/plugins/opendistro_security/tools/install_demo_configuration.sh -y -iRUN mv /opt/bitnami/elasticsearch/config/elasticsearch.yml /opt/bitnami/elasticsearch/config/my_elasticsearch.ymlCOPY my_elasticsearch.yml /opt/bitnami/elasticsearch/config/my_elasticsearch.ymlUSER 1001

Есть небольшая магия, ввиду, того что плагин дополняет elasticsearch.yml, а контейнеры bitnami только при старте генерируют этот файл. Дополнительные же настройки они просят передавать через my_elasticsearch.yml

В my_elasticsearch.yml мы изменили настройку, это позволит нам обращаться к рестам elasticsearch по http.

# turn off REST layer tlsopendistro_security.ssl.http.enabled: false

Сделано это в демонстрационных целях, для облегчения запуска плагина. Если вы захотите включить Rest Layer tls придется добавлять соответствующие настройки во все компоненты, которые общаются с elasticsearch.

Запуск docker-compose с LDAP интеграцией

Запустим проект с помощью docker-compose up , и залогинимся на http://0:5601 под admin/admin

Теперь у нас есть вкладка Security

Можно посмотреть настройку LDAP через интерфейс кибаны

Настройки должны совпадать с файлами конфигурации. Стоит обратить внимание, что плагин хранит свои данные в индексе еластика, и файлы конфигурации применяет при инициализации, т.е. если индекс не создан. Если вы измените файлы позже то вам придется воспользоваться утилитой securityadmin.sh

docker exec -it elasticsearch /bin/bashI have no name!@68ac2255bb85:/$ ./securityadmin_demo.shOpen Distro Security Admin v7Will connect to localhost:9300 ... doneConnected as CN=kirk,OU=client,O=client,L=test,C=deElasticsearch Version: 7.10.0Open Distro Security Version: 1.12.0.0Contacting elasticsearch cluster 'elasticsearch' and wait for YELLOW clusterstate ...Clustername: elasticsearchClusterstate: YELLOWNumber of nodes: 1Number of data nodes: 1.opendistro_security index already exists, so we do not need to create one.Populate config from /opt/bitnami/elasticsearch/plugins/opendistro_security/securityconfig/Will update '_doc/config' with /opt/bitnami/elasticsearch/plugins/opendistro_security/securityconfig/config.yml    SUCC: Configuration for 'config' created or updatedWill update '_doc/roles' with /opt/bitnami/elasticsearch/plugins/opendistro_security/securityconfig/roles.yml    SUCC: Configuration for 'roles' created or updatedWill update '_doc/rolesmapping' with /opt/bitnami/elasticsearch/plugins/opendistro_security/securityconfig/roles_mapping.yml    SUCC: Configuration for 'rolesmapping' created or updatedWill update '_doc/internalusers' with /opt/bitnami/elasticsearch/plugins/opendistro_security/securityconfig/internal_users.yml    SUCC: Configuration for 'internalusers' created or updatedWill update '_doc/actiongroups' with /opt/bitnami/elasticsearch/plugins/opendistro_security/securityconfig/action_groups.yml    SUCC: Configuration for 'actiongroups' created or updatedWill update '_doc/tenants' with /opt/bitnami/elasticsearch/plugins/opendistro_security/securityconfig/tenants.yml    SUCC: Configuration for 'tenants' created or updatedWill update '_doc/nodesdn' with /opt/bitnami/elasticsearch/plugins/opendistro_security/securityconfig/nodes_dn.yml    SUCC: Configuration for 'nodesdn' created or updatedWill update '_doc/whitelist' with /opt/bitnami/elasticsearch/plugins/opendistro_security/securityconfig/whitelist.yml    SUCC: Configuration for 'whitelist' created or updatedWill update '_doc/audit' with /opt/bitnami/elasticsearch/plugins/opendistro_security/securityconfig/audit.yml    SUCC: Configuration for 'audit' created or updatedDone with successI have no name!@68ac2255bb85:/$ 

настройка Ldap

Для настройки интеграции с LDAP необходимо заполнить соответствующие секции в elastisearch-opendistro-sec/config.yml, ссылка на официальную документацию по authentication

config:  dynamic:    authc:      # тут можно настроить авторизацию          authz:      # а здесь аутентификацию

В случае с Active Directory, необходимо будет изменить конфигурационный файл примерно так:

      # тут можно настроить авторизацию          ldap:        ...            hosts:              - ldaphost:389            bind_dn: cn=LDAP,ou=Example,dc=example,dc=ru            password: CHANGEME            userbase: 'DC=example,DC=ru'            usersearch: '(sAMAccountName={0})'            username_attribute: sAMAccountName

Не забудьте воспользоваться securityadmin.sh после изменения конфигурации. Процедура была описана в предыдущем параграфе.

Установка EFK + Opendistro в kubernetes

Вернемся к проекту с kubernetes, установим проект командой

helm upgrade --install efk . -f values.yaml  

Нам необходимо будет

Для настройка OpenDistro Security plugin мы скопировали файл конфигурации, которые поместим в секреты kubernetes.

  extraVolumes:  - name: config    secret:      secretName: opendistro-config      items:        - key: config.yml          path: config.yml  - name: roles-mapping    secret:      secretName: opendistro-config      items:        - key: roles_mapping.yml          path: roles_mapping.yml  extraVolumeMounts:    - mountPath: /opt/bitnami/elasticsearch/plugins/opendistro_security/securityconfig/config.yml      subPath: config.yml      name: config    - mountPath: /opt/bitnami/elasticsearch/plugins/opendistro_security/securityconfig/roles_mapping.yml      subPath: roles_mapping.yml      name: roles-mapping

Чтобы настройки применялись при команде helm upgrade, мы сделали job, который будет запускаться при каждой команде helm upgrade

apiVersion: batch/v1kind: Jobmetadata:    name: opendistro-config-reload    labels:        app.kubernetes.io/managed-by: {{.Release.Service | quote }}        app.kubernetes.io/instance: {{.Release.Name | quote }}    annotations:        "helm.sh/hook": post-upgrade        "helm.sh/hook-delete-policy": hook-succeededspec:    template:        metadata:            name: config-reload            labels:                app.kubernetes.io/managed-by: {{.Release.Service | quote }}                app.kubernetes.io/instance: {{.Release.Name | quote }}        spec:            initContainers:                - name: "wait-for-db"                  image: "alpine:3.6"                  command:                      - 'sh'                      - '-c'                      - >                          until nc -z -w 2 efk-elasticsearch-coordinating-only 9300 && echo elastic is ok; do                            sleep 2;                          done;            containers:                - name: opendistro-config-reload                  image: "{{ .Values.elasticsearch.image.registry }}/{{ .Values.elasticsearch.image.repository}}:{{ .Values.elasticsearch.image.tag }}"                  imagePullPolicy: {{ .Values.elasticsearch.image.pullPolicy | quote }}                {{- if .Values.elasticsearch.master.securityContext.enabled }}                  securityContext:                    runAsUser: {{ .Values.elasticsearch.master.securityContext.runAsUser }}                 {{- end }}                  command:                    - 'bash'                    - '-c'                    - >                       "/opt/bitnami/elasticsearch/plugins/opendistro_security/tools/securityadmin.sh" -h efk-elasticsearch-coordinating-only -cd "/opt/bitnami/elasticsearch/plugins/opendistro_security/securityconfig" -icl -key "/opt/bitnami/elasticsearch/config/kirk-key.pem" -cert "/opt/bitnami/elasticsearch/config/kirk.pem" -cacert "/opt/bitnami/elasticsearch/config/root-ca.pem" -nhnv            restartPolicy: Never    backoffLimit: 1

Итоги

Если вы собираетесь организовать централизованную сборку логов для приложений под управление kubernetes, данный вариант мог бы стать вполне обоснованным решением. Все продукты поставляются под открытыми лицензиями. В статье приведена заготовка из которой можно создать production ready решение. Спасибо за внимание!

Подробнее..

Изучаем ELK. Часть II Установка Kibana и Logstash

28.01.2021 22:20:35 | Автор: admin

Вступительное слово

В предыдущей статье была описана процедура установки Elasticsearch и настройка кластера. В этой статье будет рассмотрена процедура установки Kibana и Logstash, а также их настройка для работы с кластером Elasticsearch.

План действий

  1. Скачиваем и устанавливаем Kibana.

  2. Настраиваем Kibana для работы с кластером Elasticsearch.

  3. Настраиваем балансировку нагрузки между Kibana и Elasticsearch.

  4. Настраиваем несколько экземпляров Kibana.

  5. Скачиваем и устанавливаем Logstash.

  6. Настраиваем Logstash для чтения данных из файла.

  7. Смотрим полученные данные в Kibana.

Скачиваем и устанавливаем Kibana

Установка из Deb пакета

  • Импортируем PGP ключ:

wget -qO - https://artifacts.elastic.co/GPG-KEY-elasticsearch | sudo apt-key add -
  • Устанавливаем apt-transport-https пакет:

sudo apt-get install apt-transport-https
  • Перед установкой пакета необходимо добавить репозиторий Elastic:

echo "deb https://artifacts.elastic.co/packages/7.x/apt stable main" | sudo tee -a /etc/apt/sources.list.d/elastic-7.x.list
  • Устанавливаем Kibana:

sudo apt-get update && sudo apt-get install kibana
  • Настраиваем Kibana для автоматического запуска при старте системы:

sudo /bin/systemctl daemon-reload && sudo /bin/systemctl enable kibana.service

Так же возможен вариант установки из скаченного Deb пакет с помощью dpkg

wget https://artifacts.elastic.co/downloads/kibana/kibana-7.10.2-amd64.debsudo dpkg -i kibana-7.10.2-amd64.deb

Установка из RPM пакета

  • Импортируем PGP ключ

sudo rpm --import https://artifacts.elastic.co/GPG-KEY-elasticsearch
  • В директории /etc/yum.repos.d/ создаем файл репозитория kibana.repo для CentOS или Red Hat. Для дистрибутива OpenSUSE в директории /etc/zypp/repos.d/:

[kibana-7.x]name=Kibana repository for 7.x packagesbaseurl=https://artifacts.elastic.co/packages/7.x/yumgpgcheck=1gpgkey=https://artifacts.elastic.co/GPG-KEY-elasticsearchenabled=1autorefresh=1type=rpm-md
  • Устанавливаем Kibana c помощью пакетного менеджера в зависимости от операционной системы,yumилиdnfдляCentOS,Red Hat,FedoraилиzypperдляOpenSUSE:

# Yumsudo yum install kibana # Dnfsudo dnf install kibana # Zyppersudo zypper install kibana
  • Настраиваем Kibana для автоматического запуска при старте системы:

sudo /bin/systemctl daemon-reload && sudo /bin/systemctl enable kibana.service

Так же возможен вариант установки из скаченного RPM пакет с помощью rpm

wget https://artifacts.elastic.co/downloads/kibana/kibana-7.10.2-x86_64.rpmsudo rpm --install kibana-7.10.2-x86_64.rpm

Установка из архива tar.gz

  • Скачиваем архив c Kibana:

curl -O https://artifacts.elastic.co/downloads/kibana/kibana-7.10.2-linux-x86_64.tar.gz
  • Извлекаем данные и переходим в директорию с Kibana:

tar -xzf kibana-7.10.2-linux-x86_64.tar.gzcd kibana-7.10.2-linux-x86_64/

Текущий каталог считается как $KIBANA_HOME.

Конфигурационные файлы находятся в каталоге$KIBANA_HOME/config/.

Для запускаKibanaможно создать отдельного пользователя предоставив все необходимые права к каталогу сKibana.

Настраиваем Kibana для работы с кластером Elasticsearch

Для настройки Kibana используется YAML файл, который лежит по следующему пути /etc/kibana/kibana.yml при установке из Deb и RPM пакетов или $KIBANA_HOME/config/kibana.yml при установке из архива.

  • Определяем адрес и порт, на которых будет работать Kibana (по умолчанию localhost:5601):

server.host: 10.0.3.1server.port: 5601
  • Указываем узлы кластера Elasticsearch:

elasticsearch.hosts:  - http://10.0.3.11:9200  - http://10.0.3.12:9200  - http://10.0.3.13:9200

В случае недоступности узла, с которым Kibana установила соединение, произойдет переключение на другой узел кластера, указанный в конфигурационном файле.

  • Указываем, где Kibana будет хранить свои логи (по умолчанию stdout):

logging.dest: /var/log/kibana/kibana.log

Необходимо предоставить доступ на запись к данному каталогу, чтобы Kibana писала логи. В случае установки из пакетов Dep или RPM доступ предоставляется пользователю или группе kibana, а для установки из архива - пользователю, который осуществляет запуск Kibana.

  • Настраиваем частоту опроса Elasticsearch для получения обновлённого списка узлов кластера:

elasticsearch.sniffInterval: 600000
  • Определяем, запрашивать ли обновленный список узлов кластера Elasticsearch в случае сбоя соединения с кластером:

elasticsearch.sniffOnConnectionFault: true

На выходе получаем конфигурационный файл:

# Адрес и портserver.host: 10.0.3.1server.port: 5601# Узлы кластера Elasticsearchelasticsearch.hosts:  - http://10.0.3.11:9200  - http://10.0.3.12:9200  - http://10.0.3.13:9200# Частота запросов обновления узлов кластера# Запрос обновлений в случае сбоя подключенияelasticsearch.sniffInterval: 60000elasticsearch.sniffOnConnectionFault: true# Логи Kibanalogging.dest: /var/log/kibana/kibana.log

По умолчанию Kibana имеет ограничение на использование памяти в размере 1,4 Гб. Для изменения этого значения необходимо в файле node.options указать новые значения для параметра --max-old-space-size. Значения указываются в мегабайтах.

Данный файл находится в каталоге /etc/kibana/ при установке из Deb и RPM пакетов или $KIBANA_HOME/config/ - при установке из архива

  • Запускаем службу kibana:

sudo systemctl start kibana.service

Для установки из архива используем:

$KIBANA_HOME/bin/kibana

Для выключения службы запущенной из архива используйтеCtrl-C.

В браузере набираем IP адрес и порт (в примере выше это http://10.0.3.1:5601), который указывали в конфигурации Kibana. В результате должна открыться приветственная страница.

Приветственная страница KibanaПриветственная страница Kibana

Настраиваем балансировку нагрузки между Kibana и Elasticsearch

Для организации балансировки между Kibana и узлами Elastcisearch в Elastcisearch имеется встроенный механизм. На хост c установленной Kibana, ставится Elasticsearch с ролью Coordinating only. Узел Elasticsearch с данной ролью обрабатывает HTTP запросы и распределяет их между узлами кластера.

Установка и настройка Elasticsearch

  • Устанавливаем Elasticseach на узел с Kibana. Как это сделать описано в предыдущей статье.

  • Настраиваем новому узлу роль Coordinating only. Для этого для ролей master, data и ingest указываем параметр false:

node.master: falsenode.data: falsenode.ingest: false

ingest роль позволяет построить конвейер дополнительной обработки данных до их индексирования. Выделения отдельных узлов с этой ролью снижает нагрузку на другие узлы. Узел с ролью master и/или data имеют эту роль по умолчанию.

  • Указываем имя Elasticsearch кластера:

cluster.name: es_cluster  # Имя кластера
  • Указываем адреса узлов текущего кластера. Для этого создадим файл unicast_hosts.txt с перечнем узлов в директории с конфигурационными файлами Elasticsearch. Для установки из Deb и RPM пакетов это /etc/elasticsearch/ или $ES_HOME/config/ при установке из архива:

# Список узлов кластера10.0.3.1110.0.3.1210.0.3.13
  • Указываем, что адреса узлов кластера нужно брать из файла:

discovery.seed_providers: file

Данный метод имеет преимущество над методом из предыдущей статьи (discovery.seed_hosts). Elasticsearch следит за изменением файла и применяет настройки автоматически без перезагрузки узла.

  • Настраиваем IP адрес и порт для приема запросов от Kibana (network.host и http.port) и для коммуникации с другими узлами кластера Elasticsearch(transport.host и transport.tcp.port). По умолчанию параметр transport.host равен network.host:

network.host: 10.0.3.1          # Адрес узлаhttp.port: 9200          # Портtransport.host: 10.0.3.1          # Адрес для связи с кластеромtransport.tcp.port: 9300-9400   # Порты для коммуникации внутри кластера

Итоговый конфигурационный файл:

# ------------------------------------ Node ------------------------------------# Имя узлаnode.name: es-nlb01# Указываем роль Coordinating onlynode.master: falsenode.data: falsenode.ingest: false## ---------------------------------- Cluster -----------------------------------#cluster.name: es_cluster  # Имя кластера## --------------------------------- Discovery ----------------------------------discovery.seed_providers: file                       ## ---------------------------------- Network -----------------------------------#network.host: 10.0.3.1          # Адрес узлаhttp.port: 9200          # Портtransport.host: 10.0.3.1          # Адрес для связи с кластеромtransport.tcp.port: 9300-9400     # Порты для коммуникации внутри кластера## ----------------------------------- Paths ------------------------------------#path.data: /var/lib/elasticsearch # Директория с даннымиpath.logs: /var/log/elasticsearch # Директория с логами
  • Запускаем Elasticsearch и проверяем, что узел присоединился к кластеру:

curl -X GET "http://10.0.3.1:9200/_cluster/health?pretty"{  "cluster_name" : "es_cluster",  "status" : "green",  "timed_out" : false,  "number_of_nodes" : 4,  "number_of_data_nodes" : 3,  "active_primary_shards" : 6,  "active_shards" : 12,  "relocating_shards" : 0,  "initializing_shards" : 0,  "unassigned_shards" : 0,  "delayed_unassigned_shards" : 0,  "number_of_pending_tasks" : 0,  "number_of_in_flight_fetch" : 0,  "task_max_waiting_in_queue_millis" : 0,  "active_shards_percent_as_number" : 100.0}

Теперь в кластере 4 узла, из них 3 с ролью data.

Настраиваем Kibana

  • В конфигурации Kibana указываем адрес Coordinating only узла:

elasticsearch.hosts:  - http://10.0.3.1:9200
  • Перезапускаем Kibana и проверяем, что служба запустилась, а UI Kibana открывается в браузере.

Настраиваем несколько экземпляров Kibana

Чтобы организовать работу нескольких экземпляров Kibana, размещаем их за балансировщиком нагрузки. Для каждого экземпляра Kibana необходимо:

  • Настроить уникальное имя:

    • server.name уникальное имя экземпляра. По умолчанию имя узла.

В документации к Kibana указана необходимость настройки server.uuid, однако, в списке параметров конфигурации данный параметр отсутствует. На практике uuid генерируется автоматически и хранится в директории, в которой Kibana хранит свои данные. Данная директория определяется параметром path.data. По умолчанию для установки из Deb и RPM пакетов это /var/lib/kibana/ или $KIBANA_HOME/data/ для установки из архива.

  • Для каждого экземпляра в рамках одного узла необходимо указать уникальные настройки:

    • logging.dest директория для хранения логов;

    • path.data директория для хранения данных;

    • pid.file файл для записи ID процесса;

    • server.port порт, который будет использовать данный экземпляр Kibana ;

  • Указать следующие параметры одинаковыми для каждого экземпляра:

    • xpack.security.encryptionKey произвольный ключ для шифрования сессии. Длина не менее 32 символов.

    • xpack.reporting.encryptionKey произвольный ключ для шифрования отчетов. Длина не менее 32 символов.

    • xpack.encryptedSavedObjects.encryptionKey ключ для шифрования данных до отправки их в Elasticsearch. Длина не менее 32 символов.

    • xpack.encryptedSavedObjects.keyRotation.decryptionOnlyKeys список ранее использованных ключей. Позволит расшифровать ранее сохраненные данные.

Настройки безопасности(xpack) ELK будут рассмотрены в отдельной статье.

Чтобы запустить несколько экземпляров Kibana на одном узле, необходимо указать путь к файлу конфигурации каждого из них. Для этого используется ключ -c:

bin/kibana -c <путь_к_файлу_конфигурации_#1>bin/kibana -c <путь_к_файлу_конфигурации_#2>

Скачиваем и устанавливаем Logstash

Установка из Deb пакета

  • Импортируем GPG ключ:

wget -qO - https://artifacts.elastic.co/GPG-KEY-elasticsearch | sudo apt-key add -
  • Устанавливаем apt-transport-https пакет:

sudo apt-get install apt-transport-https
  • Добавляем репозиторий Elastic:

echo "deb https://artifacts.elastic.co/packages/7.x/apt stable main" | sudo tee -a /etc/apt/sources.list.d/elastic-7.x.list
  • Устанавливаем Logstash:

sudo apt-get update && sudo apt-get install logstash
  • Настраиваем Logstash для автоматического запуска при старте системы:

sudo /bin/systemctl daemon-reload && sudo /bin/systemctl enable logstash.service

Установка из RPM пакета

  • Импортируем PGP ключ

sudo rpm --import https://artifacts.elastic.co/GPG-KEY-elasticsearch
  • В директории /etc/yum.repos.d/ создаем файл репозитория logstash.repo для CentOS или Red Hat. Для дистрибутива OpenSUSE - в директории /etc/zypp/repos.d/

[logstash-7.x]name=Elastic repository for 7.x packagesbaseurl=https://artifacts.elastic.co/packages/7.x/yumgpgcheck=1gpgkey=https://artifacts.elastic.co/GPG-KEY-elasticsearchenabled=1autorefresh=1type=rpm-md
  • Устанавливаем Logstash c помощью пакетного менеджера в зависимости от операционной системы,yumилиdnfдляCentOS,Red Hat,FedoraилиzypperдляOpenSUSE:

# Yumsudo yum install logstash # Dnfsudo dnf install logstash # Zyppersudo zypper install logstash
  • Настраиваем Logstash для автоматического запуска при старте системы:

sudo /bin/systemctl daemon-reload && sudo /bin/systemctl enable logstash.service

Установка из архива

  • Скачиваем архив с Logstash:

curl -O https://artifacts.elastic.co/downloads/logstash/logstash-7.10.2-linux-x86_64.tar.gz
  • Извлекаем данные и переходим в директорию с Logstash:

tar -xzf logstash-7.10.2-linux-x86_64.tar.gzcd logstash-7.10.2/

Текущий каталог считается как $LOGSTASH_HOME.

Конфигурационные файлы находятся в каталоге$LOGSTASH_HOME/config/.

Помимо архива сайта можно скачать Deb или RPM пакет и установить с помощью dpkg или rpm.

Настраиваем Logstash для чтения данных из файла

В качестве примера настроим считывание собственных логов Logstash из директории /var/log/logstash/. Для этого необходимо настроить конвейер (pipeline).

Logstash имеет два типа файлов конфигурации. Первый тип описывает запуск и работу Logstash (settings files).

Второй тип отвечает за конфигурацию конвейера (pipeline) обработки данных. Этот файл состоит из трех секций: input, filter и output.

  • Чтобы описать конвейер создаем файл logstash.conf в директории /etc/logstash/conf.d/, если установка была из Deb и RPM, или в директории $LOGSTASH_HOME/conf.d/ для установки из архива, предварительно создав эту директорию.

Для установки из архива необходимо в конфигурационном файле $LOGSTASH_HOME/config/pipelines.yml указать путь до директории с настройками конвейера:

- pipeline.id: main  path.config: "./conf.d/*.conf"

Для установки из архива также требуется указать, где хранить логи в файле конфигурации $LOGSTASH_HOME/config/logstash.yml:

path.logs: /var/log/logstash/# логи Logstash

Не забудьте предоставить доступ на запись к данному каталогу пользователю, который осуществляет запуск Logstash.

  • В файле logstash.conf настроим плагин file в секции input для чтения файлов. Указываем путь к файлам через параметр path, а через параметр start_position указываем, что файл необходимо читать с начала:

input {  file {    path => ["/var/log/logstash/*.log"]    start_position => "beginning"  }}

Каждое событие журнала логов будет представлять собой JSON документ и будет записано целиком в поле с ключом message. Чтобы из каждого события извлечь дату, время, источник и другую информацию, необходимо использовать секцию filter.

  • В секции filter с помощью плагина grok и встроенных шаблонов извлекаем из каждой записи (message) журнала логов необходимую информацию:

filter {  grok {    match => { "message" => "\[%{TIMESTAMP_ISO8601:timestamp}\]\[%{DATA:severity}%{SPACE}\]\[%{DATA:source}%{SPACE}\]%{SPACE}%{GREEDYDATA:message}" }    overwrite => [ "message" ]  }}

Шаблон имеет формат %{SYNTAX:SEMANTIC} и по сути это оболочка над регулярным выражением, соответственно шаблон можно описать регулярным выражением. С перечнем всех встроенных шаблоном можно ознакомится тут.

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

С помощью параметра match указываем, из какого поля (message) извлекаем данные по описанным шаблонам. Параметр overwrite сообщает, что оригинальное поле message необходимо перезаписать в соответствии с теми данными, которые мы получили с помощью шаблона %{GREEDYDATA:message}.

  • Для сохранения данных в Elasticsearch настраиваем плагин elasticsearch в секции output. Указываем адреса узлов кластера Elasticseach и имя индекса.

Индекс (index) - оптимизированный набор JSON документов, где каждый документ представляет собой набор полей ключ - значение. Каждый индекс сопоставляется с одним или более главным шардом (primary shard) и может иметь реплики главного шарда (replica shard).

Что такое индекс, шард и репликация шардов рассмотрю в отдельной статье.

output {  elasticsearch {    hosts => ["http://10.0.3.11:9200","http://10.0.3.12:9200","http://10.0.3.13:9200"]    index => "logstash-logs-%{+YYYY.MM}"  }}

К названию индекса добавлен шаблон %{+YYYY.MM}, описывающий год и месяц. Данный шаблон позволит создавать новый индекс каждый месяц.

Итоговый файл:

# Читаем файлinput {  file {    path => ["/var/log/logstash/*.log"]    start_position => "beginning"  }}# Извлекаем данные из событийfilter {  grok {    match => { "message" => "\[%{TIMESTAMP_ISO8601:timestamp}\]\[%{DATA:severity}%{SPACE}\]\[%{DATA:source}%{SPACE}\]%{SPACE}%{GREEDYDATA:message}" }    overwrite => [ "message" ]  }}# Сохраняем все в Elasticsearchoutput {  elasticsearch {    hosts => ["http://10.0.3.11:9200","http://10.0.3.12:9200","http://10.0.3.13:9200"]    index => "logstash-logs-%{+YYYY.MM}"  }}
  • Запускаем Logstash:

sudo systemctl start logstash.service

Для установки из архива:

$LOGSTASH_HOME/bin/logstash

Смотрим полученные данные в Kibana

Открываем Kibana, в верхнем левом углу нажимаем меню и в секции Management выбираем Stack Management. Далее слева выбираем Index patterns и нажимаем кнопку Create Index Patern. В поле Index pattern name описываем шаблон logstash* , в который попадут все индексы, начинающиеся с logstash.

Создание шаблона индексаСоздание шаблона индекса

Жмем Next step и выбираем Time field поле timestamp, чтобы иметь возможность фильтровать данные по дате и времени. После жмем Create index pattern:

Выбор Time fieldВыбор Time field

Logstash при анализе событий добавил поле @timestamp, в результате получилось 2 поля с датой и временем. Это поле содержит дату и время обработки события, следовательно, эти даты могут различаться. Например, если анализируемый файл имеет старые записи, то поле timestamp будет содержать данные из записей, а @timestamp текущее время, когда каждое событие были обработано.

После создания шаблона индексов Kibana покажет информацию об имеющихся полях, типе данных и возможности делать агрегацию по этим полям.

Чтобы посмотреть полученные данные на основе созданного шаблона нажимаем меню и в секции Kiban выбираем Discover.

Kibana DiscoverKibana Discover

В правой части экрана можно выбрать интервал в рамках которого отображать данные.

Выбор временного интервалаВыбор временного интервала

В левой часте экрана можно выбрать шаблон индекса или поля для отображения из списка Available fields. При нажатии на доступные поля можно получить топ-5 значений.

Шаблон индекса и доступные поляШаблон индекса и доступные поля

Для фильтрации данных можно использовать Kibana Query Language (KQL). Запрос пишется в поле Search. Запросы можно сохранять, чтобы использовать их в будущем.

Фильтрация данных с помощью KQLФильтрация данных с помощью KQL

Для визуализации полученных данных нажимаем меню и в секции Kiban выбираем Visualize. Нажав Create new visualization , откроется окно с перечнем доступных типов визуализации.

Типы визуализации KibanaТипы визуализации Kibana

Для примера выбираем Pie, чтобы построить круговую диаграмму. В качестве источника данных выбираем шаблон индексов logstash*. В правой части в секции Buckets жмем Add , далее - Split slices. Тип агрегации выбираем Terms, поле severity.keyword. Жмем Update в правом нижнем углу и получаем готовую диаграмму. В секции Options можно добавить отображение данных или изменить вид диаграммы.

Если вместо графика отобразилась надпись No results found, проверьте выбранный интервал времени.

Круговая диаграммаКруговая диаграмма

Чтобы посмотреть данные в Elasticsearch необходимо сделать GET запрос /имя_индекса/_search к любому узлу кластера. Добавление параметра pretty позволяет отобразить данные в читабельном виде. По умолчанию вывод состоит из 10 записей, чтобы увеличить это количество необходимо использовать параметр size:

curl -X GET "http://10.0.3.1:9200/logstash-logs-2021.01/_search?pretty&size=100"...{        "_index" : "logstash-logs-2021.01",        "_type" : "_doc",        "_id" : "XlXeQncBKsFiW7wX45A0",        "_score" : 1.0,        "_source" : {          "path" : "/var/log/logstash/logstash-plain.log",          "@version" : "1",          "source" : "logstash.outputs.elasticsearch",          "message" : "[main] Restored connection to ES instance {:url=>\"http://10.0.3.11:9200/\"}",          "host" : "logstash01",          "timestamp" : "2021-01-27T08:03:55,979",          "@timestamp" : "2021-01-27T08:03:58.907Z",          "severity" : "WARN"        }      },...

Заключение

В рамках этой статьи была рассмотрена процедура установки и настройки Kibana и Logstash, настройка балансировки трафика между Kibana и Elasticsearch и работа нескольких экземпляров Kibana. Собрали первые данные с помощью Logstash, посмотрели на данные с помощью Kibana Discover и построили первую визуализацию.

Прежде чем углубляться в изучение плагинов Logstash, сбор данных с помощью Beats, визуализацию и анализ данных в Kibana, необходимо уделить внимание очень важному вопросу безопасности кластера. Об этом также постоянно намекает Kibana, выдавая сообщение Your data is not secure.

Теме безопасности будет посвящена следующая статья данного цикла.

Полезные ссылки

Подробнее..

Изучаем ELK. Часть III Безопасность

03.03.2021 20:06:54 | Автор: admin

Вступительное слово

В первой и второй частях данной серии статей была описана процедура установки и настройки кластера Elasticsearch, Kibana и Logstash, но не был освящен вопрос безопасности.

В этой статье я расскажу, как настроить шифрование трафика между узлами кластера Elasticsearch и его клиентам, шифрование трафика между Kibana и клиентами, покажу, как создавать роли и пользователей, а так же выдавать API ключи для доступа к кластеру Elasticsearch.

План действий

  1. "Включаем" безопасность.

  2. Настраиваем шифрование между узлами Elasticsearch.

  3. Настраиваем аутентификацию.

  4. Настраиваем шифрование клиентского трафика Elasticsearch.

  5. Подключаем Kibana к Elasticsearch.

  6. Настраиваем шифрование трафика между Kibana и клиентами.

  7. Создаем пользователей и роли.

  8. Настраиваем пользовательские сессии в Kibana.

  9. Настраиваем Logstash.

  10. Создаем API ключи.

"Включаем" безопасность

Чтобы использовать функции безопасности в Elasticsearch, их необходимо активировать. Для этого на каждом узле в файле конфигурации указывается следующая настройка:

xpack.security.enabled: true

Настраиваем шифрование между узлами Elasticsearch

Следующим шагом необходимо настроить шифрование трафика между узлами Elasticsearch. Для этого выполняем несколько шагов:

  • Создаем CA (Certificate Authority) для кластера Elasticsearch:

./bin/elasticsearch-certutil ca

Для установки из пакетов Deb и RPM исполняемые файлы Elasticsearch лежат в каталоге /usr/share/elasticsearch/bin/. Для установки из архива - в каталоге $ES_HOME/bin/.

В примерах все команды выполнены из каталога /usr/share/elasticsearch/.

Во время генерации корневого сертификата можно задать имя PKCS#12 файла, по умолчанию это elastic-stack-ca.p12 и пароль к нему.

Для получения сертификата и ключа в PEM формате укажите ключ --pem. На выходе будет ZIP архив с .crt и .key файлами.

С помощью ключа --out можно указать каталог для создаваемого файла.

Полученный корневой сертификат (для PEM формата еще и ключ) надо переместить на все узлы кластера для генерации сертификата узла.

  • Генерируем на каждом узле кластера сертификат и ключ:

./bin/elasticsearch-certutil cert --ca elastic-stack-ca.p12 --ip 10.0.3.11 --dns es-node01

Ключ --ca указывает путь к корневому сертификату CA в формате PKCS#12. Если сертификат и ключ были получены в PEM формате, то необходимо использовать ключи --ca-cert и --ca-key соответственно.

Ключи --dns и --ip добавляют проверку по имени узла и IP адресу и являются опциональными. Если вы указываете их, то укажите эти параметры для каждого узла.

Ключ --pass позволяет установить passphrase для ключа.

  • Включаем TLS в файле конфигурации Elasticsearch:

xpack.security.transport.ssl.enabled: truexpack.security.transport.ssl.verification_mode: fullxpack.security.transport.ssl.keystore.path: es-node01-cert.p12xpack.security.transport.ssl.truststore.path: elastic-stack-ca.p12

Где:

xpack.security.transport.ssl.enabled - включаем TLS/SSL

xpack.security.transport.ssl.verification_mode - режим проверки сертификатов. none - проверка не выполняется, certificate - выполняется проверка сертификата без проверки имени узла и IP адреса, full - проверка сертификата, а также имени узла и адреса указанных в сертификате.

xpack.security.transport.ssl.keystore.path - путь к файлу с сертификатом и ключем узла.

xpack.security.transport.ssl.truststore.path - путь к доверенному сертификату (CA).

Если сертификаты CA и/или узла в формате PEM, то необходимо изменить последние два параметра на следующие:

xpack.security.transport.ssl.key - путь к закрытому ключу узла

xpack.security.transport.ssl.certificate - путь к сертификату узла

xpack.security.transport.ssl.certificate_authorities - список путей к сертифкатам CA.

  • Cоздаем keystore и добавляем пароли от сертификатов(если они были заданы):

./bin/elasticsearch-keystore create -p

Ключ -p требуется для установки пароля keystone во время создания

Для PKCS#12 формата:

./bin/elasticsearch-keystore add xpack.security.transport.ssl.keystore.secure_password./bin/elasticsearch-keystore add xpack.security.transport.ssl.truststore.secure_password

Для PEM сертификата:

./bin/elasticsearch-keystore add xpack.security.transport.ssl.securekeypassphrase

Чтобы запустить Elasticsearch с keystore, на который установлен пароль, необходимо передать этот пароль Elasticsearch. Это делается с помощью файла на который будет ссылаться переменная ES_KEYSTORE_PASSPHRASE_FILE. После запуска файл можно удалить, но при каждом последующим запуске файл необходимо создавать.

echo "password" > /etc/elasticsearch/ks_secret.tmpchmod 600 /etc/elasticsearch/ks_secret.tmpsudo systemctl set-environment ES_KEYSTORE_PASSPHRASE_FILE=/etc/elasticsearch/ks_secret.tmp
  • Перезапускаем Elasticsearch

В логах Elasticsearch должны появится записи о создании кластера:

[INFO ][o.e.c.s.ClusterApplierService] [es-node01] master node changed {previous [], current [{es-node02}{L1KdSSwCT9uBFhq0QlxpGw}{ujCcXRmOSn-EbqioSeDNXA}{10.0.3.12}{10.0.3.12:9300}...

Если обратиться к API, то будет ошибка "missing authentication credentials for REST request". С момента включения функций безопасности для обращения к кластер необходимо пройти аутентификацию.

Настраиваем аутентификацию

Elasticsearch имеет несколько встроенных пользователей:

Пользователь

Описание

elastic

Суперпользователь

kibana_system

Используется для коммуникации между Kibana и Elasticsearch

logstash_system

Пользователь, которого использует Logstash сервер, когда сохраняет информацию в Elasticsearch

beats_system

Пользователь, которого использует агент Beats, когда сохраняет информацию в Elasticsearch

apm_system

Пользователь, которого использует APM сервер, когда сохраняет информацию в Elasticsearch

remote_monitoring_user

Пользователь Metricbeat, который используется при сборе и хранении информации мониторинга в Elasticsearch

Прежде чем воспользоваться перечисленными пользователями, необходимо установить для них пароль. Для этого используется утилита elasticsearch-setup-passwords. В режиме interactive для каждого пользователя необходимо ввести пароли самостоятельно, в режиме auto Elasticsearch создаст пароли автоматически:

./bin/elasticsearch-setup-passwords autoInitiating the setup of passwords for reserved users elastic,apm_system,kibana,kibana_system,logstash_system,beats_system,remote_monitoring_user.The passwords will be randomly generated and printed to the console.Please confirm that you would like to continue [y/N]yChanged password for user apm_systemPASSWORD apm_system = NtvuRYhwbKpIEVUmHsZBChanged password for user kibana_systemPASSWORD kibana_system = ycXvzXglaLnrFMdAFsvyChanged password for user kibanaPASSWORD kibana = ycXvzXglaLnrFMdAFsvyChanged password for user logstash_systemPASSWORD logstash_system = vU3CuRbjBpax1RrsCCLFChanged password for user beats_systemPASSWORD beats_system = c9GQ85qhNL59H2AXUvcAChanged password for user remote_monitoring_userPASSWORD remote_monitoring_user = wB320seihljmGsjc29W5Changed password for user elasticPASSWORD elastic = iOrMTBbfHOAkm5CPeOj7

Второй раз elasticsearch-setup-passwords запустить не получится, так как bootstrap password изменился. Чтобы изменить пароль пользователям можно воспользоваться API.

Попробуем сделатьAPI запрос к любому узлу Elasticsearch с использованием учетной записи elastic и пароля к от неё:

curl -u 'elastic' -X GET "http://10.0.3.1:9200/_cluster/health?pretty"Enter host password for user 'elastic':{  "cluster_name" : "es_cluster",  "status" : "green",  "timed_out" : false,  "number_of_nodes" : 4,  "number_of_data_nodes" : 3,  "active_primary_shards" : 9,  "active_shards" : 18,  "relocating_shards" : 0,  "initializing_shards" : 0,  "unassigned_shards" : 0,  "delayed_unassigned_shards" : 0,  "number_of_pending_tasks" : 0,  "number_of_in_flight_fetch" : 0,  "task_max_waiting_in_queue_millis" : 0,  "active_shards_percent_as_number" : 100.0}

Как видно из примера все работает, но мы обращались через http, значит трафик между клиентом и кластером Elasticsearch не шифрованный.

Настраиваем шифрование клиентского трафика Elasticsearch

Для шифрования клиентского трафика необходимо выпустить сертификат, который будет использоваться для шифрования трафика между узлом Elasticsearch и клиентом. При этом для Kibana будет сформирован отдельный сертификат, чтобы настроить соединение с Elasticsearch. Для этого делаем следующее:

  • Генерируем сертификат:

./bin/elasticsearch-certutil http

В процессе генерации необходимо определить:

1) Необходимо ли сгенерировать Certificate Signing Request (CSR). Потребуется, если сертификат будет выпускаться сторонним CA (Certificate Authority).

2) Использовать ли собственный CA (Certificate Authority). Если да, то указываем путь до ключа, которым будет подписаны будущие сертификаты.

3) Срок действия сертификата. По умолчанию 5 лет. Можно определить дни(D), месяцы (M), года (Y).

4) Как выпустить сертификат, на каждый узел или общий. Если все узлы имеют общий домен, то можно выпустить wildcard сертификат (например *.domain.local). Если общего домена нет и в будущем возможно добавление узлов, то необходимо генерировать на каждый узел, указав имя узла и его адрес. В будущем можно выпустить отдельный сертификат для нового узла.

На выходе получаем архив сертификатами, в моём случае со всеми сертификатам для каждого узла Elasticsearch, а так же сертификат для подключения Kibana к Elasticsearch:

. elasticsearch    es-nlb01       README.txt       http.p12       sample-elasticsearch.yml    es-node01       README.txt       http.p12       sample-elasticsearch.yml    es-node02       README.txt       http.p12       sample-elasticsearch.yml    es-node03        README.txt        http.p12        sample-elasticsearch.yml kibana     README.txt     elasticsearch-ca.pem     sample-kibana.yml

В архиве также лежит инструкция по дальнейшим действиям с сертификатом (README.txt) и пример конфигурационного файла (sample-...yml).

Сертификат elasticsearch-ca.pem из директории kibana потребуется в следующем шаге.

  • Размещаем сертификаты на узлах и добавляем необходимые настройки в файле конфигурации:

    xpack.security.http.ssl.enabled: truexpack.security.http.ssl.keystore.path: "http.p12"
    
  • Добавляем пароль от сгенерированного сертификата в keystore:

./bin/elasticsearch-keystore add xpack.security.http.ssl.keystore.secure_password
  • Проверяем, что все работает по https:

curl -u 'elastic' -k -X GET "https://10.0.3.1:9200/_cluster/health?pretty"Enter host password for user 'elastic':{  "cluster_name" : "es_cluster",  "status" : "green",  "timed_out" : false,  "number_of_nodes" : 4,  "number_of_data_nodes" : 3,  "active_primary_shards" : 9,  "active_shards" : 18,  "relocating_shards" : 0,  "initializing_shards" : 0,  "unassigned_shards" : 0,  "delayed_unassigned_shards" : 0,  "number_of_pending_tasks" : 0,  "number_of_in_flight_fetch" : 0,  "task_max_waiting_in_queue_millis" : 0,  "active_shards_percent_as_number" : 100.0}

Подключаем Kibana к Elasticsearch

Если сейчас обратиться к Kibana, то ответом будет "Kibana server is not ready yet".

Это связанно с тем, что сейчас Kibana не может получить доступ к Elasticsearch по http, для этого необходим сертификат Kibana, который был получен в предыдущем шаге, а также требуется пройти аутентификацию. Чтобы настроить работу Kibana:

  • В конфигурационном файле Kibana указываем ключ к сертификату elasticsearch-ca.pem(из предыдущего шага) и меняем протокол http на https:

elasticsearch.hosts: ['https://10.0.3.1:9200']elasticsearch.ssl.certificateAuthorities: [ "/etc/kibana/elasticsearch-ca.pem" ]

В примере выше я использую адрес Coordinating only узла для балансировки нагрузки между Kibana и Elasticsearch, который был настроен в предыдущей части.

  • Создаем keystore для хранения пользователя и пароля:

sudo ./bin/kibana-keystore create --allow-root

Команда выполняются из директории /usr/share/kibana

Так как keystore будет создан в каталоге /etc/kibana/ , и keystone требуется файл конфигурации /etc/kibana/kibana.yml, то пользователю, запускающему команду, необходим доступ к этому каталогу и файлу. Я использую sudo и ключ --allow-root. Ключ необходим, так как запускать из под пользователя root нельзя.

Kibana keystore не имеет пароля. Для ограничения доступа используем стандартные средства Linux.

  • Добавляем пользователя kibana_system(встроенный пользователь Elasticsearch) и пароль учетной записи в Kibana keystore:

sudo ./bin/kibana-keystore add elasticsearch.username --allow-rootsudo ./bin/kibana-keystore add elasticsearch.password --allow-root 

Можно использовать так же пользователя kibana, однако, он в системе считается устаревшим (deprecated).

  • перезапускаем Kibana и проверяем открыв нужный адрес в браузере:

Kibana Kibana

Для входа используем учетную запись elastic.

Как можно заменить, трафик между браузером и Kibana не шифруется.

Настраиваем шифрование трафика между Kibana и клиентами

Данная процедура максимально похожа на ту, которую выполняли для получения сертификатов для узлов Elasticsearch.

  • Получаем сертификат при помощи elsticsearch-certutil :

./bin/elasticsearch-certutil cert  -ca /etc/elasticsearch/elastic-stack-ca.p12 -name kibana-certificate -dns kibana01,10.0.3.1,127.0.0.1,localhost -ip 10.0.3.1

При необходимости можем использовать CA (Certificate Authority). В процессе генерации сертификата указываем пароль от CA, если используется, имя будущего сертификата и пароль к нему.

Через -dns и -ip указываем DNS имена и адрес Kibana.

  • Указываем путь к сертификату в файле kibana.yml:

Так как я использовал ранее полученный CA, то указываю путь и к нему (server.ssl.truststore.path).

server.ssl.keystore.path: "/etc/kibana/kibana-certificate.p12"server.ssl.truststore.path: "/etc/elasticsearch/elastic-stack-ca.p12"

Не забывайте о правах доступа, пользователь kibana должен иметь права на чтения.

  • Включаем использование TLS:

server.ssl.enabled: true
  • Добавляем пароли от сертификатов в keystore:

sudo ./bin/kibana-keystore add server.ssl.keystore.password --allow-rootsudo ./bin/kibana-keystore add server.ssl.truststore.password --allow-root
  • Перезагружаем Kibana и проверяем:

Доступ к Kibana по httpsДоступ к Kibana по https

Теперь для подключения к Kibana используем https.

Создаем пользователей и роли

Ранее мы активировали встроенные учетные записи и сгенерировали для них пароли, но использовать пользователя elastic (superuser) не лучшая практика, поэтому рассмотрим, как создавать пользователей и роли к ним.

Для примера создадим администратора Kibana. Открываем Menu > Management > Stack Management, выбираем Users и нажимаем Create user. Заполняем все поля и жмем Create User.

Создание пользователя в KibanaСоздание пользователя в Kibana

Или же можно воспользоваться API. Делать запросы к кластеру можно через консоль Dev Tools инструмента Kibana. Для этого перейдите Menu > Management > Dev Tools. В открывшейся консоли можно писать запросы к Elasticsearch.

POST /_security/user/kibana_admin{  "password" : "password",  "roles" : [ "kibana_admin" ],  "full_name" : "Kibana Administrator",  "email" : "kibana.administrator@example.local"}
Создание пользователя kibana_admin через APIСоздание пользователя kibana_admin через API

После создания пользователя, можно его использовать.

Роль kibana_admin не может создавать пользователей и роли. Для создания пользователей и роли необходима привилегия кластера manage_security. С перечнем всех встроенных ролей можно ознакомится тут.

Далее создадим роль для работы с данными в ранее созданном индексом logstash-logs*. Открываем Menu > Management > Stack Management. Слева выбираем Roles и нажимаем Create role. Настраиваем привилегии, указав в качестве индекса logstash-logs*:

Index privileges

read

Read only права на индекс

Предоставляем пользователю доступ к Kibana, для этого ниже наживаем Add Kibana privilege и выбираем требуемые привилегии:

В поле Spaces указываю All spaces. Что такое Space (пространства) можно почитать на официальном сайте. В рамках данной серии статей Space будет описан в статье о Kibana dashboard.

Чтобы создать роль с привилегиями в Elasticsearch и Kibana через API, делаем запрос к Kibana:

curl -k -i -u 'elastic' -X PUT 'https://10.0.3.1:5601/api/security/role/logstash_reader' \--header 'kbn-xsrf: true' \--header 'Content-Type: application/json' \--data-raw '{  "elasticsearch": {    "cluster" : [ ],    "indices" : [      {        "names": [ "logstash-logs*" ],        "privileges": ["read"]      }      ]  },  "kibana": [    {      "base": [],      "feature": {       "discover": [          "all"        ],        "visualize": [          "all"        ],        "dashboard": [          "all"        ],        "dev_tools": [          "read"        ],        "indexPatterns": [          "read"        ]      },      "spaces": [        "*"      ]    }  ]}'

Создаем пользователя logstash_reader и связываем его с созданной ролью (это мы уже научились делать) и заходим данным пользователем в Kibana.

Главная страница Kibana для пользователя logstash_readerГлавная страница Kibana для пользователя logstash_reader

Как видно, у данного пользователя не так много прав. Он может просматривать индексы logstash-logs*, строить графики, создавать панели и делать GET запросы к этому индексам через Dev Tools.

Настраиваем пользовательские сессии Kibana

  • Закрываем неактивные сессии:

xpack.security.session.idleTimeout: "30m"
  • Устанавливаем максимальную продолжительность одной сессии:

xpack.security.session.lifespan: "1d"
  • Настраиваем интервал принудительной очистки данных о неактивных или просроченных сессиях из сессионного индекса:

xpack.security.session.cleanupInterval: "8h"

Для всех параметров формат времени может быть следующим: ms | s | m | h | d | w | M | Y

Данные о сессии удаляются после того, как пользователь осуществляет закрытие сессии. Если не настроить закрытие неактивных сессий или не ограничить максимальную длительность сессии, то информация об этих сессиях будет накапливаться в сессионном индексе, и Kibana не сможет автоматически удалить данные.

Настраиваем Logstash

На данный момент Logstash не отправляет данные в кластер Elasticsearch, и чтобы это исправить, сделаем несколько настроек.

  • Создаем роль для подключения к кластеру:

В Kibana открываем Menu > Management > Stack Management. Слева выбираем Roles и нажимаем Create role. Указываем имя роли и настраиваем привелегии:

Cluster privileges

manage_index_templates

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

monitor

Read-only права на получение информации о кластере

Index privileges

create_index

Создание индексов

write

Индексирование, обновление и удаление индексов

manage

Мониторинг и управление индексом

manage_ilm

Управление жизненным циклом индексов (IML)

  • Создаем пользователя для подключения к кластер:

Открываем Menu > Management > Stack Management, выбираем Users и нажимаем Create user. Заполняем требуемые данные, указав в качестве роли созданную выше роль.

Создание пользователя logstash_userСоздание пользователя logstash_user
  • Создаем Logstash keystore, и добавляем туда пользователя и пароль от него:

# Создаем keystore с паролемset +o historyexport LOGSTASH_KEYSTORE_PASS=mypasswordset -o historysudo /usr/share/logstash/bin/logstash-keystore create --path.settings /etc/logstash/# Добавляем данныеsudo /usr/share/logstash/bin/logstash-keystore add ES_USER --path.settings /etc/logstash/sudo /usr/share/logstash/bin/logstash-keystore add ES_PWD --path.settings /etc/logstash/
  • Настраиваем аутентификацию в output плагине:

output {  elasticsearch {    ...    user => "${ES_USER}"    password => "${ES_PWD}"  }}
  • Настраиваем TLS в Logstash:

Для подключения по https к Elasticsearch необходимо настроить использование ssl и установить .pem сертификат, который был получен для Kibana.

output {  elasticsearch {    ...    ssl => true    cacert => '/etc/logstash/elasticsearch-ca.pem'  }}
  • Перезагружаем Logstash и проверяем новые записи в индексе

Проверку можно сделать в Kibana через Discovery, как делали в прошлой статье, или через API:

Dev Tools - Console в KibanaDev Tools - Console в Kibana

Создание API ключей

Для работы с Elasticsearch можно не только использовать учетные записи пользователей, но и генерировать API ключ. Для этого необходимо сделать POST запрос:

POST /_security/api_key

В качестве параметров указывают:

name - имя ключа

role_descriptors - описание роли. Структура совпадает с запросом на создание роли.

expiration - Срок действия ключа. По умолчанию без срока действия.

Для примера заменим базовую аутентификацию в Logstash на API ключ.

  • Создаем API ключ в Kibana Dev Tool:

POST /_security/api_key{  "name": "host_logstash01",   "role_descriptors": {    "logstash_api_writer": {       "cluster": ["manage_index_templates", "monitor"],      "index": [        {          "names": ["logstash-logs*"],          "privileges": ["create_index", "write", "manage", "manage_ilm"]        }      ]    }  },  "expiration": "365d"}

Получаем следующий результат:

{  "id" : "979DkXcBv3stdorzoqsf",  "name" : "host_logstash01",  "expiration" : 1644585864715,  "api_key" : "EmmnKb6NTES3nlRJenFKrQ"}
  • Помещаем ключ в формате id:api_key в Keystore:

sudo /usr/share/logstash/bin/logstash-keystore add API_KEY --path.settings /etc/logstash/
  • Указываем API ключ в конфигурационном файле конвейера:

output {  elasticsearch {    hosts => ["https://10.0.3.11:9200","https://10.0.3.12:9200","https://10.0.3.13:9200"]    index => "logstash-logs-%{+YYYY.MM}"    ssl => true    api_key => "${API_KEY}"    cacert => '/etc/logstash/elasticsearch-ca.pem'  }}

Информацию по ключам можно получить в Kibana Menu > Management > API Keys или через API запрос:

GET /_security/api_key?name=host_logstash01{  "api_keys" : [    {      "id" : "9r8zkXcBv3stdorzZquD",      "name" : "host_logstash01",      "creation" : 1613048800876,      "expiration" : 1613049520876,      "invalidated" : false,      "username" : "elastic",      "realm" : "reserved"    }  ]}

Заключение

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

В следующей статье будет рассмотрена процедура создание кластера ELK с помощью Docker.

Полезные ссылки

Подробнее..

Elasticsearch сайзинг шардов как завещал Elastic анонс вебинара предложения по митапу

15.03.2021 20:13:27 | Автор: admin

Каждый индекс Elasticsearch состоит из шардов. Шарды это логическое и физическое разделение индекса. В этой статье мы расскажем о сайзинге шардов Elasticsearch важной его части, серьёзно влияющей на производительность кластера. В высоконагруженных системах выбор правильной конфигурации архитектуры хранения позволит серьёзно сэкономить на железе. Бльшая часть статьи основана на соответствующем разделе документации Elastic. Подробности под катом.

Сайзинг шардов Elasicsearch


Как Elasticsearch работает с шардами


Поисковые запросы обычно попадают в несколько шардов (в продукционных нагруженных средах рекомендуем использовать ноду с ролью coordinating). Каждый шард выполняет поисковый запрос в одном процессорном треде. Если запросов много, пул тредов поиска заканчивается (именно поиска, т.к. есть и другие), что приводит к возникновению очередей, снижению производительности и, как следствие, медленной скорости поиска.

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

Давайте теперь краем глаза взглянем на сегменты (см. картинку ниже). Каждый шард Elasticsearch является индексом Lucene. Максимальное количество документов, которое можно закинуть в индекс Lucene 2 147 483 519. Индекс Lucene разделен на блоки данных меньшего размера, называемые сегментами. Сегмент это небольшой индекс Lucene. Lucene выполняет поиск во всех сегментах последовательно. Большинство шардов содержат несколько сегментов, в которых хранятся данные индекса. Elasticsearch хранит метаданные сегментов в JVM Heap, чтобы их можно было быстро извлечь для поиска. По мере роста объёма шарда его сегменты объединяются в меньшее количество более крупных сегментов. Это уменьшает количество сегментов, что означает, что в динамической памяти хранится меньше метаданных (см. также forcemerge, к которому мы вернемся чуть дальше в статье).

Еще стоит сказать о ребалансировке кластера. Если добавляется новая нода или одна из нод выходит из строя, происходит ребалансировка кластера. Ребалансировка сама по себе недешёвая с точки зрения производительности операция. Кластер сбалансирован, если он имеет равное количество шардов на каждой ноде и отсутствует концентрация шардов любого индекса на любой ноде. Elasticsearch запускает автоматический процесс, называемый ребалансировкой, который перемещает шарды между узлами в кластере, чтобы его сбалансировать. При перебалансировке применяются заранее заданные правила выделения сегментов (об allocation awareness и других правилах мы подробнее расскажем в одной из следующих статей). Если вы используете data tiers, Elasticsearch автоматически разместит каждый шард на соответствующем уровне. Балансировщик работает независимо на каждом уровне.

Как заставить Elasticsearch ещё лучше работать с шардами


Правильно удалять данные. Если вы удалили документ, из файловой системы он удалится не сразу. Вместо этого, Elasticsearch помечает документ как удаленный на каждом шарде. Отмеченный документ будет продолжать использовать ресурсы, пока он не будет удален во время периодического слияния сегментов. Если нужно физически освободить место, лучше всего сразу удалять индексы целиком, которые в итоге освободят файловую системы.

Создавать шарды размером от 10 до 50 ГБ. Elastic говорит, шарды размером более 50 ГБ потенциально могут снизить вероятность восстановления кластера после сбоя. Из-за той самой ребалансировки, о которой мы говорили в начале статьи. Ну, и большие шарды накладнее передавать по сети. Предел в 50 ГБ выглядит, конечно, как сферический конь в вакууме, поэтому мы сами больше склоняемся к 10 ГБ. Вот тут человек советует 10 ГБ и смотреть на размер документов в следующем плане:

  • От 0 до 4 миллионов документов на индекс: 1 шард.
  • От 4 до 5 миллионов документов на индекс: 2 шарда.
  • Более 5 миллионов документов считать по формуле: (количество документов / 5 миллионов) + 1 шард.

20 или менее шардов на 1 ГБ JVM Heap. Количество шардов, которыми может жонглировать нода, пропорциональны объему JVM Heap ноды. Например, нода с 30 ГБ JVM Heap должна иметь не более 600 шардов. Чем меньше, тем, скорее всего, лучше. Если это пропорция не выполняется можно добавить ноду. Посмотрим сколько там используется JVM Heap на каждой ноде:



А теперь посмотрим сколько шардов на каждой ноде и видим, что с нашим тестовым стендов всё в порядке. Жить будет.



Количество шардов на узле можно ограничить при помощи опции index.routing.allocation.total_shards_per_node, но если их уже много, присмотритесь к Shrink API.

Совсем необязательно создавать индексы размером в 1 день. Часто встречали у заказчиков подход, при котором каждый новый день создавался новый индекс. Иногда это оправдано, иногда можно и месяц подождать. Ролловер ведь можно запускать не только с max_age, но и с max_size или max_docs. На Хабре была статья, в которой Адель Сачков, в ту пору из Яндекс Денег (сейчас уже нет), делился полезным лайфхаком: создавал индексы не в момент наступления новых суток, а заранее, чтобы этот процесс не аффектил на производительность кластера, но у него там были микросервисы.
каждые сутки создаются новые индексы по числу микросервисов поэтому раньше каждую ночь эластик впадал в клинч примерно на 8 минут, пока создавалась сотня новых индексов, несколько сотен новых шардов, график нагрузки на диски уходил в полку, вырастали очереди на отправку логов в эластик на хостах, и Zabbix расцветал алертами как новогодняя ёлка. Чтобы этого избежать, по здравому размышлению был написан скрипт на Python для предварительного создания индексов.

С новогодней ёлкой неплохой каламбурчик получился.

Не пренебрегайте ILM и forcemerge. Индексы должны плавно перетекать между соответствующими нодами согласно ILM. В OpenDistro есть аналогичный механизм.



С индексами, в которые уже не ведется запись можно выполнить forcemerge слияние меньших по размеру сегментов в более крупные. Это в итоге снизит накладные расходы на эксплуатацию шардов и повысит скорость поиска. Forcemerge требует значительных ресурсов, поэтому лучше это делать к какие-то в непиковые часы. Добавим, что forcemerge это фактические создание нового сегмента из двух старых, поэтому свободное место на диске лишним точно не будет.

Приходите в комментарии и расскажите о своём опыте с раскладыванием шардов по нодам. Было бы интересно узнать о том, что работает в вашем случае.

Анонс вебинара. Elastic приглашает посетить 17 марта в 12 часов по московскому времени вебинар Elastic Telco Day: Applications and operational highlights from telco environments. Эксперты расскажут о применении в решений Elastic в телекоме. Регистрация.

Предложения по митапу. Планируем проведение онлайн-митап по Elastic в апреле. Напишите в комментариях или в личку какие темы вам было бы интересно разобрать, каких спикеров услышать. Если бы вы хотели сами выступить и у вас есть что рассказать, тоже напишите. Вступайте в группу Elastic Moscow User Group, чтобы не пропустить анонс митапа.

Канал в телеге. Подписывайтесь на наш канал Elastic Stack Recipes, там интересные материалы и анонсы мероприятий.

Читайте наши другие статьи:




Если вас интересуют услуги внедрения, администрирования и поддержки Elastic Stack, вы можете оставить заявку в форме обратной связи на специальной странице.
Подробнее..

Удобное логирование на бэкенде. Доклад Яндекса

28.11.2020 12:19:52 | Автор: admin
Что-то всегда идет не по плану. Приходится отвечать на вопросы, Что сломалось?, Почему тормозит? и Почему мы не увидели этого раньше?. На примере простого приложения Даниил Галиев zefirior из Яндекс.Путешествий показал, как отвечать на эти вопросы и какие инструменты в этом помогут. Настроим логирование, прикрутим трассировку, разложим ошибки, и все это в удобном интерфейсе.

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



Что будем делать? Мы построим небольшое приложение, наш стартап. Потом внедрим в него базовое логирование, это маленькая часть доклада, то, что поставляет Python из коробки. И дальше самая большая часть мы разберем типичные проблемы, которые встречаются нам во время отладки, выкатки, и инструменты для их решения.

Небольшой дисклеймер: я буду говорить такие слова, как ручка и локаль. Поясню. Ручка возможно, яндексовый сленг, это обозначает ваши API, http или gRPC API или любые другие комбинации букв перед APU. Локаль это когда я разрабатываю на ноутбуке. Вроде бы я рассказал обо всех словах, которые я не контролирую.

Приложение Книжная лавка


Начнем. Наш стартап это Книжная лавка. Главной фичей этого приложения будет продажа книг, это все, что мы хотим сделать. Дальше немного начинки. Приложение будет написано на Flask. Все сниппеты кода, все инструменты общие и абстрагированы от Python, поэтому их можно будет интегрировать в большинство ваших приложений. Но в нашем докладе это будет Flask.

Действующие лица: я, разработчик, менеджеры и мой любимый коллега Эраст. Любые совпадения случайны.



Давайте немного про структуру. Это приложение с микросервисной архитектурой. Первый сервис Books, хранилище книг с метаданными о книгах. Он использует базу данных PostgreSQL. Второй микросервис микросервис доставки, хранит метаданные о заказах пользователей. Cabinet это бэкенд для кабинета. У нас нет фронтенда, в нашем докладе он не нужен. Cabinet агрегирует запросы, данные из сервиса книг и сервиса доставки.



По-быстрому покажу код ручек этих сервисов, API Books. Это ручка выгребает данные из базы, сериализует их, превращает в JSON и отдает.



Пойдем дальше. Сервис доставки. Ручка точно такая же. Выгребаем данные из базы, сериализуем их и отправляем.



И последняя ручка ручка кабинета. В ней немного другой код. Ручка кабинета запрашивает данные из сервиса доставки и из сервиса книг, агрегирует ответы и отдает пользователю его заказы. Всё. Со структурой приложения мы по-быстрому разобрались.

Базовое логирование в приложении


Теперь давайте про базовое логирование, то, которое мы впилили. Начнем с терминологии.



Что нам дает Python? Четыре базовые, главные сущности:

Logger, входная точка логирования в вашем коде. Вы будете пользоваться каким-то Logger, писать logging.INFO, и все. Ваш код больше ничего не будет знать о том, куда сообщение улетело и что с ним дальше произошло. За это уже отвечает сущность Handler.

Handler обрабатывает ваше сообщение, решает, куда его отправить: в стандартный вывод, в файл или кому-нибудь на почту.

Filter одна из двух вспомогательных сущностей. Удаляет сообщения из лога. Другой стандартный вариант использования это наполнение данными. Например, в вашей записи нужно добавить атрибут. Для этого тоже может помочь Filter.

Formatter приводит ваше сообщение к нужному виду.



На этом с терминологией закончили, дальше к логированию прямо на Python, с базовыми классами, мы возвращаться не будем. Но вот пример конфигурации нашего приложения, которая раскатана на всех трех сервисах. Здесь два главных, важных для нас блока: formatters и handlers. Для formatters, есть пример, который вы можете увидеть здесь, шаблон того, как будет выводиться сообщение.

В handlers вы можете увидеть, что logging.StreamHandler использован. То есть мы выгружаем все наши логи в стандартный вывод. Всё, с этим закончили.

Проблема 1. Логи разбросаны


Переходим к проблемам. Для начала проблема первая: логи разбросаны.

Немножечко контекста. Наше приложение мы написали, конфетка уже готова. Мы можем на нем зарабатывать денежку. Мы его выкатываем в продакшен. Конечно, там не один сервер. По нашим скромным подсчетам, нашему сложнейшему приложению нужно порядка трех-четырех машинок, и так для каждого сервиса.

Теперь вопрос. Прибегает к нам менеджер и спрашивает: Там сломалось, помоги! Вы бежите. У вас же все логируется, это великолепно. Вы заходите на первую машинку, смотрите там ничего нет по вашему запросу. Заходите на вторую машинку ничего. И так далее. Это плохо, это надо как-то решать.



Давайте формализуем результат, который мы хотим увидеть. Я хочу, чтобы логи были в одном месте. Это простое требование. Немного круче то, что я хочу поиск по логам. То есть да, оно лежит в одном месте и я умею грепать, но было бы прикольно, если бы были какие-нибудь tools, крутые фичи помимо простого грепа.

И я не хочу писать. Это Эраст любит писать код. Я не про это, я сразу сделал продукт. То есть хочется меньше дополнительного кода, обойтись одним-двумя файликами, строчками, и всё.



Решение, которое можно использовать, ElasticSearch. Давайте его попробуем поднять. Какие плюсы ElasticSearch нам даст? Это интерфейс с поиском логов. Сразу из коробки есть интерфейс, это вам не консолька, а единственное место хранения. То есть главное требование мы отработали. Нам не нужно будет ходить по серверам.

В нашем случае это будет довольно простая интеграция, и с недавним релизом у ElasticSearch выпустился новый агент, который отвечает за большинство интеграций. Они там сами впилили интеграции. Очень круто. Я составлял доклад чуть раньше и использовал filebeat, так же просто. Для логов все просто.

Немного об ElasticSearch. Не хочу рекламировать, но там много дополнительных фич. Например, крутая штука полнотекстовый поиск по логам из коробки. Очень круто звучит. Нам пока достаточно этих плюсов. Прикручиваем.



В первую очередь нам нужно будет развернуть агент, который будет отправлять наши логи в Elastic. Вы регистрируете аккаунт в Elastic и дальше добавляете в ваш docker-compose. Если у вас не docker-compose, можете поднимать ручками или в вашей системе. В нашем случае добавляется вот такой блок кода, интеграции в docker-compose. Всё, сервис настроен. И вы можете увидеть в блоке volumes файл конфигурации filebeat.yml.



Вот пример filebeat.yml. Здесь настроен автоматический поиск логов контейнеров docker, которые крутятся рядом. Кастомизован выбор этих логов. По условию можно задавать, вешать на ваши контейнеры лейблы, и в зависимости от этого ваши логи будут отправляться только у определенных контейнеров. С блоком processors:add_docker_metadata все просто. В логи добавляем немножечко больше информации о ваших логах в контексте docker. Необязательно, но прикольно.



Что мы получили? Это все, что мы написали, весь код, очень круто. При этом мы получили все логи в одном месте и есть интерфейс. Мы можем поискать наши логи, вот плашечка search. Они доставляются. И можно даже в прямом эфире включить так, чтобы стрим летел к нам в логи в интерфейс, и мы это видели.



Тут я бы и сам спросил: а чего, как грепать-то? Что из себя представляет поиск по логам, что там можно сделать?

Да, из коробки в таком подходе, когда у нас текстовые логи, есть небольшой затык: мы можем по тексту задать запрос, например message:users. Это выведет нам все логи, у которых есть подстрока users. Можно пользоваться звездочками, большинством других юниксовых wild cards. Но кажется, этого недостаточно, хочется сделать сложнее, чтобы можно было нагрепать в Nginx раньше, как мы это умеем.



Давайте немного отступим от ElasticSearch и попытаемся сделать это не с ElasticSearch, а с другим подходом. Рассмотрим структурные логи. Это когда каждая ваша запись лога это не просто текстовая строчка, а сериализованный объект с атрибутами, которые любая ваша сторонняя система может сериализовать, чтобы получить уже готовый объект.

Какие от этого есть плюсы? Это единый формат данных. Да, у объектов могут быть разные атрибуты, но любая внешняя система может прочитать именно JSON и получить какой-то объект.

Какая-никакая типизация. Это упрощает интеграцию с другими системами: не нужно писать десериализаторы. И как раз десериализаторы это другой пункт. Вам не нужно писать в приложении прозаичные тексты. Пример: User пришел с таким-то айдишником, с таким-то заказом. И это все нужно писать каждый раз.

Меня это напрягало. Я хочу написать: Прилетел запрос. Дальше: Такой-то, такой-то, такой-то, очень просто, очень по-айтишному.



Давайте дальше. Договоримся: логировать будем в формате JSON, это простой формат. Сразу ElasticSearch поддерживается, filebeat, которым мы сериализуем и попробуем впилить. Это не очень сложно. Для начала вы добавляете из библиотеки pythonjsonlogger в блок formatters JSONFormatter файлика settings, где у нас хранится конфигурация. У вас в системе это может быть другое место. И дальше в атрибуте format вы передаете, какие атрибуты вы хотите добавлять в ваш объект.

Блок ниже это блок конфигурации, который добавляется в filebeat.yml. Здесь из коробки есть интерфейс у filebeat для парсинга JSON-логов. Очень круто. Это все. Для этогов вам больше ничего писать не придется. И теперь ваши логи похожи на объекты.



Что мы получили в ElasticSearch? В интерфейсе вы сразу видите, что ваш лог превратился в объект с отдельными атрибутами, по которым вы можете искать, создавать фильтрации и делать сложные запросы.



Давайте подведем итог. Теперь наши логи имеют структуру. По ним несложно грепать и можно писать интеллектуальные запросы. ElasticSearch знает об этой структуре, так как он распарсил все эти атрибуты. А в kibana это интерфейс для ElasticSearch можно фильтровать такие логи с помощью специализированного языка запросов, который предоставляет Elastic Stack.

И это проще, чем грепать. Греп имеет довольно сложный и крутой язык. Там очень много можно написать. В kibana можно сделать многие вещи проще. С этим разобрались.

Проблема 2. Тормоза


Следующая проблема тормоза. В микросервисной архитектуре всегда и везде бывают тормоза.



Тут немного контекста, расскажу вам историю. Ко мне прибегает менеджер, главное действующее лицо нашего проекта, и говорит: Эй-эй, кабинет тормозит! Даня, спаси, помоги!

Мы пока ничего не знаем, лезем в Elastic в наши логи. Но давайте я расскажу, что, собственно, произошло.



Эраст добавил фичу. В книгах мы теперь отображаем не айдишник автора, а его имя прямо в интерфейсе. Очень круто. Сделал он это вот таким кодом. Небольшой кусок кода, ничего сложного. Что может пойти не так?

Вы наметанным взглядом можете сказать, что с SQLAlchemy так делать нельзя, с другой ORM тоже. Нужно сделать прекэш или что-нибудь еще, чтобы в цикле не ходить в базу с маленьким подзапросиком. Неприятная проблема. Кажется, что такую ошибку вообще нельзя допустить.

Давайте расскажу. У меня был опыт: мы работали с Django, и у нас в проекте был реализован кастомный прекэш. Много лет все шло хорошо. В какой-то момент мы с Эрастом решили: давай пойдем в ногу со временем, обновим Django. Естественно, Django ничего не знает о нашем кастомном прекэше, и интерфейс поменялся. Прикэш отвалился, молча. На тестировании это не отловили. Та же самая проблема, просто ее сложнее было отловить.

Проблема в чем? Как я помогу вам решить проблему?



Давайте расскажу, что я делал до того, как начал решать именно проблему поиска тормозов.

Первое, что делаю, иду в ElasticSearch, у нас он уже есть, помогает, не нужно бегать по серверам. Я захожу в логи, ищу логи кабинета. Нахожу долгие запросы. Воспроизвожу на ноутбуке и вижу, что тормозит не кабинет. Тормозит Books.

Бегу в логи Books, нахожу проблемные запросы собственно, он у нас уже есть. Точно так же воспроизвожу Books на ноутбуке. Очень сложный код ничего не понимаю. Начинаю дебажить. Тайминги довольно сложно отлавливать. Почему? Внутри SQLAlchemy довольно сложно это определить. Пишу кастомные тайм-логгеры, локализую и исправляю проблему.



Мне было больно. Сложно, неприятно. Я плакал. Хочется, чтобы этот процесс поиска проблемы был быстрее и удобнее.

Формализуем наши проблемы. Сложно по логам искать, что тормозит, потому что наш лог это лог несвязанных событий. Приходится писать кастомные таймеры, которые показывают нам, сколько выполнялись блоки кода. Причем непонятно, как логировать тайминги внешних систем: например, ORM или библиотек requests. Надо наши таймеры внедрять внутрь либо каким-то Wrapper, но мы не узнаем, от чего оно тормозит внутри. Сложно.



Хорошее решение, которое я нашел, Jaeger. Это имплементация протокола opentracing, то есть давайте внедрим трассировку.

Что дает Jaeger? Это удобный интерфейс с поиском запросов. Вы можете отфильтровать долгие запросы или сделать это по тегам. Наглядное представление потока запросов, очень красивая картинка, чуть позже покажу.

Тайминги логируются из коробки. С ними ничего не надо делать. Если вам нужно про какой-то кастомный блок проверить, сколько он выполняется, вы можете его обернуть в таймеры, предоставляемые Jaeger. Очень удобно.



Посмотрим, как вообще можно было найти проблему в интерфейсе и там же ее локализовать. Мы заходим в интерфейс Jaeger. Вот так выглядят наши запросы. Мы можем поискать запросы именно кабинета или другого сервиса. Сразу фильтруем долгие запросы. Нас интересуют именно долгие, их по логам довольно сложно найти. Получаем их список.



Проваливаемся в этот запрос, и видим большую портянку SQL-подзапросов. Мы прямо наглядно видим, как они исполнялись по времени, какой блок кода за что отвечал. Очень круто. Причем в контексте нашей проблемы это не весь лог. Там есть еще большая портянка на два-три слайда вниз. Мы довольно быстро локализовали проблему в Jaeger. В чем после решения проблемы нам может помочь контекст, который предоставляет Jaeger?



Jaeger логирует, например, SQL-запросы: вы можете посмотреть, какие запросы повторяются. Очень быстро и круто.



Проблему мы решили и сразу видим в Jaeger, что все хорошо. Мы проверяем по тому же запросу, что у нас теперь нет подзапросов. Почему? Предположим, мы проверим тот же запрос, узнаем тайминг посмотрим в Elastic, сколько запрос выполнялся. Тогда мы увидим время. Но это не гарантирует, что подзапросов не было. А здесь мы это видим, круто.



Давайте внедрим Jaeger. Кода нужно не очень много. Вы добавляете зависимости для opentracing, для Flask. Теперь о том, какой код мы делаем.

Первый блок кода это настройка клиента Jaeger.

Затем мы настраиваем интеграцию с Flask, Django или с любым другим фреймворком, на который есть интеграция.

install_all_patches самая последняя строчка кода и самая интересная. Мы патчим большинство внешних интеграция, взаимодействуя с MySQL, Postgres, библиотекой requests. Мы все это патчим и именно поэтому в интерфейсе Jaeger сразу видим все запросы с SQL и то, в какой из сервисов ходил наш искомый сервис. Очень круто. И вам не пришлось много писать. Мы просто написали install_all_patches. Магия!

Что мы получили? Теперь не нужно собирать события по логам. Как я сказал, логи это разрозненные события. В Jaeger это одно большое событие, структуру которого вы видите. Jaeger позволяет отловить узкие места в приложении. Вы просто делаете поиск по долгим запросам, и можете проанализировать, что идет не так.

Проблема 3. Ошибки


Последняя проблема ошибки. Да, я лукавлю. Я не помогу вам избавиться от ошибок в приложении, но я расскажу, что с ними можно делать дальше.



Контекст. Вы можете сказать: Даня, мы логируем ошибки, у нас есть алерты на пятисотки, мы настроили. Чего ты хочешь? Логировали, логируем и будем логировать и отлаживать.

По логам вы не знаете важность ошибки. Что такое важность? Вот у вас есть одна крутая ошибка, и ошибка подключения к базе. База просто флапнула. Хочется сразу видеть, что эта ошибка не так важна, и если нет времени, не обращать на нее внимания, а фиксить более важную.

Частота появления ошибок это контекст, который может нам помочь в ее отладке. Как отслеживать появление ошибок? Преподолжим, у нас месяц назад была ошибка, и вот она снова появилась. Хочется сразу найти решение и поправить ее или сопоставить ее появление с одним из релизов.



Вот наглядный пример. Когда я впиливал интеграцию с Jaeger, то немного поменял свою API. У меня изменился формат ответа приложения. Я получил вот такую ошибку. Но в ней непонятно, почему у меня нет ключа, lots в объекте order, и нет ничего, что мне бы помогло. Мол, смотри ошибку здесь, воспроизведи и самостоятельно отлови.



Давайте внедрим sentry. Это баг-трекер, который поможет нам в решении подобных проблем и поиске контекста ошибки. Возьмем стандартную библиотеку, поддерживаемую разработчиками sentry. В четыре строчки кода мы ее добавляем в наше приложение. Всё.



Что мы получили на выходе? Вот такой дашборд с ошибками, которые можно сгруппировать по проектам и за которыми можно следить. Огромная портянка логов с ошибками группируется в одинаковые, похожие. По ним приводится статистика. И вы также можете работать с этими ошибками с помощью интерфейса.



Посмотрим на нашем примере. Проваливаемся в ошибку KeyError. Сразу видим контекст ошибки, что было в объекте order, чего там не было. Я сразу по ошибке вижу, что мне приложение Delivery отдало новую структуру данных. Кабинет просто к этому не готов.



Что дает sentry, помимо того, что я перечислил? Формализуем.

Это хранилище ошибок, в котором можно искать их. Для этого есть удобные инструменты. Есть группировка ошибок по проектам, по похожести. Sentry дает интеграции с разными трекерами. То есть вы можете следить за вашими ошибками, работать с ними. Вы можете просто добавлять задачу в ваш контекст, и все. Это помогает в разработке.

Статистика по ошибкам. Опять же, легко сопоставить с выкаткой релиза. В этом вам поможет sentry. Похожие события, которые произошли рядом с ошибкой, тоже могут вам помочь в поиске и понимании того, что к ней привело.



Давайте подведем итог. Мы с вами написали приложение, простое, но отвечающее потребностям. Оно помогает вам в разработке и поддержке, в его жизненном цикле. Что мы сделали? Мы собрали логи в одно хранилище. Это дало нам возможность не искать их по разным местам. Плюс у нас появился поиск по логам и сторонние фичи, наши tools.

Интегрировали трассировку. Теперь мы наглядно можем следить за потоком данных в нашем приложении.

И добавили удобный инструмент для работы с ошибками. Они в нашем приложении будут, как бы мы ни старались. Но мы будем фиксить их быстрее и качественнее.

Что еще можно добавить? Само приложение готово, оно есть по ссылке, вы можете посмотреть, как оно сделано. Там поднимаются все интеграции. Например, интеграции с Elastic или трассировки. Заходите, смотрите.

Еще одна крутая штука, которую я не успел осветить, requests_id. Почти ничем не отличается от trace_id, который используется в трассировках. Но за requests_id отвечаем мы, это самая главная его фича. Менеджер может прийти к нам сразу с requests_id, нам не потребуется его искать. Мы сразу начнем решать нашу проблему. Очень круто.

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

Проверяйте. Если на вас это не сказывается, круто. Вы получили только плюсы и не решаете проблемы с тормозами. Не забывайте об этом. Всем спасибо, что слушали.
Подробнее..

Делаем поиск в веб-приложении с нуля

05.11.2020 18:21:17 | Автор: admin
В статье Делаем современное веб-приложение с нуля я рассказал в общих чертах, как выглядит архитектура современных высоконагруженных веб-приложений, и собрал для демонстрации простейшую реализацию такой архитектуры на стеке из нескольких предельно популярных и простых технологий и фреймворков. Мы построили single page application с server side rendering, поддерживающее просмотр неких карточек, набранных в Markdown, и навигацию между ними.

В этой статье я затрону чуть более сложную и интересную (как минимум мне, разработчику команды поиска) тему: полнотекстовый поиск. Мы добавим в наш контейнерный рай ноду Elasticsearch, научимся строить индекс и делать поиск по контенту, взяв в качестве тестовых данных описания пяти тысяч фильмов из TMDB 5000 Movie Dataset. Также мы научимся делать поисковые фильтры и копнём совсем немножко в сторону ранжирования.




Инфраструктура: Elasticsearch


Elasticsearch популярное хранилище документов, умеющее строить полнотекстовые индексы и, как правило, используемое именно как поисковый движок. Elasticsearch добавляет к движку Apache Lucene, на котором он основан, шардирование, репликацию, удобный JSON API и ещё миллион мелочей, которые сделали его одним из самых популярных решений для полнотекстового поиска.

Давайте добавим одну ноду Elasticsearch в наш docker-compose.yml:

services:  ...  elasticsearch:    image: "elasticsearch:7.5.1"    environment:      - discovery.type=single-node    ports:      - "9200:9200"  ...


Переменная окружения discovery.type=single-node подсказывает Elasticsearch, что надо готовиться к работе в одиночку, а не искать другие ноды и объединяться с ними в кластер (таково поведение по умолчанию).

Обратите внимание, что мы публикуем 9200 порт наружу, хотя наше приложение ходит в него внутри сети, создаваемой docker-compose. Это исключительно для отладки: так мы сможем обращаться в Elasticsearch напрямую из терминала (до тех пор, пока не придумаем более умный способ об этом ниже).

Добавить клиент Elasticsearch в наш вайринг не составит труда благо, Elastic предоставляет минималистичный Python-клиент.

Индексация


В прошлой статье мы положили наши основные сущности карточки в коллекцию MongoDB. Из коллекции мы умеем быстро извлекать их содержимое по идентификатору, потому что MongoDB построила для нас прямой индекс в ней для этого используются B-деревья.

Теперь же перед нами стоит обратная задача по содержимому (или его фрагментам) получить идентификаторы карточек. Стало быть, нам нужен обратный индекс. Для него-то нам и пригодится Elasticsearch!

Общая схема построения индекса обычно выглядит как-то так.
  1. Создаём новый пустой индекс с уникальным именем, конфигурируем его как нам нужно.
  2. Обходим все наши сущности в базе и кладём их в новый индекс.
  3. Переключаем продакшн, чтобы все запросы начали ходить в новый индекс.
  4. Удаляем старый индекс. Тут по желанию вы вполне можете захотеть хранить несколько последних индексов, чтобы, например, удобнее было отлаживать какие-то проблемы.


Давайте создадим скелет индексатора и потом разберёмся подробнее с каждым шагом.

import datetimefrom elasticsearch import Elasticsearch, NotFoundErrorfrom backend.storage.card import Card, CardDAOclass Indexer(object):    def __init__(self, elasticsearch_client: Elasticsearch, card_dao: CardDAO, cards_index_alias: str):        self.elasticsearch_client = elasticsearch_client        self.card_dao = card_dao        self.cards_index_alias = cards_index_alias    def build_new_cards_index(self) -> str:        # Построение нового индекса.        # Сначала придумываем для индекса оригинальное название.        index_name = "cards-" + datetime.datetime.now().strftime("%Y-%m-%d-%H-%M-%S")        # Создаём пустой индекс.         # Здесь мы укажем настройки и опишем схему данных.        self.create_empty_cards_index(index_name)        # Кладём в индекс все наши карточки одну за другой.        # В настоящем проекте вы очень скоро захотите         # переписать это на работу в пакетном режиме.        for card in self.card_dao.get_all():            self.put_card_into_index(card, index_name)        return index_name    def create_empty_cards_index(self, index_name):        ...     def put_card_into_index(self, card: Card, index_name: str):        ...    def switch_current_cards_index(self, new_index_name: str):        ... 


Индексация: создаём индекс


Индекс в Elasticsearch создаётся простым PUT-запросом в /имя-индекса или, в случае использования Python-клиента (нашем случае), вызовом

elasticsearch_client.indices.create(index_name, {    ...})


Тело запроса может содержать три поля.

  • Описание алиасов ("aliases": ...). Система алиасов позволяет держать знание о том, какой индекс сейчас актуальный, на стороне Elasticsearch; мы поговорим про неё ниже.
  • Настройки ("settings": ...). Когда мы будем большими дядями с настоящим продакшном, мы сможем сконфигурировать здесь репликацию, шардирование и другие радости SRE.
  • Схема данных ("mappings": ...). Здесь мы можем указать, какого типа какие поля в документах, которые мы будем индексировать, для каких из этих полей нужны обратные индексы, по каким должны быть поддержаны агрегации и так далее.


Сейчас нас интересует только схема, и у нас она очень простая:

{    "mappings": {        "properties": {            "name": {                "type": "text",                "analyzer": "english"            },            "text": {                "type": "text",                "analyzer": "english"            },            "tags": {                "type": "keyword",                "fields": {                    "text": {                        "type": "text",                        "analyzer": "english"                    }                }            }        }    }}


Мы пометили поля name и text как текстовые на английском языке. Анализатор это сущность в Elasticsearch, которая обрабатывает текст перед сохранением в индекс. В случае english анализатора текст будет разбит на токены по границам слов (подробности), после чего отдельные токены будут лемматизированы по правилам английского языка (например, слово trees упростится до tree), слишком общие леммы (вроде the) будут удалены и оставшиеся леммы будут положены в обратный индекс.

С полем tags чуть-чуть сложнее. Тип keyword предполагает, что значения этого поля некие строковые константы, которые не надо обрабатывать анализатором; обратный индекс будет построен по их сырым значениям без токенизации и лемматизации. Зато Elasticsearch создаст специальные структуры данных, чтобы по значениям этого поля можно было считать агрегации (например, чтобы одновременно с поиском можно было узнать, какие теги встречались в документах, удовлетворяющих поисковому запросу, и в каком количестве). Это очень удобно для полей, которые по сути enum; мы воспользуемся этой фичей, чтобы сделать клёвые поисковые фильтры.

Но чтобы по тексту тегов можно было искать и текстовым поиском тоже, мы добавляем к нему подполе "text", настроенное по аналогии с name и text выше по существу это означает, что Elasticsearch во всех приходящих ему документах будет создавать ещё одно виртуальное поле под названием tags.text, в которое будет копировать содержимое tags, но индексировать его по другим правилам.

Индексация: наполняем индекс


Для индексации документа достаточно сделать PUT-запрос в /имя-индекса/_create/id-документа или, при использовании Python-клиента, просто вызвать нужный метод. Наша реализация будет выглядеть так:

    def put_card_into_index(self, card: Card, index_name: str):        self.elasticsearch_client.create(index_name, card.id, {            "name": card.name,            "text": card.markdown,            "tags": card.tags,        })


Обратите внимание на поле tags. Хотя мы описали его как содержащее keyword, мы отправляем не одну строку, а список строк. Elasticsearch поддерживает такое; наш документ будет находиться по любому из значений.

Индексация: переключаем индекс


Чтобы реализовать поиск, нам надо знать имя самого свежего полностью достроенного индекса. Механизм алиасов позволяет нам держать эту информацию на стороне Elasticsearch.

Алиас это указатель на ноль или более индексов. API Elasticsearch позволяет использовать имя алиаса вместо имени индекса при поиске (POST /имя-алиаса/_search вместо POST /имя-индекса/_search); в таком случае Elasticsearch будет искать по всем индексам, на которые указывает алиас.

Мы заведём алиас под названием cards, который всегда будет указывать на актуальный индекс. Соответственно, переключение на актуальный индекс после завершения построения будет выглядеть так:

    def switch_current_cards_index(self, new_index_name: str):        try:            # Нужно удалить ссылку на старый индекс, если она есть.            remove_actions = [                {                    "remove": {                        "index": index_name,                         "alias": self.cards_index_alias,                    }                }                for index_name in self.elasticsearch_client.indices.get_alias(name=self.cards_index_alias)            ]        except NotFoundError:            # Ого, старого индекса-то и не существует вовсе.            # Наверное, мы впервые запустили индексацию.            remove_actions = []        # Одним махом удаляем ссылку на старый индекс         # и добавляем ссылку на новый.        self.elasticsearch_client.indices.update_aliases({            "actions": remove_actions + [{                "add": {                    "index": new_index_name,                     "alias": self.cards_index_alias,                }            }]        })


Я не стану подробнее останавливаться на alias API; все подробности можно посмотреть в документации.

Здесь надо сделать ремарку, что в реальном высоконагруженном сервисе такое переключение может быть довольно болезненным и может иметь смысл сделать предварительный прогрев нагрузить новый индекс каким-то пулом сохранённых пользовательских запросов.

Весь код, реализующий индексацию, можно посмотреть в этом коммите.

Индексация: добавляем контент


Для демонстрации в этой статье я использую данные из TMDB 5000 Movie Dataset. Чтобы избежать проблем с авторскими правами, я лишь привожу код утилиты, импортирующей их из CSV-файла, который предлагаю вам скачать самостоятельно с сайта Kaggle. После загрузки достаточно выполнить команду

docker-compose exec -T backend python -m tools.add_movies < ~/Downloads/tmdb-movie-metadata/tmdb_5000_movies.csv


, чтобы создать пять тысяч карточек, посвящённых кино, и команду

docker-compose exec backend python -m tools.build_index


, чтобы построить индекс. Обратите внимание, что последняя команда на самом деле не строит индекс, а только ставит задачу в очередь задач, после чего она выполнится на воркере подробнее об этом подходе я рассказывал в прошлой статье. docker-compose logs worker покажут вам, как воркер старался!

Прежде, чем мы приступим к, собственно, поиску, нам хочется своими глазами увидеть, записалось ли что-нибудь в Elasticsearch, и если да, то как оно выглядит!

Наиболее прямой и быстрый способ это сделать воспользоваться HTTP API Elasticsearch. Сперва проверим, куда указывает алиас:

$ curl -s localhost:9200/_cat/aliasescards                cards-2020-09-20-16-14-18 - - - -


Отлично, индекс существует! Посмотрим на него пристально:

$ curl -s localhost:9200/cards-2020-09-20-16-14-18 | jq{  "cards-2020-09-20-16-14-18": {    "aliases": {      "cards": {}    },    "mappings": {      ...    },    "settings": {      "index": {        "creation_date": "1600618458522",        "number_of_shards": "1",        "number_of_replicas": "1",        "uuid": "iLX7A8WZQuCkRSOd7mjgMg",        "version": {          "created": "7050199"        },        "provided_name": "cards-2020-09-20-16-14-18"      }    }  }}


Ну и, наконец, посмотрим на его содержимое:

$ curl -s localhost:9200/cards-2020-09-20-16-14-18/_search | jq{  "took": 2,  "timed_out": false,  "_shards": {    "total": 1,    "successful": 1,    "skipped": 0,    "failed": 0  },  "hits": {    "total": {      "value": 4704,      "relation": "eq"    },    "max_score": 1,    "hits": [      ...    ]  }}


Итого в нашем индексе 4704 документа, а в поле hits (которое я пропустил, потому что оно слишком большое) можно даже увидеть содержимое некоторых из них. Успех!

Более удобным способом просмотра содержимого индекса и вообще всевозможного баловства с Elasticsearch будет воспользоваться Kibana. Добавим контейнер в docker-compose.yml:

services:  ...  kibana:    image: "kibana:7.5.1"    ports:      - "5601:5601"    depends_on:      - elasticsearch  ...


После повторного docker-compose up мы сможем зайти в Kibana по адресу localhost:5601 (внимание, сервер может стартовать небыстро) и, после короткой настройки, просмотреть содержимое наших индексов в симпатичном веб-интерфейсе.



Очень советую вкладку Dev Tools при разработке вам часто нужно будет делать те или иные запросы в Elasticsearch, и в интерактивном режиме с автодополнением и автоформатированием это гораздо удобнее.

Поиск


После всех невероятно скучных приготовлений пора нам уже добавить функциональность поиска в наше веб-приложение!

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

  1. Добавляем в бэкенд компонент Searcher, отвечающий за логику поиска. Он будет формировать запрос к Elasticsearch и конвертировать результаты в более удобоваримые для нашего бэкенда.
  2. Добавляем в API эндпоинт (ручку/роут/как у вас в компании это называют?) /cards/search, осуществляющий поиск. Он будет вызывать метод компонента Searcher, обрабатывать полученные результаты и возвращать клиенту.
  3. Реализуем интерфейс поиска на фронтенде. Он будет обращаться в /cards/search, когда пользователь определился, что он хочет искать, и отображать результаты (и, возможно, какие-то дополнительные контролы).


Поиск: реализуем


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

# backend/backend/search/searcher.pyimport abcfrom dataclasses import dataclassfrom typing import Iterable, Optional@dataclassclass CardSearchResult:    total_count: int    card_ids: Iterable[str]    next_card_offset: Optional[int]class Searcher(metaclass=abc.ABCMeta):    @abc.abstractmethod    def search_cards(self, query: str = "",                      count: int = 20, offset: int = 0) -> CardSearchResult:        pass


Какие-то вещи очевидны. Например, пагинация. Мы амбициозный молодой убийца IMDB стартап, и результаты поиска никогда не будут вмещаться на одну страницу!

Какие-то менее очевидны. Например, список ID, а не карточек в качестве результата. Elasticsearch по умолчанию хранит наши документы целиком и возвращает их в результатах поиска. Это поведение можно отключить, чтобы сэкономить на размере поискового индекса, но для нас это явно преждевременная оптимизация. Так почему бы не возвращать сразу карточки? Ответ: это нарушит single-responsibility principle. Возможно, когда-нибудь мы накрутим в менеджере карточек сложную логику, переводящую карточки на другие языки в зависимости от настроек пользователя. Ровно в этот момент данные на странице карточки и данные в результатах поиска разъедутся, потому что добавить ту же самую логику в поисковый менеджер мы забудем. И так далее и тому подобное.

Реализация этого интерфейса настолько проста, что мне было лень писать этот раздел :-(

# backend/backend/search/searcher_impl.pyfrom typing import Anyfrom elasticsearch import Elasticsearchfrom backend.search.searcher import CardSearchResult, SearcherElasticsearchQuery = Any  # для аннотаций типовclass ElasticsearchSearcher(Searcher):    def __init__(self, elasticsearch_client: Elasticsearch, cards_index_name: str):        self.elasticsearch_client = elasticsearch_client        self.cards_index_name = cards_index_name    def search_cards(self, query: str = "", count: int = 20, offset: int = 0) -> CardSearchResult:        result = self.elasticsearch_client.search(index=self.cards_index_name, body={            "size": count,            "from": offset,            "query": self._make_text_query(query) if query else self._match_all_query        })        total_count = result["hits"]["total"]["value"]        return CardSearchResult(            total_count=total_count,            card_ids=[hit["_id"] for hit in result["hits"]["hits"]],            next_card_offset=offset + count if offset + count < total_count else None,        )    def _make_text_query(self, query: str) -> ElasticsearchQuery:        return {            # Multi-match query делает текстовый поиск по             # совокупности полей документов (в отличие от match            # query, которая ищет по одному полю).            "multi_match": {                "query": query,                # Число после ^  приоритет. Найти фрагмент текста                # в названии карточки лучше, чем в описании и тегах.                "fields": ["name^3", "tags.text", "text"],            }        }    _match_all_query: ElasticsearchQuery = {"match_all": {}}


По сути мы просто ходим в API Elasticsearch и аккуратно достаём ID найденных карточек из результата.

Реализация эндпоинта тоже довольно тривиальна:

# backend/backend/server.py...    def search_cards(self):        request = flask.request.json        search_result = self.wiring.searcher.search_cards(**request)        cards = self.wiring.card_dao.get_by_ids(search_result.card_ids)        return flask.jsonify({            "totalCount": search_result.total_count,            "cards": [                {                    "id": card.id,                    "slug": card.slug,                    "name": card.name,                    # Здесь не нужны все поля, иначе данных на одной                    # странице поиска будет слишком много, и она будет                    # долго грузиться.                } for card in cards            ],            "nextCardOffset": search_result.next_card_offset,        })...


Реализация фронтенда, пользующегося этим эндпоинтом, хоть и объёмна, но в целом довольно прямолинейна и в этой статье я не хочу заострять на ней внимание. На весь код можно посмотреть в этом коммите.



So far so good, идём дальше.

Поиск: добавляем фильтры


Поиск по тексту это клёво, но если вы когда-нибудь пользовались поиском на серьёзных ресурсах, вы наверняка видели всякие плюшки вроде фильтров.

У наших описаний фильмов из базы TMDB 5000 помимо названий и описаний есть теги, так что давайте для тренировки реализуем фильтры по тегам. Наша цель на скриншоте: при клике на тег в выдаче должны остаться только фильмы с этим тегом (их число указано в скобках рядом с ним).



Чтобы реализовать фильтры, нам нужно решить две проблемы.
  • Научиться по запросу понимать, какой набор фильтров доступен. Мы не хотим показывать все возможные значения фильтра на каждом экране, потому что их очень много и при этом большинство будет приводить к пустому результату; нужно понять, какие теги есть у документов, найденных по запросу, и в идеале оставить N самых популярных.
  • Научиться, собственно, применять фильтр оставить в выдаче только документы с тегами, фильтр по которым выбрал пользователь.


Второе в Elasticsearch элементарно реализуется через API запросов (см. terms query), первое через чуть менее тривиальный механизм агрегаций.

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

# backend/backend/search/searcher.pyimport abcfrom dataclasses import dataclassfrom typing import Iterable, Optional@dataclassclass TagStats:    tag: str    cards_count: int@dataclassclass CardSearchResult:    total_count: int    card_ids: Iterable[str]    next_card_offset: Optional[int]    tag_stats: Iterable[TagStats]class Searcher(metaclass=abc.ABCMeta):    @abc.abstractmethod    def search_cards(self, query: str = "",                      count: int = 20, offset: int = 0,                     tags: Optional[Iterable[str]] = None) -> CardSearchResult:        pass


Теперь перейдём к реализации. Первое, что нам нужно сделать завести агрегацию по полю tags:

--- a/backend/backend/search/searcher_impl.py+++ b/backend/backend/search/searcher_impl.py@@ -10,6 +10,8 @@ ElasticsearchQuery = Any  class ElasticsearchSearcher(Searcher): +    TAGS_AGGREGATION_NAME = "tags_aggregation"+     def __init__(self, elasticsearch_client: Elasticsearch, cards_index_name: str):         self.elasticsearch_client = elasticsearch_client         self.cards_index_name = cards_index_name@@ -18,7 +20,12 @@ class ElasticsearchSearcher(Searcher):         result = self.elasticsearch_client.search(index=self.cards_index_name, body={             "size": count,             "from": offset,             "query": self._make_text_query(query) if query else self._match_all_query,+            "aggregations": {+                self.TAGS_AGGREGATION_NAME: {+                    "terms": {"field": "tags"}+                }+            }         })


Теперь в поисковом результате от Elasticsearch будет приходить поле aggregations, из которого по ключу TAGS_AGGREGATION_NAME мы сможем достать бакеты, содержащие информацию о том, какие значения лежат в поле tags у найденных документов и как часто они встречаются. Давайте извлечём эти данные и вернём в удобоваримом виде (as designed above):

--- a/backend/backend/search/searcher_impl.py+++ b/backend/backend/search/searcher_impl.py@@ -28,10 +28,15 @@ class ElasticsearchSearcher(Searcher):         total_count = result["hits"]["total"]["value"]+        tag_stats = [+            TagStats(tag=bucket["key"], cards_count=bucket["doc_count"])+            for bucket in result["aggregations"][self.TAGS_AGGREGATION_NAME]["buckets"]+        ]         return CardSearchResult(             total_count=total_count,             card_ids=[hit["_id"] for hit in result["hits"]["hits"]],             next_card_offset=offset + count if offset + count < total_count else None,+            tag_stats=tag_stats,         )


Добавить применение фильтра самая лёгкая часть:

--- a/backend/backend/search/searcher_impl.py+++ b/backend/backend/search/searcher_impl.py@@ -16,11 +16,17 @@ class ElasticsearchSearcher(Searcher):         self.elasticsearch_client = elasticsearch_client         self.cards_index_name = cards_index_name -    def search_cards(self, query: str = "", count: int = 20, offset: int = 0) -> CardSearchResult:+    def search_cards(self, query: str = "", count: int = 20, offset: int = 0,+                     tags: Optional[Iterable[str]] = None) -> CardSearchResult:         result = self.elasticsearch_client.search(index=self.cards_index_name, body={             "size": count,             "from": offset,-            "query": self._make_text_query(query) if query else self._match_all_query,+            "query": {+                "bool": {+                    "must": self._make_text_queries(query),+                    "filter": self._make_filter_queries(tags),+                }+            },             "aggregations": {


Подзапросы, включённые в must-клаузу, обязательны к выполнению, но также будут учитываться при расчёте скоров документов и, соответственно, ранжировании; если мы когда-нибудь будем добавлять ещё какие-то условия на тексты, их лучше добавить сюда. Подзапросы в filter-клаузе только фильтруют, не влияя на скоры и ранжирование.

Осталось реализовать _make_filter_queries():

    def _make_filter_queries(self, tags: Optional[Iterable[str]] = None) -> List[ElasticsearchQuery]:        return [] if tags is None else [{            "term": {                "tags": {                    "value": tag                }            }        } for tag in tags]


На фронтенд-части опять-таки не стану останавливаться; весь код в этом коммите.

Ранжирование


Итак, наш поиск ищет карточки, фильтрует их по заданному списку тегов и выводит в каком-то порядке. Но в каком? Порядок очень важен для практичного поиска, но всё, что мы сделали за время наших разбирательств в плане порядка это намекнули Elasticsearch, что находить слова в заголовке карточки выгоднее, чем в описании или тегах, указав приоритет ^3 в multi-match query.

Несмотря на то, что по умолчанию Elasticsearch ранжирует документы довольно хитрой формулой на основе TF-IDF, для нашего воображаемого амбициозного стартапа этого вряд ли хватит. Если наши документы это товары, нам надо уметь учитывать их продажи; если это user-generated контент уметь учитывать его свежесть, и так далее. Но и просто отсортировать по числу продаж/дате добавления мы не можем, потому что тогда мы никак не учтём релевантность поисковому запросу.

Ранжирование это большое и запутанное царство технологий, которое никак не покрыть за один раздел в конце статьи. Поэтому здесь я перехожу в режим крупных мазков; я попробую рассказать в самых общих словах, как может быть устроено industrial grade ранжирование в поиске, и раскрою немного технических деталей того, как его можно реализовать с Elasticsearch.

Задача ранжирования очень сложна, так что неудивительно, что один из основных современных методов её решения машинное обучение. Приложение технологий машинного обучения к ранжированию собирательно называется learning to rank.

Типичный процесс выглядит так.

Определяемся, что мы хотим ранжировать. Мы кладём интересующие нас сущности в индекс, научаемся для заданного поискового запроса получать какой-то разумный топ (например, какой-нибудь простой сортировкой и отсечением) этих сущностей и теперь хотим научиться его ранжировать более умным способом.

Определяемся, как мы хотим ранжировать. Мы решаем, по какой характеристике надо отранжировать нашу выдачу, в соответствии с бизнес-целями нашего сервиса. Например, если наши сущности это товары, которые мы продаём, мы можем хотеть отсортировать их по убыванию вероятности покупки; если мемы по вероятности лайка или шера и так далее. Эти вероятности мы, конечно, не умеем считать в лучшем случае прикидывать, да и то только для старых сущностей, для которых у нас набрано достаточно статистики, но мы попытаемся научить модель предсказывать их, исходя из косвенных признаков.

Извлекаем признаки. Мы придумываем для наших сущностей какое-то множество признаков, которые могли бы помочь нам оценить релевантность сущностей поисковым запросам. Помимо того же TF-IDF, который уже умеет для нас вычислять Elasticsearch, типичный пример CTR (click-through rate): мы берём логи нашего сервиса за всё время, для каждой пары сущность+поисковый запрос считаем, сколько раз сущность появлялась в выдаче по этому запросу и сколько раз её кликали, делим одно на другое, et voil простейшая оценка условной вероятности клика готова. Мы также можем придумать признаки для пользователя и парные признаки пользователь-сущность, чтобы сделать ранжирование персонализированным. Придумав признаки, мы пишем код, который их вычисляет, кладёт в какое-то хранилище и умеет отдавать в real time для заданного поискового запроса, пользователя и набора сущностей.

Собираем обучающий датасет. Тут много вариантов, но все они, как правило, формируются из логов хороших (например, клик и потом покупка) и плохих (например, клик и возврат на выдачу) событий в нашем сервисе. Когда мы собрали датасет, будь то список утверждений оценка релевантности товара X запросу Q примерно равна P, список пар товар X релевантнее товара Y запросу Q или набор списков для запроса Q товары P1, P2, правильно отранжировать так-то, мы ко всем фигурирующим в нём строкам подтягиваем соответствующие признаки.

Обучаем модель. Тут вся классика ML: train/test, гиперпараметры, переобучение, перфовидеокарты и так далее. Моделей, подходящих (и повсеместно использующихся) для ранжирования, много; упомяну как минимум XGBoost и CatBoost.

Встраиваем модель. Нам остаётся так или иначе прикрутить вычисление модели на лету для всего топа, чтобы до пользователя долетали уже отранжированные результаты. Тут много вариантов; в иллюстративных целях я (опять-таки) остановлюсь на простом Elasticsearch-плагине Learning to Rank.

Ранжирование: плагин Elasticsearch Learning to Rank


Elasticsearch Learning to Rank это плагин, добавляющий в Elasticsearch возможность вычислить ML-модель на выдаче и тут же отранжировать результаты согласно посчитанным ею скорам. Он также поможет нам получить признаки, идентичные используемым в real time, переиспользовав при этом способности Elasticsearch (TF-IDF и тому подобное).

Для начала нам нужно подключить плагин в нашем контейнере с Elasticsearch. Нам потребуется простенький Dockerfile

# elasticsearch/DockerfileFROM elasticsearch:7.5.1RUN ./bin/elasticsearch-plugin install --batch http://es-learn-to-rank.labs.o19s.com/ltr-1.1.2-es7.5.1.zip


и сопутствующие изменения в docker-compose.yml:

--- a/docker-compose.yml+++ b/docker-compose.yml@@ -5,7 +5,8 @@ services:   elasticsearch:-    image: "elasticsearch:7.5.1"+    build:+      context: elasticsearch     environment:       - discovery.type=single-node


Также нам потребуется поддержка плагина в Python-клиенте. С изумлением я обнаружил, что поддержка для Python не идёт в комплекте с плагином, так что специально для этой статьи я её запилил. Добавим elasticsearch_ltr в requirements.txt и проапгрейдим клиент в вайринге:

--- a/backend/backend/wiring.py+++ b/backend/backend/wiring.py@@ -1,5 +1,6 @@ import os +from elasticsearch_ltr import LTRClient from celery import Celery from elasticsearch import Elasticsearch from pymongo import MongoClient@@ -39,5 +40,6 @@ class Wiring(object):         self.task_manager = TaskManager(self.celery_app)          self.elasticsearch_client = Elasticsearch(hosts=self.settings.ELASTICSEARCH_HOSTS)+        LTRClient.infect_client(self.elasticsearch_client)         self.indexer = Indexer(self.elasticsearch_client, self.card_dao, self.settings.CARDS_INDEX_ALIAS)         self.searcher: Searcher = ElasticsearchSearcher(self.elasticsearch_client, self.settings.CARDS_INDEX_ALIAS)


Ранжирование: пилим признаки


Каждый запрос в Elasticsearch возвращает не только список ID документов, которые нашлись, но и некоторые их скоры (как вы бы перевели на русский язык слово score?). Так, если это match или multi-match query, которую мы используем, то скор это результат вычисления той самой хитрой формулы с участием TF-IDF; если bool query комбинация скоров вложенных запросов; если function score query результат вычисления заданной функции (например, значение какого-то числового поля в документе) и так далее. Плагин ELTR предоставляет нам возможность использовать скор любого запроса как признак, позволяя легко скомбинировать данные о том, насколько хорошо документ соответствует запросу (через multi-match query) и какие-то предрассчитанные статистики, которые мы заранее кладём в документ (через function score query).

Поскольку на руках у нас база TMDB 5000, в которой лежат описания фильмов и, помимо прочего, их рейтинги, давайте возьмём рейтинг в качестве образцово-показательного предрассчитанного признака.

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

  • Признаки мы будем хранить в отдельной коллекции и доставать отдельным менеджером. Сваливать все данные в одну сущность порочная практика.
  • В этот менеджер мы будем обращаться на этапе индексации и класть все имеющиеся признаки в индексируемые документы.
  • Чтобы знать схему индекса, нам надо перед началом построения индекса знать список всех существующих признаков. Этот список мы пока что захардкодим.
  • Поскольку мы не собираемся фильтровать документы по значениям признаков, а собираемся только извлекать их из уже найденных документов для обсчёта модели, мы выключим построение по новым полям обратных индексов опцией index: false в схеме и сэкономим за счёт этого немного места.


Ранжирование: собираем датасет


Поскольку, во-первых, у нас нет продакшна, а во-вторых, поля этой статьи слишком малы для рассказа про телеметрию, Kafka, NiFi, Hadoop, Spark и построение ETL-процессов, я просто сгенерирую случайные просмотры и клики для наших карточек и каких-то поисковых запросов. После этого нужно будет рассчитать признаки для получившихся пар карточка-запрос.

Пришла пора закопаться поглубже в API плагина ELTR. Чтобы рассчитать признаки, нам нужно будет создать сущность feature store (насколько я понимаю, фактически это просто индекс в Elasticsearch, в котором плагин хранит все свои данные), потом создать feature set список признаков с описанием, как вычислять каждый из них. После этого нам достаточно будет сходить в Elasticsearch с запросом специального вида, чтобы получить вектор значений признаков для каждой найденной сущности в результате.

Начнём с создания feature set:

# backend/backend/search/ranking.pyfrom typing import Iterable, List, Mappingfrom elasticsearch import Elasticsearchfrom elasticsearch_ltr import LTRClientfrom backend.search.features import CardFeaturesManagerclass SearchRankingManager:    DEFAULT_FEATURE_SET_NAME = "card_features"    def __init__(self, elasticsearch_client: Elasticsearch,                  card_features_manager: CardFeaturesManager,                 cards_index_name: str):        self.elasticsearch_client = elasticsearch_client        self.card_features_manager = card_features_manager        self.cards_index_name = cards_index_name    def initialize_ranking(self, feature_set_name=DEFAULT_FEATURE_SET_NAME):        ltr: LTRClient = self.elasticsearch_client.ltr        try:            # Создать feature store обязательно для работы,            # но при этом его нельзя создавать дважды \_()_/            ltr.create_feature_store()        except Exception as exc:            if "resource_already_exists_exception" not in str(exc):                raise        # Создаём feature set с невероятными ТРЕМЯ признаками!        ltr.create_feature_set(feature_set_name, {            "featureset": {                "features": [                    # Совпадение поискового запроса с названием                    # карточки может быть более сильным признаком,                     # чем совпадение со всем содержимым, поэтому                     # сделаем отдельный признак про это.                    self._make_feature("name_tf_idf", ["query"], {                        "match": {                            # ELTR позволяет параметризовать                            # запросы, вычисляющие признаки. В данном                            # случае нам, очевидно, нужен текст                             # запроса, чтобы правильно посчитать                             # скор match query.                            "name": "{{query}}"                        }                    }),                    # Скор запроса, которым мы ищем сейчас.                    self._make_feature("combined_tf_idf", ["query"], {                        "multi_match": {                            "query": "{{query}}",                            "fields": ["name^3", "tags.text", "text"]                        }                    }),                    *(                        # Добавляем все имеющиеся предрассчитанные                        # признаки через механизм function score.                        # Если по какой-то причине в документе                         # отсутствует искомое поле, берём 0.                        # (В настоящем проекте вам стоит                        # предусмотреть умолчания получше!)                        self._make_feature(feature_name, [], {                            "function_score": {                                "field_value_factor": {                                    "field": feature_name,                                    "missing": 0                                }                            }                        })                        for feature_name in sorted(self.card_features_manager.get_all_feature_names_set())                    )                ]            }        })    @staticmethod    def _make_feature(name, params, query):        return {            "name": name,            "params": params,            "template_language": "mustache",            "template": query,        }


Теперь функция, вычисляющая признаки для заданного запроса и карточек:

    def compute_cards_features(self, query: str, card_ids: Iterable[str],                                feature_set_name=DEFAULT_FEATURE_SET_NAME) -> Mapping[str, List[float]]:        card_ids = list(card_ids)        result = self.elasticsearch_client.search({            "query": {                "bool": {                    # Нам не нужно проверять, находятся ли карточки                    # на самом деле по такому запросу  если нет,                     # соответствующие признаки просто будут нулевыми.                    # Поэтому оставляем только фильтр по ID.                    "filter": [                        {                            "terms": {                                "_id": card_ids                            }                        },                        # Это  специальный новый тип запроса,                        # вводимый плагином SLTR. Он заставит                        # плагин посчитать все факторы из указанного                        # feature set.                        # (Несмотря на то, что мы всё ещё в разделе                        # filter, этот запрос ничего не фильтрует.)                        {                            "sltr": {                                "_name": "logged_featureset",                                "featureset": feature_set_name,                                "params": {                                    # Та самая параметризация.                                     # Строка, переданная сюда,                                    # подставится в запросах                                    # вместо {{query}}.                                    "query": query                                }                            }                        }                    ]                }            },            # Следующая конструкция заставит плагин запомнить все            # рассчитанные признаки и добавить их в результат поиска.            "ext": {                "ltr_log": {                    "log_specs": {                        "name": "log_entry1",                        "named_query": "logged_featureset"                    }                }            },            "size": len(card_ids),        })        # Осталось достать значения признаков из (несколько        # замысловатого) результата поиска.        # (Чтобы понять, где в недрах результатов нужные мне         # значения, я просто делаю пробные запросы в Kibana.)        return {            hit["_id"]: [feature.get("value", float("nan")) for feature in hit["fields"]["_ltrlog"][0]["log_entry1"]]            for hit in result["hits"]["hits"]        }


Простенький скрипт, принимающий на вход CSV с запросами и ID карточек и выдающий CSV с признаками:

# backend/tools/compute_movie_features.pyimport csvimport itertoolsimport sysimport tqdmfrom backend.wiring import Wiringif __name__ == "__main__":    wiring = Wiring()    reader = iter(csv.reader(sys.stdin))    header = next(reader)    feature_names = wiring.search_ranking_manager.get_feature_names()    writer = csv.writer(sys.stdout)    writer.writerow(["query", "card_id"] + feature_names)    query_index = header.index("query")    card_id_index = header.index("card_id")    chunks = itertools.groupby(reader, lambda row: row[query_index])    for query, rows in tqdm.tqdm(chunks):        card_ids = [row[card_id_index] for row in rows]        features = wiring.search_ranking_manager.compute_cards_features(query, card_ids)        for card_id in card_ids:            writer.writerow((query, card_id, *features[card_id]))


Наконец можно это всё запустить!

# Создаём feature setdocker-compose exec backend python -m tools.initialize_search_ranking# Генерируем событияdocker-compose exec -T backend \    python -m tools.generate_movie_events \    < ~/Downloads/tmdb-movie-metadata/tmdb_5000_movies.csv \    > ~/Downloads/habr-app-demo-dataset-events.csv# Считаем признакиdocker-compose exec -T backend \    python -m tools.compute_features \    < ~/Downloads/habr-app-demo-dataset-events.csv \    > ~/Downloads/habr-app-demo-dataset-features.csv


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

Ранжирование: обучаем и внедряем модель


Опустим подробности загрузки датасетов (скрипт полностью можно посмотреть в этом коммите) и перейдём сразу к делу.

# backend/tools/train_model.py... if __name__ == "__main__":    args = parser.parse_args()    feature_names, features = read_features(args.features)    events = read_events(args.events)    # Разделим запросы на train и test в соотношении 4 к 1.    all_queries = set(events.keys())    train_queries = random.sample(all_queries, int(0.8 * len(all_queries)))    test_queries = all_queries - set(train_queries)    # DMatrix  это тип данных, используемый xgboost.    # Фактически это массив значений признаков с названиями     # и лейблами. В качестве лейбла мы берём 1, если был клик,     # и 0, если не было (детали см. в коммите).    train_dmatrix = make_dmatrix(train_queries, events, feature_names, features)    test_dmatrix = make_dmatrix(test_queries, events, feature_names, features)    # Учим модель!    # Поля этой статьи всё ещё крайне малы для долгого разговора     # про ML, так что я возьму минимально модифицированный пример     # из официального туториала к XGBoost.    param = {        "max_depth": 2,        "eta": 0.3,        "objective": "binary:logistic",        "eval_metric": "auc",    }    num_round = 10    booster = xgboost.train(param, train_dmatrix, num_round, evals=((train_dmatrix, "train"), (test_dmatrix, "test")))    # Сохраняем обученную модель в файл.     booster.dump_model(args.output, dump_format="json")     # Санитарный минимум проверки того, как прошло обучение: давайте    # посмотрим на топ признаков по значимости и на ROC-кривую.    xgboost.plot_importance(booster)    plt.figure()    build_roc(test_dmatrix.get_label(), booster.predict(test_dmatrix))    plt.show()


Запускаем

python backend/tools/train_search_ranking_model.py \    --events ~/Downloads/habr-app-demo-dataset-events.csv \    --features ~/Downloads/habr-app-demo-dataset-features.csv \     -o ~/Downloads/habr-app-demo-model.xgb


Обратите внимание, что поскольку мы экспортировали все нужные данные предыдущими скриптами, этот скрипт уже не надо запускать внутри докера его нужно запускать на вашей машине, предварительно установив xgboost и sklearn. Аналогично в настоящем продакшне предыдущие скрипты нужно было бы запускать где-то, где есть доступ в продакшн-окружение, а этот нет.

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



Хотя события генерировались случайно, combined_tf_idf оказался сильно значимее других потому что я сделал фокус и искусственно понизил вероятность клика для карточек, которые ниже в выдаче, отранжированной нашим старым способом. То, что модель это заметила добрый знак и признак того, что мы не совершили в процессе обучения каких-то совсем глупых ошибок.

Второй график ROC-кривая:



Синяя линия выше красной, а значит, наша модель предсказывает лейблы чуть-чуть лучше, чем бросок монетки. (Кривая ML-инженера маминой подруги должна почти касаться верхнего левого угла).

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

--- a/backend/backend/search/searcher_impl.py+++ b/backend/backend/search/searcher_impl.py@@ -27,6 +30,19 @@ class ElasticsearchSearcher(Searcher):                     "filter": list(self._make_filter_queries(tags, ids)),                 }             },+            "rescore": {+                "window_size": 1000,+                "query": {+                    "rescore_query": {+                        "sltr": {+                            "params": {+                                "query": query+                            },+                            "model": self.ranking_manager.get_current_model_name()+                        }+                    }+                }+            },             "aggregations": {                 self.TAGS_AGGREGATION_NAME: {                     "terms": {"field": "tags"}


Теперь после того, как Elasticsearch произведёт нужный нам поиск и отранжирует результаты своим (довольно быстрым) алгоритмом, мы возьмём топ-1000 результатов и переранжируем, применив нашу (относительно медленную) машинно-обученную формулу. Успех!

Заключение


Мы взяли наше минималистичное веб-приложение и прошли путь от отсутствия фичи поиска как таковой до масштабируемого решения со множеством продвинутых возможностей. Сделать это было не так уж просто. Но и не так уж сложно! Итоговое приложение лежит в репозитории на Github в ветке со скромным названием feature/search и требует для запуска Docker и Python 3 с библиотеками для машинного обучения.

Чтобы показать, как это в целом работает, какие проблемы встречаются и как их можно решить, я использовал Elasticsearch, но это, конечно, не единственный инструмент, который можно выбрать. Solr, полнотекстовые индексы PostgreSQL и другие движки точно так же заслуживают вашего внимания при выборе, на чём построить свою многомиллиардную корпорацию поисковую систему.

И, конечно, это решение не претендует на законченность и готовность к продакшну, а является исключительно иллюстрацией того, как всё может быть сделано. Улучшать его можно практически бесконечно!
  • Инкрементальная индексация. При модификации наших карточек через CardManager хорошо бы сразу обновлять их в индексе. Чтобы CardManager не знал, что у нас в сервисе есть ещё и поиск, и обошлось без циклических зависимостей, придётся прикрутить dependency inversion в том или ином виде.
  • Для индексации в конкретно нашем случае связки MongoDB с Elasticsearch можно использовать готовые решения вроде mongo-connector.
  • Пока пользователь вводит запрос, мы можем предлагать ему подсказки для этого в Elasticsearch есть специальная функциональность.
  • Когда запрос введён, стоит попытаться исправить в нём опечатки, и это тоже целое дело.
  • Для улучшения ранжирования нужно организовать логирование всех пользовательских событий, связанных с поиском, их агрегацию и расчёт признаков на основе счётчиков. Признаки сущность-запрос, сущность-пользователь, сущность-положение Меркурия тысячи их!
  • Особенно весело пилить агрегации событий не офлайновые (раз в день, раз в неделю), а реалтаймовые (задержка от события до учёта в признаках в пределах пяти минут). Вдвойне весело, когда событий сотни миллионов.
  • Предстоит разобраться с прогревом, нагрузочным тестированием, мониторингами.
  • Оркестрировать кластер нод с шардированием и репликацией это целое отдельное наслаждение.

Но чтобы статья осталась читабельного размера, я остановлюсь на этом и оставлю вас наедине с этими челленджами. Спасибо за внимание!
Подробнее..

Парсинг логов при помощи Fluent-bit

25.03.2021 18:04:40 | Автор: admin

Не так давно передо мной встала задача организации логгирования сервисов, разворачиваемых с помощью docker контейнеров. В интернете нашел примеры простого логгирования контейнеров, однако хотелось большего. Изучив возможности Fluent-bit я собрал рабочий пайплайн трансформации логов. Что в сочетании с Elasticsearch и Kibana, позволило быстро искать и анализировать лог-сообщения.

Цель туториала: организовать логгирование docker контейнеров. Также необходимо структурировать записи логов, и обеспечить поиск и фильтрацию по их полям.

Кому интересно, добро пожаловать под кат)

Необходимы базовые знания bash, docker-compose, Elasticsearch и Kibana.

Обзор используемого стека

Тестовое приложение будем запускать с помощьюdocker-compose.

Для организации логгирования воспользуемся следующими технологиями:

  • fluent-bit- осуществляет сбор, обработку и пересылку в хранилище лог-сообщений.

  • elasticsearch- централизованно хранит лог-сообщения, обеспечивает их быстрый поиск и фильтрацию.

  • kibana- предоставляет интерфейс пользователю, для визуализации данных хранимых в elasticsearch

На Хабре естьобзор стековтехнологий, используемых для логгирования контейнеров. Прежде чем идти дальше предварительно можно с ней ознакомиться.

Подготовка тестового приложения

Для примера организуем логгирование веб-сервера Nginx.

Подготовка Nginx

  1. Создадим директорию с проектом и добавим в нее docker-compose.yml, в котором будем задавать конфигурацию запуска контейнеров приложения.

  2. Определим формат логов Nginx. Для этого создадим директорию nginx c файлом nginx.conf. В нем переопределим стандартный формат логов:

    user  nginx;worker_processes  1;error_log  /var/log/nginx/error.log warn;pid        /var/run/nginx.pid;events {    worker_connections  1024;}http {    include       /etc/nginx/mime.types;    default_type  application/octet-stream;log_format  main  'access_log $remote_addr "$request" '                  '$status "$http_user_agent"';access_log  /var/log/nginx/access.log  main;sendfile        on;keepalive_timeout  65;include /etc/nginx/conf.d/*.conf;}
    
  3. Добавим сервисwebв docker-compose.yml:

    version: "3.8"services:  web:    container_name: nginx    image: nginx    ports:      - 80:80    volumes:      # добавляем конфигурацию в контейнер      - ./nginx/nginx.conf:/etc/nginx/nginx.conf
    

Подготовка fluent-bit

Для начала организуем самый простой вариант логгирования. Создадим директорию fluent-bit c конфигурационным файлом fluent-bit.conf. Про формат и схему конфигурационного файла можно прочитатьздесь.

  1. Fluent-bit предоставляет большое количество плагинов для сбора лог-сообщений из различных источников. Полный список можно найтиздесь. В нашем примере мы будем использовать плагинforward.

    Плагин выводаstdoutпозволяет перенаправить лог-сообщения в стандартный вывод (standard output).

    [INPUT]    Name              forward[OUTPUT]    Name stdout    Match *
    
  2. Добавим в docker-compose.yml сервисfluent-bit:

    version: "3.8"services:  web:    ...  fluent-bit:    container_name: fluent-bit    image: fluent/fluent-bit    ports:      # необходимо открыть порты, которые используются плагином forward      - 24224:24224      - 24224:24224/udp    volumes:      # добавляем конфигурацию в контейнер      - ./fluent-bit/fluent-bit.conf:/fluent-bit/etc/fluent-bit.conf
    
  3. Добавим настройки логгирования для сервисаweb:

    version: "3.8"services:  web:    ...    depends_on:      - fluent-bit    logging:      # используемый драйвер логгирования      driver: "fluentd"      options:        # куда посылать лог-сообщения, необходимо что бы адрес         # совпадал с настройками плагина forward        fluentd-address: localhost:24224        # теги используются для маршрутизации лог-сообщений, тема         # маршрутизации будет рассмотрена ниже        tag: nginx.logs  fluent-bit:    ...
    
  4. Запустим тестовое приложение:

    docker-compose up
    

    Сгенерируем лог-сообщение, откроем еще одну вкладку терминала и выполним команду:

    curl localhost
    

    Получим лог-сообщение в следующем формате:

    [    1616473204.000000000,    {"source"=>"stdout",    "log"=>"172.29.0.1 "GET / HTTP/1.1" 200 "curl/7.64.1"",    "container_id"=>"efb81a754706b1ece6948072934df85ea44466305b326cd45",    "container_name"=>"/nginx"}]
    

    Сообщение состоит из:

    • временной метки, добавляемой fluent-bit;

    • лог-сообщения;

    • мета данных, добавляемых драйвером fluentd.

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

 docker-compose.yml fluent-bit    fluent-bit.conf nginx     nginx.conf

Кратко о маршрутизации лог-сообщиний в fluent-bit

Маршрутизация в fluent-bit позволяет направлять лог-сообщения через различные фильтры, для их преобразования, и в конечном итоге в один или несколько выходных интерфейсов. Для организации маршрутизации используется две основные концепции:

  • тег (tag) - человеко читаемый индикатор, позволяющий однозначно определить источник лог-сообщения;

  • правило сопоставления (match) - правило, определяющее куда лог-сообщение должно быть перенаправлено.

Выглядит все следующим образом:

  1. Входной интерфейс присваивает лог-сообщению заданные тег.

  2. В настройках фильтра или выходного интерфейса обязательно необходимо указать правило сопостовления, которое определяет выполнять обработку данного лог-сообщения или нет.

Подробнее можно прочитать вофициальной документации.

Очистка лог-сообщений от мета данных.

Мета данные для нас не представляют интерес, и только загромождают лог сообщение. Давайте удалим их. Для этого воспользуемся фильтромrecord_modifier. Зададим его настройки в файле fluent-bit.conf:

[FILTER]    Name record_modifier    # для всех лог-сообщений    Match *    # оставить только поле log    Whitelist_key log

Теперь лог-сообщение имеет вид:

[    1616474511.000000000,    {"log"=>"172.29.0.1 "GET / HTTP/1.1" 200 "curl/7.64.1""}]

Отделение логов запросов от логов ошибок

На текущий момент логи посылаемые Nginx можно разделить на две категории:

  • логи с предупреждениями, ошибками;

  • логи запросов.

Давайте разделим логи на две группы и будем структурировать только логи запросов. Все логи-сообщения от Nginx помечаются тегом nginx.logs. Поменяем тег для лог-сообщений запросов на nginx.access. Для их идентификации мы заблаговременно добавили в начало сообщения префикс access_log.

Добавим новый фильтрrewrite_tag. Ниже приведена его конфигурация.

[FILTER]    Name rewrite_tag    # для сообщений с тегом nginx.logs    Match nginx.logs    # применить правило: для лог-сообщений поле log которых содержит строку    # access_log, поменять тег на nginx.access, исходное лог-сообщение отбросить.    Rule $log access_log nginx.access false

Теперь все лог-сообщения запросов будут помечены тегом nginx.access, что в будущем позволит нам выполнять фильтрацию логов описанным выше категориям.

Парсинг лог-сообщения

Давайте структурируем наше лог-сообщение. Для придания структуры лог-сообщению его необходимо распарсить. Это делается с помощью фильтраparser.

  1. Лог-сообщение представляет собой строку. Воспользуемся парсеромregex, который позволяет с помощью регулярных выражений определить пары ключ-значение для информации содержащейся в лог-сообщении. Зададим настройки парсера. Для этого в директории fluent-bit создадим файл parsers.conf и добавим в него следующее:

    [PARSER]    Name   nginx_parser    Format regex    Regex  ^access_log (?<remote_address>[^ ]*) "(?<method>\S+)(?: +(?<path>[^\"]*?)(?: +\S*)?)?" (?<status>[^ ]*) "(?<http_user_agent>[^\"]*)"$    Types  status:integer
    
  2. Обновим конфигурационный файл fluent-bit.conf. Подключим к нему файл с конфигурацией парсера и добавим фильтр parser.

    [SERVICE]    Parsers_File /fluent-bit/parsers/parsers.conf[FILTER]    Name parser    # для сообщений с тегом nginx.access    Match nginx.access    # парсить поле log    Key_Name log    # при помощи nginx_parser    Parser nginx_parser
    
  3. Теперь необходимо добавить файл parsers.conf в контейнер, сделаем это путем добавления еще одного volume к сервису fluent-bit:

    version: "3.8"services:  web:    ...  fluent-bit:    ...    volumes:      - ./fluent-bit/fluent-bit.conf:/fluent-bit/etc/fluent-bit.conf
    
  4. Перезапустим приложение, сгенерируем лог-сообщение запроса. Теперь оно имеет следующую структуру:

    [  1616493566.000000000,  {    "remote_address"=>"172.29.0.1",    "method"=>"GET",    "path"=>"/",    "status"=>200,    "http_user_agent"=>"curl/7.64.1"  }]
    

Сохранение лог-сообщений в elasticsearch

Теперь организуем отправку лог-сообщений на хранения в elasticsearch.

  1. Добавим два выходных интерфейса в конфигурацию fluent-bit, один для лог-сообщений запросов, другой для лог-сообщений ошибок. Для этого воспользуемся плагиномes.

    [OUTPUT]    Name  es    Match nginx.logs    Host  elasticsearch    Port  9200    Logstash_Format On    # Использовать префикс nginx-logs для логов ошибок    Logstash_Prefix nginx-logs[OUTPUT]    Name  es    Match nginx.access    Host  elasticsearch    Port  9200    Logstash_Format On    # Использовать префикс nginx-access для логов запросов    Logstash_Prefix nginx-access
    
  2. Добавим в docker-compose.yml сервисы elasticsearch и kibana.

    version: "3.8"services:  web:    ...  fluent-bit:    ...    depends_on:      - elasticsearch  elasticsearch:    container_name: elasticsearch    image: docker.elastic.co/elasticsearch/elasticsearch:7.10.2    environment:      - "discovery.type=single-node"  kibana:    container_name: kibana    image: docker.elastic.co/kibana/kibana:7.10.1    depends_on:      - "elasticsearch"    ports:      - "5601:5601"
    

На текущем этапе структура проекта выглядит следующим образом:

 docker-compose.yml fluent-bit    fluent-bit.conf    parsers.conf nginx     nginx.conf

Финальную версию проекта можно найти в репозитории.

Результаты

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

  • показать только лог-сообщения запросов;

  • показать лог-сообщения запросов с http статусом 404;

  • отображать не все поля лог-сообщения.

Пример фильтрации логов. Выполнена фильтрация по значению поля "status", так же выбраны только необходимые в данный момент поля.Пример фильтрации логов. Выполнена фильтрация по значению поля "status", так же выбраны только необходимые в данный момент поля.

Всем спасибо! Надеюсь туториал был полезен.

Подробнее..

Визуализация аналитики APIM Gravitee в Grafana

30.05.2021 10:22:52 | Автор: admin

Бесспорно, интерфейс Gravitee представляет достаточно наглядные и удобные средства визуализации работы шлюзов Gravitee. Но в любом случае, возникает потребность предоставить доступ к этим инструментам службе мониторинга, владельцам или потребителям API и при этом они могут находится вне закрытого контура, в котором расположен менеджер API. Да и иметь всю доступную информацию по различным API на одном экране всегда удобнее.
Видеть происходящее на шлюзах, при этом не вдаваясь в особенности пользовательского интерфейса Gravitee, а администраторам - не тратить время на создание пользователей и разделение ролей и привилегий внутри Gravitee.
На Хабре уже была пара статей посвященных APIM Gravitee, тут и тут. По этому, в своей заметке, буду подразумевать, что читатель уже знаком с процессом установки/настройки APIM Gravitee и Grafana, рассмотрю только процесс настройки их интеграции.

Почему нельзя пойти простым путём?

По умолчанию, хранилищем для аналитики Gravitee является ElasticSearch. Информация накапливается в четырёх различных индексах, с посуточной разбивкой:

  • gravitee-request-YYYY.MM.DD - здесь хранится информация по каждому запросу (аналог access.log в nginx). Это наша основная цель;

  • gravitee-log-YYYY.MM.DD - здесь уже хранится более подробная информация о запросе (при условии, что включена отладка, см. рисунок ниже). А именно полные заголовки запросов и ответов, а также полезная нагрузка. В зависимости от настроек, логироваться может как обмен между потребителем и шлюзом, так и/или шлюзом и поставщиком API;

    Экран включения/отключения расширенного логированияЭкран включения/отключения расширенного логирования
  • gravitee-monitor-YYYY.MM.DD - этот нас не интересует;

  • gravitee-health-YYYY.MM.DD - этот нас не интересует.

И казалось бы, что может быть проще: подключай ElasticSearch в качестве источника данных в Grafana и визуализируй, но не всё так просто.
Во первых, в индексе хранятся только идентификаторы объектов, т.е. человеко-читаемых имён поставщиков и потребителей, вы там не увидите. Во вторых, получить полную информацию соединив данные из двух источников непосредственно в интерфейсе Grafana, крайне проблематично. Gravitee хранит информацию о настройках и статистику своей работы в разных местах. Настройки, в MongoDB или PostgreSQL, по сути статическая информация. Таким образом в одном месте у нас (в терминах Grafana) - таблица, в другом - временной ряд.

B как же быть?

Большим преимуществом СУБД PostgreSQL является богатый набор расширений для работы с внешними источниками данных, в том числе и с ElasticSearch (тут). Благодаря этому интеграция сводится к тому, что Grafana общается с единственным источником данных - СУБД PostgreSQL, которая в свою очередь получает данные из ElasticSearch и обогащает их информацией и делает читаемой для администратора или любого другого бенефициара.
Схематически это будет выглядеть следующим образом (рисунок ниже).

Схема взаимодействия модулей GraviteeСхема взаимодействия модулей Gravitee

Ну что же, за дело!

Все ниже описанные действия актуальны для следующей конфигурации: CentOS 7, APIM Gravitee 3.6, СУБД PostgreSQL 11, ElasticSearch 7.+

Начнём с интеграции PostgreSQL и ElasticSearch. Сам процесс интеграции достаточно прост и делится на следующие шаги:

  1. Устанавливаем расширение multicorn11 и если не установлен pip, то ставим и его:

    yum install multicorn11 python3-pip
    
  2. Далее из pip-репозитория, устанавливаем библиотеку python3 для работы с ElasticSearch:

    pip3 install pg_es_fdw
    
  3. Далее, переходим к настройке PostgreSQL. Подключаемся целевой БД и добавляем расширение multicorn и подключаем необходимую библиотеку:

    GRANT USAGE on FOREIGN DATA WRAPPER multicorn TO gatewaytest;GRANT USAGE ON FOREIGN SERVER multicorn_es TO gatewaytest;
    
     CREATE EXTENSION multicorn; CREATE SERVER multicorn_es FOREIGN DATA WRAPPER multicorn  OPTIONS (wrapper 'pg_es_fdw.ElasticsearchFDW');
    
  4. Выдаём права, непривилегированному пользователю. В нашем случае это logreader:

    GRANT USAGE on FOREIGN DATA WRAPPER multicorn TO logreader;GRANT USAGE ON FOREIGN SERVER multicorn_es TO logreader;
    
  5. Для удобства, создадим отдельную схему logging, владельцем которой будет наш пользователь logreader:

    CREATE SCHEMA logging AUTHORIZATION logreader;
    
  6. Создадим родительскую таблицу, к которой мы будем подключать новые индексы и удалять не актуальные:

    CREATE TABLE logging.requests (  id varchar(36),  "@timestamp" timestamp with time zone,  api varchar(36),  "api-response-time" int,  application varchar(36),  custom json,  endpoint text,  gateway varchar(36),  "local-address" varchar(16),  method int,  path text,  plan varchar(36),  "proxy-latency" int,  "remote-address" varchar(16),  "request-content-length" int,  "response-content-length" int,  "response-time" int,  sort text,  status int,  subscription varchar(36),  uri text,  query TEXT,  score NUMERIC) PARTITION BY RANGE("@timestamp");
    

    Конечно же, индексы содержат больше полей, но для решения текущей задачи, приведенных выше - достаточно.

  7. Для подключения и отключения индексов, создадим небольшой shell-скрипт и будем запускать его раз в сутки через cron:

    #!/bin/shNEWPART=${1:-$(date +'%Y.%m.%d')}OLDPART=$(date --date='14 days ago' +'%Y.%m.%d')curl http://gateway.corp/testpsql gateway -U logreader -c "CREATE FOREIGN TABLE logging.\"requests_${NEWPART}\"PARTITION OF logging.requests   FOR VALUES FROM ('${NEWPART} 00:00:00') TO ('${NEWPART} 23:59:59')SERVER multicorn_esOPTIONS (host 'els-host',  port '9200',  index 'gravitee-request-${NEWPART}',  rowid_column 'id',  query_column 'query',  query_dsl 'false',    score_column 'score',  sort_column 'sort',  refresh 'false',  complete_returning 'false',  timeout '20',  username 'elastic-ro',  password 'Sup3rS3cr3tP@ssw0rd');"    psql gateway -U gatewaydev -c "drop foreign table logging.\"requests_${OLDPART}\""
    

    Немного пояснений:

    • NEWPART - текущая дата, для формирования имени партиции , при подключении нового индекса из ElasticSearch;

    • OLDPART - дата истекшего, неактуально индекса, здесь это 14 дней (определяется исходя из настроек ES Curator). Удалять партиции, ссылающиеся на несуществующие - обязательно. В противном случае запросы, к родительской таблице, будут прерываться с ошибками;

    • Вызов 'curl http://gateway.corp/test', необходим для того, что бы создавался индекс текущего дня, так как он создаётся в момент первого обращения к любому поставщику API. Если его не создать, то это будет приводить к ошибке, описанной выше. Такая проблема больше актуальна для тестовых стендов и стендов разработки;

    • Затем, создаём партицию на индекс текущего дня;

    • И на последнем шаге - удаляем неактуальный индекс.

    • Проверяем что всё работает

      TABLE logging.requests LIMIT 1;
      

      Если всё правильно, то должны получить похожий результат

    -[ RECORD 1 ]-----------+-------------------------------------id                      | 55efea8a-9c91-4a61-afea-8a9c917a6133@timestamp              | 2021-05-16 00:00:02.025+03api                     | 9db39338-1019-453c-b393-381019f53c72api-response-time       | 0application             | 1custom                  | {}endpoint                | gateway                 | 7804bc6c-2b72-497f-84bc-6c2b72897fa9local-address           | 10.15.79.29method                  | 3path                    | plan                    | proxy-latency           | 2remote-address          | 10.15.79.27request-content-length  | 0response-content-length | 49response-time           | 2sort                    | status                  | 401subscription            | uri                     | /testquery                   | score                   | 1.0
    

Рисуем графики

И вот мы подошли к тому, ради чего всё и делалось - визуализируем статистику Gravitee. Благодаря тому, что для доступа к аналитике используется единая точка входа, а именно СУБД PostgreSQL, это даёт дополнительные возможности. Например, выводить статическую информацию: количество поставщиков, количество потребителей и их статусы; количество и состояние подписок; параметры конфигурации для поставщика и многое другое, наряду с динамическими данными.
В том числе хотелось бы отметить, что у поставщиков и потребителей имеется раздел Metadata, которые можно заполнять кастомными данными и так же выводить в дашборды Grafana.

Вот тут:

Раздел Metadata в GraviteeРаздел Metadata в Gravitee

А вот так это можно отобразить в Grafana:

Вариант отображения Metadata в GrafanaВариант отображения Metadata в Grafana
SELECT  name "Наименование",  value "Значение"FROM  metadataWHERE  reference_id='${apis}'

Пример комплексного экрана

Вариант комплексного экранаВариант комплексного экрана

APIs (статика) - общее количество поставщиков и количество активных.

SELECT COUNT(*) AS "Всего" FROM apis;SELECT COUNT(*) AS "Активных" FROM apis WHERE lifecycle_state='STARTED';

Для Applications, запросы составляются по аналогично, только из таблицы applications

API Hits - количество вызовов по каждому поставщику. Тут уже немного по сложнее

SELECT  date_trunc('minute',"@timestamp") AS time,  apis.name,ee с Grafana  COUNT(*)FROM  logging.requests alJOIN  apis ON al.api = apis.idWHERE  query='@timestamp:[$__from TO $__to]'GROUP BY 1,2

Average response time by API - среднее время ответа, по каждому поставщику считается аналогичным способом.

SELECT  date_trunc('minute',"@timestamp") AS time,  apis.name,  AVG(al."api-response-time")FROM  logging.requests alJOIN  apis ON al.api = apis.idWHERE  query='@timestamp:[$__from TO $__to]'GROUP BY 1,2

Еще один интересный показатель Hits, by gateways, это равномерность распределения запросов по шлюзам. Считается так:

SELECT  date_trunc('minute',"@timestamp") as time,  al."local-address",  COUNT(*)FROM  logging.requests alWHERE  query='@timestamp:[$__from TO $__to]'GROUP BY 1,2
График распределения запросов по шлюзамГрафик распределения запросов по шлюзам

Заключение

Приведённое выше решение, по моему субъективному мнению, нисколько не уступает стандартным средствам визуализации APIM Gravitee, а ограничено лишь фантазией и потребностями.
Учитывая то, что Grafana, обычно является центральным объектом инфраструктуры мониторинга, то преимущества такого решения очевидны: более широкий охват, более высокая плотность информации и простая кастомизация визуальных представлений.

P.S.

В ближайшее время, планируется ещё статья по интеграции Gravitee с ActiveDirectory. Процесс достаточно прост, но как всегда, есть нюансы.

Конструктивная критика, пожелания и предложения приветствуются!

Подробнее..

Перевод Анализ данных Twitter для ленивых в Elastic Stack (сравнение Xbox и PlayStation)

23.11.2020 18:14:08 | Автор: admin

В преддверии супер-интенсива "ELK" подготовили для вас перевод полезной статьи.


Данные Twitter можно получить множеством способов но кому хочется заморачиваться и писать код? Особенно такой, который будет работать без перебоев и перерывов. В Elastic Stack вы можете с легкостью собирать данные из Twitter и анализировать их. Logstash может в качестве входных данных собирать твиты. Инструмент Kafka Connect, которому посвящена недавняя статья, тоже предоставляет такую возможность, но Logstash может отправлять данные во многие источники (включая Apache Kafka) и проще в использовании.

В этой статье мы рассмотрим следующие вопросы:

  • Сохранение потока твитов в Elasticsearch через Logstash

  • Визуализации в Kibana (сравнение Xbox и PlayStation)

  • Удаление HTML-тегов для ключевого слова с использованием механизма стандартизации

Окружение Elastic Search

Все необходимые компоненты находятся в одном Docker Compose. Если у вас уже есть кластер Elasticsearch, вам понадобится только Logstash.

version: '3.3'services:  elasticsearch:    image: docker.elastic.co/elasticsearch/elasticsearch:7.9.2    restart: unless-stopped    environment:      - discovery.type=single-node      - bootstrap.memory_lock=true      - "ES_JAVA_OPTS=-Xms512m -Xmx512m"    ulimits:        memlock:            soft: -1            hard: -1    volumes:      - esdata:/usr/share/elasticsearch/data    restart: unless-stopped    ports:      - 9200:9200  kibana:    image: docker.elastic.co/kibana/kibana:7.9.2    restart: unless-stopped    depends_on:      - elasticsearch    ports:      - 5601:5601  logstash:    image: docker.elastic.co/logstash/logstash:7.9.2    volumes:      - "./pipeline:/usr/share/logstash/pipeline"    environment:      LS_JAVA_OPTS: "-Xmx256m -Xms256m"    depends_on:      - elasticsearch    restart: unless-stoppedvolumes:  esdata:    driver: local

Конвейер Logtash

input {        twitter {        consumer_key => "loremipsum"        consumer_secret => "loremipsum"        oauth_token => "loremipsum-loremipsum"        oauth_token_secret => "loremipsum"        keywords => ["XboxSeriesX", "PS5"]        full_tweet => false        codec => "json"        }}output {        elasticsearch {            hosts => ["elasticsearch:9200"]            index => "tweets"        }}

Чтобы получить токены и ключи, вам понадобится аккаунт разработчика и приложения Twitter. Этим кодом вы улаживаете все формальности.

Конфигурация самого конвейера очень проста. Поток твитов будет подбираться по словам вkeywords. Если вам нужно больше метаданных, просто присвойте параметруfull_tweet valueзначениеtrue.

Данные

Спустя некоторое время после выполнения командыdocker-compose up -d в индексе tweets появляются данные. На момент написания этой статьи мои данные собирались примерно два дня. Весь индекс весил около 430МБ, что не так уж и много. Возможно, другая лицензия позволила бы получить больший поток данных. Визуализации в этой статье отображают данные, собранные за два дня.

ILM здесь нет. Только простой индекс.ILM здесь нет. Только простой индекс.

Итак, у нас уже есть индексtweets. Чтобы иметь возможность использовать собранные данные в Kibana, необходимо добавить шаблон индекса.

Пример документа в индексеtweets.Пример документа в индексеtweets.

Облако тегов Xbox и PlayStation

Простое облако тегов с агрегациейhashtags.text.keyword. PS5, судя по всему, выигрывает, но рассмотрим и другие варианты визуализации.

Линейный график Xbox и PlayStation

Тут у меня тоже складывается впечатление, что PlayStation встречается чаще, чем Xbox. Чтобы узнать наверняка, попробуем сгруппировать хештеги. Некоторые пишутPS5, другиеps5, а ведь это один и тот же продукт.

Однако прежде чем двигаться дальше, обратим внимание на один момент. Важен ли порядок бакетов? Разумеется. Вот что произойдет, если изменить гистограмму изTerms.

Чтобы сгруппировать хештеги, мы можем использовать агрегированные фильтры. Добавим еще несколько хештегов, намеренно опустив наименее популярные. В поле Filter используется синтаксис KQL Lucene, только мощнее.

Используем фильтрыhashtags.text.keyword: (PS5 OR ps5 OR PlayStation5 OR PlayStation) иhashtags.text.keyword: (XboxSeriesX OR Xbox OR XboxSeriesS OR xbox). Теперь мы точно знаем, что PlayStation популярнее в Twitter.

Timelion

XBOX И PLAYSTATION

Еще более полную информацию можно получить с помощью Timelion. Этот интересный инструмент позволяет визуализировать временные ряды. В отличие от предыдущего он может визуализировать данные сразу из множества источников.

К синтаксису сперва надо привыкнуть. Ниже приведен код, сгенерировавший эту диаграмму.

.es(index=tweets, q='hashtags.text.keyword: (PS5 OR ps5 OR PlayStation5 OR PlayStation)').label("PS"),.es(index=tweets, q='hashtags.text.keyword: (XboxSeriesX OR Xbox OR XboxSeriesS OR xbox)').label("XBOX")

Смещение

Timelion позволяет сдвигать функции с помощью параметра смещения. В примере ниже приведено количество твитов о PlayStation в сравнении с предыдущим днем. Данных у меня немного, так что эффект не особенно интересен.

.es(index=tweets, q='hashtags.text.keyword: (PS5 OR ps5 OR PlayStation5 OR PlayStation)').label("PS"),.es(index=tweets, q='hashtags.text.keyword: (PS5 OR ps5 OR PlayStation5 OR PlayStation)', offset=-1d).label("PS -1 day")

Вариативность функции (дельта)

Используя все тот же параметр и метод вычитания, мы можем рассчитать вариативность функции.

.es(index=tweets, q='hashtags.text.keyword: (PS5 OR ps5 OR PlayStation5 OR PlayStation)')    .subtract(        .es(index=tweets, q='hashtags.text.keyword: (PS5 OR ps5 OR PlayStation5 OR PlayStation)', offset=-1h)    )    .label("PS 1h delta"),.es(index=tweets, q='hashtags.text.keyword: (XboxSeriesX OR Xbox OR XboxSeriesS OR xbox)')    .subtract(        .es(index=tweets, q='hashtags.text.keyword: (XboxSeriesX OR Xbox OR XboxSeriesS OR xbox)', offset=-1h)    )    .label("XBOX 1h delta")

Круговая диаграмма типы клиентов

Так себе диаграмма

Теперь давайте выясним, какие клиенты используют для написания твитов. Это, оказывается, не так-то просто. Поле с типом клиента содержит HTML-тег, что уменьшает наглядность диаграммы.

Хорошая диаграмма

У Elasticsearch множество возможностей для обработки текста. Так, фильтрhtml_strip позволяет удалять HTML-теги. К сожалению, нам он ничего не даст, поскольку анализаторы можно использовать только для полей типаtext, а нас интересует поле keyword. Для полей этого типа можно использовать агрегацию.

Для полей keyword можно использоватьнормализаторы. Они работают аналогично анализаторам, но на выходе выдают одиночный токен.

Ниже представлен код, добавляющий нормализатор в индексtweets. Поскольку использовать html_strip нельзя, пришлось прибегнуть к регулярным выражениям. Для изменения настроек анализатора в индексе нужно его закрыть. Следующие фрагменты кода вы можете использовать в инструментах разработчика в Kibana.

POST tweets/_closePUT tweets/_settings{  "analysis": {    "char_filter": {      "client_extractor": {        "type": "pattern_replace",        "pattern": "<a[^>]+>([^<]+)</a>",        "replacement": "$1"      }    },    "normalizer": {      "client_extractor_normalizer": {        "type": "custom",        "char_filter": [          "client_extractor"        ]      }    }  }}POST tweets/_open

Добавив нормализатор, мы можем обновить свойство с типом клиента и добавить новое поле значения.

PUT tweets/_mapping{  "properties": {    "client": {      "type": "text",      "fields": {        "keyword": {          "type": "keyword",          "ignore_above": 256        },        "value":{          "type":"keyword",          "normalizer":"client_extractor_normalizer"        }      }    }  }}

К сожалению, это еще не все. Данные индексируются при их добавлении в индекс (интересно, кстати, почему нельзя было назвать его коллекцией, как вMongoDB? ). Мы можем осуществить повторную индексацию документов с помощью механизма Update By Query.

POST tweets/_update_by_query?wait_for_completion=false&conflicts=proceed

Эта операция возвращает task id. Она может отработать небыстро, если у вас много данных. Найти задачу можно с помощью командыGET _cat/tasks?v.

После обновления шаблона индекса в Kibana мы получим значительно более удобочитаемую диаграмму. Здесь мы видим, что примерно одинаковое количество пользователей используют iPhone и устройства Android. Меня крайне заинтриговал клиентBot Xbox Series X.

Что дальше?

У меня были планы разобраться соSpark NLP, но сначала, пожалуй, займусь потоком данных Twitter. Я собираюсь использовать готовые модели Spark NLP для определения языка, тональности текста и других параметров с помощьюSpark Structured Streaming.

Репозиторий

Ссылка


Подробнее об интенсиве "ELK" можно узнать здесь

Подробнее..

Перевод Мониторинг и управление потоком задач в рамках взаимодействия микросервисов

14.01.2021 14:20:31 | Автор: admin


Ключевые тезисы:

  • Взаимодействие между компонентами напрямую друг с другом может привести к неожиданному поведению, в котором сложно будет разобраться разработчикам, операторам и бизнес-аналитикам.
  • Чтобы обеспечить устойчивость бизнеса, вам нужно видеть все возникающие в системе взаимодействия.
  • Добиться этого позволяют разные подходы: распределённая трассировка, обычно не учитывающая бизнес-аспекты; озёра данных, требующие заметных усилий по настройке получаемых срезов данных; отслеживание процессов, когда вам приходится моделировать интересующий поток задач; контроль и анализ процессов (process mining), позволяющие исследовать поток задач; и вплоть до оркестрации, в которой прозрачность процессов уже имеется.
  • Мы поговорим о том, что вам нужно балансировать между оркестрацией и хореографией микросервисной архитектуры, чтобы понимать, управлять и менять свою систему.


В 2018-м я встретил архитектора из большой интернет-компании. Он рассказал мне, что они следуют всем правильным рекомендациям, и внутри различных областей делят функциональность на маленькие фрагменты, хотя и не называют этот архитектурный стиль микросервисами. Затем мы поговорили о том, как эти сервисы взаимодействуют друг с другом ради поддержания бизнес-логики, выходящей за границы сервисов, потому что именно здесь теория проверяется практикой. Он сказал, что их сервисы взаимодействуют с помощью событий, публикуемых в шине такой подход называют хореографией (ниже я расскажу о нём подробнее). В компании это считают оптимальным с точки зрения уменьшения связанности. Но они столкнулись с проблемой: трудно разобраться в том, что происходит в системе, а изменить её ещё труднее. Это не тот срежиссированный танец, который вы видите на презентациях, посвящённых микросервисам, это неуправляемые прыжки в стиле пого!

Всё это созвучно тому, что говорили мне другие клиенты, например, Джош Вульф из Credit Sense: система, которую мы заменяем, использует сложную пиринговую хореографию, для понимания которой нужно анализировать несколько кодовых баз.


Давайте разберёмся в этой ситуации с помощью упрощённого примера. Предположим, вы создаёте приложение для выполнения заказов. Вы решили использовать событийно-ориентированную архитектуру, а в качестве шины событий выбрали, скажем, Apache Kafka. Когда кто-нибудь размещает заказ, кассовый сервис генерирует событие, которое подхватывается платёжным сервисом. Этот платёжный сервис получает деньги и генерирует событие, которое подхватывается сервисом инвентаризации.


Хореографируемый поток событий.

Преимущество такого подхода в том, что вы можете легко добавлять в систему новые компоненты. Допустим, вы хотите создать сервис уведомлений, рассылающий письма вашим клиентам. Можно просто добавить новый сервис и подписать его на соответствующие события, не касаясь остальных компонентов системы. И теперь вы можете централизованно управлять настройками взаимодействия и сложностью уведомлений, удовлетворяющих требованиям GDPR (регламента по защите данных, действующего на территории Евросоюза).


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

Потеря прозрачности потока событий


Я хочу сосредоточиться на вопросе, возникающем чаще всего, когда я участвую в обсуждениях этой архитектуры: Как избежать потери прозрачности (и, вероятно, контроля) потока событий? В одном из исследований сотрудники Camunda (где я работаю) спрашивали об использовании микросервисов. 92 % респондентов как минимум рассматривали их, а 64 % уже использовали в том или ином виде. Это уже не хайп. Но в том исследовании мы также спрашивали и о трудностях, и получили чёткое подтверждение наших опасений: чаще всего нам говорили о потере сквозной прозрачности бизнес-процессов, задействующих многочисленные сервисы.


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

Обеспечение прозрачности


Что можно сделать в такой ситуации? Следующие подходы помогут вам вернуть прозрачность, но у каждого из них есть свои достоинства и недостатки:

  1. Распределённая трассировка (например, Zipkin или Jaeger).
  2. Озёра данных или аналитические инструменты (например, Elastic).
  3. Контроль и анализ процессов (process mining) (например, ProM).
  4. Отслеживание с помощью автоматизации потоков задач (например, Camunda).

Обратите внимание, что все эти подходы подразумевают наблюдение за работающей системой и изучение потоков данных в ней. Я не знаю ни одного инструмента статического анализа, способного извлечь полезную информацию.

Распределённая трассировка


При таком подходе отслеживается стек вызовов между разными системами и сервисами. Для этого применяют уникальные идентификаторы, обычно добавляемые в определённые заголовки (например, в заголовки HTTP или сообщений). Если все в вашей системе понимают или хотя бы ретранслируют эти заголовки, вы можете прослеживать обмен запросами между сервисами.


Обычно распределённая трассировка используется для того, чтобы понимать, как запросы проходят в системе, для поиска мест сбоев и расследования причин снижения производительности. К достоинствам подхода можно отнести зрелость инструментария и живую сопутствующую экосистему. Поэтому вам будет относительно легко начать использовать распределённую трассировку, даже если вам, как правило, придётся (возможно, агрессивно) оснащать измерительными средствами свои приложения или контейнеры.

Так почему бы не воспользоваться этим подходом, чтобы разобраться в генерировании событий бизнес-процессами? Обычно этому мешают две причины:

  • В трассировке трудно разобраться неспециалистам. Все мои попытки показать трассировки людям, далёким от техники, с треском провалились. Оказалось гораздо лучше потратить время на перерисовку той же информации в виде блоков и стрелок. И даже если вся эта информация о вызовах методов и сообщениях очень полезна для понимания схем взаимодействия, она оказывается слишком подробна для понимания ситуации с межсервисными бизнес-процессами.
  • Для управления избыточным объёмом подробных данных в распределённой трассировке применяется так называемое сэмплирование. Это означает, что собирается лишь небольшая доля запросов, и обычно больше 90 % всех запросов вообще не регистрируется. Об этом хорошо написано в статье Three Pillars with Zero Answers towards a New Scorecard for Observability. Так что у вас никогда не будет полной картины происходящего.

Озёра данных или аналитические инструменты


Итак, сама по себе трассировка вряд ли нам подходит. Логично будет обратиться к схожему подходу, но с учётом нашей конкретной задачи. Обычно это означает сбор не трассировок, а важных бизнес- или тематических событий, с которым вы, возможно, уже и так сталкивались. Часто решение сводится к созданию сервиса, который слушает все события и сохраняет их в базе, что может повышать нагрузку на систему. Сегодня многие используют для этого Elastic.


Это мощный механизм, который относительно прост во внедрении. И его уже внедрили у себя большинство наших клиентов, использующих событийно-ориентированный подход. Главным препятствием для внедрения обычно становится вопрос, кто в большой организации будет управлять таким инструментом, потому что он определённо должен управляться централизованно. Вам будет легко создать собственный пользовательский интерфейс для работы с этим механизмом, позволяющий быстро находить релевантную для определённых запросов информацию.


Пример интерфейса мониторинга событий.

К недостаткам можно отнести отсутствие графиков, облегчающих работу со списком событий. Но вы можете добавить их в инфраструктуру, скажем, проецируя события в средство визуализации вроде BPMN. Небольшие фреймворки наподобие bpmn.io позволяют добавлять информацию в диаграммы, выводимые в виде простых HTML-страниц (пример), которые можно запаковать в Kibana плагин.


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

Инструменты для контроля и анализа процессов (process mining)


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

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


Звучит как идеальное решение нашей задачи. К сожалению, такие инструменты чаще всего применяются для исследования процессов в устаревших архитектурах, поэтому они сосредоточены на анализе журналов и посредственно работают с живыми потоками событий. Другим недостатком является то, что это сугубо научные инструменты, которыми трудно пользоваться (например, ProM), либо которые очень тяжеловесны (например, Celonis). По моему опыту, непрактично применять подобные средства в типичных микросервисных начинаниях.


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

Отслеживание с помощью автоматизации потоков задач


Другой интересный подход моделирование потоков задач с последующим развёртыванием и исполнением посредством модуля управления. Эта модель особенная в том смысле, что она только отслеживает события, а сама ничего активно не делает. То есть ничем не управляет только регистрирует. Я рассказывал об этом на Kafka Summit San Francisco 2018, используя для демонстрации Apache Kafka и open source-модуль управления рабочими процессами Zeebe.


Эта возможность особенно интересна. В сфере модулей управления немало инноваций, что приводит к появлению инструментов, которые компактны, удобны для разработки и высокомасштабируемы. Я писал об этом в статье Events, Flows and Long-Running Services: A Modern Approach to Workflow Automation. К очевидным недостаткам относится необходимость предварительного моделирования потока задач. Но зато эту модель можно исполнять с помощью модуля управления процессами, в отличие от мониторинга событий. По сути, вы запускаете экземпляры процессов для входящих событий или соотносите события с каким-либо экземпляром. Также это позволяет проверить, соответствует ли реальность вашей модели.

Кроме того, этот подход позволяет использовать всю цепочку инструментов, предоставляемых платформой автоматизации потоков задач. Вы можете видеть фактическое состояние системы, отслеживать SLA, обнаруживать сбои экземпляров процессов или выполнять тщательный анализ исторических данных аудита.


Пример мониторинга потока задач.

Когда я проверял этот подход вместе с нашими клиентами, он оказался прост в настройке. Мы просто собрали универсальный компонент, получающий события из шины, и согласовали его с модулем управления потоками задач. Если событие не получалось согласовать, мы с помощью небольшой таблицы принятия решений определяли, можно ли его проигнорировать, или событие приведёт к инциденту, который позднее придётся исследовать. Также мы доработали модули управления потоками задач, используемые внутри микросервисов для исполнения бизнес-логики, чтобы они генерировали определённые события (например, экземпляр процесса запущен, завершён или выполнил какой-то этап), которые будут частью общей картины.

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

Перспективы для бизнеса


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

От отслеживания к управлению


Отслеживание процессов отличный инструмент, обеспечивающий эксплуатационный мониторинг, отчётность, KPI и прозрачность; всё это важные факторы сохранения гибкости компании. Но в текущих проектах отслеживание становится лишь первым шагом на пути к более глубокому управлению и оркестрации в вашей экосистеме микросервисов.

Например, вы можете начать с мониторинга таймаутов в сквозных процессах. Когда происходит таймаут, автоматически выполняется некое действие. В примере ниже, спустя 14 дней мы уведомим клиента о задержке, но всё ещё будем ждать. А через 21 день отменим заказ.


Любопытно, что здесь отправка команды на отмену заказа это оркестрация, которая часто обсуждается в противоречивой манере.

Оркестрация


Я часто слышу, что нужно избегать оркестрации, поскольку она приводит к связанности или нарушает автономность отдельных микросервисов, и конечно, её можно плохо реализовать. Но можно реализовать и так, чтобы оркестрация соответствовала принципам микросервисов и приносила бизнесу большую пользу. Я подробно говорил об этом на InfoQ New York 2018.

Для меня оркестрация означает, что один сервис может скомандовать другому что-то сделать. И всё. Это не тесная связанность, а связанность иного рода. Вспомним наш пример с заказом. Может оказаться целесообразным, чтобы кассовый сервис всего лишь генерировал заказ и клал его в шину событий, не зная, кто его обработает. Сервис заказов подхватывает событие о появившемся заказе. Получатель узнаёт о событии и решает что-нибудь сделать в связи с этим. То есть связанность присутствует на стороне получателя.

С оплатой ситуация иная, поскольку довольно странно, если платёжный сервис будет знать, за что получена оплата. Но ему понадобится это знание, чтобы среагировать на правильные события вроде размещения или создания заказа. Также это означает, сервис придётся менять каждый раз, когда вы хотите получать платежи за новые продукты или услуги. Во многих проектах эту неприятную связанность обходят за счёт генерирования необходимых событий о платежах, но это не события, поскольку отправитель хочет, чтобы кто-то что-нибудь сделал в связи с этим. Это команда! Сервис заказов командует платёжному сервису получить деньги. В этом случае отправитель знает, какую команду отправить, и делает это, то есть связанность присутствует на стороне отправителя.

Чтобы каждое взаимодействие между двумя сервисами было эффективным, оно подразумевает определённую степень связанности. Но в зависимости от конкретной задачи может оказаться целесообразным реализовать связанность на какой-то одной из сторон.


Сервис заказов может даже отвечать за оркестрацию и других сервисов, отслеживая этапы выполнения заказа. Я подробно обсуждал преимущества этого подхода в вышеупомянутом выступлении. Хитрость в том, что хорошей архитектуре требуется балансировка оркестрации и хореографии, а это не всегда легко сделать.

Однако в этой статье я хотел сосредоточиться на прозрачности. И у оркестрации, использующей модуль управления потоками задач, есть очевидное преимущество: модель это не просто код для исполнения оркестратором, её можно использовать для обеспечения прозрачности потока.


Заключение


Очень важно обеспечивать прозрачность ваших бизнес-процессов, вне зависимости от их реализации. Я рассмотрел разные подходы, и в реальных проектах всё обычно сводится к какому-нибудь мониторингу событий с помощью инструментов вроде Elastic, или к отслеживанию процессов с помощью модулей управления. Отчасти выбор может зависеть от конкретного случая и ролей вовлеченных людей. Например, бизнес-аналитику нужно разобраться в данных, собранных со всех экземпляров процессов с необходимой степенью подробности, тогда как эксплуатационисту нужно проанализировать конкретный процесс с разной степенью подробности и, вероятно, обзавестись инструментами для быстрого разрешения инцидентов на уровне системы.

Если в вашем проекте используется преимущественно хореография, то отслеживание процессов может привести вас к добавлению оркестрации. И я считаю это очень важным шагом в сохранении долгосрочного контроля над вашими бизнес-процессами. Иначе вы можете сделать тщательно развязанную событийно-ориентированную систему, не понимая, что потеряете прозрачность при увеличении количества событий и процессов, и тем самым обеспечите себе проблемы в последующие годы, как сказал Мартин Фаулер. Если вы работаете над совершенно новой системой, то с самого начала найдите баланс между оркестрацией и хореографией.

Тем не менее, вне зависимости от особенностей реализации системы убедитесь, что обеспечили бизнесу понятное ему отображение бизнес-процессов, реализованных с помощью взаимодействующих сервисов.
Подробнее..

Аварии как опыт 2. Как развалить Elasticsearch при переносе внутри Kubernetes

28.01.2021 10:04:30 | Автор: admin

В нашей внутренней production-инфраструктуре есть не слишком критичный участок, на котором периодически обкатываются различные технические решения, в том числе и различные версии Rook для stateful-приложений. На момент проведения описываемых работ эта часть инфраструктуры работала на основе Kubernetes-кластера версии 1.15, и возникла потребность в его обновлении.

За заказ persistent volumes в кластере отвечал Rook версии 0.9. Мало того, что этот оператор сам по себе был старой версии, его Helm-релиз содержал ресурсы с deprecated-версиями API, что препятствовало обновлению кластера. Решив не возиться с обновлением Rook вживую, мы стали полностью разбирать его.

Внимание! Это история провала: не повторяйте описанные ниже действия в production, не прочитав внимательно до конца.

Итак, вынос данных в хранилища StorageClassов, не управляемых Rookом, шел уже несколько часов успешно


Беспростойная миграция данных Elasticsearch

когда дело дошло до развернутого в Kubernetes кластера Elasticsearch из 3-х узлов:

~ $ kubectl -n kibana-production get po | grep elasticsearchelasticsearch-0                               1/1     Running     0         77d2helasticsearch-1                               1/1     Running     0         77d2helasticsearch-2                               1/1     Running     0         77d2h

Для него было принято решение осуществить переезд на новые PV без простоя. Конфиг в ConfigMap был проверен и сюрпризов не ожидалось. Хотя в алгоритме действий по миграции и присутствует пара опасных поворотов, чреватых аварией при выпадении узлов Kubernetes-кластера, эти узлы работают стабильно да и вообще: Я сто раз так делал, так что поехали!

1. Вносим изменения в StatefulSet в Helm-чарте для Elasticsearch (es-data-statefulset.yaml):

apiVersion: apps/v1kind: StatefulSetmetadata:  labels:    component: {{ template "fullname" . }}    role: data  name: {{ template "fullname" . }}spec:  serviceName: {{ template "fullname" . }}-data volumeClaimTemplates:  - metadata:      name: data      annotations:        volume.beta.kubernetes.io/storage-class: "high-speed"

В последней строчке (с определением storage class) было ранее указано значение rbd вместо нынешнего high-speed.

2. Удаляем существующий StatefulSet с cascade=false. Это опасный поворот, потому что наличие podов с ES больше не контролируется StatefulSetом и в случае внезапного отказа какого-либо K8s-узла, на котором запущен pod с ES, этот pod не возродится автоматически. Однако операция некаскадного удаления StatefulSet и его редеплоя с новыми параметрами занимает секунды, поэтому риски относительны (т.е. зависят от конкретного окружения, конечно).

Приступим:

 $ kubectl -n kibana-production delete sts elasticsearch --cascade=falsestatefulset.apps "elasticsearch" deleted

3. Деплоим заново наш Elasticsearch, а затем масштабируем StatefulSet до 6 реплик:

~ $ kubectl -n kibana-production scale sts elasticsearch --replicas=6statefulset.apps/elasticsearch scaled

и смотрим на результат:

~ $ kubectl -n kibana-production get po | grep elasticsearchelasticsearch-0                               1/1     Running     0         77d2helasticsearch-1                               1/1     Running     0         77d2helasticsearch-2                               1/1     Running     0         77d2helasticsearch-3                               1/1     Running     0         11melasticsearch-4                               1/1     Running     0         10melasticsearch-5                               1/1     Running     0         10m~ $ kubectl -n kibana-production exec -ti elasticsearch-0 bash[root@elasticsearch-0 elasticsearch]# curl --user admin:********** -sk https://localhost:9200/_cat/nodes10.244.33.142  8 98 49 7.89 4.86 3.45 dim - elasticsearch-410.244.33.118 26 98 35 7.89 4.86 3.45 dim - elasticsearch-210.244.33.140  8 98 60 7.89 4.86 3.45 dim - elasticsearch-310.244.21.71   8 93 58 8.53 6.25 4.39 dim - elasticsearch-510.244.33.120 23 98 33 7.89 4.86 3.45 dim - elasticsearch-010.244.33.119  8 98 34 7.89 4.86 3.45 dim * elasticsearch-1

Картина с хранилищем данных:

~ $ kubectl -n kibana-production get get pvc | grep elasticsearchNAME                   STATUS        VOLUME       CAPACITY   ACCESS MODES   STORAGECLASS    AGEdata-elasticsearch-0   Bound   pvc-a830fb81-...   12Gi       RWO            rbd             77ddata-elasticsearch-1   Bound   pvc-02de4333-...   12Gi       RWO            rbd             77ddata-elasticsearch-2   Bound   pvc-6ed66ff0-...   12Gi       RWO            rbd             77ddata-elasticsearch-3   Bound   pvc-74f3b9b8-...   12Gi       RWO            high-speed      12mdata-elasticsearch-4   Bound   pvc-16cfd735-...   12Gi       RWO            high-speed      12mdata-elasticsearch-5   Bound   pvc-0fb9dbd4-...   12Gi       RWO            high-speed      12m

Отлично!

4. Добавим бодрости переносу данных.

Если в вас еще жив дух авантюризма и неудержимо влечет к приключениям (т.е. данные в окружении не столь критичны), можно ускорить процесс, оставив одну реплику индексов:

~ $ kubectl -n kibana-production exec -ti elasticsearch-0 bash[root@elasticsearch-0 elasticsearch]# curl --user admin:********** -H "Content-Type: application/json" -X PUT -sk https://localhost:9200/my-index-pattern-*/_settings -d '{"number_of_replicas": 0}'{"acknowledged":true}

но мы, конечно, так делать не будем:

~ $ ^C~ $ kubectl -n kibana-production exec -ti elasticsearch-0 bash[root@elasticsearch-0 elasticsearch]# curl --user admin:********** -H "Content-Type: application/json" -X PUT -sk https://localhost:9200/my-index-pattern-*/_settings -d '{"number_of_replicas": 2}'{"acknowledged":true}

Иначе утрата одного podа приведет к неконсистентности данных до его восстановления, а утрата хотя бы одного PV в случае ошибки приведет к потере данных.

Увеличим лимиты перебалансировки:

[root@elasticsearch-0 elasticsearch]# curl --user admin:********** -XPUT -H 'Content-Type: application/json' -sk https://localhost:9200/_cluster/settings?pretty -d '{>   "transient" :{>     "cluster.routing.allocation.cluster_concurrent_rebalance" : 20,>     "cluster.routing.allocation.node_concurrent_recoveries" : 20,>     "cluster.routing.allocation.node_concurrent_incoming_recoveries" : 10,>     "cluster.routing.allocation.node_concurrent_outgoing_recoveries" : 10,>     "indices.recovery.max_bytes_per_sec" : "200mb">   }> }'{  "acknowledged" : true,  "persistent" : { },  "transient" : {    "cluster" : {      "routing" : {        "allocation" : {          "node_concurrent_incoming_recoveries" : "10",          "cluster_concurrent_rebalance" : "20",          "node_concurrent_recoveries" : "20",          "node_concurrent_outgoing_recoveries" : "10"        }      }    },    "indices" : {      "recovery" : {        "max_bytes_per_sec" : "200mb"      }    }  }}

5. Выгоним шарды с первых трех старых узлов ES:

[root@elasticsearch-0 elasticsearch]# curl --user admin:********** -XPUT -H 'Content-Type: application/json' -sk https://localhost:9200/_cluster/settings?pretty -d '{>   "transient" :{>       "cluster.routing.allocation.exclude._ip" : "10.244.33.120,10.244.33.119,10.244.33.118">    }> }'{  "acknowledged" : true,  "persistent" : { },  "transient" : {    "cluster" : {      "routing" : {        "allocation" : {          "exclude" : {            "_ip" : "10.244.33.120,10.244.33.119,10.244.33.118"          }        }      }    }  }}

Вскоре получим первые 3 узла без данных:

[root@elasticsearch-0 elasticsearch]# curl --user admin:********** -sk https://localhost:9200/_cat/shards | grep 'elasticsearch-[0..2]' | wc -l0

6. Пришла пора по одной убить старые узлы Elasticsearch.

Готовим вручную три PersistentVolumeClaim такого вида:

~ $ cat pvc2.yaml---apiVersion: v1kind: PersistentVolumeClaimmetadata:  name: data-elasticsearch-2spec:  accessModes: [ "ReadWriteOnce" ]  resources:    requests:      storage: 12Gi  storageClassName: "high-speed"

Удаляем по очереди PVC и pod у реплик 0, 1 и 2, друг за другом. При этом создаем PVC вручную и убеждаемся, что экземпляр ES в новом podе, порожденном StatefulSetом, успешно запрыгнул в кластер ES:

~ $ kubectl -n kibana-production delete pvc data-elasticsearch-2 persistentvolumeclaim "data-elasticsearch-2" deleted^C~ $ kubectl -n kibana-production delete po elasticsearch-2pod "elasticsearch-2" deleted~ $ kubectl -n kibana-production apply -f pvc2.yamlpersistentvolumeclaim/data-elasticsearch-2 created~ $ kubectl -n kibana-production get po | grep elasticsearchelasticsearch-0                               1/1     Running     0         77d3helasticsearch-1                               1/1     Running     0         77d3helasticsearch-2                               1/1     Running     0         67selasticsearch-3                               1/1     Running     0         42melasticsearch-4                               1/1     Running     0         41melasticsearch-5                               1/1     Running     0         41m~ $ kubectl -n kibana-production exec -ti elasticsearch-0 bash[root@elasticsearch-0 elasticsearch]# curl --user admin:********** -sk https://localhost:9200/_cat/nodes10.244.21.71  21 97 38 3.61 4.11 3.47 dim - elasticsearch-510.244.33.120 17 98 99 8.11 9.26 9.52 dim - elasticsearch-010.244.33.140 20 97 38 3.61 4.11 3.47 dim - elasticsearch-310.244.33.119 12 97 38 3.61 4.11 3.47 dim * elasticsearch-110.244.34.142 20 97 38 3.61 4.11 3.47 dim - elasticsearch-410.244.33.89  17 97 38 3.61 4.11 3.47 dim - elasticsearch-2

Наконец, добираемся до ES-узла 0: удаляем pod elasticsearch-0, после чего он успешно запускается с новым storageClass, заказывает себе PV. Результат:

~ $ kubectl -n kibana-production exec -ti elasticsearch-0 bash[root@elasticsearch-0 elasticsearch]# curl --user admin:********** -sk https://localhost:9200/_cat/nodes10.244.33.151 17 98 99 8.11 9.26 9.52 dim * elasticsearch-0

При этом в соседнем podе:

~ $ kubectl -n kibana-production exec -ti elasticsearch-1 bash[root@elasticsearch-0 elasticsearch]# curl --user admin:********** -sk https://localhost:9200/_cat/nodes10.244.21.71  16 97 27 2.59 2.76 2.57 dim - elasticsearch-510.244.33.140 20 97 38 2.59 2.76 2.57 dim - elasticsearch-310.244.33.35  12 97 38 2.59 2.76 2.57 dim - elasticsearch-110.244.34.142 20 97 38 2.59 2.76 2.57 dim - elasticsearch-410.244.33.89  17 97 98 7.20 7.53 7.51 dim * elasticsearch-2

Поздравляю: мы получили split-brain в production! И сейчас новые данные случайным образом сыпятся в два разных кластера ES!

Простой и потеря данных

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

Может, скинуть label у podа elasticsearch-0, чтобы исключить его из балансировки на уровне Service? Но ведь, исключив pod, мы не сможем его затолкать обратно в кластер ES, потому что при формировании кластера обнаружение членов кластера происходит через тот же Service!

За это отвечает переменная окружения:

        env:        - name: DISCOVERY_SERVICE          value: elasticsearch

и ее использование в ConfigMapе elasticsearch.yaml (см. документацию):

discovery:      zen:        ping.unicast.hosts: ${DISCOVERY_SERVICE}

В общем, по такому пути мы не пойдем. Вместо этого лучше срочно остановить workers, которые пишут данные в ES в реальном времени. Для этого отмасштабируем все три нужных deploymentа в 0. (К слову, хорошо, что приложение придерживается микросервисной архитектуры и не надо останавливать весь сервис целиком.)

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

Причина аварии и восстановление

В чем же дело? Почему узел 0 не присоединился к кластеру? Еще раз проверяем конфигурационные файлы: с ними все в порядке.

Проверяем внимательно еще раз Helm-чарты вот же оно! Итак, проблемный es-data-statefulset.yaml:

apiVersion: apps/v1kind: StatefulSetmetadata:  labels:    component: {{ template "fullname" . }}    role: data  name: {{ template "fullname" . }}     containers:      - name: elasticsearch        env:        {{- range $key, $value :=  .Values.data.env }}        - name: {{ $key }}          value: {{ $value | quote }}        {{- end }}        - name: cluster.initial_master_nodes     # !!!!!!          value: "{{ template "fullname" . }}-0" # !!!!!!        - name: CLUSTER_NAME          value: myesdb        - name: NODE_NAME          valueFrom:            fieldRef:              fieldPath: metadata.name        - name: DISCOVERY_SERVICE          value: elasticsearch        - name: KUBERNETES_NAMESPACE          valueFrom:            fieldRef:              fieldPath: metadata.namespace        - name: ES_JAVA_OPTS          value: "-Xms{{ .Values.data.heapMemory }} -Xmx{{ .Values.data.heapMemory }} -Xlog:disable -Xlog:all=warning:stderr:utctime,level,tags -Xlog:gc=debug:stderr:utctime -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.host=127.0.0.1 -Djava.rmi.server.hostname=127.0.0.1 -Dcom.sun.management.jmxremote.port=9099 -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false"...

Зачем же так определены initial_master_nodes?! Здесь задано (см. документацию) жесткое ограничение, что при первичном старте кластера в выборах мастера участвует только 0-й узел. Так и произошло: pod elasticsearch-0 поднялся с пустым PV, начался процесс бутстрапа кластера, а мастер в podе elasticsearch-2 был благополучно проигнорирован.

Ок, добавим в ConfigMap на живую:

~ $ kubectl -n kibana-production edit cm elasticsearchapiVersion: v1data:  elasticsearch.yml: |-    cluster:      name: ${CLUSTER_NAME}      initial_master_nodes:        - elasticsearch-0        - elasticsearch-1        - elasticsearch-2...

и удалим эту переменную окружения из StatefulSet:

~ $ kubectl -n kibana-production edit sts elasticsearch...      - env:        - name: cluster.initial_master_nodes          value: "elasticsearch-0"...

StatefulSet начинает перекатывать все podы по очереди согласно стратегии RollingUpdate, и делает это, разумеется, с конца, т.е. от 5-го podа к 0-му:

~ $ kubectl -n kibana-production get poNAME              READY   STATUS        RESTARTS   AGEelasticsearch-0   1/1     Running       0          11melasticsearch-1   1/1     Running       0          13melasticsearch-2   1/1     Running       0          15melasticsearch-3   1/1     Running       0          67melasticsearch-4   1/1     Running       0          67melasticsearch-5   0/1     Terminating   0          67m

Что произойдет, когда перекат дойдет до конца? Как отработает бутстрап кластера? Ведь перекат StatefulSet идет быстро как пройдут выборы в таких условиях, если даже в документации заявляется, что auto-bootstrapping is inherently unsafe? Не получим ли мы кластер, забустрапленный из 0-го узла с огрызком индекса?Примерно из-за таких мыслей спокойно наблюдать за происходящим у меня ну никак не получалось.

Забегая вперёд: нет, в заданных условиях всё будет хорошо. Однако 100% уверенности в тот момент не было. А ведь это production, данных много, они критичны для бизнеса, а это чревато дополнительной возней с бэкапами.

Поэтому, пока перекат не докатился до 0-го podа, сохраним и убьем сервис, отвечающий за discovery:

~ $ kubectl -n kibana-production get svc elasticsearch -o yaml > elasticsearch.yaml~ $ kubectl -n kibana-production delete svc elasticsearch service "elasticsearch" deleted

и оторвем PVC у 0-го podа:

~ $ kubectl -n kibana-production delete pvc data-elasticsearch-0 persistentvolumeclaim "data-elasticsearch-0" deleted^C

Теперь, когда перекат прошел, elasticsearch-0 в состоянии Pending из-за отсутствия PVC, а кластер полностью развален, т.к. узлы ES потеряли друг друга:

~ $ kubectl -n kibana-production exec -ti elasticsearch-1 bash[root@elasticsearch-1 elasticsearch]# curl --user admin:********** -sk https://localhost:9200/_cat/nodesOpen Distro Security not initialized.

На всякий случай исправим ConfigMap вот так:

~ $ kubectl -n kibana-production edit cm elasticsearchapiVersion: v1data:  elasticsearch.yml: |-    cluster:      name: ${CLUSTER_NAME}      initial_master_nodes:        - elasticsearch-3        - elasticsearch-4        - elasticsearch-5...

После этого создадим новый пустой PV для elasticsearch-0, создав PVC:

 $ kubectl -n kibana-production apply -f pvc0.yamlpersistentvolumeclaim/data-elasticsearch-0 created

И перезапустим узлы для применения изменений в ConfigMap:

~ $ kubectl -n kibana-production delete po elasticsearch-0 elasticsearch-1 elasticsearch-2 elasticsearch-3 elasticsearch-4 elasticsearch-5pod "elasticsearch-0" deletedpod "elasticsearch-1" deletedpod "elasticsearch-2" deletedpod "elasticsearch-3" deletedpod "elasticsearch-4" deletedpod "elasticsearch-5" deleted

Можно возвращать на место сервис из недавно сохраненного нами YAML-манифеста:

~ $ kubectl -n kibana-production apply -f elasticsearch.yaml service/elasticsearch created

Посмотрим, что получилось:

~ $ kubectl -n kibana-production exec -ti elasticsearch-0 bash[root@elasticsearch-0 elasticsearch]# curl --user admin:********** -sk https://localhost:9200/_cat/nodes10.244.98.100  11 98 32 4.95 3.32 2.87 dim - elasticsearch-010.244.101.157 12 97 26 3.15 3.00 2.10 dim - elasticsearch-310.244.107.179 10 97 38 1.66 2.46 2.52 dim * elasticsearch-110.244.107.180  6 97 38 1.66 2.46 2.52 dim - elasticsearch-210.244.100.94   9 92 36 2.23 2.03 1.94 dim - elasticsearch-510.244.97.25    8 98 42 4.46 4.92 3.79 dim - elasticsearch-4[root@elasticsearch-0 elasticsearch]# curl --user admin:********** -sk https://localhost:9200/_cat/indices | grep -v green | wc -l0

Ура! Выборы прошли нормально, кластер собрался полностью, индексы на месте.

Осталось только:

  1. Снова вернуть в ConfigMap значения initial_master_nodes для elasticsearch-0..2;

  2. Еще раз перезапустить все podы;

  3. Аналогично шагу, описанному в начале статьи, выгнать все шарды на узлы 0..2 и отмасштабировать кластер с 6 до 3-х узлов;

  4. Наконец, сделанные вручную изменения донести до репозитория.

Заключение

Какие уроки можно извлечь из данного случая?

Работая с переносом данных в production, всегда следует иметь в виду, что что-то может пойти не так: будет допущена ошибка в конфигурации приложения или сервиса, произойдет внезапная авария в ЦОД, начнутся сетевые проблемы да все что угодно! Соответственно, перед началом работ должны быть предприняты меры, которые позволят либо предотвратить аварию, либо максимально купировать ее последствия. Обязательно должен быть подготовлен План Б.

Использованный нами алгоритм действий был неустойчив к внезапным проблемам. Перед выполнением этих работ в более важном окружении было бы необходимо:

  1. Выполнить переезд в тестовом окружении с production-конфигурацией ES.

  2. Запланировать простой сервиса. Либо временно переключить нагрузку на другой кластер. (Предпочтительный путь зависит от требований к доступности.) В случае варианта с простоем следовало предварительно остановить workers, пишущие данные в Elasticsearch, снять затем свежую резервную копию, а после этого приступить к работам по переносу данных в новое хранилище.

P.S.

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

Подробнее..

Поиск в Кафке

26.02.2021 10:11:26 | Автор: admin

Меня зовут Сергей Калинец, я архитектор в компании Parimatch Tech, и в этой публикации хочу поделиться нашим опытом в области поиска сообщений в Kafka.

Для нашей компании Kafka является центральной нервной системой, через которую микросервисы обмениваются информацией. От входа до выхода сообщение может пройти через десяток сервисов, которые его фильтруют и трансформируют, перекладывая из одного топика в другой. Этими сервисами владеют разные команды, и очень полезно бывает посмотреть, что же содержится в том или ином сообщении. Особенно интересно это в случаях, когда что-то идет не по плану важно понять на каком этапе все превратилось в тыкву (ну и кому нужно в тыкву дать, чтобы такого больше не повторялось). С высоты птичьего полета решение простое нужно взять соответствующие сообщения из кафки и посмотреть что в них не так. Но, как обычно, интересноеначинаетсяв деталях.

Начнем с того, что кафка это не просто брокер сообщений, как многие думают и пользуются ей, но и распределенный лог. Это значит много чего, но нам интересно то, что сообщения не удаляются из топиков после того, как их прочитали получатели, и технически можно в любой момент их прочитать заново и посмотреть, что там внутри. Однако все усложняется тем, что читать из Kafka можно только последовательно. Нужно знать смещение (для упрощения это порядковый номер в топике), с которого нам нужны сообщения. Также возможно в качестве начальной точки указать время, но потом все равно можно только читать все сообщения по порядку.

Получается, что для того, чтобы найти нужное сообщение, нужно прочитать пачку и найти среди них те, которые интересны. Например, если мы хотим разобраться в проблемах игрока с id=42, нужно найти все сообщения, где он упоминается (playerId: 42), выстроить их в цепочку, ну и дальше уже смотреть, на каком этапе все пошло не так.

В отличии от баз данных вроде MySQL или MSSQL, у которых в комплекте поставки сразу есть клиентские приложения с графическим интерфейсом, ванильная Kafka не балует нас такими изысками и предлагает разве что консольные утилиты с довольно узким (на первый взгляд) функционалом.

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

Я опишу некоторые наиболее часто используемые, а в конце более детально разберем самое, на наш взгляд, близкое к идеальному.

Итак, из чего можно выбирать?

Kafka Tool

(скриншот с официального сайта https://www.kafkatool.com/features.html)

Наверное самый популярный инструмент, которым пользуются люди, привыкшие к GUI инструментам. Позволяет увидеть список топиков, и прочитать отдельные сообщения. Есть возможность фильтровать по содержимому сообщений, указывать с какого смещения читать и сколько сообщений нужно прочитать. Но все же нужно знать примерные смещения интересующих нас сообщений, потому что Kafka Tool ищет только в рамках тех сообщений, которые были прочитаны. Говоря словами Дятлова из сериала Чернобыль (в оригинальной озвучке): Not great not terrible.

Часто это единственная альтернатива, с которой вынуждены работать люди, волей судьбы столкнувшиеся с кафкой. Но есть и другие средства.

Kafka Console Consumer

Это одна из утилит, входящая в комплект поставки кафки. И она позволяет читать данные из нее. Как и сама Kafka, это JVM приложение, которое для своей работы требует установленной Java. В принципе, это справедливо и для Kafka Tool, но в данном случае можно обойтись без Java если запускать через docker:

По сути, нужно просто перед самой командой добавить docker run --rm -it taion809/kafka-cli:2.2.0, что на докерском значит запусти вот такой образ, выведи, то, что он показывает, на мой экран и удали образ, когда он закончит работу. Можно пойти еще дальшеи добавить алиас типа

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

Kafkacat

Это уже весьма могучая штука, которая позволяет читать и писать из Кафки, а также получать список топиков. Она тоже консольная, но при этом поудобнее в использовании, чем стандартные утилиты вроде kafka-console-consumer (и ее тоже можно запустить из докера).

Вот так можно сохранить 10 сообщений в файле messages (в формате JSON):

Файл можно будет использовать для анализа или чтобы воспроизвести какой-то сценарий.Безусловно, для решения этой задачи можно было бы и использовать родной консьюмер, но kafkacat позволяет выразить это короче.

Или вот пример посложнее (в смысле букв немного больше, но решение проще многих других альтернатив):

Код взят с блога одного из самых топовых популяризаторов Kafka экосистемы Robin Moffatt. В двух словах один инстанс kafkacat читает сообщения из топика Kafka, потом сообщения трансформируются в необходимый формат и скармливаются другому инстансу kafkacat, который пишет их в другой топик. Многим такое может показаться вырвиглазным непонятным брейнфаком, однако на самом деле у нас готовое решение, которое можно запустить в докере и оно будет работать. Реализация такого сценария на вот этих ваших дотнетах и джавах потребует больше букв однозначно.

Но это меня уже занесло немного не в ту степь. Статья же про поиск. Так вот, похожим способом можно организовать и поиск сообщений просто перенаправить вывод в какой-то grep и дело с концом.

Из возможностей для улучшений можно отметить тот факт, что kafkacat поддерживает Avro из коробки, а вот protobuf нет.

Kafka Connect + ELK

Все вышеперечисленные штуки работают и решают поставленные задачи, однако не всем удобны. При разборе инцидентов нужно именно поискать сообщения в разных топиках по определенному тексту это может быть определенный идентификатор или имя. Наши QA (а именно они в 90% случаев занимаются подобными расследованиями) наловчились пользоваться Kafka Tool, а некоторые и консольными утилитами. Но это все меркнет по сравнению с возможностями, которые дает Kibana, UI оболочка вокруг базы данных Elasticsearch. Kibana тоже используется нами QA для анализа логов. И не раз поднимался вопрос давайте логировать все сообщения, чтобы можно было искать в Kibana. Но оказалось, что есть способ намного проще, чем добавление вызова логера в каждый из наших сервисов, и имя ему Kafka Connect.

Kafka Connect это решение для интеграции Kafka с другими системами. В двух словах,оно (или он?) позволяет экспортировать из и импортировать данные в Kafka без написания кода. Все, что нужно это поднятый кластер Connect и конфиги наших хотелок в формате JSON. Слово кластер звучит дорого и сложно, но на самом деле это один или больше инстансов, которые можно поднять где угодно мы, например, запускаем их там же, где и обычные сервисы в Kubernetes.

Kafka Connect предоставляет REST API, c помощью которого можно управлять коннекторами, занимающимися перегонкой данных из и в Kafka. Коннекторы задаются конфигурациями, в случае Elasticsearch эта конфигурация может быть вот такой:

Если такой конфиг через HTTP PUT передать на сервер Connect, то, при определенном стечении обстоятельств, создастся коннектор с именем ElasticSinkConnector, который будет в три потока читать данные из топика и писать их в Elastic.

Выглядит всё крайне просто, но самое интересное, конечно же, в деталях. А деталей тут есть )

Большинство проблем связанно с данными. Обычно, как и в нашем случае, нужно работать с форматами данных, разработчики которого явно не думали, что когда-то эти данные будут попадать в Elasticsearch.

Для решения нюансов с данными есть трансформации. Это такие себе функции, которые можно применять к данным и мутировать их, подстраивая под требования получателя. При этом всегда есть возможность использовать любую Kafka клиент технологию для случаев, когда трансформации бессильны. Какие же сценарии мы решали с помощью трансформаций?

В нашем случае есть 4 трансформации. Вначале мы их перечисляем, а потом конфигурим. Трансформации применяются в порядке их перечисления, и это позволяет интересно их комбинировать.

Имена

Вначале добавляем возможность поиска по имени топика просто дополняем наши сообщения полем с нужной информацией.

Индексы

Elasticsearch работает с индексами, которые имеют свойство забиваться и нервировать девопсов. Нужна поддержка удобной для управления схемой индексирования. В нашем случае мы остановились на индексе на сервис / команду с ежедневной ротацией. Плюс решения хранение данные из разных топиков в одном индексе с возможностью контролировать его возраст.

Даты

Для того, чтобы в Kibana можно было искать по дате, необходимо задать поле, в котором эта дата содержится. Мы используем дату публикации сообщения в Kafka. Чтобы ее получить, мы вначале вставляем поле с датой сообщения, а потом конвертируем ее в UTC формат. Конвертация была добавлена, чтобы помочь Elasticsearch распознать в этом поле timestamp, однако в нашем случае это не всегда происходило, поэтому мы добавили index template, который явно говорил в этом поле дата:

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

Тут, конечно, стоит отметить, что данная инициатива сейчас у нас на этапе внедрения, поэтому нельзя сказать,что все массово ищут все что нужно в Kibana, но мы к этому уверенно идем.

Вообще, Kafka Connect применимо не только для таких задач. Его можно вполне использовать в тех случаях, где нужна интеграция с другими системами, Реально, например, реализовать полнотекстовый поиск в вашем приложении с помощью двух коннекторов. Один будет читать из операционной базы обновления и писать их в Kafka. А второй читать из Kafka и отсылать в Elasticsearch. Приложение делает поисковый запрос в Elasticsearch, получает id и по нему находит нужные данные в базе.

Заключение

Ну а наша публикация подходит к концу. Очень надеюсь, что вы узнали что-то новое для себя, а иначе зачем это все? Если что-то не получилось раскрыть, или вы категорически не согласны с чем-то, или может есть более удобные способы решения подобных проблем напишите про это в комментариях, обсудим )

Подробнее..

Перевод Как построить систему распознавания лиц с помощью Elasticsearch и Python

13.05.2021 18:23:09 | Автор: admin

Перевод материала подготовлен в рамках практического интенсива Централизованные системы логирования Elastic stack.


Пытались ли вы когда-нибудь искать объекты на изображениях? Elasticsearch может помочь вам хранить, анализировать и искать объекты на изображениях или видео.

В этом кратком руководстве мы покажем вам, как создать систему распознавания лиц с помощью Python. Узнайте больше о том, как обнаруживать и кодировать информацию о внешности - и находить совпадения в поиске.

Краткое описание основ

Вам нужно освежить информацию? Давайте вкратце рассмотрим несколько основных понятий.

Распознавание лиц

Распознавание лиц - это процесс идентификации человека по его лицу, например, для реализации механизма аутентификации (как разблокировка смартфона). Он фиксирует, анализирует и сравнивает паттерны, основанные на деталях лица человека. Этот процесс можно разделить на три этапа:

  • Обнаружение лица: Идентификация человеческих лиц на цифровых изображениях.

  • Кодирование данных о лице: Преобразование черт лица в цифровое представление

  • Сопоставление лиц: Поиск и сравнение черт лица

Мы рассмотрим каждый этап на нашем примере.

128-мерный вектор

Черты лица могут быть преобразованы в набор цифровой информации для хранения и анализа.

Векторный тип данных

Elasticsearch предлагает тип данных dense_vector для хранения плотных векторов плавающих значений. Максимальное количество элементов в векторе не должно превышать 2048, что вполне достаточно для хранения репрезентаций черт лица.

Теперь давайте реализуем все эти концепции.

Все, что вам нужно

Для обнаружения лиц и кодирования информации вам понадобится следующее:

Обратите внимание, что мы протестировали следующие инструкции на Ubuntu 20.04 LTS и Ubuntu 18.04 LTS. В зависимости от вашей операционной системы могут потребоваться некоторые изменения.

Установите Python и библиотеки Python

Ubuntu 20 и другие версии Debian Linux поставляются с установленным Python 3. Если у вас не такой пакет, загрузите и установите Python по этой ссылке.

Чтобы убедиться, что ваша версия является самой свежей, вы можете выполнить следующую команду:

sudo apt update sudo apt upgrade

Убедитесь, что версия Python - 3.x:

python3 -V

Установите pip3 для управления библиотеками Python:

sudo apt install -y python3-pip

Установите cmake, необходимый для работы библиотеки face_recognition:

pip3 install CMake

Добавьте папку cmake bin в каталог $PATH:

export PATH=$CMake_bin_folder:$PATH

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

pip3 install dlib pip3 install numpy pip3 install face_recognition  pip3 install elasticsearch

Обнаружение и кодирование информации о лице из изображения

Используя библиотеку face_recognition, мы можем обнаружить лица на изображении и преобразовать черты лица в 128-мерный вектор.

Создайте файл getVectorFromPicture.py:

touch getVectorFromPicture.py

Дополните файл следующим сценарием:

import face_recognition import numpy as np import sys image = face_recognition.load_image_file("$PATH_TO_IMAGE") # detect the faces from the images  face_locations = face_recognition.face_locations(image) # encode the 128-dimension face encoding for each face in the image face_encodings = face_recognition.face_encodings(image, face_locations) # Display the 128-dimension for each face detected for face_encoding in face_encodings:       print("Face found ==>  ", face_encoding.tolist())

Давайте выполним getVectorFromPicture.py, чтобы получить репрезентацию черт лица для изображений основателей компании Elastic. В сценарии необходимо изменить переменную $PATH_TO_IMAGE, чтобы задать имя файла изображения.

Теперь мы можем сохранить представление черт лица в Elasticsearch.

Сначала создадим индекс с отображением, содержащим поле с типом dense_vector:

# Store the face 128-dimension in Elasticsearch ## Create the mapping curl -XPUT "http://localhost:9200/faces" -H 'Content-Type: application/json' -d' {   "mappings" : {       "properties" : {         "face_name" : {           "type" : "keyword"         },         "face_encoding" : {           "type" : "dense_vector",           "dims" : 128         }       }     } }'

Нам необходимо создать один документ для каждой репрезентации лица, это можно сделать с помощью Index API:

## Index the face feature representationcurl -XPOST "http://localhost:9200/faces/_doc" -H 'Content-Type: application/json' -d'{  "face_name": "name",  "face_encoding": [     -0.14664565,     0.07806452,     0.03944433,     ...     ...     ...     -0.03167224,     -0.13942884  ]}'

Сопоставление лиц

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

Создайте файл recognizeFaces.py:

touch recognizeFaces.py

В этом сценарии мы извлечем векторы для каждого лица, обнаруженного на входном изображении, и используем эти векторы при создании запроса для отправки в Elasticsearch:

Импортируйте библиотеки:

import face_recognition import numpy as np from elasticsearch import Elasticsearch import sys

Добавьте следующий раздел для подключения к Elasticsearch:

# Connect to Elasticsearch cluster from elasticsearch import Elasticsearch es = Elasticsearch( cloud_id="cluster-1:dXMa5Fx...",      http_auth=("elastic", "<password>"), )

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

i=0 for face_encoding in face_encodings:         i += 1         print("Face",i)         response = es.search(         index="faces",         body={          "size": 1,          "_source": "face_name",         "query": {         "script_score": {                  "query" : {                      "match_all": {}                  },          "script": {                 "source": "cosineSimilarity(params.query_vector, 'face_encoding')",                  "params": {                  "query_vector":face_encoding.tolist()                 }            }           }          }         }         )

Предположим, что значение меньше 0,93 считается неизвестным лицом:

for hit in response['hits']['hits']:                 #double score=float(hit['_score'])                 if (float(hit['_score']) > 0.93):                     print("==> This face  match with ", hit['_source']['face_name'], ",the score is" ,hit['_score'])                 else:                         print("==> Unknown face")

Давайте выполним наш скрипт:

Скрипт смог обнаружить все лица с совпадением результатов более 0,93.

Сделайте еще один шаг вперед с расширенным поиском

Распознавание лиц и поиск можно объединить для расширенных сценариев использования. Вы можете использовать Elasticsearch для создания более сложных запросов, таких как geo-queries, query-dsl-bool-query и search-aggregations.

Например, следующий запрос применяет поиск cosineSimilarity к определенному местоположению в радиусе 200 км:

GET /_search {   "query": {     "script_score": {       "query": {     "bool": {       "must": {         "match_all": {}       },       "filter": {         "geo_distance": {           "distance": "200km",           "pin.location": {             "lat": 40,             "lon": -70           }         }       }     }   },        "script": {                 "source": "cosineSimilarity(params.query_vector, 'face_encoding')",                  "params": {                  "query_vector":[                         -0.14664565,                       0.07806452,                       0.03944433,                       ...                       ...                       ...                       -0.03167224,                       -0.13942884                    ]                 }            }     }   } }

Комбинирование cosineSimilarity с другими запросами Elasticsearch дает вам неограниченные возможности для реализации более сложных сценариев использования.

Заключение

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

Elasticsearch может помочь в решении сложных задач. Попробуйте это с помощью бесплатной 14-дневной пробной версии Elastic Cloud, нашего официального управляемого предложения Elasticsearch, и сообщите о своем мнении на наших форумах для обсуждения.


Узнать больше об экспресс-курсе Централизованные системы логирования Elastic stack.

Подробнее..

Продолжаем знакомство с APIM Gravitee

27.05.2021 20:23:24 | Автор: admin

Всем привет! Меня всё ещё зовут Антон. В предыдущейстатьея провел небольшой обзор APIM Gravitee и в целом систем типа API Management. В этой статье я расскажу,как поднять ознакомительный стенд APIM Gravitee (https://www.gravitee.io), рассмотрим архитектуру системы, содержимое docker compose file, добавим некоторые параметры, запустим APIM Gravitee и сделаем первую API. Статья немного погружает в технические аспекты и может быть полезна администраторам и инженерам, чтобы начать разбираться в системе.

Архитектура

Для ознакомительного стенда будем использовать простейшую архитектуру

Все в докере, в том числе и MongoDB и Elasticsearch. Чтобы сделать полноценную среду для тестирования крайне желательно компоненты MongoDB и Elasticsearch вынести за пределы Docker. Также для простоты манипулирования настройками можно вынести конфигурационные файлы Gateway и Management API: logback.xml и gravitee.yml.

docker-compose.yml

Среду для начальных шагов будем поднимать, используяdocker-compose file, предоставленный разработчиками наgithub. Правда,внесемнесколькокорректив.

docker-compose.yml# Copyright (C) 2015 The Gravitee team (<http://gravitee.io>)## Licensed under the Apache License, Version 2.0 (the "License");# you may not use this file except in compliance with the License.# You may obtain a copy of the License at##<http://www.apache.org/licenses/LICENSE-2.0>## Unless required by applicable law or agreed to in writing, software# distributed under the License is distributed on an "AS IS" BASIS,# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.# See the License for the specific language governing permissions and# limitations under the License.#version: '3.5'networks:frontend:name: frontendstorage:name: storagevolumes:data-elasticsearch:data-mongo:services:mongodb:image: mongo:${MONGODB_VERSION:-3.6}container_name: gio_apim_mongodbrestart: alwaysvolumes:- data-mongo:/data/db- ./logs/apim-mongodb:/var/log/mongodbnetworks:- storageelasticsearch:image: docker.elastic.co/elasticsearch/elasticsearch:${ELASTIC_VERSION:-7.7.0}container_name: gio_apim_elasticsearchrestart: alwaysvolumes:- data-elasticsearch:/usr/share/elasticsearch/dataenvironment:- http.host=0.0.0.0- transport.host=0.0.0.0- xpack.security.enabled=false- xpack.monitoring.enabled=false- cluster.name=elasticsearch- bootstrap.memory_lock=true- discovery.type=single-node- "ES_JAVA_OPTS=-Xms512m -Xmx512m"ulimits:memlock:soft: -1hard: -1nofile: 65536networks:- storagegateway:image: graviteeio/apim-gateway:${APIM_VERSION:-3}container_name: gio_apim_gatewayrestart: alwaysports:- "8082:8082"depends_on:- mongodb- elasticsearchvolumes:- ./logs/apim-gateway:/opt/graviteeio-gateway/logsenvironment:- gravitee_management_mongodb_uri=mongodb://mongodb:27017/gravitee?serverSelectionTimeoutMS=5000&connectTimeoutMS=5000&socketTimeoutMS=5000- gravitee_ratelimit_mongodb_uri=mongodb://mongodb:27017/gravitee?serverSelectionTimeoutMS=5000&connectTimeoutMS=5000&socketTimeoutMS=5000- gravitee_reporters_elasticsearch_endpoints_0=http://elasticsearch:9200networks:- storage- frontendmanagement_api:image: graviteeio/apim-management-api:${APIM_VERSION:-3}container_name: gio_apim_management_apirestart: alwaysports:- "8083:8083"links:- mongodb- elasticsearchdepends_on:- mongodb- elasticsearchvolumes:- ./logs/apim-management-api:/opt/graviteeio-management-api/logsenvironment:- gravitee_management_mongodb_uri=mongodb://mongodb:27017/gravitee?serverSelectionTimeoutMS=5000&connectTimeoutMS=5000&socketTimeoutMS=5000- gravitee_analytics_elasticsearch_endpoints_0=http://elasticsearch:9200networks:- storage- frontendmanagement_ui:image: graviteeio/apim-management-ui:${APIM_VERSION:-3}container_name: gio_apim_management_uirestart: alwaysports:- "8084:8080"depends_on:- management_apienvironment:- MGMT_API_URL=http://localhost:8083/management/organizations/DEFAULT/environments/DEFAULT/volumes:- ./logs/apim-management-ui:/var/log/nginxnetworks:- frontendportal_ui:image: graviteeio/apim-portal-ui:${APIM_VERSION:-3}container_name: gio_apim_portal_uirestart: alwaysports:- "8085:8080"depends_on:- management_apienvironment:- PORTAL_API_URL=http://localhost:8083/portal/environments/DEFAULTvolumes:- ./logs/apim-portal-ui:/var/log/nginxnetworks:- frontend

Первичные системы, на основе которых строится весь остальной сервис:<o:p>

  1. MongoDB - хранение настроек системы, API, Application, групп, пользователей и журнала аудита.

  2. Elasticsearch(Open Distro for Elasticsearch) - хранение логов, метрик, данных мониторинга.

MongoDB

docker-compose.yml:mongodb

mongodb:image: mongo:${MONGODB_VERSION:-3.6}<o:p>container_name: gio_apim_mongodb<o:p>restart: always<o:p>volumes:<o:p>- data-mongo:/data/db<o:p>- ./logs/apim-mongodb:/var/log/mongodb<o:p>networks:<o:p>- storage<o:p>

С MongoDB всё просто поднимается единственный экземпляр версии 3.6, если не указано иное, с volume для логов самой MongoDB и для данных в MongoDB.<o:p>

Elasticsearch

docker-compose.yml:elasticsearch

elasticsearch:image: docker.elastic.co/elasticsearch/elasticsearch:${ELASTIC_VERSION:-7.7.0}<o:p>container_name: gio_apim_elasticsearchrestart: alwaysvolumes:- data-elasticsearch:/usr/share/elasticsearch/dataenvironment:- http.host=0.0.0.0- transport.host=0.0.0.0<o:p>- xpack.security.enabled=false- xpack.monitoring.enabled=false- cluster.name=elasticsearch- bootstrap.memory_lock=true- discovery.type=single-node- "ES_JAVA_OPTS=-Xms512m -Xmx512m"ulimitsmemlock:soft: -1hard: -1nofile: 65536networks:- storage

elasticsearch:

elasticsearch:image: docker.elastic.co/elasticsearch/elasticsearch:${ELASTIC_VERSION:-7.7.0}container_name: gio_apim_elasticsearchrestart: alwaysvolumes:- data-elasticsearch:/usr/share/elasticsearch/data<o:p>environment:- http.host=0.0.0.0- transport.host=0.0.0.0- xpack.security.enabled=false- xpack.monitoring.enabled=false- cluster.name=elasticsearch- bootstrap.memory_lock=true- discovery.type=single-node- "ES_JAVA_OPTS=-Xms512m -Xmx512m"ulimits:memlock:soft: -1hard: -1nofile: 65536networks:- storage

С Elasticsearch также всё просто поднимается единственный экземпляр версии 7.7.0, если не указано иное, с volume для данных в Elasticsearch. Сразу стоит убрать строки xpack.security.enabled=false и xpack.monitoring.enabled=false, так как хоть они и указаны как false, Elasticsearch пытается найти XPack и падает. Исправили ли этот баг в новых версиях не понятно, так что просто убираем их, или комментируем. Также стоит обратить внимание на секцию ulimits, так как она требуется для нормальной работы Elasticsearch в docker.<o:p>

Дальше уже поднимаются компоненты сервиса:

  1. Gateway

  2. Management API

  3. Management UI

  4. Portal UI

Gateway/APIM Gateway

docker-compose.yml:gatewaygateway:image: graviteeio/apim-gateway:${APIM_VERSION:-3}container_name: gio_apim_gatewayrestart: alwaysports:- "8082:8082"depends_on:- mongodb- elasticsearchvolumes:- ./logs/apim-gateway:/opt/graviteeio-gateway/logs      environment:    - gravitee_management_mongodb_uri=mongodb://mongodb:27017/gravitee?serverSelectionTimeoutMS=5000&connectTimeoutMS=5000&socketTimeoutMS=5000      - gravitee_ratelimit_mongodb_uri=mongodb://mongodb:27017/gravitee?serverSelectionTimeoutMS=5000&connectTimeoutMS=5000&socketTimeoutMS=5000- gravitee_reporters_elasticsearch_endpoints_0=http://elasticsearch:9200networks:- storage- frontend<o:p>

С Gateway всё несколько сложнее поднимается единственный экземпляр версии 3, если не указано иное. Если мы посмотрим, что лежит наhub.docker.com, то увидим, что у версий 3 и latest совпадают хеши. Дальше мы видим, что запуск данного сервиса, зависит от того, как будут работать сервисы MongoDB иElasticsearch. Самое интересное, что если Gateway запустился и забрал данные по настроенным API из mongodb, то дальше связь с mongodb и elasticsearch не обязательна. Только в логи будут ошибки сыпаться, но сам сервис будет работать и соединения обрабатывать согласно той версии настроек, которую последний раз закачал в себя Gateway. В секции environment можно указать параметры, которые будут использоваться в работе самого Gateway, для переписывания данных из главного файла настроек: gravitee.yml. Как вариант можно поставить теги, тенанты для разграничения пространств, если используется Open Distro for Elasticsearch вместо ванильного Elasticsearch. Например, так мы можем добавить теги, тенанты и включить подсистему вывода данных о работе шлюза.

environment:- gravitee_tags=service-tag #включаемтег: service-tag- gravitee_tenant=service-space #включаемтенант: service-space- gravitee_services_core_http_enabled=true # включаем сервис выдачи данных по работе Gateway  - gravitee_services_core_http_port=18082 # порт сервиса- gravitee_services_core_http_host=0.0.0.0 # адрес сервиса- gravitee_services_core_http_authentication_type=basic # аутентификация либо нет, либо basic - логин + пароль  - gravitee_services_core_http_authentication_type_users_admin=password #логин: admin,пароль: password  Чтобы к подсистеме мониторинга был доступ из вне, надо ещё открыть порт 18082.ports:- "18082:18082"Management API/APIM APIdocker-compose.yml:management_apimanagement_api:image: graviteeio/apim-management-api:${APIM_VERSION:-3}container_name: gio_apim_management_apirestart: alwaysports:- "8083:8083"links:- mongodb- elasticsearchdepends_on:- mongodb- elasticsearchvolumes:- ./logs/apim-management-api:/opt/graviteeio-management-api/logsenvironment:- gravitee_management_mongodb_uri=mongodb://mongodb:27017/gravitee?serverSelectionTimeoutMS=5000&connectTimeoutMS=5000&socketTimeoutMS=5000  - gravitee_analytics_elasticsearch_endpoints_0=http://elasticsearch:9200 networks:    - storage- frontend

ManagementAPI это ядро всей системы и предоставляет службы для управления и настройкиAPI, логи, аналитики и веб-интерфейсовManagementUIиPortalUI. Зависит от MongoDB и Elasticsearch. Также можно через секцию environment указать параметры, которые будут использоваться в работе самого ядра системы. Дополним наши настройки:

environment:- gravitee_email_enable=true # включаем возможность отправлять письма- gravitee_email_host=smtp.domain.example # указываем сервер через который будем отправлять письма- gravitee_email_port=25 # указываем порт для сервера- gravitee_email_username=domain.example/gravitee #логиндлясервера- gravitee_email_password=password #парольдлялогинаотсервера  - gravitee_email_from=noreply@domain.example # указываем от чьего имени будут письма- gravitee_email_subject="[Gravitee.io] %s" #указываемтемуписьма

Management UI/APIM Console

docker-compose.yml:apim_consolemanagement_ui:image: graviteeio/apim-management-ui:${APIM_VERSION:-3}container_name: gio_apim_management_uirestart: alwaysports:- "8084:8080"depends_on:- management_apienvironment:- MGMT_API_URL=http://localhost:8083/management/organizations/DEFAULT/environments/DEFAULT/      volumes:    - ./logs/apim-management-ui:/var/log/nginxnetworks:- frontend

Management UI предоставляет интерфейс для работы администраторам и разработчикам. Все основные функции можно осуществлять и выполняя запросы непосредственно к REST API. По опыту могу сказать, что в переменной MGMT_API_URL вместоlocalhostнадо указать IP адрес или название сервера, где вы это поднимаете, иначе контейнер не найдет Management API.

Portal UI/APIM Portaldocker-compose.yml:apim_portalportal_ui:image: graviteeio/apim-portal-ui:${APIM_VERSION:-3}container_name: gio_apim_portal_uirestart: alwaysports:- "8085:8080"depends_on:- management_apienvironment:- PORTAL_API_URL=http://localhost:8083/portal/environments/DEFAULTvolumes:- ./logs/apim-portal-ui:/var/log/nginxnetworks:- frontend

Portal UI это портал для менеджеров. Предоставляет доступ к логам, метрикам и документации по опубликованным API. По опыту могу сказать, что в переменной PORTAL_API_URL вместоlocalhostнадо указать IP-адрес или название сервера, где вы это поднимаете, иначе контейнер не найдет Management API.<o:p>

Теперь соберем весь файл вместе.

docker-compose.yml

# Copyright (C) 2015 The Gravitee team (<http://gravitee.io>)## Licensed under the Apache License, Version 2.0 (the "License");# you may not use this file except in compliance with the License.# You may obtain a copy of the License at##         <http://www.apache.org/licenses/LICENSE-2.0>## Unless required by applicable law or agreed to in writing, software# distributed under the License is distributed on an "AS IS" BASIS,# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.# See the License for the specific language governing permissions and# limitations under the License.#version: '3.5'networks:  frontend:    name: frontend  storage:    name: storagevolumes:  data-elasticsearch:  data-mongo:services:  mongodb:    image: mongo:${MONGODB_VERSION:-3.6}    container_name: gio_apim_mongodb    restart: always    volumes:      - data-mongo:/data/db      - ./logs/apim-mongodb:/var/log/mongodb    networks:      - storage  elasticsearch:    image: docker.elastic.co/elasticsearch/elasticsearch:${ELASTIC_VERSION:-7.7.0}    container_name: gio_apim_elasticsearch    restart: always    volumes:      - data-elasticsearch:/usr/share/elasticsearch/data    environment:      - http.host=0.0.0.0      - transport.host=0.0.0.0      - cluster.name=elasticsearch      - bootstrap.memory_lock=true      - discovery.type=single-node      - "ES_JAVA_OPTS=-Xms512m -Xmx512m"    ulimits:      memlock:        soft: -1        hard: -1      nofile: 65536    networks:      - storage  gateway:    image: graviteeio/apim-gateway:${APIM_VERSION:-3}    container_name: gio_apim_gateway    restart: always    ports:      - "8082:8082"      - "18082:18082"    depends_on:      - mongodb      - elasticsearch    volumes:      - ./logs/apim-gateway:/opt/graviteeio-gateway/logs    environment:      - gravitee_management_mongodb_uri=mongodb://mongodb:27017/gravitee?serverSelectionTimeoutMS=5000&connectTimeoutMS=5000&socketTimeoutMS=5000      - gravitee_ratelimit_mongodb_uri=mongodb://mongodb:27017/gravitee?serverSelectionTimeoutMS=5000&connectTimeoutMS=5000&socketTimeoutMS=5000      - gravitee_reporters_elasticsearch_endpoints_0=http://elasticsearch:9200         - gravitee_tags=service-tag # включаем тег: service-tag         - gravitee_tenant=service-space # включаем тенант: service-space         - gravitee_services_core_http_enabled=true # включаем сервис выдачи данных по работе Gateway         - gravitee_services_core_http_port=18082 # порт сервиса         - gravitee_services_core_http_host=0.0.0.0 # адрес сервиса          - gravitee_services_core_http_authentication_type=basic # аутентификация либо нет, либо basic - логин + пароль         - gravitee_services_core_http_authentication_type_users_admin=password # логин: admin, пароль: password    networks:      - storage      - frontend  management_api:    image: graviteeio/apim-management-api:${APIM_VERSION:-3}    container_name: gio_apim_management_api    restart: always    ports:      - "8083:8083"    links:      - mongodb      - elasticsearch    depends_on:      - mongodb      - elasticsearch    volumes:      - ./logs/apim-management-api:/opt/graviteeio-management-api/logs    environment:      - gravitee_management_mongodb_uri=mongodb://mongodb:27017/gravitee?serverSelectionTimeoutMS=5000&connectTimeoutMS=5000&socketTimeoutMS=5000      - gravitee_analytics_elasticsearch_endpoints_0=http://elasticsearch:9200         - gravitee_email_enable=true # включаем возможность отправлять письма         - gravitee_email_host=smtp.domain.example # указываем сервер через который будем отправлять письма         - gravitee_email_port=25 # указываем порт для сервера         - gravitee_email_username=domain.example/gravitee # логин для сервера         - gravitee_email_password=password # пароль для логина от сервера         - gravitee_email_from=noreply@domain.example # указываем от чьего имени будут письма          - gravitee_email_subject="[Gravitee.io] %s" # указываем тему письма    networks:      - storage      - frontend  management_ui:    image: graviteeio/apim-management-ui:${APIM_VERSION:-3}    container_name: gio_apim_management_ui    restart: always    ports:      - "8084:8080"    depends_on:      - management_api    environment:      - MGMT_API_URL=http://localhost:8083/management/organizations/DEFAULT/environments/DEFAULT/    volumes:      - ./logs/apim-management-ui:/var/log/nginx    networks:      - frontend  portal_ui:    image: graviteeio/apim-portal-ui:${APIM_VERSION:-3}    container_name: gio_apim_portal_ui    restart: always    ports:      - "8085:8080"    depends_on:      - management_api    environment:      - PORTAL_API_URL=http://localhost:8083/portal/environments/DEFAULT    volumes:      - ./logs/apim-portal-ui:/var/log/nginx    networks:      - frontend

Запускаем

Итоговый файл закидываем на сервер с примерно следующими характеристиками:

vCPU: 4

RAM: 4 GB

HDD: 50-100 GB

Для работы Elasticsearch, MongoDB и Gravitee Gateway нужно примерно по 0.5 vCPU, больше только лучше. Примерно тоже самое и с оперативной памятью - RAM. Остальные сервисы по остаточному принципу. Для хранения настроек много места не требуется, но учитывайте, что в MongoDB еще хранятся логи аудита системы. На начальном этапе это будет в пределах 100 MB. Остальное место потребуется для хранения логов в Elasticsearch.

docker-compose up -d # если не хотите видеть логиdocker-compose up # если хотите видеть логи и как это все работает

Как только в логах увидите строки:

gio_apim_management_api_dev | 19:57:12.615 [graviteeio-node] INFO  i.g.r.a.s.node.GraviteeApisNode - Gravitee.io - Rest APIs id[5728f320-ba2b-4a39-a8f3-20ba2bda39ac] version[3.5.3] pid[1] build[23#2f1cec123ad1fae2ef96f1242dddc0846592d222] jvm[AdoptOpenJDK/OpenJDK 64-Bit Server VM/11.0.10+9] started in 31512 ms.

Можно переходить по адресу: http://ваш_адрес:8084/.

Нужно учесть, что Elasticsearch может подниматься несколько дольше, поэтому не пугайтесь если увидите такое "приглашение":

Надо просто ещё немного подождать. Если ошибка не ушла, то надо закапываться в логи и смотреть, что там за ошибки. Видим такое приглашение отлично!

Вводим стандартный логин и пароль: admin/admin и мы в системе!

Первичная настройка

Настройки самой системы

Переходим в менюSettings PORTAL Settings

Здесь можно настроить некоторый параметры системы. Например: доступные методы аутентификации наших клиентов: Keyless, API_KEY, Oauth2 или JWT. Подробно их мы рассмотрим скорее всего в третьей статье, а пока оставим как есть. Можно подключить Google Analytics. Время обновления по задачам и нотификациям. Где лежит документация и много ещё чего по мелочи.

Добавление tags и tenant

ПереходимвменюSettings GATEWAY Shardings Tags

Здесь надо добавить теги, по которым у нас будут различаться шлюзы. Нажимаем "+" и добавляем тег и его описание.

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

ПереходимвменюSettings GATEWAY Tenants

То же самое и с настройкой тенантов. Только тут нет кнопки "+", но есть серенькая надпись "New tenant", которую надо нажать для добавления нового тенанта. Естественно, данный тенант должен быть создан в Open Distro for Elasticsearch, и к нему должны быть выданы права.

Добавление пользователей

Переходим вSettings USER MANAGEMENT Users

Здесь можно добавлять пользователей, вот только работать это будет, если у нас настроена рассылка по email. Иначе новым пользователям не придёт рассылка от системы со ссылкой на сброс пароля. Есть ещё один способ добавить пользователя, но это прям хардкод-хардкод!

В файле настроек Management API: gravitee.yml есть такой кусочек настроек:

security:  providers:  # authentication providers    - type: memory      # password encoding/hashing algorithm. One of:      # - bcrypt : passwords are hashed with bcrypt (supports only $2a$ algorithm)      # - none : passwords are not hashed/encrypted      # default value is bcrypt      password-encoding-algo: bcrypt      users:        - user:          username: admin          password: $2a$10$Ihk05VSds5rUSgMdsMVi9OKMIx2yUvMz7y9VP3rJmQeizZLrhLMyq          roles: ORGANIZATION:ADMIN,ENVIRONMENT:ADMIN

Здесьперечисленытипыхранилищдляпользователей: memory, graviteeиldap.Данные из хранилищаmemoryберутся из файла настроек:gravitee.yml. Данные из хранилищаgraviteeхранятся вMongoDB. Для хранения пользовательских паролей, по умолчанию используется тип хеширования BCrypt с алгоритмом $2a$. В представленных настройках мы видим пользователя: admin с хешированным паролем: admin и его роли. Если мы будем добавлять пользователей через UI, то пользователи будут храниться в MongoDB и тип их будет уже gravitee.

Создание групп пользователей

Переходим вSettings USER MANAGEMENT Groups

При нажатии на "+" получаем возможность добавить группу и пользователей в эту группу.

Проверка доступных шлюзов

При переходе в меню Gateways у нас отображаются все шлюзы, которые у нас есть в доступе.

Здесь мы видим настройки шлюза. В частности, Sharding tags и Tenant. Их мы добавили чуть раньше.

Естественно, есть возможность мониторинга состояния шлюза. Данные по мониторингу хранятся в Elasticsearch в отдельном индексе.

Публикация первого API

Для публикации первого API нам сначала потребуется сделать какой-нибудь backend с API.

BackEnd с API, балеринами и Swagger.

Возьмём FastAPI и сделаем простейшее backend с API.

#!/bin/env python3import uvicornfrom fastapi import FastAPIapp = FastAPI()@app.get('/')@app.get('/{name}')def read_root(name: str = None):    """    Hello world    :return: str = Hello world    """    if name:        return {"Hello": name}    return {"Hello": "World"}@app.get("/items/{item_id}")@app.post("/items/{item_id}")@app.put("/items/{item_id}")def gpp_item(item_id: str):    """    Get items    :param item_id: id    :return: dict    """    return {"item_id": item_id}if __name__ == "__main__":    uvicorn.run(app, host="0.0.0.0", port=8000)

Это иAPIможно назвать с трудом, но для примера работы вполне хватит.

Теперь запустим его! Можно даже на том же сервере.

python3 main.py

Теперь, если мы зайдем на этот серверhttp://backend_server:8000/,мы увидим приветствие миру! Если подставим своё имя, типа так:http://backend_server:8000/Anton, то приветствовать уже будут вас! Если же хотите познать всю мощьFastAPI, то сразу заходите на адрес:http://backend_server:8000/docsилиhttp://backend_server:8000/redoc. На этих страницах можно найти все методы, с которым работает данное API и также ссылку на swagger файл. Копируем URL до swagger файла.

В прошлый раз мы создавали наш план вручную. Было несколько утомительно. Сегодня мы сделаем все быстрее, через импорт swagger файла!

На главном экране Gravitee нажимаем большой "+", выбираем "IMPORT FROM LINK", вставляем URL и нажимаем "IMPORT".

Получается как-то так

Нажимаем "IMPORT"!

Почти полностью сформированный API! Теперь немного доработаем напильником...

Для начала нажимаем "START THE API" чтобы API запустить.

Переходим в "Plans" и нажимаем "+".

Создаем тестовый план.

Тип аутентификации выбираем Keyless (public) и нажимаем "NEXT".

Ограничения по количеству запросов и путям пропускаем. Нажимаем "NEXT".

Политики нам тоже пока не нужны. Нажимаем "SAVE".

План создан, но пока находиться в стадии "Staging"

Нажимаем на кнопку публикации плана - синее облачко со стрелочкой вверх! Подтверждаем кнопкой "PUBLISH"

И получаем опубликованный план и какую-то желтую полоску с призывом синхронизировать новую версию API.

Нажимаем "deploy your API" и подтверждаем наше желание "OK"

Переходим вAPIs Proxy Entrypoints

Здесь можно указать точки входа для нашего API и URL пути. У нас указан только путь "/fastapi". Можно переключиться в режим "virtual-hosts" и тогда нам будет доступен вариант с указанием конкретных серверов и IP. Это может быть актуально для серверов с несколькими сетевыми интерфейсами.

ВAPIs Proxy GENERAL CORSможнопроизвестинастройкиCross-origin resource sharing.

ВAPIs Proxy GENERAL Deploymentsнадоуказатьвсеsharding tags,которыебудутиспользоватьсяэтимAPI.

ВAPIs Proxy BACKEND SERVICES Endpointsможно указать дополнительные точки API и настроить параметры работы с ними.

Сейчас нас интересуют настройки конкретной Endpoint, поэтому нажимаем на нижнюю шестеренку.

Исправляем "Target" на http://backend_server:8000/, устанавливаем tenant, сохраняем и деплоим!

ВAPIs Proxy Deploymentsнадо указать те sharding tags, которые могут использовать данное API. После этого необходимо вернуться в созданный план и в списке Sharding tags выбрать тег "service-tag".

ВAPIs Designможно указать политики, которые будут отрабатывать при обработке запросов.

ВAPIs Analytics Overviewможно смотреть статистику по работе данного конкретного API.

ВAPIs Analytics Logsможно настроить логи и потом их смотреть.

ВAPIs Auditможно посмотреть, как изменялись настройки API.

Остальное пока рассматривать не будем, на работу оно не влияет.

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

Переходим наhttp://gravitee_host:8082/fastapi/ , и вам покажется приветствие миру:

Также сразу можно заглянуть вAPIs Analytics Overview/Logsдля просмотра статистики обработки запросов.

Заключение

Итак, поздравляю всех, кто дочитал до сюда и остался в живых! Теперь вы знаете, как поднять ознакомительный стенд APIM Gravitee, как его настроить, создать новое API из swagger файла и проверить, что всё работает. Вариантов настройки шлюзов, точек входа и выхода, сертификатов, балансировок нагрузки и записи логов много. В одной статье всего и не расскажешь. Так что в следующей статье я расскажу о более продвинутых настройках системы APIM Gravitee. В Телеграмме есть канал по данной системе:https://t.me/gravitee_ru, вопросы по нюансам настройки можно задавать туда.

Подробнее..

Manticore Search форк Sphinx отчёт за 3 года

08.02.2021 18:21:29 | Автор: admin

В мае 2017 мы, команда Manticore Software, сделали форк Sphinxsearch 2.3.2, который назвали Manticore Search. Ниже вы найдёте краткий отчёт о проделанной работе за три с половиной года, прошедших с момента форка.

Зачем был нужен форк?

Прежде всего, зачем мы сделали форк? В конце 2016 работа над Sphinxsearch была приостановлена. Пользователи, которые пользовались продуктом и поддерживали развитие проекта волновались, так как:

  • баги не фиксились

  • новые фичи, которые были давно обещаны не производились

  • коммуникация с командой Sphinx была затруднена.

По прошествии нескольких месяцев ситуация не поменялась и в середине июня группа инициативных опытных пользователей и клиентов поддержки Sphinx собрались, и было принято решение попытаться сохранить продукт в виде форка, под именем Manticore Search. Удалось собрать большую часть команды Sphinx (а именно тех, кто занимался непосредственно разработкой и поддержкой пользователей), привлечь инвестиции и в короткие сроки восстановить полноценную работу над проектом.

Чего мы хотели добиться?

Целей у форка было три:

  1. Поддержка кода в целом: багфиксинг, мелкие и крупные доработки

  2. Поддержка пользователей Sphinx и Manticore

  3. И, по возможности, более интенсивное развитие продукта, чем это было раньше. К большому сожалению, на момент форка Elasticsearch уже много лет как обогнал Sphinx по многим показателям. Такие вещи, как:

    1. отсутствие репликации

    2. отсутствие auto id

    3. отсутствие JSON интерфейса

    4. невозможность создать/удалить индекс на лету

    5. отсутствие хранилища документов

    6. слаборазвитые real-time индексы

    7. фокусировка на полнотекстовом поиске, а не поиске в целом

    делали Sphinx очень узкоспециализированным решением с необходимостью доработки его напильником во многих случаях. Многие пользователи к тому моменту уже мигрировали на Elasticsearch. Это было очень досадно, так так фундаментальные структуры данных и алгоритмы в Sphinx потенциально и фактически во многих случаях превосходили Elasticsearch по производительности, а SQL, который в Sphinx был развит намного лучше, чем в Elasticsearch даже сейчас, был привлекателен для многих.

Кроме поддержки имеющихся пользователей Sphinx, глобальной целью Manticore была доработка вышеупомянутых и других характеристик, которые позволили бы сделать Manticore Search реальной альтернативой Elasticsearch в большинстве сценариев использования.

Что нам удалось сделать?

Намного более активная разработка

Если посмотреть на статистику гитхаба, то видно, что как только случился форк (середина 2017-го) разработка активизировалась:

За три с половиной года мы выпустили 39 релизов. В 2021 году выпускались примерно каждые два месяца. Так и планируем продолжать.

Репликация

Многие пользователи ждали репликации в Sphinx много лет. Одной из первых крупных фичей, которые мы сделали в Manticore была репликация. Как и всё в Мантикоре, попытались сделать её максимально простой в использовании. Например, чтобы подключиться к кластеру достаточно выполнить команду:

JOIN CLUSTER posts at '127.0.0.1:9312';

, после чего в текущем индексе появятся индексы из кластера. Репликация:

  • синхронная

  • основана на библиотеке Galera, которую так же используют MariaDB и Percona XtraDB.

Auto id

Без auto-id Sphinx / Manticore за исключением редких случаев можно использовать только как приложение к внешнему хранилищу документов и источнику ID. Мы реализовали auto-id на основе алгоритма UUID_SHORT. Гарантируется уникальность до 16 миллионов инсертов в секунду на сервер.

mysql> create table idx(doc text);Query OK, 0 rows affected (0.01 sec)mysql> insert into idx(doc) values('abc def');Query OK, 1 row affected (0.00 sec)mysql> select * from idx;+---------------------+---------+| id                  | doc     |+---------------------+---------+| 1514145039905718278 | abc def |+---------------------+---------+1 row in set (0.00 sec)mysql> insert into idx(doc) values('def ghi');Query OK, 1 row affected (0.00 sec)mysql> select * from idx;+---------------------+---------+| id                  | doc     |+---------------------+---------+| 1514145039905718278 | abc def || 1514145039905718279 | def ghi |+---------------------+---------+2 rows in set (0.00 sec)

Хранилище документов

В Sphinx 2.3.2 и ранее хранить исходные тексты можно было только в строковых атрибутах. Они, как и все атрибуты требуют памяти. Многие пользователи так и делали, расходуя лишнюю память, что на больших объёмах было дорого и чревато другими проблемами. В Manticore мы сделали новый тип данных text, который объединяет полнотекстовую индексацию и хранение на диске с отложенным чтением (т.е. значение достаётся на самой поздней стадии выполнения запроса). По "stored" полям нельзя фильтровать, сортировать, группировать. Они просто лежат на диске в сжатом виде, чтоб не было нужды хранить их в mysql/hbase и прочих БД, когда это совсем не нужно. Фича оказалась очень востребованной. Теперь Manticore не требует ничего, кроме самой себя для реализации поискового приложения.

mysql> desc idx;+-------+--------+----------------+| Field | Type   | Properties     |+-------+--------+----------------+| id    | bigint |                || doc   | text   | indexed stored |+-------+--------+----------------+2 rows in set (0.00 sec) 

Real-time индексы

В Sphinx 2.3.2 и ранее многие пользователи опасались использовать real-time индексы, т.к. это часто приводило к крэшам и другим побочным эффектам. Большую часть известных багов и недоработок мы устранили, над некоторыми оптимизациями ещё работаем (в основном связанным с автоматически OPTIMIZE). Но можно с уверенностью сказать, что real-time индексы можно использовать в проде, что многие пользователи и делают. Из наиболее заметных фичей добавили:

  • многопоточность: поиск в нескольких дисковых чанках одного real-time индекса делается параллельно

  • оптимизации OPTIMIZE: по умолчанию чанки мерджатся не до одного, а до кол-ва ядер на сервере * 2 (регулируется через опцию cutoff)

Работаем над автоматическим OPTIMIZE.

charset_table = cjk, non_cjk

Раньше, если требовалась поддержка какого-то языка кроме русского и английского часто нужно было поддерживать длинные массивы в charset_table. Это было неудобно. Мы попытались это упростить, собрав всё, что может понадобиться во внутренние массивы с алиасами non_cjk (для большинства языков) и cjk (для китайского, корейского и японского). И сделали non_cjk настройкой по умолчанию. Теперь можно без проблем искать по русскому, английскому и, скажем, турецкому:

mysql> create table idx(doc text);Query OK, 0 rows affected (0.01 sec)mysql> insert into idx(doc) values('abc абв renim');Query OK, 1 row affected (0.00 sec)mysql> select * from idx where match('abc');+---------------------+----------------------+| id                  | doc                  |+---------------------+----------------------+| 1514145039905718280 | abc абв renim      |+---------------------+----------------------+1 row in set (0.00 sec)mysql> select * from idx where match('абв');+---------------------+----------------------+| id                  | doc                  |+---------------------+----------------------+| 1514145039905718280 | abc абв renim      |+---------------------+----------------------+1 row in set (0.00 sec)mysql> select * from idx where match('ogrenim');+---------------------+----------------------+| id                  | doc                  |+---------------------+----------------------+| 1514145039905718280 | abc абв renim      |+---------------------+----------------------+1 row in set (0.00 sec)

Официальный Docker image

Выпустили и поддерживаем официальный docker image . Запустить Manticore теперь можно за несколько секунд где угодно, если там есть докер:

  ~ docker run --name manticore --rm -d manticoresearch/manticore && docker exec -it manticore mysql && docker stop manticore525aa92aa0bcef3e6f745ddeb11fc95040858d19cde4c9118b47f0f414324a79mysql> create table idx(f text);mysql> desc idx;+-------+--------+----------------+| Field | Type   | Properties     |+-------+--------+----------------+| id    | bigint |                || f     | text   | indexed stored |+-------+--------+----------------+

Кроме того, в manticore:dev всегда лежит самая свежая development версия.

Репозиторий для пакетов

Релизы и свежие development версии автоматически попадают в https://repo.manticoresearch.com/

Оттуда же можно легко установить Manticore через YUM и APT. Homebrew тоже поддерживаем, как и сборку под Мак в целом.

NLP: обработка естественного языка

По NLP сделали такие улучшения:

  • сегментация китайского с помощью библиотеки ICU

  • стоп-слова для большинства языков из коробки:

    mysql> create table idx(doc text) stopwords='ru';Query OK, 0 rows affected (0.01 sec)mysql> call keywords('кто куда пошёл - я не знаю', 'idx');+------+------------+------------+| qpos | tokenized  | normalized |+------+------------+------------+| 3    | пошел      | пошел      || 6    | знаю       | знаю       |+------+------------+------------+2 rows in set (0.00 sec)
    
  • поддержка Snowball 2.0 для стемминга большего количества языков

  • более лёгкая в плане синтаксиса подсветка:

    mysql> insert into idx(doc) values('кто куда пошёл - я не знаю');Query OK, 1 row affected (0.00 sec)mysql> select highlight() from idx where match('пошёл');+------------------------------------------------------+| highlight()                                          |+------------------------------------------------------+| кто куда <b>пошёл</b> - я не знаю                    |+------------------------------------------------------+1 row in set (0.00 sec)
    

Новая многозадачность

Упростили многозадачность за счёт использования корутин. Кроме того, что код стал намного проще и надёжнее, для улучшения распараллеливания теперь не нужно выставлять dist_threads и думать какое значение поставить какому индексу, чтобы поиск по нему максимально оптимально распараллеливался. Теперь есть глобальная настройка threads, которая по умолчанию равна количеству ядер на сервере. В большинстве случаев для оптимальной работы вообще трогать ничего не нужно.

Поддержка OR в WHERE

В Sphinx 2/3 нельзя легко фильтровать по атрибутам через ИЛИ. Это неудобно. Сделали такую возможность в Manticore:

mysql> select i, s from t where i = 1 or s = 'abc';+------+------+| i    | s    |+------+------+|    1 | abc  ||    1 | def  ||    2 | abc  |+------+------+3 rows in set (0.00 sec)Sphinx 3:mysql> select * from t where i = 1 or s = 'abc';ERROR 1064 (42000): sphinxql: syntax error, unexpected OR, expecting $end near 'or s = 'abc''

Поддержка протокола JSON через HTTP

SQL - это круто. Мы любим SQL. И в Sphinx / Manticore всё, что касается синтаксиса запросов можно сделать через SQL. Но бывают ситуации, когда оптимальным решением является использование JSON-интерфейса, как, например, в Elasticsearch. SQL крут для дизайна запроса, а используя JSON сложный запрос часто легче интегрировать с приложением.

Кроме того, HTTP сам по себе позволяет делать интересные вещи: использовать внешние HTTP load balancer'ы, proxy, что позволяет довольно-таки несложно реализовать аутентификацию, RBAC и прочее.

Новые клиенты для большего количества языков

Ещё легче может быть использовать клиент для конкретного языка программирования. Мы реализовали новые клиенты для php, python, java, javascript, elixir, go. Большинство из них основаны на новом JSON-интерфейсе и их код генерируется автоматически, что позволяет добавлять новые фичи в клиенты намного быстрее.

Поддержка HTTPS

Сделали поддержку HTTPS из коробки. Светить Мантикорой наружу всё ещё не стоит, т.к. встроенных средств аутентификации нет, но гонять трафик от клиента к серверу по локальной сети теперь можно безопасно. SSL для mysql-интерфейса тоже поддерживается.

Поддержка FEDERATED

Вместо SphinxSE (встроенный в mysql движок, который позволяет теснее интегрировать Sphinx/Manticore с mysql) теперь можно использовать FEDERATED.

Поддержка ProxySQL

И ProxySQL тоже можно.

RT mode

Одним из главных изменений стало то, что мы сделали императивный (т.е. через CREATE/ALTER/DROP table) способ работы со схемой данных Manticore дефолтным. Как видно из примеров выше - о конфиге, source, index и прочем речи теперь в большинстве случаев не идёт. С индексами можно полноценно работать без необходимости править конфиг, рестартить инстанс, удалять файлы real-time индексов и т.д. Схема данных теперь отделена от настроек сервера полностью. И это режим по-умолчанию.

Но декларативный режим всё так же поддерживается. Мы не считаем его рудиментом и не планируем от него избавляться. Как с Kubernetes можно общаться и через yaml-файлы и через конкретные команды, так и c Manticore:

  • можно описать всё в конфиге и пользоваться возможностью лёгкого его портирования и т.д.,

  • а можно создавать индексы на лету

Смешивать использование режимов нельзя и не планируется. Чтобы как-то обозначить режимы, мы назвали декларативный режим (как в Sphinx 2) plain mode, а императивный режим RT mode (real-time mode).

Percolate index

Решили задачу обратного поиска, когда в индексе лежат запросы, а документы поступают в запросах. Работает шустро по сравнению с эластиком.

mysql> create table t(f text, j json) type='percolate';mysql> insert into t(query,filters) values('abc', 'j.a=1');mysql> call pq('t', '[{"f": "abc def", "j": {"a": 1}}, {"f": "abc ghi"}, {"j": {"a": 1}}]', 1 as query);+---------------------+-------+------+---------+| id                  | query | tags | filters |+---------------------+-------+------+---------+| 8215503050178035714 | abc   |      | j.a=1   |+---------------------+-------+------+---------+

Новая удобная документация

Полностью переработали документацию - https://manual.manticoresearch.com

Для ключевой функциональности есть примеры для большинства поддерживаемых клиентов. Поиск работает так же через Manticore. Удобная умная подсветка в результатах поиска. HTTP примеры можно копировать одним кликом прямо в в виде команды curl с параметрами. Кроме того, специально зарегистрировали короткий домен mnt.cr, чтоб можно было очень быстро найти информацию по какой-то настройке, детали по которой подзабылись, например: mnt.cr/proximity , mnt.cr/quorum, mnt.cr/percolate

Интерактивные курсы

Сделали платформу для интерактивных курсов https://play.manticoresearch.com и, собственно, сами курсы, с помощью которых можно ознакомиться с работой Manticore прямо из браузера, вообще ничего не устанавливая. Проще уже, по-моему, некуда, хотя мы стараемся всё сделать максимально просто для пользователя.

Github в качестве bug tracker'а

В качестве публичного bug/task tracker'а используем Гитхаб.

Есть же Sphinx 3

В декабре 2017 года был выпущен Sphinx 3.0.1. До октября 2018 было сделано ещё три релиза и ещё один в июле 2020-го (3.3.1, последняя версия на момент февраля 2021). Появилось много интересных фич, в том числе вторичные индексы и интеграция с machine learning, которая позволяет решать некоторые задачи, требующие подходов машинного обучения. В чём же проблема? На кой вообще нужен этот ваш Manticore? Одной из причин является то, что к сожалению, ни первый релиз Sphinx 3, ни последний на текущий момент не open source:

  • в том смысле, что код Sphinx 3 недоступен

  • а также в более широком понимании open source. Cказано, что с версии 3.0 Sphinx теперь доступен под лицензией "delayed FOSS". Что именно это за лицензия и где можно с ней ознакомиться не разглашается. Непонятно:

    • то ли оно всё ещё GPLv2 (т.е. под "delayed FOSS" подразумевается отложенный GPLv2), т.к. код вероятно основан на Sphinx 2, который GPLv2 (как и Manticore). Но где тогда исходный код?

    • то ли нет, т.к. никакой лицензии к бинарным файлам не прилагается. И основан ли код вообще на Sphinx 2, который GPLv2? И действуют ли ограничения GPLv2? Или можно уже распространять исполняемые файлы sphinx 3 без оглядки на GPL? "delayed FOSS" за отсутствием какого-либо текста лицензии это ведь не запрещает.

  • релизов не было с июля 2020. Багов много. Когда они будут фикситься?

Вопросов много. Ответов нет. Всё это делает крайне затруднительным использование Sphinx 3 людьми и компаниями, которым важна юридическая сторона вопроса и стабильность проекта. Не все имеют возможность вкладывать время своих сотрудников во что-то настолько непонятное.

В общем и целом, Sphinx 3 сейчас можно воспринимать как проприетарное решение для ограниченного круга пользователей с ограниченными целями. Очень жаль, что open source мир потерял Sphinx. Можно только надеяться, что в будущем что-то поменяется.

Побенчим?

Дано:

  • датасет, состоящий из 1M+ комментов с HackerNews с численными атрибутами

  • plain index из этого датасета. Размер файлов индекса около 1 гигабайта

  • набор разнообразных запросов (132 запроса) от различных вариантов полнотекстового поиска до фильтрации и группировки по атрибутам

  • запуск в докере в равнозначных условиях на пустой железной машине с различными ограничениями по памяти (через cgroups)

  • запрос через SQL протокол клиентом mysqli из PHP-скрипта

  • перед каждым запросом перезапуск докеров с предварительной очисткой всех кэшей, далее 5 попыток, в статистику попадает самая быстрая

Результаты:

Лимит памяти 100 мегабайт:

500 мегабайт:

1000 мегабайт:

Будущее Manticore Search

Итак, у нас уже есть активно разрабатываемый продукт под всем понятной open source лицензией GPLv2 с репликацией, auto-id, нормально работающими real-time индексами, JSON интерфейсом, адекватной документацией, курсами и многим ещё чем. Что дальше? Роадмэп у нас такой:

Новый Manticore engine

С начала 2020 года мы работаем над библиотекой хранения и обработки данных в поколоночном виде с индексацией по умолчанию (в отличие от, скажем, clickhouse). Для пользователей Manticore / Sphinx это решит проблемы:

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

  • не всегда быстрой группировки

У нас уже готова альфа-версия и вот, например, какие получаются результаты, если использовать её в Manticore Search и сравнить её производительность на том же датасете с Elasticsearch (исключая полнотекстовые запросы, т.е. в основном группировочные запросы):

Но работы всё ещё много. Библиотека будет доступна под permissive open source лицензией и её можно будет использовать как в Manticore Search, так и в других проектах (может быть и в Sphinx).

Auto OPTIMIZE

Доделываем, надеемся выложить в феврале.

Интеграция с Kibana

Так как с новым Manticore engine можно делать больше аналитики, то встаёт вопрос как это лучше визуализировать. Grafana - круто, но для полнотекста надо что-то придумывать. Kibana - тоже норм, многие знают и используют. У нас готова альфа версия интеграции Manticore с Кибаной. Работать будет прямо из коробки. Отлаживаем.

Интеграция с Logstash

JSON протокол у нас уже есть. В планах немного доработать его методы PUT и POST до совместимости с Elasticsearch в плане INSERT/REPLACE запросов. Кроме того, планируем разработку создания индекса на лету на основе первых вставленных в него документов.

Всё это позволит писать данные в Manticore Search вместо Elasticsearch из Logstash, Fluentd, Beats и им подобных.

Автоматическое шардирование

Проектируем. Уже понимаем с какими сложностями придётся столкнуться и более-менее как их решать. Планируем на второй квартал.

Альтернатива Logstash?

Logstash требует от пользователя потратить много времени для того, чтобы начать обрабатывать новый кастомный тип лога вашего приложения. А если добавите какую-нибудь строчку, то правила парсинга придётся поменять. Мы разрабатываем систему автоматического парсинга логов, которая позволяет разбить лог на составляющие с достаточной точностью для того, чтобы использовать полученные структурированные документы для фильтрации по полям, сортировки, группировки, а не только полнотекстового поиска по логам. Альфа-версия тоже имеется и в принципе задачу решает, но работы по окультуриванию её предстоит ещё много.


Если вам небезразличен проект, присоединяйтесь:

Будем рады любой критике. Оревуар!

Подробнее..

Организация сбора и парсинга логов при помощи Filebeat

02.04.2021 12:23:26 | Автор: admin

В комментариях к моему туториалу, рассказывающему о парсинге логов с помощью Fluent-bit, было приведено две альтернативы: Filebeat и Vector. Этот туториал рассказывает как организовать сбор и парсинг лог-сообщений при помощи Filebeat.


Цель туториала: Организовать сбор и парсинг лог-сообщений с помощью Filebeat.


Дисклеймер: Туториал не содержит готовых для продакшена решений, он написан с целью помочь тем кто только начинает разбираться с filebeat и для закрепления изученного материала автором. Так же в туториале не проводиться сравнение доставщиков логов. Сравнение можно найти здесь.


Кому данная тема интересна, прошу под кат:)


Тестовое приложение будем запускать с помощью docker-compose.


Общая информация


Filebeat это легковесный доставщик лог-сообщений. Принцип его работы состоит в мониторинге и сборе лог-сообщений из лог-файлов и пересылки их в elasticsearch или logstash для индексирования.


Filebeat состоит из ключевых компонентов:


  • сборщики (harvesters) отвечают за чтение лог-файлов и отправку лог-сообщений в заданный выходной интерфейс, на каждый лог-файл задается отдельный сборщик;
  • входные интерфейсы (inputs) отвечают за поиск источников лог-сообщений и управление сборщиками.

Подробнее о принципе работы можно почитать в официальном руководстве.


Организация сбора лог-сообщений


Filebeat имеет множество входных интерфейсов для различных источников лог-сообщений. В рамках туториала предлагаю двигаться от настройки сбора вручную до автоматического поиска источников лог-сообщений в контейнерах. По моему мнению данный подход позволит глубже понять filebeat, к тому же я сам двигался тем же путем.


Нам необходим сервис, лог-сообщения которого будем пересылать на хранение.
В качестве такого сервиса возьмем простое приложение, написанное с помощью FastAPI, единственной целью которого является генерация лог-сообщений.


Сбор лог-сообщений с помощью volume


Для начала клонируем репозиторий. В нем находится тестовое приложение, конфигурационный файл Filebeat и docker-compose.yml.
Настройка сбора лог-сообщений с помощью volume состоит из следующих шагов:


  1. Настройка логгера приложения на запись лог-сообщений в файл:
    app/api/main.py
    logger.add(    "./logs/file.log",    format="app-log - {level} - {message}",    rotation="500 MB")
    
  2. Создание volume для хранения лог-файлов вне контейнеров:
    docker-compose.yml


    version: "3.8"services:  app:    ...    volumes:      # создаем volume, для хранения лог-файлов вне контейнера      - app-logs:/logs  log-shipper:    ...    volumes:      # заменяем конфигурационный файл в контейнере      - ./filebeat.docker.yml:/usr/share/filebeat/filebeat.yml:ro      # подключаем volume с лог-файлами в контейнер      - app-logs:/var/app/logvolumes:  app-logs:
    

  3. Определение входного и выходного интерфейсов filebeat:
    filebeat.docker.yml


    filebeat.inputs:- type: log  # Определяем путь к лог-файлам  paths:    - /var/app/log/*.log# Пока будем выводить лог-сообщения в консольoutput.console:  pretty: true
    

    Запускаем тестовое приложение, генерируем лог-сообщения и получаем их в следующем формате:


    {"@timestamp": "2021-04-01T04:02:28.138Z","@metadata": {"beat": "filebeat","type": "_doc","version": "7.12.0"},"ecs": {"version": "1.8.0"},"host": {"name": "aa9718a27eb9"},"message": "app-log - ERROR - [Item not found] - 1","log": {"offset": 377,"file": {  "path": "/var/app/log/file.log"}},"input": {"type": "log"},"agent": {"version": "7.12.0","hostname": "aa9718a27eb9","ephemeral_id": "df245ed5-bd04-4eca-8b89-bd0c61169283","id": "35333344-c3cc-44bf-a4d6-3a7315c328eb","name": "aa9718a27eb9","type": "filebeat"}}
    


Сбор лог-сообщений с помощью входного интерфейса container


Сontainer позволяет осуществлять сбор лог-сообщений с лог-файлов контейнеров.
Настройка сбора лог-сообщений с помощью входного интерфейса container состоит из следующих шагов:


  1. Удаление настроек входного интерфейса log, добавленного на предыдущем этапе, из конфигурационного файла.
  2. Определение входного интерфейса container в конфигурационном файле:
    filebeat.docker.yml


    filebeat.inputs:- type: container  # путь к лог-файлам контейнеров  paths:    - '/var/lib/docker/containers/*/*.log'# Пока будем выводить лог-сообщения в консольoutput.console:  pretty: true
    

  3. Отключаем volume app-logs из сервисов app и log-shipper и удаляем его, он нам больше не понадобиться.
  4. Подключаем к сервису log-shipper лог-файлы контейнеров и сокет докера:
    docker-compose.yml


    version: "3.8"services:  app:    ...  log-shipper:    ...    volumes:      # заменяем конфигурационный файл в контейнере      - ./filebeat.docker.yml:/usr/share/filebeat/filebeat.yml:ro      - /var/lib/docker/containers:/var/lib/docker/containers:ro      - /var/run/docker.sock:/var/run/docker.sock:ro
    

  5. Настройка логгера приложения на запись лог-сообщений в стандартный вывод:
    app/api/main.py
    logger.add(    sys.stdout,    format="app-log - {level} - {message}",)
    

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


Сбор лог-сообщений с помощью автообнаружения


При сборе лог-сообщений контейнеров, могут возникнуть трудности, так как контейнеры могут перезапускаться, удаляться и т.д. В связи с этим в filebeat есть автообнаружение контейнеров, с возможностью определения настроек сбора лог-сообщений для каждого обнаруженного контейнера. Механизм автообнаружения состоит из двух частей:


  • шаблона поиска контейнера;
  • конфигурации сбора лог-сообщений.

Подробнее можно почитать здесь.


Настройка состоит из следующих шагов:


  1. Удаление настроек входного интерфейса container, добавленного на предыдущем этапе, из конфигурационного файла.
  2. Определение настроек автообнаружение в конфигурационном файле:
    filebeat.docker.yml


    filebeat.autodiscover:  providers:    # искать docker контейнер    - type: docker      templates:        - condition:            contains:              # имя которого fastapi_app              docker.container.name: fastapi_app          # определим конфигурацию сбора для этого контейнера          config:            - type: container              paths:                - /var/lib/docker/containers/${data.docker.container.id}/*.log              # исключим лог-сообщения asgi-сервера              exclude_lines: ["^INFO:"]# Пока будем выводить лог-сообщения в консольoutput.console:  pretty: true
    


Вот и все. Теперь filebeat будет собирать лог-сообщения только с указанного контейнера.


Сбор лог-сообщений с использованием подсказок (hints)


Filebeat поддерживает автообнаружение на основе подсказок.
Он ищет информацию (подсказки) о конфигурации сбора в лейблах контейнера.
Как только контейнер запустится, Filebeat проверит, содержит ли он какие-либо подсказки, и запустит для него сбор с правильной конфигурацией.


Подробнее можно почитать здесь.


Настройка сбора состоит из следующих шагов:


  1. Удаляем шаблон обнаружения сервиса app и включаем подсказки:
    filebeat.docker.yml


    filebeat.autodiscover:  providers:    - type: docker      hints.enabled: true# Пока будем выводить лог-сообщения в консольoutput.console:  pretty: true
    

  2. Отключаем сбор лог-сообщения для сервиса log-shipper:
    docker-compose.yml


    version: "3.8"services:  app:    ...  log-shipper:    ...    labels:      co.elastic.logs/enabled: "false"
    


Парсинг лог-сообщений


Для обработки лог-сообщений в Filebeat есть большое количество обработчиков (processors).
Их можно подключить с помощью лейблов контейнеров либо определить в конфигурационном файле.
Воспользуемся вторым способом.


  1. Для начала очистим лог-сообщения от метаданных. Для этого в конфигурационный файл добавим обработчик drop_fields:
    filebeat.docker.yml


    processors:  - drop_fields:      fields: ["agent", "container", "ecs", "log", "input", "docker", "host"]      ignore_missing: true
    

    Теперь лог-сообщение выглядит следующим образом:


    {  "@timestamp": "2021-04-01T04:02:28.138Z",  "@metadata": {    "beat": "filebeat",    "type": "_doc",    "version": "7.12.0"  },  "message": "app-log - ERROR - [Item not found] - 1",  "stream": ["stdout"]}
    

  2. Для отделения лог-сообщений API от лог-сообщений asgi-сервера, добавим к ним тег с помощью обработчика add_tags:
    filebeat.docker.yml


    processors:  - drop_fields:      ...  - add_tags:    when:      contains:        "message": "app-log"    tags: [test-app]    target: "environment"
    

  3. Структурируем поле message лог-сообщения с помощью обработчика dissect и удалим его с помощью drop_fields:
    filebeat.docker.yml


    processors:  - drop_fields:    ...  - add_tags:    ... - dissect:     when:       contains:         "message": "app-log"     tokenizer: 'app-log - %{log-level} - [%{event.name}] - %{event.message}'     field: "message"     target_prefix: "" - drop_fields:     when:       contains:         "message": "app-log"     fields: ["message"]     ignore_missing: true
    

    Теперь лог-сообщение выглядит следующим образом:


    {  "@timestamp": "2021-04-02T08:29:07.349Z",  "@metadata": {    "beat": "filebeat",    "type": "_doc",    "version": "7.12.0"  },  "log-level": "ERROR",  "event": {    "name": "Item not found",    "message": "Foo"  },  "environment": [    "test-app"  ],  "stream": "stdout"}
    


Дополнение


Filebeat так же имеет готовые решения сбора и парсинга лог-сообщений для широко используемых инструментов таких, как Nginx, Postgres и т.д.
Они называются модулями.
К примеру для сбора лог-сообщений Nginx, достаточно добавить к его контейнеру лейбл:


  co.elastic.logs/module: "nginx"

и включить подсказки в конфигурационном файле. После этого мы получим готовое решение для сбора и парсинга лог-сообщений + удобный dashboard в Kibana.


Всем спасибо за внимание!

Подробнее..
Категории: Devops , Elasticsearch , Filebeat

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru