前面我们已经搭建了kafka的单机和集群环境,分别写了简单的实例代码,对于代码里面使用到的参数并没有做解释。下面我们来详细说一下各个参数的作用。
1. 创建kafka生产者
kafka生产者有3个必选的属性:
bootstrap.servers
该属性指定broker的地址清单,地址的格式为host:port。清单里不需要包含所有的broker 地址,生产者会从给定的broker里查找到其他broker 的信息。不过建议至少要提供两个broker的信息,一且其中一个若机,生产者仍然能够连接到集群上。
key.serializer
broker 希望接收到的消息的键和值都是字节数组。生产者接口允许使用参数化类型,因此可以把Java对象作为键和值发送给broker。这样的代码具有良好的可读性,不过生产者需要知道如何把这些Java 对象转换成字节数组。key.serializer必须被设置为一个实现了org.apache.kafka.common.serialization.Serializer接口的类,生产者会使用这个类把键对象序列化成字节数组。
要注意, key.serializer是必须设置的,就算你打算只发送值内容。
value.serializer
与key.serializer一样,value.serializer指定的类会将值序列化。如果键和值都是字符串,可以使用与key.serializer一样的序列化器。否则就得使用不同的序列化器。
下面的代码中展示了如何创建一个生产者,这里只指定了必要的字段。
Properties props = new Properties();
props.put("bootstrap.servers", "192.168.131.128:9092,192.168.131.130:9092,192.168.131.131:9092");
props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");
Producer<String, String> producer = new KafkaProducer<>(props);
我们的键值都定义为String对象,所以都是用的是内置的StringSerializer。
实例化生产者对象之后我们就可以发送消息了,发消息主要有一下3种方式:
发送并忘记(fire-and-forget)
我们把消息发送给服务器,但井不关心它是否正常到达。大多数情况下,消息会正常到达,因为Kafka 是高可用的,而且生产者会自动尝试重发。不过,使用这种方式有时候也会丢失一些消息。
同步发送
我们使用send()方怯发送消息, 它会返回一个Future对象,调用get()方法进行等待,就可以知道消息是否发送成功。
异步发送
我们调用send()方法并且指定一个回调函数,服务器在返回响应时调用该函数。
上面我们已经创建了一个生产者实例,接下来就可以发送消息了:
ProducerRecord<String, String> data = new ProducerRecord<String, String>("page_visits","test_key","test_msg");
try{
producer.send(data);
}catch{
e.printStackTrace();
}
上面我们构造了一个ProducerRecord对象用来封装topic,消息的key-value。然后用生产者实例的send方法将消息体发送出去。
1.1 同步发送消息
最简单的同步发送消息如下:
ProducerRecord<String, String> data = new ProducerRecord<String, String>("page_visits","test_key","test_msg");
try{
producer.send(data).get();
}catch{
e.printStackTrace();
}
即producer.send(data)返回的是一个Future对象,然后调用Future的get()方法僧带Kafka的响应。如果服务器返回错误,get()方法会抛异常。没有发生错误的话会得到一个RecordMetadata对象。可以用它来获取消息的偏移量。
1.2 异步发送消息
producer发送消息给broker,如果发送成功,会把目标主题,分区信息以及消息偏移量发送回来。但是发送端大多数时候并不关心这些,只在乎是否发送成功。有的时候对于发送是否成功的状态也不是那么的着急需要。在消息量比较大的时候,如果每一条消息都需要同步去确认发送状态很显然是会发生超时的,这个时候异步回调机制很好的帮我们解决了这个问题。
ProducerRecord<String, String> data = new ProducerRecord<>("page_visits","test_key","test_msg");
try{
producer.send(data,new Callback() {
public void onCompletion(RecordMetadata metadata, Exception e) {
if(e != null) {
e.printStackTrace();
} else {
System.out.println("The offset of the record we just sent is: " + metadata.offset());
}
}
});
}catch{
e.printStackTrace();
}
实现回调需要实现org.apache.kafka.clients.producer.Callback接口,这个接口只有一个onCompletion方法。
上面我们简单实现了一个生产者,提供了一个基本实现,producer还有很多其余的配置可以灵活使用,下面我们来看一下其余的参数:
acks :acks参数指定了必须要有多少个分区副本收到消息,生产者才会认为消息写入是成功的。这个参数对消息丢失的可能性有重要影响。该参数有如下选项:
- acks=0 生产者在成功写入悄息之前不会等待任何来自服务器的响应。
- acks=1 只要集群的首领节点收到消息, 生产者就会收到一个来自服务器的成功响应。
- acks=all 只有当所有参与复制的节点全部收到消息时, 生产者才会收到一个来自服务器的成功响应。这种模式是最安全的,它可以保证不止一个服务器收到消息,就算有服务器发生崩溃,整个集群仍然可以运行,不过,它的延迟比acks=l 时更高,因为我们要等待不只一个服务器节点接收消息。
buffer.memory: 该参数用来设置生产者内存缓冲区的大小,生产者用它缓冲要发送到服务器的消息。如果应用程序发送消息的速度超过发送到服务器的速度,会导致生产者空间不足。
- compression.type : 默认情况下,消息发送时不会被压缩。该参数可以设置为snappy,gzip或lz4,它指定了消息被发送给broker 之前使用哪一种压缩算陆进行压缩。
- retries : 生产者从服务器收到的错误有可能是临时性的错误(比如分区找不到首领)。在这种情况下,retries参数的值决定了生产者可以重发消息的次数,如果达到这个次数,生产者会放弃重试并返回错民。默认情况下,生产者会在每次重试之间等待lOOms,不过可以通过retry.backoff.ms参数来改变这个时间间隔。
- batch.size : 当有多个消息需要被发送到同一个分区时,生产者会把它们放在同一个批次里。该参数指定了一个批次可以使用的内存大小,按照字节数计算(而不是消息个数)。当批次被填满,批次里的所有消息会被发送出去。不过生产者井不一定都会等到批次被填满才发送,半满的批次,甚至只包含一个消息的批次也有可能被发送。
- linger.ms : 该参数指定了生产者在发送批次之前等待更多消息加入批次的时间。KafkaProducer会在批次填满或linger.ms达到上限时把批次发送出去。默认情况下,只要有可用的线程,生产者就会把消息发送出去,就算批次里只有一个消息。把linger.ms设置成比0大的数,让生产者在发送批次之前等待一会儿,使更多的消息加入到这个批次。
- partitioner.class :实现Partitioner接口的分区类
- max.block.ms : 这个配置控制着KafkaProducer.send() 和 KafkaProducer.partitionsFor()会阻塞多久。这个方法会因为缓冲区满了或是元数据不可用而被阻塞。在用户提供的 serializers 或 partitioner 里的阻塞将不会被计算在这个超时里。
- client.id : 发送请求时传递给服务器的一个标识字符串。这样做的目的是能过追踪请求的来源除了除了IP/端口。它是通过允许一个逻辑应用名称被包括在一个服务器端的请求日志来实现的。
1.3分区和实现自定义分区策略
在前面的基本知识学习里面我们已经讲过kafka分区的问题,一个topic的消息会对应着一个或者多个partition。如果使用默认的分区器,那么记录将会被随机的发送到主题可用的各个分区上,分区器使用轮询算法均衡的分布到各个分区上。
kafka给用户提供了灵活的分区策略。如果你不想使用默认的均衡分布策略,而是有自己的逻辑需求,那么你可以自己控制。比如在一个topic下的消息分为两种:一种是用户信息增量更新的消息,一种是任职信息增量更新的消息。那么你可以使用自定义分区功能将两种消息分别散列在同一个topic的各个partition上。
实现自定义分区策略需要继承Partitioner接口。
import java.util.List;
import java.util.Map;
import org.apache.kafka.clients.producer.Partitioner;
import org.apache.kafka.common.Cluster;
import org.apache.kafka.common.PartitionInfo;
public class SimplePartitioner2 implements Partitioner {
private static String USER_INFO_KEY = "simple_user_info";
@Override
public void configure(Map<String, ?> configs) {
// TODO Auto-generated method stub
}
@Override
public int partition(String topic, Object key, byte[] keyBytes,
Object value, byte[] valueBytes, Cluster cluster) {
int partition = 0;
List<PartitionInfo> partitions = cluster.partitionsForTopic(topic);
int numPartitions = partitions.size();
if(numPartitions != 2){
return partition;
}
String stringKey = (String) key;
if(stringKey.contains(USER_INFO_KEY)){
partition =0;
}
return partition;
}
@Override
public void close() {
// TODO Auto-generated method stub
}
}
在上面的例子中,我们通过实现Partitioner的partition方法,对消息的key进行硬编码,确定key中是否包含某个标志来区分消息的类型进而确定该消息应该进入哪个分区。
2. 创建kafka消费者
2.1 消费者中的群组
熟悉JMS消息协议的应该知道,消息的消费应该有两种方式:一种是点对对的消息,一方发送,一方消费。一种是发布-订阅模式,一方发送,所有订阅方都可以消费。kafka也是遵循JMS消息规范的,所以这两种消息模式都是可以支持。但是支持的方式可能和我们想象的有些不一样!
kafka里面的消费者对应着一个消费者群组的概念。与别的消息中间件产品不同的是,每个消费者必须是属于某个消费者群组。一个群组里的消费者订阅的是同一个主题。我们通过kafka的”group.id”参数来配置消费者所属的群组。
如果你以为kafka的服务端收到一条推送的消息之后,订阅的群组里面所有消费者可以同时消费到这一条消息那你就错了。kafka对于同属一个群组的消费者的定义是:
- 同一个分区的消息只能被某个群组中的某个消费者同时消费。
- 如果一个群组中的多个消费者订阅了同一个分区,那么只能有一个消费者可以同时获得这一条消息。
- 只有一个消费者的群组是可以同时监听到某个主题下的多个分区的。
- 同一个群组下的一个消费者可以指定监听多个分区,如果消费者数量和分区数量相等也可以指定1V1的监听模式。但是,如果消费者数量大于分区数量,那么必然会有消费者无法获得消息。
由上我们得知:群组的概念并不是消费-订阅模式!
我们需要严格保证同一个群组下的消费者数量必须小于等于所订阅的topic的分区数量。
如果存在消费者数量多于分区数量的情况,我们可以将消费者置于不同的分组中,然后再订阅该主题。kafka支持多个分组同时订阅。
关于具体的消费者的消费模式我们后面详细讨论,先来实现一个简单的消费者。
Properties props = new Properties();
props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "192.168.131.128:9092,192.168.131.130:9092,192.168.131.131:9092");
props.put(ConsumerConfig.GROUP_ID_CONFIG ,"test") ;
props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
Consumer<String, String> consumer = new KafkaConsumer<>(props);
消费者基本的配置和生产者差不多,我们看到第二个设置,指定了分组id。
然后需要指定要订阅哪一个topic,当然一个分组是可以订阅多个topic的,所以参数为List:
consumer.subscribe(Arrays.asList("page_visits"));
参数也可以是正则匹配。
消息轮询是消费者API的核心,通过一个简单的轮询向服务器请求数据:
while (true) {
ConsumerRecords<String, String> records = consumer.poll(100);
for (ConsumerRecord<String, String> record : records) {
System.out.printf("offset = %d, key = %s, value = %s%n", record.offset(), record.key(), record.value());
}
}
消费者必须不停地向kafka轮询,否则会被认为死亡。它的分区会被交给组里的其他消费者。poll()方法里面的参数是一个超时时间,用于设置该次请求的超时时间,如果参数被设置为0 ,请求会立即返回,否则请求会在指定的毫秒数内返回。
2.2 消费者基本配置项
消费者也有一些基本的配置项:
- fetch.min.bytes:该属性指定了消费者从服务器获取记录的最小字节数。
- fetch.max.wait.ms:该属性指定了消费者从服务器获取记录的最大等待时间。该参数和fetch.min.bytes参数会互相克制。如果fetch.min.bytes没有得到满足,则会继续等待消息填充,但是如果到了fetch.max.wait.ms设置的时间,那么会直接返回。同理相反也是一样的。
- max.partition.fetch.bytes:该属性指定了服务器从每个分区里返回给消费者的最大字节数。它的默认值是lMB。
- session.timeout.ms:该属性指定了消费者在被认为死亡之前可以与服务器断开连接的时间,默认是3s。如果消费者没有session.timeout.ms定的时间内发送心跳给群组协调器,就被认为已经死亡,协调器就会触发再均衡,把它的分区分配给群组里的其他消费者。该属性与heartbeat.interval.ms紧密相关heartbeat.interval.ms指定了poll()向服务器发送心跳的频率, session.timeout.ms则指定了消费者可以多久不用给服务器发送心跳。
- auto.offset.reset:该属性指定了消费者在读取一个没有偏移量的分区或者偏移量无效的情况下(因消费者长时间失效,包含偏移量的记录已经过时井被删除)该作何处理。即指定该从何位置开始读取记录。
- enable.auto.commit:该属性指定了消费者是否自动提交偏移量,默认值是true。为了尽量避免出现重复数据和数据丢失,可以把它为false,由自己控制何时提交偏移量。如果把它设为true ,还可以通过配置auto.commit.interval.ms属性来控制提交的频率。
partition.assignment.strategy:分区会被分配给群组里的消费者。PartitionAssignor根据给定的消费者和主题,决定哪些分区应该被分配给哪个消费者。Kafka有两个默认的分配策略:
range
该策略会把主题的若干个连续的分区分配给消费者。假设消费者Cl和消费者C2 同时订阅了主题Tl和主题口,井且每个主题有3 个分区。那么消费者Cl有可能分配到这两个主题的分区0和分区i,而消费者C2分配到这两个主题的分区2。因为每个主题拥有奇数个分区,而分配是在主题内独立完成的,第一个消费者最后分配到比第二个消费者更多的分区。只要使用了Range策略,而且分区数量无法被消费者数量整除,就会出现这种情况。
RoundRobin
该策略把主题的所有分区逐个分配给消费者。如果使用RoundRobin策略来给消费者Cl和消费者C2分配分区,那么消费者Cl将分到主题Tl的分区0和分区2以及主题T2的分区1 ,消费者C2 将分配到主题Tl 的分区l 以及主题口的分区0 和分区2 。一般来说,如果所有消费者都订阅相同的主题(这种情况很常见), RoundRobin策略会给所有消费者分配相同数量的分区(或最多就差一个分区)。
可以通过设置partition.assignment.strategy来选择分区策略.
client.id:该属性可以是任意字符串,broker用它来标识从客户端发送过来的消息,通常被用在日志、度量指标和配额里。
主要的配置项如上,如果工作中还有别的需要可以去看官网的api查看。
客户端其实要说的东西还是挺多,下一节学习我们就来看关于自动提交、手动提交和偏移量的关系,又够喝一壶的。