Ответ 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()
, но это не строгое требование.