Kafka总结
Kafka总结
什么是kafka:分布式流处理平台。主要三个特点:可以发布和订阅消息的系统;可容错且持久地存储流记录;流计算。通常使用前两个特征。
发布订阅信息系统一般规则:消费者可订阅多个topic(消息队列),同一条数据可被多个消费者消费,消息被消费后不会被立刻删除。
概念
Broker:Kafka服务器,一个broker可容纳多个topic或存储Topic的一个或多个Partitions。
Topic:一个消息队列,使得消息数据可分开处理。
Partition:Topic的基本存储单元。partition在一个节点上(太大会多个节点)且可设置Replica。一个partition对应一个单独日志,内部多个Segment,Payload不断地被追加到最后一个Segment末尾。
producer:发布者,发布消息到broker相应topic的客户端。
consumer:订阅者,向broker拉取信息的客户端。少于等于partition数目。
Consumer Group(实现发布/订阅):对于同一个topic可以有多个group,所以会广播给不同的group。一个group只对应一个topic,consumers协助消费,保证一个partition只会被一个consumer消费,所以partition会被有序消费。consumer出现故障,组内其他consumer接管。
Replication Leader:每个分区都有一个leader副本,它负责该分区与Producer和Consumer的交互。Followers做备份。
ReplicationManager:负责管理当前broker所有分区和副本的信息、KafkaController发起的请求、副本状态切换、添加/读取信息等。
Payload:消息单元,其结构:
Offset | Length | CRC32 | Magic | attributes | Timestamp | Key Length | Key | Value Length | Value |
---|---|---|---|---|---|---|---|---|---|
4 | 4 | 4 | 1 | 1 | 8 | 4 | 不限 | 4 | 不限 |
CRC32是校验字段,确保完整性。Magic判断kafka服务程序协议版本号。当Magic为1时才有attributes(是否压缩、压缩格式)
实际上是一个字节数组,并不关心实际格式(上面key和value不限大小的字节数组)。key是optional,有key时默认hash分派,没有就是轮询分派。
payload都是以批量方式写入broker,按大小或timeout作为触发。
Zookeeper:Kafka依赖于zookeeper来保证系统可用性。
- broker加入和退出集群
- kafka的meta信息(表,记录各个节点IP、端口信息、有哪些topic、分区leader是哪个broker、partition分布、offset等)
- 建立起生产者和消费者的订阅关系,并实现生产者与消费者的负载均衡等等。
简单流程:生产者push给broker,消费者pull存储在broker的数据。生产者和消费者需要的broker地址信息都在zookeeper中。
基本命令
基本命令
# 启动
zkServer.sh start
kafka-server-start.sh $KAFKA_HOME/config/server.properties
#集群
bin/kafka-server-start.sh config/server.properties
#创建topic
kafka-topics.sh --create --zookeeper localhost:2181 --replication-factor 1 --partitions 1 --topic test1
#查看topics
kafka-topics.sh --list --zookeeper localhost:2181
#发送消息
kafka-console-producer.sh --broker-list localhost:9092 --topic test
#接收信息
kafka-console-consumer.sh --zookeeper localhost:2181 --topic test
#查看topic信息
kafka-topics.sh --describe --zookeeper localhost:2181 --topic hello_topic
#单节点多broker
kafka-server-start.sh -daemon $KAFKA_HOME/config/server-1.properties &
kafka-server-start.sh -daemon $KAFKA_HOME/config/server-2.properties &
kafka-server-start.sh -daemon $KAFKA_HOME/config/server-3.properties
kafka-topics.sh --create --zookeeper localhost:2181 --replication-factor 3 --partitions 1 --topic my-replicated-topic
配置
broker
broker.id
, port
, zookeeper.connect
, log.dirs
同一个分区的文件会在相同的文件夹,多个路径会均衡, num.recovery.threads.per.data.dir
每个log文件目录的线程数,这个值乘上面的值就是总的线程数,auto.create.topics.enable
一般设为false
unclean.leader.election.enable
:默认true,对于绝不允许出错的系统,如银行,会设置为false。
min.insync.replicas
:少于这个数量的同步副本,就会停止生产者的写入请求。
topic
number.partitions
根据消费者处理速度和topic的产出速度确定
log.segment.bytes
每个segment的最大数据容量,默认1G。超过后会另起一个,旧的超过一定时间会被删除。同样根据生产者和消费者的速度调整。
log.segment.ms
segment强制关闭的时间,默认为null。如果有大量partition,且在未达到大小上限时达到关闭时间,会同时关闭全部,性能销毁大。
message.max.bytes
broker对接收的payload的最大数据量限制(压缩后的,默认1M)。也有限制消费者获取消息的大小的参数。
生产者
创建ProducerRecord对象后调用send(),生产者会先把键和值进行序列化。之后如果没有设置partition,那么partitioner会根据key来给ProducerRecord分区。选好分区后生产者便确定了ProducerRecord将要发往的主题和分区。有一个独立的线程负责把ProducerRecord批次发往响应broker上。如果信息写入成功就返回一个RecordMetaData对象,它包含主题、分区和记录在分区的偏移量。失败是返回错误,重试,多次失败便返回错误信息。
创建生产者
-
必要参数
bootstrap.servers
:一个host:port的broker列表,建议至少两个,防止一个broker down掉。key.serializer
:必须设置,即便ProducerRecord没有设置key。Kafka内置ByteArraySerializer, StringSerializer 和 IntegerSerializer。也可以自定义,需要实现org.apache.kafka.common.serialization.Serializer
接口。value.serializer
:同上。 -
其他较重要参数:
acks
多少个副本被写入后才算成功写入。可选0,1(leader写入成功为成功),allbuffer.memory
缓冲发送消息的内存大小。当buffer满后send被阻塞。默认1M,一般都足够。如果阻塞时间超过max.block.ms
会抛异常。compression.type
一般用snappyretries
batch.size
当多条信息要发送到一个分区时,生产者会用批量发送。linger.ms
发送批量消息的等待时间。Kafka默认一空闲就发送。client.id
用于识别消息来自哪个客户端max.request.size
限制生产者发送消息的大小。broker也有接收信息的大小设置。
发送方式
fire-and-forget:我们把消息发送给服务器,但井不关它是否正常到达。大多数情况下,消息会正常到达,因为kafka是高可用的,而且生产者会自动尝试重发。不过,使用这种方式有时候
也会丢失 些消息。
Synchronous send:使用 send()方怯发送消息它会返回Future对象,调用get()方法进行等待就可以知道悄息是否发送成功。
Asynchronous send:调用send()方怯,并指定一个回调函数,服务器在返回响应时调用该函数。
public class MyProducer {
private static KafkaProducer<String, String> producer;
static {
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
//自定义分区器
props.put("partitioner.class", "partitioner.CustomPartitioner");
producer = new KafkaProducer<>(props);
}
// fire-and-forget
private static void sendMessageForgetResult()throws Exception{
ProducerRecord<String, String> record = new ProducerRecord<>(
"kafka", "name", "forgetresult");
producer.send(record); // send后可能的异常BufferExhaustedException或TimeoutException(说明缓冲区已满)等
producer.close();
}
// Synchronous
private static void sendMessageSync()throws Exception{
ProducerRecord<String, String> record = new ProducerRecord<>(
"kafka", "name", "sync");
RecordMetadata result = producer.send(record).get();
System.out.println(result.topic());
System.out.println(result.partition());
System.out.println(result.offset());
producer.close();
}
// Asynchronous
private static void sendMessageCallback(){
ProducerRecord<String, String> record = new ProducerRecord<>(
"kafka", "name", "callback");
producer.send(record, new MayProducerCallback());
producer.close();
}
private static class MayProducerCallback implements Callback {
@Override
public void onCompletion(RecordMetadata metadata, Exception exception) {
if (exception != null) {
exception.printStackTrace();
return;
}
System.out.println(metadata.topic());
System.out.println(metadata.partition());
System.out.println(metadata.offset());
}
}
}
自定义序列化器(略)
自定义分区策略(略)
消费者
应用程序需要创建一个消费者对象来从 Kafka 主题读取消息井验证这些消息,然后再把信息保存起来。由于kafka消费者通常需要做一些高延迟的操作,如把数据写到数据库或 HDFS ,或者使用数据进行比较耗时的计算,所以才有了消费者组这一实现来增加信息的消费能力。下图表明可以让多个应用(多个消费组)都获得一个topic的所有信息。
分区的所有权从个消费者转移到另一个消费者,这样的行为被称为再均衡。在再均衡期间,消费者无陆读取消息,造成整个群组一小段时间的不可用。另外,当分区被重新分配给另一个消费者时,消费者当前的读取状态会丢失,它有可能还需要去刷新缓存,在它重新恢复状态之前会拖慢应用程序。在均衡的触发:topic和consumer数量的变动。
消费者通过向被指派为群组协调器的 broker (不同的群组可以有不同的协调器)发送心跳来维持它们和群组的从属关系以及它们对分区的所有权关系。消费者会在轮询消息(为了获取消息)或提交偏移量时发送心跳。如果broker与消费者之间的心跳会话过期就会触发在均衡。在0.10.1版本里,Kafka 社区引入了一个独立的心跳线程,可以在轮均消息的空档发送心跳。
创建消费者
必要参数:bootstrap.servers
,key.deserializer
,value.deserializer
准必要:group.id
其他重要参数:
fetch.min.bytes
当消费者写入量少,且消费者个数多时,提高。
fetch.max.wait.ms
默认500ms
max.partition.fetch.bytes
每个分区返回的最大字节数,1M。如果一个topic的分区数/消费者数 x 1M,实际情况会设置更高,应对某些分区down机情况。要比broker能够接收的数据大小要大。
session.timeout.ms
3s,如果3s没有会话,认为该消费者down掉,会触发分区重平衡。
enable.auto.commit
默认true
partition.assignment.strategy
范围或均匀。前者默认,如果消费者不能整除分区数,最后一个消费者只会处理余数的分区。例如c1和c2处理t1和t2,每个topic有3个partition,那么c1会处理每个topic的前两个分区。
client.id
max.poll.records
轮询
消息轮询是消费者 API 的核心,通过一个简单的轮询向服务器请求数据。一旦消费者订阅
了主题,轮询就会处理所有的细节,包括群组协调、分区再均衡、发送心跳和获取数据。
// 自动提交
try {
while (true) {
boolean flag = true;
ConsumerRecords<String, String> records = consumer.poll(100); // 100 是timeout,等待 broker 返回数据的时间。
for (ConsumerRecord<String, String> record : records) {
// 模仿输出
System.out.println(String.format("topic = %s, partition = %s, offset = %d, key = %s, value = %s\n",
record.topic(), record.partition(), record.offset(), record.key(), record.value()));
if (record.value().equals("done")) {
flag = false;
}
}
if (!flag) {
break;
}
}
} finally {
consumer.close(); // 当调用这个方法时,socket关闭,立即触发在均衡。
}
在第一次调用新消费者的 poll() 方桂时,它会负责查找GroupCoordinator 然后加入群组,接受分配的分区。如果发生了再均衡,整个过程也是在轮询期间进行的。
按照规则,一个消费者使用一个线程。如果要在同一个消费者群组里运行多个消费者,需要让每个消费者运行在自己的线程里。
位移提交与信息保障
提交位移:消费者读取消息后可以commit,在ZK中记录当前的offset。如果消费者宕机,重新读取这个partition的信息会从这个offset的下一条信息开始读取。
根据commit和信息处理的顺序或方式分为三种语义(Kafka都支持):处理前提交实现at most once,处理后提交实现at least once,二阶段提交/通过事务操作进行commit和数据处理来实现exactly once。
commit方式:
- 自动提交(最常用):每隔一段时间提交,默认5s。(at least once)
- 手动提交当前位移(只是一定程度减少重复处理的数量,at least once)、手动异步提交当前位移(失败重试一次,因有可能后面的提交成功了)、手动异步提交当前位移带回调、同步与异步组合提交位移、提交特定的偏移量
// 同步与异步组合提交位移
try {
while (true) {
ConsumerRecords<String, String> records = consumer.poll(100);
for (ConsumerRecord<String, String> record : records) {
// 同上
consumer.commitAsync(); // 尝试一次
}
} catch (Exception e) {
log.error("commit async error: ", e);
} finally {
try {
consumer.commitSync(); // 一直尝试直至提交成功
} finally {
consumer.close();
}
}
再均衡监昕器(略)
消费者在退出和进行分区再均衡之前,会做一些清理工作。
从特定偏移量处开始处理记录
kafka的末端exactly-one
补充
exactly-once实现
在0.11.x之后,kafka内部可以实现exactly-once。
幂等:partition内部的exactly-once顺序语义。配置enable.idempotence=true
大致工作原理:发送到Kafka的每批消息将包含一个序列号,该序列号用于重复数据的删除。序列号将被持久化存储topic中,因此即使leader replica失败,接管的任何其他broker也将能感知到消息是否重复。另外这种机制的消耗相当低。
producerProps.put("enable.idempotence", "true");
producerProps.put("transactional.id", "prod-1");
KafkaProducer<String, String> producer = new KafkaProducer(producerProps);
某个Kafka topic partition内部的消息可能是事务完整提交后的消息,也可能是事务执行过程中的部分消息。必须通过配置consumer端的配置isolation.level,来正确使用事务API,通过使用 new Producer API并且对一些unique ID设置transaction.id(该配置属于producer端),该unique ID用于提供事务状态的连续性。
props.put(ENABLE_AUTO_COMMIT_CONFIG, "false");
consumerProps.put("isolation.level", "read_committed");
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(consumerProps);
consumer.subscribe(singleton(“sentences”));
事务:跨partition的原子性写操作。Kafka现在支持使用新事务API原子性的对跨partition进行写操作,该API允许producer发送批量消息到多个partition。该功能同样支持在同一个事务中提交消费者offsets。大概原理:事务通过transactional.id and a sequence number, or epoch来确定。broker会删除任何伴有相同transaction id和更早epoch的操作。
一个基于wordcount的exactly-once实现。假设已经有了exactly的producer产生sentences流。
KafkaConsumer<String, String> consumer = createKafkaConsumer();
KafkaProducer<String, String> producer = createKafkaProducer();
producer.initTransactions();
try {
while (true) {
ConsumerRecords<String, String> records = consumer.poll(ofSeconds(60));
Map<String, Integer> wordCountMap = records.records(new TopicPartition(INPUT_TOPIC, 0))
.stream()
.flatMap(record -> Stream.of(record.value().split(" ")))
.map(word -> Tuple.of(word, 1))
.collect(Collectors.toMap(tuple -> tuple.getKey(), t1 -> t1.getValue(), (v1, v2) -> v1 + v2));
producer.beginTransaction();
wordCountMap.forEach((key, value) -> producer.send(new ProducerRecord<String, String>(OUTPUT_TOPIC, key, value.toString())));
Map<TopicPartition, OffsetAndMetadata> offsetsToCommit = new HashMap<>();
for (TopicPartition partition : records.partitions()) {
List<ConsumerRecord<String, String>> partitionedRecords = records.records(partition);
long offset = partitionedRecords.get(partitionedRecords.size() - 1).offset();
offsetsToCommit.put(partition, new OffsetAndMetadata(offset + 1));
}
producer.sendOffsetsToTransaction(offsetsToCommit, CONSUMER_GROUP_ID);
producer.commitTransaction();
}
} catch (KafkaException e) {
producer.abortTransaction();
}
log工作
每个topic有映射到相应的log目录,有多少个分区就有多少个log目录。在log目录里,新数据不断append到尾部。当log文件达到阈值(大小、记录数、期限)就会合并成新log。
.Logs
├── Logs
│ └── topicA # topicA 没有分区
└── Logs
├── topicB_0
├── topicB_1
└── topicB_2
对应上面生产者部分进行理解。
broker
broker启动时通过创建临时节点把自己的 ID 注册到 Zookeeper。 Kafka 组件订阅 Zookeeper 的 /brokers/ids 路径(broker 在 Zookeeper 上的注册路径),当有 broker 加入集群或退出集群时,这些组件就可以获得通知。
不能启动具有相同id的broker。
在 broker 停机、出现网络分区或长时间垃圾回收停顿时, broker 会从 Zookeeper 上断开连接,此时 broker 在启动时创建的临时节点会自动从 Zookeeper 上移除。监听 broker 列表的Kafka 组件会被告知该 broker 已移除。
新broker会接替旧id。
broker 的大部分工作是处理客户端、分区副本和控制器发送给分区首领的请求。
replication:如果follower在10s内不与leader发送最新的数据请求,那么就被判定为不同步,从而失去参选leader的资格。
首选首领:创建主题时选定的首领。
控制器
broker中有一个controller,负责维持集群中分区leader的存在(某个broker挂掉后,看是否存储有分区leader,如果有,则会选出存储这个分区leader的新broker,把leadership交给这个broker)管理新broker的加入(该broker之前是否有处理过某些分区的信息,有的话让它去同步)。这个controller会在ZK中创建临时节点来让自己成为controller(其他broker在启动时发现ZK中已经有controller临时节点,就会转而创建ZK watch对象)
物理存储
分区分配:随机后leader分区轮询,之后错位follower分区轮询,从而达到leader分区的均匀分布,进而达到cluster的负载均衡。
文件管理:片段,包含1GB或一周数据。当前写入的片段为活跃片段,永远不会被删除。
primary-backup复制机制
当leader brokers接收后,follower会从leader brokers中拉取数据。acks
中设计多少个follower拉取成果才算成功。
可靠性
Kafka 可以保证分区消息的顺序。
只有当消息被写入分区的所有同步副本时(但不一定要写入磁盘),它才被认为是“提交”的。
只要还有一个副本是活跃的,那么已经提交的消息就不会丢失
消费者只能读取已经提交的悄息。
高级别的可靠性配置:broker禁止不完全选举,acks设置为all,生产者设置一定的retries
请求处理原理
请求类型、版本(能处理不同版本的请求)、关联id、客户端id(请求来源)
生成请求:由生产者发送,包含写入的信息。
拉取请求:消费者和follower发送。
客户端有MetaData Cache来保存谁是leader的信息。
应用场景:信息队列、行为跟踪、元信息监控、日志收集、流处理
其他问题
提高远程用户吞吐量:调大Socket缓冲区大小,从而对长网络延迟进行摊销。
如何准确处理信息:生产和消费避免重复,每个分区使用单独的写入器,遇到网络错误时检查该分区中的最后一条信息是否写入成功;消息包含主键,并在用户中进行反复制。
数据丢失问题:producer设置acks;broker设置retries、replicas等;consumer设置commit形式。
使用磁盘而非内存:Linux对于磁盘的读写优化也比较多,包括read-ahead和write-behind,磁盘缓存等。当对内存数据多时,GC效率低。磁盘顺序读写速度超过内存随机读写。系统冷启动后,磁盘缓存依然能用。
参考:
《Kafka权威指南》