Top.Mail.Ru
Метрики LuaJIT | Tarantool
Tarantool
Узнайте содержание релиза 2.8
Сервер приложений Метрики LuaJIT

Метрики LuaJIT

Tarantool может возвращать метрики текущего экземпляра через Lua API или C API.

getmetrics()

Получение значений метрик в виде таблицы.

Параметры: нет

возвращает:таблицу

Пример: metrics_table = misc.getmetrics()

Таблица метрик содержит 19 значений типа number. Они получены в результате приведения к вещественному типу двойной точности (double), что позволяет практически не терять в точности исходных значений. Значения, имена которых начинаются на gc_, связаны со сборщиком мусора в LuaJIT. Полную информацию о сборщике мусора можно найти на вики-странице lua-users и слайде от создателя языка Lua. Значения, имена которых начинаются на jit_, связаны с фазами JIT-компиляции. Более подробно фазы описаны в научной работе, написанной в рамках одного из проектов ЦЕРН.

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

Поскольку многие значения монотонны, анализ обычно проходит так: вызывается getmetrics(), полученная таблица сохраняется, а затем getmetrics() вызывается вновь и новая таблица сравнивается с сохраненной. Разницу между значениями, измеренными в два момента времени, называют кривой наклона. Интерес может представлять, например, кривая наклона, которая демонстрирует ускорение. Разница между последним и предыдущим значениями на такой кривой все время увеличивается. Для некоторых элементов таблицы ниже будут приведены примеры.

Имя Содержимое Монотонное?
gc_allocated количество выделенной памяти в байтах да
gc_cdatanum количество размещенных объектов cdata нет
gc_freed количество освобожденной памяти в байтах да
gc_steps_atomic количество шагов сборщика мусора, фаза atomic, инкрементальная да
gc_steps_finalize количество шагов сборщика мусора, фаза finalize да
gc_steps_pause количество шагов сборщика мусора, фаза pauses да
gc_steps_propagate количество шагов сборщика мусора, фаза propagate да
gc_steps_sweep количество шагов сборщика мусора, фаза sweep. См. описание фазы sweep да
gc_steps_sweepstring количество шагов сборщика мусора, фаза sweep для строк да
gc_strnum количество размещенных объектов-строк нет
gc_tabnum количество размещенных объектов-таблиц нет
gc_total текущее количество выделенной памяти в байтах (обычно равно разности gc_allocated и gc_freed) нет
gc_udatanum количество размещенных объектов udata нет
jit_mcode_size общий объем выделенного машинного кода нет
jit_snap_restore общее количество восстановлений стека по снимку (сработавших защитных утверждений, которые привели к остановке выполнения трассы). См. внешнее руководство по SNAP. да
jit_trace_abort общее количество прерванных трассировок да
jit_trace_num количество JIT-трассировок нет
strhash_hit количество интернированных строк (если строка уже есть в пуле, новая копия не создается и память под нее не выделяется) да
strhash_miss общее количество памяти, выделенной для строк за время жизни платформы да

Примечание. Функция ujit.getmetrics() возвращает похожие имена. Однако многие значения, используемые в uJIT, не монотонны.

Примечание. В LuaJIT metrics используются похожие имена и аналогичные значения. Отличие функции misc.getmetrics() в том, что она не требует вызывать require для модуля misc.

Lua-функция getmetrics() — обертка для C-функции luaM_metrics().

Программы на C могут включать заголовок libmisclib.h, куда входят следующие определения:

struct luam_Metrics { /* имена, описанные ранее для Lua */ }

LUAMISC_API void luaM_metrics(lua_State *L, struct luam_Metrics *metrics);

Имена элементов структуры luam_Metrics совпадают с именами в таблице значений getmetrics для Lua. У всех элементов структуры luam_Metrics тип данных — size_t. Функция luaM_metrics() заполняет структуру *metrics метриками, относящимися к Lua-состоянию, которое связано с корутиной L.

Пример с программой на языке C

В руководстве Tarantool по хранимым процедурам на языке C перейдите к примеру с файлом easy.c. Удалите содержимое файла и вставьте следующий код:

#include "module.h"
#include <lmisclib.h>

int easy(box_function_ctx_t *ctx, const char *args, const char *args_end)
{
  lua_State *ls = luaT_state();
  struct luam_Metrics m;
  luaM_metrics(ls, &m);
  printf("allocated memory = %lu\n", m.gc_allocated);
  return 0;
}

Теперь, как в исходном примере, выполните через клиент запросы до capi_connection:call('easy') включительно. На экране появится следующее: allocated memory = 4431950 (число приведено для примера).

Отслеживать размещение новых строковых объектов можно так:

function f()
  collectgarbage("collect")
  local oldm = misc.getmetrics()
  local table_of_strings = {}
  for i = 3000, 4000 do table.insert(table_of_strings, tostring(i)) end
  for i = 3900, 4100 do table.insert(table_of_strings, tostring(i)) end
  local newm = misc.getmetrics()
  print("gc_strnum diff = " .. newm.gc_strnum - oldm.gc_strnum)
  print("strhash_miss diff = " .. newm.strhash_miss - oldm.strhash_miss)
  print("strhash_hit diff = " .. newm.strhash_hit - oldm.strhash_hit)
end
f()

Вероятный результат — gc_strnum diff = 1101, так как мы добавили 1202 строки, 101 из которых были дубликатами. По той же причине strhash_miss = 1101, а strhash_hit = 101 плюс некоторые издержки. (strhash_hit всегда предполагает небольшие издержки, которые можно игнорировать.)

Результат лишь вероятный, поскольку память для строк могла быть выделена ранее. Хорошо, если кривая наклона strhash_miss менее крутая, чем у strhash_hit.

Доступ к остальным значениям gc_*numgc_cdatanum, gc_tabnum и gc_udatanum можно получить аналогичным образом. Любое значение gc_*num поможет при поиске утечки памяти: общее количество этих объектов не должно постоянно расти. Более общий способ искать утечки памяти — наблюдать за переменной gc_total. Также можно отслеживать значение jit_mcode_size, отражающее объем памяти, выделенной для трасс машинного кода.

Чем меньше работает сборщик мусора, тем лучше. Отслеживать, насколько его нагружает приложение, можно так:

function f()
  for i = 1, 10 do collectgarbage("collect") end
  local oldm = misc.getmetrics()
  local newm = misc.getmetrics()
  oldm = misc.getmetrics()
  collectgarbage("collect")
  newm = misc.getmetrics()
  print("gc_allocated diff = " .. newm.gc_allocated - oldm.gc_allocated)
  print("gc_freed diff = " .. newm.gc_freed - oldm.gc_freed)
end
f()

Результат: gc_allocated diff = 800, gc_freed diff = 800. Отсюда видно, что строка local ... = getmetrics() вызывает аллокацию памяти, поскольку создает таблицу и наполняет ее значениями. Когда имя переменной (в данном случае oldm) используется повторно, память освобождается. Обычно это происходит не сразу, однако collectgarbage("collect") принудительно запускает сборку мусора, благодаря чему можно немедленно увидеть результат.

Этот пример показывает, что можно оптимизировать расход памяти для таблиц.

function f()
  collectgarbage("collect")
  local oldm = misc.getmetrics()
  local t = {}
  for i = 1, 513 do
    t[i] = i
  end
  local newm = misc.getmetrics()
  local diff = newm.gc_allocated - oldm.gc_allocated
  print("diff = " .. diff)
end
f()

Результат покажет, что значение diff примерно равно 18000.

Если инициализировать таблицу по-другому, получится вот что:

function f()
  local table_new = require "table.new"
  local oldm = misc.getmetrics()
  local t = table_new(513, 0)
  for i = 1, 513 do
    t[i] = i
  end
  local newm = misc.getmetrics()
  local diff = newm.gc_allocated - oldm.gc_allocated
  print("diff = " .. diff)
end
f()

Результат покажет, что значение diff примерно равно 6000.

Нагрузку на сборщик мусора помогают отслеживать кривые наклона метрик gc_steps_*. Во время долго выполняющихся процедур значения gc_steps_* увеличиваются. При этом большие промежутки времени между увеличениями gc_steps_atomic — хороший признак. Поскольку gc_steps_atomic увеличивается только раз в цикл сбора мусора, можно увидеть, сколько циклов прошло.

Кроме того, по тому, насколько выросло значение gc_steps_propagate, можно косвенно оценить количество объектов. Это значение также коррелирует с множителем шага сборщика мусора. Например, количество инкрементальных шагов может увеличиваться, однако множитель шага настроен так, что за каждый шаг можно обработать лишь небольшое количество объектов. Поэтому при настройке сборщика мусора следует учитывать эти показатели.

Следующая функция оценивает, вызывает ли оператор SQL большую нагрузку:

function f()
  collectgarbage("collect")
  local oldm = misc.getmetrics()
  collectgarbage("collect")
  box.execute([[DROP TABLE _vindex;]])
  local newm = misc.getmetrics()
  print("gc_steps_atomic = " .. newm.gc_steps_atomic - oldm.gc_steps_atomic)
  print("gc_steps_finalize = " .. newm.gc_steps_finalize - oldm.gc_steps_finalize)
  print("gc_steps_pause = " .. newm.gc_steps_pause - oldm.gc_steps_pause)
  print("gc_steps_propagate = " .. newm.gc_steps_propagate - oldm.gc_steps_propagate)
  print("gc_steps_sweep = " .. newm.gc_steps_sweep - oldm.gc_steps_sweep)
end
f()

Очевидно, значения метрик gc_steps_ *, полученные до вызова box.execute(), существенно не отличаются от значений, полученных после этого вызова.

JIT-компиляторы трассируют код, пытаясь найти возможность для компиляции, чтобы повысить производительность. Значение jit_trace_abort показывает, как часто эти попытки оканчиваются неудачей (чем меньше это значение, тем лучше), а jit_trace_num — как много трассировок сгенерировано с момента последнего выполнения операции flush (обычно чем больше значение, тем лучше).

Код следующей функции будет успешно трассирован:

function f()
  jit.flush()
  for i = 1, 10 do collectgarbage("collect") end
  local oldm = misc.getmetrics()
  collectgarbage("collect")
  local sum = 0
  for i = 1, 57 do
    sum = sum + 57
  end
  for i = 1, 10 do collectgarbage("collect") end
  local newm = misc.getmetrics()
  print("trace_num = " .. newm.jit_trace_num - oldm.jit_trace_num)
  print("trace_abort = " .. newm.jit_trace_abort - oldm.jit_trace_abort)
end
f()

Результат: trace_num = 1, trace_abort = 0. Отлично.

А в следующей функции, похоже, есть код, способный вызвать у LuaJIT проблемы:

jit.opt.start(0, "hotloop=2", "hotexit=2", "minstitch=15")
_G.globalthing = 5
function f()
  jit.flush()
  collectgarbage("collect")
  local oldm = misc.getmetrics()
  collectgarbage("collect")
  local sum = 0
  for i = 1, box.space._vindex:count()+ _G.globalthing do
    box.execute([[SELECT RANDOMBLOB(0);]])
    require('buffer').ibuf()
    _G.globalthing = _G.globalthing - 1
  end
  local newm = misc.getmetrics()
  print("trace_num = " .. newm.jit_trace_num - oldm.jit_trace_num)
  print("trace_abort = " .. newm.jit_trace_abort - oldm.jit_trace_abort)
end
f()

Результат: trace_num — от 2 до 4, trace_abort = 1. Это означает, что нужно было сгенерировать до четырех трассировок вместо одной, причем что-то заставило LuaJIT прекратить попытки. Дальнейшая трассировка показывает, что проблема не в подозрительно выглядящих операторах внутри функции, а в вызове jit.opt.start. (Содержимое файла jit.dump может помочь разобраться, как протекала компиляция трасс.)

Если кривая наклона метрики jit_snap_restore после изменений в старом коде растет, это может означать, что LuaJIT чаще останавливает выполнение кода на трассах. А это, в свою очередь, может указывать на снижение производительности.

Рассмотрим следующий код:

function f()
  local function foo(i)
    return i <= 5 and i or tostring(i)
  end
  -- параметр minstitch нужен, чтобы эмулировать поведение non-stitching
  jit.opt.start(0, "hotloop=2", "hotexit=2", "minstitch=15")
  local sum = 0
  local oldm = misc.getmetrics()
  for i = 1, 10 do
    sum = sum + foo(i)
  end
  local newm = misc.getmetrics()
  local diff = newm.jit_snap_restore - oldm.jit_snap_restore
  print("diff = " .. diff)
end
f()

Результат: diff = 3, поскольку при выполнении трассы возможны три сторонних выхода. Один находится в конце цикла. Другие два переключают выполнение на интерпретатор, прежде чем LuaJIT решит, что фрагмент кода «горячий» (значение параметра hotloop по умолчанию равно 56 согласно документации LuaJIT).

А теперь изменим единственную строку в функции local foo, чтобы получить следующий код:

function f()
  local function foo(i)
    -- функция math.fmod еще не скомпилирована
    return i <= 5 and i or math.fmod(i, 11)
  end
  -- параметр minstitch нужен, чтобы эмулировать поведение non-stitching
  jit.opt.start(0, "hotloop=2", "hotexit=2", "minstitch=15")
  local sum = 0
  local oldm = misc.getmetrics()
  for i = 1, 10 do
    sum = sum + foo(i)
  end
  local newm = misc.getmetrics()
  local diff = newm.jit_snap_restore - oldm.jit_snap_restore
  print("diff = " .. diff)
end
f()

Результат: значение diff увеличилось, так как сторонних выходов стало больше. Этот тест показывает, что измение кода влияет на производительность.