Итак, вы измерили и настроили свою систему, как описано в Части I и Части II этой серии сообщений в блоге. Теперь вы хотите получить цифры, сколько конечных пользователей сможет обслуживать ваша система. Поэтому вы определяете сценарии, которые будут типичными для того, что делают ваши пользователи.
Одним из таких пользовательских сценариев может быть:

  • авторизоваться
  • сделай что-нибудь
  • выйти

Поскольку ваши пользователи не будут стоять в очереди и ждать, пока другие пользователи закончат свои дела, темп, который вам нужен для тестирования вашей определенной системы, — это «запуск n сценариев каждую секунду». Многие сценарии, имитирующие разных пользователей, могут выполняться параллельно. Если вашему сценарию требуется 10 секунд для завершения, а вы будете запускать 1 раз в секунду, это означает, что ваша система должна быть способна обрабатывать 10 пользователей параллельно. Если это не справится, вы увидите, что параллельно выполняется более 10 сеансов, и время, необходимое для обработки такого сценария, увеличится. Вы увидите, как использование ресурсов сервера будет расти и расти, и, наконец, оно взорвется.

Чтобы найти узкие места в вашем сценарии, вы используете зонды statsd, как это делали мы в последних статьях. Поскольку мы хотим оценить возможную пропускную способность нашей системы, мы загоним ее в красную область, и, наконец, заставим ее загореться — чтобы было понятно — нам нужно сделать это, чтобы увидеть возможные пределы.

Как создавать сценарии использования

Вы должны моделировать клиентов параллельно. Одним из решений было бы иметь один поток для каждого клиента. Однако порождение потоков не так уж и дешево, и вы в конечном итоге перегрузите свою систему, выполняя инъекцию нагрузки. Поскольку ваш клиент все равно будет ждать ответов от ArangoDB, мы можем использовать цикл событий для обработки тестовых запусков параллельно: gevent.

Соблюдайте правила событийного цикла!

Мы используем pyArango, который использует запросы Python в качестве своего бэкэнда. Поскольку запросы python используют блокирующий ввод-вывод (таким образом — ждут ответа сервера), мы должны заменить его на неблокирующую версию. Мы можем использовать monkey patching, чтобы заменить его под капотом:

Как видите, мы сообщаем об отключениях/подключениях в статистику, поэтому мы также можем отслеживать, что это не происходит часто (поскольку это также имеет значение для производительности).

Обращение с работниками

В Gevent есть концепции гринлетов как одного легковесного процессора. Мы собираемся сопоставить один гринлет с одним сценарием.

Основной процесс в Python будет порождать гринлеты (пока мы называем их рабочими). Один гринлет станет жизненным циклом одного такого смоделированного пользовательского сеанса.

Есть два способа терминации гринлетов, один из основных процессов — их сбор. Затем это будет ждать освобождения ресурсов зеленых, пока процесс не завершится. Таким образом, мы могли сделать это только после нашего тестового сценария. Поэтому эти гринлеты будут задерживаться, используя память, ожидая окончательного завершения тестового прогона. Поскольку мы также собираемся проводить тесты на выносливость, они будут накапливаться до тех пор, пока наша основная память не будет израсходована — не очень разумно. Таким образом, мы пойдем другим путем, мы поднимем GreenletExit, чтобы сказать gevent, что он должен их очистить:

Масштабирование нагрузочного теста

Наш процесс моделирования загрузки Python должен:

  • симулировать поток нашего приложения
  • генерировать почтовые документы json
  • генерировать http-запросы
  • анализировать http-ответы
  • парсить json-ответы
  • проверьте ответы на ошибки

Таким образом, нам также нужно определенное количество ЦП. Поскольку мы решили использовать один поток с гринлетами, мы ограничены максимальной пропускной способностью, которую может обработать один процесс Python. Поскольку мы хотим, чтобы симулятор выдавал стабильный шаблон, мы должны следить за нашим процессом python и его использованием ЦП. В то время как в многоядерной системе для многопоточных процессов может быть возможно более 100% загрузки ЦП, это не так для нашего процесса Python. Чтобы иметь стабильный паттерн, мы не должны использовать более 80% ЦП.

Если мы хотим генерировать большую нагрузку, нам нужно запустить больше процессов Python и, таким образом, иметь возможность разделить пользовательскую базу, на которой мы запускаем тест, среди этих процессов.

Как лучше всего оценить возможную пропускную способность?

Теперь, когда у нас есть достаточные сведения обо всем, что работает, — как нам оценить возможную пропускную способность данной системы?

Хотя ArangoDB может пережить короткий «всплеск», на самом деле это не то, что нам нужно. Нам интересно узнать, какую нагрузку он может выдержать в течение более длительного периода времени, включая работу внутренних фоновых задач базы данных и т. д. Мы определяем время нарастания, которое система должна выдерживать при определенной схеме нагрузки. т. е. 4 минуты могут быть хорошим временем разгона для первой оценки. После того, как он пережил время нарастания, мы нажимаем сильнее, увеличивая схему нагрузки. Таким образом, мы сможем быстро сузить возможную полосу пропускания.

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

Как определить, что SUT исчерпан?

Есть несколько показателей, по которым ваша ArangoDB, также известная как тестируемая система (SUT), исчерпана:

  • Количество файловых дескрипторов — (поэтому мы заранее добавили возможность мониторинга в collectd) Каждое подключение клиента к БД равняется файловому дескриптору, плюс виртуальная память БД и открытые лог-файлы, если это кластерное подключение внутри нод, и т. д. это должно быть довольно постоянное число. Как только вы увидите, что это число растет, число параллельно работающих клиентов увеличивается из-за того, что отдельные клиенты не готовы вовремя из-за того, что запросы к базе данных занимают больше времени.
  • Частота ошибок на клиенте. Вы обязательно должны отлавливать ошибки в своем клиентском потоке, и как только они происходят, сообщать statsd, что они происходят, и записывать в журнал какие-либо выходные данные. Однако вам нужно убедиться, что они не из-за повторного использования идентификатора и, следовательно, перекрывающихся запросов, которые обычно не происходят.
  • Загрузка ЦП — после того, как все ЦП будут загружены до определенного уровня, дополнительная нагрузка будет невозможна. Как только это произойдет, вы ограничены процессором
  • Количество операций ввода-вывода. Если диск не может читать/записывать больше данных одновременно, вы ограничены операциями ввода-вывода.
  • Память — ее сложнее всего обнаружить, поскольку система может многое делать с памятью, например, использовать ее для дискового кэша, имитировать доступную память из подкачки и так далее. Таким образом, привязка к памяти обычно указывает только на то, что диск и ЦП находятся значительно ниже красных уровней.
  • Карты памяти — они используются для резервного копирования больших кусков памяти на диске или для загрузки динамических библиотек в процессы. Ядро Linux дает обзор в /proc//maps. Мы сможем отслеживать их с помощью collectd 5.8.
  • Доступные контексты V8 — хотя можно выполнять широкий спектр запросов AQL без использования Javascript / V8, Транзакции, Определяемые пользователем функции или Микросервисы Foxx требуют их; и, таким образом, они могут стать редким ресурсом

Запускаем пример

Инициализация данных

Чтобы получить приблизительное представление о возможностях ArangoDB, мы создадим крошечный график, который мы можем использовать позже в запросах документов или обходах:

Фактический тестовый код

Мы создаем пример, выполняющий транзакцию с запросом графа, обновляющим некоторые найденные нами вершины уровня 1. Мы запускаем некоторые другие запросы, чтобы напрямую изменить основную вершину. Мы думаем, что этот вариант использования представляет собой процедуру входа в систему, которая подсчитывает входы пользователя и группы, в которые он входит. Мы контролируем диапазон пользователей и шаблон загрузки, который мы хотим внедрить, с помощью параметров командной строки:

Дополнительные зависимости приведенного выше кода могут быть установлены следующим образом в системе Debian:

Анализ результатов тестирования

Мы запускаем первую загрузочную форсунку в 17:51 : ./test.py 100 2000 1 и вторую, увеличивая нагрузку в 17:55: ./test.py 2000 4000 1

Вот что мы видим в kcollectd из тестового прогона (График 1 внизу):

Мы анализируем этот график, начиная с самого низкого и поднимаясь вверх график за графиком. Поскольку KCollectd может группировать датчики только на одной оси и не может выполнять какое-либо преобразование значений, графики сгруппированы по диапазонам значений, чтобы мы могли видеть значимые отклонения. В Graphite мы можем добавить вторую ось Y, добавить шкалы и т. д., чтобы лучше сгруппировать значения по их значению, а не по значению.

График 1

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

В текущем тестовом прогоне мы видим, что этот счетчик ошибок начинает расти в 17:57 — таким образом, сервер не выдерживает шаблон нагрузки. В выводе ошибки теста мы видим фактическую причину:

Таким образом, ресурс, который у нас закончился, — это контексты V8 для выполнения нашей транзакции. Однако процентиль запросов указывает, что время выполнения постоянно.

График 2

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

График 3

На 3-м графике мы видим, что количество отображаемых в память файлов удваивается с менее 4 КБ до более 8 КБ — что было бы примерно в ожидаемых цифрах при удвоении тестового сценария.

График 4

На четвертом графике мы видим, что использование ЦП пользовательского пространства удваивается, в то время как использование ЦП системы резко возрастает. Частью работы с большим количеством потоков является блокировка ресурсов, совместно используемых потоками. Таким образом, использование процессорного пространства ядра увеличивается с перегрузкой сервера.

График 5

Пятый график показывает количество потоков и количество открытых файловых дескрипторов и серверных потоков. Когда мы поднимаем груз, мы видим, как они оба поднимаются. Таким образом, эффект, который мы видим, заключается в том, что клиент нагрузочного теста открывает больше tcp-соединений (которые также являются файловыми дескрипторами). Сервер порождает больше потоков, чтобы следовать сценарию нагрузки, однако доступная пропускная способность в этой системе не больше.

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

Вывод

Мы успешно продемонстрировали тестовый клиент, имитирующий клиентские сценарии. Поскольку мы выполняем большую часть нашей рабочей нагрузки в транзакции и не передаем большие ответы клиенту, комбинация python/gevent может легко создать желаемый шаблон загрузки. SUT может поддерживать пропускную способность 1 сценарий в секунду. В последующих тестовых прогонах линейного изменения можно попытаться оценить более мелкозернистыми шагами, возможно ли, например, 1,5 сценария в секунду. С таким результатом испытание на выносливость может быть выполнено в течение более длительного периода времени.

Как ранее публиковалось на ArangoDB