X.Spectator - monitoramento de estado no .NET



Hoje, a maioria dos sistemas de informação são soluções complexas, com uma arquitetura bastante complexa e um grande número de dependências mútuas. Durante a operação de tais sistemas, no momento do pico de carga, alguns dos módulos podem falhar ou podem não funcionar corretamente. Nesse caso, o sistema deixa de ser estável e pode deixar de processar corretamente todas as solicitações recebidas. Para garantir uma operação estável do sistema, várias estratégias podem ser implementadas.

Em alguns casos (se isso for permitido), a tradução do sistema para o chamado. "Modo de segurança". Nesse modo, o sistema pode processar um número maior de chamadas com uma ligeira diminuição na precisão dos resultados do processamento de consultas. Em outros casos, o dimensionamento de recursos de computação e o aumento do número de módulos de sistema individuais responsáveis ​​pelo processamento de solicitações de entrada podem ajudar. Nesse caso, a tarefa mais importante é determinar o estado do sistema , dependendo de qual, uma ou outra ação já deve ser tomada para estabilizar seu trabalho.

Nos meus projetos, venho resolvendo um problema semelhante de várias maneiras há algum tempo. Desde que eu estava na universidade, eu tinha um pequeno projeto escrito no .NET 4.0, do qual de vez em quando eu pegava pequenos pedaços de código para projetos reais. Há muito que planejo refatorar esse projeto, limpá-lo e organizá-lo na forma de uma mini-estrutura separada, que nos permite resolver de maneira bela e minimalista o problema de monitorar o estado do sistema. Depois de passar várias noites e alguns dias de folga, coloquei esse código em ordem e o publiquei no GitHub . Além disso, proponho considerar com mais detalhes o que e como é implementado neste projeto.

Para determinar a necessidade de alterar o modo de operação do sistema, propõe-se usar uma abordagem usando "sensores" e "observadores" virtuais.



Entidades e definições


Entidades base a serem usadas:

  • Sensor - é responsável por verificar o status de um dos indicadores do sistema;
  • Observador - interroga um ou vários sensores. Altera seu estado dependendo das leituras atuais dos sensores;
  • Calculadora de estado - calcula o estado atual com base no log de métricas.
  • Registro de estado - um conjunto de indicadores para cada um dos sensores indicando o tempo de pesquisa.

Para cada abstração, há uma implementação básica, bem como mecanismos para expansão simples e conveniente. Vamos considerá-los com mais detalhes.

Sensor


Interface básica do IProbe . As classes que implementam o IProbe fornecem, mediante solicitação, o valor dos parâmetros do sistema que eles observam ou um módulo / serviço específico.

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

Como resultado do método Check , a instância IProbe retorna uma estrutura 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}"; } 

onde o campo Sucesso determina se o parâmetro foi testado com êxito do ponto de vista do sensor e os campos Dados e Exceção podem armazenar informações adicionais, caso seja necessário para depuração ou registro.

Observador


A interface básica do ISpectator . Ele monitora o sistema, gera eventos no momento da alteração do estado do sistema para notificar todos os módulos inscritos nesses eventos. Ele usa uma instância de uma classe que implementa a interface IStateEvaluator para calcular o estado atual.

  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 uma das pesquisas de sensor, o observador gera o evento StateChanged e um objeto do tipo StateEventArgs é passado aos assinantes desse evento, que contém informações sobre o estado atual do sistema.

Calculadora de estado


A interface básica do IEvaluator . Calcula o estado atual do sistema com base no log de status do sensor.

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

Registro de estado


Uma coleção de instâncias da estrutura JournalRecord. A instância JournalRecord armazena informações sobre todos os sensores pesquisados ​​no momento em que a pesquisa foi iniciada pelo observador.

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

Works Como funciona


O processo de cálculo do estado do sistema pode ser descrito da seguinte maneira: cada um dos sensores embutidos no sistema pode determinar um dos parâmetros do sistema / módulo / serviço observado. Por exemplo, ele pode corrigir o número de solicitações ativas externas à API, a quantidade de RAM usada, o número de entradas no cache etc.

Cada sensor pode ser atribuído a um ou vários observadores. Cada observador pode trabalhar com um ou vários sensores. O observador deve implementar a interface ISpectator e gerar eventos no caso de uma mudança de estado ou pesquisa de sensores.

Durante a próxima verificação do estado do sistema, o observador pesquisa todos os seus "sensores", formando uma matriz para gravar no registro de estado. Se o estado do sistema mudou, o observador gera um evento apropriado. Os assinantes de eventos que recebem informações sobre a alteração podem alterar os parâmetros operacionais do sistema. Ao mesmo tempo, "computadores" de vários tipos podem ser usados ​​para determinar o estado do sistema.

Modos de operação síncrona e assíncrona


Considere dois cenários principais em que o observador inicia uma pesquisa de "sensores".

Modo síncrono


Uma pesquisa de sensores seguida de um recálculo do estado do sistema é causada pelo apelo direto de um dos módulos do sistema ao observador.
Nesse caso, o observador e os sensores trabalham no mesmo segmento. O erro de cálculo do estado do sistema é executado como parte de uma operação dentro do sistema.

O projeto já possui uma implementação básica desse 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 assíncrono


Nesse caso, a interrogação dos sensores ocorre de forma assíncrona a partir dos processos do sistema e pode ser realizada em um encadeamento separado. A implementação básica desse observador também já é um projeto - 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(); } 


Conclusão


O uso do X.Spectator me ajudou pessoalmente em vários projetos altamente carregados para aumentar significativamente a estabilidade de vários serviços. A melhor estrutura proposta se provou quando implementada em sistemas distribuídos construídos com base na arquitetura de microsserviços. A opção de integração mais ideal é usar o princípio de inversão de controle, ou seja, a implementação de dependências, quando o processo de implementação de sensores é implementado usando um contêiner de IoC, e os observadores são apresentados na forma de singletones, onde uma única instância de cada um dos observadores pode ser acessada por diferentes classes de módulos e serviços.

Links e informações úteis


Repositório do projeto
Exemplos
Pacote NuGet

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


All Articles