由于域名解析异常导致的Kafka消息发送失败但无异常抛出

本文介绍了一次Kafka跨网段消息发送失败的排查经历,详细分析了Kafka异步发送机制及回调接口的作用,并最终定位到DNS解析问题导致的消息发送失败。

背景

由于业务需要,最近部署了一个跨网段的服务,由一个网段中的应用服务器向另一个网段中的Kafka集群写入消息,应用服务器与Kafka集群之间已经开通网络,telnet结果显示相应端口之间连接正常。

初次上线之后,发现如下方法执行正常,未出现超时阻塞运行现象,但Kafka消费者无法接收发送的消息,怀疑消息并未真正写入Kafka集群中。

producer.send(record)

源码追踪

实际上,我们追踪源码可以发现,Kafka客户端发送时,为了提高吞吐量采用了batch异步发送机制,在真实发送消息时遵循如下的流程:

1.将待发消息写入本进程内存中,类似WAL的形式

2.在待发消息达到一定数量或者超过linger.ms之后,通过Sender调用RecordBatch进行批量发送,同时记录异常信息

由于发送过程是异步的,所以在发送时不会抛出异常,误以为消息已经正常发送。

事实上,作为异步发送方式的配套,Kafka提供了回调接口以供客户端查看消息发送状态,函数原型如下:

    @Override
    public Future<RecordMetadata> send(ProducerRecord<K, V> record, Callback callback) {
        // intercept the record, which can be potentially modified; this method does not throw exceptions
        ProducerRecord<K, V> interceptedRecord = this.interceptors == null ? record : this.interceptors.onSend(record);
        return doSend(interceptedRecord, callback);
    }

再来看Callback接口

public interface Callback {

    /**
     * A callback method the user can implement to provide asynchronous handling of request completion. This method will
     * be called when the record sent to the server has been acknowledged. Exactly one of the arguments will be
     * non-null.
     * @param metadata The metadata for the record that was sent (i.e. the partition and offset). Null if an error
     *        occurred.
     * @param exception The exception thrown during processing of this record. Null if no error occurred.
     *                  Possible thrown exceptions include:
     *
     *                  Non-Retriable exceptions (fatal, the message will never be sent):
     *
     *                  InvalidTopicException
     *                  OffsetMetadataTooLargeException
     *                  RecordBatchTooLargeException
     *                  RecordTooLargeException
     *                  UnknownServerException
     *
     *                  Retriable exceptions (transient, may be covered by increasing #.retries):
     *
     *                  CorruptRecordException
     *                  InvalidMetadataException
     *                  NotEnoughReplicasAfterAppendException
     *                  NotEnoughReplicasException
     *                  OffsetOutOfRangeException
     *                  TimeoutException
     *                  UnknownTopicOrPartitionException
     */
    public void onCompletion(RecordMetadata metadata, Exception exception);
}

调试异常

抛开冗长的注释,我们可以看到,如果发送过程中存在异常,可以通过回调接口获取异常。

                producer.send(record, new Callback() {
                    @Override
                    public void onCompletion(RecordMetadata metadata, Exception exception) {
                        if (exception != null) {
                            logger.error("Caught exception ", exception);
                        }
                    }
                });

将应用再次上线后,可以发现,应用大量抛出异常。异常内容为“Expiring xxx records for xxxx due to x ms has passed since batch creation plus linger time”。

由此可见,消息并未被真实发送至Kafka broker,而是在后台被静默抛弃了。在网络已经开通的情况下却无法发送消息,需要更进一步挖掘原因。

深入挖掘

我们将应用的日志调至DEBUG,观察从应用启动开始的日志,可以发现,KafkaProducer在初始化时,尝试从指定的brokers中获取meta信息,其中包括:topic信息、partition信息、broker节点信息、topic/partition与节点的对应关系。

观察返回的meta信息可以发现,broker节点信息的返回值为主机域名,这就是问题所在。

由此,我们知道了原因所在:

1.KafkaProducer初始化时尝试从指定的ip获取meta information

2.依据获取到的meta info向指定broker发送消息

由于两个网段间的DNS信息不同步,导致应用所在网段无法解析broker的域名,从而发送消息失败,在DNS无法修改的情况下,在应用节点的/etc/hosts文件中加入broker节点的域名和ip映射,问题解决。

后续

Kafka对外提供的meta info中的节点地址信息是直接读取zookeeper的注册信息,该注册信息是在集群启动时写入的,信息的最终来源为server.properties。依据Kafka官方文档http://kafka.apache.org/0101/documentation.html#brokerconfigs的描述,配置的优先级如下:

1.首先读取advertised.listeners的值

2.若1中值为空,使用advertised.host.name & advertised.port注册到zookeeper

KafkaProducer的发送模型在0.9版本之后默认为异步发送模式(提高吞吐量),如果想要使用同步模式,可以在获取到send(ProducerRecord<K, V> producerRecord)方法的future返回之后,使用future.get()方法阻塞等待,如下:

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

对于中间件的运行机制和细节需要更深入的了解,才能快速定位问题。

 

<think>我们需要在解密过程中捕获异常,当解密失败时将原始加密数据(或包含其他上下文信息)发送到指定的Kafka主题。 步骤如下: 1. 创建Kafka生产者,用于在异常时将数据发送Kafka。 2. 在捕获异常后,将原始数据(以及可能的异常信息)发送到指定的Kafka主题。 3. 注意资源管理,如Kafka生产者的关闭。 4. 考虑性能:Kafka生产者应该被重用,而不是每次异常都创建新的。 优化方案: 我们可以将Kafka生产者作为静态变量,在类加载时初始化,并在程序结束时关闭。 但需要注意:在Flink等环境中,需要考虑上下文环境,这里我们假设是独立应用。 修改后的代码结构: - 在SecureDecryptor类中增加Kafka生产者相关配置。 - 在静态初始化块中初始化Kafka生产者(或者使用懒加载)。 - 在解密异常时,将原始数据发送Kafka。 - 添加一个关闭钩子,在JVM关闭时关闭Kafka生产者。 注意:为了简化,我们假设Kafka配置是固定的。实际应用中,可能需要从配置文件中读取。 代码实现:</think>以下是增加异常数据发送Kafka 功能的完整实现方案。该方案在解密失败时将原始加密数据、错误信息和时间戳发送到指定的 Kafka 主题,便于后续分析和处理: ### 完整实现代码 ```java import org.apache.kafka.clients.producer.*; import org.apache.kafka.common.serialization.StringSerializer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Properties; import java.util.Date; import java.text.SimpleDateFormat; public class SecureDecryptorWithKafka { private static final Logger logger = LoggerFactory.getLogger(SecureDecryptorWithKafka.class); private static final String SECRET_KEY = "2A00000000000000"; // 16字节密钥 private static final String ALGORITHM = "SM4/ECB/PKCS5Padding"; // Kafka 配置 private static final String KAFKA_BOOTSTRAP_SERVERS = "kafka-server:9092"; private static final String ERROR_TOPIC = "decryption_errors"; private static KafkaProducer<String, String> kafkaProducer; static { // 初始化安全提供者 Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider()); // 初始化 Kafka 生产者 initKafkaProducer(); } private static void initKafkaProducer() { Properties props = new Properties(); props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, KAFKA_BOOTSTRAP_SERVERS); props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class.getName()); props.put(ProducerConfig.ACKS_CONFIG, "all"); // 确保消息可靠传输 props.put(ProducerConfig.RETRIES_CONFIG, 3); // 失败重试次数 kafkaProducer = new KafkaProducer<>(props); // 注册JVM关闭钩子,确保生产者关闭 Runtime.getRuntime().addShutdownHook(new Thread(() -> { logger.info("Closing Kafka producer"); kafkaProducer.close(); })); } public static String eval(String encryptedInput) throws DecryptionException { if (encryptedInput == null || encryptedInput.trim().isEmpty()) { logger.warn("Invalid input: null or empty string"); return ""; } try { // 密钥处理 byte[] keyBytes = SECRET_KEY.getBytes(); if (keyBytes.length != 16) { throw new IllegalArgumentException("Invalid key length: " + keyBytes.length + " bytes"); } // SM4 解密 SecretKeySpec secretKey = new SecretKeySpec(keyBytes, "SM4"); Cipher cipher = Cipher.getInstance(ALGORITHM, "BC"); cipher.init(Cipher.DECRYPT_MODE, secretKey); byte[] encryptedBytes = Base64.getDecoder().decode(encryptedInput); byte[] decryptedBytes = cipher.doFinal(encryptedBytes); return new String(decryptedBytes); } catch (Exception e) { // 记录原始错误 logger.error("Decryption failed for input: {}", encryptedInput, e); // 发送错误信息到 Kafka sendToErrorTopic(encryptedInput, e); throw new DecryptionException("SM4 decryption error", e); } } private static void sendToErrorTopic(String encryptedInput, Exception exception) { try { // 构建错误消息 String errorMessage = buildErrorMessage(encryptedInput, exception); // 创建 Kafka 记录 ProducerRecord<String, String> record = new ProducerRecord<>( ERROR_TOPIC, null, // 分区键 errorMessage ); // 异步发送 kafkaProducer.send(record, (metadata, exception) -> { if (exception != null) { logger.error("Failed to send error message to Kafka", exception); } else { logger.info("Error message sent to topic {}, partition {}, offset {}", metadata.topic(), metadata.partition(), metadata.offset()); } }); } catch (Exception kafkaEx) { logger.error("Failed to send error to Kafka topic", kafkaEx); } } private static String buildErrorMessage(String encryptedInput, Exception exception) { // 使用 JSON 格式构建错误消息 SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS"); String timestamp = sdf.format(new Date()); return String.format( "{\"timestamp\": \"%s\", " + "\"encrypted_data\": \"%s\", " + "\"error_type\": \"%s\", " + "\"error_message\": \"%s\"}", timestamp, encryptedInput, exception.getClass().getSimpleName(), exception.getMessage().replace("\"", "'") // 转义双引号 ); } } // 自定义异常类保持不变 class DecryptionException extends Exception { public DecryptionException(String message, Throwable cause) { super(message, cause); } } ``` ### 关键功能说明 1. **Kafka 生产者初始化** - 在静态初始化块中创建 Kafka 生产者 - 配置高可靠性参数(ACKS=all, RETRIES=3) - 注册 JVM 关闭钩子确保资源释放 2. **错误消息构建** - 使用 JSON 格式包含完整错误上下文 - 包含时间戳、加密数据、错误类型和错误消息 - 对特殊字符进行转义处理 3. **异常处理流程** ```java catch (Exception e) { // 1. 记录原始错误日志 logger.error("Decryption failed for input: {}", encryptedInput, e); // 2. 发送错误到 Kafka sendToErrorTopic(encryptedInput, e); // 3. 抛出业务异常 throw new DecryptionException("SM4 decryption error", e); } ``` 4. **异步发送机制** - 使用 `kafkaProducer.send()` 的异步回调 - 成功/失败时记录相应日志 - 避免阻塞主线程 ### 错误消息示例 发送Kafka 的错误消息格式如下: ```json { "timestamp": "2023-11-15 14:30:45.123", "encrypted_data": "U2FsdGVkX1+ABcdefGHIjklmNOPQRSTUVWXYZ", "error_type": "IllegalBlockSizeException", "error_message": "Input length must be multiple of 16 when decrypting with padded cipher" } ``` ### 消费者处理建议 在 Kafka 消费者,可以使用以下代码处理错误消息: ```java import org.apache.kafka.clients.consumer.*; import java.util.Collections; import java.util.Properties; public class ErrorConsumer { public static void main(String[] args) { Properties props = new Properties(); props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, "kafka-server:9092"); props.put(ConsumerConfig.GROUP_ID_CONFIG, "error-handler-group"); props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringDeserializer"); props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, "org.apache.kafka.common.serialization.StringDeserializer"); try (KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props)) { consumer.subscribe(Collections.singletonList("decryption_errors")); while (true) { ConsumerRecords<String, String> records = consumer.poll(1000); for (ConsumerRecord<String, String> record : records) { System.out.println("Received error: " + record.value()); // 这里可以添加错误处理逻辑,如: // 1. 存储到数据库进行后续分析 // 2. 发送告警通知 // 3. 尝试自动修复或重试 } } } } } ``` ### 架构优势 1. **解耦设计**:解密逻辑与错误处理分离 2. **可靠性**:Kafka 保证消息至少投递一次 3. **可追溯性**:完整保留原始加密数据和错误上下文 4. **可扩展性**:可通过增加消费者实现: - 实时告警 - 错误统计分析 - 自动修复尝试 5. **性能优化**:异步发送不阻塞主流程 ### 安全注意事项 1. 对 Kafka 主题设置访问控制(ACL) 2. 敏感数据加密传输(启用 Kafka SSL/TLS) 3. 错误消息中避免包含超长数据(可截断) 4. 设置消息保留策略(如保留7天)
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值