kafka生产者

Kafka-生产者

生产者主要是负责向broker写消息。客户端在向broker消息写消息时,不但可以通过kafka内置的客户端外,还可以通过Kafka提供的二进制连接协议向kafka网络端口发送适当的字节序列进行消息写(客户端通过哪种语言来发送二进制协议这就不是kafka关心的)。


生产消息流程

kafka生产者_第1张图片
kafka_producter.png
  • ProducterRecord :是生产者发送给broker的消息对象结构,主要包含消息的主题和内容,我们还可以指定键和分区。
  • 序列化器 :主要负责对消息的键和消息内容进行序列化,将其序列化为字节数组然后才能进行网络传输。
  • 分区器 :如果在ProducterRecord指定了分区,那么分区器不做任何事情,直接把指定的分区返回;如果说ProducterRecord未指定分区,那么分区器会根据键key来选择一个分区;选好分区后,生产者就知道往哪个主题和分区发送这条记录。

注意:这里需要注意的是,当生产者得到分区和主题后,它会发送这条记录。紧接着,这条记录它是会先被添加缓存到到一个批次里,这个批次里的所有消息会被发送到同一个主题和分区上。会有一个独立的线程负责把这些记录发送到相应的分区上。通过将消息合并到同一个批次后进行发送,可以减少网络请求,提高服务器的性能。如果Kafka写消息成功后,会返回一个RecordMetaData对象,这个对象里包含了主题和分区信息,以及记录在分区里的偏移量。

创建生产者

生产者生产消息的时候需要定义一个生产者来生产消息并发送,下面我们看下怎么创建生产者

package demo;

import org.apache.kafka.clients.producer.Callback;
import org.apache.kafka.clients.producer.KafkaProducer;
import org.apache.kafka.clients.producer.ProducerRecord;
import org.apache.kafka.clients.producer.RecordMetadata;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;

public class Producter {
    public static final String zk_address="127.0.0.1:2181";
    private static Properties kafkaProps = new Properties();
    public static KafkaProducer producer = null;
    static{
        //在定义一个生产者的时候,有几个参数是必须配置指定的,
        //分别是bootstrap.severs、key.serializer和value.serializer
        kafkaProps.put("bootstrap.severs",zk_address);
        kafkaProps.put("key.serializer",
                    "org.apache.kafka.common.serialization.StringSerializer");
        kafkaProps.put("value.serializer",
                    "org.apache.kafka.common.serialization.StringSerializer");
    }
    public static void main(String args[]){
        //初始化生产者
        producer=new KafkaProducer(kafkaProps);
        List records = getProducerRecords();
        //普通发送
        producer.send(records.get(0));
        
        //同步发送
        Future feature = producer.send(records.get(1));
        try {//等待响应,返回一个包含元数据信息的RecordMetadata,可以用它获取到偏移量
            RecordMetadata metadata =feature.get();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }

        //异步发送
        producer.send(records.get(2), new MyCallBack();

    }
    public static List getProducerRecords(){
        List records = new ArrayList ();
        for(int i = 0;i<10000;i++){
        //创建一个消息ProducerRecord需要topic、key、value,键和值必须和序列化器匹配(当然它还有别的构造函数)
            ProducerRecord record = new ProducerRecord ("xuzf_topic1","key_"+i,"val_"+i);
            records.add(record);
        }
        return records;
    }
    static class MyCallBack implements Callback {
        public void onCompletion(RecordMetadata recordMetadata, Exception e) {
            System.out.println("topic="+recordMetadata.topic()+",partition="+recordMetadata.partition()
                    +",offset="+recordMetadata.offset() );
        }
    }
}

  • bootstrap.severs :主要是用来指定broker的地址,地址格式为host:post。这里要注意,对于一个集群的broker,这里不需要包含所有的清单,因为它自动会根据我们配置的broker找到集群中其它的broker。但是为了防止配置的broker出现网络分裂,所以建议还是要配置多个,避免其中的某一个下线而导致无法连接上集群。
  • key.serializer :上面我们说,对于broker,它只接收序列化的key-value,所以我们这里通过key.serializer来指定特定的序列化类对key进行序列化,序列化为字节数组。
  • value.serializer :和key.serializer一样,不过它是负责将消息的内容进行序列化。
发送消息

我们在发送消息的时候主要是通过调用send方法进行发送消息。通过上面的demo,我们可以发现在发送的时候共有三种方式:普通发送、同步发送、异步发送。

  • 普通发送 :生产者在发送完消息后,直接继续处理其它事情,而不关心它是否能够正常发送到kafka中。通常这种情况下性能比同步会更好,而且它也不用担心消息发送失败,因为kafka在最初设计的时候本身就是设计成高可用的。当消息发送失败的时候,生产者端会默认的进行自动重试,不好的地方就是它有可能会出现消息丢失。代码实现 :producer.send(records.get(0))。
  • 同步发送 :是指消息在发送给broker的时候,会进行同步阻塞并等待broker端返回结果,这送发送方式的调用其实和普通发送一样,只不过它会通过调用返回的feature对象获取发送结果。这种性能比较差,通常不会这么使用。代码实现 :producer.send(records.get(0)).get()。
  • 异步发送 :和前面两种不一样,它是通过服务器在返回响应时,会回调一个回调函数,来通知生产者获取消息发送的结果,对于失败的,我们可以通过回调函数记录日志。相对来会所,这种不但性能要好,而是可以通知生产者并返回结果。producer.send(records.get(2), new Callback() {});

注意:在调用send方法的时候,消息是先被缓存到缓冲区里,然后由一个单独的线程在合适的时机发送给服务端。kafka在消息发送失败的时候,默认会重试5次。但是要注意,并不是所有的失败都会重试,像消息长度太长,序列化不对等这种异常,都不会进行重试。但是像请求超时这种异常就会进行重试。

生产者配置

前面,我们主要介绍了定义一个生产者的基本配置。在生产者中,除了这些基本配置外,还有一些其它的配置:

属性 value desc
acks 0、1和all 指定必须要收到多少个副本分片(包括住分片)返回的确认响应,才算消息发送成功,如果acks=1,则只要leader的响应就可以。(如果是异步发送,客户端不受该值影响)
buffer.memory 该值用来设置生产者的内存缓冲区,生产者用它来缓冲要发送给服务器的消息(如果生产者发送给缓冲区的速度超过从缓冲区发送到服务器的速度,那么就会出现服务器缓冲区不足,这个时候send方法会被阻塞一段时间后抛异常,取决于max.block.ms)
copression.type snappy、gzip或lz4 默认情况下,消息发送时是不压缩的。该参数指定了消息在被发送给broker之前使用了哪一种压缩算法对消息进行压缩。snappy:使用较少的cpu却能换来客观的压缩比并提供客观的性能。gzip:占用较多的cpu,但会提供更高的压缩比,如果网络宽带有限,可以采用此种。使用压缩算法,可以降低网络开销
retries 数值,默认是5 生产者接收到服务器返回的错误可能是临时性的,比如leader没有了,这个时候生产者会进行重试,该配置就是用来控制重试的次数。每次重试的时间间隔是100ms,可以通过retry.backoff.ms来改变这个间隔(不可重试的错误比如消息过长)
batch.size 内存的大小 当多个消息被发送到一个分区时,生产者会把他们放到一个批次里,该参数指定了一个批次可以使用的内存大小,按照字节来计算 。当批次被填满时,批次会被发送出去,不过生产者不一定会等到批次满了才发送出去。
linger.ms 该参数用来指定生产者在发送批次之前等待更多消息加入批次的时间。kafka会在批次填满或者 linger.ms达到上限时把批次发送出去。默认的情况下,只要有可用的线程,就会把批次发送出去,即使批次里只有一个消息。把 linger.ms设置的大一点,这样在批次被发送出去时可以等待一段时间,以便让更多的消息加入到批次里,即使这样可能会增加部分消息的延迟,但也会提升吞吐量
max.in.flight.requests.per.connection kafka可以在一个connection中发送多个请求,叫作一个flight,这样可以减少开销,但是如果产生错误,可能会造成数据的发送顺序改变,默认是5 (修改)。它的值越高就会占用越多的内存,不过也会提升吞吐量。当值设置为1的时候可以保证消息是顺序写入服务器的,即使发生了重试
timeout.ms 指定了broker等待同步副本返回消息确认的时间,与acks的配置相匹配
request.timeout.ms 指定生产者发送完消息后等待服务器返回响应的时间。如果等待超时,那么生产者就会重试
metadata.fetch.timeout.ms 指定了生产者在获取元数据时等待响应的时间
max.block.ms 用来指定在调用send()和partitionsFor()获取元数据时生产者的阻塞时间。当生产者的发送缓冲区已满,或者没有可用的元数据的时,这些方法会阻塞,在阻塞超时时,会抛出异常
max.request.size 用于控制生产者一次请求最大能发送的消息大小,不管是单个还是批次,都不能超过这个值。另外block对可接收的最大消息也有自己的限制:message.max.bytes。所以两边的配置最好匹配
receive.buffer.size 指定了tcp socket接收时数据包的缓冲大小,当值为-1时采用系统默认的值
send.buffer.bytes 指定了tcp socket发送时时数据包的缓冲大小,当值为-1时采用系统默认的值

序列化器

在创建生产者的时候必须指定序列化器:

  • key.serializer :上面我们说,对于broker,它只接收序列化的key-value,所以我们这里通过key.serializer来指定特定的序列化类对key进行序列化,序列化为字节数组。
  • value.serializer :和key.serializer一样,不过它是负责将消息的内容进行序列化。
private static Properties kafkaProps = new Properties();
 static{
            //在定义一个生产者的时候,有几个参数是必须配置指定的,
            //分别是bootstrap.severs、key.serializer和value.serializer
            kafkaProps.put("bootstrap.servers",broker_address);
            kafkaProps.put("key.serializer",
                        "org.apache.kafka.common.serialization.StringSerializer");
            kafkaProps.put("value.serializer",
                        "org.apache.kafka.common.serialization.StringSerializer");
        }
1、自定义序列化器

我们知道kafka为我们提供了集中默认的整型和字节数组序列化器,但在某些场景情况下:比如发送到kafka是一个对象,而不再是简单的字符串或者整型,这个时候我们还是需要通过自定义来应对特定的场景。这个时候我们可以使用序列化框架来创建消息记录,比如avro/thrift和protobuf等,或者使用自定义的序列化器。
当我们自定义系列化器的时候,可以实现kafka提供的接口Serializer:

import java.nio.ByteBuffer;
import java.util.Map;
import org.apache.kafka.common.errors.SerializationException;
import org.apache.kafka.common.serialization.Serializer;
import kafka.message.bean.User;
/***
 * 自定义序列化器
 * 自定义的系列化器要实现org.apache.kafka.common.serialization.Serializer
 * @author xuzf
 *
 */
public class UserSerializer implements Serializer{

    public void configure(Map configs, boolean isKey) {
        // TODO 这里可以做额外的配置,比如字节码啊等等,可以做一些附加操作       
    }
    
    public byte[] serialize(String topic, User user) {
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        if(user == null) {
            return null;
        }
        try {//通过下面的写法,我们可以看到,这种方式,灵活性低,只要生产者字段类型发生变化,就会出现版本兼容的问题
            buffer.putInt(user.getId());//第一个字段是
            if(user.getName()!=null) {
                buffer.putInt(user.getName().getBytes("UTF-8").length);
                buffer.put(user.getName().getBytes("UTF-8"));
            }else {//注意没有也要补上,这样消费端才会根据顺序读取
                buffer.putInt(0);
                buffer.put(new byte[0]);
            }
            buffer.putInt(user.getAge());//int类型的长度是固定的,所以不要传长度
            
            if(user.getSex()!=null) {
                buffer.putInt(user.getSex().getBytes("UTF-8").length);
                buffer.put(user.getSex().getBytes("UTF-8"));
            }else {//注意没有也要补上,这样消费端才会根据顺序读取
                buffer.putInt(0);
                buffer.put(new byte[0]);
            }
            return buffer.array();
        }catch(Exception e) {
            throw new SerializationException("error when Serialize user to byte[]"+e);
        }
    }

    public void close() {
        // 可以关闭任何东西
    }
    
}

对于自定义的序列化器,序列化和反序列化一般需要严格依赖相同的数据格式。一旦一方出现版本升级,就有可能导致反序列化失败。

2、使用Avro序列化

Avro 是一种跟编程语言无关的序列化格式。它的主要思想就在于,就在于将序列化的对象分成两部分:JSON描述的schema和二进制或Json格式的数据。其中schema主要负责描述数据的格式,一般会被内嵌在数据文件里。它的特性是,无论是写消息的应用程序使用了新的schema还是说负责读消息采用旧的schema,都不会影响到程序正常的序列化,只是可能有些信息序列化后为空。

我们来看个例子:

{  “namespace”:"kafka.message.avro.bean",//namespace在java项目中翻译成包名 
    "type":"record",  //必须配置type为record
    "name":"User",  //类名
    "fields":[  //fields就是配置的属性
        {"name":"id","type":"int"},  //这里表示id字段是必须的
        {"name":"name","type":"string"},  //这里表示name字段是必须的
      //对于反序列化,如果没有fixNumber,则返回null  
        {"name":"fixNumber","type":["null","string"],"default":"null"}//这里表示faxNhumber是可选的,默认是null
    ]  
}  
  • 注意
    avro可以让我们避免了在序列化的数据结构发生版本升级的时候,而因为反序列化的一端未及时的跟进,而导致应用异常或终端。但是它还是有自己的局限性:

1、用户写入数据和读取数据的schema必须是相互兼容的。
2、反序列化器需要用于写入数据的schema,即使它可能用于读取数据的schema不一样。Avro数据文件里就包含了用于写入数据的schema,不过在Kafka里采用了更好的处理方式,注册的方式。

3、kafka的Avro序列化

通过对avro序列化的了解后我们发现。一方面,传统的avro对于写入数据和读取数据采用的是两个独立的schema,这就有可能会出现两个schema不一致,或者不兼容;另一方面,对于传统的avro,需要每次将shcema都潜入到数据体中,这本身就增加了网络传输的损耗。针对以上两种可能潜在的问题,我们采用了schema注册表。它通过将每一个版本的schema注册到一个注册表上,并返回与schema一一对应的id。而消息体中只要将id潜入到数据中即可。对于反序列化的一端,它通过id从注册表中拿到对应的schema进行反序列化即可。


kafka生产者_第2张图片
schema注册和拉取
分区

前面讲过,当消息被发送到broker上的某个主题的时候,producter会把消息根据分区算法和键从而将消息分发到不同的分区里,从而实现了broker的负载均衡。而且通过分区,还可以实现动态扩容。

producter在将消息分发送到broker时,不同情况,分区算法不一致:

  • key为null且默认分区器 :则分区器会采用默认的轮询(random robin)算法将消息均衡分布到各个分区上。
  • key不为null且默认分区器 :则分区器会先对key进行散列(使用的kafka自己的散列算法,即使升级java版本,散列值也不会发生变化),然后根据散列值将消息分发到特定得分区上,这样可以保证同一个键会被映射到同一分区上。

只有在不改变主题分区数量的情况下,键和分区之间的映射才能保持不变。一旦分区树目发生了变化,那么旧的数据会仍然保留在原来的分区上,而新的数据则会被写到其它分区上。所以如果使用键来映射分区的化,最好在创建主题的时候就把分区规划好。

当然,用户也可以通过自定义分区器,来实现分区的规则。自定义的分区器需要实现Partitioner接口。

package kafka.partioner;

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;
import org.apache.kafka.common.record.InvalidRecordException;
import org.apache.kafka.common.utils.Utils;
/***
 * 自定义分区器
 * @author xuzf
 *
 */
public class UserPartioner implements Partitioner {
    public void configure(Map configs) { 
    }
    //返回分区编号
    public int partition(String topic, Object key, byte[] keyBytes,
            Object value, byte[] valueBytes, Cluster cluster) {
        //获取集群下某个主题的所有分区
        List partitions =cluster.partitionsForTopic(topic);
        int partionSize = partitions.size();
        //如果key为空或者key不是String类型,则抛出异常
        if((keyBytes == null) ||(!(key instanceof String))) {
            throw new InvalidRecordException("we expect all messages to have user name as key");
        }
        if(((String)key).indexOf("xuzf")!=-1) {
            return partionSize;
        }
        return Math.abs(Utils.murmur2(keyBytes)) % (partionSize-1);

    }
    public void close() {
    }

}

你可能感兴趣的:(kafka生产者)