Всем привет! В данной статье расскажу о том, как мы решали задачу нагрузочных тестов для сервиса поиска, как познакомились с замечательным 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 на Хабре от замечательных авторов
Как я сократил код для нагрузочного тестирования в три раза
ссылка на оригинал статьи https://habr.com/ru/post/691108/
Добавить комментарий