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

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

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

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

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

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

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

3fc5b5eb32bd3b48d7810fd67b37f9a1?s=128

Moscow JUG

July 25, 2019
Tweet

Transcript

  1. Kafka Streams API Шаг за рамки Hello World Иван Пономарёв,

    КУРС/МФТИ  @inponomarev ponomarev@corchestra.ru 1
  2. Tech Lead at KURS ERP systems & Java background Speaker

    at JPoint, Devoops, Heisenbug, JUG.MSK, PermDevDay, DevopsForum, Стачка etc. Текущий проект: Real-time Webscraping 2
  3. 3

  4. Celesta & 2bass — хабрапосты: Миграция схемы данных без головной боли: идемпотентность

    и конвергентность для DDL-скриптов Celesta 7.x: ORM, миграции и тестирование «в одном флаконе» 4
  5. Celesta 7.x Только Java (отказались от Jython) Maven Plugin JUnit5

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

    github.com/inponomarev/kstreams-examples 6
  7. Зачем нам Kafka? 7

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

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

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

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

    это очень мало!) Удобные штуки «из коробки»: Персистентный, но «подрезаемый» лог Microbatching 8
  12. Зачем нам Streams API? 9

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

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

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

    / reduce) Под капотом: Ребалансировка Внутреннее состояние обработчиков (репликация) Легкое масштабирование 9
  16. Disclaimer #1: предполагается базовое понимание Кафки Виктор Гамов — Все

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

    жизни в production: Григорий Кошелев — Когда всё пошло по Кафке (JPoint 2019) Никита Сальников-Тарновский — Streaming architecture — шаг за рамки примеров кода (devclub.eu 2019.03.26) 11
  18. Kafka за 30 секунд Источник: Kafka. The Definitive Guide 12

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

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

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

    с использованием локального состояния 3. Дуализм «поток—таблица» и табличные join-ы 4. Время и оконные операции 15
  22. Kafka Streams API: общая структура KStreams-приложения StreamsConfig config = ...;

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

  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
  25. В Спринге достаточно определить две вещи @Bean KafkaStreamsConfiguration @Bean Topology

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

    И не забудьте про @EnableKafkaStreams 19
  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
  28. @Bean KafkaConfiguration //ВАЖНО! @Bean(name = KafkaStreamsDefaultConfiguration .DEFAULT_STREAMS_CONFIG_BEAN_NAME) public KafkaStreamsConfiguration getStreamsConfig()

    { Map<String, Object> 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
  29. @Bean NewTopic @Bean NewTopic getFilteredTopic() { Map<String, String> props =

    new HashMap<>(); props.put( TopicConfig.CLEANUP_POLICY_CONFIG, TopicConfig.CLEANUP_POLICY_COMPACT); return new NewTopic("mytopic", 10, (short) 1).configs(props); } 22
  30. @Bean Topology @Bean public Topology createTopology(StreamsBuilder streamsBuilder) { KStream<String, Bet>

    input = streamsBuilder.stream(...); KStream<String, Long> 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
  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
  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
  33. TopologyTestDriver: использование ProducerRecord<String, Long> record = topologyTestDriver.readOutput( GAIN_TOPIC, new StringDeserializer(),

    new JsonDeserializer<>(Long.class) ); assertEquals(bet.key(), record.key()); assertEquals(170L, record.value().longValue()); 26
  34. Если что-то пошло не так… default.deserialization.exception.handler — не смогли десериализовать default.production.exception.handler — брокер отверг

    сообщение (например, оно слишком велико) 27
  35. Если всё совсем развалилось streams.setUncaughtExceptionHandler( (Thread thread, Throwable throwable) ->

    { . . . }); 28
  36. Если всё совсем развалилось В Спринге всё сложнее (см. код)

    streams.setUncaughtExceptionHandler( (Thread thread, Throwable throwable) -> { . . . }); 28
  37. Состояния приложения KafkaStreams 29

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

  39. Простое ветвление стримов Java-стримы так не могут: KStream<..> foo =

    ... KStream<..> bar = foo.mapValues(…).map... to... Kstream<..> baz = foo.filter(…).map... forEach... 31
  40. Ветвление стримов по условию Не используйте KStream.branch, используйте KafkaStreamsBrancher! new

    KafkaStreamsBrancher<String, String>() .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
  41. Простое слияние KStream<String, Integer> foo = ... KStream<String, Integer> bar

    = ... KStream<String, Integer> merge = foo.merge(bar); 33
  42. Наш план 1. Конфигурация приложения. Простые (stateless) трансформации 2. Трансформации

    с использованием локального состояния 3. Дуализм «поток—таблица» и табличные join-ы 4. Время и оконные операции 34
  43. Локальное состояние Facebook’s RocksDB — что это и зачем? Embedded key/value storage

    LSM Tree (Log-Structured Merge-Tree) High-performant (data locality) Persistent, optimized for SSD 35
  44. RocksDB похож на TreeMap<K,V> Сохранение K,V в бинарном формате Лексикографическая

    сортировка Iterator (snapshot view) Удаление диапазона (deleteRange) 36
  45. Пишем “Bet Totalling App” Какова сумма выплат по сделанным ставкам,

    если сыграет исход? 37
  46. @Bean Topology KStream<String, Bet> input = streamsBuilder. stream(BET_TOPIC, Consumed.with(Serdes.String(), new

    JsonSerde<>(Bet.class))); KStream<String, Long> counted = new TotallingTransformer() .transformStream(streamsBuilder, input); 38
  47. Суммирование ставок @Override public KeyValue<String, Long> transform(String key, Bet value,

    KeyValueStore<String, Long> stateStore) { long current = Optional .ofNullable(stateStore.get(key)) .orElse(0L); current += value.getAmount(); stateStore.put(key, current); return KeyValue.pair(key, current); } 39
  48. StateStore доступен в тестах @Test void testTopology() { topologyTestDriver.pipeInput(...); topologyTestDriver.pipeInput(...);

    KeyValueStore<String, Long> store = topologyTestDriver .getKeyValueStore(TotallingTransformer.STORE_NAME); assertEquals(..., store.get(...)); assertEquals(..., store.get(...)); } 40
  49. Демо: Ребалансировка / репликация Ребалансировка / репликация партиций state при

    запуске / выключении обработчиков. 41
  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
  51. Сохранение локального состояния в топик $kafka-topics --zookeeper localhost --describe Topic:bet-totalling-demo-app-totalling-store-changelog

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

    PartitionCount:10 ReplicationFactor:1 Configs:cleanup.policy=compact 43
  53. Партиционирование и local state 44

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

  55. Репартиционирование Явное при помощи through(String topic, Produced<K, V> produced) Неявное

    при операциях, меняющих ключ + stateful-операциях 46
  56. Дублирующееся неявное репартиционирование KStream source = builder.stream("topic1"); KStream mapped =

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

    = source.map(...).through("topic2",..); KTable counts = shuffled.groupByKey().aggregate(...); KStream sink = shuffled.leftJoin(counts, ...); 48
  58. Ключ лучше лишний раз не трогать Key only: selectKey Key

    and Value Value Only map mapValues flatMap flatMapValues transform transformValues flatTransform flatTransformValues 49
  59. Подробнее про «лишнее» репартиционирование Guozhang Wang Performance Analysis and Optimizations

    for Kafka Streams Applications (Kafka Summit London, 2019) 50
  60. Наш план 1. Конфигурация приложения. Простые (stateless) трансформации 2. Трансформации

    с использованием локального состояния 3. Дуализм «поток—таблица» и табличные join-ы 4. Время и оконные операции 51
  61. Таблицы vs стримы Местонахождение пользователя Michael G. Noll. Of Streams

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

    Streams and Tables in Kafka and Stream Processing 53
  63. Таблицы vs стримы Производная и интеграл Martin Kleppmann, “Designing Data

    Intensive Applications” 54
  64. Join 55

  65. Переписываем 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 56
  66. Получаем таблицу счетов матчей 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 57
  67. Демо: Объединяем сумму ставок с текущим счётом KTable<String, String> joined

    = totals.join(tableScores, (total, eventScore) -> String.format("(%s)\t%d", eventScore, total)); 58
  68. Копартиционирование Join работает 59

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

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

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

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

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

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

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

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

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

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

    с использованием локального состояния 3. Дуализм «поток—таблица» и табличные join-ы 4. Время и оконные операции 69
  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
  80. Быстрое извлечение значений по ключу из диапазона времени 71

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

    в момент смены счёта в матче Штамп времени ставки и события смены счёта должны «почти совпадать». 72
  82. Время, вперёд! (Ещё время можно извлечь из WallClock и RecordMetadata.)

    KStream<String, Bet> bets = streamsBuilder.stream(BET_TOPIC, Consumed.with( Serdes...) .withTimestampExtractor( (record, previousTimestamp) -> ((Bet) record.value()).getTimestamp() )); 73
  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
  84. Демо: 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) Joined.with(Serdes.... )); 75
  85. Tumbling window Источник: Kafka Streams in Action TimeWindowedKStream<..., ...> windowed

    = stream.groupByKey() .windowedBy(TimeWindows.of(Duration.ofSeconds(20))); 76
  86. 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() */ 77
  87. Hopping Window Источник: Kafka Streams in Action TimeWindowedKStream<..., ...> windowed

    = stream.groupByKey() .windowedBy(TimeWindows.of(Duration.ofSeconds(20)) .advanceBy(Duration.ofSeconds(10))); 78
  88. Session Window SessionWindowedKStream<..., ...> windowed = stream.groupByKey() .windowedBy(SessionWindows.with(Duration.ofMinutes(5))); 79

  89. Window Retention time vs. Grace Time 80

  90. Иногда нужны не окна, а Punctuator class MyTransformer implements Transformer<...>

    { @Override public void init(ProcessorContext context) { context.schedule( Duration.ofSeconds(10), PunctuationType.WALL_CLOCK_TIME, timestamp->{. . .}); } 81
  91. Наш план 1. Конфигурация приложения. Простые (stateless) трансформации 2. Трансформации

    с использованием локального состояния 3. Дуализм «поток—таблица» и табличные join-ы 4. Время и оконные операции Пора закругляться! 82
  92. Kafka Streams in Action William Bejeck, “Kafka Streams in Action”,

    November 2018 Примеры кода для Kafka 1.x 83
  93. Kafka: The Definitive Guide Gwen Shapira, Neha Narkhede, Todd Palino

    September 2017 84
  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
  95. Сообщества, конференции Телеграм: Грефневая Кафка Kafka Summit Conference https://t.me/AwesomeKafka_ru https://t.me/proKafka

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

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

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

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

    начать пользоваться, надо настроить мышление под потоковую обработку Технология переживает бурное развитие + живой community, есть шанс повлиять на процесс самому - публичные интерфейсы изменяются очень быстро 87
  100. На этом всё!   Спасибо! inponomarev/kstreams-examples @inponomarev ponomarev@corchestra.ru 88