java kafka 0.9 api_Kafka 0.9+Zookeeper3.4.6集群搭建、配置,新Client API的使用要点,高可用性测试,以及各种坑 (转载)...

4. 使用Kafka的Producer API来完成消息的推送

1) Kafka 0.9.0.1的java client依赖:

org.apache.kafka

kafka-clients

0.9.0.1

2) 写一个KafkaUtil工具类,用于构造Kafka Client

public class KafkaUtil {

private static KafkaProducer kp;

public static KafkaProducer getProducer() {

if (kp == null) {

Properties props = new Properties();

props.put("bootstrap.servers", "10.0.0.100:9092,10.0.0.101:9092");

props.put("acks", "1");

props.put("retries", 0);

props.put("batch.size", 16384);

props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");

props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");

kp = new KafkaProducer(props);

}

return kp;

}

}

KafkaProducer的K代表每条消息的key类型,V代表消息类型。消息的key用于决定此条消息由哪一个partition接收,所以我们需要保证每条消息的key是不同的。

Producer端的常用配置

bootstrap.servers:Kafka集群连接串,可以由多个host:port组成

acks:broker消息确认的模式,有三种:

0:不进行消息接收确认,即Client端发送完成后不会等待Broker的确认

1:由Leader确认,Leader接收到消息后会立即返回确认信息

all:集群完整确认,Leader会等待所有in-sync的follower节点都确认收到消息后,再返回确认信息

我们可以根据消息的重要程度,设置不同的确认模式。默认为1

retries:发送失败时Producer端的重试次数,默认为0

batch.size:当同时有大量消息要向同一个分区发送时,Producer端会将消息打包后进行批量发送。如果设置为0,则每条消息都DuLi发送。默认为16384字节

linger.ms:发送消息前等待的毫秒数,与batch.size配合使用。在消息负载不高的情况下,配置linger.ms能够让Producer在发送消息前等待一定时间,以积累更多的消息打包发送,达到节省网络资源的目的。默认为0

key.serializer/value.serializer:消息key/value的序列器Class,根据key和value的类型决定

buffer.memory:消息缓冲池大小。尚未被发送的消息会保存在Producer的内存中,如果消息产生的速度大于消息发送的速度,那么缓冲池满后发送消息的请求会被阻塞。默认33554432字节(32MB)

更多的Producer配置见官网:http://kafka.apache.org/documentation.html#producerconfigs

3) 写一个简单的Producer端,每隔1秒向Kafka集群发送一条消息:

public class KafkaTest {

public static void main(String[] args) throws Exception{

Producer producer = KafkaUtil.getProducer();

int i = 0;

while(true) {

ProducerRecord record = new ProducerRecord("test", String.valueOf(i), "this is message"+i);

producer.send(record, new Callback() {

public void onCompletion(RecordMetadata metadata, Exception e) {

if (e != null)

e.printStackTrace();

System.out.println("message send to partition " + metadata.partition() + ", offset: " + metadata.offset());

}

});

i++;

Thread.sleep(1000);

}

}

}

在调用KafkaProducer的send方法时,可以注册一个回调方法,在Producer端完成发送后会触发回调逻辑,在回调方法的 metadata对象中,我们能够获取到已发送消息的offset和落在的分区等信息。注意,如果acks配置为0,依然会触发回调逻辑,只是拿不到 offset和消息落地的分区信息。

跑一下,输出是这样的:

message send to partition 0, offset: 28

message send to partition 1, offset: 26

message send to partition 0, offset: 29

message send to partition 1, offset: 27

message send to partition 1, offset: 28

message send to partition 0, offset: 30

message send to partition 0, offset: 31

message send to partition 1, offset: 29

message send to partition 1, offset: 30

message send to partition 1, offset: 31

message send to partition 0, offset: 32

message send to partition 0, offset: 33

message send to partition 0, offset: 34

message send to partition 1, offset: 32

乍一看似乎offset乱掉了,但其实这是因为消息分布在了两个分区上,每个分区上的offset其实是正确递增的。

5. 使用Kafka的Consumer API来完成消息的消费

1) 改造一下KafkaUtil类,加入Consumer client的构造。

public class KafkaUtil {

private static KafkaProducer kp;

private static KafkaConsumer kc;

public static KafkaProducer getProducer() {

if (kp == null) {

Properties props = new Properties();

props.put("bootstrap.servers", "10.0.0.100:9092,10.0.0.101:9092");

props.put("acks", "1");

props.put("retries", 0);

props.put("batch.size", 16384);

props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");

props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");

kp = new KafkaProducer(props);

}

return kp;

}

public static KafkaConsumer getConsumer() {

if(kc == null) {

Properties props = new Properties();

props.put("bootstrap.servers", "10.0.0.100:9092,10.0.0.101:9092");

props.put("group.id", "1");

props.put("enable.auto.commit", "true");

props.put("auto.commit.interval.ms", "1000");

props.put("session.timeout.ms", "30000");

props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");

props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");

kc = new KafkaConsumer(props);

}

return kc;

}

}

同样,我们介绍一下Consumer常用配置

bootstrap.servers/key.deserializer/value.deserializer:和Producer端的含义一样,不再赘述

fetch.min.bytes:每次最小拉取的消息大小(byte)。Consumer会等待消息积累到一定尺寸后进行批量拉取。默认为1,代表有一条就拉一条

max.partition.fetch.bytes:每次从单个分区中拉取的消息最大尺寸(byte),默认为1M

group.id:Consumer的group id,同一个group下的多个Consumer不会拉取到重复的消息,不同group下的Consumer则会保证拉取到每一条消息。注意,同一个group下的consumer数量不能超过分区数。

enable.auto.commit:是否自动提交已拉取消息的offset。提交offset即视为该消息已经成功被消费,该组下的Consumer无法再拉取到该消息(除非手动修改offset)。默认为true

auto.commit.interval.ms:自动提交offset的间隔毫秒数,默认5000。

全部的Consumer配置见官方文档:http://kafka.apache.org/documentation.html#newconsumerconfigs

2) 编写Consumer端:

public class KafkaTest {

public static void main(String[] args) throws Exception{

KafkaConsumer consumer = KafkaUtil.getConsumer();

consumer.subscribe(Arrays.asList("test"));

while(true) {

ConsumerRecords records = consumer.poll(1000);

for(ConsumerRecord record : records) {

System.out.println("fetched from partition " + record.partition() + ", offset: " + record.offset() + ", message: " + record.value());

}

}

}

}

运行输出:

fetched from partition 0, offset: 28, message: this is message0

fetched from partition 0, offset: 29, message: this is message2

fetched from partition 0, offset: 30, message: this is message5

fetched from partition 0, offset: 31, message: this is message6

fetched from partition 0, offset: 32, message: this is message10

fetched from partition 0, offset: 33, message: this is message11

fetched from partition 0, offset: 34, message: this is message12

fetched from partition 1, offset: 26, message: this is message1

fetched from partition 1, offset: 27, message: this is message3

fetched from partition 1, offset: 28, message: this is message4

fetched from partition 1, offset: 29, message: this is message7

fetched from partition 1, offset: 30, message: this is message8

fetched from partition 1, offset: 31, message: this is message9

fetched from partition 1, offset: 32, message: this is message13

说明:

KafkaConsumer的poll方法即是从Broker拉取消息,在poll之前首先要用subscribe方法订阅一个Topic。

poll方法的入参是拉取超时毫秒数,如果没有新的消息可供拉取,consumer会等待指定的毫秒数,到达超时时间后会直接返回一个空的结果集。

如 果Topic有多个partition,KafkaConsumer会在多个partition间以轮询方式实现负载均衡。如果启动了多个 Consumer线程,Kafka也能够通过zookeeper实现多个Consumer间的调度,保证同一组下的Consumer不会重复消费消息。注 意,Consumer数量不能超过partition数,超出部分的Consumer无法拉取到任何数据。

可以看出,拉取到的消息并不是完全顺序化的,kafka只能保证一个partition内的消息先进先出,所以在跨partition的情况下,消息的顺序是没有保证的。

本 例中采用的是自动提交offset,Kafka client会启动一个线程定期将offset提交至broker。假设在自动提交的间隔内发生故障(比如整个JVM进程死掉),那么有一部分消息是会被 重复消费的。要避免这一问题,可使用手动提交offset的方式。构造consumer时将enable.auto.commit设为false,并在代 码中用consumer.commitSync()来手动提交。

如果不想让kafka控制consumer拉取数据时在partition间的负载均衡,也可以手工控制:

public static void main(String[] args) throws Exception{

KafkaConsumer consumer = KafkaUtil.getConsumer();

String topic = "test";

TopicPartition partition0 = new TopicPartition(topic, 0);

TopicPartition partition1 = new TopicPartition(topic, 1);

consumer.assign(Arrays.asList(partition0, partition1));

while(true) {

ConsumerRecords records = consumer.poll(100);

for(ConsumerRecord record : records) {

System.out.println("fetched from partition " + record.partition() + ", offset: " + record.offset() + ", message: " + record.value());

}

consumer.commitSync();

}

}

使用consumer.assign()方法为consumer线程指定1个或多个partition。

此处的坑:

在测试中我发现,如果用手工指定partition的方法拉取消息,不知为何kafka的自动提交offset机制会失效,必须使用手动方式才能正确提交已消费的消息offset。

题外话:

在 真正的应用环境中,Consumer端将消息拉取下来后要做的肯定不止是输出出来这么简单,在消费消息时很有可能需要花掉更多的时间。1个 Consumer线程消费消息的速度很有可能是赶不上Producer产生消息的速度,所以我们不得不考虑Consumer端采用多线程模型来消费消息。

然而KafkaConsumer并不是线程安全的,多个线程操作同一个KafkaConsumer实例会出现各种问题,Kafka官方对于Consumer端的多线程处理给出的指导建议如下:

1. 每个线程都持有一个KafkaConsumer对象

好处:

实现简单

不需要线程间的协作,效率最高

最容易实现每个Partition内消息的顺序处理

弊端:

每个KafkaConsumer都要与集群保持一个TCP连接

线程数不能超过Partition数

每一batch拉取的数据量会变小,对吞吐量有一定影响

2. 解耦,1个Consumer线程负责拉取消息,数个Worker线程负责消费消息

好处:

可自由控制Worker线程的数量,不受Partition数量限制

弊端:

消息消费的顺序无法保证

难以控制手动提交offset的时机

个人认为第二种方式更加可取,consumer数不能超过partition数这个限制是很要命的,不可能为了提高Consumer消费消息的效率而把Topic分成更多的partition,partition越多,集群的高可用性就越低。

6. Kafka集群高可用性测试

1) 查看当前Topic的状态:

/kafka-topics.sh --describe --zookeeper 10.0.0.100:2181,10.0.0.101:2181,10.0.0.102:2181 --topic test

输出:

Topic:test PartitionCount:2 ReplicationFactor:2 Configs:

Topic: test Partition: 0 Leader: 1 Replicas: 1,0 Isr: 0,1

Topic: test Partition: 1 Leader: 0 Replicas: 0,1 Isr: 0,1

可以看到,partition0的leader是broker1,parition1的leader是broker0

2) 启动Producer向Kafka集群发送消息

输出:

message send to partition 0, offset: 35

message send to partition 1, offset: 33

message send to partition 0, offset: 36

message send to partition 1, offset: 34

message send to partition 1, offset: 35

message send to partition 0, offset: 37

message send to partition 0, offset: 38

message send to partition 1, offset: 36

message send to partition 1, offset: 37

3) 登录SSH将broker0,也就是partition 1的leader kill掉

再次查看Topic状态:

Topic:test PartitionCount:2 ReplicationFactor:2 Configs:

Topic: test Partition: 0 Leader: 1 Replicas: 1,0 Isr: 1

Topic: test Partition: 1 Leader: 1 Replicas: 0,1 Isr: 1

可以看到,当前parition0和parition1的leader都是broker1了

此时再去看Producer的输出:

[kafka-producer-network-thread | producer-1] DEBUG org.apache.kafka.common.network.Selector - Connection with /10.0.0.100 disconnected

java.net.ConnectException: Connection refused: no further information

at sun.nio.ch.SocketChannelImpl.checkConnect(Native Method)

at sun.nio.ch.SocketChannelImpl.finishConnect(SocketChannelImpl.java:739)

at org.apache.kafka.common.network.PlaintextTransportLayer.finishConnect(PlaintextTransportLayer.java:54)

at org.apache.kafka.common.network.KafkaChannel.finishConnect(KafkaChannel.java:72)

at org.apache.kafka.common.network.Selector.poll(Selector.java:274)

at org.apache.kafka.clients.NetworkClient.poll(NetworkClient.java:256)

at org.apache.kafka.clients.producer.internals.Sender.run(Sender.java:216)

at org.apache.kafka.clients.producer.internals.Sender.run(Sender.java:128)

at java.lang.Thread.run(Thread.java:745)

[kafka-producer-network-thread | producer-1] DEBUG org.apache.kafka.clients.Metadata - Updated cluster metadata version 7 to Cluster(nodes = [Node(1, 10.0.0.101, 9092)], partitions = [Partition(topic = test, partition = 1, leader = 1, replicas = [1,], isr = [1,], Partition(topic = test, partition = 0, leader = 1, replicas = [1,], isr = [1,]])

能看到Producer端的DEBUG日志显示与broker0的链接断开了,此时Kafka立刻开始更新集群metadata,更新后的metadata表示broker1现在是两个partition的leader,Producer进程很快就恢复继续运行,没有漏发任何消息,能够看出Kafka集群的故障切换机制还是很厉害的

4) 我们再把broker0启动起来

bin/kafka-server-start.sh -daemon config/server.properties

然后再次检查Topic状态:

Topic:test PartitionCount:2 ReplicationFactor:2 Configs:

Topic: test Partition: 0 Leader: 1 Replicas: 1,0 Isr: 1,0

Topic: test Partition: 1 Leader: 1 Replicas: 0,1 Isr: 1,0

我们看到,broker0启动起来了,并且已经是in-sync状态(注意Isr从1变成了1,0),但此时两个partition的leader还都是 broker1,也就是说当前broker1会承载所有的发送和拉取请求。这显然是不行的,我们要让集群恢复到负载均衡的状态。

这时候,需要使用Kafka的选举工具触发一次选举:

bin/kafka-preferred-replica-election.sh --zookeeper 10.0.0.100:2181,10.0.0.101:2181,10.0.0.102:2181

选举完成后,再次查看Topic状态:

Topic:test PartitionCount:2 ReplicationFactor:2 Configs:

Topic: test Partition: 0 Leader: 1 Replicas: 1,0 Isr: 1,0

Topic: test Partition: 1 Leader: 0 Replicas: 0,1 Isr: 1,0

可以看到,集群重新回到了broker0挂掉之前的状态

但此时,Producer端产生了异常:

org.apache.kafka.common.errors.NotLeaderForPartitionException: This server is not the leader for that topic-partition.

原因是Producer端在尝试向broker1的parition0发送消息时,partition0的leader已经切换成了broker0,所以消息发送失败。

此时用Consumer去消费消息,会发现消息的编号不连续了,确实漏发了一条消息。这是因为我们在构造Producer时设定了retries=0,所以在发送失败时Producer端不会尝试重发。

将retries改为3后再次尝试,会发现leader切换时再次发生了同样的问题,但Producer的重发机制起了作用,消息重发成功,启动Consumer端检查也证实了所有消息都发送成功了。

每 次集群单点发生故障恢复后,都需要进行重新选举才能彻底恢复集群的leader分配,如果嫌每次这样做很麻烦,可以在broker的配置文件(即 server.properties)中配置auto.leader.rebalance.enable=true,这样broker在启动后就会自动进 行重新选举

至此,我们通过测试证实了集群出现单点故障和恢复的过程中,Producer端能够保持正确运转。接下来我们看一下Consumer端的表现:

5) 同时启动Producer进程和Consumer进程

此时Producer一边在生产消息,Consumer一边在消费消息

6) 把broker0干掉,观察Consumer端的输出:

能看到,在broker0挂掉后,consumer也端产生了一系列INFO和WARN输出,但同Producer端一样,若干秒后自动恢复,消息仍然是连续的,并未出现断点。

7) 再次把broker0启动,并触发重新选举,然后观察输出:

fetched from partition 0, offset: 418, message: this is message48

fetched from partition 0, offset: 419, message: this is message49

[main] INFO org.apache.kafka.clients.consumer.internals.ConsumerCoordinator - Offset commit for group 1 failed due to NOT_COORDINATOR_FOR_GROUP, will find new coordinator and retry

[main] INFO org.apache.kafka.clients.consumer.internals.AbstractCoordinator - Marking the coordinator 2147483646 dead.

[main] WARN org.apache.kafka.clients.consumer.internals.ConsumerCoordinator - Auto offset commit failed: This is not the correct coordinator for this group.

fetched from partition 1, offset: 392, message: this is message50

fetched from partition 0, offset: 420, message: this is message51

能看到,重选举后Consumer端也输出了一些日志,意思是在提交offset时发现当前的调度器已经失效了,但很快就重新获取了新的有效调度器,恢复 了offset的自动提交,验证已提交offset的值也证明了offset提交并未因leader切换而发生错误。

如上,我们也通过测试证实了Kafka集群出现单点故障时,Consumer端的功能正确性。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,以下是一个基本的在k8s上部署zookeeper + kafka集群的配置教程: 1. 创建一个namespace 首先,我们需要创建一个namespace,用于部署zookeeperkafka集群。可以使用以下命令创建一个名为“zookeeper-kafka”的namespace: ``` kubectl create namespace zookeeper-kafka ``` 2. 部署Zookeeper 接下来,我们需要部署Zookeeper。可以使用以下YAML文件创建一个Zookeeper服务: ``` apiVersion: v1 kind: Service metadata: name: zookeeper spec: selector: app: zookeeper ports: - name: client port: 2181 protocol: TCP - name: follower port: 2888 protocol: TCP - name: leader port: 3888 protocol: TCP --- apiVersion: apps/v1 kind: StatefulSet metadata: name: zookeeper spec: serviceName: zookeeper replicas: 3 selector: matchLabels: app: zookeeper template: metadata: labels: app: zookeeper spec: containers: - name: zookeeper image: zookeeper:3.5.8 ports: - name: client containerPort: 2181 - name: follower containerPort: 2888 - name: leader containerPort: 3888 volumeMounts: - name: datadir mountPath: /data env: - name: ZOO_MY_ID valueFrom: fieldRef: fieldPath: metadata.name - name: ZOO_SERVERS value: zookeeper-0.zookeeper:2888:3888,zookeeper-1.zookeeper:2888:3888,zookeeper-2.zookeeper:2888:3888 volumeClaimTemplates: - metadata: name: datadir spec: accessModes: - ReadWriteOnce resources: requests: storage: 1Gi ``` 这将创建一个3个Pod的Zookeeper StatefulSet,并创建一个名为“zookeeper”的Service,暴露Zookeeper的客户端端口2181,follower端口2888和leader端口3888。 3. 部署Kafka 现在,我们可以部署Kafka。以下是一个Kafka部署的YAML文件示例: ``` apiVersion: v1 kind: Service metadata: name: kafka spec: type: NodePort selector: app: kafka ports: - name: kafka port: 9092 nodePort: 30092 protocol: TCP --- apiVersion: apps/v1 kind: StatefulSet metadata: name: kafka spec: serviceName: kafka replicas: 3 selector: matchLabels: app: kafka template: metadata: labels: app: kafka spec: containers: - name: kafka image: wurstmeister/kafka:2.13-2.7.0 ports: - name: kafka containerPort: 9092 env: - name: KAFKA_BROKER_ID valueFrom: fieldRef: fieldPath: metadata.name - name: KAFKA_ZOOKEEPER_CONNECT value: zookeeper-0.zookeeper:2181,zookeeper-1.zookeeper:2181,zookeeper-2.zookeeper:2181 - name: KAFKA_ADVERTISED_LISTENERS value: PLAINTEXT://$(hostname -f):9092 - name: KAFKA_LISTENERS value: PLAINTEXT://0.0.0.0:9092 - name: KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR value: "3" volumeMounts: - name: datadir mountPath: /data volumeClaimTemplates: - metadata: name: datadir spec: accessModes: - ReadWriteOnce resources: requests: storage: 1Gi ``` 这将创建一个3个Pod的Kafka StatefulSet和一个名为“kafka”的Service,它将Kafka的9092端口暴露为NodePort 30092。 4. 验证部署 现在,您可以使用以下命令检查ZookeeperKafka是否正在运行: ``` kubectl get pods -n zookeeper-kafka ``` 您应该看到3个Zookeeper和3个Kafka Pod处于“Running”状态。 接下来,您可以使用以下命令检查Kafka是否正在监听端口30092(或您自己选择的端口): ``` kubectl get services -n zookeeper-kafka ``` 您应该看到一个名为“kafka”的service,它将Kafka的9092端口暴露为30092端口。可以使用此端口测试Kafka是否正常运行。 至此,您已经成功地在k8s上部署了zookeeper + kafka集群。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值