Skip to content

Commit

Permalink
Adding replication benchmark
Browse files Browse the repository at this point in the history
This is not utilizing maven submodules, that would require a lot of
work. This commits add a new subproject in the `tests` folder to run
benchmarks written with JMH.

The benchmark we added here is not "micro". It uses `RaftHandle` object
to replicate data with Raft, pretty much the way a user would. We vary
the cluster and data size in the benchmark. Manually, we can tune the
RAFT instance, changing the log implementation, fsync, etc.
  • Loading branch information
jabolina committed Mar 18, 2024
1 parent 9ef3640 commit d8aedcb
Show file tree
Hide file tree
Showing 4 changed files with 332 additions and 1 deletion.
3 changes: 2 additions & 1 deletion src/org/jgroups/protocols/raft/election/BaseElection.java
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,8 @@ public void init() throws Exception {

public void stop() {
stopVotingThread();
raft.setLeaderAndTerm(null);
if (raft != null)
raft.setLeaderAndTerm(null);
}

public Object down(Event evt) {
Expand Down
39 changes: 39 additions & 0 deletions tests/benchmark/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# JGroups Raft benchmarks

This subproject includes all the benchmarks created utilizing JMH.

## How to

This specific repository uses the latest Java version available. Currently, Java 21.
To compile the project and runs the benchmarks.
First, execute:

```bash
$ mvn clean verify
```

The output is located at `./target/benchmarks.jar`.
Now, to execute a benchmark, check the current implementations and select the name.
For example, for `MyBenchmark`:

```bash
$ java -jar target/benchmarks.jar "MyBenchmark"
```

To pass configuration for `MyBenchmark`, execute:

```bash
$ java -jar target/benchmarks.jar -pkey1=value1 -pkey2=value2 "MyBenchmark"
```

For more options, related to JMH:

```bash
$ java -jar target/benchmarks.jar -h
```

## Current benchmarks

The current list of benchmarks include:

1. `DataReplicationBenchmark`: Benchmark the complete data replication utilizing `RaftHandle`;
132 changes: 132 additions & 0 deletions tests/benchmark/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>org.jgroups.raft</groupId>
<artifactId>benchmark</artifactId>
<version>1.0.13.Final-SNAPSHOT</version>
<packaging>jar</packaging>

<name>JMH benchmark sample: Java</name>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>21</java.version>
<javac.target>21</javac.target>

<jmh.version>1.37</jmh.version>
<uberjar.name>benchmarks</uberjar.name>
</properties>

<dependencies>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-core</artifactId>
<version>${jmh.version}</version>
</dependency>
<dependency>
<groupId>org.openjdk.jmh</groupId>
<artifactId>jmh-generator-annprocess</artifactId>
<version>${jmh.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.jgroups</groupId>
<artifactId>jgroups-raft</artifactId>
<version>${project.version}</version>
<scope>compile</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
<configuration>
<compilerVersion>${javac.target}</compilerVersion>
<source>${javac.target}</source>
<target>${javac.target}</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.1</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<finalName>${uberjar.name}</finalName>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>org.openjdk.jmh.Main</mainClass>
</transformer>
<transformer implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"/>
</transformers>
<filters>
<filter>
<!--
Shading signed JARs will fail without this.
http://stackoverflow.com/questions/999489/invalid-signature-file-when-attempting-to-run-a-jar
-->
<artifact>*:*</artifact>
<excludes>
<exclude>META-INF/*.SF</exclude>
<exclude>META-INF/*.DSA</exclude>
<exclude>META-INF/*.RSA</exclude>
</excludes>
</filter>
</filters>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
<pluginManagement>
<plugins>
<plugin>
<artifactId>maven-clean-plugin</artifactId>
<version>2.5</version>
</plugin>
<plugin>
<artifactId>maven-deploy-plugin</artifactId>
<version>2.8.1</version>
</plugin>
<plugin>
<artifactId>maven-install-plugin</artifactId>
<version>2.5.1</version>
</plugin>
<plugin>
<artifactId>maven-jar-plugin</artifactId>
<version>2.4</version>
</plugin>
<plugin>
<artifactId>maven-javadoc-plugin</artifactId>
<version>2.9.1</version>
</plugin>
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<version>2.6</version>
</plugin>
<plugin>
<artifactId>maven-site-plugin</artifactId>
<version>3.3</version>
</plugin>
<plugin>
<artifactId>maven-source-plugin</artifactId>
<version>2.2.1</version>
</plugin>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.17</version>
</plugin>
</plugins>
</pluginManagement>
</build>

</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
package org.jgroups.raft;

import org.jgroups.JChannel;
import org.jgroups.protocols.raft.FileBasedLog;
import org.jgroups.protocols.raft.RAFT;
import org.jgroups.raft.testfwk.RaftTestUtils;
import org.jgroups.util.Util;

import java.io.DataInput;
import java.io.DataOutput;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
import java.util.function.BooleanSupplier;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.Param;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.Setup;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.TearDown;
import org.openjdk.jmh.annotations.Warmup;

/**
* Benchmark the replication throughput.
*
* @author José Bolina
* @since 1.0.13
*/
@BenchmarkMode({Mode.Throughput})
@Warmup(iterations = 10, time = 5)
@Measurement(iterations = 5, time = 10)
@Fork(value = 3, jvmArgsPrepend = "-Djava.net.preferIPv4Stack=true -Djgroups.udp.ip_ttl=0")
public class DataReplicationBenchmark {

@Benchmark
public byte[] testReplication(ClusterState state) throws Exception {
return state.leader.set(state.data, 0, state.dataSize);
}

@State(Scope.Benchmark)
public static class ClusterState {

// Change the majority between values.
@Param({"3", "5"})
public int clusterSize;

// Keep a factor of 4 between values.
@Param({"256", "1024", "4096"})
public int dataSize;

// Storage implementations that write to disk can enable/disable fsync.
@Param({"false", "true"})
public boolean useFsync;

public byte[] data;

public RaftHandle[] members;

public RaftHandle leader;


@Setup
public void initialize() throws Exception {
List<String> memberList = IntStream.range(0, clusterSize)
.mapToObj(i -> Character.toString('A' + i))
.collect(Collectors.toList());

members = new RaftHandle[clusterSize];
for (int i = 0; i < clusterSize; i++) {
String name = Character.toString('A' + i);

// Utilize the default configuration shipped with jgroups-raft.
JChannel ch = new JChannel("raft.xml");
ch.name(name);

// A no-op state machine.
RaftHandle handler = new RaftHandle(ch, new EmptyStateMachine(dataSize));
RAFT raft = handler.raft();

// Default configuration.
raft.raftId(name);
raft.members(memberList);

// Fine-tune the RAFT protocol below.
raft.logClass(FileBasedLog.class.getCanonicalName());
raft.logUseFsync(useFsync);

members[i] = handler;
ch.connect("jmh-replication");
}

data = new byte[dataSize];
ThreadLocalRandom.current().nextBytes(data);

// Block until ALL members have a leader installed.
BooleanSupplier bs = () -> Arrays.stream(members)
.map(RaftHandle::raft)
.allMatch(r -> r.leader() != null);
Supplier<String> message = () -> Arrays.stream(members)
.map(RaftHandle::raft)
.map(r -> String.format("%s: %s", r.raftId(), r.leader()))
.collect(Collectors.joining(System.lineSeparator()));
assert RaftTestUtils.eventually(bs, 1, TimeUnit.MINUTES) : message.get();

for (RaftHandle member : members) {
if (member.isLeader()) {
leader = member;
break;
}
}

assert leader != null : "Leader not found";
}

@TearDown
public void tearDown() throws Exception {
for (int i = clusterSize - 1; i >= 0; i--) {
RaftHandle rh = members[i];
Util.close(rh.channel());
}

for (RaftHandle member : members) {
RaftTestUtils.deleteRaftLog(member.raft());
}
}
}

private static final class EmptyStateMachine implements StateMachine {
private static final byte[] RESPONSE = new byte[0];

private final int dataSize;

public EmptyStateMachine(int dataSize) {
this.dataSize = dataSize;
}

@Override
public byte[] apply(byte[] data, int offset, int length, boolean serialize_response) throws Exception {
if (data.length != dataSize) throw new IllegalArgumentException("Data size does not match");

return RESPONSE;
}

@Override
public void readContentFrom(DataInput in) throws Exception { }

@Override
public void writeContentTo(DataOutput out) throws Exception { }
}
}

0 comments on commit d8aedcb

Please sign in to comment.