You are viewing a plain text version of this content. The canonical link for it is here.
Posted to user@beam.apache.org by Gruß, Hendrik <hg...@pixum.com> on 2021/01/08 13:56:40 UTC

Side Inputs or CoGroupByKey

Hi everybody,

I would like to hear your thoughts on which technique would be used in
Apache Beam for the following problem:

*Problem definition*:

I have two streams of data, one with pageviews of users, and another with
requests of the users. They share the key session_id which describes the
users session, but each have other additional data.

The goal is to append the number of pageviews in a session to the requests
of that session. That means, I want to have a stream of data that has every
request together with the number of pageviews before the request. It
suffices to have the pageviews of lets say the last 5 minutes, and it is
not important to have all the pageviews, if there is late data. There
should only be low latency on receiving the requests.

What would be the appropriate technique? Side inputs? CoGroupByKey? Here
are my first attempts:
https://stackoverflow.com/questions/65625961/windowed-joins-in-apache-beam
Kind regards,
Hendrik Gruß

----
Hendrik Gruß
Data Engineer
Diginet Gmbh & Co. KG

-- 


Pixum und artboxONE sind geschützte Marken der Diginet GmbH & Co. KG - 
Industriestr.161 - 50999 Köln

Fon: +49 (2236) 886-0 - Fax: +49 (2236) 88 
66 99 Sitz Köln, HRA 25531, Umsatzsteuer-ID: DE-209867661,

Komplementärin: 
Diginet Management GmbH, Sitz Köln, HRB 69766, Geschäftsführer: Daniel 
Attallah, Oliver Thomsen

----------------------------------------------------

Pixum hat die beste 
Bildqualität. Ausgezeichnet von der Stiftung Warentest -  Jetzt mehr 
erfahren: www.pixum.de/testsiege <http://www.pixum.de/testsiege>

----------------------------------------------------

Außergewöhnliche 
Kunstwerke - modern und bezahlbar: Jetzt artboxONE entdecken:  
<http://www.artboxone.de/>www.artboxone.de <http://www.artboxone.de>

 


Re: Side Inputs or CoGroupByKey

Posted by Gruß, Hendrik <hg...@pixum.com>.
Hello, everybody,

I found a solution myself, here's it in case somebody is interested:
Idea

The trick is to combine the two streams using the beam.Flatten operation
and to use a *Stateful DoFn* to compute the number of pageviews before one
request. Each stream contains json dictionaries. I embedded them by
using {'request'
: request} and {'pageview' : pageview} as a surrounding block, so that I
can keep the different events apart in the *Stateful DoFn*. I also computed
things like first pageview timestamp and seconds since first pageview
along. The streams have to use the session_id as a key, such that the *Stateful
DoFn* is receiving all the events of one session only.
Code

First of all, this is the pipeline code:

# Beam pipeline, that extends requests by number of pageviews before
request in that sessionwith beam.Pipeline(options=options) as p:
    # The stream of requests
    requests = (
          'Read from PubSub subscription'   >>
beam.io.ReadFromPubSub(subscription=request_sub)
        | 'Extract JSON'                    >> beam.ParDo(ExtractJSON())
        | 'Add Timestamp'                   >> beam.ParDo(AssignTimestampFn())
        | 'Use Session ID as stream key'    >> beam.Map(lambda
request: (request['session_id'], request))
        | 'Add type of event'               >> beam.Map(lambda r:
(r[0], ('request', r[1])))
    )

    # The stream of pageviews
    pageviews = (
          'Read from PubSub subscription'   >>
beam.io.ReadFromPubSub(subscription=pageview_sub)
        | 'Extract JSON'                    >> beam.ParDo(ExtractJSON())
        | 'Add Timestamp'                   >> beam.ParDo(AssignTimestampFn())
        | 'Use Session ID as stream key'    >> beam.Map(lambda
pageview: (pageview['session_id'], pageview))
        | 'Add type of event'               >> beam.Map(lambda p:
(p[0], ('pageview', p[1])))
    )

    # Combine the streams and apply Stateful DoFn
    combined = (
        (
            p | ('Prepare requests stream' >> requests),
            p | ('Prepare pageviews stream' >> pageviews)
        )
        | 'Combine event streams'       >> beam.Flatten()
        | 'Global Window'               >>
beam.WindowInto(windowfn=window.GlobalWindows(),

trigger=trigger.AfterCount(1),

accumulation_mode=trigger.AccumulationMode.DISCARDING)
        | 'Stateful DoFn'               >> beam.ParDo(CountPageviews())
        | 'Compute processing delay'    >> beam.ParDo(LogTimeDelay())
        | 'Format for BigQuery output'  >> beam.ParDo(FormatForOutputDoFn())
    )

    # Write to BigQuery.
    combined | 'Write' >> beam.io.WriteToBigQuery(
        requests_extended_table,
        schema=REQUESTS_EXTENDED_TABLE_SCHEMA,
        create_disposition=beam.io.BigQueryDisposition.CREATE_IF_NEEDED,
        write_disposition=beam.io.BigQueryDisposition.WRITE_APPEND)

The interesting part is the combination of the two streams using
beam.Flatten and applying the stateful DoFn CountPageviews()

Here's the code of the used custom DoFns:

# This DoFn just loads a json messageclass ExtractJSON(beam.DoFn):
  def process(self, element):
    import json

    yield json.loads(element)
# This DoFn adds the event timestamp of messages into it json elements
for further processingclass AssignTimestampFn(beam.DoFn):
  def process(self, element, timestamp=beam.DoFn.TimestampParam):
    import datetime

    timestamped_element = element
    timestamp_utc = datetime.datetime.utcfromtimestamp(float(timestamp))
    timestamp = timestamp_utc.strftime("%Y-%m-%d %H:%M:%S")
    timestamped_element['timestamp_utc'] = timestamp_utc
    timestamped_element['timestamp'] = timestamp
    yield timestamped_element
# This class is a stateful dofn# Input elements should be of form
(session_id, {'event_type' : event}# Where events can be requests or
pageviews# It computes on a per session basis the number of pageviews
and the first pageview timestamp# in its internal state# Whenever a
request comes in, it appends the internal state to the request and
emits# a extended request# Whenever a pageview comes in, the internal
state is updated but nothing is emittedclass
CountPageviewsStateful(beam.DoFn):
  # The internal states
  NUM_PAGEVIEWS = userstate.CombiningValueStateSpec('num_pageviews',
combine_fn=sum)
  FIRST_PAGEVIEW =
userstate.ReadModifyWriteStateSpec('first_pageview',
coder=beam.coders.VarIntCoder())

  def process(self,
              element,
              num_pageviews_state=beam.DoFn.StateParam(NUM_PAGEVIEWS),
              first_pageview_state=beam.DoFn.StateParam(FIRST_PAGEVIEW)
              ):
    import datetime

    # Extract element
    session_id = element[0]
    event_type, event = element[1]

    # Process different event types
    # Depending on event type, different actions are done
    if event_type == 'request':
        # This is a request
        request = event

        # First, the first pageview timestamp is extracted and the
seconds since first timestamp are calculated
        first_pageview = first_pageview_state.read()
        if first_pageview is not None:
            seconds_since_first_pageview =
(int(request['timestamp_utc'].timestamp()) - first_pageview)

            first_pageview_timestamp_utc =
datetime.datetime.utcfromtimestamp(float(first_pageview))
            first_pageview_timestamp =
first_pageview_timestamp_utc.strftime("%Y-%m-%d %H:%M:%S")
        else:
            seconds_since_first_pageview = -1
            first_pageview_timestamp = None

        # The calculated data is appended to the request
        request['num_pageviews'] = num_pageviews_state.read()
        request['first_pageview_timestamp'] = first_pageview_timestamp
        request['seconds_since_first_pageview'] = seconds_since_first_pageview

        # The pageview counter is reset
        num_pageviews_state.clear()

        # The request is returned
        yield (session_id, request)
    elif event_type == 'pageview':
        # This is a pageview
        pageview = event

        # Update first pageview state
        first_pageview = first_pageview_state.read()
        if first_pageview is None:
            first_pageview_state.write(int(pageview['timestamp_utc'].timestamp()))
        elif first_pageview > int(pageview['timestamp_utc'].timestamp()):
            first_pageview_state.write(int(pageview['timestamp_utc'].timestamp()))

        # Increase number of pageviews
        num_pageviews_state.add(1)

        # Do not return anything, pageviews are not further processed
# This DoFn logs the delay between the event time and the processing
timeclass LogTimeDelay(beam.DoFn):
  def process(self, element, timestamp=beam.DoFn.TimestampParam):
    import datetime
    import logging

    timestamp_utc = datetime.datetime.utcfromtimestamp(float(timestamp))
    seconds_delay = (datetime.datetime.utcnow() - timestamp_utc).total_seconds()

    logging.warning('Delayed by %s seconds', seconds_delay)

    yield element

This seems to work and gives me an average delay of about 1-2 seconds on
the direct runner. On Cloud Dataflow the average delay is about 0.5-1
seconds. So all in all, this seems to solve the problem definition.
Further considerations

There are some open questions, though:

   - I am using global windows, which means internal state will be kept
   forever as far as i am concerned. Maybe session windows are the correct way
   to go: When there are no pageviews/requests for x seconds, the window is
   closed and internal state is given free.
   - Processing delay is a little bit high, but maybe I need to tweak the
   pubsub part a little bit.
   - I do not know how much overhead or memory consumption this solution
   adds over standard beam methods. I also didn't test high workload and
   parallelisation.


Any input is welcome!

Kind regards,
Hendrik Gruß

Am Fr., 8. Jan. 2021 um 14:56 Uhr schrieb Gruß, Hendrik <hg...@pixum.com>:

> Hi everybody,
>
> I would like to hear your thoughts on which technique would be used in
> Apache Beam for the following problem:
>
> *Problem definition*:
>
> I have two streams of data, one with pageviews of users, and another with
> requests of the users. They share the key session_id which describes the
> users session, but each have other additional data.
>
> The goal is to append the number of pageviews in a session to the requests
> of that session. That means, I want to have a stream of data that has every
> request together with the number of pageviews before the request. It
> suffices to have the pageviews of lets say the last 5 minutes, and it is
> not important to have all the pageviews, if there is late data. There
> should only be low latency on receiving the requests.
>
> What would be the appropriate technique? Side inputs? CoGroupByKey? Here
> are my first attempts:
> https://stackoverflow.com/questions/65625961/windowed-joins-in-apache-beam
> Kind regards,
> Hendrik Gruß
>
> ----
> Hendrik Gruß
> Data Engineer
> Diginet Gmbh & Co. KG
>


-- 
Hendrik Gruß
Data Engineer

-- 


Pixum und artboxONE sind geschützte Marken der Diginet GmbH & Co. KG - 
Industriestr.161 - 50999 Köln

Fon: +49 (2236) 886-0 - Fax: +49 (2236) 88 
66 99 Sitz Köln, HRA 25531, Umsatzsteuer-ID: DE-209867661,

Komplementärin: 
Diginet Management GmbH, Sitz Köln, HRB 69766, Geschäftsführer: Daniel 
Attallah, Oliver Thomsen

----------------------------------------------------

Pixum hat die beste 
Bildqualität. Ausgezeichnet von der Stiftung Warentest -  Jetzt mehr 
erfahren: www.pixum.de/testsiege <http://www.pixum.de/testsiege>

----------------------------------------------------

Außergewöhnliche 
Kunstwerke - modern und bezahlbar: Jetzt artboxONE entdecken:  
<http://www.artboxone.de/>www.artboxone.de <http://www.artboxone.de>