java Client:
元数据:
public final class Cluster {
private final List<Node> nodes;
private final Set<String> unauthorizedTopics;
private final Set<String> internalTopics;
private final Map<TopicPartition, PartitionInfo> partitionsByTopicPartition;
private final Map<String, List<PartitionInfo>> partitionsByTopic;
private final Map<String, List<PartitionInfo>> availablePartitionsByTopic;
private final Map<Integer, List<PartitionInfo>> partitionsByNode;
private final Map<Integer, Node> nodesById;
public class PartitionInfo {
private final String topic;
private final int partition;
private final Node leader;
private final Node[] replicas;
private final Node[] inSyncReplicas; //用处?
KafkaProducer:
先看下构造,做了一些参数初始化,关键类的初始化;
Metadata(支持topic超时机制),metadata.update(创建node通过指定的broker),通知sender线程;
RecordAccumulator初始化(内存池大小,压缩格式,重试间隔,批次发送时间阈值)
初始化sender(是否保证消息顺序,重试次数(处理batch,如果有异常,且小于重试次数,重新把批次加入到队列),ack(0,发送不管;1,等leader响应;-1,写入所有副本))
启动sender线程;
发送数据:
先看下消息生成,ProducerRecord:
private final String topic;
private final Integer partition;
private final K key;
private final V value;
private final Long timestamp;
我们再生成消息的时候,可以指定这些值,也可以指定部分;
- send--》doSend
- waitOnMetadata,等待获取元数据
- metadata添加topic,cluster获取partition info,如果存在则返回,否则-》
- 唤醒sender线程
- 循环获取partion count,直到不等0或者,超时或异常,awaitUpdate-》循环判断version,没有增加,直到超时
- 返回数据后,cluster判断topic是否授权,未授权,异常;
- 如果获取的partition数目小于指定的partition,异常;
- 对key,value进行序列化
- partition-》
- record指定partition,则返回,否则,
- 获取分区,如果有key,key%partitions(总共partition数目);
- 如果没有指定key,如果有可用的partition(partition info的leader不为null,则添加到available partitions中),根据递增值%可用个数,轮询,否则,递增值%partitions;
- ensureValidRecordSize,验证大小,若超过最大请求大小maxRequestSize1M,异常;如果超过totalMemorySize 32M,异常;
- accumulator.append
- ConcurrentMap<TopicPartition, Deque<RecordBatch>> batches;(topic和partition组合key)获取batch queue;putIfAbsent,寻找实现
- CopyOnWriteMap,实现了上面的map,如果不存在则添加,否则获取,该类有些不一样
- 首先get方法没有锁,put方法有锁,这种复杂的数据结构,读也要加上锁,防止结构错误
- 这里get避免加锁原因是,put时候,Map<K, V> copy = new HashMap<K, V>(this.map);把原map数据拷贝过去,然后
- 调用this.map = Collections.unmodifiableMap(copy);并且map变量是volatile,
- 这样做get方法使用旧的map也没有结构上的异常,也可以立即使用新的map;
- 如果批次满了,或者创建新的批次,则唤醒sender,io在sender线程,唤醒发送数据;
sender线程:
- poll操作:
- maybeUpdate封装请求,满足一定时间情况才做后续处理,否则直接返回-》leastLoadedNode,从连接池选择目前未完成请求个数最小的node,如果是0,就使用,否则继续查找,找到node
-
// node info private final int id; private final String idString; private final String host; private final int port; private final String rack;
- 如果可以发送请求,封装成,MetadataRequest,发送请求;否则,查看是否可以建立链接,建立链接;
- 执行io操作,selector.poll
- handleCompletedSends(把发送出去的数据,存储在completedSends),处理该结构,遍历,在inFlightRequests查找node下面第一个请求,如果不期望broker响应,不处理,否则头部弹出该节点,添加到responses
- handleCompletedReceives,处理响应-》maybeHandleCompletedReceive,version+1-》Metadata.update,notifyall唤醒等待获取元数据的线程;添加到responses
- metadata.update
-
this.needUpdate = false; this.lastRefreshMs = now; this.lastSuccessfulRefreshMs = now; this.version += 1;
- 这里遍历topics,key就是topic,判断value,如果是-1,更新value(更新时间),超时,删除,(超时说明该topic长时间未使用了,刷新改时间的方法是add,把value置为-1);
- 覆盖cluster,唤醒等待的线程;
- 从头看一遍做了什么
- 获取cluster,ready-》
- 遍历所有topic|partition获取队列第一个batche(topicpartition,queue,组成的map)获取partition的leader信息
- 如果leader为null同时queue不为空,加入unknowLeaderTopics
- 临时set,readdyNodes去重判断,不重复,则获取deque的第一个batch,继续处理
- 判断是否满了(deque.size()>1或batch满了),或者超时,或者内存池空,或者服务要关闭,或者flushInProgress
- 同时,如果满足(如果是重试的批次,是否达到重试间隔),才能把leader node添加到readyNodes,(也就是如果batch超时,该queue后面都不会发送)
- return new ReadyCheckResult(readyNodes,nextReadyCheckDelayMs,unknownLeaderTopics);
-
if (!result.unknownLeaderTopics.isEmpty()) { // The set of topics with unknown leader contains topics with leader election pending as well as // topics which may have expired. Add the topic again to metadata to ensure it is included // and request metadata update, since there are messages to send to the topic. for (String topic : result.unknownLeaderTopics) this.metadata.add(topic); this.metadata.requestUpdate(); }
- 上面是对没有获取到leader的node进行获取元数据标识,请求更新,设置 needUpdate为true;
- 遍历readyNodes,判断node网络状态,client.ready(node,now),如果检查有问题(如网络),从readyNodes删除
- 对每个批次group,封装为一个请求,然后发送;
网络封装结构图:
核心参数(名字可能完整):
- metadata.max.age.ms:元数据更新时间,默认300s
- retry.backoff.ms:更新cluster时间间隔,还表示batch重试时间间隔,默认100ms
- 上面两个搭配使用,有些时间处理逻辑,防止请求元数据太频繁,也防止需要请求时时间太长;
- retries: 重试次数
- acks:1,leader写入返回;0,不关心返回;-1:isr集合副本全部写入返回;
- 如果acks是-1,当isr只有leader时,退化为acks=1,此时保证数据可靠性,需要配合min.insync.replicas参数,大于该值才算提交数据
- linger.ms: 批次多久发送,默认0,批次满了发送
- max.request.size:一条消息大小,默认1M
- buffer.memory:缓存大小(内存池),batch获取使用,默认32M
- batch.size:批次大小
- receive.buffer.bytes 接收缓冲区大小
- send.buffer.bytes 发送缓冲区大小
- compression.type 设置压缩格式
- connections.max.idle.ms 链接空闲多久关闭,默认9分钟
- max.in.flight.requests.per.connection,每个链接发送后,容忍多少条没有响应,默认5,(acks非0才有效,因为重试机制,可能会乱序,设置1可以保证有序,但会影响吞吐)
问题:
- 对于无状态消息,没问题;
- 对于有状态的,就要通过key或指定partition来映射到目标机器
- 这里虽然发送的是批消息,但也会出现下面问题
- 这里涉及到消息丢失,比如A、B消息,A丢失,B到,这时候,要通过业务服务自己保证正确性;
- 根据业务情况判断,消息重要性,是否开启重试,如果开启会出现,A、B消息,A丢失,B到,然后A再到,这样要业务保证正确性
- 其实对于这个问题,即使没有kafka,端对端,也会有同样的问题,需要根据业务场景来决定,是否重发(这里要注意,如何判断是否要重发?是发送失败?(一般也会重发几次,没啥问题,并且不会出现乱序情况,因为是顺序执行,这种情况会出现消息丢失;)还是对方没响应?(这种情况会出现乱序))