数日前、本番環境でのRedis接続の作成に失敗しました。分析プロセス中に、ServiceStack.Redisの接続作成と接続プールのメカニズムについて理解を深めました。問題分析が終わった後、学んだ知識ポイントはこの記事を通して体系的に分類されます。
接続プールからRedisClientを取得するプロセス
ビジネスプログラムでは、クライアントオブジェクトはPooledRedisClientManagerオブジェクトのGetClient()メソッドを介して取得され、ここのソースコードがエントリとして使用されます。
コードを表示
public IRedisClient GetClient()
{
RedisClient redisClient = null;
DateTime now = DateTime.Now;
for (; ; )
{
if (!this.deactiveClientQueue.TryPop(out redisClient))
{
if (this.redisClientSize >= this.maxRedisClient)
{
Thread.Sleep(3);
if (this.PoolTimeout != null && (DateTime.Now - now).TotalMilliseconds >= (double)this.PoolTimeout.Value)
{
break;
}
}
else
{
redisClient = this.CreateRedisClient();
if (redisClient != null)
{
goto Block_5;
}
}
}
else
{
if (!redisClient.HadExceptions)
{
goto Block_6;
}
List<RedisClient> obj = this.writeClients;
lock (obj)
{
this.writeClients.Remove(redisClient);
this.redisClientSize--;
}
RedisState.DisposeDeactivatedClient(redisClient);
}
}
bool flag2 = true;
if (flag2)
{
throw new TimeoutException("Redis Timeout expired. The timeout period elapsed prior to obtaining a connection from the pool. This may have occurred because all pooled connections were in use.");
}
return redisClient;
Block_5:
this.writeClients.Add(redisClient);
return redisClient;
Block_6:
redisClient.Active = true;
this.InitClient(redisClient);
return redisClient;
}
このメソッドの本体は無限ループであり、主に次の関数を実装します。
- this.deactiveClientQueueは、ConcurrentStack<RedisClient>タイプのアイドル状態のクライアントコレクションを表します。
- this.deactiveClientQueueがredisClientをポップアウトできる場合は、Block_6ブランチにジャンプします。redisClient.Activeプロパティをマークし、this.InitClient(redisClient)を実行してから、redisClientインスタンスを返します。
- this.deactiveClientQueueにポップできる要素がない場合は、最初にクライアント数の上限の判断を実行しますthis.redisClientSize> = this.maxRedisClient;
- 上限に達していない場合は、redisClient = this.CreateRedisClient();を実行します。
- 上限に達した場合は、最初に3ミリ秒スリープしてから、接続プールのタイムアウト時間がthis.PoolTimeoutを超えているかどうかをミリ秒単位で判別します。タイムアウトした場合は、直接中断してループを中断し、タイムアウトしない場合は、次のforループに進みます。
上記のプロセスは、接続プールからクライアントを取得するメインプロセスです。ここで、this.deactiveClientQueueは「クライアントプール」と同等です。this.PoolTimeoutの意味は、接続プールが使い果たされたときに呼び出し元が待機する時間であることに注意してください。
上記のプロセスは、フローチャートで次のように表されます。
新しいクライアントを作成するプロセス:CreateRedisClient()
ソースコードは次のとおりです。
コードを表示
private RedisClient CreateRedisClient()
{
if (this.redisClientSize >= this.maxRedisClient)
{
return null;
}
object obj = this.lckObj;
RedisClient result;
lock (obj)
{
if (this.redisClientSize >= this.maxRedisClient)
{
result = null;
}
else
{
Random random = new Random((int)DateTime.Now.Ticks);
RedisClient newClient = this.InitNewClient(this.RedisResolver.CreateMasterClient(random.Next(100)));
newClient.OnDispose += delegate()
{
if (!newClient.HadExceptions)
{
List<RedisClient> obj2 = this.writeClients;
lock (obj2)
{
if (!newClient.HadExceptions)
{
try
{
this.deactiveClientQueue.Push(newClient);
return;
}
catch
{
this.writeClients.Remove(newClient);
this.redisClientSize--;
RedisState.DisposeDeactivatedClient(newClient);
}
}
}
}
this.writeClients.Remove(newClient);
this.redisClientSize--;
RedisState.DisposeDeactivatedClient(newClient);
};
this.redisClientSize++;
result = newClient;
}
}
return result;
}
同時実行の考慮事項に基づいて、新しいクライアントを作成するプロセスでは、同時ロック制限、つまりロック(obj)を増やす必要があります。このとき、複数のスレッドがCreateRedisClient()メソッドに入ると、実際には1つのスレッドだけが実行され、他のスレッドはロックが解放されるのを待ってブロックします。この現象は、windbgのsyncblkおよびclrstackコマンドを使用して分析および表示できます。残りは、this.InitNewClient(this.RedisResolver.CreateMasterClient(random.Next(100)))を引き続き呼び出してオブジェクトを作成し、newClientのOnDisposeイベントに処理ロジックを追加します。ここでのOnDisposeイベントは、従来の意味でのデストラクタではなく、呼び出し元がRedisClientオブジェクトを使い切った後、つまりnewClientオブジェクトが使用された後に接続プールにリサイクルするために使用する操作であることに注意してください。異常ではない場合は、this.deactiveClientQueueスタックにプッシュします。ここで、接続プールがリサイクルおよび拡張されます。
this.InitNewClient()メソッドの解釈
これは、Id、Activeなどを含む新しく作成されたRedisClientオブジェクトの初期化であり、さらに初期化するためにthis.InitClient()を呼び出し続けます。
this.RedisResolver.CreateMasterClient()の解釈
this.redisResolverは、IRedisResolverインターフェイスタイプです。次のスクリーンショットに示すように、ソースコードには3つの実装があります。ここでは、一般的なセンチネル生産モードを分析の例として取り上げます。
RedisSentinelResolverクラスはセンチネルモードに対応しており、関連する操作のソースコードは次のとおりです。
コードを表示
public RedisClient CreateMasterClient(int desiredIndex)
{
return this.CreateRedisClient(this.GetReadWriteHost(desiredIndex), true);
}
public RedisEndpoint GetReadWriteHost(int desiredIndex)
{
return this.sentinel.GetMaster() ?? this.masters[desiredIndex % this.masters.Length];
}
public virtual RedisClient CreateRedisClient(RedisEndpoint config, bool master)
{
RedisClient result = this.ClientFactory(config);
if (master)
{
RedisServerRole redisServerRole = RedisServerRole.Unknown;
try
{
using (RedisClient redisClient = this.ClientFactory(config))
{
redisClient.ConnectTimeout = 5000;
redisClient.ReceiveTimeout = 5000;
redisServerRole = redisClient.GetServerRole();
if (redisServerRole == RedisServerRole.Master)
{
this.lastValidMasterFromSentinelAt = DateTime.UtcNow;
return result;
}
}
}
catch (Exception exception)
{
Interlocked.Increment(ref RedisState.TotalInvalidMasters);
using (RedisClient redisClient2 = this.ClientFactory(config))
{
redisClient2.ConnectTimeout = 5000;
redisClient2.ReceiveTimeout = 5000;
if (redisClient2.GetHostString() == this.lastInvalidMasterHost)
{
object obj = this.oLock;
lock (obj)
{
if (DateTime.UtcNow - this.lastValidMasterFromSentinelAt > this.sentinel.WaitBeforeForcingMasterFailover)
{
this.lastInvalidMasterHost = null;
this.lastValidMasterFromSentinelAt = DateTime.UtcNow;
RedisSentinelResolver.log.Error("Valid master was not found at '{0}' within '{1}'. Sending SENTINEL failover...".Fmt(redisClient2.GetHostString(), this.sentinel.WaitBeforeForcingMasterFailover), exception);
Interlocked.Increment(ref RedisState.TotalForcedMasterFailovers);
this.sentinel.ForceMasterFailover();
Thread.Sleep(this.sentinel.WaitBetweenFailedHosts);
redisServerRole = redisClient2.GetServerRole();
}
goto IL_16E;
}
}
this.lastInvalidMasterHost = redisClient2.GetHostString();
IL_16E:;
}
}
if (redisServerRole != RedisServerRole.Master && RedisConfig.VerifyMasterConnections)
{
try
{
Stopwatch stopwatch = Stopwatch.StartNew();
for (;;)
{
try
{
RedisEndpoint master2 = this.sentinel.GetMaster();
using (RedisClient redisClient3 = this.ClientFactory(master2))
{
redisClient3.ReceiveTimeout = 5000;
redisClient3.ConnectTimeout = this.sentinel.SentinelWorkerConnectTimeoutMs;
if (redisClient3.GetServerRole() == RedisServerRole.Master)
{
this.lastValidMasterFromSentinelAt = DateTime.UtcNow;
return this.ClientFactory(master2);
}
Interlocked.Increment(ref RedisState.TotalInvalidMasters);
}
}
catch
{
}
if (stopwatch.Elapsed > this.sentinel.MaxWaitBetweenFailedHosts)
{
break;
}
Thread.Sleep(this.sentinel.WaitBetweenFailedHosts);
}
throw new TimeoutException("Max Wait Between Sentinel Lookups Elapsed: {0}".Fmt(this.sentinel.MaxWaitBetweenFailedHosts.ToString()));
}
catch (Exception exception2)
{
RedisSentinelResolver.log.Error("Redis Master Host '{0}' is {1}. Resetting allHosts...".Fmt(config.GetHostString(), redisServerRole), exception2);
List<RedisEndpoint> list = new List<RedisEndpoint>();
List<RedisEndpoint> list2 = new List<RedisEndpoint>();
RedisClient redisClient4 = null;
foreach (RedisEndpoint redisEndpoint in this.allHosts)
{
try
{
using (RedisClient redisClient5 = this.ClientFactory(redisEndpoint))
{
redisClient5.ReceiveTimeout = 5000;
redisClient5.ConnectTimeout = RedisConfig.HostLookupTimeoutMs;
RedisServerRole serverRole = redisClient5.GetServerRole();
if (serverRole != RedisServerRole.Master)
{
if (serverRole == RedisServerRole.Slave)
{
list2.Add(redisEndpoint);
}
}
else
{
list.Add(redisEndpoint);
if (redisClient4 == null)
{
redisClient4 = this.ClientFactory(redisEndpoint);
}
}
}
}
catch
{
}
}
if (redisClient4 == null)
{
Interlocked.Increment(ref RedisState.TotalNoMastersFound);
string message = "No master found in: " + string.Join(", ", this.allHosts.Map((RedisEndpoint x) => x.GetHostString()));
RedisSentinelResolver.log.Error(message);
throw new Exception(message);
}
this.ResetMasters(list);
this.ResetSlaves(list2);
return redisClient4;
}
return result;
}
return result;
}
return result;
}
GetReadWriteHost()メソッドのロジックは次のとおりです。this.sentinel.GetMaster()によって取得されたマスターノード情報を優先的に使用します。GetMaster()が失敗した場合は、接続するマスターの既存のセットからランダムなものを選択します。
次に、CreateRedisClient()メソッドを入力します。
- まず、オブジェクトredisClientがthis.ClientFactory()ファクトリを介して作成され、カウントおよび新しいRedisClient()操作がファクトリ内に実装されます。あまり内容はありません。
- 次に、redisClient.GetServerRole()を実行します。これは、現在接続されているノードが実際にマスターロールであることをサーバーで確認することを意味します。確認された場合は、発信者に直接返されます。[クエリ要求を送信するプロセスが異常であり、特定の条件が満たされた場合、フェイルオーバー要求が開始されます。つまり、this.sentinel.ForceMasterFailover();]
- 現在接続されているノードがマスターロールでない場合は、this.sentinel.GetMaster()を複数回呼び出して、マスターノード情報を照会し、RedisClientオブジェクトを再インスタンス化します。
- タイムアウト後もマスターノードへの接続に失敗した場合は、catch例外処理プロセスに入り、this.allHostsのすべてのノードをトラバースし、対応するノードの役割を更新します。
これまでのところ、上記のプロセスを通じて、マスターノードのRedisClientオブジェクトを最終的に取得し、呼び出し元に返すことができます。
上記のプロセスでは、いくつかのメソッドの実装がより重要で複雑です。以下では、それらを1つずつ説明します。
RedisSentinelクラスのGetMaster()の実装原理の分析
呼び出し場所は非常に単純ですが、このメソッドの実装は多数あります。RedisSentinelクラスのソースコードは次のとおりです。
コードを表示
public RedisEndpoint GetMaster()
{
RedisSentinelWorker validSentinelWorker = this.GetValidSentinelWorker();
RedisSentinelWorker obj = validSentinelWorker;
RedisEndpoint result;
lock (obj)
{
string masterHost = validSentinelWorker.GetMasterHost(this.masterName);
if (this.ScanForOtherSentinels && DateTime.UtcNow - this.lastSentinelsRefresh > this.RefreshSentinelHostsAfter)
{
this.RefreshActiveSentinels();
}
result = ((masterHost != null) ? ((this.HostFilter != null) ? this.HostFilter(masterHost) : masterHost).ToRedisEndpoint(null) : null);
}
return result;
}
private RedisSentinelWorker GetValidSentinelWorker()
{
if (this.isDisposed)
{
throw new ObjectDisposedException(base.GetType().Name);
}
if (this.worker != null)
{
return this.worker;
}
RedisException innerException = null;
while (this.worker == null && this.ShouldRetry())
{
try
{
this.worker = this.GetNextSentinel();
this.GetSentinelInfo();
this.worker.BeginListeningForConfigurationChanges();
this.failures = 0;
return this.worker;
}
catch (RedisException ex)
{
if (this.OnWorkerError != null)
{
this.OnWorkerError(ex);
}
innerException = ex;
this.worker = null;
this.failures++;
Interlocked.Increment(ref RedisState.TotalFailedSentinelWorkers);
}
}
this.failures = 0;
Thread.Sleep(this.WaitBetweenFailedHosts);
throw new RedisException("No Redis Sentinels were available", innerException);
}
private RedisSentinelWorker GetNextSentinel()
{
object obj = this.oLock;
RedisSentinelWorker result;
lock (obj)
{
if (this.worker != null)
{
this.worker.Dispose();
this.worker = null;
}
int num = this.sentinelIndex + 1;
this.sentinelIndex = num;
if (num >= this.SentinelEndpoints.Length)
{
this.sentinelIndex = 0;
}
result = new RedisSentinelWorker(this, this.SentinelEndpoints[this.sentinelIndex])
{
OnSentinelError = new Action<Exception>(this.OnSentinelError)
};
}
return result;
}
private void OnSentinelError(Exception ex)
{
if (this.worker != null)
{
RedisSentinel.Log.Error("Error on existing SentinelWorker, reconnecting...");
if (this.OnWorkerError != null)
{
this.OnWorkerError(ex);
}
this.worker = this.GetNextSentinel();
this.worker.BeginListeningForConfigurationChanges();
}
}
まず、GetValidSentinelWorker()を使用してRedisSentinelWorkerオブジェクトを取得します。このメソッドの実装には、再試行メカニズムの制御が含まれ、最後にthis.GetNextSentinel()メソッドを介してthis.workerフィールド、つまりRedisSentinelWorkerオブジェクトインスタンスを提供します。
GetNextSentinel()メソッドには、同期ロック、this.worker.Dispose()の呼び出し、センチネルノードのランダムな選択、RedisSentinelWorkerオブジェクトのインスタンス化などの操作が含まれます。
次に、validSentinelWorkerをロックしてから、文字列masterHost = validSentinelWorker.GetMasterHost(this.masterName);を実行し続けます。
対応するRedisSentinelWorkerクラスのコードは次のとおりです。
コードを表示
internal string GetMasterHost(string masterName)
{
string result;
try
{
result = this.GetMasterHostInternal(masterName);
}
catch (Exception obj)
{
if (this.OnSentinelError != null)
{
this.OnSentinelError(obj);
}
result = null;
}
return result;
}
private string GetMasterHostInternal(string masterName)
{
List<string> list = this.sentinelClient.SentinelGetMasterAddrByName(masterName);
if (list.Count <= 0)
{
return null;
}
return this.SanitizeMasterConfig(list);
}
public void Dispose()
{
new IDisposable[]
{
this.sentinelClient,
this.sentinePubSub
}.Dispose(RedisSentinelWorker.Log);
}
GetMasterHost()メソッドの注意:例外が発生すると、このオブジェクトのOnSentinelErrorイベントがトリガーされます。名前が示すように、このイベントは、その後のセンチネル例外の処理に使用されます。ソースコード検索では、GetNextSentinel()メソッドのみがハンドラーをOnSentinelErrorイベントに追加します->RedisSentinelのprivatevoid OnSentinelError(Exception ex)メソッド。そして、このメソッドは内部でログを出力し、イベントthis.OnWorkerErrorをトリガーしてから、GetNextSentinel()を呼び出してthis.workerフィールドを再割り当てします。
注:Dispose()メソッドは、実際にはthis.sentinelClientとthis.sentinePubSubのログアウト操作をそれぞれ呼び出します。
関連する関数とRedisNativeClientクラスの実装
次に、RedisNativeClientクラスのSentinelGetMasterAddrByName()メソッドが呼び出されます。
このクラスのいくつかのメソッドの意味が組み合わされています。SentryクライアントのクエリコマンドをSocketを介してサーバーに送信し、返された結果を必要なRedisEndpointタイプにフォーマットします。
SendReceive()メソッドには、ソケット接続、再試行、周波数制御、タイムアウト制御などのメカニズムも含まれています。
コードを表示
public List<string> SentinelGetMasterAddrByName(string masterName)
{
List<byte[]> list = new List<byte[]>
{
Commands.Sentinel,
Commands.GetMasterAddrByName,
masterName.ToUtf8Bytes()
};
return this.SendExpectMultiData(list.ToArray()).ToStringList();
}
protected byte[][] SendExpectMultiData(params byte[][] cmdWithBinaryArgs)
{
return this.SendReceive<byte[][]>(cmdWithBinaryArgs, new Func<byte[][]>(this.ReadMultiData), (this.Pipeline != null) ? new Action<Func<byte[][]>>(this.Pipeline.CompleteMultiBytesQueuedCommand) : null, false) ?? TypeConstants.EmptyByteArrayArray;
}
protected T SendReceive<T>(byte[][] cmdWithBinaryArgs, Func<T> fn, Action<Func<T>> completePipelineFn = null, bool sendWithoutRead = false)
{
int num = 0;
Exception ex = null;
DateTime utcNow = DateTime.UtcNow;
T t;
for (;;)
{
try
{
this.TryConnectIfNeeded();
if (this.socket == null)
{
throw new RedisRetryableException("Socket is not connected");
}
if (num == 0)
{
this.WriteCommandToSendBuffer(cmdWithBinaryArgs);
}
if (this.Pipeline == null)
{
this.FlushSendBuffer();
}
else if (!sendWithoutRead)
{
if (completePipelineFn == null)
{
throw new NotSupportedException("Pipeline is not supported.");
}
completePipelineFn(fn);
t = default(T);
t = t;
break;
}
T t2 = default(T);
if (fn != null)
{
t2 = fn();
}
if (this.Pipeline == null)
{
this.ResetSendBuffer();
}
if (num > 0)
{
Interlocked.Increment(ref RedisState.TotalRetrySuccess);
}
Interlocked.Increment(ref RedisState.TotalCommandsSent);
t = t2;
}
catch (Exception ex2)
{
RedisRetryableException ex3 = ex2 as RedisRetryableException;
if ((ex3 == null && ex2 is RedisException) || ex2 is LicenseException)
{
this.ResetSendBuffer();
throw;
}
Exception ex4 = ex3 ?? this.GetRetryableException(ex2);
if (ex4 == null)
{
throw this.CreateConnectionError(ex ?? ex2);
}
if (ex == null)
{
ex = ex4;
}
if (!(DateTime.UtcNow - utcNow < this.retryTimeout))
{
if (this.Pipeline == null)
{
this.ResetSendBuffer();
}
Interlocked.Increment(ref RedisState.TotalRetryTimedout);
throw this.CreateRetryTimeoutException(this.retryTimeout, ex);
}
Interlocked.Increment(ref RedisState.TotalRetryCount);
Thread.Sleep(RedisNativeClient.GetBackOffMultiplier(++num));
continue;
}
break;
}
return t;
}
要約する
この記事では、Redis接続の作成と取得に焦点を当て、SDKの内部実装メカニズムについて深く理解しています。これに基づいて、本番環境でRedisSDKに関連する障害を分析する方が便利です。