ScyllaDB: как настраивать метрики в java-драйвере и параметры запросов для измерения их характеристик

от автора

Привет, Хабр! Это Александр Коваль, я разработчик IoT-сервисов в МТС Web Services. При работе с данными часто возникает вопрос: как быстро система может вернуть результат по определенным параметрам? Не является исключением и ScyllaDB.

Для ответа нужны инструменты измерения и возможность настраивать систему. Java-драйвер для ScyllaDB умеет передавать информацию о своей внутренней работе, и ему можно настроить отдельные компоненты. Звучит как отличный план — в этом материале я поделюсь результатами экспериментов с java-драйвером для ScyllaDB при различных запросах к данным.

Код, ссылки и ресурсы располагаются в GitHub.

Данные и запросы

Для примера создадим в ScyllaDB таблицу с помощью Cql-скрипта:

CREATE TABLE IF NOT EXISTS property ( group text, name text, date timestamp, value_string text, PRIMARY KEY((group,name),date)) WITH CLUSTERING ORDER BY (date DESC);

Она хранит изменения (date) одного свойства с именем (name) и принадлежит определенной группе (group). Само значение хранится в текстовом виде (value_string). Group и name задают основной ключ (primary key). Date — кластерный (cluster key).

Примечание: в скриптах и примерах для простоты нет информации о keyspace.

Запрос:

SELECT group, name, date, value_string FROM property WHERE group=:group AND name=:name AND date>=:start AND date<:end

Он выполняется через вызов сервиса, реализованного связкой Spring Boot 3, и асинхронного api java-драйвера.

Код запроса в контроллере:

@GetMapping("/find") public CompletionStage<Stream<Property>> find(        @RequestParam(name = "group") String group,        @RequestParam(name = "name") String name,        @RequestParam(name = "start") Instant start,        @RequestParam(name = "end") Instant end,        @RequestParam(name = "offset") int offset,        @RequestParam(name = "limit") int limit ) {    return propertyService.findByData(group, name, start, end, offset, limit); }

И в репозитории:

public CompletionStage<Stream<Property>> findByData(        String group,        String name,        Instant start,        Instant end,        long offset,        long limit ) {    long startTime = Instant.now().toEpochMilli();    BoundStatement bound = findByDataPreparedStatement.bind(group, name, start, end);    CompletionStage<AsyncResultSet> stage = session.executeAsync(bound);    return stage            .thenCompose(first -> new RowCollector(first, offset, limit))            .thenApply(rows -> rows.stream().map(rowMapper))            .whenComplete((propertyOpt, exception) -> {                if (exception == null) {                    metrics.propertyFindByDataTimerRegister(startTime);                } else {                    metrics.propertyFindByDataWithErrorTimerRegister(startTime);                }            }); }

Несколько слов о методе получения данных в репозитории. Сначала создается команда на исполнение с привязкой ко входным данным. Затем у сессии из java-драйвера асинхронно вызывается подготовленная команда, а результат возвращается в контроллер в виде готового списка свойств. Регистрация метрик вызывается при получении результата от ScyllaDB.

Пример вызова запроса через curl:

curl -l 'localhost:8080/api/v1/property/find?group=g&name=a_0&start=20250101000000000&end=20250101000000900&offset=0&limit=10'

Все эксперименты я запускал локально на компьютере с CPU 1,4GHz и 8GB RAM.

Настройка инструментов

Я использовал:

Я не буду описывать работу и настройку этих инструментов — приведу лишь некоторые детали.

Для запуска используем docker-compose.yaml. Тут стоит выделить параметры valumes, которые применяются для связки grafana и prometheus. Установку jmeter можно провести по этой инструкции.

Для базовой отправки метрик и телеметрии добавим следующие зависимости:

<dependency>    <groupId>io.micrometer</groupId>    <artifactId>micrometer-tracing-bridge-otel</artifactId> </dependency> <dependency>    <groupId>io.opentelemetry</groupId>    <artifactId>opentelemetry-exporter-otlp</artifactId> </dependency> <dependency>    <groupId>io.micrometer</groupId>    <artifactId>micrometer-registry-prometheus</artifactId> </dependency>

Подробности есть тут: spring-boot-3-observability, jaeger.

Для самих данных используем jmeter с вызовом метода создания свойства:

curl -l 'localhost:8080/api/v1/property' \ --header 'Content-Type: application/json' \ --data-raw '{"group": "g", "name": "a_0", "date": "20250101000000000", "valueString": "data_1"}'

С помощью тестового плана добавляем данные. При вызове случайно создается свойство с именем a_[0..9] с разбросом дат. В приведенных результатах для каждого имени сохранены от 30 до 50 значений.

Настройка java-драйвера

К созданию конфигурации драйвера подключимся через наследование DriverConfigLoaderBuilderCustomizer. Для этого реализуем метод void customize(ProgrammaticDriverConfigLoaderBuilder builder), дающий доступ к нужным настройкам. Рассмотрим лишь некоторые из них.

Метрики:

  • advanced.metrics.session.enabled содержит список метрик, которые описывают состояние сессий клиента и ScyllaDB. К примеру, cql-requests, характеризующий количество запросов в секунду в сессии к ScyllaDB.

  • advanced.metrics.node.enabled содержит список метрик, описывающих состояние взаимодействия клиента и узлов ScyllaDB. К примеру, pool.available-streams описывает количество доступных потоков работы между клиентом и узлами.

Настройки соединения (performance, pooling):

  • advanced.netty.io-group.size — количество потоков netty для запросов обмена данными с ScyllaDB и клиента;

  • advanced.connection.pool.local.size — число соединений в локальном пуле.

Настройка метрик сервиса

Для сбора метрик с запросов используется таймер из micrometer:

Timer.builder("propertyFindByDataTimer")        .serviceLevelObjectives(DURATIONS)        .tag(EXCEPTION_TAG_NAME, "none")        .register(meterRegistry);

Таймер для учета ошибок:

Timer.builder("propertyFindByDataTimer")        .serviceLevelObjectives(DURATIONS)        .tag(EXCEPTION_TAG_NAME, "Error")        .register(meterRegistry);

Пример вызова:

timer.record(Instant.now().toEpochMilli() - start, TimeUnit.MILLISECONDS);

Подробности можно найти тут: timers, SB + micrometer.

Настройка grafana

Для визуализации метрик в grafana необходимо создать панель с параметрами данных таймеров из prometheus-источника.

Примеры:

rate(propertyFindByDataTimer_seconds_count {exception="none"}[1m]) rate(propertyFindByDataTimer_seconds_count {exception="Error"}[1m])

Функция rate используется для вычисления значения запросов в секунду.

Эксперимент с запросом получения данных

Тут я покажу, насколько быстрее начинают обрабатываться запросы при изменении параметров драйвера и как можно избавиться от ошибок.

Создадим нагрузку с помощью jmeter-плана. Будем отслеживать показатели FindByData и FindByDataError. Это micrometer-таймеры, отслеживающие время работы одного запроса. Для начала возьмем 400 потоков, 50 запросов при стандартных настройках.

Получим:

В результате FindByData ~300, FindByDataError = 0.

Теперь увеличим число потоков до 1 400:

В результате получаем FindByData ~700, FindByDataError ~20.

Видно, что при увеличении интенсивности запросов начали появляться запросы, которые не выполняются.

Пример ошибки:

com.datastax.oss.driver.api.core.AllNodesFailedException: All 1 node(s) tried for the query failed (showing first 1 nodes, use getAllErrors() for more): Node(endPoint=/127.0.0.1:9042…

Теперь меняем стандартные настройки на io-group.size = 4, pool.local.size = 2. Получаем:

В результате получаем FindByData ~1 000, FindByDataError ~10.

Видим, что количество ошибок снизилось, а число доступных потоков (pool.available-streams) увеличилось.

Увеличим io-group.size до 10, pool.local.size до 8. Получаем:

В результатах видно, что FindByData ~1 100 и FindByDataError = 0, а количество ошибок снизилось до 0. Соответственно, при таких настройках и условиях сервис может держать заданную нагрузку.

Эксперимент получения данных с помощью UDA

Рассмотрим функцию most_common_text(text), которая ищет самое частое значение в колонках с текстовым типом. Про UDA я писал в прошлом материале. Здесь уже другие micrometer-таймеры: MostCommonText, MostCommonTextError.

Пример cql-запроса:

SELECT most_common_text(value_string) FROM property WHERE date >= '2025-03-11 00:00:00' AND date < '2025-03-11 23:00:00'

Ставим количество потоков = 100, количество запросов = 50, io-group.size = 10, pool.local.size = 8.

Получаем:

В результате видно, что MostCommonText ~80 и MostCommonTextError = 0. Соответственно, при заданных условиях сервис может выполнять около 80 запросов в секунду для указанной функции.

Трассировка

Реализуем ее с помощью jaeger:

В рабочих кейсах его можно использовать для отслеживания продолжительности запросов и расследования причин ошибок работы сервиса.

Что можно увидеть из моих экспериментов

Метрики java-драйвера включаются легко. Да, их много, и в документации они описаны не очень детально, но, меняя их через настройки, можно аккуратно тестировать и настраивать взаимодействие между своими сервисами, java-драйвером и самой ScyllaDB.

Отмечу, что сами эксперименты, хотя и являются искусственными, показывают возможность с помощью изменения настроек подобрать значения для нужной интенсивности запросов. Для регулирования пропускной способности можно изменять advanced.netty.io-group.size и advanced.connection.pool.local.size. Хотя точных формул этих параметров нет (см. performance, pooling), эти значения легко подобрать экспериментально. Можно отталкиваться от количества ядер, потоков и таймаутов.

На этом у меня все. Если возникли вопросы, отвечу на них в комментах.


ссылка на оригинал статьи https://habr.com/ru/articles/916684/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *