diff --git a/README.md b/README.md new file mode 100644 index 0000000..e0027a7 --- /dev/null +++ b/README.md @@ -0,0 +1,134 @@ +# FireflyDB + +FireflyDB is a fast, thread-safe, JVM-based key-value storage engine with microsecond latency. FireflyDB is 20x faster +for reads and 10x faster for writes than [Bitcask](https://github.com/basho/bitcask), which has a similar architecture. + +FireflyDB is hash-based and gives up range queries to achieve high throughput and low latency. As a result, it is about +100x faster at writes than [LevelDB](https://github.com/google/leveldb) (Google) +and [RocksDB](https://github.com/facebook/rocksdb) (Facebook). + +FireflyDB relies on sensible defaults and does not expose many configuration options. FireflyDB is designed with +educated tradeoffs to achieve high performance: + +1. All the keys must fit in memory. This is a tradeoff with all hash-based storage engines. Even with the largest key + size of 32KB, FireflyDB can store 32,000+ keys per 1GB of memory. +2. FireflyDB does not support range queries. +3. Maximum key size is 32768 bytes or 32KB. +4. Maximum value size is 2,147,483,647 bytes or 2.14 GB. + +## Installation + +### Maven + +```xml + + + com.sahilbondre + fireflydb + 0.1.0 + +``` + +### Gradle + +```gradle +implementation 'com.sahilbondre:fireflydb:0.1.0' +``` + +## API + +```java +FireflyDB fireflyDB=FireflyDB.getInstance("path/to/db"); + fireflyDB.start(); + +// Write + byte[]key="testKey".getBytes(); + byte[]value="testValue".getBytes(); + + fireflyDB.put(key,value); + +// Read + byte[]result=fireflyDB.get(key); + +// Compaction +// FireflyDB will compact automatically but can be triggered on demand. + fireflyDB.compact(); + + fireflyDB.stop(); +``` + +## Benchmarks + +``` +iterations: 100,000 +cpu: 1 +memory: 1GB +key-size: 8 bytes +value-size: 100 bytes +``` + +### Random Write Test + +Test: Generate a random key and value and write it to the database. + +![write-test](./docs/write-test.png) + +| Database | Avg Time (microseconds) | P90 Latency (microseconds) | +|-----------|-------------------------|----------------------------| +| In-Memory | 0.53 | 1 | +| LevelDB | 445.94 | 811 | +| Bitcask | 71.33 | 48 | +| RocksDB | 568.60 | 872 | +| FireflyDB | 7.10 | 5 | + +### Random Read Test + +Test: Pick a random key from the ones written in the previous test and read it from the database. + +![read-test](./docs/read-test.png) + +| Database | Avg Time (microseconds) | P90 Latency (microseconds) | +|-----------|-------------------------|----------------------------| +| In-Memory | 0.49 | 1 | +| LevelDB | 1.55 | 2 | +| Bitcask | 108.03 | 62 | +| RocksDB | 0.94 | 2 | +| FireflyDB | 4.97 | 4 | + +### Alternating Read-Write Test + +Test: Perform a read and write operation alternately. + +![alternating-read-write-test](./docs/rw-test.png) + +| Database | Avg Time (microseconds) | P90 Latency (microseconds) | +|-------------------|-------------------------|----------------------------| +| In-Memory (read) | 0.61 | 1 | +| In-Memory (write) | 0.57 | 1 | +| LevelDB (read) | 3.43 | 5 | +| LevelDB (write) | 441.38 | 814 | +| Bitcask (read) | 120.15 | 60 | +| Bitcask (write) | 66.78 | 57 | +| RocksDB (read) | 4.54 | 7 | +| RocksDB (write) | 567.14 | 971 | +| FireflyDB (read) | 3.89 | 4 | +| FireflyDB (write) | 3.91 | 4 | + +## Potential Improvements + +- [ ] Add an explicit delete operation. +- [ ] Expose compaction size as a configuration option. +- [ ] Expose compaction interval as a configuration option. +- [ ] Allow larger key size as a configuration option. +- [ ] Expose read only mode. + +## Contributing + +Pull requests are welcome. For major changes, please open an issue first +to discuss what you would like to change. + +Please make sure to update tests as appropriate. + +## License + +[Apache 2.0](https://raw.githubusercontent.com/godcrampy/fireflydb/master/LICENSE) diff --git a/benchmarks/firefly/.gitignore b/benchmarks/firefly/.gitignore new file mode 100644 index 0000000..b425f09 --- /dev/null +++ b/benchmarks/firefly/.gitignore @@ -0,0 +1,35 @@ +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### IntelliJ IDEA ### +.idea/ +*.iws +*.iml +*.ipr + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/benchmarks/firefly/Dockerfile b/benchmarks/firefly/Dockerfile new file mode 100644 index 0000000..9bfa555 --- /dev/null +++ b/benchmarks/firefly/Dockerfile @@ -0,0 +1,7 @@ +FROM eclipse-temurin:17-jre-jammy + +WORKDIR /app/firefly + +COPY ./target/benchmark-1.0-SNAPSHOT-jar-with-dependencies.jar /app/firefly/ + +CMD ["java", "-jar", "benchmark-1.0-SNAPSHOT-jar-with-dependencies.jar"] diff --git a/benchmarks/firefly/build_image.sh b/benchmarks/firefly/build_image.sh new file mode 100755 index 0000000..2756d9a --- /dev/null +++ b/benchmarks/firefly/build_image.sh @@ -0,0 +1,3 @@ +mvn clean compile assembly:single + +docker build -t fireflydb-benchmark-firefly . diff --git a/benchmarks/firefly/delete_image.sh b/benchmarks/firefly/delete_image.sh new file mode 100644 index 0000000..df03b8d --- /dev/null +++ b/benchmarks/firefly/delete_image.sh @@ -0,0 +1 @@ +docker rmi fireflydb-benchmark-firefly diff --git a/benchmarks/firefly/docker-compose.yml b/benchmarks/firefly/docker-compose.yml new file mode 100644 index 0000000..eac7b1b --- /dev/null +++ b/benchmarks/firefly/docker-compose.yml @@ -0,0 +1,9 @@ +version: '3' +services: + fireflydb-benchmark-bitcask: + image: fireflydb-benchmark-firefly + deploy: + resources: + limits: + cpus: '1' + memory: '1G' diff --git a/benchmarks/firefly/pom.xml b/benchmarks/firefly/pom.xml new file mode 100644 index 0000000..dac145e --- /dev/null +++ b/benchmarks/firefly/pom.xml @@ -0,0 +1,46 @@ + + + 4.0.0 + + com.sahilbondre.fireflydb + benchmark + 1.0-SNAPSHOT + + + 17 + 17 + UTF-8 + + + + + com.sahilbondre + fireflydb + 0.1.0 + + + + + + + + org.apache.maven.plugins + maven-assembly-plugin + 3.6.0 + + + + com.sahilbondre.fireflydb.benchmark.Main + + + + jar-with-dependencies + + + + + + + \ No newline at end of file diff --git a/benchmarks/firefly/src/main/java/com/sahilbondre/fireflydb/benchmark/Main.java b/benchmarks/firefly/src/main/java/com/sahilbondre/fireflydb/benchmark/Main.java new file mode 100644 index 0000000..96d20f5 --- /dev/null +++ b/benchmarks/firefly/src/main/java/com/sahilbondre/fireflydb/benchmark/Main.java @@ -0,0 +1,168 @@ +package com.sahilbondre.fireflydb.benchmark; + +import com.sahilbondre.firefly.FireflyDB; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Random; +import java.util.logging.Logger; + +public class Main { + private static final String TEST_FOLDER = "src/test/resources/test_folder"; + private static final int ITERATIONS = 100000; + private static final int KEY_LENGTH = 8; + private static final int VALUE_LENGTH = 100; + private static final Logger logger = Logger.getLogger(Main.class.getName()); + + private static final FireflyDB fireflyDB = FireflyDB.getInstance(TEST_FOLDER); + + + + public static void main(String[] args) throws IOException { + Files.createDirectories(Paths.get(TEST_FOLDER)); + + fireflyDB.start(); + + logger.info("Starting benchmark..."); + logger.info("Iterations: " + ITERATIONS); + logger.info("Key length: " + KEY_LENGTH); + logger.info("Value length: " + VALUE_LENGTH); + + logger.info("Starting writes..."); + + long[] writeTimes = new long[ITERATIONS]; + + List availableKeys = new ArrayList<>(); + + long startTime = System.nanoTime(); + for (int i = 0; i < ITERATIONS; i++) { + byte[] key = getRandomBytes(KEY_LENGTH); + byte[] value = getRandomBytes(VALUE_LENGTH); + + long writeTime = saveKeyValuePairAndGetTime(key, value); + availableKeys.add(key); + writeTimes[i] = writeTime; + } + long totalTime = (System.nanoTime() - startTime) / 1000; // Convert nanoseconds to microseconds + logger.info("Total time for writes: " + totalTime + " mus"); + + double averageWriteTime = 0; + for (long writeTime : writeTimes) { + averageWriteTime += writeTime; + } + averageWriteTime /= ITERATIONS; + + logger.info("Average write latency: " + averageWriteTime + " mus"); + + // Calculate p90 write latency + // Sort write times + Arrays.sort(writeTimes); + long p90WriteTime = writeTimes[(int) (ITERATIONS * 0.9)]; + logger.info("p90 write latency: " + p90WriteTime + " mus"); + + // Benchmark reads + logger.info("\nStarting reads..."); + + long[] readTimes = new long[ITERATIONS]; + + startTime = System.nanoTime(); + for (int i = 0; i < ITERATIONS; i++) { + // Get a random key from the list of available keys + byte[] key = availableKeys.get(new Random().nextInt(availableKeys.size())); + + long readTime = getKeyValuePairAndGetTime(key); + readTimes[i] = readTime; + } + totalTime = (System.nanoTime() - startTime) / 1000; // Convert nanoseconds to microseconds + logger.info("Total time for reads: " + totalTime + " mus"); + + double averageReadTime = 0; + for (long readTime : readTimes) { + averageReadTime += readTime; + } + averageReadTime /= ITERATIONS; + + logger.info("Average read latency: " + averageReadTime + " mus"); + + // Calculate p90 read latency + // Sort read times + Arrays.sort(readTimes); + long p90ReadTime = readTimes[(int) (ITERATIONS * 0.9)]; + logger.info("p90 read latency: " + p90ReadTime + " mus"); + + + // Benchmark reads and writes + logger.info("\nStarting reads and writes..."); + + + startTime = System.nanoTime(); + for (int i = 0; i < ITERATIONS; i++) { + byte[] writeKey = getRandomBytes(KEY_LENGTH); + byte[] writeValue = getRandomBytes(VALUE_LENGTH); + + long writeTime = saveKeyValuePairAndGetTime(writeKey, writeValue); + + availableKeys.add(writeKey); + + byte[] readKey = availableKeys.get(new Random().nextInt(availableKeys.size())); + + long readTime = getKeyValuePairAndGetTime(readKey); + + writeTimes[i] = writeTime; + readTimes[i] = readTime; + } + totalTime = (System.nanoTime() - startTime) / 1000; // Convert nanoseconds to microseconds + logger.info("Total time for reads and writes: " + totalTime + " mus"); + + averageReadTime = 0; + for (long readTime : readTimes) { + averageReadTime += readTime; + } + averageReadTime /= ITERATIONS; + + averageWriteTime = 0; + for (long writeTime : writeTimes) { + averageWriteTime += writeTime; + } + averageWriteTime /= ITERATIONS; + + logger.info("Average read latency: " + averageReadTime + " mus"); + logger.info("Average write latency: " + averageWriteTime + " mus"); + + // Calculate p90 read latency + // Sort read times + Arrays.sort(readTimes); + + // Calculate p90 write latency + // Sort write times + Arrays.sort(writeTimes); + + p90ReadTime = readTimes[(int) (ITERATIONS * 0.9)]; + logger.info("p90 read latency: " + p90ReadTime + " mus"); + + p90WriteTime = writeTimes[(int) (ITERATIONS * 0.9)]; + logger.info("p90 write latency: " + p90WriteTime + " mus"); + } + + private static byte[] getRandomBytes(int length) { + byte[] bytes = new byte[length]; + new Random().nextBytes(bytes); + return bytes; + } + + private static long saveKeyValuePairAndGetTime(byte[] key, byte[] value) throws IOException { + long startTime = System.nanoTime(); + fireflyDB.set(key, value); + return (System.nanoTime() - startTime) / 1000; // Convert nanoseconds to microseconds + } + + private static long getKeyValuePairAndGetTime(byte[] key) throws IOException { + long startTime = System.nanoTime(); + fireflyDB.get(key); + return (System.nanoTime() - startTime) / 1000; // Convert nanoseconds to microseconds + } +} diff --git a/docs/read-test.png b/docs/read-test.png new file mode 100644 index 0000000..71e4207 Binary files /dev/null and b/docs/read-test.png differ diff --git a/docs/rw-test.png b/docs/rw-test.png new file mode 100644 index 0000000..3dad256 Binary files /dev/null and b/docs/rw-test.png differ diff --git a/docs/write-test.png b/docs/write-test.png new file mode 100644 index 0000000..4d9d9af Binary files /dev/null and b/docs/write-test.png differ