2019独角兽企业重金招聘Python工程师标准>>>
分区
设置分区数
我们无法通过Producer相关的API设定分区数和复制因子的,因为Producer相关API创建topic的是通过读取server.properties
文件中的num.partitions
和default.replication.factor
的。
kafka分区分配策略
当以下事件发生时,Kafka 将会进行一次分区分配:
- 同一个 Consumer Group 内新增消费者
- 消费者离开当前所属的Consumer Group,包括shuts down 或 crashes
- 订阅的主题新增分区
每个分区只能由同一个消费组内的一个consumer来消费。那么问题来了,同一个 Consumer Group 里面的 Consumer 是如何知道该消费哪些分区里面的数据呢?
在 Kafka 内部存在两种默认的分区分配策略:Range 和 RoundRobin。具体策略可通过参数配置:partition.assignment.strategy
。
Range策略
range分区的对象是topic,会对同一个topic里面的partition进行排序,然后除以consumer数量,得到的步长就是每个consumer消费的数量,对consumer也进行了排序,每个consumer消费对应步长里面的partition。如果不能均分,则那么前几个消费者线程将多消费一个分区。这样的缺点也就是考前的消费者压力较大。例如:
topic A 的分区数量是10,则排序后是:0,1,2,3,4,5,6,7,8,9
consumer有两个C1和C2,C1有一个线程C1-0,C2有两个线程C2-0,C2-1
则对应的消费情况是:
C1-0 将消费 0, 1, 2, 3 分区(10&3=1分配给c1-0消费)
C2-0 将消费 4, 5, 6 分区
C2-1 将消费 7, 8, 9 分区
RoundRobin 策略
使用RoundRobin策略有两个前提条件必须满足:
- 同一个Consumer Group里面的所有消费者的num.streams必须相等;
- 每个消费者订阅的主题必须相同。
所以这里假设前面提到的2个消费者的num.streams = 2。RoundRobin策略的工作原理:将所有主题的分区组成 TopicAndPartition 列表,然后对 TopicAndPartition 列表按照 hashCode(data.hashCode ``%
numPartitions
) 进行排序,遍历消费者线程给分配。
假如按照 hashCode排序完的topic-partitions组依次为T1-5, T1-3, T1-0, T1-8, T1-2, T1-1, T1-4, T1-7, T1-6, T1-9,我们的消费者线程排序为C1-0, C1-1, C2-0, C2-1,最后分区分配的结果为:
C1-0 将消费 T1-5, T1-2, T1-6 分区;
C1-1 将消费 T1-3, T1-1, T1-9 分区;
C2-0 将消费 T1-0, T1-4 分区;
C2-1 将消费 T1-8, T1-7 分区;
新分区存放目录
在启动 Kafka 集群之前,我们需要配置好 log.dirs
参数,其值是 Kafka 数据的存放目录,这个参数可以配置多个目录,目录之间使用逗号分隔,通常这些目录是分布在不同的磁盘上用于提高读写性能。
如果 log.dirs
参数配置了多个目录,Kafka 会在含有分区目录最少的文件夹中创建新的分区目录,分区目录名为 Topic名+分区ID。注意,是分区文件夹总数最少的目录,而不是磁盘使用量最少的目录!也就是说,如果你给 log.dirs
参数新增了一个新的磁盘,新的分区目录肯定是先在这个新的磁盘上创建直到这个新的磁盘目录拥有的分区目录不是最少为止。
这种实现没有考虑到磁盘容量的负载,以及新加入的磁盘读写的负载。
查看详情
当key为null时
我们往Kafka发送消息时一般都是将消息封装到KeyedMessage类中:
val message = new KeyedMessage[String, String](topic, key, content)
producer.send(message)
Kafka会根据传进来的key进行hash计算其分区ID。但是这个Key可以不传,根据Kafka的官方文档描述:如果key为null,那么Producer将会把这条消息发送给随机的一个Partition。
具体的随机方式:
0.8版本中:在key为null的情况下,Kafka并不是每条消息都随机选择一个Partition;而是每隔topic.metadata.refresh.interval.ms
才会随机选择一次,使用这种伪随机的以此来减少服务器端的sockets数。
leader分区
Kafka 是使用 Scala 语言编写的,但是其支持很多语言的客户端,包括:C/C++、PHP、Go以及Ruby等等,他们怎么和kafka通讯呢,这是因为 Kafka 内部实现了一套基于TCP层的协议,只要使用这种协议与Kafka进行通信,就可以使用很多语言来操作Kafka。
其中负责如何找到Leader分区是由Metadata协议完成的。Metadata包含了kafka包含哪些主题,每个主题的分区以及分区的leaer所在broker的地址和端口等信息。
客户端只需要构造一个 TopicMetadataRequest
,Kafka 将会把内部所有的主题相关的信息发送给客户端。
// metadata包含信息
partitionId=0
leader=Some(id:1,host:1.iteblog.com,port:9092)
isr=Vector(id:1,host:1.iteblog.com,port:9092)
replicas=Vector(id:1,host:1.iteblog.com,port:9092,
id:8,host:8.iteblog.com,port:9092)
重新分区
添加集群操作:
我们往已经部署好的集群里面添加机器是最正常不过的需求,而且添加起来非常地方便,我们需要做的事是从已经部署好的节点中复制相应的配置文件,然后把里面的broker id修改成全局唯一的,最后启动这个节点即可将它加入到现有集群中。
但是问题来了,新添加的Kafka节点并不会自动地分配数据,所以无法分担集群的负载,除非我们新建一个topic。但是现在我们想手动将部分分区移到新添加的Kafka节点上,Kafka内部提供了相关的工具来重新分布某个topic的分区。
重新分区:
Kafka自带的kafka-reassign-partitions.sh
工具来重新分布分区。总共三步:
- 生成分区计划。generate模式,给定需要重新分配的Topic,自动生成reassign plan
- 执行分区计划。execute模式,根据指定的reassign plan重新分配Partition
- 查看分区结果是否正确。verify模式,验证重新分配Partition是否成功z
查看详情
broker的状态
从消费者角度来看,broker并不记录offset的偏移量,从整个角度来说是无状态的。
从metadata角度来看,每个broker都维护了相同的一份metadata cache,在partition数量,broker的新增或者删除时会更新这个cache,以保证producer端无论请求那个broker都能够获得metadata信息。
查看详情
topic
我们可以通过Kafka提供的AdminUtils.createTopic
函数或者TopicCommand
来创建topic
def createTopic(zkClient: ZkClient,
topic: String,
partitions: Int,
replicationFactor: Int,
topicConfig: Properties = new Properties)
val arguments = Array("--create", "--zookeeper", zk, "--replication-factor", "2", "--partition", "2", "--topic", "iteblog")
TopicCommand.main(arguments)
查看详情
topic数量选择
经过测试,在producer端,单个partition的吞吐量通常是在10MB/s左右。在consumer端,单个partition的吞吐量依赖于consumer端每个消息的应用逻辑处理速度。因此,我们需要对consumer端的吞吐量进行测量。
一开始,我们可以基于当前的业务吞吐量为kafka集群分配较小的broker数量,随着时间的推移,我们可以向集群中增加更多的broker,然后在线方式将适当比例的partition转移到新增加的broker中去。通过这样的方法,我们可以在满足各种应用场景(包括基于key消息的场景)的情况下,保持业务吞吐量的扩展性。
通常情况下,kafka集群中越多的partition会带来越高的吞吐量。但是,我们必须意识到集群的partition总量过大或者单个broker节点partition过多,都会对系统的可用性和消息延迟带来潜在的影响。
增加过多的分区带来的负面影响
- 越多的分区需要打开更多地文件句柄,一个分区有index和data两个句柄。
- 更多地分区会导致更高的不可用性,每个broker和controller的恢复都有耗时,这个耗时由量变到质变。
- 越多的分区可能增加端对端的延迟。一个broker上所有的repartition操作只有一个线程。
- 越多的partition意味着需要客户端需要更多的内存。
查看详情
topic划分
- 副本因子不能大于 Broker 的个数;
- 第一个分区(编号为0)的第一个副本放置位置是随机从
brokerList
选择的;第一个放置的分区副本一般都是 Leader,其余的都是 Follower 副本。 - 其他分区的第一个副本放置位置相对于第0个分区依次往后移。也就是如果我们有5个 Broker,5个分区,假设第一个分区放在第四个 Broker 上,那么第二个分区将会放在第五个 Broker 上;第三个分区将会放在第一个 Broker 上;第四个分区将会放在第二个 Broker 上,依次类推;
- 剩余的副本相对于第一个副本放置位置其实是随机产生的;
查看详情
topic分区数变化
如果在Kafka Producer往Kafka的Broker发送消息的时候用户通过命令修改了改主题的分区数,Kafka Producer能动态感知吗?答案是可以的。那是立刻就感知吗?不是,是过一定的时间(topic.metadata.refresh.interval.ms
参数决定)才知道分区数改变的。
每隔指定的时间,客户端会主动去更新topicPartitionInfo(HashMap[String, TopicMetadata]
)信息。
在启动Kafka Producer往Kafka的Broker发送消息的时候,用户修改了该Topic的分区数,Producer可以在最多topic.metadata.refresh.interval.ms
的时间之后感知到,此感知同时适用于async
和sync
模式,并且可以将数据发送到新添加的分区中。
consumer动态修改topic订阅
定义一个线程安全的存放topic的集合对象ConcurrentLinkedQueue
,每次消费时判断此集合是否有需要监听的topic,如果有则调用consumer.subscribe(topics)
更新consumer的订阅。
public static void main(String[] args) {
// 存储需要变化的topic
final ConcurrentLinkedQueue subscribedTopics
= new ConcurrentLinkedQueue<>();
// 最开始的订阅列表:atopic、btopic
consumer.subscribe(Arrays.asList("atopic", "btopic"));
while (true) {
//表示每2秒consumer就有机会去轮询一下订阅状态是否需要变更,也可以在此间隔执行一些其他相关的操作,比如定期日志记录等
consumer.poll(2000);
// 本例不关注消息消费,因此每次只是打印订阅结果!
System.out.println(consumer.subscription());
if (!subscribedTopics.isEmpty()) {
Iterator iter = subscribedTopics.iterator();
List topics = new ArrayList<>();
while (iter.hasNext()) {
topics.add(iter.next());
}
subscribedTopics.clear();
consumer.subscribe(topics); // 重新订阅topic
}
}
// 创建另一个测试线程,启动后首先暂停10秒然后变更topic订阅
Runnable runnable = new Runnable() {
@Override
public void run() {
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
// swallow it.
}
// 变更为订阅topic: btopic, ctopic
subscribedTopics.addAll(Arrays.asList("btopic", "ctopic"));
}
};
new Thread(runnable).start();
topic的管理
Kafka官方提供了两个脚本来管理topic,包括topic的增删改查。其中kafka-topics.sh
负责topic的创建与删除;kafka-configs.sh
脚本负责topic的修改和查询,但很多用户都更加倾向于使用程序API的方式对topic进行操作。
创建topic:AdminUtils.createTopic
ZkUtils zkUtils = ZkUtils.apply("localhost:2181", 30000, 30000, JaasUtils.isZkSecurityEnabled());
// 创建一个单分区单副本名为t1的topic
AdminUtils.createTopic(zkUtils, "t1", 1, 1, new Properties(), RackAwareMode.Enforced$.MODULE$);
zkUtils.close();
删除topic:AdminUtils.deleteTopic(zkUtils, "t1")
ZkUtils zkUtils = ZkUtils.apply("localhost:2181", 30000, 30000, JaasUtils.isZkSecurityEnabled());
// 删除topic 't1'
AdminUtils.deleteTopic(zkUtils, "t1");
zkUtils.close();
比较遗憾地是,不管是创建topic还是删除topic,目前Kafka实现的方式都是后台异步操作的,而且没有提供任何回调机制或返回任何结果给用户,所以用户除了捕获异常以及查询topic状态之外似乎并没有特别好的办法可以检测操作是否成功。
查询topic:AdminUtils.fetchEntityConfig
ZkUtils zkUtils = ZkUtils.apply("localhost:2181", 30000, 30000, JaasUtils.isZkSecurityEnabled());
// 获取topic 'test'的topic属性属性
Properties props = AdminUtils.fetchEntityConfig(zkUtils, ConfigType.Topic(), "test");
// 查询topic-level属性
Iterator it = props.entrySet().iterator();
while(it.hasNext()){
Map.Entry entry=(Map.Entry)it.next();
Object key = entry.getKey();
Object value = entry.getValue();
System.out.println(key + " = " + value);
}
zkUtils.close();
修改topic:AdminUtils.changeTopicConfig
ZkUtils zkUtils = ZkUtils.apply("localhost:2181", 30000, 30000, JaasUtils.isZkSecurityEnabled());
Properties props = AdminUtils.fetchEntityConfig(zkUtils, ConfigType.Topic(), "test");
// 增加topic级别属性
props.put("min.cleanable.dirty.ratio", "0.3");
// 删除topic级别属性
props.remove("max.message.bytes");
// 修改topic 'test'的属性
AdminUtils.changeTopicConfig(zkUtils, "test", props);
zkUtils.close();
日志留存策略
日志留存方式相关的策略类型主要有删除和压缩两种:delete
和compact
删除策略又分为三种:删除的单位是一个分区下面的一个日志段,即LogSegment,而且当前正在写入的日志,无论哪种策略都不会被删除。
- 基于空间的维度,默认-1,不启动,可设置broker级别或者topic级别
- 基于时间的维度,默认为保存7天,根据传入的时间戳和服务器时间比较
- 基于起始位移的维度,适用于流处理场景,在同一个partition下,删除掉指定offset之前的日志。
查看详情
数据压缩格式
kafka默认是以二进制的形式组织message的,同时也可以设置为字符串,json等格式的数据。
// 默认是二进制数组形式:
props.put("serializer.class", "kafka.serializer.DefaultEncoder");
// 发送的数据是String
props.put("serializer.class", "kafka.serializer.StringEncoder")
压缩算法:NONE、GZIP、SNAPPY、LZ4。Apache Kafka 2.1.0正式支持ZStandard,优点是压缩比高
通过crc32校验值在broker接收消息和consumer消费消息时针对每一条Message可通过crc32进行校验。
kafka压缩的是message的value,但是可以将一个messageSet放到Messsage中压缩以提高压缩比率。
KSQL
ksql是kafka提供的一个sql引擎,但是它和我们传统意义上的sql不一样,传统的sql是建立在静态的表之上的,而ksql是建立在流数据(日志)之上的,更适合于基于事件的统计分析,例如日志报警机制,在多长时间内发送了多少erro日志等,以及联机数据整合,将多个数据源通过KSQL整合成一个输出。
查看详情