关于kafka

初衷

认为 Kafka 是一个流平台,在这个平台上可以发布和订阅数据流,并把它们保存起 来、进行处理,这就是构建 Kafka 的初衷。

相关概念

消息

Kafka的数据单元被称为消息,类似数据库里的一个“数据行”或一条“记录”。消息以一种可控的方式写入不同的分区时。消息有一个可选的元数据, 也就是key(键)。根据key决定把消息写到哪个分区(实际是对key做hash,然后对分区数取模),如果key为空,那么按照轮询的方式写入分区。

批次

批次就是一组消息,这些消息属于同一个主题和分区。如果每一个消息都单独穿行于网络,会导致大量的网络开销,把消息分成批次传 输可以减少网络开销。不过,这要在时间延迟和吞吐量之间作出权衡:批次越大,单位时间内处理的消息就越多,单个消息的传输时间就越长。批次数据会被压缩,这样可以提升数据的传输和存储能力,但要做更多的计算处理。

模式

可以理解为消息的格式/序列化方式,类似json/xml,但是比json和/xml强大,它具有强类型处理能力。

主题&分区

很重要的概念。主题类似数据库中的表,主题可以被分为若干个分区,一个分区就是一个提交日志。消息以追加的方式写入分区,然 后以先入先出的顺序读取。要注意,由于一个主题一般包含几个分区,因此无法在整个主题范围内保证消息的顺序,但可以在单个分区保证消息的顺序。kafka通过分区来实现数据冗余和伸缩性。分区可以分布在不同的服务器上,以此来提供比单个服务器更强大的性能。

配置项说明
num.partitions主题分区数,默认为1,很重要的1个参数设置
log.retention.ms根据时间来决定数据可以被保留多久。默认为168小时(7天)。
log.retention.bytes通过保留的消息字节数来判断消息是否过期。默认为1GB。
log.segment.bytes当日志片段大小达到 log.segment . bytes 指定的上限 (默认是 lGB)时,当前日志片段就会被关闭,一个新的日志片段被打开。如果一个日志 片段被关闭,就开始等待过期。
log.segment.ms它指定了多长时间之后日志片段会被关闭。
message.max.bytes限制单个消息的大小,默认1mb(压缩后的大小)

如何选取分区数量

主要考虑如下因素
1、主题需要达到多大的吞吐量?
2、从单个分区读取数据的最大吞吐量是多少?很显然从1个分区读取数据的吞吐量不会大于该分区数据写入的吞吐量。
3、可以通过类似的方法估算生产者向单个分区写入数据的吞吐量,不过生产 者的速度一般比消费者快得多,所以最好为生产者多估算一些吞吐量。
4、每个 broker 包含的分区个数、可用的磁盘空间和网络带宽。
5、如果消息是按照不同的键采写入分区的,那么为已有的主题新增分区就会很困难。 因为key做hash都分区数取模,增加分区后,很大可能会造成同1个key,老消息和新消息会在不同的分区。
6、单个broker对分区个数是有限制的,因为分区越多,占用的内存越多,完成首领选举需要的时间也越长。

生产者

生产者创建消息,有地方也称为发布者、写入者。

配置项说明
bootstrap.servers指定 broker的地址清单,地址的格式为 host:port。建议至少提供两个broker的信息,一且其中一个若机,生产者仍然能够连接到集群上。
key.serializer必须设置
value.serializer必须设置
acks指定了必须要有多少个分区副本收到消息,生产者才会认为消息写入是成功。可取0,1,all。
buffer.memory设置生产者内存缓冲区的大小,生产者用它缓冲要发送到服务器的消息。
compression.type默认情况下,消息发送时不会被压缩。可设置为snappy、gzip或lz4。
retries生产者可以重发消息的次数,如果达到这个次数,生产者会放弃重试并返回错误。默认情况下,生产者会在每次重试之间等待 lOOms,不过可以通过 retry.backoff.ms 参数来改变这个时间间隔。
batch.size当有多个消息需要被发送到同一个分区时,生产者会把它们放在同一个批次里。该参数指定了一个批次可以使用的内存大小。
linger.ms该参数指定了生产者在发送批次之前等待更多消息加入批次的时间。
client.id该参数可以是任意的字符串,服务器会用它来识别消息的来源,还可以用在日志和配额指标里。
max.in.flight.requests.per.connection参数指定了生产者在收到服务器晌应之前可以发送多少个消息。
timeout.ms指定了broker等待同步副本返回消息确认的时间,与 asks的配置相匹配。
request.timeout.ms指定了生产者在发送数据时等待服务器返回响应的时间。
metadata.fetch.timeout.ms指定了生产者在获取元数据(比如目标分区的首领是谁)时等待服务器返回响应的时间。如果等待响应超时,那么生产者要么重试发送数据,要么返回一个错误(抛出异常或执行回调)。
max.block.ms在阻塞时间达到max.block.ms时,生产者会抛出超时异常
max.request.size该参数用于控制生产者发送的请求大小。
receive.buffer.bytesTCP socket接收数据包的缓冲区大小
send.buffer.bytesTCP socket发送数据包的缓冲区大小

消费者&消费群组

消费者读取消息,有地方也称为订阅者、读者。消费者是消费者群组的一部分,也就是说,会有一个或多个消费者共同读取一个主题。 消费群组保证每个分区只能被一个消费者使用。

配置项说明
bootstrap.servers指定 broker的地址清单,地址的格式为 host:port。建议至少提供两个broker的信息,一且其中一个若机,生产者仍然能够连接到集群上。
key.serializer必须设置
value.serializer必须设置
gourp.id消费组id
fetch.min.bytes指定消费者从服务器获取记录的最小字节数。broker在收到消费者的数据请求时,如果可用的数据量小于fetch.min.bytes指定的大小,那么它会等到有足够的可用数据时 才把它返回给消费者。
fetch.max.wait.ms等到有足够的数据时才把它返回给消费者,默认是500ms。和fetch.min.bytes满足1个即返回消息。
max.partition.fetch.bytes指定服务器从每个分区里返回给消费者的最大字节数。它的默认值是lMB,也就是说,KafkaConsumer.poll()从每个分区里返回的记录最多不超过max.partition.fetch.bytes指定的字节。如果一个主题有20个分区和5个消费者,那么每个消费者需要至少4MB的可用内存来接收记录。
session.timeout.ms该属性指定了消费者在被认为死亡之前可以与服务器断开连接的时间,默认是3s。该属性与heartbeat.interval.ms紧密相关。heartbeat.interval.ms指定了poll()方法向协调器发送心跳的频率,session.timeout.ms则指定了消费者可以多久不发送心跳。所以, 一般需要同时修改这两个属性,heartbeat.interval.ms必须比session.timeout.ms小,一般是三分之一。
auto.offset.reset它的默认值是latest,在偏移量无效的情况下,消费者将从最新的记录开始读取数据(在消费者启动之后生成的记录)。另一个值是 earliest,在偏移量无效的情况下,消费者将从起始位置读取分区的记录。
enable.auto.commit该属性指定了消费者是否自动提交偏移量,默认值是true,为true时,还可以通过配置auto.commit.interval.ms属性来控制提交的频率。
partition.assignment.strategyRange:该策略会把主题的若干个连续的分区分配给消费者RoundRobin:该策略把主题的所有分区逐个分配给消费者。默认:org.apache.kafka.clients.consumer.RangeAssignor,这个类实现了Range策略。org.apache.kafka.clients.consumer.RoundRobinAssignor。
client.idbroker用它来标识从客户端发送过来的消息
max.poll.records该属性用于控制单次调用call()方住能够返回的记录数量。
receive.buffer.bytessocket在读写数据时用到的TCP缓冲区也可以设置大小。
send.buffer.bytessocket在读写数据时用到的TCP缓冲区也可以设置大小。

偏移量

另一种元数据,它是一个不断递增的整数值,在创建消息时, Kafka会把它添加到消息里。在给定的分区里,每个消息的偏移量都是唯一的。消费者把每个分区最后读取的消息偏移量保存在Zookeeper或Kafka上,如果消费者关闭或重启,它的读取状态不会丢失。

broker&集群

可理解为服务节点。broker接收来自生产者的消息,为消息设置偏移量,并提交消息到磁盘保存。broker为消费者提供服务,对读取分区的请求作出响应,返回已经提交到磁盘上的消息。broker是集群的组成部分,每个集群都有一个 broker同时充当了集群控制器的角色(自动从集群的活跃成员中选举出来)。控制器负责管理工作,包括将分区分配给broker和监控broker.。
broker相关配置:

配置项说明
broker.idbroker标识符,在Kafka集群中唯一,默认为0
port监听端口,如使用1024以下端口需要root权限,默认为9092
zookeeper.connect用于保存 broker元数据的 Zookeeper地址,该配置参数是用冒号分隔的一组 hostname:port/path 列表
log.dirsKafka把所有消息都保存在磁盘上,存放这些日志片段的目录
num.recovery.threads.per.data.dir
auto.create.topics.enable是否自动创建主题,默认true

多集群

使用场景

区域集群和中心集群
冗余(DR)
云迁移
其他的一些,如存在的问题,架构,优化等这里不细讲了。

kafka与Zookeeper(zk群组)

1、Kafka使用Zookeeper保存broker、主题和分区的元数据信息。
2、消费者也会使用Zookeeper来保存一些信息,比如消费者群组的信息、 主题信息、消费分区的偏移量。kafka0.9.0.0 版本之后, Kafka引入了一 个新的消费者接口,允许broker直接维护这些信息。建议使用最新版本的Kafka,让消费者把偏移量提交到 Kafka 服务器上,消除对 Zookeeper的依赖。

为什么选择kafka?

1、多个生产者
2、多个消费者
3、基于磁盘的数据存储
4、伸缩性
5、高性能

(生产者)发送消息到kafka

在这里插入图片描述

发送并忘记(fire-and-forget)

把消息发送给服务器,但并不关心它是否正常到达。大多数情况下,消息会正常到达,因为Kafka是高可用的,而且生产者会自动尝试重发。不过,使用这种方式有时候也会丢失一些消息。

        ProducerRecord<String, String> record = new ProducerRecord<>("topic", "key", "value");
        try {
            producer.send(record);
        } catch (Exception e) {
            log.error("send error", e);
        }

同步发送消息

使用send()方法发送消息,它会返回1个Future对象,调用get()方法进行等待,就可以知道悄息是否发送成功。

        ProducerRecord<String, String> record = new ProducerRecord<>("topic", "key", "value");
        try {
          Future<RecordMetadata> future = producer.send(record);
          future.get();
          //future.get(1, TimeUnit.SECONDS);
        } catch (Exception e) {
            log.error("send error", e);
        }

异步发送消息

调用send()方法,并指定一个回调函数,服务器在返回响应时调用该函数。

        Callback callback = (recordMetadata, e) -> {
            if (Objects.nonNull(e)) {
                log.error("send error", e);
            }
        };
        Future<RecordMetadata> future = producer.send(record, callback);

利用springboot中的KafkaTemplate发送消息

        ListenableFuture listenableFuture = kafkaTemplate.send("topic", 1, "key", "data");
        //这个方法有很多重载,这里不做过多说明
        //发送成功后回调
        SuccessCallback successCallback = result -> {
            log.info("发送core kafka成功");
        };
        //发送失败回调
        FailureCallback failureCallback = ex -> {
            log.error("发送core kafka失败", ex);
        };
        listenableFuture.addCallback(successCallback, failureCallback);

(消费者)从kafka消费消息

群组中的消费者数量超过分区数量,那么会有一部分消费组被闲置。
1、创建消费者
2、订阅主题

 consumer.subscribe(Collections.singletonList("topic"));
 //该方法有重载,这里不详述

3、轮询消费消息

try {
    while (true) {
        ConsumerRecords<String, String> records = consumer.poll(100);//100为超时时间
        for (ConsumerRecord<String, String> record : records) {
            log.info("topic={},partition={},offset={},key={},value={}", 
            record.topic(), record.partition(), record.offset(), record.key(), record.value());
            //TODO 业务处理
        }
    }
} finally {
    consumer.close();
}

轮询不只是获取数据那么简单。在第一次调用新消费者的 poll()方法时,它会负责查找 GroupCoordinator,然后加入群组,接受分配的分区。如果发生了再均衡,整个过程也是在轮询期间进行的。当然,心跳也是从轮询里发迭出去的。所以,我们要确保在轮询期间所做的任何处理工作都应该尽快完成。
ps:1个线程中是无法运行多个消费者,也无法让多个线程安全的共享1个消费者,因此,通常是1个线程对应1个消费者。
4、提交偏移量
后面详述

再均衡&再均衡监听器

1、分区的所有权从一个消费者转移到另一个消费者,这样的行为被称为再均衡。它为消费者群组带来了高可用性和伸缩性(我们可以放心地添加或移除梢费者)。
2、正常情况下不希望发生在均衡,再均衡期间,消费者无法读取消息,造成整个群组一小段时间的不可用。另外,当分区被重新分配给另一个消费者时,消费者当前的读取状态会丢失,它有可能还需要去刷新缓存,在它重新恢复状态之前会拖慢应用程序。
3、群组协调器:消费者通过向被指派为群组协调器的 broker (不同的群组可以有不同的协调器)发送心跳来维持它们和群组的从属关系以及它们对分区的所有权关系。只要消费者以正常的时间间隔发送心跳,就被认为是活跃的,说明它还在读取分区里的消息。
4、如果消费者停止发送心跳的时间足够长,会话就会过期,群组协调器认为它已经死亡,就会触发一次再均衡。
5、当消费者要加入群组时,它会向群组协调器发送一个JoinGroup请求。第一个加入群组的消费者将成为“群主”。群主从协调器那里获得群组的成员列表(列表中包含了所有最近发送过心跳的消费者,它们被认为是活跃的),并负责给每一个消费者分配分区。
ConsumerRebalanceListener接口:
public void onPartitionsRevoked(Collection< TopicPartition > partitions)方法会在再均衡开始之前和消费者停止读取消息之后被调用。如果在这里提交偏移量,下一个接管分区的消费者就知道该从哪里开始读取了。
(2) public void on PartitionsAssigned(Collection< TopicPartition > partitions)方法会在重新分配分区之后和消费者开始读取消息之前被调用。

偏移量的提交

1、Kafka不会像其他 JMS 队列那样需要得到消费者的确认,这是 Kafka 的一个独特之处。相反,消费者可以使用 Kafka 来追踪消息在分区里的位置(偏移量)。
2、消费者往一个叫作__consul’ler_offset的特殊主题发送消息,消息里包含每个分区的偏移量。
3、如果消费者一直处于运行状态,那么偏移量就没有 什么用处。不过,如果消费者发生崩溃或者有新的消费者加入群组,就会触发再均衡,完成再均衡之后,每个消费者可能分配到新的分区,而不是之前处理的那个。为了能够继续 之前的工作,消费者需要读取每个分区最后一次提交的偏移量,然后从偏移量指定的地方继续处理。
4、如果提交的偏移量小于客户端处理的最后一个消息的偏移量,那么处于两个偏移量之间的 消息就会被重复处理;如果提交的偏移量大于客户端处理的最后一个消息的偏移量,那么处于两个偏移量之间的 消息将会丢失。

a、自动提交

最简单的提交方式是让悄费者自动提交偏移量。如果 enable.auto.commit被设为 true,那 么每过5s(由auto.commit.interval.ms控制),消费者会自动把从 poll()方法接收到的最大偏移量提交上去。与消费者里的其他东西一样,自动提交也是在轮询里进行的。
自动提交虽然方便、简单,但是不利于异常等的处理。

b、提交当前偏移量

1、enable.auto.commit被设为 false,借助consumer.commitSync()提交。
2、处理完当前批次的消息,在轮询更多的消息之前, 调用consumer.commitSync()提交当前批次最新的偏移量。只要没有发生不可恢复的错误,consumer.commitSync()方法会一直尝试直至提交成功。如果提交失败, 我们也只能把异常记录到错误日志里。
3、同步提交会存在阻塞

while (true) {
    ConsumerRecords<String, String> records = consumer.poll(100);
    for (ConsumerRecord<String, String> record : records) {
        log.info("topic={},partition={},offset={},key={},value={}", 
        record.topic(), record.partition(), record.offset(), record.key(), record.value());
        //TODO 业务处理
    }
    try {
        consumer.commitSync();
    } catch (Exception e) {
        log.error("commit error", e);
    }
}

c、异步提交

异步提交无回调

        try {
            while (true) {
                ConsumerRecords<String, String> records = consumer.poll(100);
                for (ConsumerRecord<String, String> record : records) {
                    log.info("topic={},partition={},offset={},key={},value={}", record.topic(), record.partition(), record.offset(), record.key(), record.value());
                    //TODO 业务处理
                }
              consumer.commitAsync();
            }
        } finally {
            consumer.close();
        }

有回调的异步提交,提交失败,也有错误信息记录

try {
    while (true) {
        ConsumerRecords<String, String> records = consumer.poll(100);
        for (ConsumerRecord<String, String> record : records) {
            log.info("topic={},partition={},offset={},key={},value={}", record.topic(), record.partition(), record.offset(), record.key(), record.value());
            //TODO 业务处理
        }
        consumer.commitAsync((map, e) -> {
            if (Objects.nonNull(e)) {
                log.error("commit failed", e);
            }
        });
    }
} finally {
    consumer.close();
}

d、同步和异步组合提交

一般情况下,针对偶尔出现的提交失败,不进行重试不会有太大问题,因为如果提交失败是因为临时问题导致的,那么后续的提交总会有成功的。但如果这是发生在关闭消费者或再均衡前的最后一次提交,就要确保能够提交成功。

        try {
            while (true) {
                ConsumerRecords<String, String> records = consumer.poll(100);
                for (ConsumerRecord<String, String> record : records) {
                    log.info("topic={},partition={},offset={},key={},value={}", record.topic(), record.partition(), record.offset(), record.key(), record.value());
                    //TODO 业务处理
                }
                consumer.commitAsync();
            }
        } catch (Exception e) {
            log.error("unexpected error", e);
        } finally {
            consumer.commitSync();
            consumer.close();
        }

1、如果一切正常,我们使用consumer.commitAsync()方法来提交。这样速度更快,而且即使这次提交失败,下一次提交很可能会成功。
2、如果直接关闭消费者,就没有所谓的“下一次提交”了。使用consumer.commitSync()方法也会一直重试,直到提交成功或发生无法恢复的错误。

e、提交特定的偏移量

Map<TopicPartition, OffsetAndMetadata> currentOffsets = new HashMap<>();
int count = 0;
try {
    while (true) {
        ConsumerRecords<String, String> records = consumer.poll(100);
        for (ConsumerRecord<String, String> record : records) {
            log.info("topic={},partition={},offset={},key={},value={}", record.topic(), record.partition(), record.offset(), record.key(), record.value());
            //TODO 业务处理
            TopicPartition tp = new TopicPartition(record.topic(), record.partition());
            OffsetAndMetadata offset = new OffsetAndMetadata(record.offset() + 1, "no metadata");
            currentOffsets.put(tp, offset);
            if (count % 1000 == 0) {//1000条提交1次
                consumer.commitSync(currentOffsets);
                //consumer.commitAsync(currentOffsets,null);//同步、异步提交均可
            }
            count++;
        }
    }
} finally {
    consumer.close();
}

从特定的偏移量处理数据

主要是调用consumer.seek()、seekToBeginning()、seekToEnd()方法。

String topic = "topic";
int partition = 1;
TopicPartition tp = new TopicPartition(topic,1);
int offset = 100;
consumer.seek(tp,offset);
try {
    while (true) {
        ConsumerRecords<String, String> records = consumer.poll(100);
        for (ConsumerRecord<String, String> record : records) {
            log.info("topic={},partition={},offset={},key={},value={}", record.topic(), record.partition(), record.offset(), record.key(), record.value());
            //TODO 业务处理
        }
    }
} finally {
    consumer.close();
}

独立的消费者——没有群组的消费者

某些情况下可能只需要一个消费者从一个主题的所有分区或者某个特定的分区读取数据。这个时候就不需要消费者群组和再均衡了,只需要把主题或者分区分配给消费者,然后开始读取消息井提交偏移量。 这样就不需要订阅主题,取而代之的是为自己分配分区。
一个消费者可以订阅主题(并加入消费者群组),或者为自己分配分区, 但不能同时做这两件事情。

List<PartitionInfo> partitionInfos = consumer.partitionsFor("topic");
List<TopicPartition> partitions = partitionInfos.stream().map(x -> new TopicPartition(x.topic(), x.partition())).collect(Collectors.toList());
consumer.assign(partitions);//可以是部分partition
try {
    while (true) {
        ConsumerRecords<String, String> records = consumer.poll(100);
        for (ConsumerRecord<String, String> record : records) {
            log.info("topic={},partition={},offset={},key={},value={}", record.topic(), record.partition(), record.offset(), record.key(), record.value());
            //TODO 业务处理
        }
    }
} finally {
    consumer.close();
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值