Всем привет! В данной статье расскажу о том, как мы решали задачу нагрузочных тестов для сервиса поиска, как познакомились с замечательным K6 и о том, как ведет себя облачный Elastic Search под нагрузкой.
ТЗ: Нужно протестировать мульти-тенант (multi-tenant) сервис поиска. У каждого тенанта свой собственный индекс в Elastic Search. Количество тенантов = 100. Количество документов в каждом тенанте = 500 000. Количество пользователей 90 тенантов по 20 пользователей + 10 тенантов по 100 пользователей. Каждый пользователь выполняет по одному запросу раз в 5 минут максимум.
Выбор подхода генерации тестовых данных
Первая задача - генерация тестовых данных. Для оценки была нарисована схема согласно которой данные попадают в поиск в наших сервисах
Согласно схеме подход "в лоб" требует задействовать множество других сервисов и ресурсов. Мы решили что это не есть гуд - и упростили схему
Было решено отказаться от всех промежуточных сервисов. В тестируемом сервисе был использован метод для создания индекса. Документы же было решено записывать в Elastic напрямую используя логику, аналогичную тестируемому сервису.
Минус такого подхода: Если меняется логика в сервисе - то ее нужно будет поменять и в утилите для генерации тестовых данных.
Генерация тестовых данных
Очевидно что если добавлять в Elastic по одному документу 50 000 000 раз, то процесс генерации будет совсем небыстрым. Для ускорения процесса генерации мы использовали две фишки: добавление документов в Elastic батчами в несколько потоков в исходный индекс. Затем этот индекс склонировали нужное количество раз.
В итоге 50 000 000 документов сгенерили за 1 минуту.
Графически процесс генерации выглядит так
Здесь пример модуля по работе с Elastic через NEST
using Nest;
using System;
using System.Collections.Generic;
using System.Dynamic;
namespace ElasticApiClient
{
public class NestClient
{
private readonly ElasticClient _api;
public NestClient(string url, string user, string password)
{
var connectionSettings = new ConnectionSettings(new Uri(url));
_api = new ElasticClient(connectionSettings);
connectionSettings.BasicAuthentication(
user,
password);
}
public void DeleteUnusedIndices()
{
var response = _api.Indices.GetAsync(new GetIndexRequest(Indices.All)).GetAwaiter().GetResult();
foreach (var index in response.Indices)
{
var indexName = index.Key;
var countRequest = new CountRequest(Indices.Index(indexName));
var numberOfDocuments = _api.CountAsync(countRequest).GetAwaiter().GetResult().Count;
if (numberOfDocuments == 0)
{
_api.Indices.DeleteAsync(indexName).GetAwaiter().GetResult();
}
}
}
public void CloneIndices(string sourceName, List<string> targetNames)
{
_api.Indices.UpdateSettingsAsync(Indices.Index(sourceName), u => u
.IndexSettings(i => i
.Setting("index.blocks.write", true)
)
).GetAwaiter().GetResult();
foreach (var targetName in targetNames)
{
_api.Indices.CloneAsync(new CloneIndexRequest(sourceName, targetName)).GetAwaiter().GetResult();
}
_api.Indices.UpdateSettingsAsync(Indices.Index(sourceName), u => u
.IndexSettings(i => i
.Setting("index.blocks.write", false)
)
).GetAwaiter().GetResult();
}
public void DeleteTestIndices(List<string> testTenantIds)
{
var testIndexNames = new List<string>();
foreach (var testTenantId in testTenantIds)
{
testIndexNames.Add($"{testTenantId}-documents");
}
var response = _api.Indices.GetAsync(new GetIndexRequest(Indices.All)).GetAwaiter().GetResult();
foreach (var index in response.Indices)
{
var indexName = index.Key;
if (testIndexNames.Contains(indexName.Name))
{
_api.Indices.DeleteAsync(indexName).GetAwaiter().GetResult();
}
}
}
public void IndexMany(List<ExpandoObject> expandos, string indexName)
{
var ids = new List<Guid>();
foreach (var expando in expandos)
{
var byName = (IDictionary<string, object>)expando;
var documentId = (Guid)byName["documentId"];
ids.Add(documentId);
}
var id = 0;
_api.Bulk(bd => bd.IndexMany(expandos, (descriptor, s) => descriptor.Index(indexName).Id(ids[id++])));
}
}
}
K6 - это мегакрутая штука для нагрузки!
Нагрузку на сервис решили сделать через K6. Здесь можно глянуть сравнение K6 и JMeter.
Шикарнейшая документация сильно упростила нам всю работу. Для решения задачи нам потребовалось:
Установка
Запуск
Отправка запросов
Батчи
Проверки
Фиксированный уровень нагрузки
Возможность выставить граничные значения
Отчеты
Итого весь код скрипта нагрузки на сервис со всеми нужными нам ништяками уложился в 200 строчек.
Как ведет себя Elastic под нагрузкой
У нас используется облачный инстанс Elastic. В нем есть такая штука как CPU Credits. То есть если нагрузка на Elastic превышает оплаченный лимит, то CPU Credits начинают стремительно расходоваться, уходят в ноль, а response time соответственно начинает резко расти. Если нагрузку убираем, то CPU Credits потихоньку восстанавливаются. Графически процесс выглядит так
По ТЗ сервис в максимуме должен отрабатывать 9.33 запроса в секунду
maxRequestsPerUser = once in 5 minutes = 0.2 requests per minute
totalNumberOfUsers * maxRequestsPerUser = 2800 * 0.2 = 560 requests per minute = 9.33 requests per second
maxRequestsPerSecond = 9.33 requests\s
15 запросов в секунду наш инстанс Elastic отработал без проблем. А вот на 20 запросах в секунду - проблемы уже будут и потребуется заплатить за более мощный инстанс Elastic.
Итого
По результатам проделанной работы сделали утилиту для быстрой генерации тестовых данных, освоили K6, выяснили максимально допустимое число запросов в секунду для стабильной работы сервиса на заданных мощностях. Спасибо за внимание. Всем мира!
P.S. Еще статьи про K6 на Хабре от замечательных авторов
Как я сократил код для нагрузочного тестирования в три раза
Инструментарий для нагрузочного тестирования и не только
Интеграция нагрузочного тестирования на Grafana K6 в CI/CD