3

最近、データ フィードと動的に生成されたイメージ (6k rpm スループット) を提供する MVC アプリケーションを v3.9.67 ServiceStack.Redis クライアントから最新の StackExchange.Redis クライアント (v1.0.450) に切り替えましたが、パフォーマンスが低下したり、パフォーマンスが低下したりしています。新しい例外。

私たちの Redis インスタンスは S4 レベル (13GB) で、CPU はほぼ一定の 45% 程度を示し、ネットワーク帯域幅はかなり低く見えます。Azure portal の取得/設定グラフを解釈する方法は完全にはわかりませんが、約 100 万回の取得と 100,000 セットを示しています (これは 5 分刻みのようです)。

クライアント ライブラリの切り替えは簡単で、現在も v3.9 ServiceStack JSON シリアライザーを使用しているため、クライアント ライブラリのみが変更されています。

New Relic を使用した外部モニタリングでは、平均応答時間が ServiceStack ライブラリと StackExchange ライブラリの間で約 200 ミリ秒から約 280 ミリ秒に増加していることが明確に示されています (StackExchange の方が遅い)。

次の行に沿ったメッセージで多数の例外を記録しました。

GET フィード チャネルの実行タイムアウト:ag177kxj_egeo-_nek0cew、inst: 12、mgr: 非アクティブ、キュー: 30、qu=0、qs=30、qc=0、wr=0/0、in=0/0

これは、送信されたが Redis からの応答がないキューに多数のコマンドがあり、タイムアウトを超える長時間実行されているコマンドが原因である可能性があることを意味すると理解しています。これらのエラーは、データ サービスの 1 つの背後にある SQL データベースがバックアップされている期間に表示されたので、おそらくそれが原因でしたか? そのデータベースをスケールアウトして負荷を軽減した後、このエラーはそれほど多くは見られませんでしたが、DB クエリは .Net で発生しているはずであり、それがどのように redis コマンドまたは接続を保持するのかわかりません。

また、今朝の短い期間 (数分) で、次のようなメッセージを含む大量のエラーを記録しました。

この操作を処理するための接続がありません: SETEX feed-channels:vleggqikrugmxeprwhwc2a:last-retry

ServiceStack ライブラリで一時的な接続エラーに慣れていましたが、これらの例外メッセージは通常次のようなものでした。

接続できません: sPort: 63980

SE.Redis がバックグラウンドで接続とコマンドを再試行する必要があるという印象を受けました。独自の再試行ポリシーで SE.Redis を介して呼び出しをラップする必要がありますか? おそらく、別のタイムアウト値の方が適切でしょう (ただし、どの値を使用すればよいかわかりません)。

Redis 接続文字列は次のパラメーターを設定します: abortConnect=false,syncTimeout=2000,ssl=true. のシングルトン インスタンスConnectionMultiplexerと一時的なインスタンスを使用しますIDatabase

Redis の使用の大部分は Cache クラスを経由します。実装の重要な部分を以下に示します。

通常、キーは 10 ~ 30 程度の文字列です。値は大部分がスカラーまたは適度に小さいシリアル化されたオブジェクト セット (通常は数百バイトから数 kB) ですが、キャッシュには jpg 画像も格納するため、データの大きなチャンクは数百 kB から数 MB になります。

おそらく、小さな値と大きな値には異なるマルチプレクサを使用し、大きな値にはタイムアウトを長くする必要がありますか? または、1 つが機能停止した場合に備えて、複数のマルチプレクサを結合しますか?

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;
    }
}
4

1 に答える 1