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

Уже довольно давно Apache Flink был в моем списке «вещей, которые мне действительно нужно досконально понять». Я видел упоминания о нем наряду с Kafka, слышал его в разговорах о конвейерах обработки данных в реальном времени и в общих чертах понимал его применение. Но я никогда по-настоящему не садился за его изучение.
Если вы разделяете это мнение, вы не одиноки. Есть веская причина изучить Flink, это один из самых популярных инструментов в разработке программного обеспечения на данный момент. Netflix использует его для обнаружения аномалий в режиме, близком к реальному времени, в своей инфраструктуре потокового вещания. Сообщается, что Alibaba использует одну из крупнейших в мире сетей Flink, обрабатывая сотни миллиардов событий в день на десятках тысяч машин. Uber построил свою аналитическую платформу на его основе. Flink стал основой для обработки информации в режиме реального времени некоторыми из самых ресурсоемких компаний мира. Поэтому, если Flink тоже был в вашем списке, сейчас самое время разобраться в нем.
Итак, я погрузился в это с головой. И, честно говоря, был удивлен не только тем, что представляет собой Flink, но и тем, почему он существует и как он построен. История Flink — это история гораздо более глубокой идеи: идеи о том, как понимать большие объемы постоянно поступающих данных. Постановка задачи довольно проста: как создавать реальные и практические решения на основе огромных массивов непрерывных данных. В этом посте я пытаюсь объяснить эту идею с нуля и показать, какое место в ней занимает Flink.
Давайте начнём.
Прежде чем мы начнём
В этом посте постоянно поднимаются две темы, по которым стоит убедиться, что мы понимаем друг друга, прежде чем двигаться дальше.
Что такое поток? Поток — это непрерывная, потенциально бесконечная последовательность записей, поступающих с течением времени. Представьте себе пользователя, просматривающего веб-сайт — каждый просмотр страницы, каждый клик, каждая прокрутка — это событие, которое генерируется. Одно за другим, в реальном времени. У этого нет естественного «конца» — пока пользователь активен, события продолжают поступать. Это и есть поток.

Что такое пакетная обработка? Пакетная обработка означает обработку конечного, ограниченного набора данных за один раз. Вместо того чтобы реагировать на каждое событие по мере его поступления, вы собираете события в течение определенного периода времени — скажем, часа — а затем выполняете вычисления над всеми ними одновременно. У вычислений есть четкое начало и четкий конец.

Оба способа обработки данных являются законными. Именно это противоречие между ними и был призван разрешить Flink — и мы к этому придем.
Вернемся к проблеме: как мы на самом деле получаем данные.
Позвольте мне привести конкретный пример, который мы будем использовать на протяжении всей этой статьи.
Представьте, что вы создаёте систему рекомендаций — такую, которая показывает пользователям «вам также могут понравиться эти товары» на основе того, что они просматривали. Для эффективной работы вашей системе необходимо знать следующее:
На что этот пользователь кликал в последние несколько минут?
Какие товары сейчас пользуются наибольшей популярностью среди всех пользователей?
Какие товары пользователь просматривал, но не покупал в последней сессии?
Откуда же берутся эти данные? Каждый раз, когда пользователь открывает страницу товара, вы записываете событие. Каждый клик, каждая покупка, каждый поиск — ваше приложение непрерывно записывает записи, которые выглядят примерно так:
{ «user_id»: «u-8821», «item_id»: «p-443», «event_type»: «view», «timestamp»: «2024–03–10T14:32:01Z» } { «user_id»: «u-1042», «item_id»: «p-117», «event_type»: «purchase», «timestamp»: «2024–03–10T14:32:03Z» } { «user_id»: «u-8821», «item_id»: «p-501», «event_type»: «click», «timestamp»: «2024–03–10T14:32:07Z» }
Одна запись каждые несколько секунд для каждого пользователя, для миллионов одновременно работающих пользователей, непрерывно. Это ваши данные. Не файл. Не таблица, которая обновляется раз в день. Поток — непрерывная, бесконечная последовательность событий, описывающая то, что ваши пользователи делают прямо сейчас.
Повторюсь, вот как выглядит этот поток —

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

Почему? Потому что пакетная обработка концептуально проста. Вы точно знаете, какие у вас данные. Вы можете четко рассуждать о вычислениях — они начинаются, выполняются и заканчиваются. Такие системы, как Hadoop и MapReduce (вам не нужно знать их подробно для этой статьи), были построены на основе этой модели и масштабировались до огромных объемов данных. Они работали.
Но есть и существенная цена: задержка . Если ваша пакетная обработка запускается каждый час, то в худшем случае поведение пользователя в данный момент не повлияет на рекомендации в течение часа. Для системы рекомендаций это означает, что пользователю, который только что проявил большой интерес к туристическому снаряжению, будут показаны аксессуары для ноутбука — потому что система еще не успела обработать информацию. Пользователь искал туристический рюкзак, и вам нужно показать ему палатки и треккинговые палки при следующей загрузке страницы, а не через час.
В системах обнаружения мошенничества почасовая задержка означает, что мошеннические транзакции остаются незамеченными в течение часа. Для панели мониторинга в реальном времени это означает, что ваши «реальные» показатели могут быть устаревшими до 59 минут. Недостаток пакетной обработки заключается в том, что события происходят в реальном времени, но ваша система узнает о них только по расписанию.
По мере роста объемов данных и ужесточения требований к задержке инженеры начали создавать потоковые системы наряду с пакетными системами — системы, способные обрабатывать каждое событие по мере его поступления, за миллисекунды. Apache Storm был одним из первых лидеров в этой области. Amazon Kinesis. Samza от LinkedIn.

Но создание новой потоковой системы при сохранении существующей пакетной системы — задача не из простых. Теперь вам нужно поддерживать две системы. Ваш потоковый конвейер вычислял приблизительные результаты в реальном времени. Ваш пакетный конвейер работал всю ночь и выдавал точные и полные результаты. Вам приходилось писать одну и ту же бизнес-логику дважды — по одному разу для каждой системы, в разных фреймворках, на разных языках, синхронизируя их вручную. Когда пакетное и потоковое задания расходились в результатах (а в конечном итоге они всегда расходились), вам приходилось выяснять, какое из них ошибочно.
В этом новом мире ваша система рекомендаций теперь выглядит так: потоковый компонент, который обновляет рекомендации практически в режиме реального времени на основе последних событий, и пакетный компонент, который каждую ночь перестраивает всю модель рекомендаций на основе исторических данных.
Две кодовые базы. Два конвейера развертывания. Два набора ошибок. Один серверный слой пытается их согласовать.
Главный вывод: пакетная обработка — это лишь частный случай потоковой обработки.
В основе Flink лежит довольно простая идея:
Ограниченный набор данных — это всего лишь частный случай неограниченного потока данных, который имеет конечный результат.
Ваша историческая база данных, содержащая события пользователей за 5 лет, — это поток, который начался 5 лет назад и закончился сегодня. Ваши файлы журналов за прошлый месяц — это поток, имеющий начало и конец. Разница между «пакетными данными» и «потоковыми данными» не заключается в принципиальном различии в природе данных. В конечном счете, это просто события в формате JSON, описывающие поисковые запросы и клики пользователя. Вопрос в том, продолжается ли поток или он остановился.
Вернемся к нашей системе рекомендаций: «исторические данные», которые вы обрабатываете в ночном пакетном задании, и «события в реальном времени», которые вы обрабатываете в потоковом конвейере, — это всего лишь записи в одной и той же последовательности пользовательских событий. Разница лишь во времени их чтения. Ночное пакетное задание считывает записи шестимесячной давности. Потоковой конвейер считывает записи шестисекундной давности. Данные одни и те же, но временной интервал разный.
Если вы создадите систему, которая обрабатывает потоки данных нативно — и работает как с бесконечными, так и с конечными потоками — вам не понадобятся отдельные системы. Вам не нужно поддерживать две кодовые базы. У вас будет один движок, один набор логики, и вы направляете его на тот фрагмент потока, который вам нужен.
Именно это и пытается сделать Flink.
Итак, что же такое Apache Flink?
Apache Flink — это распределенная платформа для обработки потоковых данных . Она принимает потенциально неограниченный поток данных (или ограниченный пакет данных — одно и то же), обрабатывает его параллельно на кластере машин и непрерывно выдает результаты по мере поступления данных.

Внутри Flink-задачи написаны на коде и преобразуются в направленный ациклический граф (DAG). Например, код для Flink-задачи будет выглядеть так (понимание всех деталей не обязательно, это лишь общее представление):
// ── 1. ИСТОЧНИКИ ─────────────────────────────────────────── searches = readFromKafka(«search-events») clicks = readFromKafka(«click-events») // ── 2. АКТИВНОСТЬ ПО ПОЛЬЗОВАТЕЛЮ (оконная агрегация) ────────────── // Группировка событий по пользователю, вычисление скользящих характеристик за последние 30 минут. userActivity = (поиски + клики) .keyBy(userId) .window(slidingWindow(size=30min, slide=1min)) .aggregate(activityAggregator) // → { userId, recentQueries, recentClicks, categories, … } // ── 3. Встраивание пользователя (вызов модели «башня пользователя») ──────────────── // Преобразование характеристик активности в вектор. userState = userActivity.asyncMap(callUserTowerModel) // → { userId, embedding[128], features } // ── 4. ГЕНЕРАЦИЯ КАНДИДАТОВ (2 источника, затем слияние) ───────── annCandidates = userState.asyncMap(vectorAnnLookup) // ~500 элементов trendingCandidates = userState.asyncMap(trendingLookup) // ~200 элементов allCandidates = (annCandidates + trendingCandidates) .keyBy(userId) .window(2sec) .reduce(mergeAndDedupe) // → { userId, candidates: ~1000 itemIds } // ── 5. ИЗВЛЕЧЕНИЕ ПРИЗНАКОВ ЭЛЕМЕНТОВ (пакетный поиск) ───────────────── scoringInputs = allCandidates .joinWith(userState, on=userId) .asyncMap(fetchItemFeatures) // → { userId, userFeatures, [(itemId, itemFeatures) × ~1000] } // ── 6. РАНЖИРОВАНИЕ (модель ранжирования звонков) ────────────────────────── ranked = scoringInputs.asyncMap(callRankingModel) // → { userId, top 100 (itemId, score) pairs } // ── 7. SINK ──────────────────────────────────────────────── ranked.writeTo(redis)
Внутри Flink этот код разбивается на граф физических задач, которые необходимо выполнить, а затем эти задачи разбиваются на более мелкие наборы параллельных «подзадач».

Flink отправляет задачи на рабочие узлы. Каждый рабочий узел непрерывно выполняет назначенные ему задачи, периодически отправляет сигналы подтверждения (heartbeat) обратно в Flink и сообщает о сбое задачи, чтобы Flink мог перезапустить ее.

Давайте разберем основные концепции Flink.
Основные концепции
Потоки и операторы
Позвольте мне начать с самой простой картины и постепенно двигаться дальше.
Каждая программа Flink представляет собой граф потока данных: набор операторов, соединенных потоками данных. Не беспокойтесь, если сейчас это звучит абстрактно — мы будем строить картину по частям, и все быстро станет понятно.
Источники генерируют данные (считывают данные из Kafka, файла, базы данных).
Операторы преобразуют его.
Приемники обрабатывают выходные данные (запись в базу данных, в другую тему Kafka, на панель мониторинга).
Оператор — это единица обработки логики. В нашей системе рекомендаций оператор может отфильтровывать трафик ботов, обогащать событие метаданными о продукте или подсчитывать количество просмотров каждого продукта. Каждый оператор получает записи из одного или нескольких входных потоков, обрабатывает их и отправляет записи в один или несколько выходных потоков.
Поток — это последовательность записей, передаваемых между операторами. В нашем случае это поток пользовательских событий: события просмотра, события кликов, события покупки — одно за другим по мере их возникновения.
Это базовая структура любого задания Flink.
Параллелизм
Одна машина может быстро обрабатывать события, но если вы работаете с миллионами пользователей, одной машины недостаточно. Flink решает эту проблему, запуская каждый оператор параллельно : каждый оператор разбивается на несколько подзадач , которые выполняются одновременно на разных машинах в вашем кластере.
Если у вас есть оператор Filter с уровнем параллелизма 4, то одновременно работают 4 экземпляра, каждый из которых обрабатывает свою часть потока. Добавьте больше машин, получите больше подзадач, обрабатывайте большие объемы. Именно так Flink масштабируется до миллиардов событий в день.
Для нашей системы рекомендаций это означает, что агрегирование окон для 10 миллионов пользователей выполняется не последовательно на одной машине, а распределяется между десятками рабочих процессов.
Состояние
Вернемся к нашей системе рекомендаций: когда пользователь просматривает товар, само по себе это событие практически ничего не говорит. Необходим контекст. Что еще этот пользователь просматривал за последние несколько минут? Просматривал ли он товары из той же категории? Чуть не купил ли он что-то похожее в прошлый раз? Чтобы ответить на эти вопросы, вашей системе нужна память — ей нужно помнить, что произошло раньше.
На заре обработки потоковых данных большинство систем были без сохранения состояния. Каждое событие обрабатывалось изолированно: оператор видел событие, преобразовывал его и переходил к следующему. Никакой памяти о том, что было раньше. Это хорошо работало для простых конвейеров — фильтрации бот-трафика, обогащения событий метаданными из таблицы поиска. Но это было принципиально слишком ограничено для чего-либо, требующего анализа закономерностей во времени.
Подумайте, что на самом деле должна делать наша система рекомендаций. Для каждого входящего события она должна спрашивать: «Что делал пользователь u-8821 за последние 10 минут?» Чтобы ответить на этот вопрос, кто-то должен вести постоянно обновляемый список последних событий пользователя u-8821, последних событий пользователя u-1042 и всех остальных пользователей. Это состояние — данные, которые накапливаются и изменяются по мере прохождения записей через оператор, а не формируются заново из каждой отдельной записи.
Flink делает состояние первоклассным понятием. Оператор может явно объявить состояние — счетчик, хеш-таблицу с ключом по идентификатору пользователя, отсортированный список последних событий. Flink предоставляет вам это состояние в виде управляемого объекта, который вы можете читать и записывать во время обработки. Для нашей системы рекомендаций состояние может представлять собой хеш-таблицу от идентификатора пользователя к «списку идентификаторов товаров, просмотренных за последние 10 минут». Каждый раз, когда поступает новое событие просмотра, вы находите пользователя на карте, добавляете товар и удаляете события старше 10 минут.
Но управление состоянием в распределенной системе — задача действительно сложная. Что происходит, когда машина, на которой работает ваш оператор, выходит из строя? Хэш-карта в оперативной памяти исчезает. Flink решает эту проблему: он периодически делает снимки всего состояния оператора в надежное хранилище, поэтому при восстановлении он может восстановить все до состояния, которое было до сбоя. И он гарантирует, что обновления состояния применяются ровно один раз — даже если машина выходит из строя и те же события воспроизводятся во время восстановления, ваши счетчики не удвоятся.
В одной из будущих статей об архитектуре мы подробно рассмотрим, как Flink обеспечивает гарантию точности до одного раза. А пока знайте, что Flink предоставляет вам состояние, которое ощущается таким же надежным, как запись в базу данных, с производительностью хэш-карты в оперативной памяти.
Windows
У нас есть поток пользовательских событий, параллельно работающие операторы и состояние, накапливающееся для каждого пользователя. И вот тут возникает проблема, которая почти сразу же проявляется при любой реальной агрегации.
Допустим, вы хотите вычислить «10 самых просматриваемых товаров за последние 5 минут» — для раздела «Тренды сейчас» на вашем сайте. У вас есть оператор, который подсчитывает просмотры каждого товара. Но ваш поток бесконечен. Когда же вы должны выдать результат? Вы не можете ждать, пока поступят «все события» — они никогда не прекращаются.
Вам нужен способ разбить бесконечный поток на конечные части и выполнить вычисления для каждой части. Это и есть окно .
Окно — это ограниченный фрагмент вашего потока данных. Вы определяете его, Flink группирует события в этот фрагмент, и когда фрагмент «завершен», он выполняет агрегацию и выдает результат. Flink имеет несколько типов окон: скользящие окна, окна сдвига, окна сессии и т. д. Понимание различий между типами окон не очень важно, но суть окон заключается в том, что они анализируют данные за определенный период времени.
Фрагменты из оригинальной статьи
Я некоторое время посвятил чтению статьи 2015 года об Apache Flink — «Apache Flink: потоковая и пакетная обработка в одном движке» авторов Carbone, Katsifodimos, Ewen, Markl, Haridi и Tzoumas. Вот несколько моментов из этой статьи, которые дополняют то, что мы рассмотрели выше:
О отказоустойчивости и гарантиях точного однократного выполнения.
В статье семантика «точно один раз» описывается следующим образом: «Flink предлагает строгие гарантии согласованности обработки «точно один раз» для операторов с сохранением состояния за счет сочетания распределенных снимков и частичного повторного выполнения при восстановлении». Ключевая фраза здесь — *частичное* повторное выполнение: при сбое машины Flink не перезапускает всю задачу с самого начала. Он откатывает все операторы к их последнему успешному снимку, а затем воспроизводит только входные данные с этого момента. Максимальный объем повторной обработки ограничен промежутком между двумя последовательными контрольными точками — это настраиваемый параметр.
Механизм, обеспечивающий бесперебойную работу вычислений, называется асинхронным созданием снимков барьеров (Asynchronous Barrier Snapshotting, ABS) — и это действительно умно. Мы подробно рассмотрим его в следующем посте. Но суть в следующем: Flink внедряет специальные «барьерные» маркеры в поток данных, которые проходят через операторы как обычные записи. Когда оператор получает барьер, он делает снимок его состояния в постоянном хранилище и пересылает барьер дальше по потоку — и всё это, продолжая обрабатывать записи. Никаких пауз, никаких зависаний, никаких пропущенных событий.
Об унифицированной пакетной и потоковой обработке
Одно из самых ясных утверждений в статье звучит так: «Ограниченный набор данных — это частный случай неограниченного потока данных». Авторы выдвигают не просто техническое, а философское утверждение. И они подкрепляют его: «Пакетные вычисления выполняются той же средой выполнения, что и потоковые вычисления. Исполняемый файл среды выполнения может быть параметризован блокированными потоками данных, чтобы разбить большие вычисления на изолированные этапы, которые планируются последовательно».
Проще говоря: в Flink нет отдельного механизма пакетной обработки. Пакетные задания выполняются на той же распределенной среде выполнения потоков данных, которая обрабатывает ваши потоки Kafka. Единственное отличие заключается в том, что пакетные задания используют «блокированный» обмен данными между этапами — оператор, обрабатывающий данные вышестоящего потока, завершается полностью до того, как начнется оператор, обрабатывающий данные нижестоящего потока. Все остальное — модель оператора, управление состоянием, сериализация — идентично.
Возвращаясь к нашей системе рекомендаций: это означает, что задача, подсчитывающая тренды просмотров в реальном времени, и задача, обрабатывающая события за 6 месяцев для переобучения модели, могут использовать одни и те же операторы, один и тот же кластер и одну и ту же кодовую базу. В статье обещается, что архитектура Lambda — с её двумя системами и двумя кодовыми базами — просто больше не нужна.
Подведение итогов
Давайте быстро перейдем к краткому изложению:
Данные генерируются в виде непрерывных потоков, но исторически мы обрабатывали их партиями, что приводило к задержкам и сложностям в обслуживании двух систем.
Flink основан на понимании того, что пакетная обработка — это лишь частный случай потоковой обработки , и объединяет оба типа обработки в едином движке.
Основные составляющие части системы: операторы (логика обработки), потоки (данные в движении), состояние (память, сохраняющаяся между записями) и окна (ограниченные фрагменты потока для вычислений).
Встроена отказоустойчивость с гарантией однократного срабатывания .
В идеале мне бы очень хотелось подробно рассмотреть каждую из этих тем (а их немало), но этот пост и так получился довольно длинным, поэтому пока я отложу это на будущее, Санил. Вы также можете подписаться на меня в LinkedIn, чтобы получать более короткие посты и узнавать о том, что я изучаю в данный момент.
Мы много говорили об Apache Kafka (учитывая, что это основа большинства архитектур обработки данных), но задумывались ли вы когда-нибудь, как работает Apache Kafka и какова его архитектура? Я был удивлен, узнав, насколько проста Kafka на самом деле внутри. Я написал об этом подробную статью в блоге —
Серия статей по проектированию систем: Apache Kafka с высоты птичьего полета.
Давайте разберемся, что такое Kafka, как она работает и когда ее следует использовать! medium.com
Если вам нужна более подробная информация, я бы порекомендовал ознакомиться с одной из моих самых популярных статей о Temporal, инструменте для организации рабочих процессов, где подробно объясняется, как планируются, запускаются и завершаются события.
Серия статей по системному проектированию: пошаговый анализ внутренней архитектуры Temporal.
Пошаговое подробное изучение архитектуры Temporal — включая рабочие процессы, задачи, сегменты, разделы и то, как Temporal…medium.com
Источник: towardsdatascience.com

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