Kafka生产者介绍(十一):NetWorkClient

本文详细解析了Kafka中NetworkClient的工作原理,包括其如何管理网络I/O,处理请求和响应,以及核心方法ready()的运作流程。深入探讨了NetworkClient如何检测节点是否准备接收数据,以及在不同状态下如何发起连接。

NetworkClient是kafka的网络层,也就是真正发生网络I/O的地方,是一个通用的网络客户端实现,不只用于生产者消息的发送,也用于消费者消费消息以及服务端Broker之间的通信。

public class NetworkClient implements KafkaClient {
    /* 执行网路io的selector */
    private final Selectable selector;
    // 管理metadata更新操作
    private final MetadataUpdater metadataUpdater;

    private final Random randOffset;

    /* 每个节点连接的状态 */
    private final ClusterConnectionStates connectionStates;

    /* 发送但是还没有收到响应的消息 */
    private final InFlightRequests inFlightRequests;

    /* 发送buffer的socket */
    private final int socketSendBuffer;

    /* 接受byteBuffer的socket */
    private final int socketReceiveBuffer;

    /* clientID */
    private final String clientId;

    /* 关联id */
    private int correlation;

    /* 发送等待时间*/
    private final int requestTimeoutMs;

    private final Time time;
}

NetWorkClient中所有连接的状态由ClusterConnectionStates管理,它底层使用Map<String, NodeConnectionState>实现,NodeConnectionState枚举状态连接,为DISCONNECTED, CONNECTING, CONNECTED。
NetWorkClient的核心方法是ready(),检测Node是否准备好接受数据,首先通过isReady方法检测是否可以向一个Node发送请求,符合以下三个条件:
1. metadata不处于正在更新或者需要更新的状态,!metadataUpdater.isUpdateDue(now)
2. 成功建立连接并且连接正常:connectionState.isConnect(node)
3. canSendRequest返回true:return connectionStates.isConnected(node) && selector.isChannelReady(node) && inFlightRequests.canSendMore(node);
如果isReady返回false,则当满足以下两个条件,就会调用initiateConnect发起连接:
1. 连接是DISCONNECTED状态。
2. 两次重试的时间必须大于reconnectBackoffMs。

public class NetworkClient implements KafkaClient {
    public boolean ready(Node node, long now) {
        if (node.isEmpty())
            throw new IllegalArgumentException("Cannot connect to empty node " + node);

        if (isReady(node, now))
            return true;
        
        if (connectionStates.canConnect(node.idString(), now))
            // if we are interested in sending to a node and we don't have a connection to it, initiate one
            initiateConnect(node, now);

        return false;
    }
	public boolean isReady(Node node, long now) {
        // if we need to update our metadata now declare all requests unready to make metadata requests first
        // priority
		// metadata不处于正在更新或者需要更新的状态
        return !metadataUpdater.isUpdateDue(now) && canSendRequest(node.idString());
    }
	
	private boolean canSendRequest(String node) {
        return connectionStates.isConnected(node) && selector.isChannelReady(node) && inFlightRequests.canSendMore(node);
    }
	
	    private void initiateConnect(Node node, long now) {
        String nodeConnectionId = node.idString();
        try {
            log.debug("Initiating connection to node {} at {}:{}.", node.id(), node.host(), node.port());
            this.connectionStates.connecting(nodeConnectionId, now);
            selector.connect(nodeConnectionId,
                             new InetSocketAddress(node.host(), node.port()),
                             this.socketSendBuffer,
                             this.socketReceiveBuffer);
        } catch (IOException e) {
            /* attempt failed, we'll try again after the backoff */
            connectionStates.disconnected(nodeConnectionId, now);
            /* maybe the problem is our metadata, update it */
            metadataUpdater.requestUpdate();
            log.debug("Error connecting to node {} at {}:{}:", node.id(), node.host(), node.port(), e);
        }
    }
}

NetworkClient.send()方法吧请求放在KafkaChannel.send字段,放在InFlightRequests中等待。
 

public class NetworkClient implements KafkaClient {
    public void send(ClientRequest request, long now) {
        String nodeId = request.request().destination();
		//按照以上条件检测node是否可以发送请求
        if (!canSendRequest(nodeId))
            throw new IllegalStateException("Attempt to send a request to node " + nodeId + " which is not ready.");
        doSend(request, now);
    }

    private void doSend(ClientRequest request, long now) {
        request.setSendTimeMs(now);
		//放在InFlightRequests中等待
        this.inFlightRequests.add(request);
		//selector.send写入channel中
        selector.send(request.request());
    }
}

NetworkClient.poll()方法调用KSelecter.poll()进行网络I/O,并调用handle*()方法对poll回来的各种数据和队列进行处理:

public class NetworkClient implements KafkaClient {
    public List<ClientResponse> poll(long timeout, long now) {
		//更新metadata
        long metadataTimeout = metadataUpdater.maybeUpdate(now);
        try {
            this.selector.poll(Utils.min(timeout, metadataTimeout, requestTimeoutMs));
        } catch (IOException e) {
            log.error("Unexpected error during I/O", e);
        }

        // process completed actions
        long updatedNow = this.time.milliseconds();
        List<ClientResponse> responses = new ArrayList<>();
		//处理各种情况,生成响应,添加到列表中
		//完成发送的handler(不需要 response 的 request,如 send)
        handleCompletedSends(responses, updatedNow);
		//完成接收的handler(如 Metadata 请求)
        handleCompletedReceives(responses, updatedNow);
		//断开连接的handler
        handleDisconnections(responses, updatedNow);
		//处理连接的handler
        handleConnections();
		//超时请求的handler
        handleTimedOutRequests(responses, updatedNow);

        // 调用 callbacks
        for (ClientResponse response : responses) {
            if (response.request().hasCallback()) {
                try {
                    response.request().callback().onComplete(response);
                } catch (Exception e) {
                    log.error("Uncaught error in request completion:", e);
                }
            }
        }

        return responses;
    }
}

下面看各种handle方法的处理:
handleCompletedSends处理已经完成的队列,如果发现不需要响应的请求,就从inFlightRequests中删除,向response中添加一个ClientResponse。
客户端发送请求后,handleCompletedSends中对于有响应的请求,并不会将ClientRequest从inFlightRequests中移除。(因为没收到响应),除非是客户端请求不需要响应,则这时候是可以将ClientRequest从中删除,添加时放到头部,删除时也是从头部删除。
 

public class NetworkClient implements KafkaClient {
    private void handleCompletedSends(List<ClientResponse> responses, long now) {
        // 遍历completeSend集合
        for (Send send : this.selector.completedSends()) {
			//获取inFlightRequests队列的第一个元素
            ClientRequest request = this.inFlightRequests.lastSent(send.destination());
			//检测请求是否需要响应
            if (!request.expectResponse()) {
				//把第一个请求删除
                this.inFlightRequests.completeLastSent(send.destination());
				//生成ClientResponse对象,添加到response集合中。
                responses.add(new ClientResponse(request, now, false, null));
            }
			// 如果客户端请求需要有响应, 那么它的响应是在下面的handleCompletedReceives中设置的
        }
    }
}

handleCompletedReceives遍历completedReceives队列,

public class NetworkClient implements KafkaClient {
    private void handleCompletedReceives(List<ClientResponse> responses, long now) {
		//遍历响应,通过Selector返回未处理的响应
        for (NetworkReceive receive : this.selector.completedReceives()) {
			//获得返回响应的nodeId
            String source = receive.source();
			//从缓存队列获取已发送请求并删除(这里会从inFlightRequests删除,因为inFlightRequests存的是未收到请求的ClientRequest,现在这个请求已经有响应了,就不需要再其中保存了。)
            ClientRequest req = inFlightRequests.completeNext(source);
			//解析响应,并且验证响应头,生成Struct实例
            Struct body = parseResponse(receive.payload(), req.request().header());
			//调用maybeHandleCompletedReceive处理MetadataResponse。
            if (!metadataUpdater.maybeHandleCompletedReceive(req, now, body))
				//如果不是MetadataResponse,则直接创建Clientsresponses添加到Responses中
                responses.add(new ClientResponse(req, now, false, body));
        }
    }
}

handleDisconnections处理disconnect列表

public class NetworkClient implements KafkaClient {
    private void handleDisconnections(List<ClientResponse> responses, long now) {
		//更新连接状态,清理掉inFlightRequests中断开连接的node对应的ClientRequest
        for (String node : this.selector.disconnected()) {
            log.debug("Node {} disconnected.", node);
            processDisconnection(responses, node, now);
        }
        // we got a disconnect so we should probably refresh our metadata and see if that broker is dead
        if (this.selector.disconnected().size() > 0)
            metadataUpdater.requestUpdate();
    }
	
	private void processDisconnection(List<ClientResponse> responses, String nodeId, long now) {
        //更新连接状态为disconnect
		connectionStates.disconnected(nodeId, now);
        for (ClientRequest request : this.inFlightRequests.clearAll(nodeId)) {
            log.trace("Cancelled request {} due to node {} being disconnected", request, nodeId);
			//看是否是metadata请求。
            if (!metadataUpdater.maybeHandleDisconnection(request))
				//如果不是MetadataResponse,则直接创建Clientsresponses添加到Responses中
                responses.add(new ClientResponse(request, now, true, null));
        }
    }
}

handleConnections把状态设置为Connect,handleTimedOutRequests与handleDisconnections大致相同,调用this.selector.close(nodeId)。
在经过一系列的handle之后,poll方法中产生的全部ClientResponse已经都在responses中了,之后,遍历response调用每个ClientRequest中的回调,如果是异常响应则请求重发,正常就调用callback:
response.request().callback().onComplete(response);最终会调用创建PproduceRequst时候的注册handleProduceResponse方法:
 

public class NetworkClient implements KafkaClient {
    private void handleProduceResponse(ClientResponse response, Map<TopicPartition, RecordBatch> batches, long now) {
        int correlationId = response.request().request().header().correlationId();
		//对于连接断开而产生的ClientResponse,则重新发送,如果不能则低啊用回调。
        if (response.wasDisconnected()) {
            log.trace("Cancelled request {} due to node {} being disconnected", response, response.request()
                                                                                                  .request()
                                                                                                  .destination());
            for (RecordBatch batch : batches.values())
                completeBatch(batch, Errors.NETWORK_EXCEPTION, -1L, Record.NO_TIMESTAMP, correlationId, now);
        } else {
            log.trace("Received produce response from node {} with correlation id {}",
                      response.request().request().destination(),
                      correlationId);
            // if we have a response, parse it
            if (response.hasResponse()) {
                ProduceResponse produceResponse = new ProduceResponse(response.responseBody());
                for (Map.Entry<TopicPartition, ProduceResponse.PartitionResponse> entry : produceResponse.responses().entrySet()) {
                    TopicPartition tp = entry.getKey();
                    ProduceResponse.PartitionResponse partResp = entry.getValue();
                    Errors error = Errors.forCode(partResp.errorCode);
                    RecordBatch batch = batches.get(tp);
					//调用completeBatch处理
                    completeBatch(batch, error, partResp.baseOffset, partResp.timestamp, correlationId, now);
                }
                this.sensors.recordLatency(response.request().request().destination(), response.requestLatencyMs());
                this.sensors.recordThrottleTime(response.request().request().destination(),
                                                produceResponse.getThrottleTime());
            } else {
                // 不需要响应,则直接调用completeBatch
                for (RecordBatch batch : batches.values())
                    completeBatch(batch, Errors.NONE, -1L, Record.NO_TIMESTAMP, correlationId, now);
            }
        }
    }
	
	private void completeBatch(RecordBatch batch, Errors error, long baseOffset, long timestamp, long correlationId, long now) {
        if (error != Errors.NONE && canRetry(batch, error)) {
            // retry
            log.warn("Got error produce response with correlation id {} on topic-partition {}, retrying ({} attempts left). Error: {}",
                     correlationId,
                     batch.topicPartition,
                     this.retries - batch.attempts - 1,
                     error);
			//对于可以重试的RecordBatch,重新添加到accumulator中,等待发送
            this.accumulator.reenqueue(batch, now);
            this.sensors.recordRetries(batch.topicPartition.topic(), batch.recordCount);
        } else {
			//不能重试的,就标记为异常完成,释放accumulator
            RuntimeException exception;
            if (error == Errors.TOPIC_AUTHORIZATION_FAILED)
                exception = new TopicAuthorizationException(batch.topicPartition.topic());
            else
                exception = error.exception();
            //done调用callback函数
            batch.done(baseOffset, timestamp, exception);
			//释放accumulator空间
            this.accumulator.deallocate(batch);
            if (error != Errors.NONE)
                this.sensors.recordErrors(batch.topicPartition.topic(), batch.recordCount);
        }
		//如果exception是因为错误的metadata,如leader已经改变,则标记更新metadata。
        if (error.exception() instanceof InvalidMetadataException)
            metadata.requestUpdate();
        // Unmute the completed partition.
        if (guaranteeMessageOrder)
            this.accumulator.unmutePartition(batch.topicPartition);
    }
	
}

 

<think> 我们正在处理的是Kafka生产者客户端(clientId=producer-1)与节点1004(地址为localhost/127.0.0.1:9092)断开连接的问题。根据之前的上下文,我们已经知道节点1004在本地主机上,因此问题可能出在本地Kafka环境。下面将针对此问题提供详细的解决方案。 ### 问题原因分析 节点断开连接可能有以下原因: 1. **Broker节点1004未运行**:Kafka broker进程可能已经停止。 2. **端口冲突或配置错误**:Broker可能没有在127.0.0.1:9092上监听。 3. **资源限制**:如文件描述符不足、内存溢出等。 4. **网络配置问题**:本地回环接口(lo)可能被禁用或防火墙阻止了9092端口。 5. **生产者配置问题**:如连接超时设置过短、重试机制未正确配置等。 ### 解决方案 #### 1. 检查Broker节点1004状态 首先确认节点1004是否正在运行: ```bash # 查看Kafka进程 ps aux | grep kafka ``` 如果未运行,启动它: ```bash # 进入Kafka目录,启动Broker(假设使用默认配置文件) bin/kafka-server-start.sh config/server.properties ``` #### 2. 验证Broker监听地址 检查Broker配置文件(server.properties)中是否正确配置了监听地址: ```properties listeners=PLAINTEXT://127.0.0.1:9092 advertised.listeners=PLAINTEXT://127.0.0.1:9092 ``` #### 3. 检查端口监听 使用以下命令验证Broker是否在9092端口监听: ```bash netstat -tuln | grep 9092 ``` 如果没有输出,说明Broker未正确监听端口。 #### 4. 检查资源限制 - **文件描述符限制**: ```bash # 查看当前限制 ulimit -n # 如果小于10000,建议修改(临时) ulimit -n 100000 ``` 永久修改:编辑`/etc/security/limits.conf`,添加: ``` * soft nofile 100000 * hard nofile 100000 ``` - **内存检查**: 查看Kafka日志(默认为`logs/server.log`),检查是否有`OutOfMemoryError`。 #### 5. 生产者配置优化 在生产者客户端添加以下配置以增强连接稳定性: ```java properties.put("retries", 10); // 重试次数 properties.put("retry.backoff.ms", 1000); // 重试间隔 properties.put("reconnect.backoff.max.ms", 10000); // 最大重连间隔 ``` #### 6. 防火墙检查 虽然本地连接通常不受防火墙影响,但仍需确认: ```bash # 查看防火墙规则(如果使用firewalld) sudo firewall-cmd --list-ports # 确保9092端口开放 sudo firewall-cmd --add-port=9092/tcp --permanent sudo firewall-cmd --reload ``` #### 7. 测试连接 使用`telnet`测试Broker的连通性: ```bash telnet 127.0.0.1 9092 ``` 如果连接失败,说明Broker未启动或配置错误。 ### 紧急恢复 若需快速恢复生产者的消息发送,可临时移除故障节点(但注意:如果整个集群只有一个节点,则不可行): ```java // 在生产者配置中移除节点1004(如果有多节点) properties.put("bootstrap.servers", "其他健康节点:9092"); ``` ### 监控与预防 - **启用JMX监控**: 启动Broker时添加JMX参数: ```bash export JMX_PORT=9999 bin/kafka-server-start.sh config/server.properties ``` 监控关键指标:`kafka.server:type=BrokerTopicMetrics,name=FailedProduceRequestsPerSec` - **日志监控**: 定期检查Broker日志(`logs/server.log`),搜索WARN/ERROR级别的日志。 ### 注意事项 - 在单节点环境下,节点1004即整个集群,必须确保其稳定运行。 - 生产者断开连接可能导致消息发送失败,建议配置`acks=all`和`enable.idempotence=true`以最大限度保证数据可靠性[^1]。 --- ### 相关问题 1. Kafka单节点环境下如何实现高可用? 2. Kafka生产者如何配置才能避免因节点故障导致的消息丢失? 3. 如何监控Kafka Broker的资源使用情况? 4. Kafka Broker启动失败有哪些常见原因? 5. 如何测试Kafka生产者和Broker之间的网络延迟? [^1]: Kafka官方文档 - 生产者配置: https://kafka.apache.org/documentation/#producerconfigs </think> 针对Kafka生产者客户端提示"Node 1004 disconnected"(节点断开连接)的问题,以下是系统化的排查和解决方案,重点关注localhost/127.0.0.1:9092本地环境的处理: ### 根本原因分析 1. **Broker进程异常** - Kafka broker未运行或意外终止 - JVM崩溃(检查`hs_err_pid*.log`文件) 2. **端口冲突** - 其他进程占用9092端口:$$ \text{netstat -tuln | grep :9092} $$ - 多个Kafka实例配置相同端口 3. **资源限制** - 文件描述符不足:$$ \lim_{n \to \infty} \text{ulimit} < \text{连接需求} $$ - 内存溢出导致OOM Killer终止进程 4. **配置错误** - `server.properties`中`listeners=PLAINTEXT://127.0.0.1:9092`配置缺失 - `advertised.listeners`与生产者连接地址不一致 ### 逐步解决方案 #### 1. 验证Broker进程状态 ```bash # 检查Kafka进程 ps aux | grep -i 'kafka\.Kafka' # 强制重启服务 sudo systemctl restart kafka # 或使用启动脚本 bin/kafka-server-start.sh -daemon config/server.properties ``` #### 2. 诊断端口冲突 ```bash # 查看端口占用 sudo lsof -i :9092 # 终止冲突进程 sudo kill -9 $(sudo lsof -t -i :9092) ``` #### 3. 资源优化 ```bash # 检查文件描述符限制 ulimit -n # 永久修改限制(添加至/etc/security/limits.conf) * soft nofile 100000 * hard nofile 100000 # 调整JVM内存(修改kafka-server-start.sh) export KAFKA_HEAP_OPTS="-Xms2G -Xmx4G" ``` #### 4. 关键配置验证 确保`server.properties`包含: ```properties listeners=PLAINTEXT://127.0.0.1:9092 advertised.listeners=PLAINTEXT://127.0.0.1:9092 num.network.threads=8 socket.send.buffer.bytes=1024000 ``` #### 5. 生产者容错配置 ```java // 增加重试和超时设置 props.put(ProducerConfig.RETRIES_CONFIG, 10); props.put(ProducerConfig.RETRY_BACKOFF_MS_CONFIG, 1000); props.put(ProducerConfig.RECONNECT_BACKOFF_MAX_MS_CONFIG, 10000); props.put(ProducerConfig.MAX_BLOCK_MS_CONFIG, 60000); // 阻塞超时 ``` ### 高级诊断工具 1. **启用DEBUG日志** ```properties # log4j.properties log4j.logger.org.apache.kafka=DEBUG ``` 2. **网络跟踪** ```bash sudo tcpdump -i lo -nn 'port 9092' -w kafka.pcap ``` 3. **堆内存分析** ```bash jmap -dump:live,format=b,file=heap.bin <kafka_pid> ``` ### 预防措施 - **监控配置**: ```bash # 监控打开文件数 watch -n 1 'cat /proc/sys/fs/file-nr' ``` - **健康检查脚本**: ```bash echo dump | nc localhost 9092 | grep -q "brokers" && echo "OK" || echo "FAIL" ``` - **启用JMX**: ```properties KAFKA_JMX_OPTS="-Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=9999" ``` > **关键提示**:本地环境需特别注意`127.0.0.1`和`localhost`的DNS解析一致性,建议在`/etc/hosts`中明确配置:`127.0.0.1 localhost`[^1]。 --- ### 相关问题 1. 如何排查Kafka Broker的JVM内存泄漏问题? 2. 单节点Kafka环境下如何配置高可用? 3. Kafka生产者`RETRIES_CONFIG`和`RETRY_BACKOFF_MS_CONFIG`的最佳实践是什么? 4. Linux系统如何永久修改文件描述符限制? 5. Kafka的`advertised.listeners`配置错误会导致哪些具体问题? [^1]: Kafka官方文档 - 网络配置: https://kafka.apache.org/documentation/#brokerconfigs_listeners
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值