Как стать автором
Обновить

Генерация тестовых данных и нагрузочные K6 тесты для сервиса поиска

Время на прочтение4 мин
Количество просмотров5.4K

Всем привет! В данной статье расскажу о том, как мы решали задачу нагрузочных тестов для сервиса поиска, как познакомились с замечательным 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.

Шикарнейшая документация сильно упростила нам всю работу. Для решения задачи нам потребовалось:

  1. Установка

  2. Запуск

  3. Отправка запросов

  4. Батчи

  5. Проверки

  6. Фиксированный уровень нагрузки

  7. Возможность выставить граничные значения

  8. Отчеты

Итого весь код скрипта нагрузки на сервис со всеми нужными нам ништяками уложился в 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

Теги:
Хабы:
+3
Комментарии0

Публикации