X.Spectator - monitoreo de estado en .NET



Hoy, la mayoría de los sistemas de información son soluciones complejas con una arquitectura bastante compleja y una gran cantidad de dependencias mutuas. Durante el funcionamiento de dichos sistemas, en el momento de las cargas máximas, algunos de los módulos pueden fallar o no funcionar correctamente. En este caso, el sistema deja de ser estable y puede dejar de procesar correctamente todas las solicitudes entrantes. Para garantizar un funcionamiento estable del sistema, se pueden implementar varias estrategias.

En algunos casos (si esto es permisible), la traducción del sistema al llamado. "Modo seguro". En este modo, el sistema puede procesar una mayor cantidad de llamadas con una ligera disminución en la precisión de los resultados del procesamiento de consultas. En otros casos, puede ayudar la ampliación de los recursos informáticos y el aumento del número de módulos de sistema individuales responsables del procesamiento de las solicitudes entrantes. En este caso, la tarea más importante es determinar el estado del sistema , dependiendo de cuál, ya se debe tomar una u otra acción para estabilizar su trabajo.

En mis proyectos, llevo bastante tiempo resolviendo un problema similar de varias maneras. Desde que estaba en la universidad, tenía un pequeño proyecto escrito en .NET 4.0, del cual de vez en cuando tomaba pequeñas piezas de código para proyectos reales. Durante mucho tiempo he planeado refactorizar este proyecto, limpiarlo y organizarlo en forma de un mini-marco separado que nos permite resolver de manera hermosa y minimalista el problema de monitorear el estado del sistema. Después de pasar varias tardes y un par de días libres, puse este código en orden y lo publiqué en GitHub . Además, propongo considerar con más detalle qué y cómo se implementa en este proyecto.

Para determinar la necesidad de cambiar el modo operativo del sistema, se propone utilizar un enfoque utilizando "sensores" y "observadores" virtuales.



Entidades y definiciones


Entidades base a utilizar:

  • Sensor : es responsable de verificar el estado de uno de los indicadores del sistema;
  • Observador : interroga a uno o varios sensores. Cambia su estado dependiendo de las lecturas actuales de los sensores;
  • Calculadora de estado : calcula el estado actual en función del registro de métricas.
  • Registro de estado : un conjunto de indicadores para cada uno de los sensores que indican el tiempo de sondeo.

Para cada abstracción, hay una implementación básica, así como mecanismos para una expansión simple y conveniente. Consideremos con más detalle.

Sensor


Interfaz básica de IProbe . Las clases que implementan IProbe proporcionan, a pedido, el valor de los parámetros del sistema que observan, o un módulo / servicio específico.

public interface IProbe { string Name { get; } Task<ProbeResult> Check(); } 

Como resultado del método Check , la instancia de IProbe devuelve una estructura ProbeResult

  public struct ProbeResult { public string ProbeName { get; set; } public DateTime Time { get; set; } public bool Success { get; set; } public string Data { get; set; } public Exception Exception { get; set; } public override string ToString() => $"{Time}: {Success}"; } 

donde el campo Éxito determina si el parámetro se probó con éxito desde el punto de vista del sensor, y los campos Datos y Excepción pueden almacenar información adicional en caso de que sea necesario para la depuración o el registro.

Observador


La interfaz básica de ISpectator . Monitorea el sistema, genera eventos al momento de cambiar el estado del sistema para notificar a todos los módulos que están suscritos a estos eventos. Utiliza una instancia de una clase que implementa la interfaz IStateEvaluator para calcular el estado actual.

  public interface ISpectator<TState> where TState : struct, IConvertible { event EventHandler<StateEventArgs<TState>> StateChanged; event EventHandler<HealthCheckEventArgs> HealthChecked; TState State { get; } TimeSpan Uptime { get; } string Name { get; } void AddProbe(IProbe probe); void CheckHealth(); } 

Durante cada una de las encuestas de sensores, el observador genera el evento StateChanged y se pasa un objeto de tipo StateEventArgs a los suscriptores de este evento, que contiene información sobre el estado actual del sistema.

Calculadora de estado


La interfaz base de IEvaluator . Calcula el estado actual del sistema en función del registro de estado del sensor.

  public interface IStateEvaluator<TState> { TState Evaluate( TState currentState, DateTime stateChangedLastTime, IReadOnlyCollection<JournalRecord> journal); } 

Registro de estado


Una colección de instancias de la estructura JournalRecord. La instancia de JournalRecord almacena información sobre todos los sensores encuestados en el momento en que el observador inició la encuesta.

  public struct JournalRecord { public JournalRecord(DateTime time, IEnumerable<ProbeResult> values) { Time = time; Values = values.ToImmutableList(); } public DateTime Time { get; set; } public IReadOnlyCollection<ProbeResult> Values { get; set; } public override string ToString() => $"{Time}: [{string.Join(",", Values)}]"; } 

Cómo funciona


El proceso de cálculo del estado del sistema se puede describir de la siguiente manera: cada uno de los sensores integrados en el sistema puede determinar uno de los parámetros del sistema / módulo / servicio observado. Por ejemplo, puede corregir el número de solicitudes activas externas a la API, la cantidad de RAM utilizada, el número de entradas en la memoria caché, etc.

Cada sensor puede asignarse a uno o varios observadores. Cada observador puede trabajar con uno o varios sensores. El observador debe implementar la interfaz ISpectator y generar eventos en caso de un cambio de estado o sondeo de sensores.

Durante la próxima comprobación del estado del sistema, el observador sondea todos sus "sensores", formando una matriz para escribir en el registro de estado. Si el estado del sistema ha cambiado, el observador genera un evento apropiado. Los suscriptores de eventos que reciben información sobre el cambio pueden cambiar los parámetros operativos del sistema. Al mismo tiempo, se pueden usar "computadoras" de varios tipos para determinar el estado del sistema.

Modos de funcionamiento síncrono y asíncrono


Considere dos escenarios principales en los que el observador inicia una encuesta de "sensores".

Modo sincrónico


Una encuesta de sensores seguida de un recálculo del estado del sistema es causada por el atractivo directo de uno de los módulos del sistema para el observador.
En este caso, el observador y los sensores trabajan en el mismo hilo. El error de cálculo del estado del sistema se realiza como parte de una operación dentro del sistema.

El proyecto ya tiene una implementación básica de dicho observador: SpectatorBase .

Ver código
  public class SpectatorBase<TState> : ISpectator<TState> where TState : struct, IConvertible { private TState _state; private readonly IList<IProbe> _probes; private readonly IStateEvaluator<TState> _stateEvaluator; private readonly List<JournalRecord> _journal; private readonly ReaderWriterLockSlim _journalLock; private readonly ReaderWriterLockSlim _stateLock; private readonly Stopwatch _stopwatch; public event EventHandler<StateEventArgs<TState>> StateChanged; public event EventHandler<HealthCheckEventArgs> HealthChecked; public virtual TState State { get { _stateLock.EnterReadLock(); try { return _state; } finally { _stateLock.ExitReadLock(); } } } public TimeSpan Uptime => _stopwatch.Elapsed; public string Name { get; set; } public IReadOnlyCollection<JournalRecord> Journal { get { _journalLock.EnterReadLock(); try { return _journal; } finally { _journalLock.ExitReadLock(); } } } public DateTime StateChangedDate { get; private set; } public TimeSpan RetentionPeriod { get; private set; } public SpectatorBase(IStateEvaluator<TState> stateEvaluator, TimeSpan retentionPeriod, TState initialState) { RetentionPeriod = retentionPeriod; _state = initialState; StateChangedDate = DateTime.UtcNow; _stateEvaluator = stateEvaluator; _stopwatch = Stopwatch.StartNew(); _probes = new List<IProbe>(); _journal = new List<JournalRecord>(); _journalLock = new ReaderWriterLockSlim(); _stateLock = new ReaderWriterLockSlim(); } public void AddProbe(IProbe probe) => _probes.Add(probe); protected virtual void ChangeState(TState state, IEnumerable<string> failedProbes) { _stateLock.EnterWriteLock(); try { _state = state; } finally { _stateLock.ExitWriteLock(); } StateChangedDate = DateTime.UtcNow; StateChanged?.Invoke(this, new StateEventArgs<TState>(state, StateChangedDate, failedProbes)); } public virtual void CheckHealth() { var results = new Stack<ProbeResult>(); var tasks = _probes .Select(async o => { results.Push(await o.Check().ConfigureAwait(false)); }) .ToArray(); Task.WaitAll(tasks); var now = DateTime.UtcNow; _journalLock.EnterWriteLock(); try { //cleanup state records _journal.RemoveAll(o => o.Time < now.Subtract(RetentionPeriod)); _journal.Add(new JournalRecord(now, results)); } finally { _journalLock.ExitWriteLock(); } //Recalculate state var state = _stateEvaluator.Evaluate(State, StateChangedDate, _journal); if (!EqualityComparer<TState>.Default.Equals(State, state)) { ChangeState(state, results.Where(o => !o.Success).Select(o => o.ProbeName)); } OnHealthChecked(now, results); } protected virtual void OnHealthChecked(DateTime now, IReadOnlyCollection<ProbeResult> results) => HealthChecked?.Invoke(this, new HealthCheckEventArgs(now, results)); } 


Modo asincrónico


En este caso, la interrogación de los sensores se produce de forma asíncrona a partir de los procesos del sistema y puede realizarse en un hilo separado. La implementación básica de dicho observador también es ya un proyecto: AutomatedSpectator .

Ver código
 public class AutomatedSpectator<TState> : SpectatorBase<TState>, IAutomatedSpectator<TState> where TState : struct, IConvertible { public TimeSpan CheckHealthPeriod { get; } private readonly System.Timers.Timer _timer; public AutomatedSpectator( TimeSpan checkHealthPeriod, TimeSpan retentionPeriod, IStateEvaluator<TState> stateEvaluator, TState initialState) : base(stateEvaluator, retentionPeriod, initialState) { CheckHealthPeriod = checkHealthPeriod; _timer = new System.Timers.Timer(CheckHealthPeriod.TotalMilliseconds); _timer.Elapsed += (sender, args) => CheckHealth(); _timer.AutoReset = true; } public void Start() => _timer.Start(); } 


Conclusión


El uso de X.Spectator me ayudó personalmente en varios proyectos altamente cargados para aumentar significativamente la estabilidad de una serie de servicios. El mejor marco propuesto ha demostrado su eficacia cuando se implementa en sistemas distribuidos basados ​​en la arquitectura de microservicios. La opción de integración más óptima es utilizar el principio de inversión de control, es decir, la implementación de dependencias, cuando el proceso de implementación de sensores se implementa utilizando un contenedor IoC, y los observadores se presentan en forma de singletones, donde se puede acceder a una sola instancia de cada uno de los observadores mediante diferentes clases de módulos y servicios.

Enlaces e información útil


Repositorio de proyectos
Ejemplos
paquete NuGet

Source: https://habr.com/ru/post/459092/


All Articles