Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

3

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

Зачем нам Kafka? 7

Slide 8

Slide 8 text

Зачем нам Kafka? 8

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

Легенда Идут футбольные матчи (меняется счёт) Делаются ставки: 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

Slide 28

Slide 28 text

@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

Slide 29

Slide 29 text

@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

Slide 30

Slide 30 text

@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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

Ветвление стримов по условию Не используйте 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

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

Суммирование ставок @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

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

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

Slide 59

Slide 59 text

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

Slide 60

Slide 60 text

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

Slide 61

Slide 61 text

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

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

Join 55

Slide 65

Slide 65 text

Переписываем 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

Slide 66

Slide 66 text

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

Slide 67

Slide 67 text

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

Slide 68

Slide 68 text

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

Slide 69

Slide 69 text

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

Slide 70

Slide 70 text

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

Slide 71

Slide 71 text

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

Slide 72

Slide 72 text

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

Slide 73

Slide 73 text

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

Slide 74

Slide 74 text

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

Slide 75

Slide 75 text

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

Slide 76

Slide 76 text

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

Slide 77

Slide 77 text

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

Slide 78

Slide 78 text

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

Slide 79

Slide 79 text

Сохранение 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

Slide 80

Slide 80 text

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

Slide 81

Slide 81 text

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

Slide 82

Slide 82 text

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

Slide 83

Slide 83 text

Демо: 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

Slide 84

Slide 84 text

Демо: 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

Slide 85

Slide 85 text

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

Slide 86

Slide 86 text

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

Slide 87

Slide 87 text

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

Slide 88

Slide 88 text

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

Slide 89

Slide 89 text

Window Retention time vs. Grace Time 80

Slide 90

Slide 90 text

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

Slide 91

Slide 91 text

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

Slide 92

Slide 92 text

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

Slide 93

Slide 93 text

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

Slide 94

Slide 94 text

Другие источники Исходники! 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

Slide 95

Slide 95 text

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

Slide 96

Slide 96 text

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

Slide 97

Slide 97 text

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

Slide 98

Slide 98 text

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

Slide 99

Slide 99 text

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

Slide 100

Slide 100 text

На этом всё!   Спасибо! inponomarev/kstreams-examples @inponomarev [email protected] 88