Всем привет! Меня зовут Александр Кленов, и я работаю в Tarantool. Любой серьезный продукт в промышленной эксплуатации требует инструментов observability. «Проверка пульса» продукта стоит на всем известных китах: логирование, трейсинг, мониторинг.

Мне всегда было интересно, как устроена внутренняя кухня больших компаний, как и какими инструментами observability пользуются сотрудники больших проектов. Сегодня я поделюсь руководством по практическому применению модуля метрик Tarantool, которое подготовил для своих коллег. Статья будет интересна как тем, кто использует Taranool в своей практике, так и всем, кто отвечает за observability и мониторинг в своих проектах. Приглашаю под кат!

В экосистеме Tarantool есть готовый инструмент, который несколькими строчками кода закрывает вопрос мониторинга. Речь идёт про Open Source‑модуль metrics. Далее речь пойдет про типовые сценарии использования модуля в приложениях Tarantool. В прошлой статье на тему метрик описаны основы и вопросы экспорта метрик, их визуализации, хранения и кастомизации. Рекомендую для погружения в контекст.

Напомню, что документация по метрикам находится здесь.

Ограничения и предостережения

В статье идёт речь о версиях:

  • Tarantool 2.7.8+

  • Cartridge 2.7.5+

  • Метриках 0.13+

Вы можете захотеть добавить свою метрику. Модуль это позволяет, но есть нюансы по работе с конкретными инструментами. Эта часть может быть полезна не только для разработчиков на Tarantool.

Если вы добавляете собственную метрику, необходимо добиться, чтобы число комбинаций значений подписи данных было минимальным. Это нужно, чтобы предостеречь базу данных, в которой будут сохраняться данные метрик, от «комбинаторного взрыва» (пример). Примеры подписей данных:

  • лейблы в Prometheus;

  • теги в InfluxDB.

Например, если в компании для сбора метрик используется InfluxDB, вы можете положить весь мониторинг — и  для своего приложения, и для всех остальных в контуре компании. В результате данные по мониторингу, скорее всего, будут утеряны.

Рассмотрим пример:

local collectors = {}

local function init()
   collectors.source_part_update_count = metrics.counter(
           'source_part_update_count', 'Count of requests for update source data by "source" and "source-part"'
   )
end

-- ТАК МОЖНО
local function on_source_update(instance_alias)
   collectors.source_part_update_count:inc(count, { alias = instance_alias })
end

-- ТАК НЕЛЬЗЯ
local function on_source_update(part_id)
   collectors.source_part_update_count:inc(count, { part_id = part_id })
end

В примере две версии функции on_source_update. Верхняя подписывает данные названием узла кластера. Узлов относительно малое количество, поэтому их использовать можно. Во втором случае используется идентификатор записи. Записей много, такой ситуации следует избегать.

То же самое с URL‑ами. Весь URL с параметрами — плохо, шаблон URL‑а или просто название команды — хорошо. И так далее.

Стандартный набор метрик в приложениях Tarantool Cartridge

Если вы используете фреймворк Cartridge для разработки своего приложения, в него уже добавлена перманентная роль metrics, которая подключается по умолчанию. Всё, что нужно — сконфигурировать её. Это делается добавлением секции в конфигурацию кластера. Пример:

metrics:
  export:
    - path: '/metrics'
      format: 'json'
    - path: '/health'
      format: 'health'

Здесь мы указываем по каким адресам будут доступны команды и формат ответа. Данные команды теперь доступны по адресам:

http://url:port/metrics
http://url:port/health

Где url:port — это адрес и порт конкретного экземпляра приложения.

Стандартный набор метрик в нативном приложении Tarantool

Для применения метрик в своём приложении без использования фреймворка потребуется немного больше усилий. Нам дополнительно понадобится http-сервер.

  1. Добавляем зависимости в свой .rockspec:

dependencies = {
    ...
    'http == 1.4.0',
    'metrics == 0.15.1',
}

Или устанавливаем вручную:

cd ${PROJECT_ROOT}
tarantoolctl rocks install http
tarantoolctl rocks install metrics
  1. Подключаем модули в коде приложения:

local http_server = require('http.server')
local metrics = require('metrics')
  1. Добавляем обработчик команды

Если нужен формат JSON:

local json_exporter = require('metrics.plugins.json')
local function http_metrics_handler(request)
    return request:render({ text = json_exporter.export() })
end

Если нужен формат Prometheus:

local prometheus_exporter = require('metrics.plugins.prometheus')
local function http_metrics_handler(...)
    return prometheus_exporter.collect_http(...)
end
  1. Устанавливаем название узла:

metrics.set_global_labels{alias = 'my-tnt-app'}
  1. Включаем вывод стандартного набора метрик:

metrics.enable_default_metrics()
  1. Регистрируем команду и запускаем HTTP-сервер:

local server = http_server.new('0.0.0.0', 8081)
server:route({path = '/metrics'}, http_metrics_handler)
server:start()

В итоге по адресу http://localhost:8081/metrics мы увидим показатели метрик в формате, например, JSON:

[
  {
    "label_pairs": {
      "alias": "my-tnt-app"
    },
    "timestamp": 1679663602823779,
    "metric_name": "tnt_vinyl_disk_index_size",
    "value": 0
  },
  ...
  {
    "label_pairs": {
      "alias": "my-tnt-app"
    },
    "timestamp": 1679663602823779,
    "metric_name": "tnt_info_memory_data",
    "value": 39272
  },
  {
    "label_pairs": {
      "alias": "my-tnt-app"
    },
    "timestamp": 1679663602823779,
    "metric_name": "tnt_election_vote",
    "value": 0
  }
]

Про стандартные метрики

Модуль предоставляет стандартный набор метрик. Полный перечень и описание можно посмотреть здесь. К стандартным метрикам существует готовый дашборд.

Если вы используете собственный дашборд, стоит присмотреться к следующим метрикам:

  • tnt_info_memory_data —  объём памяти, занимаемый  данными. Стоит поставить алерт на 80 % отведённой RAM-памяти.

  • tnt_info_memory_lua —  объём памяти, занимаемый скриптами приложения. Обычно вызывает подозрение превышение 1ГБ.

  • tnt_fiber_csv —  количество переключений контекста. Если на узле дольше минуты сохраняется высокое значение в миллионы переключений — обрабатывается что-то огромное, или программа просто зависла

  • tnt_net_sent_total —  количество исходящих запросов с инстанса. Пересчитать в RPS, порог определить эмпирически.

  • tnt_net_recieved_total — количество входящих запросов на инстанс. Пересчитать в RPS, порог определить эмпирически.

Если используете Tarantool Cartridge:

  • tnt_cartridge_issues — число проблем на кластере. Алерт на значение больше нуля.

  • tnt_cartridge_failover_trigger_total — число переключений мастера фейловером. Доступно в метриках начиная с версии 0.15.

С использованием выделенной оперативной памяти нужны такие алерты:

  • (tnt_slab_quota_used_ratio >= 80) and (tnt_slab_arena_used_ratio >= 80) —  это проблема, у узла заканчивается выделенный ему объём памяти.

  • (tnt_slab_quota_used_ratio >= 90) and (tnt_slab_arena_used_ratio >= 90) –—  это уже инцидент, память кончилась.

  • (tnt_slab_quota_used_ratio >= 80) and (tnt_slab_items_used_ratio <= 85) —  это проблема, у узла заканчивается выделенный ему объём памяти. Возможна большая фрагментация данных, но  достаточно перезапустить узел.

  • (tnt_slab_quota_used_ratio >= 90) and (tnt_slab_items_used_ratio <= 85) —  то же самое, но уже инцидент.

Стандартные метрики можно отключать на лету или включать обратно — смотрите  enable_default_metrics. Здесь следует указывать название группы метрик строкой. Например:

local metrics = require('metrics')
---
metrics.enable_default_metrics(nil, 'operations') -- отключить группу метрик operations

Добавляем метрики на HTTP API-команды приложения

К HTTP API-командам приложения можно подключать стандартную метрику http_server_request_latency, которая фиксирует количество вызовов и общее время выполнения (latency) каждой отдельной команды.

Рассмотрим пример. Он справедлив и для приложений на Cartridge и для нативных приложений.

  1. Сначала подключаем модуль HTTP-сервера:

local http_server = require('http.server') -- вызов для нативного приложения
-- или --
local http_server = cartridge.service_get('httpd') -- вызов для приложения на Cartridge
                                                   -- (пользуемся встроенным сервером) 
  1. Затем подключаем модуль метрик

Если используем Cartridge:

local metrics = require('cartridge.roles.metrics')

Если не используем Cartridge:

local metrics = require('metrics')
  1. Добавляем тестовую команду:

local function http_app_api_handler(request)
    return request:render({ text = 'Hello world!!!' })
end

local server = http_server.new('0.0.0.0', 8081)
server:route({path = '/hello'}, http_app_api_handler)
server:start()
  1. Добавляем обёртку модуля метрик на обработчик выполнения команды

Было так:

server:route({path = '/hello'}, http_app_api_handler)

Стало так:

server:route({path = '/hello'}, metrics.http_middleware.v1(http_app_api_handler))

Теперь если мы подёргаем нашу HTTP API-команду hello и после этого запросим данные метрик всё по тому же адресу http://url:port/metrics — мы увидим, что ответ содержит новые данные:

{
    "label_pairs": {
      "path": "/hello",
      "method": "ANY",
      "status": 200,
      "alias": "my-tnt-app"
    },
    "timestamp": 1679668258972227,
    "metric_name": "http_server_request_latency_count",
    "value": 9
  },
  {
    "label_pairs": {
      "path": "/hello",
      "method": "ANY",
      "status": 200,
      "alias": "my-tnt-app"
    },
    "timestamp": 1679668258972227,
    "metric_name": "http_server_request_latency_sum",
    "value": 0.00008015199273359
  },

Добавляем в метрики на HTTP API-команды приложения квантили

Для этого нам понадобится другой тип метрики – summary:

local collector = metrics.http_middleware.build_default_collector('summary')

Добавляем в обёртку ещё один параметр:

Было так:

server:route({path = '/hello'}, metrics.http_middleware.v1(http_app_api_handler))

Стало так:

server:route({path = '/hello'}, metrics.http_middleware.v1(http_app_api_handler, collector))

Теперь если мы подёргаем нашу HTTP API-команду hello и после этого запросим данные метрик, мы увидим вычисленные перцентили:

 {
    "label_pairs": {
      "path": "/hello",
      "method": "ANY",
      "alias": "my-tnt-app",
      "status": 200,
      "quantile": 0.5
    },
    "timestamp": 1679918308729842,
    "metric_name": "http_server_request_latency",
    "value": 0.0000100580000435
  },
  {
    "label_pairs": {
      "path": "/hello",
      "method": "ANY",
      "alias": "my-tnt-app",
      "status": 200,
      "quantile": 0.9
    },
    "timestamp": 1679918308729842,
    "metric_name": "http_server_request_latency",
    "value": 0.000013445001968648
  },
  {
    "label_pairs": {
      "path": "/hello",
      "method": "ANY",
      "alias": "my-tnt-app",
      "status": 200,
      "quantile": 0.99
    },
    "timestamp": 1679918308729842,
    "metric_name": "http_server_request_latency",
    "value": 0.00011089599865954
  },

Кстати, чтобы изменить название этой метрики на своё, требуется передать дополнительно аргумент. А с подсказкой — два аргумента:

local collector = metrics.http_middleware.build_default_collector(
        'summary', -- или 'histogram'
        'my_http_server_request_latency',
        'My HTTP API requests summary'
)

Свой формат healthcheck для приложений Tarantool Cartridge

Формат healthcheck-а Tarantool Cartridge довольно простой — это просто текстовое "1" или "0". Если ваша инфраструктура требует подробностей, их можно добавить. К примеру, мы хотим сделать вывод в формате JSON и хотим туда добавить название приложения и инстанса (узла).

  1. Для этого сначала получим названия:

local argparse = require("cartridge.argparse")

local instance_params = argparse.get_opts{
   app_name = 'string',
   instance_name = 'string',
   alias = 'string',
}
  1. Далее обернём Cartridge функцию своей:

local cartridge_health = require('cartridge.health')

local cartridge_health_func = cartridge_health.is_healthy
cartridge_health.is_healthy = function (req)
   local result_orig = cartridge_health_func()
   result_orig = type(result_orig) == 'table' and result_orig or {}
   
   local resp = req:render{
      json = {
         app_name = instance_params.app_name,
         instance_name = instance_params.instance_name or instance_params.alias or '',
         instance_health = (result_orig.status == 200),
      }
   }
   resp.status = 200
   return resp
end

Такой healthcheck вернёт следующий результат, например:

{
   app_name = 'test-app',
   instance_name = 'router-1',
   instance_health = true
}

Ещё пару слов про histogramm и summary

Histogramm

Метрика приложения типа «гистограмма» используется для сбора и анализа статистических данных о распределении значений определенного показателя в приложении. В отличие от простых метрик, которые позволяют отслеживать только среднее значение или количество событий, гистограмма позволяет увидеть подробную картину распределения показателей и выявить скрытые зависимости.

В частности, гистограмма может использоваться для:

  • Измерения производительности приложения, например, времени отклика на запросы.

  • Мониторинга использования ресурсов, таких как память или CPU.

  • Определения выбросов или аномалий в данных, которые могут указывать на проблемы в приложении.

Гистограммы выдают несколько показателей, например, общее число замеров и сумма измеренных значений.

Кроме того, гистограммы работают с диапазонами измеряемых значений. Допустим нас интересует величина Demo value. И мы хотим знать, как часто измеренная величина попадает в определённый отрезок (bucket):

Далее мы производим замеры. Допустим в результате замеров мы получили следующие величины: 8, 7, 6, 8, 1, 7, 4, 8.

Таким образом у нас в отрезок:

  • от 0 до 2 включительно попал 1 замер

  • от 2 до 4 включительно попал тоже 1 замер

  • от 4 до 6 включительно попало 1 замер

  • и от 6 до бесконечности у нас попало 5 замеров.

В выдаче метрик мы при этом увидим немного другую картину. Здесь результат выдаётся с накопительным эффектом: 

  • от 0 до 2 включительно попал 1 замер

  • от 0 до 4 включительно попало 2 замера

  • от 0 до 6 включительно попало 3 замера

  • от 0 до бесконечности попало 8 замеров (равно показателю histogramm_demo_count)

 {
     "label_pairs": {
        "le": 2,
        "alias": "my-tnt-app"
     },
     "timestamp": 1680174378390303,
     "metric_name": "histogramm_demo_bucket",
     "value": 1
  },
  {
    "label_pairs": {
      "le": 4,
      "alias": "my-tnt-app"
    },
    "timestamp": 1680174378390303,
    "metric_name": "histogramm_demo_bucket",
    "value": 2
  },
  {
    "label_pairs": {
      "le": 6,
      "alias": "my-tnt-app"
    },
    "timestamp": 1680174378390303,
    "metric_name": "histogramm_demo_bucket",
    "value": 3
  },
  {
    "label_pairs": {
      "le": "inf",
      "alias": "my-tnt-app"
    },
    "timestamp": 1680174378390303,
    "metric_name": "histogramm_demo_bucket",
    "value": 8
  },

Summary

Метрика приложения типа "summary" также используется для сбора статистических данных о распределении значений определенного показателя в приложении.

Сводка выдаёт также несколько показателей:

  • общее число замеров;

  • сумма измеренных значений.

Сводка (summary) тоже работает с диапазонами значений. Но в отличие от гистограмм использует для этого так называемые процентили (percentile, задаются в %), оно же квантили (quantile, задаются числом от 0 до 1). В этом случае не требуется жёстко задавать границы, как у гистограмм. В данной метрике диапазоны зависят от измеренных величин и количества замеров.

Давайте возьмём серию замеров из предыдущего примера и отсортируем по возрастанию: 1, 4, 6, 7, 7, 8, 8, 8

Таким образом:

  • 0 % процентиль — это значение первого, минимального элемента, минимального. По примеру это 1.

  • 100 % процентиль — это значение последнего, максимального элемента, максимального. По примеру – это 8.

  • 50 % процентиль — это значение среднего элемента. В примере у нас чётное число элементов, серединой будет пятый = 7. Это значит, что половина замеров у нас даёт разброс значений от 1 до 7.

Обычно в метриках используется один процентиль:

  • 95 % — большинство замеров

  • и/или 99 % —  практически все замеры, кроме отдельных случаев, выбросов.

При большом числе замеров в секунду нам понадобится большой массив, чтобы хранить их все. Чтобы сэкономить память, массив сжимается. Степень сжатия определяется допустимой ошибкой. Обычно применяют степень ошибки колеблется от 1 % до 10 %. Это значит, что 50 % процентиль с ошибкой 10 % из примера выше вернёт значение в диапазоне 6,65...7,35, вместо 7.

Кроме того сводка не хранит значения всё время работы приложения. Эта метрика использует скользящее окно, поделённое на участки (buckets), котором хранятся замеры:

Отметим, что бакеты (buckets) в гистограммах и квантилях summary имеют разный смысл.

По итогу:

local summary_demo = metrics.summary(
    'summary_demo', -- название метрики
    'Summary demo', -- подсказка
    {
       [0.5] = 0.01, -- квантиль 50% с ошибкой 1% 
       [0.95] = 0.01, -- квантиль 95% с ошибкой 1%
       [0.99] = 0.01, -- квантиль 99% с ошибкой 1%
    },
    {
       max_age_time = 60, -- продолжительность участка (bucket) в секундах
       age_buckets_count = 5 -- сколько всего участков (buckets) в скользящем окне
                             -- длительность окна = max_age_time * age_buckets_count секунд, или в
                             -- данном случае = 5 минут
    }
)

Такая метрика по примеру выше вернёт следующие значения по квантилям:

 {
   "label_pairs": {
      "quantile": 0.5,
      "alias": "my-tnt-app"
   },
   "timestamp": 1680180929162484,
   "metric_name": "summary_demo",
   "value": 7
  },
  {
    "label_pairs": {
      "quantile": 0.95,
      "alias": "my-tnt-app"
    },
    "timestamp": 1680180929162484,
    "metric_name": "summary_demo",
    "value": 8
  },
  {
    "label_pairs": {
      "quantile": 0.99,
      "alias": "my-tnt-app"
    },
    "timestamp": 1680180929162484,
    "metric_name": "summary_demo",
    "value": 8
  },

Ну и число замеров и сумму по ним:

{
    "label_pairs": {
      "alias": "my-tnt-app"
    },
    "timestamp": 1680180929162484,
    "metric_name": "summary_demo_count",
    "value": 8
  },
  {
    "label_pairs": {
      "alias": "my-tnt-app"
    },
    "timestamp": 1680180929162484,
    "metric_name": "summary_demo_sum",
    "value": 49
  },

Заключение

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

Стандартный набор метрик, предоставляемый модулем для Cartridge и нативных приложений, может быть расширен с помощью добавления метрик на HTTP API-команды приложения и процентилей для более подробного анализа распределения данных. Кроме того, можно использовать свой формат healthcheck и метрики типа «гистограмма» и "summary" для более точного анализа распределения данных.

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

Скачать Tarantool можно на официальном сайте, а получить помощь — в Telegram-чате.

Комментарии (3)


  1. PensijaPro
    25.04.2023 12:29

    Физика процесса не ясна. Почему на фото раскалена гайка - кажется по ней вообще не длжен протекать электрический ток? Думаю цепь через болт идет и он дожен быть раскален. Хотя кто его знает, может там хитрая прокладка изолирующая... Но фото классное!


    1. 1div0 Автор
      25.04.2023 12:29
      +1

      Не должен. Алерт бы дал знать, что там что-то не так )