Высокопроизводительное кэширование с помощью Redis и Go

Go – отличный язык для создания высокопроизводительных веб-приложений, а высокопроизводительные веб-приложения часто требуют централизованного кэширования.

Стандартом де-факто для централизованного кэширования является Redis, но, популярные сегодня библиотеки Go не поддерживают потоковую передачу данных, эффективных с точки зрения памяти. 

Вместо этого они предлагают []byte API, с которыми вы взаимодействуете следующим образом:

// В этом коде используется https://github.com/redis/go-redis, но те же
// ограничения действуют для Rueidis и Redigo.
func redisHandler(w http.ResponseWriter, r *http.Request) {
	ctx := context.Background()

	// Извлечение ключа из RequestURI
	key := strings.TrimLeft(r.RequestURI, "/")

	// Получить значение из Redis в виде байтового фрагмента
	val, err := rdb.Get(ctx, key).Bytes()
	if err == redis.Nil {
		http.Error(w, "Key not found in Redis", http.StatusNotFound)
		return
	} else if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}

	_, err = w.Write(val)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
	}
}

Этот паттерн работает хорошо, если вы кэшируете небольшие объекты, но если вы кэшируете объекты размером более 1 кб, []байт-ориентированные API работют не так хорошо.

В протоколе Redis нет ничего такого, что мешало бы создать потоковый API. Поэтому пердставляем redjet, библиотеку Redis, ориентированную на производительность.

С помощью redjet вы можете написать приведенный выше код следующим образом:

func redisHandler(w http.ResponseWriter, r *http.Request) {
	ctx := context.Background()

	// Извлечение ключа из RequestURI
	key := strings.TrimLeft(r.RequestURI, "/")

	// Передаем значение непосредственно из Redis в ответ.
	_, err := rdb.Command("GET", key).WriteTo(w)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
	}
}

Код стал проще и эффективнее.

Потоковая запись

Популярные библиотеки Redis страдают от одной и той же проблемы, когда дело доходит до записи значений в Redis. Они требуют, чтобы вы хранили все значение в памяти в виде []byte, прежде чем отправить его .

С помощью redjet вы можете передавать значения в Redis следующим образом:

fi := strings.NewReader("Содержимое файла")

err := rdb.Command("SET", "key", fi).Ok()
// обработка ошибки

Здесь есть важная оговорка. В протоколе Redis значения имеют префикс длины, поэтому мы не можем передавать потоковую запись через io.Reader. Я предполагаю, что это одна из основных причин, почему популярные библиотеки не поддерживают потоковую запись.

Чтобы обойти это, redjet необходим redjet.LenReader, который определяется как:

type LenReader interface {
	Len() int
	io.Reader
}

и может быть создаы с помощью redjet.NewLenReader:

Удобно, что некоторые типы в стандартной библиотеке, такие как bytes.Reader, strings.Reader и bytes.Buffer, неявно реализуют redjet.LenReader.

Бенчмарки

Redjetс сравнили с бенчмарками:

Если рассматривать чтения размером 1 кб, то вот результаты:

Время на операцию

Librarysec/opvs base
redjet1.302µ ± 2%
redigo1.802µ ± 1%+38.42%
go-redis1.713µ ± 3%+31.58%
rueidis1.645µ ± 1%+26.35%

Bandwidth

LibraryB/svs base
redjet750.4Mi ± 2%
redigo542.1Mi ± 1%-27.76%
go-redis570.3Mi ± 3%-24.01%
rueidis593.8Mi ± 1%-20.87%

Memory Allocation

LibraryB/opvs base
redjet0.000Ki ± 0%
redigo1.039Ki ± 0%?
go-redis1.392Ki ± 0%?
rueidis1.248Ki ± 1%?

Allocations per Operation

Libraryallocs/opvs base
redjet0.000 ± 0%
redigo3.000 ± 0%?
go-redis4.000 ± 0%?
rueidis2.000 ± 0%?

Все результаты бенчмарков доступны здесь.

+1
0
+1
2
+1
0
+1
0
+1
0

Ответить

Ваш адрес email не будет опубликован. Обязательные поля помечены *