Источник Vs PTransform

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

В документации четко указано, что я должен использовать Source и Sink, но я вижу, что многие люди используют напрямую PTransform, связанный с PInput или PDone.

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

Если я ссылаюсь на код, используются два метода. Я не вижу никакого варианта использования, где будет более интересно использовать API-интерфейс PTransform.

Предполагается, что API-интерфейс Source/Sink должен заменить API PTranform?

Я пропустил что-то, что четко различает два метода?

Является ли API-интерфейс Source/Sink достаточно стабильным, чтобы считаться хорошим способом кодирования входов и выходов?

Спасибо за совет!

Ответ 1

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

Возьмите, например, Google Cloud Datastore. В фрагменте кода:

    PCollection<Entity> entities =
      p.apply(DatastoreIO.readFrom(dataset, query));
    ...
    p.apply(some processing)
     .apply(DatastoreIO.writeTo(dataset));

возвращаемый тип DatastoreIO.readFrom(dataset, query) является подклассом PTransform<PBegin, PCollection<Entity>>, а тип DatastoreIO.writeTo(dataset) является подклассом PTransform<PCollection<Entity>, PDone>.

Верно, что эти функции находятся под капотом, реализованным с использованием классов Source и Sink, но для пользователя, который просто хочет что-то прочитать или записать что-то в Datastore, что деталь реализации, которая обычно не имеет значения ( однако см. примечание в конце этого ответа об экспонировании класса Source или Sink). Любой соединитель или, если на то пошло, любая другая задача обработки данных - это PTransform.

Примечание. В настоящее время разъемы, которые читают откуда-то, имеют значение PTransform<PBegin, PCollection<T>>, а коннекторы, которые пишут где-то, имеют тенденцию PTransform<PCollection<T>, PDone>, но мы рассматриваем варианты, которые упрощают использование соединителей более гибкими способами (например, считая из PCollection имен файлов).

Однако, конечно, эта деталь важна для тех, кто хочет внедрить новый соединитель. В частности, вы можете спросить:

В: Почему мне нужны классы Source и Sink вообще, если бы я мог реализовать свой соединитель как PTransform?

A: Если вы можете реализовать свой коннектор, просто используя встроенные преобразования (например, ParDo, GroupByKey и т.д.), это совершенно правильный способ разработки коннектора. Однако классы Source и Sink предоставляют некоторые низкоуровневые возможности, которые в случае необходимости вам были бы громоздкими или невозможными для развития.

Например, BoundedSource и UnboundedSource предоставляют крючки для управления процессом распараллеливания (как первоначальное, так и динамическое перебалансирование работы - BoundedSource.splitIntoBundles, BoundedReader.splitAtFraction), тогда как эти крючки в настоящее время не отображаются для произвольных DoFn s.

Вы можете технически реализовать парсер для формата файла, написав DoFn<FilePath, SomeRecord>, который принимает имя файла как ввод, считывает файл и испускает SomeRecord, но этот DoFn не сможет динамически распараллелить части чтения файл для нескольких сотрудников в случае, если файл оказался очень большим во время выполнения. С другой стороны, FileBasedSource имеет встроенную встроенную возможность, а также обработку файловых шаблонов glob и т.д.

Аналогично, вы можете попробовать реализовать соединитель для потоковой системы, реализовав DoFn, который принимает фиктивный элемент в качестве входных данных, устанавливает соединение и передает все элементы в ProcessingContext.output(), но DoFn в настоящее время не поддерживает написание неограниченных объемов вывода из одного пакета, а также явное поддержка механизмов проверки и дедупликации, необходимых для обеспечения надежной последовательности. Dataflow предоставляет потоковые конвейеры. UnboundedSource, с другой стороны, поддерживает все это.

Sink (точнее, Write.to() PTransform) также интересен: это просто составное преобразование, которое вы могли бы написать сами, если хотите (т.е. он не имеет жестко запрограммированной поддержки в бегунке Dataflow или бэкэнд), но он был разработан с учетом типичных распределенных ошибок, связанных с ошибками, возникающих при параллельном письме данных в систему хранения, и он обеспечивает перехваты, которые заставляют вас учитывать эти проблемы: например, поскольку наборы данных записываются параллельно, и некоторые пучки могут быть повторены или дублированы для отказоустойчивости, есть крючок для "фиксации" только результатов успешно завершенных пакетов (WriteOperation.finalize).

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

Q: Предположим, я решил использовать Source и Sink. Затем как я могу упаковать свой коннектор в виде библиотеки: должен ли я просто предоставить класс Source или Sink, или я должен его перенести в PTransform?

A: В конечном итоге ваш коннектор должен быть упакован как PTransform,, чтобы пользователь мог просто p.apply() его в своем конвейере. Однако под капотом ваше преобразование может использовать классы Source и Sink.

Общий шаблон состоит в том, чтобы выставлять классы Source и Sink, используя шаблон Fluent Builder и позволяя пользователю обернуть их в преобразование Read.from() или Write.to(), но это не строгое требование.