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 коментарі
Додати коментар Підписатись на коментаріВідписатись від коментарів