Недавно я переключил приложение MVC, которое обслуживает каналы данных и динамически генерируемые изображения (пропускная способность 6 000 об/мин) с клиента ServiceStack.Redis версии 3.9.67 на последнюю версию клиента StackExchange.Redis (версия 1.0.450). новые исключения.
Наш экземпляр Redis имеет уровень S4 (13 ГБ), ЦП показывает довольно постоянные 45% или около того, а пропускная способность сети кажется довольно низкой. Я не совсем уверен, как интерпретировать график получения/набора на нашем портале Azure, но он показывает нам около 1 млн получений и 100 тыс. наборов (кажется, что это может быть с шагом в 5 минут).
Переключение клиентской библиотеки было простым, и мы все еще используем сериализатор JSON ServiceStack версии 3.9, поэтому клиентская библиотека была единственной изменяемой частью.
Наш внешний мониторинг с New Relic ясно показывает, что наше среднее время отклика увеличивается с примерно 200 мс до примерно 280 мс между библиотеками ServiceStack и StackExchange (StackExchange медленнее) без каких-либо других изменений.
Мы зафиксировали ряд исключений с сообщениями следующего содержания:
Тайм-аут при выполнении каналов GET: ag177kxj_egeo-_nek0cew, inst: 12, mgr: Inactive, queue: 30, qu=0, qs=30, qc=0, wr=0/0, in=0/0
Я понимаю, что это означает, что в очереди есть несколько команд, которые были отправлены, но нет ответа от Redis, и что это может быть вызвано длительным выполнением команд, которые превышают тайм-аут. Эти ошибки появлялись в период, когда наша база данных sql за одной из наших служб данных выполнялась резервным копированием, так что, возможно, это было причиной? После масштабирования этой базы данных для уменьшения нагрузки мы не видели больше этой ошибки, но запрос к БД должен выполняться в .Net, и я не понимаю, как это может задержать команду или соединение redis.
Мы также зафиксировали сегодня утром большое количество ошибок за короткий период (несколько минут) с сообщениями типа:
Нет доступного соединения для обслуживания этой операции: SETEX feed-channels:vleggqikrugmxeprwhwc2a:last-retry
Мы привыкли к временным ошибкам соединения с библиотекой ServiceStack, и эти сообщения об исключениях обычно были такими:
Не удается подключиться: спорт: 63980
У меня сложилось впечатление, что SE.Redis должен для меня повторять подключения и команды в фоновом режиме. Нужно ли мне по-прежнему оборачивать наши вызовы через SE.Redis в собственную политику повторных попыток? Возможно, более подходящими были бы другие значения времени ожидания (хотя я не уверен, какие значения использовать)?
Наша строка подключения Redis устанавливает следующие параметры: abortConnect=false,syncTimeout=2000,ssl=true
. Мы используем одноэлементный экземпляр ConnectionMultiplexer
и переходные экземпляры IDatabase
.
Подавляющее большинство нашего использования Redis проходит через класс Cache, и важные части реализации приведены ниже, на случай, если мы делаем что-то глупое, что вызывает у нас проблемы.
Наши ключи обычно состоят из 10-30 или около того строк символов. Значения в основном являются скалярными или достаточно небольшими сериализованными наборами объектов (как правило, от сотен байт до нескольких КБ), хотя мы также храним изображения в формате jpg в кэше, поэтому большой объем данных составляет от пары сотен КБ до пары МБ.
Возможно, мне следует использовать разные мультиплексоры для малых и больших значений, возможно, с более длительными тайм-аутами для больших значений? Или пара/несколько мультиплексоров на случай, если один из них заглохнет?
public class Cache : ICache
{
private readonly IDatabase _redis;
public Cache(IDatabase redis)
{
_redis = redis;
}
// storing this placeholder value allows us to distinguish between a stored null and a non-existent key
// while only making a single call to redis. see Exists method.
static readonly string NULL_PLACEHOLDER = "$NULL_VALUE$";
// this is a dictionary of https://github.com/StephenCleary/AsyncEx/wiki/AsyncLock
private static readonly ILockCache _locks = new LockCache();
public T GetOrSet<T>(string key, TimeSpan cacheDuration, Func<T> refresh) {
T val;
if (!Exists(key, out val)) {
using (_locks[key].Lock()) {
if (!Exists(key, out val)) {
val = refresh();
Set(key, val, cacheDuration);
}
}
}
return val;
}
private bool Exists<T>(string key, out T value) {
value = default(T);
var redisValue = _redis.StringGet(key);
if (redisValue.IsNull)
return false;
if (redisValue == NULL_PLACEHOLDER)
return true;
value = typeof(T) == typeof(byte[])
? (T)(object)(byte[])redisValue
: JsonSerializer.DeserializeFromString<T>(redisValue);
return true;
}
public void Set<T>(string key, T value, TimeSpan cacheDuration)
{
if (value.IsDefaultForType())
_redis.StringSet(key, NULL_PLACEHOLDER, cacheDuration);
else if (typeof (T) == typeof (byte[]))
_redis.StringSet(key, (byte[])(object)value, cacheDuration);
else
_redis.StringSet(key, JsonSerializer.SerializeToString(value), cacheDuration);
}
public async Task<T> GetOrSetAsync<T>(string key, Func<T, TimeSpan> getSoftExpire, TimeSpan additionalHardExpire, TimeSpan retryInterval, Func<Task<T>> refreshAsync) {
var softExpireKey = key + ":soft-expire";
var lastRetryKey = key + ":last-retry";
T val;
if (ShouldReturnNow(key, softExpireKey, lastRetryKey, retryInterval, out val))
return val;
using (await _locks[key].LockAsync()) {
if (ShouldReturnNow(key, softExpireKey, lastRetryKey, retryInterval, out val))
return val;
Set(lastRetryKey, DateTime.UtcNow, additionalHardExpire);
try {
var newVal = await refreshAsync();
var softExpire = getSoftExpire(newVal);
var hardExpire = softExpire + additionalHardExpire;
if (softExpire > TimeSpan.Zero) {
Set(key, newVal, hardExpire);
Set(softExpireKey, DateTime.UtcNow + softExpire, hardExpire);
}
val = newVal;
}
catch (Exception ex) {
if (val == null)
throw;
}
}
return val;
}
private bool ShouldReturnNow<T>(string valKey, string softExpireKey, string lastRetryKey, TimeSpan retryInterval, out T val) {
if (!Exists(valKey, out val))
return false;
var softExpireDate = Get<DateTime?>(softExpireKey);
if (softExpireDate == null)
return true;
// value is in the cache and not yet soft-expired
if (softExpireDate.Value >= DateTime.UtcNow)
return true;
var lastRetryDate = Get<DateTime?>(lastRetryKey);
// value is in the cache, it has soft-expired, but it's too soon to try again
if (lastRetryDate != null && DateTime.UtcNow - lastRetryDate.Value < retryInterval) {
return true;
}
return false;
}
}