StackExchange.Redis в Azure выдает тайм-аут при выполнении получения и отсутствии доступных исключений для подключения

Недавно я переключил приложение 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;
    }
}

person quentin-starin    schedule 20.05.2015    source источник


Ответы (1)


Несколько рекомендаций. - Вы можете использовать разные мультиплексоры с разными значениями времени ожидания для разных типов ключей и значений http://azure.microsoft.com/en-us/documentation/articles/cache-faq/ — убедитесь, что вы не привязаны к сети на клиенте и сервере. если вы находитесь на сервере, перейдите на более высокий SKU с большей пропускной способностью. Прочтите этот пост для получения более подробной информации http://azure.microsoft.com/blog/2015/02/10/Investigating-timeout-исключения-в-стекеexchange-redis-for-azure-redis-cache/

person pranav rastogi    schedule 22.05.2015
comment
Экземпляры наших облачных сервисов — это экземпляры D3 (4 ядра, мы запускаем 3 из них) и используют общую пропускную способность сети около 50–100 Мбит/с, что не должно перегружать клиента (трудно найти достоверную информацию о том, каким должно быть максимальное значение, но то, что я нашел, говорит о 100 Мбит / с на ядро) и, конечно же, не должно насыщать наш экземпляр Redis. Знаете ли вы, следует ли увеличивать параметр OperationTimeout, если большие значения занимают слишком много времени (по сравнению с параметром ResponseTimeout)? - person quentin-starin; 22.05.2015
comment
ConnectTimeout и ConnectRetry azure.microsoft.com/en-us/documentation/articles/cache-faq/ Вы можете использовать программу Bandwidth Monitor для проверки пропускной способности, которую вы получаете на клиенте. github.com/JonCole/SampleCode/tree/master/BandWidthMonitor - person pranav rastogi; 23.05.2015