Работа над корпоративными веб-приложениями обычно требует написания массы дополнительного кода, помогающего вести мониторинг этих приложений. В этой статье я поясню, как я использовал фильтры Model-View-Controller (MVC) для расчистки и замены повторяющегося, запутанного кода, который был разбросан по бесчисленным методам в одном приложении.
Менеджеры по эксплуатации часто устанавливают Microsoft Operations Manager (MOM) для мониторинга работоспособности веб-сайта (или сервиса) и используют счетчики производительности для оповещения о достижении пороговых значений. Эти оповещения помогают быстро обнаруживать деградацию производительности различных частей веб-сайта.
Проблемный код
Я работаю над проектом (с применением ASP.NET MVC Framework), где требуется добавлять счетчики производительности к веб-страницам и веб-сервисам, которые должны помогать группе эксплуатации. Этой группе на каждой странице нужны счетчики Request Latency (задержка обработки запроса), Total Requests per Second (общее количество запросов в секунду) и Failure Ratio (частота отказов).
При реализации таких требований возникает впечатление, что проблемы неизбежны. Я начал с изучения текущей реализации этих счетчиков от более прилежных разработчиков. И был разочарован. Уверен, вы все бывали в таких ситуациях: вы смотрите на код и ощущаете досаду. Что я увидел? Повторяющийся код, вкрапленный в каждый метод, с небольшими изменения в именах переменных там и сям. Радости от текущей реализации я не испытал.
Код, вызвавший у меня досаду, показан на рис. 1.
Рис. 1. Код, заставивший меня покривиться
public ActionResult AccountProfileInformation()
{
try
{
totalRequestsAccountInfoCounter.Increment();
// Запуск счетчика задержек
long startTime = Stopwatch.GetTimestamp();
// Здесь выполняем какие-то операции
long stopTime = Stopwatch.GetTimestamp();
latencyAccountInfoCounter.IncrementBy((stopTime - startTime) /
Stopwatch.Frequency);
latencyAccountInfoBaseCounter.Increment();
}
catch (Exception e)
{
failureAccountInfoCounterCounter.Increment();
}
return View();
}
Глядя на этот код, я захотел удалить его из каждого метода операции в проекте. Шаблон такого типа крайне затрудняет понимание того, где находится код самого метода, поскольку он напрочь закрывается кодом мониторинга производительности. Я искал более разумный способ рефакторинга этого кода, чтобы он не замусоривал все методы операций. И выбрал для этого MVC-фильтры.
MVC-фильтры
Это собственные атрибуты, которые вы помещаете в методы операций (или в контроллеры) для добавления общей функциональности. MVC-фильтры позволяют добавлять пред- и постобработку. Список встроенных MVC-фильтров см. по ссылке bit.ly/jSaD5N. Я использовал некоторые встроенные фильтры, такие как OutputCache, но знал, что в MVC-фильтрах скрыта колоссальная мощь, которую я никогда не смог бы задействовать в полной мере (подробнее о классе FilterAttribute см. по ссылке bit.ly/kMPBYB).
Поэтому я начал размышлять: а что если мне удастся инкапсулировать всю логику этих счетчиков производительности в атрибуте фильтра MVC? И идея родилась! Я смог соблюсти требования всех ранее перечисленных счетчиков производительности со следующими операциями.
- Total Requests per Second (общее количество запросов в секунду):
- реализуем IActionFilter с двумя методами: OnActionExecuting и OnActionExecuted;
- увеличиваем счетчик в OnActionExecuting.
- Request Latency (задержка в обработке запроса):
- реализуем IResultFilter с двумя методами: OnResultExecuting и OnResultExecuted;
- запускаем таймер в OnActionExecuting и фиксируем задержку в ходе выполнения OnResultExecuted.
- Failure Ratio (частота отказов):
- Реализуем IExceptionFilter с методом OnException.
Этот процесс показан на рис. 2.
Увеличить
Рис. 2. Конвейер обработки MVC-фильтров
Давайте вкратце обсудим применение каждого фильтра, показанного на рис. 2.
IActionFilter OnActionExecuting (2a) выполняется до метода операции, а OnActionExecuted (2b) — после метода операции, но до обработки результата.
IResultFilter OnResultExecuting (4a) выполняется до результата операции (например, рендеринга представления), а OnResultExecuted (4b) — после результата операции.
IExceptionFilter OnException (не показан на рис. 2, чтобы не засорять общую картину) выполняется всякий раз, когда генерируется исключение (необработанное).
IAuthorizationFilter OnAuthorization (отсутствует на рис. 2и не используется в этой статье) вызывается, когда требуется авторизация.
Управление счетчиками
Однако, если я задействую эти фильтры счетчика производительности, я столкнусь с проблемой: как получать счетчик производительности (для каждой операции) в каждом из фильтров в период выполнения? Я не хотел создавать отдельный класс атрибута фильтра для каждой операции. В таком случае мне пришлось бы «зашить» имя счетчика производительности в этот атрибут. А это привело бы к взрывному росту имен классов, необходимых для реализации решения. Я решил вернуться к технологии, которую использовал в первой работе с Microsoft .NET Framework: к отражению. Механизм отражения интенсивно используется самой инфраструктурой MVC. Вы можете узнать больше об отражении по ссылке bit.ly/iPHdHz.
Моя идея заключалась в создании двух классов.
- WebCounterAttribute:
- реализует интерфейсы MVC-фильтров (IExceptionFilter, IActionFilter и IResultFilter);
- увеличивает счетчики, хранящиеся в WebCounterManager.
- WebCounterManager:
- реализует код отражения для загрузки атрибутов WebCounterAttribute из MVC-операции;
- хранит карту для ускорения поиска объектов счетчиков производительности;
- предоставляет методы для увеличения счетчиков, хранящихся в этой карте.
Механизм отражения интенсивно используется самой инфраструктурой MVC.
Реализация проекта
Располагая этими классами, я могу дополнять WebCounterAttribute в методах каждой операции, в которой нужно реализовать счетчики производительности, как показано ниже:
public sealed class WebCounterAttribute : FilterAttribute, IActionFilter, IExceptionFilter, IResultFilter
{
/// Реализации интерфейсов не показаны
}
Вот пример метода операции:
[WebCounter("Contoso Site", "AccountProfileInformation")]
public ActionResult AccountProfileInformation()
{
// Загрузка какой-либо модели
return View();
}
Затем я могу считывать эти атрибуты в методе Application_Start, используя отражения, и создавать счетчик для каждой из операций, как показано на рис. 3. (Заметьте, что счетчики регистрируются в системе программой установки, а экземпляры счетчиков создаются в коде.)
Рис. 3. Отражение сборок
/// <summary>
/// Этот метод отражает указанную сборку (сборки) по заданному
/// пути и создает базовые операции, необходимые счетчикам
/// </summary>
/// <param name="assemblyPath"></param>
/// <param name="assemblyFilter"></param>
public void Create(string assemblyPath, string assemblyFilter)
{
counterMap = new Dictionary<string, PerformanceCounter>();
foreach (string assemblyName in Directory.EnumerateFileSystemEntries(
assemblyPath, assemblyFilter))
{
Type[] allTypes = Assembly.LoadFrom(assemblyName).GetTypes();
foreach (Type t in allTypes)
{
if (typeof(IController).IsAssignableFrom(t))
{
MemberInfo[] infos = Type.GetType(t.AssemblyQualifiedName).GetMembers();
foreach (MemberInfo memberInfo in infos)
{
foreach (object info in memberInfo.GetCustomAttributes(
typeof(WebCounterAttribute), true))
{
WebCounterAttribute webPerfCounter = info as WebCounterAttribute;
string category = webPerfCounter.Category;
string instance = webPerfCounter.Instance;
// Создаем агрегированные экземпляры, если их нет
foreach (string type in CounterTypeNames)
{
if (!counterMap.ContainsKey(KeyBuilder(Total, type)))
{
counterMap.Add(KeyBuilder(Total, type),
CreateInstance(category, type, Total));
}
}
// Создаем счетчики производительности
foreach (string type in CounterTypeNames)
{
counterMap.Add(KeyBuilder(instance, type),
CreateInstance(category, type, instance));
}
}
}
}
}
}
}
Обратите внимание на важную строку, где заполняется карта:
(counterMap.Add(KeyBuilder(instance, type), CreateInstance(category, type, instance));),
Она создает сопоставление между конкретным экземпляром WebCounterAttribute операции, в том числе типом счетчика, и созданным экземпляром PerformanceCounter.
Тогда я получаю возможность написать код, который позволяет использовать это сопоставление для поиска экземпляра PerformanceCounter (и увеличивать его) для данного экземпляра WebCounterAttribute (рис. 4).
Рис. 4. WebCounterManager RecordLatency
/// <summary>
/// Записываем задержку для экземпляра с данным именем
/// </summary>
/// <param name="instance"></param>
/// <param name="latency"></param>
public void RecordLatency(string instance, long latency)
{
if (counterMap.ContainsKey(KeyBuilder(instance,
CounterTypeNames[(int)CounterTypes.AverageLatency]))
&& counterMap.ContainsKey(KeyBuilder(instance,
CounterTypeNames[(int)CounterTypes.AverageLatencyBase])))
{
counterMap[KeyBuilder(instance,
CounterTypeNames[(int)CounterTypes.AverageLatency])].IncrementBy(latency);
counterMap[KeyBuilder(Total,
CounterTypeNames[(int)CounterTypes.AverageLatency])].IncrementBy(latency);
counterMap[KeyBuilder(instance,
CounterTypeNames[(int)CounterTypes.AverageLatencyBase])].Increment();
counterMap[KeyBuilder(Total,
CounterTypeNames[(int)CounterTypes.AverageLatencyBase])].Increment();
}
}
После этого я могу регистрировать данные счетчика производительности при выполнении этих фильтров. Например, на рис. 5 вы видите реализацию регистрации задержки.
Рис. 5. WebCounterAttribute, запускающий WebCounterManager RecordLatency
/// <summary>
/// Этот метод вызывается, когда результат был обработан
/// (это происходит перед возвратом ответа). Он регистрирует
/// задержку от начала запроса до возврата ответа.
/// </summary>
/// <param name="filterContext"></param>
public void OnResultExecuted(ResultExecutedContext filterContext)
{
// Stop counter for latency
long time = Stopwatch.GetTimestamp() - startTime;
WebCounterManager countManager = GetWebCounterManager(filterContext.HttpContext);
if (countManager != null)
{
countManager.RecordLatency(Instance, time);
...
}
}
private WebCounterManager GetWebCounterManager(HttpContextBase context)
{
WebCounterManager manager =
context.Application[WebCounterManager.WebCounterManagerApplicationKey]
as WebCounterManager;
return manager;
}
Вероятно, вы заметили, что в этом вызове я получаю WebCounterManager из состояния приложения (Application State). Чтобы это работало, вам нужно добавить следующий код в свой global.asax.cs:
WebCounterManager webCounterMgr = new WebCounterManager();
webCounterMgr.Create(Server.Map("~/bin"), "*.dll");
Application[WebCounterManager.WebCounterManagerApplicationKey] = webCounterMgr;
В заключение замечу, что MVC-фильтры предоставляют элегантное решение для устранения часто повторяемых шаблонов кода. Они позволяют переработать общий код, благодаря чему вам исходный код станет понятнее и легче в сопровождении. Очевидно, что нужно искать некий баланс между элегантностью и простотой реализации. В моем случае пришлось добавлять счетчики производительности примерно в 50 веб-страниц. Выигрыш явно стоит дополнительных усилий.
MVC-фильтры — отличный способ добавления поведений без вмешательства в существующий код, поэтому с чем бы вы ни имели дело — со счетчиками производительности, протоколированием или аудитом, вы найдете безграничные возможности для четкой реализации необходимой логики.