private Map<String, List<MemberInfo>> consumersPerTopic(Map<String, Subscription> consumerMetadata) {
        Map<String, List<MemberInfo>> topicToConsumers = new HashMap<>();
        consumerMetadata.forEach((consumerId, subscription) -> {
            MemberInfo memberInfo = new MemberInfo(consumerId, subscription.groupInstanceId(), subscription.rackId());
            subscription.topics().forEach(topic -> put(topicToConsumers, topic, memberInfo));
        });
        return topicToConsumers;
    }
<p>The 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> -&gt; <code>C3</code> <code>C1</code> -&gt; <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>
<p>
Rack-aware assignment is used if both consumer and partition replica racks are available and
some partitions have replicas only on a subset of racks. We attempt to match consumer racks with
partition replica racks on a best-effort basis, prioritizing balanced assignment over rack alignment.
Topics with equal partition count and same set of subscribers guarantee co-partitioning by prioritizing
co-partitioning over rack alignment. In this case, aligning partition replicas of these topics on the
same racks will improve locality for consumers. For example, if partitions 0 of all topics have a replica
on rack 'a', partition 1 on rack 'b' etc., partition 0 of all topics can be assigned to a consumer
on rack 'a', partition 1 to a consumer on rack 'b' and so on.
<p>
Note that rack-aware assignment currently takes all replicas into account, including any offline replicas
and replicas that are not in the ISR. This is based on the assumption that these replicas are likely
to join the ISR relatively soon. Since consumers don't rebalance on ISR change, this avoids unnecessary
cross-rack traffic for long durations after replicas rejoin the ISR. In the future, we may consider
rebalancing when replicas are added or removed to improve consumer rack alignment.
</p>
clients/src/main/java/org/apache/kafka/clients/consumer/RangeAssignor.java