Deep Anomaly Detection

Детекция аномалий с помощью методов глубокого обучения

Выявление аномалий (или выбросов) в данных — задача, интересующая ученых и инженеров из разных областей науки и технологий. Хотя выявлением аномалий (объектов, подозрительно не похожих на основной массив данных) занимаются уже давно и первые алгоритмы были разработаны еще в 60-ых годах прошлого столетия, в этой области остается много неразрешенных вопросов и проблем, с которыми сталкиваются люди в таких сферах, как консалтинг, банковский скоринг, защита информации, финансовые операции и здравоохранение.  В связи с бурным развитием алгоритмов глубоко обучения за последние несколько лет было предложено много современных подходов к решению данной проблемы для различных видов исследуемых данных, будь то изображения, записи с камер видеонаблюдений, табличные данные (о финансовых операциях) и др.

Глубокое обучение для детекции аномалий — Deep Anomaly Detection (DAD) — позволяет разрешить следующий ряд ограничений:

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

  • Гетерогенность разных классов объектов: непохожесть может быть разной

  • Редкость появление аномалий: имбаланс классов в обучении

  • Различные типы аномалий: одиночные объекты, обычные объекты, но в аномальных условиях, группы объектов (слишком плотный граф фейк-аккаунтов социальной сети)

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

  • Низкие значения precision метрики в задаче классификации на аномальный / нормальный (частое ложное срабатывание алгоритмов на нормальных данных)

  • Проблема больших размерностей данных

  • Отсутствие или недостаток размеченных данных

  • Неустойчивость алгоритмов к зашумленным объектам

  • Детекция аномалий целой группы объектов

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

Категоризация подходов

В своей недавней статье [2] G. Pang приводит следующую классификацию существующих подходов для решения поставленной задачи.

рис. 1
рис. 1

Автор разбивает все алгоритмы на три большие группы:

Deep learning for feature extraction — по сути раздельная задача получения нового домена признаков меньшего размера, чем исходный (при этом можно будет использовать предобученные модели из других задач глубокого обучения), и решение задачи классификации уже на данных нового домена с помощью классических методов детекции аномалий. Тут две части решения никак не связаны между собой и только первую часть можно отнести к DAD. 
На рис.2 схематично показан пайплайн данного подхода. Сначала мы с помощью нейронной сети φ(·) : X→ Z переводим исходное признаковое пространство в пространство низкой размерности Z, а потом независимо делаем скоринг наличия аномалий классическими методами.

рис. 2. Deep learning for feature extraction
рис. 2. Deep learning for feature extraction

Learning feature representation of normality — теперь нейросеть φ(·) : X→ Z является не независимым экстрактором новых признаков, а обучается вместе с скоринговой системой аномалий, то есть пространство Z будет формироваться с оглядкой на поставленную в конечном итоге задачу.

рис. 3. Learning feature representation of normality
рис. 3. Learning feature representation of normality

End-to-end anomaly score learning — end-to-end пайплайн, где нейронная сеть будет сразу предсказывать anomaly score.

рис. 4. End-to-end anomaly score learning
рис. 4. End-to-end anomaly score learning

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

Deep learning for feature extraction

Как уже отмечалось в этом подходе мы попытаемся уменьшить размерность входных данных. Можно было бы использовать стандартный методы, такие как PCA (principal component analysis) [3] или random projection [4], но для детекции аномалий необходимо сохранить семантическую составляющую каждого объекта, с чем отлично справятся нейронные сети. И в зависимости от типа данных можем выбрать предобученные модели MLP, если имеем дело с табличный данными, СNNs для работы с изображениями, RNNs для предобработки последовательных данных (видеоряд).
При этом основным плюсом такого подхода будет наличие предобученных моделей для почти любого типа данных, но при этом для нахождении anomaly score полученное признаковое пространство может быть далеко не оптимальным.

Learning feature representation of normality

Как видно из рис.1 этот тип можно разделить на два подтипа.

Generic Normality Feature Learning. Подходы в этом классе алгоритмов при преобразовании исходных признаков всё ещё используют функцию потерь не для детекции аномалий напрямую. Но генеративные модели здесь позволяют вычленить ключевую информацию о структуре объектов, тем самым способствуя самой детекции выбросов.

Математическое описание семейства алгоритмов

где ψ  — нейронная сеть для выделения структурной информации объекта, l — функция потерь, соответствующая выбранным ψ, φ (конкретному подходу генеративных моделей), f — скоринговая функция аномалий.

 Рассмотрим конкретные виды архитектур:

Автоэнкодеры
Для задачи DAD главная идея заключается в том, что нормальные данные будут легко реконструированы автоэнкодером, тогда как аномальный объект для модели будет восстановить сложно. [5]

Подробнее об алгоритме

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

где φ_e (.)энкодер, преобразующий объекты в промежуточное пространство низкой размерностиφ_d (.)  — декодер, пытающийся восстановить исходный объект. При этом s_x (data reconstruction error) будет играть роль меры аномальности.

Generative Adversarial Networks

Опредение подхода

GANs — это алгоритм машинного обучения без учителя, построенный на комбинации двух нейросетей с противоположными целями, где первая сеть (генератор G) генерирует образы, а другая (дискриминатор D) старается отличить правильные образы от неправильных.

здесь G и D — генератор и дискриминатор соответственно.

В конкретной задаче DAD генератор будет стараться найти в обученном латентном пространстве вектор, соответветствующий поступающему в него объекту. При этом делается предположение, что так как аномальный объект придет из другого распределения, система быстро различит выброс. AnoGAN [6].

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

Формульное описание

x̂_(t +1) = ψ (φ (x1 , x2 , · · · , xt ; Θ); W),
l_pred и l_adv — предсказательный и адверсариал лоссы, выступающие здесь в роли меры аномальности.

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

Self-supervised Classification. Основная идея данного подхода в том, что на признаках исходных объектов решается задачи классификации (где новые признаки —  это (n — 1) фичей, целевая переменная — оставшийся признак, и так для каждого признака). Тогда пришедший в обученную модель аномальный объект не будет подходить к построенным таким образом классификаторам. Заметим, что для такой постановки задачи размеченные данные будут и вовсе не обязательны.

Anomaly Measure-dependent Feature Learning.

Теперь будем обучать модель φ(·) : X→ Z, оптимизируясь уже на специальную функцию теперь аномальности.

Математическое описание

здесь l — функция потерь специальная для детекции аномалий.

Конкретные подходы в этом семействе:

  • Distance-based Measure. Оптимизируется конкретно по мерам, связанным с расстояниями: DB outliers [8], k-nearest neighbor distance [9] и др. При этом легко определить выброс —  он не лежит в скоплении большого числа соседей, как это делают нормальные данные.

  • One-class Classification-based Measure. Предполагаем, что нормальные данные приходят из одного класса, который может быть описан одной моделью, когда как аномальный объект в этот класс на попадёт. Тут можно найти one-class SVM [10], Support Vector Data Description (SVDD) [11].

  • Clustering-based Measure. Классический подход детекции аномалий, где предполагается, что выбросы легко отделимы от кластеров в сформированном на обучающей выборке новом подпространстве [12].

End-to-end anomaly score learning

 Из названия видим, что нейронная сеть будет теперь напрямую вычислять anomaly score.

Формульное описание

Семейство таких пайплайнов можно описать следующим образом:

где τ (x; Θ) : X→ R нейронная сеть, выдающая сразу скоринговую величину.

Ranking Models. Один из разновидностей end-to-end подхода. Здесь нейронная сеть сортирует все объекты по некоторой величине, ассоциирующейся у модели со степенью аномальности. Self-trained deep ordinal regression model [13].

Prior-driven Models. Основной подход — the Bayesian inverse RL-based sequential anomaly detection. Здесь основная идея — агент получает на вход последовательность данных, и его нормальное поведение рассматривается как получение вознаграждения. В случае же аномальных данных агент получит слабое вознаграждение за  последовательность [14].

Softmax Models. В данных архитектурах модели обучаются, максимизируя функцию правдоподобия нормального объекта из входных данных. При этом предполагается, что выброс будет иметь низкую вероятность в качетсве события.

Deviation Networks (end-to-end pipeline) [1]

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

рис. 5
рис. 5

здесь function φ — anomaly scoring network, которая будет в качестве выхода отдавать предсказанные значения меры аномальности. Reference score generator — генератор референсных величин той же меры, полученных сэмплированием для случайных объектов  неразмеченного сета (принимаемого за нормальный). Далее обе величины (предсказанная φ(x; Θ) и референсная μ_R) отправляются в deviation loss function L, целью которой будет заставить anomaly scoring network для нормальных объектов выдавать значения, близкие к референсным, и сильно отличные от референсных для аномальных.

Объяснение работы deviation loss function

где y = 1, если объект является аномальным, y = 0 иначе. При этом из функции потерь можно заметить, что в процессе оптимизации модель будет стараться приблизить значения anomaly score к референсному у нормальных объектах, тогда как для аномальных будет заставлять предсказательную сеть выдавать такие значения φ(x; Θ), чтобы dev(x) было положительным, а значит, будет устремлять его к «a» (заранее заданной достаточно большой положительной величиной). Тем самым модель обучится четко разделять аномальные и нормальные объекты.

Заключение

В качестве заключения стоит отметить, что для тех или иных условий, в которых предполагается решать задачу выявления аномальных объектов подходят совершенно разные архитектуру и идеи решения. В каждом из рассмотренных типов есть свои SOTA-подходы. Хотя за последние несколько лет всё больше и больше популярность набирают именно end-to-end алгоритмы.

Ссылки на литературу

[1] Deep Anomaly Detection with Deviation Networks. G. Pang
[2] Deep Learning for Anomaly Detection: A Review. G. Pang
[3] Emmanuel J Candès, Xiaodong Li, Yi Ma, and John Wright. 2011. Robust principal component analysis?
[4] Ping Li, Trevor J Hastie, and Kenneth W Church. 2006. Very sparse random projections.
[5] Alireza Makhzani and Brendan Frey. 2014. K-sparse autoencoders. In ICLR.
[6] Thomas Schlegl, Philipp Seeböck, Sebastian M Waldstein, Ursula Schmidt-Erfurth, and Georg Langs. 2017. Unsupervised anomaly detection with generative adversarial networks to guide marker discovery.
[7] Wen Liu, Weixin Luo, Dongze Lian, and Shenghua Gao. 2018. Future frame prediction for anomaly detection–a new baseline.
[8] Edwin M Knorr and Raymond T Ng. 1999. Finding intensional knowledge of distance-based outliers.[9] Fabrizio Angiulli and Clara Pizzuti. 2002. Fast outlier detection in high dimensional spaces.
[10] Bernhard Schölkopf, John C Platt, John Shawe-Taylor, Alex J Smola, and Robert C Williamson. 2001. Estimating the support of a high-dimensional distribution.
[11] David MJ Tax and Robert PW Duin. 2004. Support vector data description.
[12] Mathilde Caron, Piotr Bojanowski, Armand Joulin, and Matthijs Douze. 2018. Deep clustering for unsupervised learning of visual features.
[13] Guansong Pang, Cheng Yan, Chunhua Shen, Anton van den Hengel, and Xiao Bai. 2020. Self-trained Deep Ordinal Regression for End-to-End Video Anomaly Detection. 
[14] Andrew Y Ng and Stuart J Russell. 2000. Algorithms for Inverse Reinforcement Learning.

ссылка на оригинал статьи https://habr.com/ru/post/530574/

Snowflake, Anchor Model, ELT и как с этим жить

Привет! Меня зовут Антон Поляков, и я разрабатываю аналитическое хранилище данных и ELT-процессы в ManyChat. В настоящий момент в мире больших данных существуют несколько основных игроков, на которых обращают внимание при выборе инструментария и подходов к работе аналитических систем. Сегодня я расскажу вам, как мы решили отклониться от скучных классических OLAP-решений в виде Vertica или Exasol и попробовать редкую, но очень привлекательную облачную DWaaS (Data Warehouse as a Service) Snowflake в качестве основы для нашего хранилища.

С самого начала перед нами встал вопрос о выборе инструментов для работы с БД и построении ELT-процессов. Мы не хотели использовать громоздкие и привычные всем готовые решения вроде Airflow или NiFi и пошли по пути тонкой кастомизации. Это был затяжной прыжок в неизвестность, который пока продолжается и вполне успешно.

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

Описание данных ManyChat


ManyChat — это платформа для общения компаний с клиентами через мессенджеры. Нашим продуктом пользуется более 1.8 млн бизнесов по всему миру, которые общаются c 1.5 млрд подписчиков.

Моя команда занимается разработкой хранилища и ELT-платформы для сбора и обработки всех доступных данных для последующей аналитики и принятия решений.

Большую часть данных мы получаем из собственного приложения: нажатия пользователями кнопок, попапов, события и изменения моделей бэкэнда (пользователя/подписчика/темплейтов/взаимодействия с нашим апи и десятки других). Также получаем информацию из логов и исторических данных из Postgres-баз.

Некоторые данные мы принимаем от внешних сервисов, взаимодействие с которыми происходит посредством вебхуков. Пока это Intercom и Wistia, но список постепенно пополняется.

Данные для аналитиков


Аналитики ManyChat для своей работы пользуются данными из слоя DDS (Data Distribution Storage / Service), где они хранятся в шестой нормальной форме (6 нф). По сути, аналитики хорошо осведомлены о структуре данных в Snowflake и сами выбирают способы объединения и обработки множеств с помощью SQL.

В своей ежедневной работе аналитики пишут запросы к десяткам таблиц разного размера, на обработку которых у СУБД уходит определенное время. За счет своей архитектуры Snowflake хорошо подходит для аналитики больших данных и работы со сложными SQL запросами. Приведу конкретные цифры:

  • Размер больших таблиц — от 6 до 21 миллиарда строк;
  • Среднее количество просканированных в одном аналитическом запросе микро-партиций — 1052;
  • Отношение количества запросов с использованием SSD к запросам без использования локального диска — 48/52.

В таблице ниже приведена производительность реальных запросов за последний месяц в зависимости от количества используемых в них объектов. Все эти запросы были выполнены на кластере размера S (запросы от ELT-процессов в данных расчетах не участвовали).

Все запросы

Объектов в запросе Количество запросов AVG Время выполнения (сек) MED Время выполнения (сек)
1 — 3 15149 33 1.27
4 — 10 3123 48 8
11 + 729 188 38

Запросы, выполняемые быстрее, чем за 1 секунду, вынесены в отдельную группу. Это позволяет разделить запросы, использующие SSD (локальный кэш и сохраненные данные), от тех, которым приходится основную часть данных читать с медленных HDD.

Запросы > 1 сек

Объектов в запросе Количество запросов AVG Время выполнения (сек) MED Время выполнения (сек)
1 — 3 5747 71 9
4 — 10 2301 61 15
11 + 659 201 52

Увеличение количества объектов в запросе усложняет его процессинг.

В этом примере анализ запросов производился с помощью поиска названий существующих таблиц в SQL-коде запросов аналитиков. Таким образом мы находим приблизительное количество использованных объектов.

Anchor Model


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

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

Подробнее про Anchor Model, сущности, атрибуты и отношения вы можете почитать у Николая Голова aka @azazoth (здесь и здесь).

Немного о Snowflake



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

СУБД выделяет расчетные мощности on-demand, как и во многих других продуктах AWS. Бюджет расходуется только если вы используете предоставленные для расчетов мощности — тарифицируется каждая секунда работы кластера. То есть, при отсутствии запросов, вы тратите деньги только на хранение данных.

Для простых запросов можно использовать самый дешёвый кластер (warehouse). Для ELT-процессов, в зависимости от объема обрабатываемых данных, поднимаем подходящий по размеру кластер: XS / S / M / L / XL / 2XL / 3XL / 4XL – прямо как размеры одежды. После загрузки и / или обработки выключаем его, дабы не тратить деньги. Время выключения кластера можно настраивать: от «тушим сразу, как закончили расчет запроса» до «никогда не выключать».


Выделяемое на каждый размер кластера железо и цена за секунду работы

Подробнее про кластеры Snowflake читайте тут. А так же в последней статье Николая Голова.

В настоящий момент ManyСhat использует 9 различных кластеров:

  • 2 X-Small – для ELT процессов с маленькими наборами данных до миллиарда записей.
  • 4 Small – для запросов из Tableau и ELT процессов, требующих больших join’ов и тяжелых расчетов, например, заполнение строкового атрибута. Также этот кластер используется для работы аналитиков по умолчанию.
  • 1 Medium – для материализации данных (View Materialization).
  • 1 Large – для работы с данными больших объемов.
  • 1 X-Large – для единоразовой загрузки / правки огромных исторических данных.

Объем наших данных в Snowflake составляет приблизительно 11 Тбайт. Объем данных без сжатия — около 55 Тбайт (фактор сжатия х5).

Особенности Snowflake


Архитектура


Все кластеры в системе работают изолированно. Архитектура решения Snowflake представляется тремя слоями:

  1. Слой хранилища данных
  2. Слой обработки запросов
  3. Сервисный слой аутентификации, метаданных и др.


Иллюстрация архитектуры Snowflake

Snowflake работает с «горячими» и «холодными» данными. «Холодными» считаются данные, лежащие в S3 на обычных HDD (Remote Disk). При запросе они дольше считываются и загружаются в быстрые SSD отдельно для каждого кластера. Пока кластер работает, данные доступны на локальном SSD (Local Disk), что ускоряет запросы в несколько раз по сравнению с работой на «холодную».

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

Микро-партиции


Одной из интересных фичей Snowflake является работа с динамическими микро-партициями. Обычно в базах данных используются статические, но в ряде случаев, например, при перекосе данных (data skew), данные между партициями распределяются неравномерно что усложняет / замедляет обработку запросов.

В Snowflake все таблицы хранятся в очень маленьких партициях, содержащих от 50 до 500 Мбайт данных без сжатия. СУБД хранит в метаданных информацию обо всех строках в каждой микро-партиции, включая:

  • диапазон значений каждой колонки партиции;
  • количество уникальных (distinct) значений;
  • дополнительные параметры.

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

ELT Pipelines


Потоки данных и слои их хранения и обработки в ManyChat выглядят примерно так:

Данные поступают в DWH из нескольких источников:

  • PHP-бэкенд – события и изменения моделей данных;
  • Внешние API – Intercom, Wistia, FaceBook и другие;
  • ManyChat Frontend – события с фронтенда;
  • WebHooks – сервисы, отдающие данные через вебхуки.

Давайте рассмотрим, как устроена эта схема, на примере события из бэкенда:

  1. PHP-бэкенд отправляет событие о создании нового аккаунта в ManyChat.
  2. Redis принимает данные и складывает в очередь.
  3. Отдельный python-процесс вычитывает эту очередь и сохраняет данные во временный JSON, загружая его в последующем в Snowflake.
  4. В Snowflake, с помощью python-ELT-процессов, мы прогоняем данные по всем необходимым слоям и, в итоге, раскладываем в Анкор-Модель.
  5. Аналитики используют DDS и SNP-слои с данными для сборки агрегированных витрин данных в слой DMA.

Аббревиатуры слоёв SA* расшифровываются как Staging Area for (Archive/Loading/Extract)

  • SNP – слой для хранения агрегированных исторических данных из бэкэнд баз данных.
  • SAE – слой для хранения сырых данных из Redis в виде одной колонки типа variant.
  • SAA – слой для хранения обогащенных данных из Redis с добавлением служебных колонок с датами и id загрузки.
  • SAL – более детальный слой данных с типизированными колонками. Таблицы в нем хранят только актуальные данные, при каждом запуске скрипта загрузки производится truncate table.
  • DDS – 6 нф для хранения данных в виде «1 колонка SAL ⇒ 1 таблица DDS».
  • DMA – аналитический слой, в котором хранятся вьюхи, материализации и исследования аналитиков на базе DDS.

Статистика по объектам в схемах

Схема Количество объектов Количество представлений AVG строк (млн) AVG объём GB
SNP 3337 2 2 0.2
SAA 52 2 590 60
SAL 124 121 25 2.2
DDS 954 6 164 2.5
DMA 57 290 746 15

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

SAA занимает более 80% объема хранилища из-за неструктурированных данных типа variant (сырой JSON). Раз в месяц SAA-слой скидывает данные в историческую схему.

В настоящий момент мы храним более 11 Тбайт данных в Snowflake с фактором сжатия х5, ежедневно получая сотни миллионов новых строк. Но это только начало пути, и мы планируем увеличивать количество источников, а значит и поступающих данных кратно год к году.

Redis


https://habrastorage.org/webt/ix/6m/a2/ix6ma2hvzmnxbfwkzhm_dc6ihl0.jpeg
В ManyChat активно используется Redis, и наш проект не стал исключением: он является шиной для обмена данными. Для быстрого и безболезненного старта в качестве языка написания ELT-движка был выбран python, а для хранения логов и статистики — Postgres. Redis выступает в нашей архитектуре местом для временного хранения поступающей информации от всех источников. Данные в Redis хранятся в виде списка (List) JSON’ов.

https://habrastorage.org/webt/6x/4k/52/6x4k52kwhxnrzj40geyblmoepfq.jpeg
Структура хранения данных в Redis

В каждом списке могут находится от 1 до N разнообразных моделей данных. Модели объединяются в списки методом дедукции. Например, все клики пользователей в независимости от источника кладутся в один список, но могут иметь разные модели данных (список полей).

Ключами для списков в Redis являются придуманные названия, которые описывают находящиеся в нем модели.

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

  • EmailEvent (события происходящие с почтой)
    • email
    • email_package_reduce
  • SubscriberEvent (при создании или изменении подписчика, он появляется в этой очереди)
    • subscriber
  • ModelEvent (модели данных из бэкэнда и их события)
    • account_user
    • pro_subscription
    • wallet_top_up
    • И еще 100500 разных моделей
  • StaticDictionaries
    • Статичные словари из бэкенда. Информация о добавлении или изменении элемента словаря.

Весь ELT построен на python и использовании multiprocessing. Железо для всего ELT в ManyChat работает в AWS на m5.2xlarge инстансе:

  • 32 Гбайт RAM
  • Xeon® Platinum 8175M CPU @ 2.50GHz

Первый подход


Первым подходом к построению ELT-процесса для нас стала простая загрузка данных, выполняющаяся в несколько шагов в одном скрипте по cron’у.

https://habrastorage.org/webt/cx/zi/3q/cxzi3q-0qpk9lmxhaurtjdfi-9o.jpeg
Каждая очередь в Redis вычитывается своим собственным лоадером, запускаемым по расписанию в cron.

Первым этапом на рисунке выше является загрузка данных из очереди Redis в JSON-файл командой lpop(). Они вычитываются поэлементно из Redis, из каждой строки (словаря из JSON) снимается статистика по наполнению элементов словаря и затем записывается в Postgres. В этом же цикле данные записываются построчно в JSON-файл.

https://habrastorage.org/webt/qu/6h/vo/qu6hvooizkjd5zvzzvwrmunwfho.png
Лоадеры для загрузки данных. Названия лоадеров совпадают с названиями загружаемых очередей.

Псевдокод цикла считывания данных из Redis в JSON:

batch_size = 1000000 # Количество элементов для считывания из очереди Redis with open(json_file) as f:     while batch_size > 0:         row = redis.lpop('Model')         save_statistics(row)         batch_size -= 1         f.write(row) 

Вся последующая загрузка данных поделена на этапы:

  1. Загрузка из JSON в SAE-слой;
  2. Обогащение и загрузка из SAE в SAA;
  3. Загрузка из SAA в заранее созданную структурированную таблицу в SAL-схеме;
  4. Загрузка данных из SAL в DDS схему.

Из плюсов такого подхода можно выделить:

  • Скорость адаптации. На внедрение нового сотрудника в пайплайн и процессы уходит 1 день.
  • Скорость реализации. Python позволяет делать практически что угодно с очень низким порогом входа.
  • Простота. При неисправности легко починить или запустить код руками.
  • Стоимость. Вся инфраструктура создана на уже существующих мощностях, из нового – только Snowflake.

Конечно, были и минусы:

  • Определенные сложности с масштабированием. Если лоадер был настроен на считывание 1кк записей из Redis раз в 10 минут, а в очередь прилетело, например, 5кк событий, они считывались 50 минут. Бывали случаи, когда очередь не пустела в течении суток.

    Такие ситуации происходили крайне редко. Зная среднюю нагрузку наших сервисов, мы заранее выставляли более высокое ограничение на объем вычитываемых событий. А в случае внезапных увеличений объемов данных, производили загрузку «руками» с использованием более быстрого кластера и увеличенного количества вычитываемых объектов.

  • При любом вынужденном простое, тесте ELT-процессов или исправлении ошибок, мы останавливали загрузку из одной или нескольких очередей. Redis начинал наполняться бесконтрольно, и у нас могло закончиться место (30 Гбайт), что приводило к потере новых данных.
    https://habrastorage.org/webt/az/pg/qe/azpgqey53ut27ilztvxajcye_nm.png
    Остановка загрузки одной из очередей в Redis могла привести к расходованию всей памяти и невозможности принимать данные
  • Скрипт загрузки данных (Loader) содержал полный цикл от Redis до DDS, и в случае поломки его приходилось запускать заново. Если ошибка произошла где-то посередине, например, потерялся только что записанный JSON-файл, восстановить его было проблематично. Помочь могла только infra-команда и выгрузка исторических данных за определенные даты к нам в шину. В других случаях инженерам приходилось комментировать код и запускать определенную часть скрипта вручную, контролируя загрузку данных.

Второй подход


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

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

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

  • Чтение данных из Redis. Загрузка данных из Redis должна быть максимально глупой: код выполняет только одну функцию, не затрагивая остальные компоненты системы.
  • Трансформация данных внутри Snowflake. Подразумевает загрузку данных из слоя SAA в SAL со сбором статистики, ведением истории загрузок и информированием в Slack о появлении новых моделей и / или полей в моделях.
  • Сборка DDS. Множество параллельно работающих процессов, загружающих данные.

Чтение данных из Redis


https://habrastorage.org/webt/ca/uq/pa/cauqpayg8avputdrpbqrbrdk514.jpeg

RedisReader — скрипт для непрерывного вычитывания шины Redis. Conf-файл для supervisord создан под каждую очередь и постоянно держит запущенным необходимый ридер.

Пример conf-файла для одной из очередей email_event:

[program:model_event_reader] command=/usr/bin/env python3 $DIRECTORY/RedisReader.py --queue='manychat:::model_event' --chunk_size=500000 autostart=true autorestart=true stopsignal=TERM stopwaitsecs=1800 process_name=%(program_name)s_%(process_num)d numprocs=4 

Скрипт непрерывно мониторит определенную шину Redis, заданную через аргумент --queue на появление новых данных. Если данные в шине отсутствуют, он ждет RedisReader.IDLE_TIME секунд и повторно пробует прочитать данные. Если данные появились, скрипт считывает их через lpop() и складывает в файл вида /tmp/{queue_name}_pipe_{launch_id}_{chunk_launch_id}.json, где launch_id и chunk_launch_id – сгенерированные уникальные int’ы. Когда количество строк в файле достигает уровня --chunk_size или заданное время --chunk_timeout истекло, RedisReader завершает запись файла и начинает его загрузку в Snowflake.

Полученные данные сперва параллельно загружаются в таблицы
SAE.{queue_name}_pipe_{launch_id}_{chunk_launch_id}, а затем в одном процессе вставляются простым insert’ом в таблицу SAA.{queue_name}_pipe не блокируя работу с уже существующими данными.

Все действия в RedisReader являются multiprocessing-safe и призваны сделать загрузку наиболее безопасной при одновременном использовании множества процессов для вычитки одной очереди Redis.

Устанавливая параметр numprocs, мы можем запускать столько RedisReader’ов, сколько требуется для своевременного вычитывания очереди.

После внедрения RedisReader исчезла проблема с неконтролируемым расходованием памяти Redis. При появлении в очереди, данные практически моментально считываются и складываются в Snowflake-слое SAA по следующим колонкам:

  • model – название загружаемой модели данных
  • event_dt – дата заливки данных
  • raw – сами данные в JSON формате (variant)
  • launch_id – внутренний сгенерированный номер загрузки

Трансформация данных внутри Snowflake


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

https://habrastorage.org/webt/qb/8v/qx/qb8vqxfqzvhvustjxjxpdepfhao.jpeg

  1. На первом этапе необходимо получить список еще не обработанных launch_id. Для этого была создана специальная таблица engine.saa_to_sal_transfer, в которой хранится launch_id, статус его обработки is_done и прочая служебная информация. Задача скрипта – взять то количество необработанных строчек по каждой модели, которое указано в параметрах загрузчика либо немного меньше.
  2. После этого по каждой модели собирается статистика. Мы храним данные о min / max значениях в колонке, типе данных, количестве ненулевых записей и множестве других вспомогательных характеристик. Сбор статистики является необязательным, для некоторых лоадеров, меняющихся крайне редко, сбор статистики отключен. При появлении новых полей (колонок) в статистике, инженеры увидят сообщение в Slack и приступят к созданию сущностей DDS для последующей загрузки.

    https://habrastorage.org/webt/lb/-g/0h/lb-g0hjelxkkwapkl8gdr1qkjmi.png
    Часть таблицы статистики

  3. Далее происходит загрузка данных из слоя SAA в SAL. В SAL попадают только размеченные инженерами данные с описанием поля, правильным типом и названием, которые берутся из таблицы engine.sal_mapping
  4. Завершающий шаг трансформации – UPDATE в engine.saa_to_sal_transfer для проставления статуса is_done, если загрузка в SAL прошла успешно.

Сборка DDS


https://habrastorage.org/webt/ip/ic/8s/ipic8sjalyvmbrhq34zihe7rlw8.jpeg

Сборка таблиц для слоя DDS происходит на основе данных из SAL-схемы. Она изменилась меньше всего с момента первой реализации. Мы добавили полезные фичи: выбор типа отслеживания изменений данных (Slowly Changing Dimension) в виде SCD1 / SCD0, а также более быстрые неблокирующие вставки в таблицы.

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

Загрузка в DDS разделена на 2 этапа:

  1. Сначала грузятся сущности для формирования суррогатного ключа;
  2. Затем загружаются атрибуты и отношения.

Загрузка сущностей


Загрузка сущностей подразумевает загрузку только уникальных значений в таблицы типа DDS.E_{EntityName}, где EntityName – название загружаемой сущности.

self.entity_loader(entity_name: str, source_schema: str, id_source_table_list: list),

Метод загрузки принимает в качестве атрибутов название сущности, схему исходных данных, а также массив из названия колонки в SAL-таблице и самого названия исходной SAL-таблицы. Внутри происходит либо обычный MERGE INTO, либо INSERT FIRST.

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

Загрузка Отношений и Атрибутов


Загрузка отношений и атрибутов реализована похожим образом, единственное отличие – при вставке данных в DDS-схему происходит больше join’ов и проверок данных на актуальность.

Атрибуты:

self.attribute_loader(entity: str, attribute: str, source_table: str, id_column: str, value_column: str, historicity: str)

Отношения:

self.relation_loader(left_entity: str, right_entity: str, source_table: str, left_id: str, right_id: str, historicity: str)

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

Псевдокод одного из лоадеров

 from Loaders.SnowflakeLoaders import SnowflakeLoader      class ModelEventLoader(SnowflakeLoader):         DEFAULT_SOURCE_TABLE = 'saa.model_pipe'         DEFAULT_BATCH_STAT_SAMPLE_PERCENT = 50         DEFAULT_BATCH_SIZE = 1000000         DEFAULT_BUS_FULFILMENT_THRESHOLD = 100000         DEFAULT_HOURS_PASSED_THRESHOLD = 1.0          def sal_to_dds(self):             loaders = [                 self.entity_loader('Account', 'sal', ['page_id', 'rb_model_event']),                 self.entity_loader('Subscriber', 'sal', ['subscriber_id', 'rb_model_event']),                 self.entity_loader('Device', 'sal', ['device_id', 'rb_model_event']),             ]             self.run_loaders(loaders)              loaders = [                 self.attribute_loader('Account', 'IsActive', 'sal.rb_model_event', 'page_id', 'is_active', historicity='scd1'),                 self.attribute_loader('Subscriber', 'Name', 'sal.rb_model_event', 'subscriber_id', 'name', historicity='scd1'),                 self.attribute_loader('Device', 'Platform', 'sal.rb_model_event', 'device_id', 'platform', historicity='scd0'),                  self.relation_loader('Subscriber', 'Account', 'sal.rb_model_event', 'subscriber_id', 'account_id', historicity='scd1'),                 self.relation_loader('Subscriber', 'Device', 'sal.rb_model_event', 'subscriber_id', 'device_id', historicity='scd0'),             ]             self.run_loaders(loaders)          def run(self):             self.truncate_sal('rb_model_event')             self.saa_to_sal()             self.run_sal_to_dds()      if __name__ == '__main__':         ModelEventLoader().do_ELT()

Лоадер каждый раз проверяет условия запуска. Если они заданы, и необработанных данных в SAA-слое накопилось больше чем DEFAULT_BUS_FULFILMENT_THRESHOLD или после последнего запуска прошло больше чем DEFAULT_HOURS_PASSED_THRESHOLD часа, то будет взято не более DEFAULT_BATCH_SIZE строк из SAA-таблицы DEFAULT_SOURCE_TABLE, а также собрано статистики по DEFAULT_BATCH_STAT_SAMPLE_PERCENT процентам данных.

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

RedisReader в свою очередь работает независимо от всей остальной системы, ежесекундно опрашивая очереди в Redis. Загрузка данных SAA ⇒ SAL и далее в DDS тоже может работать абсолютно независимо, но запускается в одном скрипте.

Так мы смогли избавиться от прежних проблем:

  • Затирание JSON файла с данными.
  • Переполнение памяти Redis при остановке лоадеров (теперь можно останавливать на сколько угодно, данные уже будут в Snowflake в SAA-слое).
  • Ручное комментирование кода и запуск скриптов загрузки.

Сейчас на постоянной основе мы загружаем данные из 26 очередей в Redis. Как только данные появляются в них, они сразу попадают в SAA-слой и ждут своей очереди на обработку и доведения до DDS. В среднем мы получаем 1400 событий в секунду в диапазоне от 100 до 5000 в зависимости от времени суток и сезонности.

image
Количество полученных данных. Каждый цвет отвечает за отдельный поток данных.

Заключение


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

При этом было реализовано множество сторонних процессов, например Data Quality, Data Governance и материализация представлений.

Фактически добавление нового лоадера теперь сводится к заполнению полей в Google Sheet и построению модели будущих таблиц в схеме DDS.

Про нюансы работы наших ELT-процессов или аспекты работы со Snowflake спрашивайте меня в комментариях – обязательно отвечу.

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

ссылка на оригинал статьи https://habr.com/ru/company/manychat/blog/530054/

Многозадачность в shell скриптах

Иногда, при написании скрипта на shell хочется выполнять какие-то действия в несколько потоков. Подходящими ситуациями могут быть, например, сжатие большохо количества больших файлов на многопроцессорном хосте и передача файлов по широкому каналу, на котором ограничена скорость индивидуального соединения.
Все примеры написаны на bash, но (с минимальными изменениями) будут работать в ksh. В csh тоже есть средстава управления фоновыми процессами, поэтому подобных подход тоже может быть использован.

JOB CONTROL

Так называется секция в man bash где описаны подробности, на случай если вы любите читать man. Мы используем следующие простые возможности:

command & — запускает команду в фоне
jobs — печатает список фоновых команд

Простой пример, не выполняющий никаких полезных действий. Из файла test.txt читаются числа, и параллельно запускается 3 процесса, которые спят соответствующее количество секунд. Каждые три секунды проверяется число запущенных процессов, и если их меньше трех, запускается новый. Запуск фонового процесса вынесен в отдельную функцию mytask, но можно запускть его непосредственно в цикле.

test.sh

#!/bin/bash   NJOBS=3 ; export NJOBS  function mytask () { echo sleeping for $1  sleep $1 }  for i in $( cat test.txt ) do     while [  $(jobs | wc -l ) -ge $NJOBS ]         do              sleep 3         done     echo executing task for $i     mytask $i & done  echo waiting for $( jobs | wc -l ) jobs to complete wait

Входные данные:

test.txt

60
50
30
21
12
13

Обратите внимание на wait после цикла, команда ждет завершения исполняющихся в фоне процессов. Без нее скрипт будет завершен сразу после завершения цикла и все фоновые процессы будут прерваны. Возможно именно этот wait упоминается в известном меме «oh, wait!!!».

Завершение фоновых процессов.

Если прервать скрипт по Ctrl-C, он будет убит со всеми фоновыми процессами, т.к. все процессы работающие в терминале получают сигналы от клавиатуры (например, SIGINT). Если же скрипт убить из другого терминала командой kill, то фоновые процессы останутся работать до завершения и об этом нужно помнить.

Заголовок спойлера

user@somehost ~/tmp2 $ ps -ef | grep -E «test|sleep»
user 1363 775 0 12:31 pts/5 00:00:00 ./test.sh
user 1368 1363 0 12:31 pts/5 00:00:00 ./test.sh
user 1370 1368 0 12:31 pts/5 00:00:00 /usr/bin/coreutils —coreutils-prog-shebang=sleep /usr/bin/sleep 60
user 1373 1363 0 12:31 pts/5 00:00:00 ./test.sh
user 1375 1373 0 12:31 pts/5 00:00:00 /usr/bin/coreutils —coreutils-prog-shebang=sleep /usr/bin/sleep 50
user 1378 1363 0 12:31 pts/5 00:00:00 ./test.sh
user 1382 1378 0 12:31 pts/5 00:00:00 /usr/bin/coreutils —coreutils-prog-shebang=sleep /usr/bin/sleep 30
user 1387 1363 0 12:31 pts/5 00:00:00 /usr/bin/coreutils —coreutils-prog-shebang=sleep /usr/bin/sleep 3
user 1389 556 0 12:31 pts/2 00:00:00 grep —colour=auto -E test|sleep
user@somehost ~/tmp2 $ kill 1363
user@somehost ~/tmp2 $ ps -ef | grep -E «test|sleep»
user 1368 1 0 12:31 pts/5 00:00:00 ./test.sh
user 1370 1368 0 12:31 pts/5 00:00:00 /usr/bin/coreutils —coreutils-prog-shebang=sleep /usr/bin/sleep 60
user 1373 1 0 12:31 pts/5 00:00:00 ./test.sh
user 1375 1373 0 12:31 pts/5 00:00:00 /usr/bin/coreutils —coreutils-prog-shebang=sleep /usr/bin/sleep 50
user 1378 1 0 12:31 pts/5 00:00:00 ./test.sh
user 1382 1378 0 12:31 pts/5 00:00:00 /usr/bin/coreutils —coreutils-prog-shebang=sleep /usr/bin/sleep 30
user 1399 556 0 12:32 pts/2 00:00:00 grep —colour=auto -E test|sleep

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

trap

function pids_recursive() {     cpids=`pgrep -P $1|xargs`     echo $cpids     for cpid in $cpids;        do           pids_recursive $cpid        done }  function kill_me () {     kill -9 $( pids_recursive $$ | xargs )     exit 1 }  #не обязательно #trap 'echo trap SIGINT; kill_me ' SIGINT trap 'echo trap SIGTERM; kill_me' SIGTERM 

kill -L выводит список существующих сигналов, при необходимости можно добавить обработчики для нужных.

ссылка на оригинал статьи https://habr.com/ru/post/530576/

SEO-оптимизация сайта на React или как добиться конверсии от поисковиков если у вас Single Page Application

SEO-оптимизация сайта на React или как добиться конверсии от поисковиков если у вас Single Page Application
SEO-оптимизация сайта на React или как добиться конверсии от поисковиков если у вас Single Page Application

Смоделируем ситуацию: Вы являетесь членом команды веб-разработчиков, занимающихся созданием frontend-части молодого интернет-ресурса на базе React. И вот, когда уже начинает казаться что ваша разработка достигла определенной функциональной, качественной и эстетической кондиции, вы сталкиваетесь с достаточно сложным и не менее интересным вопросом: А что делать с SEO? Как добиться качественной конверсии от поисковых систем? Как сделать так, чтобы о вашем ресурсе узнал весь мир, не вкладывая в это огромного количества денег за платные рекламные компании либо сил в крупномасштабную дополнительную разработку? Как заставить контент вашего Single Page Application работать на вас в поисковых выдачах и приносить клиентов? Интересно? Тогда поехали…

Привет! Меня зовут Антон и я являюсь full-stack-разработчиком со стажем работы более 12 лет. За время своей трудовой деятельности я работал над проектами различной сложности и с разнообразными стеками технологий. Последние 6 лет я сосредоточил scope своего внимания в большей мере на frontend, который стал в итоге профильным направлением, так как привлек у меня больший интерес по сравнению с backend-ом (не хочу при этом сказать, что backend в целом менее интересен. Просто дело вкуса и особенности развития карьеры исключительно в моём личном случае).

В данный момент я являюсь team-лидером команды разработчиков на проекте «Своё.Жильё» — это экосистема Россельхозбанка с помощью которой можно выбрать недвижимость, рассчитать стоимость кредита, подать заявку и получить ответ в режиме онлайн. Уже сейчас в экосистеме есть возможность оформить заявку на ипотечный кредит и выбрать недвижимость из более чем 1,2 млн вариантов жилья. Набор онлайн-сервисов позволяет сократить количество посещений офисов банка до одного – непосредственно для подписания кредитного договора и проведения расчетов с продавцом.

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

Используемый стек и преследуемые цели

Не буду заострять внимание на полном стеке frontend технологий, используемых экосистеме «Своё Жилье», а опишу лишь то, что действительно важно для дальнейшего рассказа, чтобы стало понятно, с чем в результате мы имеем дело. А именно:

  • Webpack

  • React

  • Redux

Из вышеперечисленного сразу становится ясно, что на выходе мы получаем Single Page Application на React, что влечет за собой как безграничное количество возможностей и плюсов данного стека, так и ряд подводных камней, связанных с реализацией SEO-friendly ресурса, который сможет в конечном счёте выполнить ряд задач, важных для продвижения в поисковых системах, социальных сетях и так далее:

  • Поисковой робот должен видеть все ссылки на страницы сайта;

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

  • Контент страниц для корректной индексации должен содержать все необходимые элементы, важные для создания SEO-оптимизированного сайта.

В данном списке я не затрагиваю задачи, которые необходимо решать на стороне backend (такие как поддержка last-modified, оптимизация скоростных показателей и т.п.), чтобы искусственно не раздувать общий материал и не отвлекаться от основной темы.

Казалось бы, перечень задач не так велик и решался бы практически “из коробки” на любом обычном относительно стандартном интернет ресурсе… Но тут нужно вернуться к тому, что мы имеем дело не с привычным веб-сайтом, у которого все страницы с их контентом отдаются с backend-а, а с Single Page Application, у которого контент страничек рендерится (отрисовывается средствами js) на стороне браузера. А ведь львиная доля поисковых роботов не умеет выполнять js-код при обходе интернет-ресурсов, и поэтому они попросту не увидят наш контент (Google умеет, но делает это пока недостаточно корректно и эффективно. Да и кроме Google, есть еще множество других целевых поисковых систем и кейсов, при которых “голый” SPA не сможет решить поставленные задачи).

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

Для того, чтобы поисковые роботы “увидели” страницы сайта, им необходимо иметь возможность прочесть контент страницы, в том числе содержащий ссылки на другие страницы (при этом подход с sitemap я тут описывать не буду, но упомянул, на случай если кому-то станет интересно, можете погуглить. Мы пока обходимся без карт). Из этого вытекает, что поисковой робот всё же должен “по старинке” загрузить html-разметку, содержащую контент страницы с web-сервера. Ну и конечно же, каждая из страниц, запрашиваемых с web-сервера, должна отдавать код 200 (случаи с необходимостью переадресаций с кодом 301 я тут также рассматривать не буду) и приходить в виде стандартного html-документа, содержащего текстовый и медиа-контент данной страницы, а так же ссылки на другие страницы и, конечно же, необходимые для SEO-оптимизации элементы, такие как ряд обязательных meta-тегов, заголовков и так далее. Общий список необходимого “SEO-тюнинга” любого веб-ресурса достаточно велик и про него можно написать отдельный материал и не один. Затронем тут обязательный “план минимум”, который включит в себя следующие пункты:

1 — Каждая из страниц ресурса должна в блоке <head> включать в себя:

  • Meta-тег title (заголовок страницы)

  • Meta-тег description (описание страницы)

  • Meta-тег keywords (перечень ключевых фраз)

2 — Каждая страница должна иметь в блоке <body> основной заголовок внутри html-элемента <h1> расположенный как можно выше перед началом текстового контента.

3 — Каждое изображение, которое присутствует на странице в виде html-элемента <img>, должно иметь атрибут alt, описывающий содержимое данного изображения.

Ну и конечно же, на сайте не должно быть “битых” ссылок отдающих с web-сервера код ошибки 404 (либо иной) или какой-либо пустой контент вместо ожидаемого.

И тут снова вспоминаем, что у нас SPA (Single Page Application) и с backend-а приходит лишь пустая часть разметки html-документа, включающая в себя информацию для загрузки js и css кода, который после загрузки и выполнения отрисует нам контент запрошенной страницы.

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

Подбор решения и реализация

Что же делать, если у Вас уже есть готовый SPA, либо просто отточенный архитектурный подход, нарушать который ради SEO было бы кощунством?

Ответ: Реализация пререндеринга, как конечного шага сборки приложения. Пререндеринг – это процесс выполнения js-кода после его основной сборки и сохранение получившихся html-копий отрисованных страниц, которые в последствии и будут отдаваться с web-сервера при соответствующих запросах.

Пререндеринг

Для реализации данного кейса есть ряд готовых решений и инструментов с различными ограничениями, подходящих для разных стеков frontend-технологий и с разным уровнем сложности внедрения.

Конечно есть такие решения как Next.Js и ему подобные (а также другие способы реализации того же SSR). И я не могу не упомянуть об этом. Но изучив их, я пришел к выводу, что они вносят ряд ограничений (либо усложнений) в процесс конфигурации основной сборки и имеют крайне заметное влияние на архитектуру приложения. Также, есть сторонние внешние сервисы пререндеринга, но для закрытой экосистемы банка – лишний внешний инструмент был бы крайне нежелателен (и опять же, зачем перекладывать зону ответственности на внешний инструмент, если можно реализовать его функционал внутри проекта).

После анализа, я пришел к максимально подходящему нам инструменту для покрытия описанных кейсов, реализующему проход по страницам SPA на React (и не только) и создание html-копий страниц.

Выбор пал на React-Snap.

Если в двух словах, то данный инструмент позволяет после сборки осуществить запуск Chromium-а и передать ему получившееся приложение. Затем пройти по всем его страницам, используя пул найденных ссылок так, как сделал бы это поисковой робот. При этом html-копии отрисованных средствами js страниц будут сохранены в выбранный каталог с сохранением иерархии путей, из которых они были получены относительно корня проекта.

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

Установка React-Snap не вызывает никаких дополнительных вопросов, так как его пакет доступен для скачивания стандартным образом из npm (и yarn).

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

Конфигурация запуска React-Snap описывается в корневом файле package.json проекта. Давайте рассмотрим пример минимальной конфигурации:

"scripts": {     // …Необходимые команды запуска hot-а, сборки и т.п.     "build:production": "webpack --mode production && react-snap"     // …Другие необходимые команды }, "reactSnap": {     "source": "dist", // Каталог собранного приложения     "destination": "dist", // Каталог для сохранения html-копий     "include": [ // Список энтрипоинтов для обхода страниц         "/",         "/404",         "/500"         // …Другие необходимые энрипоинты     ] }

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

К примеру, запуск React-Snap можно осуществить, просто добавив в блок “scripts” команду:

"postbuild": "react-snap"

Но тогда он будет запускаться после каждого билда, а в проекте их может быть несколько вариантов (например, для production и тестового стенда, где на последнем нам наоборот, может быть не нужен SEO и какой-либо еще функционал, такой как инструменты аналитики типа Google Analytics и т.п.).

Что касается блока ”include”, его желательно описать, иначе, например, html-копия для странички ошибки (500, либо другая техническая страница, при наличии) не будет создана, так как не на одной из страниц сайта не фигурирует ни одной ссылки на нее. Как следствие, React-Snap не узнает о её наличии. В теории и поисковик на них ходить не должен, но бывают случаи, когда страница создается для распространения ссылки за пределами сайта, а на сайте на нее ссылки может и не быть (к примеру, баннеры для рекламных компаний и тому подобное). Это как раз тот самый случай. Тут стоит проанализировать, нет ли у вас еще каких-то (возможно аналогичных «технических») страничек, на которые прямые ссылки на сайте отсутствуют.

Далее, для нормальной работы самого React-приложения у конечного пользователя, “поверх” DOM который придёт с web-сервера, нам потребуется внести небольшую правку в корневой render:

import { hydrate, render } from "react-dom"; // … Ваш код const rootElement = document.getElementById("root"); // (или ваш id при олтличии) if (rootElement.hasChildNodes()) { // …Если в корневом элементе есть контент, то…   hydrate(<App />, rootElement); // …"цепляем" приложение на существующий DOM. } else { // …Иначе рендерим приложение стандартным образом   render(<App />, rootElement); }

Вот и всё что нам потребуется для начала (а возможно в ряде случаев и в принципе всё что необходимо будет сделать). Уже можно попробовать выполнить build с созданием html-копий страниц вашего ресурса. На выходе (с приведенным выше конфигом) в каталоге /dist вы получите тот же набор js и css файлов (а также других ресурсов), что и ранее, index.html, плюс файл 200.html и другие html-файлы с копиями контента ваших страниц.

Для полноты картины сразу опишу небольшой подводный камень с пояснением, для понимания, что при таком подходе на реальном production-web-сервере вам нужно будет позаботиться о следующем нюансе…

Ранее у вас скорее всего по умолчанию на любой запрос, если ресурс отсутствует физически на сервере, отдавалась index.html, которая запускала приложение. Далее, в соответствии с запросом из адресной строки, приложение отрисовывало необходимую страницу, либо страницу 404, если не находило соответствие. Теперь же, наш index.html уже не является пустым, а содержит контент главной страницы. Но страничка с пустой html-разметкой для случая попытки запуска страницы без html-копии всё же существует. Это та самая вышеупомянутая 200.html. Таким образом, на web-сервере необходимо перенастроить дефолтный ресурс для случая 404 с index.html на 200.html, чтобы избежать открытия “кривых” страниц (с контентом главной страницы поверх которого будет пытаться запуститься наш SPA) при обращении на страницы, html-копий для которых нет, либо просто при некорректном обращении на несуществующую страницу.

И вот у нас есть готовое приложение, страницы которого доступны для любого поисковика.

Meta-теги, заголовки, описания

Если с вышеописанным разобрались, то перейдем к реализации следующей части нашей задачи, а именно к необходимым meta-тегам и другим SEO-нюансам на страницах.

<!doctype html> <html lang="ru">   <head>     <meta charset="utf-8">     <!-- ...и т.д. -->     <title>Контент meta-тега Title</title>     <meta name="description" content="Контент meta-тега Description">     <meta name="keywords" content="ключевые фразы для meta-тега keywords">   </head>   <body>     <div id="root">       <div className="content">         <h1>Заголовок страницы H1</h1>         <p>Текстовый контент страницы...</p>         <img alt="Описание изображения" src="...">         <!-- ...и т.д. -->       </div>     </div>     <script src="/application.js"></script>   </body> </html>

На заголовки <h1> и alt-ы для картинок особое внимание заострять не буду. Тут всё просто: идем по существующему js-коду react-компонентов страниц и добавляем там, где этого нет (а также не забываем это делать в дальнейшем для новых компонентов). А вот относительно meta-тегов title, description и keywords стоит немного поговорить отдельно. Они должны быть уникальными для каждой страницы. О том, зачем нужен каждый из них и как его стоит формировать, будет полезнее почитать более профильные материалы по SEO. Для нас же стоит более прагматичная задача – реализовать средствами js изменение контента данных тегов при навигации между страницами (таким образом у каждой html-копии страницы они будут разными как и положено, а при дальнейшей навигации по приложению после его запуска, они так же будут меняться в зависимости от текущей странички, но уже силами js приложения).

В целом, для реализации данного функционала есть готовый инструмент:

React-Helmet

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

В итоге мы добавили необходимые для эффективного SEO элементы на страницы нашего интернет-ресурса.

Подводные камни, нюансы реализации и советы

Хочу в первую очередь затронуть один нюанс, знать о котором будет полезно при дальнейшей разработке. Как правило, при создании html-копий рано или поздно появятся кейсы, при которых поведение приложения для пререндера должно будет отличаться от поведения приложения в браузере у реального пользователя. Это может касаться случаев, начиная от наличия каких-либо прелоадеров, которые не нужны в html-копиях или статистического функционала, который не должен выполняться на этапе создания html-копий (и возможно даже будет вызывать ошибки при их создании) заканчивая банальным объявлением адреса для API backend-а, который вероятно настроен у вас для hot в разделе devServer webpack-конфига как proxy, а в самом приложении указан как относительный, что не заработает, так как во время работы пререндера hot не запущен и нужно ходить на реальный адрес backend-а. Как вариант, могу также привести пример в виде распространенного окошка, которое сейчас есть практически на любом интернет-ресурсе, говорящее о том, что на сайте используются Cookies. Как правило, окошко отображается на любой страничке, пока пользователь в какой-то момент один раз не закроет его. Но вот беда: пререндер то не знает, что что-то нужно закрыть, а соответственно контент данного окошка будет присутствовать на всех html-копиях, что плохо для SEO.

Но не всё так страшно и решение есть. В таких местах в приложении мы можем использовать условие для запуска тех или иных функций (или использования тех или иных переменных, как в примере с адресом API). Дело в том, что у пререндера есть специфическое имя user-agent-а – “ReactSnap” (кстати через параметры можно задать своё при необходимости). К примеру:

const isPrerender = navigator.userAgent === "ReactSnap";

Как и где это можно объявить и использовать, думаю, не нуждается в дальнейшем объяснении. Полагаю, как минимум пара мест, где это пригодится, найдётся в любом проекте.

Также стоит затронуть случай, с которым с большой долей вероятности вам придется столкнуться. А именно: React-Snap обошел не все страницы, либо, нам стало необходимо, чтобы он не заходил в какие-либо разделы или на определенные страницы и не создавал для них html-копии.

Тут сразу стоит внести понимание в процесс работы пререндера React-Snap. Он запускает наше приложение и осуществляет обход по ссылкам, найденным на страницах. При этом учитываются именно html-элементы <a>. Если пререндер не сохранил html-копию для какой-либо страницы (либо мы намеренно хотим этого добиться), то скорее всего переход на эту страницу сделан (либо вы намеренно можете так сделать) с использованием, к примеру, onClick а не через обязательный атрибут ссылки — href. Тут нужно упомянуть, что стандартные компоненты Link либо NavLink из react-router-dom фактически создают в DOM именно html-элемент <a> с href, так что если не отбиваться от классических подходов, то проблем не будет.

Следующим полезным знанием будет то, что нам обязательно необходимо позаботиться о минификации размеров DOM, который будет содержаться в наших html-копиях, так как большой html-документ будет дольше загружаться с backend-а, съедать больше трафика, да и поисковые роботы могут попросту не добраться до необходимого контента, если, к примеру, у вас в <head> документа все стили заинлайнины, как и все svg-изображения в <body>, что раздует каждую из html-копий до огромных размеров. Для понимания: если логотип вашего ресурса рендерится как inline-svg, то в файле каждой html-копии он будет присутствовать именно в таком виде.

Выход: настроить webpack таким образом, чтобы при сборке все стили складывались в css-файлы, а inline-svg заменить на использование <img> (либо средствами css) для отображения картинок (и то и другое будет загружаться один раз, а далее браться из кеша браузера и, что главное, задублированный контент таких ресурсов будет отсутствовать в html-копиях).

Еще один небольшой совет: общее количество и список всех созданных html-копий страниц, либо ошибок создания и различные вызванные редиректы (к примеру 404), а также прочие проблемные места мы сможем сразу увидеть и проанализировать благодаря достаточно понятному и подробному логу, который будет выводиться в процессе работы пререндера React-Snap. Не стоит забывать смотреть в него после сборки, так как на этом этапе мы всегда сможем увидеть те же проблемы на сайте, что увидит поисковой робот, но при этом у нас будет возможность заблаговременно что-то поправить при необходимости.

Заключение

Пожалуй, вышеописанного будет достаточно, чтобы начать и относительно быстро реализовать SEO-friendly сайт, написанный в виде Single Page Application. Далее всё будет зависеть лишь от особенностей конкретно вашего интернет-ресурса и тех целей, которые вы будете преследовать при его создании. Я постарался описать основные нюансы и подводные камни, с которыми пришлось столкнуться в процессе аналогичной разработки.

Основные плюсы данного подхода заключаются в том, что мы получаем инструментарий, который не требует архитектурных доработок приложения, дополнительного обучения программистов в команде и высоких трудозатрат на реализацию. После внедрения технологии, вы просто продолжаете разрабатывать ваше приложение по всем традициям React и придерживаясь ранее реализованной архитектуры. При этом ваше приложение уже не будет являться обычным SPA в привычном смысле, так как старт любой страницы будет сопровождаться загрузкой копии содержимого данной страницы, а дальнейшая навигация по приложению будет осуществляться уже привычным образом средствами SPA, что позволит поисковым роботам полноценно взаимодействовать с вашим контентом и выводить в результатах поиска актуальные ссылки с соответствующими описаниями страниц, а конечным пользователям как и ранее будет доступен весь спектр функционала Single Page Application.

Ну вот мы и добрались до финала. Сейчас вам удалось познакомиться с реально работающим на продакшн-версии проекта «Своё.Жильё» от Россельхозбанка подходом по реализации SEO-friendly интернет-ресурса на примере React-приложения и рассмотреть основные тезисы и подводные камни процесса создания SEO-эффективного сайта на основе SPA. Надеюсь, что полученные в данном материале знания будут полезны и найдут применение. Спасибо за уделённое на прочтение время.

ссылка на оригинал статьи https://habr.com/ru/company/rshb/blog/529636/

Proxmox VE обновился до версии 6.3

Proxmox Server Solutions GmbH выпустила новый релиз платформы виртуализации Proxmox VE 6.3, сообщается в пресс-релизе компании. Обновление включает в себя Debian Buster 10.6 c ядром Linux версии 5.4 (LTS), актуальные версии приложений для сред виртуализации, таких как QEMU 5.1, LXC 4.0, Ceph 15.2, и файловую систему ZFS версии 0.85.

Ключевые изменения


Прежде всего обновления направлены на удобство интеграции с недавно вышедшей системой резервного копирования Proxmox Backup Server 1.0, обзор которой мы уже делали в одной из предыдущих статей. Интеграция сводится к добавлению Proxmox Backup Server в качестве целевого хранилища для резервных копий Proxmox VE.

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

Поддержка Ceph Octopus и Ceph Nautilus


В Proxmox 6.3 была реализована поддержка объектных хранилищ Ceph Octopus версии 15.2.6 и Ceph Nautilus версии 14.2.15. Нужную версию пользователь может выбрать на этапе установки. В интерфейс Proxmox VE было добавлено множество специфических для Ceph возможностей управления. В частности, функция мониторинга прогресса восстановления на статус-панели.

Новая версия позволяет отображать и настраивать режим автомасштабирования для групп размещения (Placement Groups) в каждом пуле кластера Ceph. Это дает дополнительную гибкость в управлении кластером и снижает затраты на обслуживание. Ceph Octopus теперь включает в себя возможность multi-site репликации, что важно для обеспечения отказоустойчивости.

Улучшения веб-интерфейса


Веб-интерфейс Proxmox версии 6.3 значительно расширил возможности управления виртуальным дата-центром за счет следующих нововведений:

  • Поддержка внешних серверов метрик. Теперь в качестве внешних сервисов можно добавить отображение данных в InfluxDB или Graphite из веб-интерфейса.
  • Улучшенный редактор порядка загрузки ВМ. Появилась возможность выбора нескольких устройств по типу (дисковые или сетевые) простым перетаскиванием.
  • Поддержка опциональной проверки TLS сертификата для LDAP и Active Directory.
  • Резервное копирование и восстановление. Возможность детального просмотра гостевых машин и дисков, которые будут задействованы в процессе бэкапа.
  • Возможность добавления комментариев. Для каждого типа хранилища теперь можно оставить заметку, что, несомненно, будет полезным для системных администраторов. Также Proxmox Backup Server будет отображать процесс проверки всех выполненных снапшотов.

Прочие улучшения

  • Новые настройки политик хранения бэкапов. Для каждого задания резервного копирования можно тонко настроить, сколько резервных копий хранить и на протяжении какого времени.
  • Улучшен механизм определения износа твердотельных накопителей.
  • Расширение возможностей контейнеров. Теперь поддерживаются системы с количеством ядер до 8192. Официальную поддержку получили контейнеры с пентест-дистрибутивом Kali Linux и Devuan, форком Debian без systemd. Разумеется, все последние версии Ubuntu, Fedora и CentOS также будут корректно работать. Улучшены возможности мониторинга запуска контейнеров и установки для контейнеров отдельных часовых поясов.
  • Управление пользователями и разрешениями. Учтены особенности Active Directory и LDAP, такие как чувствительность логина к регистру, а также возможность использования клиентских сертификатов и ключей.
  • Улучшена обработка реплицированных ВМ при миграции.
  • Файервол. Обновленный API и графический интерфейс для сопоставления типов ICMP.
  • Установка. После успешного завершения инсталляции система автоматически перезагружается.

Скачать Proxmox VE 6.3 можно на официальном сайте Proxmox Server Solutions GmbH.

ссылка на оригинал статьи https://habr.com/ru/company/selectel/blog/530540/