Análisis de código fuente de ServiceStack.Redis (conexión y grupo de conexiones)

Hace unos días, hubo una falla en la creación de conexiones de Redis en el entorno de producción.Durante el proceso de análisis, comprendí mejor la creación de conexiones y el mecanismo de agrupación de conexiones de ServiceStack.Redis. Una vez finalizado el análisis del problema, los puntos de conocimiento aprendidos se clasifican sistemáticamente a través de este artículo.

El proceso de obtener RedisClient del grupo de conexiones

En el programa comercial, el objeto de cliente se obtiene a través del método GetClient() del objeto PooledRedisClientManager, y el código fuente aquí se usa como entrada:

Ver código

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

El cuerpo principal de este método es un bucle infinito, que implementa principalmente estas funciones:

  • this.deactiveClientQueue representa la colección de clientes inactiva, que es del tipo ConcurrentStack<RedisClient>.
  • Cuando this.deactiveClientQueue pueda mostrar redisClient, salte a la rama Block_6: marque la propiedad redisClient.Active, ejecute this.InitClient(redisClient) y luego devuelva la instancia de redisClient.
  • Cuando this.deactiveClientQueue no tiene elementos que se puedan abrir, primero realice el juicio del límite superior de la cantidad de Clientes this.redisClientSize >= this.maxRedisClient;
    • Si no se alcanza el límite superior, ejecute redisClient = this.CreateRedisClient();
    • Si se alcanza el límite superior, primero suspenda durante 3 milisegundos y luego determine si se excedió el tiempo de espera del grupo de conexiones this.PoolTimeout, en milisegundos. Si se agota el tiempo, interrumpa directamente para interrumpir el bucle y, si no se agota, continúe con el siguiente bucle for.

El proceso anterior es el proceso principal para obtener el Cliente del grupo de conexiones, donde this.deactiveClientQueue es equivalente al "Grupo de clientes". Cabe señalar que el significado de this.PoolTimeout es el tiempo que la persona que llama espera cuando se agota el grupo de conexiones.

El proceso anterior se representa mediante un diagrama de flujo como:

El proceso de creación de un nuevo Cliente: CreateRedisClient()

El código fuente es el siguiente:

Ver código

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

Según las consideraciones de simultaneidad, el proceso de creación de un nuevo Cliente debe aumentar el límite de bloqueo simultáneo, es decir, en el bloqueo (obj). En este momento, si varios subprocesos ingresan al método CreateRedisClient(), solo se ejecuta realmente un subproceso y otros subprocesos se bloquean esperando que se libere el bloqueo. Este fenómeno se puede analizar y visualizar a través de los comandos syncblk y clrstack de windbg. El resto es seguir llamando a this.InitNewClient(this.RedisResolver.CreateMasterClient(random.Next(100))) para crear objetos y agregar lógica de procesamiento al evento OnDispose de newClient. Cabe señalar que el evento OnDispose aquí no es un destructor en el sentido tradicional, sino una operación utilizada por la persona que llama para reciclar el objeto RedisClient al grupo de conexiones después de que se agote, es decir, bajo la premisa de que el objeto newClient no es anormal, póngalo Push en la pila this.deactiveClientQueue, donde el grupo de conexiones se recicla y se expande.

Interpretación del método this.InitNewClient()

Aquí está la inicialización del objeto RedisClient recién creado, incluidos Id, Active, etc., y continúe llamando a this.InitClient() para continuar con la inicialización.

Interpretación de this.RedisResolver.CreateMasterClient()

this.redisResolver es el tipo de interfaz IRedisResolver Hay tres implementaciones en el código fuente, como se muestra en la siguiente captura de pantalla. Aquí, el modo de producción centinela común se toma como ejemplo para el análisis.

La clase RedisSentinelResolver corresponde al modo centinela y el código fuente de la operación relevante es el siguiente:

Ver código

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

La lógica del método GetReadWriteHost() es: usar preferentemente la información del nodo maestro obtenida por this.sentinel.GetMaster(). Si GetMaster() falla, seleccione uno aleatorio del conjunto existente de maestros para conectarse.

Luego ingrese el método CreateRedisClient():

  • Primero, el objeto redisClient se crea a través de la fábrica this.ClientFactory(), y las operaciones de conteo y new RedisClient() se implementan dentro de la fábrica. No hay mucho contenido.
  • Luego ejecute redisClient.GetServerRole(), lo que significa verificar con el servidor que el nodo actualmente conectado es de hecho el rol Maestro. Si se confirma, se devuelve directamente a la persona que llama. [Si el proceso de envío de la solicitud de consulta es anormal y se cumplen ciertas condiciones, se iniciará una solicitud de conmutación por error, a saber, this.sentinel.ForceMasterFailover();]
  • Si el nodo actualmente conectado no tiene la función de maestro, llame a this.sentinel.GetMaster() varias veces para consultar la información del nodo maestro y volver a crear una instancia del objeto RedisClient;
  • Si aún no se puede conectar al nodo maestro después del tiempo de espera, ingresará al proceso de procesamiento de excepción de captura, atravesará todos los nodos de this.allHosts y actualizará los roles de nodo correspondientes.

Hasta ahora, a través del proceso anterior, el objeto RedisClient del nodo maestro finalmente se puede obtener y devolver a la persona que llama. 

En el proceso anterior, la implementación de varios métodos es más importante y complicada, a continuación se explican uno por uno:

Análisis del principio de implementación de GetMaster() de la clase RedisSentinel

El lugar de llamada es muy simple, pero hay muchas implementaciones de este método.El código fuente de la clase RedisSentinel es el siguiente:

Ver código

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();
			}
		}

Primero obtenga el objeto RedisSentinelWorker a través de GetValidSentinelWorker(). La implementación de este método incluye el control del mecanismo de reintento, y finalmente otorga el campo this.worker, es decir, la instancia del objeto RedisSentinelWorker, a través del método this.GetNextSentinel().

El método GetNextSentinel() incluye operaciones como bloqueos de sincronización, llamadas a this.worker.Dispose(), selección aleatoria de nodos centinela e instancias de objetos RedisSentinelWorker.

Lo siguiente es bloquear validSentinelWorker y luego continuar ejecutando string masterHost = validSentinelWorker.GetMasterHost(this.masterName);

El código de la clase RedisSentinelWorker correspondiente es el siguiente:

Ver código

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

Nota en el método GetMasterHost(): cuando ocurre una excepción, se activará el evento OnSentinelError de este objeto.Como su nombre lo indica, este evento se utiliza para el procesamiento posterior de las excepciones centinela. A través de la búsqueda del código fuente, solo el método GetNextSentinel() agrega un controlador al evento OnSentinelError --> el método privado void OnSentinelError(Exception ex) en RedisSentinel. Y este método imprime internamente el registro y activa el evento this.OnWorkerError, y luego llama a GetNextSentinel() para reasignar el campo this.worker.

Nota: El método Dispose() en realidad llama a las operaciones de cierre de sesión de this.sentinelClient y this.sentinePubSub respectivamente.

Funciones relacionadas e implementación de la clase RedisNativeClient

Luego se llama al método SentinelGetMasterAddrByName() de la clase RedisNativeClient:

El significado de varios métodos en esta clase se combina: enviar el comando de consulta del cliente centinela al servidor a través de Socket y formatear el resultado devuelto en el tipo RedisEndpoint requerido.

El método SendReceive() también incluye mecanismos como conexión de socket, reintento, control de frecuencia y control de tiempo de espera.

Ver código

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

Resumir

Este artículo se centra en la creación y adquisición de conexiones de Redis y tiene una comprensión más profunda del mecanismo de implementación interna del SDK. Sobre esta base, es más conveniente analizar las fallas relacionadas con Redis SDK en el entorno de producción.

Dirección original: https://www.cnblogs.com/chen943354/p/15913197.html 

Supongo que te gusta

Origin blog.csdn.net/wdjnb/article/details/123088210
Recomendado
Clasificación