Hash, Set чи Sorted set. Який тип даних вибрати для збереження стану онлайну користувача в Redis
Привіт, я розробляю анонімний пошук роботи та хочу зробити статистику онлайну публічною, щоб рекрутер міг побачити скільки кандидатів Senior Rust Developer було онлайн за цей місяць.
В попередній статті ми протестували різні типи даних (Hash, Set та Sorted set) для задачі збереження онлайну користувачів в Redis, щоб в наступних статтях перенести з Redis-а в PostgreSQL через BATCH UPDATE.
А в цій статті ми розглянемо швидкодію, скільки пам’яті займає кожен з типів даних та яку БД вибрати Redis, KeyDB або DragonflyDB.
Всі результати доступні в репозиторії.
Методика вибору оптимального типу даних для збереження онлайну
В попередній статті ми:
- для кожного типу даних Redis (Hash, Sorted set та Set) написали сервіс-обгортку на Go;
- налаштували Redis, KeyDB, DragonflyDB, а також запуск тестів в Docker-контейнері.
В цій статті ми:
- порівняємо швидкодію збереження онлайну мільйона користувачів для кожного з типів даних та кожної бази;
- порівняємо скільки пам’яті використовують бази для збереження мільйона, 10 та 25 мільйонів користувачів;
- порівняємо стресостійкість через 10000 паралельних вставок по 10000 записів кожна.
Швидкодія збереження онлайну
Кожен тип даних Redis має свою відповідну обгортку на Go, яка реалізує інтерфейс OnlineStorage:package research_online_redis_go
import (
"context"
)
type OnlineStorage interface {
Store(ctx context.Context, pair UserOnlinePair) error
BatchStore(ctx context.Context, pairs []UserOnlinePair) error
Count(ctx context.Context) (int64, error)
GetAndClear(ctx context.Context) ([]UserOnlinePair, error)
}Для інтерфейсу OnlineStorage ми напишемо бенчмарк benchmarkOnlineStorage:
package research_online_redis_go
import (
"context"
"testing"
"time"
"github.com/redis/go-redis/v9"
"github.com/stretchr/testify/require"
)
func benchmarkOnlineStorage(
b *testing.B,
addr string,
constructor func(client *redis.Client) OnlineStorage,
) {
b.Helper()
ctx := context.Background()
client, err := Client(ctx, addr)
require.NoError(b, err)
require.NoError(b, client.FlushDB(ctx).Err())
storage := constructor(client)
startTimestamp := time.Now().Unix()
b.ResetTimer()
for index := 0; index < b.N; index++ {
err := storage.Store(ctx, UserOnlinePair{
UserID: 1e7 + int64(index),
Timestamp: startTimestamp + int64(index),
})
require.NoError(b, err)
}
count, err := storage.Count(ctx)
require.NoError(b, err)
require.Equal(b, int64(b.N), count)
}За допомогою бенчмарку benchmarkOnlineStorage ми покриємо всі типи та всі бази:
package research_online_redis_go
import (
"testing"
"github.com/redis/go-redis/v9"
)
var hashConstructor = func(client *redis.Client) OnlineStorage {
return NewHashOnlineStorage(client)
}
func BenchmarkRedisHashOnlineStorage(b *testing.B) {
benchmarkOnlineStorage(b, "redis1:6379", hashConstructor)
}
func BenchmarkKeydbHashOnlineStorage(b *testing.B) {
benchmarkOnlineStorage(b, "keydb1:6379", hashConstructor)
}
func BenchmarkDragonflydbHashOnlineStorage(b *testing.B) {
benchmarkOnlineStorage(b, "dragonflydb1:6379", hashConstructor)
}package research_online_redis_go
import (
"testing"
"github.com/redis/go-redis/v9"
)
var sortedSetConstructor = func(client *redis.Client) OnlineStorage {
return NewSortedSetOnlineStorage(client)
}
func BenchmarkRedisSortedSetOnlineStorage(b *testing.B) {
benchmarkOnlineStorage(b, "redis1:6379", sortedSetConstructor)
}
func BenchmarkKeydbSortedSetOnlineStorage(b *testing.B) {
benchmarkOnlineStorage(b, "keydb1:6379", sortedSetConstructor)
}
func BenchmarkDragonflydbSortedSetOnlineStorage(b *testing.B) {
benchmarkOnlineStorage(b, "dragonflydb1:6379", sortedSetConstructor)
}package research_online_redis_go
import (
"testing"
"github.com/redis/go-redis/v9"
)
var (
setConstructor = func(client *redis.Client) OnlineStorage {
return NewSetOnlineStorage(client, 1800)
}
)
func BenchmarkRedisSetOnlineStorage(b *testing.B) {
benchmarkOnlineStorage(b, "redis1:6379", setConstructor)
}
func BenchmarkKeydbSetOnlineStorage(b *testing.B) {
benchmarkOnlineStorage(b, "keydb1:6379", setConstructor)
}
func BenchmarkDragonflydbSetOnlineStorage(b *testing.B) {
benchmarkOnlineStorage(b, "dragonflydb1:6379", setConstructor)
}Тепер залишається запустити бенчмарки та порівняти результати:
go test ./... -v -run=$^ -bench='Redis(Hash|SortedSet|Set)' -benchmem -benchtime=1000000x
bench-go-sequence: docker exec research-online-redis-go-app \ go test ./... -v -run=$$^ -bench='Go' -benchmem -benchtime=1000000x -count=5 \ | tee ./output/bench-go-1000000x-sequence.txt bench-redis-sequence: docker exec research-online-redis-go-app \ go test ./... -v -run=$$^ -bench='Redis(Hash|SortedSet|Set)' -benchmem -benchtime=1000000x -count=5 \ | tee ./output/bench-redis-1000000x-sequence.txt bench-keydb-sequence: docker exec research-online-redis-go-app \ go test ./... -v -run=$$^ -bench='Keydb(Hash|SortedSet|Set)' -benchmem -benchtime=1000000x -count=5 \ | tee ./output/bench-keydb-1000000x-sequence.txt bench-dragonflydb-sequence: docker exec research-online-redis-go-app \ go test ./... -v -run=$$^ -bench='Dragonflydb(Hash|SortedSet|Set)' -benchmem -benchtime=1000000x -count=5 \ | tee ./output/bench-dragonflydb-1000000x-sequence.txt bench: make bench-go-sequence bench-redis-sequence bench-keydb-sequence bench-dragonflydb-sequence benchstat ./output/bench-go-1000000x-sequence.txt benchstat ./output/bench-redis-1000000x-sequence.txt benchstat ./output/bench-keydb-1000000x-sequence.txt benchstat ./output/bench-dragonflydb-1000000x-sequence.txt
make bench
| Database name | Data structure | sequence time/op |
|---|---|---|
| Go | map[int]int | 515ns ± 9% |
| Redis | Hash | 33.5µs ± 6% |
| KeyDB | Hash | 36.9µs ± 2% |
| DragonflyDB | Hash | 44.0µs ± 2% |
| Redis | Sorted Set | 34.4µs ± 1% |
| KeyDB | Sorted Set | 38.6µs ± 1% |
| DragonflyDB | Sorted Set | 52.9µs ±15% |
| Redis | Set | 32.6µs ± 1% |
| KeyDB | Set | 36.7µs ± 1% |
| DragonflyDB | Set | 45.9µs ± 4% |
Використання пам’яті
Для збереження онлайну мільйонів у раніше описану функцію benchmarkOnlineStorage додамо паралельне збереження пачками:package research_online_redis_go
import (
"context"
"os"
"strconv"
"sync/atomic"
"testing"
"time"
"github.com/redis/go-redis/v9"
"github.com/stretchr/testify/require"
)
// ...
func benchmarkOnlineStorage(
b *testing.B,
addr string,
constructor func(client *redis.Client) OnlineStorage,
) {
// ...
var (
expectedCount = int64(b.N)
startTimestamp = time.Now().Unix()
startUserID = int64(1e7)
)
b.ResetTimer()
if os.Getenv("MODE") == "parallel" {
var (
counter = int64(0)
)
if os.Getenv("BATCH") == "" {
// ...
} else {
batch, err := strconv.ParseInt(os.Getenv("BATCH"), 10, 64)
require.NoError(b, err)
require.True(b, batch >= 1)
expectedCount *= batch
b.RunParallel(func(pb *testing.PB) {
pairs := make([]UserOnlinePair, batch)
for pb.Next() {
index := atomic.AddInt64(&counter, batch)
for i := int64(0); i < batch; i++ {
pairs[i] = UserOnlinePair{
UserID: startUserID + index + i,
Timestamp: startTimestamp + index + i,
}
}
err := storage.BatchStore(ctx, pairs)
require.NoError(b, err)
}
})
}
} else {
// ...
}
actualCount, err := storage.Count(ctx)
require.NoError(b, err)
require.Equal(b, expectedCount, actualCount)
}bench-redis-memory-25m: docker exec research-online-redis-1 redis-cli flushall docker exec -e MODE=parallel -e BATCH=10000 research-online-redis-go-app \ go test ./... -v -run=$$^ -bench='Redis(Hash)' -benchmem -benchtime=2500x -count=1 docker exec research-online-redis-1 redis-cli info memory | tee ./output/redis-memory-hash-25m.txt docker exec research-online-redis-1 redis-cli flushall docker exec -e MODE=parallel -e BATCH=10000 research-online-redis-go-app \ go test ./... -v -run=$$^ -bench='Redis(SortedSet)' -benchmem -benchtime=2500x -count=1 docker exec research-online-redis-1 redis-cli info memory | tee ./output/redis-memory-sorted-set-25m.txt docker exec research-online-redis-1 redis-cli flushall docker exec -e MODE=parallel -e BATCH=10000 research-online-redis-go-app \ go test ./... -v -run=$$^ -bench='Redis(Set)' -benchmem -benchtime=2500x -count=1 docker exec research-online-redis-1 redis-cli info memory | tee ./output/redis-memory-set-25m.txt docker exec research-online-redis-1 redis-cli flushall bench-keydb-memory-25m: # ... docker exec research-online-keydb-1 keydb-cli flushall bench-dragonflydb-memory-25m: # ... docker exec research-online-dragonflydb-1 redis-cli flushall
make bench-redis-memory-25m make bench-keydb-memory-25m make bench-dragonflydb-memory-25m cat ./output/redis-memory-hash-25m.txt cat ./output/redis-memory-sorted-set-25m.txt cat ./output/redis-memory-set-25m.txt # ...
| Database name | Data structure | Users | Memory |
|---|---|---|---|
| Redis | Hash | 1 000 000 | 62.64 MB |
| KeyDB | Hash | 1 000 000 | 63.49 MB |
| DragonflyDB | Hash | 1 000 000 | 61.51 MB |
| Redis | Hash | 10 000 000 | 727.20 MB |
| KeyDB | Hash | 10 000 000 | 728.14 MB |
| DragonflyDB | Hash | 10 000 000 | 622.59 MB |
| Redis | Hash | 25 000 000 | 1592.14 MB |
| KeyDB | Hash | 25 000 000 | 1593.27 MB |
| DragonflyDB | Hash | 25 000 000 | 1481.70 MB |
| Redis | Sorted Set | 1 000 000 | 91.09 MB |
| KeyDB | Sorted Set | 1 000 000 | 91.93 MB |
| DragonflyDB | Sorted Set | 1 000 000 | 107.87 MB |
| Redis | Sorted Set | 10 000 000 | 1011.78 MB |
| KeyDB | Sorted Set | 10 000 000 | 1012.64 MB |
| DragonflyDB | Sorted Set | 10 000 000 | 1161.64 MB |
| Redis | Sorted Set | 25 000 000 | 2303.58 MB |
| KeyDB | Sorted Set | 25 000 000 | 2304.70 MB |
| DragonflyDB | Sorted Set | 25 000 000 | 2675.25 MB |
| Redis | Set | 1 000 000 | 48.14 MB |
| KeyDB | Set | 1 000 000 | 49.02 MB |
| DragonflyDB | Set | 1 000 000 | 32.60 MB |
| Redis | Set | 10 000 000 | 469.57 MB |
| KeyDB | Set | 10 000 000 | 471.44 MB |
| DragonflyDB | Set | 10 000 000 | 297.01 MB |
| Redis | Set | 25 000 000 | 1169.33 MB |
| KeyDB | Set | 25 000 000 | 1175.45 MB |
| DragonflyDB | Set | 25 000 000 | unknown, cause store less then expected, 15276400 from 25000000 |
Стресостійкість
Для тестування стресостійкості, 10000 паралельних вставок по 10000 записів, тільки оновимо параметри з попереднього прикладу.bench-keydb-memory-10k-batch-10k: docker exec research-online-keydb-1 keydb-cli flushall docker exec -e MODE=parallel -e BATCH=10000 research-online-redis-go-app \ go test ./... -v -run=$$^ -bench='Keydb(Hash)' -benchmem -benchtime=10000x -count=1 docker exec research-online-keydb-1 keydb-cli info memory \ | tee ./output/keydb-memory-hash-10k-batch-10k.txt # ... docker exec research-online-keydb-1 keydb-cli flushall
make bench-redis-memory-10k-batch-10k make bench-keydb-memory-10k-batch-10k make bench-dragonflydb-memory-10k-batch-10k cat ./output/redis-memory-hash-10k-batch-10k.txt cat ./output/redis-memory-sorted-set-10k-batch-10k.txt cat ./output/redis-memory-set-10k-batch-10k.txt # ...
| Database name | Data structure | parallel time/op |
|---|---|---|
| Redis | Hash | 8232276 ns/op |
| KeyDB | Hash | 21357358 ns/op |
| DragonflyDB | Hash | 6716157 ns/op |
| Redis | Sorted Set | 12016807 ns/op |
| KeyDB | Sorted Set | 15114051 ns/op |
| DragonflyDB | Sorted Set | 9535106 ns/op |
| Redis | Set | 3187424 ns/op |
| KeyDB | Set | 3233770 ns/op |
| DragonflyDB | Set | unknown, cause store less then expected, 15622200 from 100000000 |
Скільки пам’яті займають 100 мільйонів:
| Database name | Data structure | Memory |
|---|---|---|
| Redis | Hash | 6.72 GB |
| KeyDB | Hash | 6.22 GB |
| DragonflyDB | Hash | 5.77 GB |
| Redis | Sorted Set | 9.00 GB |
| KeyDB | Sorted Set | 9.00 GB |
| DragonflyDB | Sorted Set | 10.44 GB |
| Redis | Set | 4.58 GB |
| KeyDB | Set | 4.59 GB |
| DragonflyDB | Set | unknown, cause store less then expected, 15622200 from 100000000 |
Висновки
Для збереження онлайну користувачів найкраще себе показав Redis з типом Hash.DevOps в пошуку роботи
В одному високонавантаженому проєкті я працював з Ростиславом, то ми разом протестували Redis, MongoDB, Aerospike та ScyllaDB, щоб знайти краще рішення для однієї бізнес-задачі. Зробили звіт для керівництва, а за результатами тестування вибрали Redis. Якщо б тестували зараз, то спробували б також KeyDB та DragonflyDB.Зараз Ростислав у пошуку роботи, тому якщо ви шукаєте кваліфікованого DevOps-а, з яким комфортно працювати, то тепер знаєте, де його знайти.
2 коментарі
Додати коментар Підписатись на коментаріВідписатись від коментарівПісля такої рекомендації я вже в очікуванні на чергу ейчарів :) :) :)
Ого, технічна стаття на Доу!