Банковское ПО под давлением: как нагрузочное тестирование защищает системы от сбоев
Я занимаюсь выявлением дефектов производительности ПО в Газпромбанке и, в частности, работаю с нашей бэк-офисной системой и СУБД Sybase. СУБД обеспечивает хранение данных и выполняет логику финансовых операций с помощью хранимых процедур. В нашем стриме, в котором есть разработчики, аналитики и функциональные тестировщики, нагрузочным тестированием всей системы занимаются два человека.
Нагрузочное тестирование банковского ПО — это способ убедиться, что всё будет работать гладко, когда это нужно больше всего. Тесты имитируют реальные сценарии и проверяют, как система справляется с давлением и стрессом. Это не просто о том, чтобы ПО работало: важно, чтобы оно работало быстро, без сбоев и было готово ко всему. Если система не выдерживает нагрузки, то клиенты могут столкнуться с длительными ожиданиями или даже потерей доступа к своим деньгам.
Чем мы занимаемся
Нам важно понимать, как система ведёт себя под нагрузкой, где узкие места и как их распознать. У нас два направления работы: тестирование новых фич или процедур и работа над инцидентами, связанными с производительностью. В первом случае берём эту разработку в тестирование и смотрим, как она влияет на систему. Если замечаем проблему, то локализуем её и отправляем информацию разработчикам для оптимизации.
А если случился «затык» на проде — система неожиданно начала вести себя неадекватно и произошёл инцидент, связанный с производительностью, — то мы тут же проводим разбор полётов на нагрузочном стенде. Найдена ошибка? Создаём отчёт, отправляем его разработчикам. После исправления проверяем, исчез ли баг. Но если он живуч и повторяется, то снова запускаем цикл.
Многопоточность и оптимизация процедур для повышения производительности
Сердцевина нашей системы — СУБД Sybase ASE (бывшая Sybase, ныне — ASE под крылом SAP). Здесь хранятся процедуры, которые через адаптер очередей вызываются другими системами. Этот адаптер взаимодействует с брокерами сообщений Apache Kafka или Message Queues (MQ), откуда получает запросы. Они обрабатываются, и, в свою очередь, идёт запрос в базу данных, а полученный из базы ответ отправляется назад в очереди. Есть и прямые вызовы, когда пользователи систем делают запрос непосредственно в базу данных и получают оттуда нужный им результат.
Поскольку вся логика выполняется в базе данных, то самым узким местом здесь является загрузка процессора.
СУБД Sybase отличается от аналогичных систем тем, в частности, что в ней есть возможность распределять нагрузку с помощью процессорных пулов. Это позволяет выделять для разных пользователей и процессов свои зоны, предотвращая конфликты. Даже если один бизнес-процесс вдруг начинает «нагреваться», то он не тормозит другие потоки, которые используют свои процессорные пулы. Эту возможность мы и используем для обеспечения работы системы, изолируем нагрузки и оптимально распределяем производительность. Получается плавная работа всей системы даже под давлением. Т. е. мы распределяем процессорные пулы так, чтобы при возникновении проблем в каких-то из них они минимально влияли на производительность всей системы.
Кроме того, мы оптимизируем данные и сами процедуры, чтобы они тратили меньше процессорного времени. Процедура перед исполнением кэшируется, и для неё компилируется план запроса, по которому происходит обработка данных. На создание такого плана уходит время работы процессора.
Если при разных вызовах одной и той же процедуры создаются таблицы с различными типами данных (а такое бывает при использовании кастомных типов), то формируются два плана запросов. Это избыточно загружает процессор.
Чтобы избавиться от создания лишних планов запросов, т. е. ненужной рекомпиляции, мы оптимизировали процедуры так, чтобы создавались временные таблицы стандартного вида и туда попадали данные одного и того же типа. В этом случае план запроса компилируется только один раз, и на это требуется меньше процессорного времени. Благодаря такой оптимизации мы улучшили производительность всей системы — количество запросов, которые обрабатываются системой без деградации утилизации CPU, увеличилось на 30%.
Проблемы
В нашей архитектуре Sybase выступает не просто как хранилище данных, а как полноценный «интеллектуальный центр», работающий подобно бэк-приложению.
Например, при переводе денег с карты на карту база не просто обновляет строки в какой-то таблице у обоих пользователей, а выполняет многоступенчатую валидацию: проверяет корректность транзакции, достаточность средств и соблюдение условий безопасности. Вся вычислительная логика реализуется в хранимых процедурах на уровне базы данных.
Такая организация системы с плотной интеграцией логики в базе данных очень удобна, но у неё есть и серьёзные проблемы, которые, впрочем, не связаны с Sybase. В частности, наша система является монолитом, и поэтому её очень сложно масштабировать, когда это требуется при увеличении нагрузки по какому-нибудь потоку или при необходимости увеличения мощности какого-то сервиса.
Сейчас мы пытаемся преобразовать монолитную архитектуру в микросервисную. Это упростит масштабирование и управление нагрузкой.
Мониторинг работы СУБД при нагрузочном тестировании
Тестирование без мониторинга — штука практически бесполезная. С помощью мониторинга базы данных мы получаем доступ к важной информации, в частности, просматриваем утилизацию процессорных пулов и дисков.
С недавнего времени мы можем мониторить утилизацию процессора непосредственно по запросам. С помощью такого инструмента мы видим, какие процедуры потребляют больше всего процессорного времени.
Вообще мы мониторим много чего. Мы анализируем время выполнения запросов и количество запросов в единицу времени. Смотрим, какие блокировки возникают, из-за каких процессов и на каких объектах. Смотрим коннекты и мониторим репликацию, т. е. отставание реплики от мастера. Оцениваем нагруженность очередей и статусы процессов, а также, естественно, контролируем отклик системы, чтобы всегда знать, когда она начинает тормозить.
Утилизацию памяти мы не анализируем по очень простой причине: с этим у нас никогда не было проблем. Но при необходимости можно делать и это.
По моему субъективному мнению, Sybase очень хорошо сделана в плане мониторинга. Её легко мониторить, и это занимает мало ресурсов самой СУБД.
Как мы организуем мониторинг
В системе мониторинга у нас используются два агента. Первый разработан на базе Apache Jmeter: это java-приложение с открытым исходным кодом. Агент работает круглосуточно и делает запросы к системным процедурам и таблицам Sybase. За счёт этого он получает данные о том, какой у нас статус процессов, какие есть блокировки, сколько коннектов. С его помощью мы можем узнать, как происходят рекомпиляции, т. е. компиляции плана запросов для выполнения какой-либо процедуры.
Данные из агента отправляются в базу данных InfluxDB, специально разработанную для хранения и извлечения данных временных рядов. Затем они передаются в Grafana для визуализации.
Рекомпиляция запросов
Пример запроса
select
@@servername servername,
cmd,
affinity,
count(*) as cnt
from
master..sysprocesses
where
lower(cmd) like 'compiling'
group by
cmd,
affinity
Зачем смотреть
- Утилизируют CPU.
- При устранении рекомпилингов удалось повысить производительность на 30%.
- Происходят из-за расхождения структуры ХП.
Блокировки
Пример запроса
select
@@servername servername,
ps.spid as spid,
suser_name(ps.suid) as login,
ps.status,
ps.hostname,
ps.program_name,
ps.cmd,
ps.blocked,
db_name(lo.dbid) as dbname,
object_name(lo.id,lo.dbid) as blocked_obj,
lo.type,
lo.context,
ps.execution_time
from master..sysprocesses ps, master..syslocks lo
where lo.type >= 256
and ps.spid in (
select blocked
from master..sysprocesses
where blocked !=0
)
Master..syslocks
Содержит информацию об активных блокировках
Что важно
- Время блокировки.
- Заблокированный объект.
- Имя пользователя.
- Запрос.
Время выполнения запросов
1. Получаем SPID
select @SPID = spid
FROM master..sysprocesses
where hostname='our.host' OR status LIKE '%run%'
2. Получаем информацию о запросе
select SPID,
db_name(DBID) as db_name,
object_name(ProcedureID, DBID) as procedure_name,
PlanID,
BatchID,
ContextID,
LineNumber,
CpuTime,
MemUsageKB,
PhysicalReads,
LogicalReads,
StartTime
from master..monProcessStatement
where SPID = @SPID
Master..monProcessStatement
Содержит информацию о состоянии выполняемых запросов
Что можем увидеть
- Время начала запроса.
- Из-под какой процедуры был вызван запрос.
- Номер строки.
- Затраченное время CPU на выполнение.
- Использовано памяти.
- Сколько прочитано байт с диска.
- Сколько прочитано байт из кэша.
- Количество строк, на которые влияет запрос.
Второй агент — это bat-script, который периодически делает запросы к базе данных, а полученную информацию сохраняет в текстовом log-файле. Эта информация затем передаётся в систему агрегирования логов ELK.
С помощью bat-script отслеживаем снятие плана запроса по процессам. Раньше с помощью этого скрипта смотрели и время выполнения подзапросов в процедурах, но с недавних пор эта задача также решается через агента на базе Jmeter.
Второй агент запускается нерегулярно — только в тех случаях, когда есть подозрение, что план запроса настроен неоптимально и не попадает в индекс базы данных.
Анализ процессов
master.dbo.sysprocesses
Основная таблица для мониторинга процессов
Основные поля:
- Spid — ID процесса.
- Status — статус процесса.
- Suid — ID пользователя, из под которого запущен процесс.
- Hostname — хост, откуда пришёл запрос.
- Program_name — интерфейс, который запустил процесс.
- Cmd — текущая команда, которая выполняется.
- Cpu — время, затраченное ЦПУ на выполнение.
- Affinity — процессорный пул, в котором выполняется процесс.
- Linenum — номер строки процедуры, откуда был сформирован запрос.
Получение плана запроса по SPID
exec sp_showplan SPID, NULL, NULL, NULL
Статусы процессов
Метрики по утилизации системных ресурсов, таких, как диски, память, CPU, собираются с помощью Zabbix-agent. По утилизации CPU у нас определён SLA равный 80%. Это связано с тем, что при такой утилизации из-за неравномерной нагрузки у нас появляются процессы, попадающие в очередь на выполнение, и, следовательно, могут возникать блокировки.
Очередь процессов
Для нас основное значение имеет мониторинг утилизации процессорных пулов, а не всего CPU. Причина этого заключается в том, что бизнес-процессы генерируют запросы с разной интенсивностью. Например, поступает очень много запросов по переводам с карты на карту, и процессорный пул не справляется с таким потоком. При этом утилизация такого процессорного пула возросла и может достигать 100%. Полная же утилизация процессора может быть меньше — около 70%. Если ориентироваться только на мониторинг производительности всего CPU, то вроде бы всё нормально: мы находимся ниже заданного уровня SLA. Но на самом деле у нас могут быть проблемы в базе данных, и в отдельных процессорных пулах уже выстраиваются очереди.
Когда, по данным мониторинга загруженности процессорных пулов, мы видим такую картину, можно увеличить конкретный пул, чтобы он мог обрабатывать такой поток, и мы не столкнулись бы с блокировкой каких-то бизнес-процессов. В реальности это не всегда приводит к необходимости увеличения процессорного пула, поскольку для нас в первую очередь важно понять, не связано ли всё это с какой-то неоптимизированной процедурой, которая вызывает, например, рекомпиляцию. И, прежде чем увеличивать пул, мы должны убедиться, что не можем оптимизировать эту процедуру.
На первый взгляд, работа нагрузочного тестировщика может показаться монотонной: каждый день — поиск дефектов и «копание» в системе с целью её оптимизации. Но на самом деле это сродни настройке огромного сложного механизма, где от одного лишнего винтика может зависеть весь процесс. Мы не просто исправляем ошибки — мы предсказываем, предупреждаем и прокладываем «путь» системе, чтобы она была готова к любым нагрузкам.