Описание системы
Большинство продуктов Тинькофф Банка создаются полностью силами наших разработчиков. Один из таких продуктов — система Pds (predictive dialing system). Pds – это робот, который обзванивает клиентов вместо операторов. Причем, если клиент ответил, звонок переводится на оператора. Если клиент не ответил, робот фиксирует неуспешный звонок.
Таким образом, задача системы — обеспечить операторов постоянной работой при заданном качестве обслуживания. Под работой операторов мы понимаем обработку звонков системы Pds и фиксацию результата разговора. Уровень качества обслуживания — соотношение принятых и пропущенных оператором звонков.
Наша реализация представляет собой распределенную систему, состоящую из нескольких различных сервисов-компонент. Архитектура системы схематично представлена на риcунке.
Кратко опишу представленные на диаграмме элементы.
Компонент PDS технически представляет собой службу Windows, которая сама рассчитывает, сколько номеров обзвонить и регистрирует неуспешные звонки.
Компонент АТС — телефонная станция с открытым API, которая набирает номера клиентов и устанавливает телефонные соединения между клиентами и операторами.
Операторы — группа специалистов, которые обрабатывают совершенные системой Pds звонки.
Клиент M, Клиент X – номера телефонов клиентов, с которыми нужно связаться.
В общем виде работа системы выглядит следующим образом. PDS рассчитывает сколько номеров нужно набрать, чтобы обеспечить операторов полезной работой до запуска следующего цикла. Далее PDS получает указанное количество номеров из источника заданий и отправляет в АТС команды на осуществление звонков. АТС выполняет набор по полученным номерам и сообщает сервису PDS о результатах. Наборы, где клиент поднял трубку, переводятся на свободных операторов, указанных сервисом PDS (если у читателей возникнет интерес, опишу архитектуру подробней в отдельной статье).
Требования к качеству работы системы
Критически важным моментом для службы Pds является скорость реакции на возникающие в системе события и поступающие запросы. Медленная обработка данных может приводить к снижению показателей качества работы сервиса, например, к большому количеству необработанных звонков или долгому ожиданию операторов. Подобное поведение системы является недопустимым, т.к. приводит к снижению эффективности бизнес-подразделений. Поэтому контроль качества выпускаемого продукта является одним из приоритетных этапов цикла разработки.
В процессе эксплуатации и развития системы возникают новые требования и пожелания пользователей. Поэтому исходный код продукта сильно меняется от версии к версии. В том числе благодаря проводимому рефакторингу и оптимизационным изменениям. Таким образом, перед командой возникает задача в новой версии сохранить качество работы системы.
Обеспечение качества продукта
Нагрузочное тестирование
Чтобы система Pds работала качественно, помимо стандартных практик разработки (code review, функционального тестирования), мы применяем нагрузочное тестирование сервиса с замером скорости выполнения методов.
На ранних этапах ввода системы в эксплуатацию было замечено, что большинство возникавших на боевом контуре сбоев, было вызвано медленной работой определенных участков кода. После оптимизации качество работы системы становилось приемлемым. После одной из «успешных» версий мы провели замеры скорости работы системы, эти значения стали «эталонными».
После завершения разработки и тестирования очередной версии продукта, мы запускаем нагрузочный тест и оцениваем скорость выполнения методов. Далее сравниваем результат с «эталонными» значениями. По результатам сравнения принимаем решение о необходимости оптимизаций и выпуске обновлений.
О применяемых практиках, сценарии тестирования и оценке результатов далее в статье.
Реализация замеров
В проекте сервиса PDS применяется IoC-контейнер для простоты управления зависимостями, модульного тестирования и возможности подмены реализаций. Благодаря этому подходу мы можем обернуть создаваемые контейнером объекты в перехватчики (interceptor), в котором залогировать время выполнения каждого интересующего нас метода, входные параметры и результаты на выходе (логгирующий proxy). Для того, чтобы сервис-компонент поддерживал замер скорости выполнения, нужно отметить специальным маркерным атрибутом LoggingAttribute интерфейс, который реализует компонент. Все интерфейсы компонентов влияющих на скорость работу приложения (обработчики запросов и событий) отмечаются этим атрибутом и при создании оборачиваются контейнером в специальные proxy-объекты.
Ниже пример кода для IoC-контейнера SimpleInjector и библиотеки логирования NLog с комментариями (основная часть кода приведена на сайте SimpleInjector ):
/// <summary> /// Маркерный интерфейс, указывающий на необходимость логгирования времени выполнения методов /// </summary> [AttributeUsage(AttributeTargets.Interface | AttributeTargets.Method)] public sealed class LoggingAttribute : Attribute { } /// <summary> /// Код, отвечающий за регистрацию декорируемых объектов в контейнере /// </summary> public static class RegistrationExtensions { public static void RegisterLoggingInterceptions(this Container container) { // Перехватываем методы типов, помеченных атрибутом LoggingAttribute container.InterceptWith( x => new LoggingInterceptor(LogManager.GetLogger(x.RegisteredServiceType.FullName)), t => Attribute.IsDefined(t,typeof(LoggingAttribute)) ); } public static void InterceptWith(this Container container, Func<ExpressionBuiltEventArgs, IInterceptor> interceptorCreator, Func<Type, bool> predicate) { var interceptWith = new InterceptionHelper(container) { BuildInterceptorExpression = e => Expression.Invoke( Expression.Constant(interceptorCreator), Expression.Constant(e)), Predicate = predicate }; container.ExpressionBuilt += interceptWith.OnExpressionBuilt; } } // добавить код с сайта http://simpleinjector.readthedocs.org/en/latest/InterceptionExtensions.html /// <summary> /// Логгирующий proxy /// </summary> public class LoggingInterceptor : IInterceptor { private readonly Logger _logger; public LoggingInterceptor(Logger logger) { _logger = logger; } [DebuggerStepThrough] public void Intercept(IInvocation invocation) { var isNeedLogging = _logger.IsDebugEnabled || _logger.IsErrorEnabled; if (!isNeedLogging) { // вызываем без замера invocation.ProceedWithInnerExceptionRethrow(); return; } var method = invocation.GetConcreteMethod(); // вызываем декорируемый объект var watch = Stopwatch.StartNew(); invocation.ProceedWithInnerExceptionRethrow(); watch.Stop(); _logger.Log(LogLevel.Debug, "service={0};method={1};duration={2}", _logger.Name, method.Name, watch.ElapsedMilliseconds); } } internal static class InterceptorHelper { internal static void ProceedWithInnerExceptionRethrow(this IInvocation invocation) { try { invocation.Proceed(); } catch (TargetInvocationException ex) { ExceptionDispatchInfo.Capture(ex.InnerException).Throw(); } } }
Сообщения логгера выводятся в отдельный файл, данные из которого попадают в анализатор логов.
Удобство такого подхода заключается в том, что замер скорости выполнения при необходимости можно включить для конкретного сервиса/компонента через конфигурацию логгера (без перезапуска приложения) – это позволяет выявить проблемы производительности в конкретном компоненте. В качестве недостатков подхода стоит отметить оборачивание объектов-сервисов в прокси. Это вызывает неудобства при разработке (инспекции объектов) и небольшие потери производительности за счет добавления лишнего вызова и логики в методы прокси-объекта. От первого недостатка можно избавиться, добавив в трансформацию конфигов правило, включающее проксирование только для нужных контуров (QA, Prod). Со вторым недостатком приходится мириться, т.к. польза, приносимая им гораздо больше – всегда можно включить диагностику без перезапуска приложения.
Стоит отметить, что аналогичного результата можно было добиться использованием любого .net-профилировщика (dotTrace, ANTS и т.д.), но на боевом контуре при реальной работе сервиса использовать профилировщик сложно.
Пример данных из лог-файла:
dateTime=2016-01-25 16:00:27.3451;service=TCSBank.Pds.HostingService.PdsOperationService;method=SetReady;duration=0.0142099; dateTime=2016-01-25 16:00:31.6109;service=TCSBank.Pds.HostingService.PdsMonitoringService;method=GetStatisticsAll;duration=0.0002707; dateTime=2016-01-25 16:00:31.6109;service=TCSBank.Pds.HostingService.PdsMonitoringService;method=GetUsersByQueueId;duration=0.0005592; dateTime=2016-01-25 16:00:34.2828;service=TCSBank.Telephony.Core.Pds.IOperatorBoardService;method=RefreshUsers;duration=0,0323294; dateTime=2016-01-25 16:00:36.6110;service=TCSBank.Pds.HostingService.PdsMonitoringService;method=GetUsersByQueueId;duration=0.0006961; dateTime=2016-01-25 16:00:36.7204;service=TCSBank.Pds.HostingService.PdsMonitoringService;method=GetStatisticsAll;duration=0.0003596; 16:00:41.6112;service=TCSBank.Pds.HostingService.PdsMonitoringService;method=GetUsersByQueueId;duration=0.0002869; dateTime=2016-01-25 16:00:46.6113;service=TCSBank.Pds.HostingService.PdsMonitoringService;method=GetStatisticsAll;duration=0.0002227; dateTime=2016-01-25 16:00:49.0177;service=TCSBank.Telephony.Core.Pds.IOperatorBoardService;method=SetReadyState;duration=0,0096144; dateTime=2016-01-25 16:00:49.0177;service=TCSBank.Pds.HostingService.PdsOperationService;method=SetReady;duration=0.0109496; dateTime=2016-01-25 16:00:49.0489;service=TCSBank.Telephony.Core.Pds.IOperatorBoardService;method=SetReadyState;duration=0,0086434; dateTime=2016-01-25 16:00:49.0489;service=TCSBank.Pds.HostingService.PdsOperationService;method=SetReady;duration=0.0097707; dateTime=2016-01-25 16:00:51.6115;service=TCSBank.Pds.HostingService.PdsMonitoringService;method=GetStatisticsAll;duration=0.0003434; dateTime=2016-01-25 16:00:51.6271;service=TCSBank.Pds.HostingService.PdsMonitoringService;method=GetUsersByQueueId;duration=0.000687; dateTime=2016-01-25 16:00:54.2834;service=TCSBank.Telephony.Core.Pds.IOperatorBoardService;method=RefreshUsers;duration=0,0200767;
Проведение нагрузочного тестирования
Мы проводим нагрузочное тестирование после завершения функционального и регрессионного тестирования. Т.е. непосредственно перед выпуском продукта, когда проверена корректность работы системы.
Для корректной оценки скорости выполнения методов, замер важно выполнять при нагрузке приближенной к реальной, т.к. количество одновременных запросов (параллельных потоков) имеет значение. Поскольку нагрузка на боевом контуре значительно отличается от тестового, а количество тестировщиков, которых можно привлечь к проведению теста ограничено, для генерации нужного объема нагрузки нами был разработан эмулятор работы оператора. Эмулятор – одностраничное web-приложение, которое посылает запросы и обрабатывает результат аналогично пользователю системы. Идентификационные данные (логин) и параметры поведения (количество секунд, до совершения следующего действия) можно передавать приложению через url, что очень удобно при массовом запуске.
Тест проводится по следующему сценарию с машин разработчиков или выделенных виртуальных машин запускается по нескольку экземпляров эмулятора, которые отправляют сервису Pds нужные сигналы (команды), сервис забирает нужное количество номеров телефонов и отправляет их АТС. Для нагрузочного теста используется специально выделенный номер телефона, для которого на АТС прописано заданное поведение: ответить (снять трубку) через случайное количество секунд, положить трубку через случайное количество секунд.
Тест проводится в течение 15-20 минут. За это время набирается достаточное для анализа количество данных.
В дальнейшем планируем добавить контроль за нагрузкой на базу данных с помощью Oracle Enterprise Manager.
Оценка результатов теста
Записанные в результате теста измерения попадают в анализатор лога, который формирует таблицу с результатами. Таблица состоит из следующих столбцов:
- Service – название сервиса, метод которого вызывается.
- Method – название метода, который вызван для обработки.
- Count – количество вызовов метода за тест.
- AvgTime – среднее время выполнения метода.
- MinTime – минимальное время выполнение метода.
- MaxTime – максимальное время выполнение метода.
- Delta – разница между максимальным и минимальным временем выполнения.
Пример таблицы с результатами замеров.
Service | Method | Count | AvgTime | MinTime | MaxTime | Delta |
---|---|---|---|---|---|---|
TCSBank.Telephony.Core.Pds.ICustomerBoardService | FindCustomerByUniqueId | 60 | 0.006371 | 0.003428 | 0.010329 | 0.006901 |
TCSBank.Pds.HostingService.PdsOperationService | GetAcdAnswerer | 26 | 0.450426 | 0.011836 | 4.247844 | 4.236008 |
TCSBank.Telephony.Core.Pds.IPdsCoreService | OnConnectionSuccess | 56 | 0.119736 | 0.093473 | 0.526525 | 0.433052 |
Подобные таблицы с результатами работы методов публикуются на wiki-страницах проекта для каждой выводимой версии и доступны для оценки и сравнения.
Также оценивается соотношение принятых и пропущенных вызовов, полученных при данной скорости обработки. Если между версиями наблюдается значительное увеличение скорости выполнения методов, приводящее к росту числа пропущенных вызовов, в продукт вносятся оптимизационные изменения, после чего нагрузочный тест проводится повторно.
В дальнейшем мы дополним данный подход анализом нагрузки на базу данных, вызванной проведением теста.
Резюме
Применение описанного метода позволяет нам гарантировать качество работы системы и оперативно разбираться с нештатными ситуациями на боевом контуре. Хочу отметить, что этот подход применим не для всех систем и этапов жизненного цикла продукта. Какие методы применять для обеспечения качества в каждом конкретном случае решать Вам. Спасибо за проявленный интерес и успешной работы.
ссылка на оригинал статьи https://habrahabr.ru/post/277643/
Добавить комментарий