Мониторинг работоспособности приложения

Health Check Middleware

Последнее обновление: 02.12.2022

Любое приложение не защищено от ошибок и снижения работоспособности в силу ряда причин, некоторые из которых трудно спрогнозировать на этапе разработки. И соответственно возникает вопрос мониторинга рабоспособности приложения. Фреймворк ASP.NET Core предоставляет специальный компонент middleware для отслеживания работоспособности приложения. С помощью данного middleware мы можем настроить проверку различных метрик и показателей, которые нам важны в рамках конкретного приложения. Например, можно проверять доступность какого-то сетевого сервиса, базы данных, использование физических ресурсов сервера (памяти, диска и т.д.)

Добавление Health Checks Middleware

Для добавления функционала проверки работоспособности прежде всего необходимо добавить в коллекцию сервисов приложения сервис HealthCheckService с помощью метода AddHealthChecks()

builder.Services.AddHealthChecks();

Проверка работоспособности доступна через специальные конечные точки. Для задания конечной точки применяются два метода:

app.UseHealthChecks("/health");

и

app.MapHealthChecks("/health");

В реальности разница между этими не большая. Первый метод принимает, как минимум, строку пути, запрос по которому будет обрабатываться. Второй метод принимает шаблон пути. В случаях выше оба метода позволяют обрабатывать запросы по пути "/health"

Пример применения Health Check Middleware

Рассмотрим небольшой пример, где имитириуется применение этой функционость. Допустим, наше приложение разделено на два слоя. Первый слой (первое приложение) генерирует некоторые данные, а второй слой (второе приложение) получает данные и непосредственно взаимодействует с клиентом.

В первом проекте, который будет отвечать за генерацию данных (допустим, он будет называться DataApp) определим следующий код:

var builder = WebApplication.CreateBuilder(args);
builder.WebHost.UseUrls("https://[::]:33333");

var app = builder.Build();

app.MapGet("/reset", () =>
{
    Latency.ResetLatency();
    return "Application reset";
});
app.MapGet("/data", async () =>
{
    int latency = Latency.GetLatency();
    await Task.Delay(latency);
    return $"Application latency: {latency}";
});

app.Run();

static class Latency
{
    static int counter = 1;
    // увеличиваем счетчик
    public static int GetLatency() => counter++ * 500;
    // сбрасываем счетчик
    public static void ResetLatency() => counter = 1;
}

Это приложение будет имитировать латентность или задержки при получении и обработке запросов. Для этого определяем вспомогательный статический класс Latency. Его метод GetLatency() увеличивает счетчик и возвращает значение counter++ * 500. Метод ResetLatency() сбрасывает значение счетчика к начальному.

Для простоты данное приложение будет запускаться по адресу https://localhost:33333.

Приложение определяет две конечные точки. Конечная точка app.MapGet("/reset"... обрабатывает запросы по пути "reset" и условно осуществляет восстановление сервера (по сути сбрасывает значение счетчика к начальному).

Вторая конечная точка - app.MapGet("/data"... собственно посылает данные. Но для имитации все повыщаеющеся латентности приложения ее обработчик получает новое значение из метода Latency.GetLatency(), осуществляет задержку и отправляет ответ.

int latency = Latency.GetLatency();
await Task.Delay(latency);
return $"Application latency: {latency}";

То есть таким образом, мы имитируем повышение латентности с каждым новым запросом. Соответственно с каждым новым запросом при вызове метода Latency.GetLatency() будет все больше увеличиваться значение счетчика и будет возвращаться все большее значение. И сервер будет все медленнее и медленнее обрабатывать запросы.

Теперь определим второй проект ASP.NET Core, который будет обращаться к предыдущему приложению DataApp и проверять его работоспособность:

using Microsoft.Extensions.Diagnostics.HealthChecks;
using System.Diagnostics;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddHealthChecks()
    .AddCheck<RequestTimeHealthCheck>("RequestTimeCheck");  // проверяем работоспособность с RequestTimeCheck

builder.Services.AddHttpClient();   // подключаем HttpClient
builder.WebHost.UseUrls("https://[::]:44444");  // обрабатываем запросы по адресу https://localhost:44444

var app = builder.Build();
app.MapHealthChecks("/health");

app.MapGet("/", async (HttpClient httpClient) =>
{
    // отправляем запрос к другому сервису и возвращаем его ответ
    var response = await httpClient.GetAsync("https://localhost:33333/data");
    return await response.Content.ReadAsStringAsync();
});

app.Run();

public class RequestTimeHealthCheck : IHealthCheck
{
    int degraded_level = 2000;  // уровень плохой работы
    int unhealthy_level = 5000; // нерабочий уровень
    HttpClient httpClient;
    public RequestTimeHealthCheck(HttpClient client) => httpClient = client;
    public async Task<HealthCheckResult> CheckHealthAsync(HealthCheckContext context, 
        CancellationToken cancellationToken = default)
    {
        // получаем время запроса
        Stopwatch sw = Stopwatch.StartNew();
        await httpClient.GetAsync("https://localhost:33333/data");
        sw.Stop();
        var responseTime = sw.ElapsedMilliseconds;
        // в зависимости от времени запроса возвращаем определенный результат
        if (responseTime < degraded_level)
        {
            return HealthCheckResult.Healthy("Система функционирует хорошо");
        }
        else if (responseTime < unhealthy_level)
        {
            return HealthCheckResult.Degraded("Снижение качества работы системы");
        }
        else
        {
            return HealthCheckResult.Unhealthy("Система в нерабочем состоянии. Необходим ее перезапуск.");
        }
    }
}

Здесь прежде всего подключаем сервис проверки работоспособности:

builder.Services.AddHealthChecks()

Этот метод возвращает объект IHealthCheckBuilder, который применяется для создания и настройки сервиса HealthCheckService. Но сама проверка работоспособоности применяется объект IHealthCheck. И для добавления такого объекта применяется метод AddCheck():

.AddCheck<RequestTimeHealthCheck>("RequestTimeCheck");

Этот метод типизируется типом, который реализует интерфейс IHealthCheck, а в качества параметра принимает строку - имя для сервиса проверки.

В нашем случае в качестве реализации IHealthCheck применяется класс RequestTimeCheck. Класс должен реализовать метод интерфейса CheckHealthAsync. В примере выше в этом методе отправляем запрос к первому приложению к его конечной точке "/data" и проверяем время запроса:

Stopwatch sw = Stopwatch.StartNew();
await httpClient.GetAsync("https://localhost:33333/data");
sw.Stop();
var responseTime = sw.ElapsedMilliseconds;

Если время запроса превосходит определенные предустоновленые пределы, то возвращает соответствующее сообщение о работоспособности приложения:

 if (responseTime < degraded_level)
{
    return HealthCheckResult.Healthy("Система функционирует хорошо");
}
else if (responseTime < unhealthy_level)
{
    return HealthCheckResult.Degraded("Снижение качества работы системы");
}
else
{
    return HealthCheckResult.Unhealthy("Система в нерабочем состоянии. Необходим ее перезапуск.");
}

Метод возвращает результат проверки работоспособности - структура HealthCheckResult. Конкретный результат устанавливается с помощью одного из методов структуры: Healthy() (приложение работает нормально), Degraded() (работоспособность снижается) и Unhealthy() (приложение неработоспособно). Каждый метод возвращает соответствующий экземпляр структуры, который сигнализирует о состоянии приложения.

Когда, при каких условиях считать приложение неработоспособным - это зависит от нашей задачи, логики нашего приложения, метрик, которые мы применяем для оценки и конкретной ситуации. В данном случае все зависит от времени запроса.

Для получения информации о работоспособности приложении определена конечная точка "/health" посредством метода

app.MapHealthChecks("/health");
Health Check Middleware в приложении на ASP.NET Core и C#

Запустим сначала приложение DataApp, а затем AggregationApp. Обратимся в браузере по адресу "https://localhost:44444/" (то есть к AggregationApp):

Проверка работоспособности приложения на ASP.NET Core и C#

В данном случае AggregationApp будет обращаться по адресу "https://localhost:33333/data" к DataApp и получает данные. Но внутри DataApp это приведет к увеличению задержки при обработки запроса. И каждый последующий запрос будет обрабатываться все медленнее и медленнее.

Если после 5 запросов мы обратимся в браузере по адресу "https://localhost:44444/health", то консоль браузера выведет предупреждение о снижении работоспособности

Degraded application на ASP.NET Core и C#

Еще после нескольких запросов приложение сигнализирует об условно нерабочем состоянии, что свидетельствует, что латентность в приложении DataApp превысила сколь-нибудь допустимые пределы.

Unhealthy application на ASP.NET Core и C#

Таким образом, на основании некоторых метрик мы можем определить мехнизм уведомления о состоянии системы.

Сервис мониторинга

Следует отметить, что, как правило, за мониторинг приложения отвечает какое-то внешнее приложение. Такие приложения мониторинга еще называют "watchdog" (дословно "сторожевой пёс", но в русскоязычной литературе для этого обычно используется понятие "Сторожевой таймер"). Так, в примере выше и DataApp и AggregationApp можно рассматривать как слои/уровни одного общего приложения. Для мониторинга определим третий проект. Если речь идет о C#, то нередко для этой цели определяется фоновый сервис. Но для простоты и текста мы определим простое консольное приложение:

HttpClient client = new HttpClient();

while (true)
{
    using var response = await client.GetAsync("https://localhost:44444/health");
    var status = await response.Content.ReadAsStringAsync();
    if (status == "Unhealthy")
    {
        Console.WriteLine($"{DateTime.Now.ToLongTimeString()} : сервер в нерабочем состоянии, осуществляется перезапуск.");
        await client.GetAsync("https://localhost:33333/reset");
    }
    else
    {
        Console.WriteLine($"{DateTime.Now.ToLongTimeString()} : все норм");
    }
    await Task.Delay(10000);   // задержка на 10 секунд
}

В данном случае сначала осуществляем запрос по адресу "https://localhost:44444/health" и получаем статус. Если приложение в нерабочем состоянии, то обращаемся по адресу "https://localhost:33333/reset" и условно перезапускаем приложение DataApp (фактически сбрасываем счетчик, что увеличивает скорость обработки запросов).

watchdog для приложения на ASP.NET Core и C#
Помощь сайту
Юмани:
410011174743222
Перевод на карту
Номер карты:
4048415020898850