![JAR search and dependency download from the Maven repository](/logo.png)
org.apache.kafka.common.network.Selector Maven / Gradle / Ivy
Show all versions of kafka-clients Show documentation
/**
* Licensed to the Apache Software Foundation (ASF) under one or more contributor license agreements. See the NOTICE
* file distributed with this work for additional information regarding copyright ownership. The ASF licenses this file
* to You under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the
* License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
* an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
* specific language governing permissions and limitations under the License.
*/
package org.apache.kafka.common.network;
import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.nio.channels.CancelledKeyException;
import java.nio.channels.SelectionKey;
import java.nio.channels.SocketChannel;
import java.nio.channels.UnresolvedAddressException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import org.apache.kafka.common.KafkaException;
import org.apache.kafka.common.metrics.Measurable;
import org.apache.kafka.common.metrics.MetricConfig;
import org.apache.kafka.common.MetricName;
import org.apache.kafka.common.metrics.Metrics;
import org.apache.kafka.common.metrics.Sensor;
import org.apache.kafka.common.metrics.stats.Avg;
import org.apache.kafka.common.metrics.stats.Count;
import org.apache.kafka.common.metrics.stats.Max;
import org.apache.kafka.common.metrics.stats.Rate;
import org.apache.kafka.common.utils.Time;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A selector interface for doing non-blocking multi-connection network I/O.
*
* This class works with {@link NetworkSend} and {@link NetworkReceive} to transmit size-delimited network requests and
* responses.
*
* A connection can be added to the selector associated with an integer id by doing
*
*
* selector.connect(42, new InetSocketAddress("google.com", server.port), 64000, 64000);
*
*
* The connect call does not block on the creation of the TCP connection, so the connect method only begins initiating
* the connection. The successful invocation of this method does not mean a valid connection has been established.
*
* Sending requests, receiving responses, processing connection completions, and disconnections on the existing
* connections are all done using the poll()
call.
*
*
* List<NetworkRequest> requestsToSend = Arrays.asList(new NetworkRequest(0, myBytes), new NetworkRequest(1, myOtherBytes));
* selector.poll(TIMEOUT_MS, requestsToSend);
*
*
* The selector maintains several lists that are reset by each call to poll()
which are available via
* various getters. These are reset by each call to poll()
.
*
* This class is not thread safe!
*/
public class Selector implements Selectable {
private static final Logger log = LoggerFactory.getLogger(Selector.class);
private final java.nio.channels.Selector selector;
private final Map keys;
private final List completedSends;
private final List completedReceives;
private final List disconnected;
private final List connected;
private final Time time;
private final SelectorMetrics sensors;
private final String metricGrpPrefix;
private final Map metricTags;
/**
* Create a new selector
*/
public Selector(Metrics metrics, Time time , String metricGrpPrefix , Map metricTags) {
try {
this.selector = java.nio.channels.Selector.open();
} catch (IOException e) {
throw new KafkaException(e);
}
this.time = time;
this.metricGrpPrefix = metricGrpPrefix;
this.metricTags = metricTags;
this.keys = new HashMap();
this.completedSends = new ArrayList();
this.completedReceives = new ArrayList();
this.connected = new ArrayList();
this.disconnected = new ArrayList();
this.sensors = new SelectorMetrics(metrics);
}
/**
* Begin connecting to the given address and add the connection to this selector associated with the given id
* number.
*
* Note that this call only initiates the connection, which will be completed on a future {@link #poll(long, List)}
* call. Check {@link #connected()} to see which (if any) connections have completed after a given poll call.
* @param id The id for the new connection
* @param address The address to connect to
* @param sendBufferSize The send buffer for the new connection
* @param receiveBufferSize The receive buffer for the new connection
* @throws IllegalStateException if there is already a connection for that id
* @throws IOException if DNS resolution fails on the hostname or if the broker is down
*/
@Override
public void connect(int id, InetSocketAddress address, int sendBufferSize, int receiveBufferSize) throws IOException {
if (this.keys.containsKey(id))
throw new IllegalStateException("There is already a connection for id " + id);
SocketChannel channel = SocketChannel.open();
channel.configureBlocking(false);
Socket socket = channel.socket();
socket.setKeepAlive(true);
socket.setSendBufferSize(sendBufferSize);
socket.setReceiveBufferSize(receiveBufferSize);
socket.setTcpNoDelay(true);
try {
channel.connect(address);
} catch (UnresolvedAddressException e) {
channel.close();
throw new IOException("Can't resolve address: " + address, e);
} catch (IOException e) {
channel.close();
throw e;
}
SelectionKey key = channel.register(this.selector, SelectionKey.OP_CONNECT);
key.attach(new Transmissions(id));
this.keys.put(id, key);
}
/**
* Disconnect any connections for the given id (if there are any). The disconnection is asynchronous and will not be
* processed until the next {@link #poll(long, List) poll()} call.
*/
@Override
public void disconnect(int id) {
SelectionKey key = this.keys.get(id);
if (key != null)
key.cancel();
}
/**
* Interrupt the selector if it is blocked waiting to do I/O.
*/
@Override
public void wakeup() {
this.selector.wakeup();
}
/**
* Close this selector and all associated connections
*/
@Override
public void close() {
for (SelectionKey key : this.selector.keys())
close(key);
try {
this.selector.close();
} catch (IOException e) {
log.error("Exception closing selector:", e);
}
}
/**
* Do whatever I/O can be done on each connection without blocking. This includes completing connections, completing
* disconnections, initiating new sends, or making progress on in-progress sends or receives.
*
* The provided network sends will be started.
*
* When this call is completed the user can check for completed sends, receives, connections or disconnects using
* {@link #completedSends()}, {@link #completedReceives()}, {@link #connected()}, {@link #disconnected()}. These
* lists will be cleared at the beginning of each {@link #poll(long, List)} call and repopulated by the call if any
* completed I/O.
*
* @param timeout The amount of time to wait, in milliseconds. If negative, wait indefinitely.
* @param sends The list of new sends to begin
*
* @throws IllegalStateException If a send is given for which we have no existing connection or for which there is
* already an in-progress send
*/
@Override
public void poll(long timeout, List sends) throws IOException {
clear();
/* register for write interest on any new sends */
for (NetworkSend send : sends) {
SelectionKey key = keyForId(send.destination());
Transmissions transmissions = transmissions(key);
if (transmissions.hasSend())
throw new IllegalStateException("Attempt to begin a send operation with prior send operation still in progress.");
transmissions.send = send;
try {
key.interestOps(key.interestOps() | SelectionKey.OP_WRITE);
} catch (CancelledKeyException e) {
close(key);
}
}
/* check ready keys */
long startSelect = time.nanoseconds();
int readyKeys = select(timeout);
long endSelect = time.nanoseconds();
this.sensors.selectTime.record(endSelect - startSelect, time.milliseconds());
if (readyKeys > 0) {
Set keys = this.selector.selectedKeys();
Iterator iter = keys.iterator();
while (iter.hasNext()) {
SelectionKey key = iter.next();
iter.remove();
Transmissions transmissions = transmissions(key);
SocketChannel channel = channel(key);
// register all per-broker metrics at once
sensors.maybeRegisterNodeMetrics(transmissions.id);
try {
/* complete any connections that have finished their handshake */
if (key.isConnectable()) {
channel.finishConnect();
key.interestOps(key.interestOps() & ~SelectionKey.OP_CONNECT | SelectionKey.OP_READ);
this.connected.add(transmissions.id);
this.sensors.connectionCreated.record();
}
/* read from any connections that have readable data */
if (key.isReadable()) {
if (!transmissions.hasReceive())
transmissions.receive = new NetworkReceive(transmissions.id);
transmissions.receive.readFrom(channel);
if (transmissions.receive.complete()) {
transmissions.receive.payload().rewind();
this.completedReceives.add(transmissions.receive);
this.sensors.recordBytesReceived(transmissions.id, transmissions.receive.payload().limit());
transmissions.clearReceive();
}
}
/* write to any sockets that have space in their buffer and for which we have data */
if (key.isWritable()) {
transmissions.send.writeTo(channel);
if (transmissions.send.remaining() <= 0) {
this.completedSends.add(transmissions.send);
this.sensors.recordBytesSent(transmissions.id, transmissions.send.size());
transmissions.clearSend();
key.interestOps(key.interestOps() & ~SelectionKey.OP_WRITE);
}
}
/* cancel any defunct sockets */
if (!key.isValid())
close(key);
} catch (IOException e) {
InetAddress remoteAddress = null;
Socket socket = channel.socket();
if (socket != null)
remoteAddress = socket.getInetAddress();
log.warn("Error in I/O with {}", remoteAddress , e);
close(key);
}
}
}
long endIo = time.nanoseconds();
this.sensors.ioTime.record(endIo - endSelect, time.milliseconds());
}
@Override
public List completedSends() {
return this.completedSends;
}
@Override
public List completedReceives() {
return this.completedReceives;
}
@Override
public List disconnected() {
return this.disconnected;
}
@Override
public List connected() {
return this.connected;
}
/**
* Clear the results from the prior poll
*/
private void clear() {
this.completedSends.clear();
this.completedReceives.clear();
this.connected.clear();
this.disconnected.clear();
}
/**
* Check for data, waiting up to the given timeout.
*
* @param ms Length of time to wait, in milliseconds. If negative, wait indefinitely.
* @return The number of keys ready
* @throws IOException
*/
private int select(long ms) throws IOException {
if (ms == 0L)
return this.selector.selectNow();
else if (ms < 0L)
return this.selector.select();
else
return this.selector.select(ms);
}
/**
* Begin closing this connection
*/
private void close(SelectionKey key) {
SocketChannel channel = channel(key);
Transmissions trans = transmissions(key);
if (trans != null) {
this.disconnected.add(trans.id);
this.keys.remove(trans.id);
trans.clearReceive();
trans.clearSend();
}
key.attach(null);
key.cancel();
try {
channel.socket().close();
channel.close();
} catch (IOException e) {
log.error("Exception closing connection to node {}:", trans.id, e);
}
this.sensors.connectionClosed.record();
}
/**
* Get the selection key associated with this numeric id
*/
private SelectionKey keyForId(int id) {
SelectionKey key = this.keys.get(id);
if (key == null)
throw new IllegalStateException("Attempt to write to socket for which there is no open connection.");
return key;
}
/**
* Get the transmissions for the given connection
*/
private Transmissions transmissions(SelectionKey key) {
return (Transmissions) key.attachment();
}
/**
* Get the socket channel associated with this selection key
*/
private SocketChannel channel(SelectionKey key) {
return (SocketChannel) key.channel();
}
/**
* The id and in-progress send and receive associated with a connection
*/
private static class Transmissions {
public int id;
public NetworkSend send;
public NetworkReceive receive;
public Transmissions(int id) {
this.id = id;
}
public boolean hasSend() {
return this.send != null;
}
public void clearSend() {
this.send = null;
}
public boolean hasReceive() {
return this.receive != null;
}
public void clearReceive() {
this.receive = null;
}
}
private class SelectorMetrics {
private final Metrics metrics;
public final Sensor connectionClosed;
public final Sensor connectionCreated;
public final Sensor bytesTransferred;
public final Sensor bytesSent;
public final Sensor bytesReceived;
public final Sensor selectTime;
public final Sensor ioTime;
public SelectorMetrics(Metrics metrics) {
this.metrics = metrics;
String metricGrpName = metricGrpPrefix + "-metrics";
this.connectionClosed = this.metrics.sensor("connections-closed");
MetricName metricName = new MetricName("connection-close-rate", metricGrpName, "Connections closed per second in the window.", metricTags);
this.connectionClosed.add(metricName, new Rate());
this.connectionCreated = this.metrics.sensor("connections-created");
metricName = new MetricName("connection-creation-rate", metricGrpName, "New connections established per second in the window.", metricTags);
this.connectionCreated.add(metricName, new Rate());
this.bytesTransferred = this.metrics.sensor("bytes-sent-received");
metricName = new MetricName("network-io-rate", metricGrpName, "The average number of network operations (reads or writes) on all connections per second.", metricTags);
bytesTransferred.add(metricName, new Rate(new Count()));
this.bytesSent = this.metrics.sensor("bytes-sent", bytesTransferred);
metricName = new MetricName("outgoing-byte-rate", metricGrpName, "The average number of outgoing bytes sent per second to all servers.", metricTags);
this.bytesSent.add(metricName, new Rate());
metricName = new MetricName("request-rate", metricGrpName, "The average number of requests sent per second.", metricTags);
this.bytesSent.add(metricName, new Rate(new Count()));
metricName = new MetricName("request-size-avg", metricGrpName, "The average size of all requests in the window..", metricTags);
this.bytesSent.add(metricName, new Avg());
metricName = new MetricName("request-size-max", metricGrpName, "The maximum size of any request sent in the window.", metricTags);
this.bytesSent.add(metricName, new Max());
this.bytesReceived = this.metrics.sensor("bytes-received", bytesTransferred);
metricName = new MetricName("incoming-byte-rate", metricGrpName, "Bytes/second read off all sockets", metricTags);
this.bytesReceived.add(metricName, new Rate());
metricName = new MetricName("response-rate", metricGrpName, "Responses received sent per second.", metricTags);
this.bytesReceived.add(metricName, new Rate(new Count()));
this.selectTime = this.metrics.sensor("select-time");
metricName = new MetricName("select-rate", metricGrpName, "Number of times the I/O layer checked for new I/O to perform per second", metricTags);
this.selectTime.add(metricName, new Rate(new Count()));
metricName = new MetricName("io-wait-time-ns-avg", metricGrpName, "The average length of time the I/O thread spent waiting for a socket ready for reads or writes in nanoseconds.", metricTags);
this.selectTime.add(metricName, new Avg());
metricName = new MetricName("io-wait-ratio", metricGrpName, "The fraction of time the I/O thread spent waiting.", metricTags);
this.selectTime.add(metricName, new Rate(TimeUnit.NANOSECONDS));
this.ioTime = this.metrics.sensor("io-time");
metricName = new MetricName("io-time-ns-avg", metricGrpName, "The average length of time for I/O per select call in nanoseconds.", metricTags);
this.ioTime.add(metricName, new Avg());
metricName = new MetricName("io-ratio", metricGrpName, "The fraction of time the I/O thread spent doing I/O", metricTags);
this.ioTime.add(metricName, new Rate(TimeUnit.NANOSECONDS));
metricName = new MetricName("connection-count", metricGrpName, "The current number of active connections.", metricTags);
this.metrics.addMetric(metricName, new Measurable() {
public double measure(MetricConfig config, long now) {
return keys.size();
}
});
}
public void maybeRegisterNodeMetrics(int node) {
if (node >= 0) {
// if one sensor of the metrics has been registered for the node,
// then all other sensors should have been registered; and vice versa
String nodeRequestName = "node-" + node + ".bytes-sent";
Sensor nodeRequest = this.metrics.getSensor(nodeRequestName);
if (nodeRequest == null) {
String metricGrpName = metricGrpPrefix + "-node-metrics";
Map tags = new LinkedHashMap(metricTags);
tags.put("node-id", "node-"+node);
nodeRequest = this.metrics.sensor(nodeRequestName);
MetricName metricName = new MetricName("outgoing-byte-rate", metricGrpName, tags);
nodeRequest.add(metricName, new Rate());
metricName = new MetricName("request-rate", metricGrpName, "The average number of requests sent per second.", tags);
nodeRequest.add(metricName, new Rate(new Count()));
metricName = new MetricName("request-size-avg", metricGrpName, "The average size of all requests in the window..", tags);
nodeRequest.add(metricName, new Avg());
metricName = new MetricName("request-size-max", metricGrpName, "The maximum size of any request sent in the window.", tags);
nodeRequest.add(metricName, new Max());
String nodeResponseName = "node-" + node + ".bytes-received";
Sensor nodeResponse = this.metrics.sensor(nodeResponseName);
metricName = new MetricName("incoming-byte-rate", metricGrpName, tags);
nodeResponse.add(metricName, new Rate());
metricName = new MetricName("response-rate", metricGrpName, "The average number of responses received per second.", tags);
nodeResponse.add(metricName, new Rate(new Count()));
String nodeTimeName = "node-" + node + ".latency";
Sensor nodeRequestTime = this.metrics.sensor(nodeTimeName);
metricName = new MetricName("request-latency-avg", metricGrpName, tags);
nodeRequestTime.add(metricName, new Avg());
metricName = new MetricName("request-latency-max", metricGrpName, tags);
nodeRequestTime.add(metricName, new Max());
}
}
}
public void recordBytesSent(int node, int bytes) {
long now = time.milliseconds();
this.bytesSent.record(bytes, now);
if (node >= 0) {
String nodeRequestName = "node-" + node + ".bytes-sent";
Sensor nodeRequest = this.metrics.getSensor(nodeRequestName);
if (nodeRequest != null)
nodeRequest.record(bytes, now);
}
}
public void recordBytesReceived(int node, int bytes) {
long now = time.milliseconds();
this.bytesReceived.record(bytes, now);
if (node >= 0) {
String nodeRequestName = "node-" + node + ".bytes-received";
Sensor nodeRequest = this.metrics.getSensor(nodeRequestName);
if (nodeRequest != null)
nodeRequest.record(bytes, now);
}
}
}
}