KEMBAR78
I can't believe it's not a queue: Kafka and Spring | PDF
BombMQ
“cut the red wire”“cut the blue wire”
Bombfka
“cut the red wire”
“cut the blue wire”
replay

button
Bombfka
“cut the blue wire”
“cut the red wire”
@codefinger
Joe Kutner
Agenda
• What is Kafka?
• Kafka + Spring
• Metrics Example
• How Heroku uses Kafka
What is
Kafka?
Kafka is a distributed,
partitioned, replicated
commit log service. It
provides the functionality
of a messaging system, but
with a unique design.
Distributed
Publish
Subscribe
Messaging
Fast
Scalable
Durable
“hundreds of thousands to
millions of messages a second
on a small cluster”
Tom Crayford
Heroku Kafka
Know Your Cuts of Kafka
Producers Consumers
Partitions
Groups
Brokers
Messages
Topics
Keys
Producers & Consumers
Messages
• Header
• Key
• Value
(Byte Array)
Messages feed into Topics
Each Topic Partition
is an ordered log of
immutable messages,
append-only
Offsets
Consumer Groups
• Messages are produced in order
• Messages are consumed in order
• Topics are distributed and replicated
Kafka Guarantees
Kafka + Java
props.put("bootstrap.servers", “broker1:9092,broker2:9092”);
props.put(“key.serializer”, StringSerializer.class.getName());
props.put(“value.serializer”, StringSerializer.class.getName());
Producer<String, String> producer = new KafkaProducer<>(props);
Producer API
producer.send(new ProducerRecord<>("my-topic", message2));
producer.send(new ProducerRecord<>("my-topic", message3));
producer.send(new ProducerRecord<>("my-topic", message1));
producer.send(...).get();
Consumer API
Automatic Offset Committing
props.put("bootstrap.servers", “broker1:9092,broker2:9092”);
props.put(“key.deserializer”, StringDeserializer.class.getName());
props.put(“value.deserializer”, StringDeserializer.class.getName());
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
consumer.subscribe(singletonList(“my-topic”));
while (running.get()) {
ConsumerRecords<String, String> records = consumer.poll(100);
for (ConsumerRecord<String, String> record : records) {
// ...
}
}
Consumer API
Manual Offset Committing
props.put("enable.auto.commit", "false");
// ...
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
consumer.subscribe(singletonList(“my-topic”));
final int minBatchSize = 200;
while (running.get()) {
ConsumerRecords<String, String> records = consumer.poll(100);
for (ConsumerRecord<String, String> record : records) {
buffer.add(record);
}
if (buffer.size() >= minBatchSize) {
insertIntoDb(buffer);
consumer.commitSync();
buffer.clear();
}
}
Consumer API
Kafka Consumer is NOT threadsafe
Consumer API
Advanced!
• Per-message offset commit
• Manual Partition Assignment
• Storing Offsets Outside Kafka
• Kafka Streams (since 0.10)
Using Kafka with Spring
Producer
@SpringBootApplication
@EnableKafka
public class SpringApplicationProducer {
@Bean
public KafkaTemplate<Integer, String> kafkaTemplate() {
return new KafkaTemplate<Integer, String>(producerFactory());
}
@Bean
public ProducerFactory<Integer, String> producerFactory() {
return new DefaultKafkaProducerFactory<>(producerConfigs());
}
private Map<String, Object> producerConfigs() {
Map<String, Object> props = new HashMap<>();
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092");
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, IntegerSerializer.class);
// ...
return props;
}
}
Using Kafka with Spring
Producer
@Autowired
private KafkaTemplate<Integer, String> template;
public void send() throws Exception {
template.send(“my-topic", message);
}
Using Kafka with Spring
Consumer
@Service
public class MyKafkaListener {
@KafkaListener(topicPattern = “my-topic")
public void listen(String message) {
System.out.println("received: " + message);
}
}
Using Kafka with Spring
Consumer
@Service
public class MyKafkaListener {
@KafkaListener(id = “my-listener", topicPartitions = {
@TopicPartition(topic = "topic1", partitions = { "0", "1" }),
@TopicPartition(topic = "topic2", partitions = { "0", "1" })
})
public void listen(String message) {
System.out.println("received: " + message);
}
}
Metrics App
Example
Architecture
Web Request
Primary App
Metrics App
Router Logs
POST
Spring App
Log Drain App
(Producer)
Kafka
Cluster
Log Drain (HTTPS)
Architecture
Web Request
Metrics
Aggregator
(Consumer)
POST
Spring App
Log Drain App
(Producer)
Kafka
Cluster
Log Drain (HTTPS)
Architecture
Web Request
Metrics
Aggregator
(Consumer)
Heroku Log Drains
$ heroku drains:add https://<metrics-app>/logs
242 <158>1 2016-06-20T21:56:57.107495+00:00 host heroku router -
at=info method=GET path="/" host=demodayex.herokuapp.com
request_id=1850b395-c7aa-485c-aa04-7d0894b5f276 fwd="68.32.161.89"
dyno=web.1 connect=0ms service=6ms status=200 bytes=1548
POST
Spring App
Log Drain App
(Producer)
Kafka
Cluster
Log Drain (HTTPS)
Architecture
Web Request
Metrics
Aggregator
(Consumer)
@RequestMapping(value = "/logs", method = RequestMethod.POST)

@ResponseBody

public String logs(@RequestBody String body) throws IOException {



// "application/logplex-1" does not conform to RFC5424. 

// It leaves out STRUCTURED-DATA but does not replace it with

// a NILVALUE. To workaround this, we inject empty STRUCTURED-DATA.

String[] parts = body.split("router - ");

String log = parts[0] + "router - [] " + (parts.length > 1 ? parts[1] : "");



RFC6587SyslogDeserializer parser = new RFC6587SyslogDeserializer();

InputStream is = new ByteArrayInputStream(log.getBytes());

Map<String, ?> messages = parser.deserialize(is);

ObjectMapper mapper = new ObjectMapper();
String json = mapper.writeValueAsString(messages);

template.send("logs", json);



return "ok";

}
POST
Spring App
Log Drain App
(Producer)
Kafka
Cluster
Log Drain (HTTPS)
Architecture
Web Request
Metrics
Aggregator
(Consumer)
Heroku Kafka
$ heroku addons:create heroku-kafka
$ heroku kafka:create logs Create the Topic
$ heroku plugins:install heroku-kafka
Create the Cluster
Heroku Kafka
$ heroku kafka:info
=== KAFKA_URL
Name: kafka-asymmetrical-77749
Created: 2016-06-20 18:21 UTC
Plan: Beta Dev
Status: available
Version: 0.9.0.1
Topics: 2 topics (see heroku kafka:list)
Connections: 0 consumers (0 applications)
Messages: 32.0 messages/s
Traffic: 2.25 KB/s in / 2.25 KB/s out
Heroku Kafka
$ heroku kafka:topic logs
=== KAFKA_URL :: logs
Producers: 0.0 messages/second (0 Bytes/second)
Consumers: 0 Bytes/second total
Partitions: 32 partitions
Replication Factor: 2 (recommend > 1)
Compaction: Compaction is disabled for logs
Retention: 24 hours
POST
Spring App
Log Drain App
(Producer)
Kafka
Cluster
Log Drain (HTTPS)
Architecture
Web Request
Metrics
Aggregator
(Consumer)
public class Metrics {
// ...
public static void main(String[] args) { /* … */ }
public Metrics() throws Exception {
// ...



URI redisUri = new URI(System.getenv("REDIS_URL"));

pool = new JedisPool(redisUri);

}
private void start() {
// ...
running.set(true);
executor = Executors.newSingleThreadExecutor();

executor.submit(this::loop);

stopLatch = new CountDownLatch(1);
}
}
Main Consumer Class
private void loop() {
// ...
consumer = new KafkaConsumer<>(properties);

consumer.subscribe(singletonList(KafkaConfig.getTopic()));
while (running.get()) {

ConsumerRecords<String, String> records = consumer.poll(100);

for (ConsumerRecord<String, String> record : records) {

try {

Map<String,String> recordMap =
mapper.readValue(record.value(), typeRef);

Route route = new Route(recordMap);

receive(route);

} catch (IOException e) {

e.printStackTrace();

}

}

}



consumer.close();

stopLatch.countDown();
}
Main Consumer Method
private void receive(Route route) {
// …


jedis.hincrBy(key, "sum", value);

jedis.hincrBy(key, "count", 1);



Integer sum = Integer.valueOf(jedis.hget(key, "sum"));

Float count = Float.valueOf(jedis.hget(key, "count"));

Float avg = sum / count;
jedis.hset(key, "average", String.valueOf(avg));
}
Update Redis
?
POST
Spring App
Log Drain App
(Producer)
Kafka
Cluster
Log Drain (HTTPS)
Architecture
Web Request
Metrics
Aggregator
(Consumer)
POST
Spring App
Log Drain App
(Producer)
Kafka
Cluster
Log Drain (HTTPS)
Architecture
Web Request
Metrics
Aggregator
(Consumer)
Replay
(Consumer) Staging App
Demo App
https://github.com/jkutner/heroku-metrics-spring
$ docker-compose up web
Other Use Cases
• User Activity
• Stream Processing
Metrics
Logs
IoT Data
Kafka
Cluster
Stream Processing
Kafka @ Heroku
• Metrics
• API Event Bus
Heroku Metrics Dashboard
Heroku API

Event Bus
Heroku Kafka http://heroku.com/kafka
$ heroku addons:create heroku-kafka
@codefinger
Joe Kutner
Thank You!

I can't believe it's not a queue: Kafka and Spring

  • 5.
    BombMQ “cut the redwire”“cut the blue wire”
  • 7.
    Bombfka “cut the redwire” “cut the blue wire” replay
 button
  • 8.
    Bombfka “cut the bluewire” “cut the red wire”
  • 10.
  • 14.
    Agenda • What isKafka? • Kafka + Spring • Metrics Example • How Heroku uses Kafka
  • 15.
  • 16.
    Kafka is adistributed, partitioned, replicated commit log service. It provides the functionality of a messaging system, but with a unique design.
  • 17.
  • 18.
  • 20.
    “hundreds of thousandsto millions of messages a second on a small cluster” Tom Crayford Heroku Kafka
  • 21.
    Know Your Cutsof Kafka Producers Consumers Partitions Groups Brokers Messages Topics Keys
  • 22.
  • 23.
  • 24.
  • 25.
    Each Topic Partition isan ordered log of immutable messages, append-only
  • 26.
  • 27.
  • 28.
    • Messages areproduced in order • Messages are consumed in order • Topics are distributed and replicated Kafka Guarantees
  • 29.
  • 30.
    props.put("bootstrap.servers", “broker1:9092,broker2:9092”); props.put(“key.serializer”, StringSerializer.class.getName()); props.put(“value.serializer”,StringSerializer.class.getName()); Producer<String, String> producer = new KafkaProducer<>(props); Producer API producer.send(new ProducerRecord<>("my-topic", message2)); producer.send(new ProducerRecord<>("my-topic", message3)); producer.send(new ProducerRecord<>("my-topic", message1)); producer.send(...).get();
  • 31.
    Consumer API Automatic OffsetCommitting props.put("bootstrap.servers", “broker1:9092,broker2:9092”); props.put(“key.deserializer”, StringDeserializer.class.getName()); props.put(“value.deserializer”, StringDeserializer.class.getName()); KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props); consumer.subscribe(singletonList(“my-topic”)); while (running.get()) { ConsumerRecords<String, String> records = consumer.poll(100); for (ConsumerRecord<String, String> record : records) { // ... } }
  • 32.
    Consumer API Manual OffsetCommitting props.put("enable.auto.commit", "false"); // ... KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props); consumer.subscribe(singletonList(“my-topic”)); final int minBatchSize = 200; while (running.get()) { ConsumerRecords<String, String> records = consumer.poll(100); for (ConsumerRecord<String, String> record : records) { buffer.add(record); } if (buffer.size() >= minBatchSize) { insertIntoDb(buffer); consumer.commitSync(); buffer.clear(); } }
  • 33.
    Consumer API Kafka Consumeris NOT threadsafe
  • 34.
    Consumer API Advanced! • Per-messageoffset commit • Manual Partition Assignment • Storing Offsets Outside Kafka • Kafka Streams (since 0.10)
  • 35.
    Using Kafka withSpring Producer @SpringBootApplication @EnableKafka public class SpringApplicationProducer { @Bean public KafkaTemplate<Integer, String> kafkaTemplate() { return new KafkaTemplate<Integer, String>(producerFactory()); } @Bean public ProducerFactory<Integer, String> producerFactory() { return new DefaultKafkaProducerFactory<>(producerConfigs()); } private Map<String, Object> producerConfigs() { Map<String, Object> props = new HashMap<>(); props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, "localhost:9092"); props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, IntegerSerializer.class); // ... return props; } }
  • 36.
    Using Kafka withSpring Producer @Autowired private KafkaTemplate<Integer, String> template; public void send() throws Exception { template.send(“my-topic", message); }
  • 37.
    Using Kafka withSpring Consumer @Service public class MyKafkaListener { @KafkaListener(topicPattern = “my-topic") public void listen(String message) { System.out.println("received: " + message); } }
  • 38.
    Using Kafka withSpring Consumer @Service public class MyKafkaListener { @KafkaListener(id = “my-listener", topicPartitions = { @TopicPartition(topic = "topic1", partitions = { "0", "1" }), @TopicPartition(topic = "topic2", partitions = { "0", "1" }) }) public void listen(String message) { System.out.println("received: " + message); } }
  • 39.
  • 40.
  • 41.
    POST Spring App Log DrainApp (Producer) Kafka Cluster Log Drain (HTTPS) Architecture Web Request Metrics Aggregator (Consumer)
  • 42.
    POST Spring App Log DrainApp (Producer) Kafka Cluster Log Drain (HTTPS) Architecture Web Request Metrics Aggregator (Consumer)
  • 43.
    Heroku Log Drains $heroku drains:add https://<metrics-app>/logs 242 <158>1 2016-06-20T21:56:57.107495+00:00 host heroku router - at=info method=GET path="/" host=demodayex.herokuapp.com request_id=1850b395-c7aa-485c-aa04-7d0894b5f276 fwd="68.32.161.89" dyno=web.1 connect=0ms service=6ms status=200 bytes=1548
  • 44.
    POST Spring App Log DrainApp (Producer) Kafka Cluster Log Drain (HTTPS) Architecture Web Request Metrics Aggregator (Consumer)
  • 45.
    @RequestMapping(value = "/logs",method = RequestMethod.POST)
 @ResponseBody
 public String logs(@RequestBody String body) throws IOException {
 
 // "application/logplex-1" does not conform to RFC5424. 
 // It leaves out STRUCTURED-DATA but does not replace it with
 // a NILVALUE. To workaround this, we inject empty STRUCTURED-DATA.
 String[] parts = body.split("router - ");
 String log = parts[0] + "router - [] " + (parts.length > 1 ? parts[1] : "");
 
 RFC6587SyslogDeserializer parser = new RFC6587SyslogDeserializer();
 InputStream is = new ByteArrayInputStream(log.getBytes());
 Map<String, ?> messages = parser.deserialize(is);
 ObjectMapper mapper = new ObjectMapper(); String json = mapper.writeValueAsString(messages);
 template.send("logs", json);
 
 return "ok";
 }
  • 46.
    POST Spring App Log DrainApp (Producer) Kafka Cluster Log Drain (HTTPS) Architecture Web Request Metrics Aggregator (Consumer)
  • 47.
    Heroku Kafka $ herokuaddons:create heroku-kafka $ heroku kafka:create logs Create the Topic $ heroku plugins:install heroku-kafka Create the Cluster
  • 48.
    Heroku Kafka $ herokukafka:info === KAFKA_URL Name: kafka-asymmetrical-77749 Created: 2016-06-20 18:21 UTC Plan: Beta Dev Status: available Version: 0.9.0.1 Topics: 2 topics (see heroku kafka:list) Connections: 0 consumers (0 applications) Messages: 32.0 messages/s Traffic: 2.25 KB/s in / 2.25 KB/s out
  • 49.
    Heroku Kafka $ herokukafka:topic logs === KAFKA_URL :: logs Producers: 0.0 messages/second (0 Bytes/second) Consumers: 0 Bytes/second total Partitions: 32 partitions Replication Factor: 2 (recommend > 1) Compaction: Compaction is disabled for logs Retention: 24 hours
  • 50.
    POST Spring App Log DrainApp (Producer) Kafka Cluster Log Drain (HTTPS) Architecture Web Request Metrics Aggregator (Consumer)
  • 51.
    public class Metrics{ // ... public static void main(String[] args) { /* … */ } public Metrics() throws Exception { // ...
 
 URI redisUri = new URI(System.getenv("REDIS_URL"));
 pool = new JedisPool(redisUri);
 } private void start() { // ... running.set(true); executor = Executors.newSingleThreadExecutor();
 executor.submit(this::loop);
 stopLatch = new CountDownLatch(1); } } Main Consumer Class
  • 52.
    private void loop(){ // ... consumer = new KafkaConsumer<>(properties);
 consumer.subscribe(singletonList(KafkaConfig.getTopic())); while (running.get()) {
 ConsumerRecords<String, String> records = consumer.poll(100);
 for (ConsumerRecord<String, String> record : records) {
 try {
 Map<String,String> recordMap = mapper.readValue(record.value(), typeRef);
 Route route = new Route(recordMap);
 receive(route);
 } catch (IOException e) {
 e.printStackTrace();
 }
 }
 }
 
 consumer.close();
 stopLatch.countDown(); } Main Consumer Method
  • 53.
    private void receive(Routeroute) { // … 
 jedis.hincrBy(key, "sum", value);
 jedis.hincrBy(key, "count", 1);
 
 Integer sum = Integer.valueOf(jedis.hget(key, "sum"));
 Float count = Float.valueOf(jedis.hget(key, "count"));
 Float avg = sum / count; jedis.hset(key, "average", String.valueOf(avg)); } Update Redis
  • 54.
    ? POST Spring App Log DrainApp (Producer) Kafka Cluster Log Drain (HTTPS) Architecture Web Request Metrics Aggregator (Consumer)
  • 55.
    POST Spring App Log DrainApp (Producer) Kafka Cluster Log Drain (HTTPS) Architecture Web Request Metrics Aggregator (Consumer) Replay (Consumer) Staging App
  • 56.
  • 57.
    Other Use Cases •User Activity • Stream Processing
  • 58.
  • 59.
    Kafka @ Heroku •Metrics • API Event Bus
  • 60.
  • 61.
  • 62.
    Heroku Kafka http://heroku.com/kafka $heroku addons:create heroku-kafka
  • 63.