Kafka Streams API

Шаг за рамки Hello World

Иван Пономарёв, КУРС/МФТИ

Наш план

kafka

Лекция 1.

  1. Kafka (краткое напоминание) и Data Streaming

  2. Конфигурация приложения. Простые (stateless) трансформации

  3. Трансформации с использованием локального состояния

Лекция 2.

  1. Дуализм «поток—таблица» и табличные join-ы

  2. Время и оконные операции

kafka

Таблицы vs стримы

Местонахождение пользователя

stream table animation latestLocation
Michael G. Noll. Of Streams and Tables in Kafka and Stream Processing

Таблицы vs стримы

Количество посещенных мест

stream table animation numVisitedLocations
Michael G. Noll. Of Streams and Tables in Kafka and Stream Processing

Таблицы vs стримы

Производная и интеграл

\[\huge state(now) = \int\limits_{t=0}^{now} stream(t)\, \mathrm{d}t \quad\quad stream(t) = \frac{\mathrm{d}state(t)}{\mathrm{d}t}\]

Martin Kleppmann, “Designing Data Intensive Applications”

Table-table join

\[\huge (uv)'= u'v + uv'\]
table table

Table-Table join

\[\huge (uv)'= u'v + uv'\]
table table1

Table-Table join

\[\huge (uv)'= u'v + uv'\]
table table2

Топология Table-Table join

join storages

Переписываем totalling app при помощи KTable

KTable<String, Long> totals = input.groupByKey().aggregate(
    () -> 0L,
    (k, v, a) -> a + Math.round(v.getAmount() * v.getOdds()),
    Materialized.with(Serdes.String(), Serdes.Long())
);
$kafka-topics --zookeeper localhost --describe

Topic:
table2-demo-KSTREAM-AGGREGATE-STATE-STORE-0000000001-changelog
PartitionCount:10
ReplicationFactor:1
Configs:cleanup.policy=compact

Получаем таблицу счетов матчей

KStream<String, Score> scores =
    eventScores.flatMap((k, v) ->
        Stream.of(Outcome.H, Outcome.A).map(o ->
            KeyValue.pair(String.format("%s:%s", k, o), v))
            .collect(Collectors.toList()))
    .mapValues(EventScore::getScore);

KTable<String, Score> tableScores =
    scores.groupByKey(Grouped.with(...).reduce((a, b) -> b);
$kafka-topics --zookeeper localhost --list

table2-demo-KSTREAM-REDUCE-STATE-STORE-0000000006-repartition
table2-demo-KSTREAM-REDUCE-STATE-STORE-0000000006-changelog

Демо: Объединяем сумму ставок с текущим счётом

KTable<String, String> joined =
    totals.join(tableScores,
            (total, eventScore) ->
                String.format("(%s)\t%d", eventScore, total));

Копартиционирование

Join работает

copart norm

Несовпадение количества партиций

Join не работает (Runtime Exception)

copart diff

Несовпадение алгоритма партицирования

Join не работает молча!

copart diff algorithm

GlobalKTable

Реплицируется всюду целиком

GlobalKTable<...> global = streamsBuilder.globalTable("global", ...);
globalktable

Foreign Key Joins: join + ForeignKeyExtractor

fkjoin

Операции между стримами и таблицами: сводка

streams stateful operations

Виды Join-ов: Table-Table

table table

Виды Join-ов: Table-Table

table table1

Виды Join-ов: Table-Table

table table2

Виды Join-ов: Stream-Table

stream table

Виды Join-ов: Stream-Stream

stream stream

Наш план

kafka

Лекция 1.

  1. Kafka (краткое напоминание) и Data Streaming

  2. Конфигурация приложения. Простые (stateless) трансформации

  3. Трансформации с использованием локального состояния

Лекция 2.

  1. Дуализм «поток—таблица» и табличные join-ы

  2. Время и оконные операции

kafka

Сохранение Timestamped-значений в RocksDB

WindowKeySchema.java

static Bytes toStoreKeyBinary(byte[] serializedKey,
                              long timestamp,
                              int seqnum) {
    ByteBuffer buf = ByteBuffer.allocate(
                                serializedKey.length
                                + TIMESTAMP_SIZE
                                + SEQNUM_SIZE);
    buf.put(serializedKey);
    buf.putLong(timestamp);
    buf.putInt(seqnum);
    return Bytes.wrap(buf.array());
}

Быстрое извлечение значений по ключу из диапазона времени

timestamped record

Демо: Windowed Joins

  • «Послегольщик» — игрок, пытающийся протолкнуть правильную ставку в момент смены счёта в матче

  • Штамп времени ставки и события смены счёта должны «почти совпадать».

livebet

Время, вперёд!

KStream<String, Bet> bets = streamsBuilder.stream(BET_TOPIC,
    Consumed.with(
            Serdes...)
            .withTimestampExtractor(

                (record, previousTimestamp) ->
                    ((Bet) record.value()).getTimestamp()

            ));

(Ещё время можно извлечь из WallClock и RecordMetadata.)

Демо: Windowed Joins

По событию смены счёта понимаем, какая ставка будет «правильной»:

Score current = Optional.ofNullable(stateStore.get(key))
                .orElse(new Score());
stateStore.put(key, value.getScore());

Outcome currenOutcome =
    value.getScore().getHome() > current.getHome()
    ?
    Outcome.H : Outcome.A;

Демо: Windowed Joins

KStream<String, String> join = bets.join(outcomes,
    (bet, sureBet) ->

    String.format("%s %dms before goal",
                bet.getBettor(),
                sureBet.getTimestamp() - bet.getTimestamp()),
                JoinWindows.of(Duration.ofSeconds(1)).before(Duration.ZERO),
                StreamJoined.with(Serdes....
    ));

Tumbling window

TimeWindowedKStream<..., ...> windowed =
    stream.groupByKey()
        .windowedBy(TimeWindows.of(Duration.ofSeconds(20)));
tumbling window
Источник: Kafka Streams in Action

Tumbling window

TimeWindowedKStream<..., ...> windowed =
    stream.groupByKey()
        .windowedBy(TimeWindows.of(Duration.ofSeconds(20)));

KTable<Windowed<...>, Long> count = windowed.count();

/*
* Windowed<K> interface:
* - K key()
* - Window window()
* -- Instant startTime()
* -- Instant endTime()
*/

Hopping Window

TimeWindowedKStream<..., ...> windowed =
    stream.groupByKey()
        .windowedBy(TimeWindows.of(Duration.ofSeconds(20))
                        .advanceBy(Duration.ofSeconds(10)));
hopping window
Источник: Kafka Streams in Action

Session Window

SessionWindowedKStream<..., ...> windowed =
    stream.groupByKey()
        .windowedBy(SessionWindows.with(Duration.ofMinutes(5)));
streams session windows 02

Window Retention time vs. Grace Time

window retention

Иногда нужны не окна, а Punctuator

metronome
class MyTransformer implements Transformer<...> {
    @Override
    public void init(ProcessorContext context) {

        context.schedule(
            Duration.ofSeconds(10),
            PunctuationType.WALL_CLOCK_TIME,
            timestamp->{. . .});

    }

Наш план

kafka

Лекция 1.

  1. Kafka (краткое напоминание) и Data Streaming

  2. Конфигурация приложения. Простые (stateless) трансформации

  3. Трансформации с использованием локального состояния

Лекция 2.

  1. Дуализм «поток—таблица» и табличные join-ы

  2. Время и оконные операции

kafka

Пора закругляться!

Kafka Streams in Action

KSIA
  • William Bejeck,
    “Kafka Streams in Action, Second Edition”, Spring 2023?

  • Первое издание устарело!

Kafka: The Definitive Guide

kafka the definitive guide
  • Gwen Shapira, Todd Palino, Rajini Sivaram, Krit Petty

  • November 2021

Другие источники

Сообщества, конференции

Некоторые итоги

  • Kafka StreamsAPI — это удобная абстракция над «сырой» Кафкой

  • Чтобы начать пользоваться, надо настроить мышление под потоковую обработку

  • Технология переживает бурное развитие

    • + живой community, есть шанс повлиять на процесс самому

    • - публичные интерфейсы изменяются очень быстро

На этом всё!

Спасибо!