Работа над корпоративными веб-приложениями обычно требует написания массы дополнительного кода, помогающего вести мониторинг этих приложений. В этой статье я поясню, как я использовал фильтры 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-фильтры — отличный способ добавления поведений без вмешательства в существующий код, поэтому с чем бы вы ни имели дело — со счетчиками производительности, протоколированием или аудитом, вы найдете безграничные возможности для четкой реализации необходимой логики.