Окна сеанса Apache Beam для каждого пользователя не объединены - PullRequest
2 голосов
/ 18 марта 2019

У нас есть приложение, в котором есть пользователи; каждый пользователь использует наше приложение примерно на 10-40 минут за один раз, и я хотел бы подсчитать распределение / вхождения событий, происходящих за каждую такую ​​сессию, на основе конкретных событий, которые произошли (например, «этот пользователь преобразован», «этот пользователь»). была проблема с последней сессией "," у этого пользователя была успешная последняя сессия ").

(После этого я хотел бы считать эти события более высокого уровня в день, но это отдельный вопрос)

Для этого я изучал окна сеансов; но все документы кажутся ориентированными на глобальные окна сеансов, но я бы хотел создать их для каждого пользователя (что также является естественным разделением).

У меня проблемы с поиском документации (предпочтительно на python), как это сделать. Не могли бы вы указать мне правильное направление?

Или другими словами: Как создать окна для каждого сеанса для каждого пользователя, которые могут выводить более структурированные (обогащенные) события?

Что у меня есть

class DebugPrinter(beam.DoFn):
  """Just prints the element with logging"""
  def process(self, element, window=beam.DoFn.WindowParam):
    _, x = element
    logging.info(">>> Received %s %s with window=%s", x['jsonPayload']['value'], x['timestamp'], window)
    yield element

def sum_by_event_type(user_session_events):
  logging.debug("Received %i events: %s", len(user_session_events), user_session_events)
  d = {}
  for key, group in groupby(user_session_events, lambda e: e['jsonPayload']['value']):
    d[key] = len(list(group))
  logging.info("After counting: %s", d)
  return d

# ...

by_user = valid \
  | 'keyed_on_user_id'      >> beam.Map(lambda x: (x['jsonPayload']['userId'], x))

session_gap = 5 * 60 # [s]; 5 minutes

user_sessions = by_user \
  | 'user_session_window'   >> beam.WindowInto(beam.window.Sessions(session_gap),
                                               timestamp_combiner=beam.window.TimestampCombiner.OUTPUT_AT_EOW) \
  | 'debug_printer'         >> beam.ParDo(DebugPrinter()) \
  | beam.CombinePerKey(sum_by_event_type)

Что он выводит

INFO:root:>>> Received event_1 2019-03-12T08:54:29.200Z with window=[1552380869.2, 1552381169.2)
INFO:root:>>> Received event_2 2019-03-12T08:54:29.200Z with window=[1552380869.2, 1552381169.2)
INFO:root:>>> Received event_3 2019-03-12T08:54:30.400Z with window=[1552380870.4, 1552381170.4)
INFO:root:>>> Received event_4 2019-03-12T08:54:36.300Z with window=[1552380876.3, 1552381176.3)
INFO:root:>>> Received event_5 2019-03-12T08:54:38.100Z with window=[1552380878.1, 1552381178.1)

Итак, как вы можете видеть; окно Session () не расширяет окно, а группирует только очень близкие события ... Что не так?

1 Ответ

0 голосов
/ 01 апреля 2019

Вы можете заставить его работать, добавив преобразование Group By Key после оконного управления. Вы назначили ключи для записей, но на самом деле не сгруппировали их по ключам, а окно сеанса (которое работает по ключам) не знает, что эти события необходимо объединить.

Чтобы подтвердить это, я сделал воспроизводимый пример с некоторыми фиктивными данными в памяти (чтобы изолировать Pub / Sub от проблемы и иметь возможность быстрее ее протестировать). Все пять событий будут иметь одинаковый ключ или user_id, но они будут «прибывать» последовательно на расстоянии 1, 2, 4 и 8 секунд друг от друга. Поскольку я использую session_gap из 5 секунд, я ожидаю, что первые 4 элемента будут объединены в одном сеансе. 5-е событие займет 8 секунд после 4-го, поэтому оно должно быть перенесено на следующую сессию (разрыв более 5 с). Данные создаются так:

data = [{'user_id': 'Thanos', 'value': 'event_{}'.format(event), 'timestamp': time.time() + 2**event} for event in range(5)]

Мы используем beam.Create(data) для инициализации конвейера и beam.window.TimestampedValue для назначения «поддельных» временных отметок. Опять же, мы просто симулируем потоковое поведение с этим. После этого мы создаем пары ключ-значение благодаря полю user_id, добавляем window.Sessions и добавляем пропущенный шаг beam.GroupByKey(). Наконец, мы регистрируем результаты в слегка измененной версии DebugPrinter :. Теперь конвейер выглядит так:

events = (p
  | 'Create Events' >> beam.Create(data) \
  | 'Add Timestamps' >> beam.Map(lambda x: beam.window.TimestampedValue(x, x['timestamp'])) \
  | 'keyed_on_user_id'      >> beam.Map(lambda x: (x['user_id'], x))
  | 'user_session_window'   >> beam.WindowInto(window.Sessions(session_gap),
                                             timestamp_combiner=window.TimestampCombiner.OUTPUT_AT_EOW) \
  | 'Group' >> beam.GroupByKey()
  | 'debug_printer'         >> beam.ParDo(DebugPrinter()))

, где DebugPrinter:

class DebugPrinter(beam.DoFn):
  """Just prints the element with logging"""
  def process(self, element, window=beam.DoFn.WindowParam):
    for x in element[1]:
      logging.info(">>> Received %s %s with window=%s", x['value'], x['timestamp'], window)

    yield element

Если мы проверим это без группировки по ключу, мы получим такое же поведение:

INFO:root:>>> Received event_0 1554117323.0 with window=[1554117323.0, 1554117328.0)
INFO:root:>>> Received event_1 1554117324.0 with window=[1554117324.0, 1554117329.0)
INFO:root:>>> Received event_2 1554117326.0 with window=[1554117326.0, 1554117331.0)
INFO:root:>>> Received event_3 1554117330.0 with window=[1554117330.0, 1554117335.0)
INFO:root:>>> Received event_4 1554117338.0 with window=[1554117338.0, 1554117343.0)

Но после добавления окна теперь работают как положено. События с 0 по 3 объединяются в расширенное 12-секундное окно сеанса. Событие 4 принадлежит отдельному сеансу 5s.

INFO:root:>>> Received event_0 1554118377.37 with window=[1554118377.37, 1554118389.37)
INFO:root:>>> Received event_1 1554118378.37 with window=[1554118377.37, 1554118389.37)
INFO:root:>>> Received event_3 1554118384.37 with window=[1554118377.37, 1554118389.37)
INFO:root:>>> Received event_2 1554118380.37 with window=[1554118377.37, 1554118389.37)
INFO:root:>>> Received event_4 1554118392.37 with window=[1554118392.37, 1554118397.37)

Полный код здесь

Две дополнительные вещи, которые стоит упомянуть. Во-первых, даже при локальном запуске этого на одном компьютере с DirectRunner записи могут быть неупорядоченными (в моем случае event_3 обрабатывается до event_2). Это сделано с целью симулировать распределенную обработку, как описано здесь .

Последнее, что если вы получите трассировку стека, как это:

TypeError: Cannot convert GlobalWindow to apache_beam.utils.windowed_value._IntervalWindowBase [while running 'Write Results/Write/WriteImpl/WriteBundles']

понижение с 2.10.0 / 2.11.0 SDK до 2.9.0. См. Например ответ .

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...