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 nameData structuresequence time/op
Gomap[int]int515ns ± 9%
RedisHash33.5µs ± 6%
KeyDBHash36.9µs ± 2%
DragonflyDBHash44.0µs ± 2%
RedisSorted Set34.4µs ± 1%
KeyDBSorted Set38.6µs ± 1%
DragonflyDBSorted Set52.9µs ±15%
RedisSet32.6µs ± 1%
KeyDBSet36.7µs ± 1%
DragonflyDBSet45.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 nameData structureUsersMemory
RedisHash1 000 00062.64 MB
KeyDBHash1 000 00063.49 MB
DragonflyDBHash1 000 00061.51 MB
RedisHash10 000 000727.20 MB
KeyDBHash10 000 000728.14 MB
DragonflyDBHash10 000 000622.59 MB
RedisHash25 000 0001592.14 MB
KeyDBHash25 000 0001593.27 MB
DragonflyDBHash25 000 0001481.70 MB
RedisSorted Set1 000 00091.09 MB
KeyDBSorted Set1 000 00091.93 MB
DragonflyDBSorted Set1 000 000107.87 MB
RedisSorted Set10 000 0001011.78 MB
KeyDBSorted Set10 000 0001012.64 MB
DragonflyDBSorted Set10 000 0001161.64 MB
RedisSorted Set25 000 0002303.58 MB
KeyDBSorted Set25 000 0002304.70 MB
DragonflyDBSorted Set25 000 0002675.25 MB
RedisSet1 000 00048.14 MB
KeyDBSet1 000 00049.02 MB
DragonflyDBSet1 000 00032.60 MB
RedisSet10 000 000469.57 MB
KeyDBSet10 000 000471.44 MB
DragonflyDBSet10 000 000297.01 MB
RedisSet25 000 0001169.33 MB
KeyDBSet25 000 0001175.45 MB
DragonflyDBSet25 000 000unknown, 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 nameData structureparallel time/op
RedisHash8232276 ns/op
KeyDBHash21357358 ns/op
DragonflyDBHash6716157 ns/op
RedisSorted Set12016807 ns/op
KeyDBSorted Set15114051 ns/op
DragonflyDBSorted Set9535106 ns/op
RedisSet3187424 ns/op
KeyDBSet3233770 ns/op
DragonflyDBSetunknown, cause store less then expected, 15622200 from 100000000
Практичне застосування: якщо захочете накопичувати онлайн на стороні Go, щоб потім пачками вставляти в Redis, то будете знати, скільки це займає часу.
Скільки пам’яті займають 100 мільйонів:
Database nameData structureMemory
RedisHash6.72 GB
KeyDBHash6.22 GB
DragonflyDBHash5.77 GB
RedisSorted Set9.00 GB
KeyDBSorted Set9.00 GB
DragonflyDBSorted Set10.44 GB
RedisSet4.58 GB
KeyDBSet4.59 GB
DragonflyDBSetunknown, cause store less then expected, 15622200 from 100000000

Висновки

Для збереження онлайну користувачів найкраще себе показав Redis з типом Hash.

DevOps в пошуку роботи

В одному високонавантаженому проєкті я працював з Ростиславом, то ми разом протестували Redis, MongoDB, Aerospike та ScyllaDB, щоб знайти краще рішення для однієї бізнес-задачі. Зробили звіт для керівництва, а за результатами тестування вибрали Redis. Якщо б тестували зараз, то спробували б також KeyDB та DragonflyDB.

Зараз Ростислав у пошуку роботи, тому якщо ви шукаєте кваліфікованого DevOps-а, з яким комфортно працювати, то тепер знаєте, де його знайти.

Епілог

Дякую за увагу, наступна стаття буде про варіанти вставки онлайну в PostgreSQL. Якщо сподобалась ця стаття, то поставте зірочку репозиторію, бо для мене це додаткова мотивація писати ще.

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті

👍ПодобаєтьсяСподобалось17
До обраногоВ обраному3
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Після такої рекомендації я вже в очікуванні на чергу ейчарів :) :) :)

Ого, технічна стаття на Доу!

Підписатись на коментарі