|
/* |
|
* 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 com.sf.kafka.api.grey; |
|
|
|
import static com.sf.kafka.api.grey.SFMQEnvTagEnum.BLUE; |
|
import static com.sf.kafka.api.grey.SFMQEnvTagEnum.GREEN; |
|
import static com.sf.kafka.api.grey.SFMQEnvTagEnum.GREY; |
|
import static com.sf.kafka.api.grey.SFMQEnvTagEnum.NORMAL; |
|
import static com.sf.kafka.api.grey.SFMQGreyConstants.GROUP_INSTANCE_ID_SPLIT; |
|
import static com.sf.kafka.api.grey.SFMQGreyConstants.KAFKA_GREY_TOPICS2; |
|
import static com.sf.kafka.util.KafkaUtil.CLUSTER_ID_ALIAS; |
|
import com.google.common.collect.Lists; |
|
import com.google.common.collect.Sets; |
|
import java.util.ArrayList; |
|
import java.util.Collections; |
|
import java.util.HashMap; |
|
import java.util.List; |
|
import java.util.Map; |
|
import java.util.stream.Collectors; |
|
import org.apache.commons.lang3.StringUtils; |
|
import org.apache.kafka.clients.consumer.internals.AbstractPartitionAssignor; |
|
import org.apache.kafka.common.Cluster; |
|
import org.apache.kafka.common.TopicPartition; |
|
import org.slf4j.Logger; |
|
import org.slf4j.LoggerFactory; |
|
|
|
/** |
|
* 在 range assignor 分配策略基础上添加 小流量灰度&蓝绿 支持。 |
|
* |
|
* <p>The sf range assignor works on a per-topic basis. For each topic, we lay out the available partitions in numeric |
|
* order |
|
* and the consumers in lexicographic order. We then divide the number of partitions by the total number of |
|
* consumers to determine the number of partitions to assign to each consumer. If it does not evenly |
|
* divide, then the first few consumers will have one extra partition. |
|
* |
|
* <p>For example, suppose there are two consumers <code>C0</code> and <code>C1</code>, two topics <code>t0</code> and |
|
* <code>t1</code>, and each topic has 3 partitions, resulting in partitions <code>t0p0</code>, <code>t0p1</code>, |
|
* <code>t0p2</code>, <code>t1p0</code>, <code>t1p1</code>, and <code>t1p2</code>. |
|
* |
|
* <p>The assignment will be: |
|
* <ul> |
|
* <li><code>C0: [t0p0, t0p1, t1p0, t1p1]</code></li> |
|
* <li><code>C1: [t0p2, t1p2]</code></li> |
|
* </ul> |
|
* |
|
* Since the introduction of static membership, we could leverage <code>group.instance.id</code> to make the |
|
* assignment behavior more sticky. |
|
* For the above example, after one rolling bounce, group coordinator will attempt to assign new <code>member |
|
* .id</code> towards consumers, |
|
* for example <code>C0</code> -> <code>C3</code> <code>C1</code> -> <code>C2</code>. |
|
* |
|
* <p>The assignment could be completely shuffled to: |
|
* <ul> |
|
* <li><code>C3 (was C0): [t0p2, t1p2] (before was [t0p0, t0p1, t1p0, t1p1])</code> |
|
* <li><code>C2 (was C1): [t0p0, t0p1, t1p0, t1p1] (before was [t0p2, t1p2])</code> |
|
* </ul> |
|
* |
|
* The assignment change was caused by the change of <code>member.id</code> relative order, and |
|
* can be avoided by setting the group.instance.id. |
|
* Consumers will have individual instance ids <code>I1</code>, <code>I2</code>. As long as |
|
* 1. Number of members remain the same across generation |
|
* 2. Static members' identities persist across generation |
|
* 3. Subscription pattern doesn't change for any member |
|
* |
|
* <p>The assignment will always be: |
|
* <ul> |
|
* <li><code>I0: [t0p0, t0p1, t1p0, t1p1]</code> |
|
* <li><code>I1: [t0p2, t1p2]</code> |
|
* </ul> |
|
* |
|
* @author YANGLiiN |
|
*/ |
|
public class SFRangeAssignor extends AbstractPartitionAssignor { |
|
private static final Logger log = LoggerFactory.getLogger(SFRangeAssignor.class); |
|
|
|
private String clusterId = null; |
|
private String clusterAlias = null; |
|
|
|
public static final String SF_RANGE_ASSIGNOR_NAME = "sf-range"; |
|
|
|
@Override |
|
public String name() { |
|
return SF_RANGE_ASSIGNOR_NAME; |
|
} |
|
|
|
private Map<String, List<MemberInfo>> consumersPerTopic(Map<String, Subscription> consumerMetadata) { |
|
Map<String, List<MemberInfo>> topicToConsumers = new HashMap<>(); |
|
for (Map.Entry<String, Subscription> subscriptionEntry : consumerMetadata.entrySet()) { |
|
String consumerId = subscriptionEntry.getKey(); |
|
MemberInfo memberInfo = new MemberInfo(consumerId, subscriptionEntry.getValue().groupInstanceId()); |
|
for (String topic : subscriptionEntry.getValue().topics()) { |
|
put(topicToConsumers, topic, memberInfo); |
|
} |
|
} |
|
return topicToConsumers; |
|
} |
|
|
|
|
|
@Override |
|
public GroupAssignment assign(Cluster metadata, GroupSubscription groupSubscription) { |
|
if (clusterId == null) { |
|
clusterId = metadata.clusterResource().clusterId(); |
|
clusterAlias = CLUSTER_ID_ALIAS.getOrDefault(clusterId, ""); |
|
|
|
} |
|
return super.assign(metadata, groupSubscription); |
|
} |
|
|
|
@Override |
|
public Map<String, List<TopicPartition>> assign(Map<String, Integer> partitionsPerTopic, |
|
Map<String, Subscription> subscriptions) { |
|
Map<String, List<MemberInfo>> consumersPerTopic = consumersPerTopic(subscriptions); |
|
|
|
Map<String, List<TopicPartition>> assignment = new HashMap<>(); |
|
for (String memberId : subscriptions.keySet()) { |
|
assignment.put(memberId, new ArrayList<>()); |
|
} |
|
|
|
for (Map.Entry<String, List<MemberInfo>> topicEntry : consumersPerTopic.entrySet()) { |
|
String topic = topicEntry.getKey(); |
|
List<MemberInfo> consumersForTopic = topicEntry.getValue(); |
|
|
|
Integer numPartitionsForTopic = partitionsPerTopic.get(topic); |
|
if (numPartitionsForTopic == null) { |
|
continue; |
|
} |
|
|
|
Collections.sort(consumersForTopic); |
|
|
|
List<TopicPartition> partitions = AbstractPartitionAssignor.partitions(topic, numPartitionsForTopic); |
|
|
|
boolean isGreyNormal = false; |
|
boolean isBlueGreen = false; |
|
|
|
List<MemberInfo> consumersForTopicGrey = Lists.newArrayList(); |
|
List<MemberInfo> consumersForTopicNormal = Lists.newArrayList(); |
|
List<MemberInfo> consumersForTopicBlue = Lists.newArrayList(); |
|
List<MemberInfo> consumersForTopicGreen = Lists.newArrayList(); |
|
|
|
if (KAFKA_GREY_TOPICS2.getOrDefault(clusterAlias, Sets.newHashSet()).contains(topic)) { |
|
for (MemberInfo memberInfo : consumersForTopic) { |
|
String[] ids = StringUtils.split(memberInfo.memberId, GROUP_INSTANCE_ID_SPLIT); |
|
if (ids.length >= 2) { |
|
String envStr = ids[0]; |
|
SFMQEnvTagEnum envTag = SFMQEnvTagEnum.toEnvTagEnum(envStr); |
|
if (!envTag.isUnknown()) { |
|
switch (envTag) { |
|
case GREY: |
|
isGreyNormal = true; |
|
consumersForTopicGrey.add(memberInfo); |
|
break; |
|
case NORMAL: |
|
isGreyNormal = true; |
|
consumersForTopicNormal.add(memberInfo); |
|
break; |
|
|
|
case BLUE: |
|
isBlueGreen = true; |
|
consumersForTopicBlue.add(memberInfo); |
|
break; |
|
case GREEN: |
|
isBlueGreen = true; |
|
consumersForTopicGreen.add(memberInfo); |
|
break; |
|
} |
|
} |
|
} |
|
|
|
} |
|
|
|
|
|
// 灰度(小流量) |
|
if (isGreyNormal) { |
|
|
|
List<TopicPartition> partitionsGrey = |
|
partitions.stream().filter(GREY.topicPartitionPredicate()).collect(Collectors.toList()); |
|
doAssign(consumersForTopicGrey, partitionsGrey, assignment); |
|
|
|
|
|
List<TopicPartition> partitionsNormal = |
|
partitions.stream().filter(NORMAL.topicPartitionPredicate()).collect(Collectors.toList()); |
|
doAssign(consumersForTopicNormal, partitionsNormal, assignment); |
|
|
|
} |
|
|
|
// 蓝绿 |
|
if (isBlueGreen) { |
|
|
|
List<TopicPartition> partitionsBlue = |
|
partitions.stream().filter(BLUE.topicPartitionPredicate()).collect(Collectors.toList()); |
|
doAssign(consumersForTopicBlue, partitionsBlue, assignment); |
|
|
|
|
|
List<TopicPartition> partitionsGreen = |
|
partitions.stream().filter(GREEN.topicPartitionPredicate()).collect(Collectors.toList()); |
|
doAssign(consumersForTopicGreen, partitionsGreen, assignment); |
|
|
|
|
|
} |
|
|
|
// 只配置有灰度蓝绿主题,但是没配置环境标签(或配置错误) |
|
if (!isGreyNormal && !isBlueGreen) { |
|
log.warn("配置了灰度蓝绿主题,但没正确配置[灰度]或[蓝绿]环境标!"); |
|
doAssign(consumersForTopic, partitions, assignment); |
|
} |
|
|
|
// 同时配置了灰度、蓝绿 |
|
if (isGreyNormal && isBlueGreen) { |
|
log.warn("请忽同时开启[灰度]和[蓝绿]!"); |
|
} |
|
|
|
|
|
} else { |
|
doAssign(consumersForTopic, partitions, assignment); |
|
} |
|
|
|
|
|
} |
|
return assignment; |
|
} |
|
|
|
private void doAssign(List<MemberInfo> consumersForTopic, List<TopicPartition> partitions, |
|
Map<String, List<TopicPartition>> assignment) { |
|
if (partitions != null && partitions.size() > 0 && consumersForTopic != null && consumersForTopic.size() > 0) { |
|
int numPartitionsPerConsumer = partitions.size() / consumersForTopic.size(); |
|
int consumersWithExtraPartition = partitions.size() % consumersForTopic.size(); |
|
|
|
for (int i = 0, n = consumersForTopic.size(); i < n; i++) { |
|
int start = numPartitionsPerConsumer * i + Math.min(i, consumersWithExtraPartition); |
|
int length = numPartitionsPerConsumer + (i + 1 > consumersWithExtraPartition ? 0 : 1); |
|
assignment.get(consumersForTopic.get(i).memberId).addAll(partitions.subList(start, start + length)); |
|
} |
|
} |
|
} |
|
} |