Kafka Streams API

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

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

me
  • Tech Lead at KURS

  • ERP systems & Java background

  • Speaker at JPoint, Devoops, Heisenbug, JUG.MSK, PermDevDay, DevopsForum, Стачка etc.

  • Heisenbug Program Committee member.

  • Текущий проект: Real-time Webscraping

Всё, что я показываю, есть на гитхабе

octocat

Наш план

kafka

Лекция 1.

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

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

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

Лекция 2.

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

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

kafka

Kafka это

kafka logo

В Кафке можно

okay
  • Записать нечто в именованный лог (topic)

  • Прочитать записи из топика в FIFO порядке (в пределах партиции)

  • Зафиксировать место, до которого дочитал

В Кафке нельзя

noway
  • Стереть запись

  • Изменить запись

  • Найти в логе запись иначе, как по её порядковому номеру

Топики, партиции и сообщения

topics partitions

Топики, партиции и сообщения

topics partitions1

Топики, партиции и сообщения

topics partitions2

Анатомия сообщения

message anatomy

Анатомия сообщения

message anatomy2
// hash the keyBytes to choose a partition
return Utils.toPositive(Utils.murmur2(keyBytes)) % numPartitions;

Чтение из Кафки

ConsumerG0

Чтение из Кафки

ConsumerG

Чтение из Кафки

ConsumerG2

Чтение из Кафки

ConsumerG3

Offset Commit

offcommit1

Offset Commit

offcommit2

Offset Commit

offcommit3

Offset Commit

offcommit4

Offset Commit

offcommit5

Offset Commit

offcommit6

Offset Commit

offcommit7

Compacted topics

log compaction
Источник: Kafka Documentation

Как работает Retention

tapeloop

Потоковая обработка данных: архитектура

streaming arch1

Где нужны потоковые системы?

  • Мониторинг! Логи!

  • Отслеживание действий пользователей

  • Выявление аномалий (в т. ч. попыток мошенничества)

okay
streams ok
noway
streams noway

Существующие фреймворки потоковой обработки

spark logo
samza logo
storm logo
flink logo
kafka logo

Наш план

kafka

Лекция 1.

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

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

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

Лекция 2.

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

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

kafka

Kafka Streams API: общая структура KStreams-приложения

StreamsConfig config = ...;
//Здесь устанавливаем всякие опции

Topology topology = new StreamsBuilder()
//Здесь строим топологию
....build();

Kafka Streams API: общая структура KStreams-приложения

Топология — конвейер обработчиков:

topology sample

Преобразуем поток в поток

blockStream

map

squashedStream

List<Block> blocks = ...;

Stream<Block> blocksStream = blocks.stream();

Stream<SquashedBlock> squashedStream =
  blocksStream.map(Block::squash);

(Автор анимаций — Тагир Валеев, движущиеся картинки см. здесь)

Фильтруем

squashedStream

filter

filteredStream

Stream<SquashedBlock> filteredStream =
  squashedStream.filter(block >
         block.getColor() != YELLOW);

Отображаем в консоль (терминальная операция)

filteredStream

display
filteredStream
  .forEach(System.out::println);

Всё вместе в одну строку

fuse
blocks.stream()
      .map(Block::squash)
      .filter(block >
         block.getColor() != YELLOW)
      .forEach(System.out::println);

Ничего не напоминает?

«Соединить два файла, привести их строки к lowercase, отсортировать, вывести три последних строки в алфавитном порядке»

cat file1 file2 | tr "[A-Z]" "[a-z]" | sort | tail -3

Kafka Streams API: общая структура KStreams-приложения

StreamsConfig config = ...;
//Здесь устанавливаем всякие опции

Topology topology = new StreamsBuilder()
//Здесь строим топологию
....build();


//Это за нас делает SPRING-KAFKA
KafkaStreams streams = new KafkaStreams(topology, config);
streams.start();
...
streams.close();

В Спринге достаточно определить две вещи

  • @Bean KafkaStreamsConfiguration

  • @Bean Topology

Легенда

betting
  • Идут футбольные матчи (меняется счёт)

  • Делаются ставки: 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
}

@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;
}

@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);
}

@Bean Topology

yelling 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();
}

Три строчки кода, и что тут такого?

  • Больше сообщений в секунду? — больше машин с одинаковым application.id!

w1

Добавляем ноды

w2

Ограничены только числом партиций

w4

TopologyTestDriver: создание

KafkaStreamsConfiguration config = new KafkaConfiguration()
                                        .getStreamsConfig();
StreamsBuilder sb = new StreamsBuilder();
Topology topology = new TopologyConfiguration().createTopology(sb);
TopologyTestDriver topologyTestDriver =
        new TopologyTestDriver(topology,
                               config.asProperties());

TestInput/OutputTopic: создание

TestInputTopic<String, Bet> inputTopic =
        topologyTestDriver.createInputTopic(BET_TOPIC,
            Serdes.String().serializer(),
            new JsonSerde<>(Bet.class).serializer());
TestOutputTopic<String, Long> outputTopic =
        topologyTestDriver.createOutputTopic(GAIN_TOPIC,
            Serdes.String().deserializer(),
            new JsonSerde<>(Long.class).deserializer());

TopologyTestDriver: использование

Bet bet = Bet.builder()
            .bettor("John Doe")
            .match("Germany-Belgium")
            .outcome(Outcome.H)
            .amount(100)
            .odds(1.7).build();

inputTopic.pipeInput(bet.key(), bet);

TopologyTestDriver: использование

TestRecord<String, Long> record = outputTopic.readRecord();

assertEquals(bet.key(), record.key());
assertEquals(170L, record.value().longValue());

Если что-то пошло не так…​

  • default.deserialization.exception.handler — не смогли десериализовать

  • default.production.exception.handler — брокер отверг сообщение (например, оно слишком велико)

failure

Если всё совсем развалилось

streams.setUncaughtExceptionHandler(
  (Thread thread, Throwable throwable) -> {
    . . .
   });
uncaughtexception

В Спринге всё сложнее (см. код)

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

kstreamsstates

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

Простое ветвление стримов

Java-стримы так не могут:

KStream<..> foo = ...
KStream<..> bar = foo.mapValues().map... to...
Kstream<..> baz = foo.filter().map... forEach...
simplebranch

Ветвление стримов по условию

С версии 2.8:

gain.split()
    .branch((key, value) -> key.contains("A"),
        Branched.withConsumer(ks -> ks.to("A")))
    .branch((key, value) -> key.contains("B"),
        Branched.withConsumer(ks -> ks.to("B")));
switchbranch

Простое слияние

KStream<String, Integer> foo = ...
KStream<String, Integer> bar = ...
KStream<String, Integer> merge = foo.merge(bar);
merge

Наш план

kafka

Лекция 1.

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

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

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

Лекция 2.

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

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

kafka

Локальное состояние

Facebook’s RocksDB — что это и зачем?

rocksdb
  • Embedded key/value storage

  • LSM Tree (Log-Structured Merge-Tree)

  • High-performant (data locality)

  • Persistent, optimized for SSD

RocksDB похож на TreeMap<K,V>

  • Сохранение K,V в бинарном формате

  • Лексикографическая сортировка

  • Iterator (snapshot view)

  • Удаление диапазона (deleteRange)

Пишем “Bet Totalling App”

Какова сумма выплат по сделанным ставкам, если сыграет исход?

counting topology

@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);

Суммирование ставок

@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);
}

StateStore доступен в тестах

@Test
void testTopology() {
    topologyTestDriver.pipeInput(...);
    topologyTestDriver.pipeInput(...);

    KeyValueStore<String, Long> store =
        topologyTestDriver
        .getKeyValueStore(TotallingTransformer.STORE_NAME);

    assertEquals(..., store.get(...));
    assertEquals(..., store.get(...));
}

Демо: Ребалансировка / репликация

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

Сохранение локального состояния в топик

$kafka-topics --zookeeper localhost --describe

Topic:bet-totalling-demo-app-totalling-store-changelog
PartitionCount:10
ReplicationFactor:1
Configs:cleanup.policy=compact
counting topology changelog

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

local partitioning oneworker

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

local partitioning 1

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

local partitioning 2

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

local partitioning 25

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

local partitioning 3

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

local partitioning 4

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

local partitioning 5

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

local partitioning 6

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

through
  • Явное при помощи
    repartition(Repartitioned<K, V> repartitioned)

  • Неявное при операциях, меняющих ключ + stateful-операциях

Дублирующееся неявное репартиционирование

KStream source = builder.stream("topic1");
KStream mapped = source.map(...);
KTable counts = mapped.groupByKey().aggregate(...);
KStream sink = mapped.leftJoin(counts, ...);
doublethrough

Избавляемся от дублирующегося репартиционирования

KStream source = builder.stream("topic1");
KStream shuffled = source.map(...).repartition(...);
KTable counts = shuffled.groupByKey().aggregate(...);
KStream sink = shuffled.leftJoin(counts, ...);
implicitthrough

Ключ лучше лишний раз не трогать

Key only: selectKey

Key and Value

Value Only

map

mapValues

flatMap

flatMapValues

transform

transformValues

flatTransform

flatTransformValues