Skip to content

Commit

Permalink
Use BitSet for slot storage #715
Browse files Browse the repository at this point in the history
Lettuce now uses BitSet to represent internally cluster slots assigned to a cluster node. The slot storage is allocated lazily and nodes without a slot (non-slot masters, slave nodes) won't allocate any memory for the slot storage.

This change reduces the memory usage from the previously used ArrayList with initially 16 elements backed by an Object[]. A node with all slots assigned required about 325.000 bytes of memory.

Using a BitSet stores only bitwise whether a slot is assigned or not. The storage requires a fixed amount of memory (about 2184 bytes) for each node independent of how many slots are occupied. A cluster of 150 nodes reached the break-even between the previous and current pattern if at least a single slot is used on a particular node. Clusters with more nodes will have always a higher memory consumption if at least a single slot is occupied.
  • Loading branch information
mp911de committed Mar 12, 2018
1 parent b311bbb commit ed91964
Show file tree
Hide file tree
Showing 7 changed files with 274 additions and 97 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -16,26 +16,28 @@
package io.lettuce.core.cluster.models.partitions;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.*;

import io.lettuce.core.RedisURI;
import io.lettuce.core.cluster.SlotHash;
import io.lettuce.core.internal.LettuceAssert;
import io.lettuce.core.internal.LettuceSets;
import io.lettuce.core.models.role.RedisNodeDescription;

/**
* Representation of a Redis Cluster node. A {@link RedisClusterNode} is identified by its {@code nodeId}. A
* {@link RedisClusterNode} can be a {@link #getRole() responsible master} for zero to
* {@link io.lettuce.core.cluster.SlotHash#SLOT_COUNT 16384} slots, a slave of one {@link #getSlaveOf() master} of carry
* different {@link io.lettuce.core.cluster.models.partitions.RedisClusterNode.NodeFlag flags}.
* Representation of a Redis Cluster node. A {@link RedisClusterNode} is identified by its {@code nodeId}.
* <p/>
* A {@link RedisClusterNode} can be a {@link #getRole() responsible master} or slave. Masters can be responsible for zero to
* {@link io.lettuce.core.cluster.SlotHash#SLOT_COUNT 16384} slots. Each slave refers to exactly one {@link #getSlaveOf()
* master}. Nodes can have different {@link io.lettuce.core.cluster.models.partitions.RedisClusterNode.NodeFlag flags} assigned.
* <p/>
* This class is mutable and not thread-safe if mutated by multiple threads concurrently.
*
* @author Mark Paluch
* @since 3.0
*/
@SuppressWarnings("serial")
public class RedisClusterNode implements Serializable, RedisNodeDescription {

private RedisURI uri;
private String nodeId;

Expand All @@ -45,36 +47,45 @@ public class RedisClusterNode implements Serializable, RedisNodeDescription {
private long pongReceivedTimestamp;
private long configEpoch;

private List<Integer> slots;
private Set<NodeFlag> flags;
private BitSet slots;
private final Set<NodeFlag> flags = EnumSet.noneOf(NodeFlag.class);

public RedisClusterNode() {

}

public RedisClusterNode(RedisURI uri, String nodeId, boolean connected, String slaveOf, long pingSentTimestamp,
long pongReceivedTimestamp, long configEpoch, List<Integer> slots, Set<NodeFlag> flags) {

this.uri = uri;
this.nodeId = nodeId;
this.connected = connected;
this.slaveOf = slaveOf;
this.pingSentTimestamp = pingSentTimestamp;
this.pongReceivedTimestamp = pongReceivedTimestamp;
this.configEpoch = configEpoch;
this.slots = slots;
this.flags = flags;

setSlotBits(slots);
setFlags(flags);
}

public RedisClusterNode(RedisClusterNode redisClusterNode) {

LettuceAssert.notNull(redisClusterNode, "RedisClusterNode must not be null");

this.uri = redisClusterNode.uri;
this.nodeId = redisClusterNode.nodeId;
this.connected = redisClusterNode.connected;
this.slaveOf = redisClusterNode.slaveOf;
this.pingSentTimestamp = redisClusterNode.pingSentTimestamp;
this.pongReceivedTimestamp = redisClusterNode.pongReceivedTimestamp;
this.configEpoch = redisClusterNode.configEpoch;
this.slots = new ArrayList<>(redisClusterNode.slots);
this.flags = LettuceSets.newHashSet(redisClusterNode.flags);

if (redisClusterNode.slots != null && !redisClusterNode.slots.isEmpty()) {
this.slots = new BitSet(SlotHash.SLOT_COUNT);
this.slots.or(redisClusterNode.slots);
}

setFlags(redisClusterNode.flags);
}

/**
Expand All @@ -84,8 +95,12 @@ public RedisClusterNode(RedisClusterNode redisClusterNode) {
* @return a new instance of {@link RedisClusterNode}
*/
public static RedisClusterNode of(String nodeId) {

LettuceAssert.notNull(nodeId, "NodeId must not be null");

RedisClusterNode redisClusterNode = new RedisClusterNode();
redisClusterNode.setNodeId(nodeId);

return redisClusterNode;
}

Expand All @@ -94,11 +109,12 @@ public RedisURI getUri() {
}

/**
* Sets thhe connection point details. Usually the host/ip/port where a particular Redis Cluster node server is running.
* Sets the connection point details. Usually the host/ip/port where a particular Redis Cluster node server is running.
*
* @param uri the {@link RedisURI}, must not be {@literal null}
*/
public void setUri(RedisURI uri) {

LettuceAssert.notNull(uri, "RedisURI must not be null");
this.uri = uri;
}
Expand Down Expand Up @@ -184,20 +200,52 @@ public void setConfigEpoch(long configEpoch) {
}

public List<Integer> getSlots() {

if (slots == null || slots.isEmpty()) {
return Collections.emptyList();
}

List<Integer> slots = new ArrayList<>();

for (int i = 0; i < SlotHash.SLOT_COUNT; i++) {

if (this.slots.get(i)) {
slots.add(i);
}
}

return slots;
}

/**
* Sets the list of slots for which this {@link RedisClusterNode} is the
* {@link io.lettuce.core.cluster.models.partitions.RedisClusterNode.NodeFlag#MASTER}. The list is empty if this node
* is not a master or the node is not responsible for any slots at all.
* {@link io.lettuce.core.cluster.models.partitions.RedisClusterNode.NodeFlag#MASTER}. The list is empty if this node is not
* a master or the node is not responsible for any slots at all.
*
* @param slots list of slots, must not be {@literal null} but may be empty
*/
public void setSlots(List<Integer> slots) {

LettuceAssert.notNull(slots, "Slots must not be null");

this.slots = slots;
setSlotBits(slots);
}

private void setSlotBits(List<Integer> slots) {

if (slots.isEmpty() && this.slots == null) {
return;
}

if (this.slots == null) {
this.slots = new BitSet(SlotHash.SLOT_COUNT);
}

this.slots.clear();

for (Integer slot : slots) {
this.slots.set(slot);
}
}

public Set<NodeFlag> getFlags() {
Expand All @@ -210,7 +258,35 @@ public Set<NodeFlag> getFlags() {
* @param flags the set of node flags.
*/
public void setFlags(Set<NodeFlag> flags) {
this.flags = flags;

this.flags.clear();
this.flags.addAll(flags);
}

/**
* @param nodeFlag the node flag
* @return true if the {@linkplain NodeFlag} is contained within the flags.
*/
public boolean is(NodeFlag nodeFlag) {
return getFlags().contains(nodeFlag);
}

/**
* @param slot the slot hash
* @return true if the slot is contained within the handled slots.
*/
public boolean hasSlot(int slot) {
return slot <= SlotHash.SLOT_COUNT && this.slots != null && this.slots.get(slot);
}

/**
* Returns the {@link Role} of the Redis Cluster node based on the {@link #getFlags() flags}.
*
* @return the Redis Cluster node role
*/
@Override
public Role getRole() {
return is(NodeFlag.MASTER) ? Role.MASTER : Role.SLAVE;
}

@Override
Expand All @@ -233,13 +309,12 @@ public boolean equals(Object o) {

@Override
public int hashCode() {
int result = 31 * (nodeId != null ? nodeId.hashCode() : 0);
return result;
return 31 * (nodeId != null ? nodeId.hashCode() : 0);
}

@Override
public String toString() {
final StringBuilder sb = new StringBuilder();
StringBuilder sb = new StringBuilder();
sb.append(getClass().getSimpleName());
sb.append(" [uri=").append(uri);
sb.append(", nodeId='").append(nodeId).append('\'');
Expand All @@ -250,46 +325,16 @@ public String toString() {
sb.append(", configEpoch=").append(configEpoch);
sb.append(", flags=").append(flags);
if (slots != null) {
sb.append(", slot count=").append(slots.size());
sb.append(", slot count=").append(slots.cardinality());
}
sb.append(']');
return sb.toString();
}

/**
*
* @param nodeFlag the node flag
* @return true if the {@linkplain NodeFlag} is contained within the flags.
*/
public boolean is(NodeFlag nodeFlag) {
return getFlags().contains(nodeFlag);
}

/**
*
* @param slot the slot hash
* @return true if the slot is contained within the handled slots.
*/
public boolean hasSlot(int slot) {
return getSlots().contains(slot);
}

/**
* Returns the {@link io.lettuce.core.models.role.RedisInstance.Role} of the Redis Cluster node based on the
* {@link #getFlags() flags}.
*
* @return the Redis Cluster node role
*/
@Override
public Role getRole() {
return is(NodeFlag.MASTER) ? Role.MASTER : Role.SLAVE;
}

/**
* Redis Cluster node flags.
*/
public enum NodeFlag {
NOFLAGS, MYSELF, SLAVE, MASTER, EVENTUAL_FAIL, FAIL, HANDSHAKE, NOADDR;
}

}
18 changes: 6 additions & 12 deletions src/test/java/io/lettuce/core/cluster/RedisClusterClientTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

import org.junit.*;
import org.junit.runners.MethodSorters;
Expand Down Expand Up @@ -292,13 +294,9 @@ public void testClusterRedirection() throws Exception {
Partitions partitions = clusterClient.getPartitions();

for (RedisClusterNode partition : partitions) {
partition.setSlots(new ArrayList<>());
partition.setSlots(Collections.emptyList());
if (partition.getFlags().contains(RedisClusterNode.NodeFlag.MYSELF)) {

int[] slots = createSlots(0, 16384);
for (int i = 0; i < slots.length; i++) {
partition.getSlots().add(i);
}
partition.setSlots(IntStream.range(0, SlotHash.SLOT_COUNT).boxed().collect(Collectors.toList()));
}
}
partitions.updateCache();
Expand Down Expand Up @@ -335,13 +333,9 @@ public void testClusterRedirectionLimit() throws Exception {
for (RedisClusterNode partition : partitions) {

if (partition.getSlots().contains(15495)) {
partition.setSlots(new ArrayList<>());
partition.setSlots(Collections.emptyList());
} else {
partition.setSlots(new ArrayList<>());
int[] slots = createSlots(0, 16384);
for (int i = 0; i < slots.length; i++) {
partition.getSlots().add(i);
}
partition.setSlots(IntStream.range(0, SlotHash.SLOT_COUNT).boxed().collect(Collectors.toList()));
}

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,25 +17,46 @@

import static org.assertj.core.api.Assertions.assertThat;

import java.util.Arrays;

import org.junit.Test;

import io.lettuce.core.RedisURI;
import io.lettuce.core.cluster.SlotHash;

/**
* @author Mark Paluch
*/
public class RedisClusterNodeTest {

@Test
public void testEquality() throws Exception {
public void shouldCopyNode() {

RedisClusterNode node = new RedisClusterNode();
node.setSlots(Arrays.asList(1, 2, 3, SlotHash.SLOT_COUNT - 1));

RedisClusterNode copy = new RedisClusterNode(node);

assertThat(copy.getSlots()).containsExactly(1, 2, 3, SlotHash.SLOT_COUNT - 1);
assertThat(copy.hasSlot(1)).isTrue();
assertThat(copy.hasSlot(SlotHash.SLOT_COUNT - 1)).isTrue();
}

@Test
public void testEquality() {

RedisClusterNode node = new RedisClusterNode();

assertThat(node).isEqualTo(new RedisClusterNode());
assertThat(node.hashCode()).isEqualTo(new RedisClusterNode().hashCode());

node.setUri(new RedisURI());
assertThat(node.hashCode()).isNotEqualTo(new RedisClusterNode());

}

@Test
public void testToString() throws Exception {
public void testToString() {

RedisClusterNode node = new RedisClusterNode();

assertThat(node.toString()).contains(RedisClusterNode.class.getSimpleName());
Expand Down
Loading

0 comments on commit ed91964

Please sign in to comment.