$30 off During Our Annual Pro Sale. View Details »

Иван Пономарёв — Kafka Streams API: шаг за рамки Hello World

Иван Пономарёв — Kafka Streams API: шаг за рамки Hello World

Поточные архитектуры продолжают набирать популярность, но докладов, которые идут дальше тривиальных примеров, по-прежнему немного.
Пора открывать капот и смотреть, как оно устроено.

Тем, кто решит впервые попробовать создать рабочее приложение при помощи Kafka Streams API, предстоит освоить немало новых вещей и соответствующим образом настроить мышление.

- С чего начать?
- Как работает хранение и репликация локального состояния?
- Что такое RocksDB и как её возможности используются в Kafka Streams «под капотом»?
- Что за страшные слова: «репартиционирование» и «копартиционирование»?
- Какова семантика джойнов и оконных операций?
- Как писать тесты?
- Как отлаживать систему «на ходу»?
- Что делать с исключениями?

На эти вопросы мы попытаемся ответить, по пути рассмотрев несколько демо-примеров кода с использованием Spring, двигаясь от простого к сложному.

Доклад представляет собой расширенную версию доклада на конференции JPoint 2019.

Moscow JUG

July 25, 2019
Tweet

More Decks by Moscow JUG

Other Decks in Programming

Transcript

  1. Kafka Streams API
    Шаг за рамки Hello World
    Иван Пономарёв, КУРС/МФТИ
     @inponomarev
    [email protected]
    1

    View Slide

  2. Tech Lead at KURS
    ERP systems & Java background
    Speaker at JPoint, Devoops, Heisenbug, JUG.MSK,
    PermDevDay, DevopsForum, Стачка etc.
    Текущий проект: Real-time Webscraping
    2

    View Slide

  3. 3

    View Slide

  4. Celesta & 2bass — хабрапосты:
    Миграция схемы данных без головной боли: идемпотентность и
    конвергентность для DDL-скриптов
    Celesta 7.x: ORM, миграции и тестирование «в одном флаконе»
    4

    View Slide

  5. Celesta 7.x
    Только Java (отказались от Jython)
    Maven Plugin
    JUnit5 extension
    Spring Boot Starter
    5

    View Slide

  6. Всё, что я показываю, есть на гитхабе
    Слайды:
    Исходники:
    inponomarev.github.io/kstreams-examples
    github.com/inponomarev/kstreams-examples
    6

    View Slide

  7. Зачем нам Kafka?
    7

    View Slide

  8. Зачем нам Kafka?
    8

    View Slide

  9. Зачем нам Kafka?
    Web-scraping в реальном времени
    8

    View Slide

  10. Зачем нам Kafka?
    Web-scraping в реальном времени
    500 запросов/сек (да, это очень мало!)
    8

    View Slide

  11. Зачем нам Kafka?
    Web-scraping в реальном времени
    500 запросов/сек (да, это очень мало!)
    Удобные штуки «из коробки»:
    Персистентный, но «подрезаемый» лог
    Microbatching
    8

    View Slide

  12. Зачем нам Streams API?
    9

    View Slide

  13. Зачем нам Streams API?
    Real-time stream processing
    9

    View Slide

  14. Зачем нам Streams API?
    Real-time stream processing
    Stream-like API (map / reduce)
    9

    View Slide

  15. Зачем нам Streams API?
    Real-time stream processing
    Stream-like API (map / reduce)
    Под капотом:
    Ребалансировка
    Внутреннее состояние обработчиков (репликация)
    Легкое масштабирование
    9

    View Slide

  16. Disclaimer #1: предполагается базовое понимание Кафки
    Виктор Гамов — Все стримы ведут в Кафку (jug.msk.ru 23/04/2018)
    Виктор Гамов — Kafka Streams IQ: «Зачем нам база данных?» (jug.msk.ru
    08/05/2019)
    10

    View Slide

  17. Disclaimer #2: доклад не о жизни в production!
    Доклады о жизни в production:
    Григорий Кошелев — Когда всё пошло по Кафке (JPoint 2019)
    Никита Сальников-Тарновский — Streaming architecture — шаг за рамки примеров
    кода (devclub.eu 2019.03.26)
    11

    View Slide

  18. Kafka за 30 секунд
    Источник: Kafka. The Definitive Guide
    12

    View Slide

  19. Kafka Message
    Номер партиции
    Ключ
    Значение
    13

    View Slide

  20. Compacted topics
    Источник: Kafka Documentation
    14

    View Slide

  21. Наш план
    1. Конфигурация приложения. Простые
    (stateless) трансформации
    2. Трансформации с использованием локального
    состояния
    3. Дуализм «поток—таблица» и табличные join-ы
    4. Время и оконные операции
    15

    View Slide

  22. Kafka Streams API: общая структура KStreams-приложения
    StreamsConfig config = ...;
    //Здесь устанавливаем всякие опции
    Topology topology = new StreamsBuilder()
    //Здесь строим топологию
    ....build();
    16

    View Slide

  23. Kafka Streams API: общая структура KStreams-приложения
    Топология — конвейер обработчиков:
    17

    View Slide

  24. Kafka Streams API: общая структура KStreams-приложения
    StreamsConfig config = ...;
    //Здесь устанавливаем всякие опции
    Topology topology = new StreamsBuilder()
    //Здесь строим топологию
    ....build();
    //Это за нас делает SPRING-KAFKA
    KafkaStreams streams = new KafkaStreams(topology, config);
    streams.start();
    ...
    streams.close();
    18

    View Slide

  25. В Спринге достаточно определить две вещи
    @Bean KafkaStreamsConfiguration
    @Bean Topology
    19

    View Slide

  26. В Спринге достаточно определить две вещи
    @Bean KafkaStreamsConfiguration
    @Bean Topology
    И не забудьте про @EnableKafkaStreams
    19

    View Slide

  27. Легенда
    Идут футбольные матчи (меняется счёт)
    Делаются ставки: H, D, A.
    Поток ставок, ключ: Cyprus-Belgium:A
    Поток ставок, значение:
    class Bet {
    String bettor; //John Doe
    String match; //Cyprus-Belgium
    Outcome outcome; //A (or H or D)
    long amount; //100
    double odds; //1.7
    long timestamp; //1554215083998
    }
    20

    View Slide

  28. @Bean KafkaConfiguration
    //ВАЖНО!
    @Bean(name =
    KafkaStreamsDefaultConfiguration
    .DEFAULT_STREAMS_CONFIG_BEAN_NAME)
    public KafkaStreamsConfiguration getStreamsConfig() {
    Map props = new HashMap<>();
    //ВАЖНО!
    props.put(StreamsConfig.APPLICATION_ID_CONFIG,
    "stateless-demo-app");
    //ВАЖНО!
    props.put(StreamsConfig.NUM_STREAM_THREADS_CONFIG, 4);
    props.put(StreamsConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
    ...
    KafkaStreamsConfiguration streamsConfig =
    new KafkaStreamsConfiguration(props);
    return streamsConfig;
    }
    21

    View Slide

  29. @Bean NewTopic
    @Bean
    NewTopic getFilteredTopic() {
    Map props = new HashMap<>();
    props.put(
    TopicConfig.CLEANUP_POLICY_CONFIG,
    TopicConfig.CLEANUP_POLICY_COMPACT);
    return new NewTopic("mytopic", 10, (short) 1).configs(props);
    }
    22

    View Slide

  30. @Bean Topology
    @Bean
    public Topology createTopology(StreamsBuilder streamsBuilder) {
    KStream input = streamsBuilder.stream(...);
    KStream gain
    = input.mapValues(v -> Math.round(v.getAmount() * v.getOdds()))
    gain.to(GAIN_TOPIC, Produced.with(Serdes.String(),
    new JsonSerde<>(Long.class)));
    return streamsBuilder.build();
    }
    23

    View Slide

  31. TopologyTestDriver: создание
    KafkaStreamsConfiguration config = new KafkaConfiguration()
    .getStreamsConfig();
    StreamsBuilder sb = new StreamsBuilder();
    Topology topology = new TopologyConfiguration().createTopology(sb);
    TopologyTestDriver topologyTestDriver =
    new TopologyTestDriver(topology,
    config.asProperties());
    24

    View Slide

  32. TopologyTestDriver: использование
    Bet bet = Bet.builder()
    .bettor("John Doe")
    .match("Germany-Belgium")
    .outcome(Outcome.H)
    .amount(100)
    .odds(1.7).build();
    topologyTestDriver.pipeInput(
    betFactory.create(BET_TOPIC, bet.key(), bet));
    25

    View Slide

  33. TopologyTestDriver: использование
    ProducerRecord record =
    topologyTestDriver.readOutput(
    GAIN_TOPIC,
    new StringDeserializer(),
    new JsonDeserializer<>(Long.class)
    );
    assertEquals(bet.key(), record.key());
    assertEquals(170L, record.value().longValue());
    26

    View Slide

  34. Если что-то пошло не так…
    default.deserialization.exception.handler — не смогли
    десериализовать
    default.production.exception.handler — брокер отверг сообщение
    (например, оно слишком велико)
    27

    View Slide

  35. Если всё совсем развалилось
    streams.setUncaughtExceptionHandler(
    (Thread thread, Throwable throwable) -> {
    . . .
    });
    28

    View Slide

  36. Если всё совсем развалилось
    В Спринге всё сложнее (см. код)
    streams.setUncaughtExceptionHandler(
    (Thread thread, Throwable throwable) -> {
    . . .
    });
    28

    View Slide

  37. Состояния приложения KafkaStreams
    29

    View Slide

  38. Что ещё нужно знать про stateless-трансформации?
    30

    View Slide

  39. Простое ветвление стримов
    Java-стримы так не могут:
    KStream<..> foo = ...
    KStream<..> bar = foo.mapValues(…).map... to...
    Kstream<..> baz = foo.filter(…).map... forEach...
    31

    View Slide

  40. Ветвление стримов по условию
    Не используйте KStream.branch, используйте KafkaStreamsBrancher!
    new KafkaStreamsBrancher()
    .branch((key, value) -> value.contains("A"), ks -> ks.to("A"))
    .branch((key, value) -> value.contains("B"), ks -> ks.to("B"))
    .defaultBranch(ks -> ks.to("C"))
    .onTopOf(builder.stream("source"))
    .map(...)
    32

    View Slide

  41. Простое слияние
    KStream foo = ...
    KStream bar = ...
    KStream merge = foo.merge(bar);
    33

    View Slide

  42. Наш план
    1. Конфигурация приложения. Простые (stateless)
    трансформации
    2. Трансформации с использованием локального
    состояния
    3. Дуализм «поток—таблица» и табличные join-ы
    4. Время и оконные операции
    34

    View Slide

  43. Локальное состояние
    Facebook’s RocksDB — что это и зачем?
    Embedded key/value storage
    LSM Tree (Log-Structured Merge-Tree)
    High-performant (data locality)
    Persistent, optimized for SSD
    35

    View Slide

  44. RocksDB похож на TreeMap
    Сохранение K,V в бинарном формате
    Лексикографическая сортировка
    Iterator (snapshot view)
    Удаление диапазона (deleteRange)
    36

    View Slide

  45. Пишем “Bet Totalling App”
    Какова сумма выплат по сделанным ставкам, если сыграет исход?
    37

    View Slide

  46. @Bean Topology
    KStream input = streamsBuilder.
    stream(BET_TOPIC, Consumed.with(Serdes.String(),
    new JsonSerde<>(Bet.class)));
    KStream counted =
    new TotallingTransformer()
    .transformStream(streamsBuilder, input);
    38

    View Slide

  47. Суммирование ставок
    @Override
    public KeyValue transform(String key, Bet value,
    KeyValueStore stateStore) {
    long current = Optional
    .ofNullable(stateStore.get(key))
    .orElse(0L);
    current += value.getAmount();
    stateStore.put(key, current);
    return KeyValue.pair(key, current);
    }
    39

    View Slide

  48. StateStore доступен в тестах
    @Test
    void testTopology() {
    topologyTestDriver.pipeInput(...);
    topologyTestDriver.pipeInput(...);
    KeyValueStore store =
    topologyTestDriver
    .getKeyValueStore(TotallingTransformer.STORE_NAME);
    assertEquals(..., store.get(...));
    assertEquals(..., store.get(...));
    }
    40

    View Slide

  49. Демо: Ребалансировка / репликация
    Ребалансировка / репликация партиций state при запуске / выключении
    обработчиков.
    41

    View Slide

  50. Подробнее о ребалансировке
    Matthias J. Sax Everything You Always Wanted to Know
    About Kafka’s Rebalance Protocol but Were Afraid to Ask
    (Kafka Summit London, 2019)
    42

    View Slide

  51. Сохранение локального состояния в топик
    $kafka-topics --zookeeper localhost --describe
    Topic:bet-totalling-demo-app-totalling-store-changelog
    PartitionCount:10
    ReplicationFactor:1
    Configs:cleanup.policy=compact
    43

    View Slide

  52. Сохранение локального состояния в топик
    $kafka-topics --zookeeper localhost --describe
    Topic:bet-totalling-demo-app-totalling-store-changelog
    PartitionCount:10
    ReplicationFactor:1
    Configs:cleanup.policy=compact
    43

    View Slide

  53. Партиционирование и local state
    44

    View Slide

  54. Партиционирование и local state
    45

    View Slide

  55. Репартиционирование
    Явное при помощи
    through(String topic, Produced produced)
    Неявное при операциях, меняющих ключ + stateful-операциях
    46

    View Slide

  56. Дублирующееся неявное репартиционирование
    KStream source = builder.stream("topic1");
    KStream mapped = source.map(...);
    KTable counts = mapped.groupByKey().aggregate(...);
    KStream sink = mapped.leftJoin(counts, ...);
    47

    View Slide

  57. Избавляемся от дублирующегося репартиционирования
    KStream source = builder.stream("topic1");
    KStream shuffled = source.map(...).through("topic2",..);
    KTable counts = shuffled.groupByKey().aggregate(...);
    KStream sink = shuffled.leftJoin(counts, ...);
    48

    View Slide

  58. Ключ лучше лишний раз не трогать
    Key only: selectKey
    Key and Value Value Only
    map mapValues
    flatMap flatMapValues
    transform transformValues
    flatTransform flatTransformValues
    49

    View Slide

  59. Подробнее про «лишнее» репартиционирование
    Guozhang Wang Performance Analysis and Optimizations for
    Kafka Streams Applications (Kafka Summit London, 2019)
    50

    View Slide

  60. Наш план
    1. Конфигурация приложения. Простые (stateless)
    трансформации
    2. Трансформации с использованием локального
    состояния
    3. Дуализм «поток—таблица» и табличные join-ы
    4. Время и оконные операции
    51

    View Slide

  61. Таблицы vs стримы
    Местонахождение пользователя
    Michael G. Noll. Of Streams and Tables in Kafka and Stream Processing
    52

    View Slide

  62. Таблицы vs стримы
    Количество посещенных мест
    Michael G. Noll. Of Streams and Tables in Kafka and Stream Processing
    53

    View Slide

  63. Таблицы vs стримы
    Производная и интеграл
    Martin Kleppmann, “Designing Data Intensive Applications”
    54

    View Slide

  64. Join
    55

    View Slide

  65. Переписываем totalling app при помощи KTable
    KTable 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
    56

    View Slide

  66. Получаем таблицу счетов матчей
    KStream 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 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
    57

    View Slide

  67. Демо: Объединяем сумму ставок с текущим счётом
    KTable joined =
    totals.join(tableScores,
    (total, eventScore) ->
    String.format("(%s)\t%d", eventScore, total));
    58

    View Slide

  68. Копартиционирование
    Join работает
    59

    View Slide

  69. Несовпадение количества партиций
    Join не работает (Runtime Exception)
    60

    View Slide

  70. Несовпадение алгоритма партицирования
    Join не работает молча!
    61

    View Slide

  71. GlobalKTable
    Реплицируется всюду целиком
    GlobalKTable<...> global = streamsBuilder.globalTable("global", ...);
    62

    View Slide

  72. Операции между стримами и таблицами: сводка
    Источник: Kafka Streams DSL Documentation
    63

    View Slide

  73. Виды Join-ов: Table-Table
    Table 1
    Table 2
    64

    View Slide

  74. Виды Join-ов: Table-Table
    Table 2
    65

    View Slide

  75. Виды Join-ов: Table-Table
    Table 1
    66

    View Slide

  76. Виды Join-ов: Stream-Table
    Stream 1
    Table
    67

    View Slide

  77. Виды Join-ов: Stream-Stream
    Stream 1
    Stream 2
    68

    View Slide

  78. Наш план
    1. Конфигурация приложения. Простые (stateless)
    трансформации
    2. Трансформации с использованием локального
    состояния
    3. Дуализм «поток—таблица» и табличные join-ы
    4. Время и оконные операции
    69

    View Slide

  79. Сохранение 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());
    }
    70

    View Slide

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

    View Slide

  81. Демо: Windowed Joins
    «Послегольщик» — игрок, пытающийся протолкнуть правильную ставку в
    момент смены счёта в матче
    Штамп времени ставки и события смены счёта должны «почти совпадать».
    72

    View Slide

  82. Время, вперёд!
    (Ещё время можно извлечь из WallClock и RecordMetadata.)
    KStream bets = streamsBuilder.stream(BET_TOPIC,
    Consumed.with(
    Serdes...)
    .withTimestampExtractor(
    (record, previousTimestamp) ->
    ((Bet) record.value()).getTimestamp()
    ));
    73

    View Slide

  83. Демо: 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;
    74

    View Slide

  84. Демо: Windowed Joins
    KStream 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)
    Joined.with(Serdes....
    ));
    75

    View Slide

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

    View Slide

  86. Tumbling window
    TimeWindowedKStream<..., ...> windowed =
    stream.groupByKey()
    .windowedBy(TimeWindows.of(Duration.ofSeconds(20)));
    KTable, Long> count = windowed.count();
    /*
    * Windowed interface:
    * - K key()
    * - Window window()
    * -- Instant startTime()
    * -- Instant endTime()
    */
    77

    View Slide

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

    View Slide

  88. Session Window
    SessionWindowedKStream<..., ...> windowed =
    stream.groupByKey()
    .windowedBy(SessionWindows.with(Duration.ofMinutes(5)));
    79

    View Slide

  89. Window Retention time vs. Grace Time
    80

    View Slide

  90. Иногда нужны не окна, а Punctuator
    class MyTransformer implements Transformer<...> {
    @Override
    public void init(ProcessorContext context) {
    context.schedule(
    Duration.ofSeconds(10),
    PunctuationType.WALL_CLOCK_TIME,
    timestamp->{. . .});
    }
    81

    View Slide

  91. Наш план
    1. Конфигурация приложения. Простые (stateless)
    трансформации
    2. Трансформации с использованием локального
    состояния
    3. Дуализм «поток—таблица» и табличные join-ы
    4. Время и оконные операции
    Пора закругляться!
    82

    View Slide

  92. Kafka Streams in Action
    William Bejeck,
    “Kafka Streams in Action”, November 2018
    Примеры кода для Kafka 1.x
    83

    View Slide

  93. Kafka: The Definitive Guide
    Gwen Shapira, Neha Narkhede, Todd Palino
    September 2017
    84

    View Slide

  94. Другие источники
    Исходники!
    docs.confluent.io: Streams Developer Guide
    Getting Your Feet Wet with Stream Processing (Confluent tutorials)
    https://github.com/apache/kafka/
    https://github.com/spring-projects/spring-kafka
    85

    View Slide

  95. Сообщества, конференции
    Телеграм: Грефневая Кафка
    Kafka Summit Conference
    https://t.me/AwesomeKafka_ru
    https://t.me/proKafka
    86

    View Slide

  96. Некоторые итоги
    87

    View Slide

  97. Некоторые итоги
    Kafka StreamsAPI — это удобная абстракция над «сырой» Кафкой
    87

    View Slide

  98. Некоторые итоги
    Kafka StreamsAPI — это удобная абстракция над «сырой» Кафкой
    Чтобы начать пользоваться, надо настроить мышление под потоковую
    обработку
    87

    View Slide

  99. Некоторые итоги
    Kafka StreamsAPI — это удобная абстракция над «сырой» Кафкой
    Чтобы начать пользоваться, надо настроить мышление под потоковую
    обработку
    Технология переживает бурное развитие
    + живой community, есть шанс повлиять на процесс самому
    - публичные интерфейсы изменяются очень быстро
    87

    View Slide

  100. На этом всё!


    Спасибо!
    inponomarev/kstreams-examples
    @inponomarev
    [email protected]
    88

    View Slide