Comprenda rápidamente el uso y el principio de los productores de Kafka

Autor | Pizca de hierba

Acabado | Yang Biyu

Producido | Pizca de hierba (ID: chaycao)

Imagen de la cabeza | Descarga de CSDN de Visual China

Este artículo aprenderá sobre el uso y los principios del productor de Kafka El número de versión de los clientes de Kafka utilizados en este artículo es 2.6.0. Ingresemos el texto a continuación, veamos cómo usar la API del productor para enviar mensajes a través de un ejemplo.

public class Producer {
    
    public static void main(String[] args) {
        // 1. 配置参数
        Properties properties = new Properties();
        properties.put("bootstrap.servers", "localhost:9092");
        properties.put("key.serializer",
                "org.apache.kafka.common.serialization.StringSerializer");
        properties.put("value.serializer",
                "org.apache.kafka.common.serialization.StringSerializer");
        // 2. 根据参数创建KafkaProducer实例(生产者)
        KafkaProducer<String, String> producer = new KafkaProducer<>(properties);
        // 3. 创建ProducerRecord实例(消息)
        ProducerRecord<String, String> record = new ProducerRecord<>("topic-demo", "hello kafka");
        // 4. 发送消息
        producer.send(record);
        // 5. 关闭生产者示例
        producer.close();
    }
    
}

Tres parámetros necesarios para la configuración

Primero cree una instancia de Propiedades y configure los tres parámetros requeridos:

  • bootstrap.servers: una lista de direcciones de intermediarios;

  • key.serializer: el serializador de la clave del mensaje;

  • value.serializer: el serializador del valor del mensaje.

Dado que el corredor espera aceptar una matriz de bytes, necesita serializar el valor de la clave en el mensaje en una matriz de bytes. Después de configurar los parámetros, cree una instancia de KafkaProducer basada en los parámetros, es decir, el productor utilizado para enviar el mensaje, luego cree la instancia de ProducerRecord del mensaje que se enviará, luego use el método de envío de KafkaProducer para  enviar el mensaje y finalmente cierre el productor.

En cuanto a KafkaProducer, recordemos dos puntos:

  • Al crear una instancia, debe especificar la configuración;

  •  El método de envío puede enviar un mensaje.

método de envío

En cuanto a la configuración, solo comprendemos estos tres parámetros obligatorios. Veamos el método de envío. Hay tres formas de enviar mensajes :

1. Enviar y olvidar (disparar y olvidar)

Al enviar un mensaje a Kafka, no le importa si el mensaje llega con normalidad, solo es responsable de la transmisión exitosa y existe la posibilidad de perder el mensaje. El ejemplo dado arriba es así.

2. Transmisión síncrona (sincronización)

El valor de retorno del método de envío es un objeto Future, que bloqueará la espera de la respuesta de Kafka al llamar a su método de obtención. como sigue:

Future<RecordMetadata> recordMetadataFuture = producer.send(record);
RecordMetadata recordMetadata = recordMetadataFuture.get();

El objeto RecordMetadata contiene algunos metadatos del mensaje, como el asunto del mensaje, el número de partición, el desplazamiento en la partición y la marca de tiempo.

3. Envío asincrónico (async)

Al llamar al método de envío, especifique la función de devolución de llamada, que se llamará cuando Kafka devuelva una respuesta. como sigue:

producer.send(record, new Callback() {
    @Override
    public void onCompletion(RecordMetadata recordMetadata, Exception e) {
        if (e != null) {
            e.printStackTrace();
        } else {
            System.out.println(recordMetadata.topic() + "-"
                               + recordMetadata.partition() + ":" + recordMetadata.offset());
        }
    }
});

onCompletion tiene dos parámetros, cuyos tipos son RecordMetadata y Exception. Cuando el mensaje se envía correctamente, recordMetadata no será nulo ye será nulo. Cuando el mensaje no se envía, ocurre lo contrario.

Objeto de mensaje ProducerRecord

A continuación reconocemos el objeto de mensaje ProducerRecord, que encapsula el mensaje enviado, y su definición es la siguiente:

public class ProducerRecord<K, V> {
    private final String topic;  // 主题
    private final Integer partition;  // 分区号
    private final Headers headers;  // 消息头部
    private final K key;  // 键
    private final V value;  // 值
    private final Long timestamp;  // 时间戳
    // ...其他构造方法和成员方法
}

El tema y el valor son obligatorios y el resto no. Por ejemplo, cuando se da el número de partición, es equivalente a especificar la partición, y cuando no se da el número de partición, si se da la clave, se puede usar para calcular el número de partición. Con respecto al encabezado del mensaje y la marca de tiempo, no hablaré de eso.

Componentes utilizados al enviar mensajes

Después de comprender el objeto productor KafkaProducer y el objeto de mensaje ProducerRecord, veamos los siguientes componentes al usar el productor para enviar mensajes, el interceptor, el serializador y el particionador del productor. Su estructura (partes) es la siguiente:

1. Producer Interceptor: la  interfaz ProducerInterceptor se utiliza principalmente para realizar algunos trabajos preparatorios antes de que se envíe el mensaje, como filtrar el mensaje o modificar el contenido del mensaje. También se puede utilizar para realizar algunos requisitos personalizados antes de enviar la lógica de devolución de llamada, por ejemplo Trabajo estadístico.

2. Serializador , interfaz de serializador, que se utiliza para convertir datos en matrices de bytes.

3. Particionador, interfaz del particionador, si no se especifica el número de partición y se proporciona la clave.

Proceso

Echemos un vistazo al proceso de procesamiento en combinación con el código para profundizar la impresión.

public Future<RecordMetadata> send(ProducerRecord<K, V> record, Callback callback) {
    // 拦截器,拦截消息进行处理
    ProducerRecord<K, V> interceptedRecord = this.interceptors.onSend(record);
    return doSend(interceptedRecord, callback);
}

El anterior es el método de envío de KafkaProducer. Primero, el mensaje se pasa al método onSend del interceptor y luego se ingresa el método doSend. Entre ellos, el método doSend es más largo, pero el contenido no es complicado.Los pasos principales se explican a continuación.

private Future<RecordMetadata> doSend(ProducerRecord<K, V> record, Callback callback) {
    TopicPartition tp = null;
    try {
        throwIfProducerClosed();
        // 1.确认数据发送到的topic的metadata可用
        long nowMs = time.milliseconds();
        ClusterAndWaitTime clusterAndWaitTime;
        try {
            clusterAndWaitTime = waitOnMetadata(record.topic(), record.partition(), nowMs, maxBlockTimeMs);
        } catch (KafkaException e) {
            if (metadata.isClosed())
                throw new KafkaException("Producer closed while send in progress", e);
            throw e;
        }
        nowMs += clusterAndWaitTime.waitedOnMetadataMs;
        long remainingWaitMs = Math.max(0, maxBlockTimeMs - clusterAndWaitTime.waitedOnMetadataMs);
        Cluster cluster = clusterAndWaitTime.cluster;
        // 2.序列化器,序列化消息的key和value
        byte[] serializedKey;
        try {
            serializedKey = keySerializer.serialize(record.topic(), record.headers(), record.key());
        } catch (ClassCastException cce) {
            throw new SerializationException("Can't convert key of class " + record.key().getClass().getName() +
                                             " to class " + producerConfig.getClass(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG).getName() +
                                             " specified in key.serializer", cce);
        }
        byte[] serializedValue;
        try {
            serializedValue = valueSerializer.serialize(record.topic(), record.headers(), record.value());
        } catch (ClassCastException cce) {
            throw new SerializationException("Can't convert value of class " + record.value().getClass().getName() +
                                             " to class " + producerConfig.getClass(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG).getName() +
                                             " specified in value.serializer", cce);
        }
        // 3.分区器,获取或计算分区号
        int partition = partition(record, serializedKey, serializedValue, cluster);
        tp = new TopicPartition(record.topic(), partition);

        setReadOnly(record.headers());
        Header[] headers = record.headers().toArray();

        int serializedSize = AbstractRecords.estimateSizeInBytesUpperBound(apiVersions.maxUsableProduceMagic(),
                                                                           compressionType, serializedKey, serializedValue, headers);
        ensureValidRecordSize(serializedSize);
        long timestamp = record.timestamp() == null ? nowMs : record.timestamp();
        if (log.isTraceEnabled()) {
            log.trace("Attempting to append record {} with callback {} to topic {} partition {}", record, callback, record.topic(), partition);
        }
        Callback interceptCallback = new InterceptorCallback<>(callback, this.interceptors, tp);

        if (transactionManager != null && transactionManager.isTransactional()) {
            transactionManager.failIfNotReadyForSend();
        }
        // 4.消息累加器,缓存消息
        RecordAccumulator.RecordAppendResult result = accumulator.append(tp, timestamp, serializedKey,
                                                                         serializedValue, headers, interceptCallback, remainingWaitMs, true, nowMs);

        if (result.abortForNewBatch) {
            int prevPartition = partition;
            partitioner.onNewBatch(record.topic(), cluster, prevPartition);
            partition = partition(record, serializedKey, serializedValue, cluster);
            tp = new TopicPartition(record.topic(), partition);
            if (log.isTraceEnabled()) {
                log.trace("Retrying append due to new batch creation for topic {} partition {}. The old partition was {}", record.topic(), partition, prevPartition);
            }
            // producer callback will make sure to call both 'callback' and interceptor callback
            interceptCallback = new InterceptorCallback<>(callback, this.interceptors, tp);

            result = accumulator.append(tp, timestamp, serializedKey,
                                        serializedValue, headers, interceptCallback, remainingWaitMs, false, nowMs);
        }

        if (transactionManager != null && transactionManager.isTransactional())
            transactionManager.maybeAddPartitionToTransaction(tp);

        // 5.如果batch满了或者消息大小超过了batch的剩余空间需要创建新的batch
        // 将唤醒sender线程发送消息
        if (result.batchIsFull || result.newBatchCreated) {
            log.trace("Waking up the sender since topic {} partition {} is either full or getting a new batch", record.topic(), partition);
            this.sender.wakeup();
        }
        return result.future;
    } catch (ApiException e) {
        log.debug("Exception occurred during message send:", e);
        if (callback != null)
            callback.onCompletion(null, e);
        this.errors.record();
        this.interceptors.onSendError(record, tp, e);
        return new FutureFailure(e);
    } catch (InterruptedException e) {
        this.errors.record();
        this.interceptors.onSendError(record, tp, e);
        throw new InterruptException(e);
    } catch (KafkaException e) {
        this.errors.record();
        this.interceptors.onSendError(record, tp, e);
        throw e;
    } catch (Exception e) {
        this.interceptors.onSendError(record, tp, e);
        throw e;
    }
}

método doSend

El método doSend  se divide principalmente en 5 pasos :

  • Antes de enviar datos, confirme que los metadatos del tema al que se envían los datos están disponibles (el líder de la partición está disponible, si el control de permisos está activado, el cliente también debe tener los permisos correspondientes);

  • Serializador, serializa la clave y el valor del mensaje;

  • Particionador, obtenga o calcule el número de partición;

  • Acumulador de mensajes, mensajes de búfer;

  • En el acumulador de mensajes, los mensajes se colocarán en un lote para el envío por lotes. Cuando el lote esté lleno o el tamaño del mensaje exceda el espacio restante del lote y sea necesario crear un nuevo lote, el hilo del remitente se activará para enviar el mensaje.

Este artículo no entrará en más detalles sobre metadatos, el serializador y el particionador también se presentan en el artículo anterior. A continuación, analizamos principalmente el acumulador de mensajes.

Acumulador de mensajes

El acumulador de mensajes se utiliza para almacenar mensajes en búfer para enviar mensajes en lotes. Utilice una variable de mapa de lotes ConcurrentMap <TopicPartition, Deque <ProducerBatch >> para guardar el mensaje en RecordAccumulator. La TopicPartition como clave encapsula el tema y el número de partición, y el valor correspondiente es la cola de dos extremos de ProducerBatch, es decir, los mensajes enviados a la misma partición se almacenan en caché en ProducerBatch. Al enviar un mensaje, el Registro se agregará al final de la cola, es decir, se agregará al ProducerBatch al final. Si el espacio del ProducerBatch es insuficiente o la cola está vacía, se creará un nuevo ProducerBatch y luego se agregará. Cuando el ProducerBatch está lleno o se crea un nuevo ProducerBatch, el hilo del remitente se activará para obtener el ProducerBatch del encabezado de la cola y enviarlo.

RecordAccumulator

En el hilo del remitente  , el ProducerBatch que se enviará se convertirá en la forma de <Integer, List <ProducerBatch >>, agrupado por el ID del nodo Kafka, y luego el ProducerBatch del mismo nodo se envía en una sola solicitud.

El contenido del productor de Kafak primero entiende esto, lo siguiente es una breve revisión del contenido de este artículo a través de un mapa mental:

referencia

  • "Comprensión profunda del diseño básico y los principios prácticos de Kafka"

  • La guía definitiva de Kafka

  • Modelo de envío del productor del análisis del código fuente de Kafka (1): http://matt33.com/2017/06/25/kafka-producer-send-module/


更多精彩推荐
☞谷歌软件工程师薪资百万,大厂薪资有多高?
☞CSDN 创始人蒋涛:选择长沙作“大本营”,打造开发者中心城市
☞杜甫在线演唱《奇迹再现》、兵马俑真人还原……用AI技术打破次元壁的大谷来参加腾讯全球数字生态大会啦!
☞开放源码,华为鸿蒙HarmonyOS 2.0来了
☞20张图,带你搞懂高并发中的线程与线程池!
☞跨链,该怎么跨?
点分享点点赞点在看

Supongo que te gusta

Origin blog.csdn.net/csdnnews/article/details/108570939
Recomendado
Clasificación