В последнее время я занимался устранением проблем с памятью в процессе, которым владеет моя команда.
Я начал с просмотра этого видео об утечках памяти от Бенуа Жакмона:
Я многому научился в процессе, но также заметил красивые графики памяти в видео и решил, что будет трудно устранить неполадки, если у меня их не будет.
При использовании php-расширений, таких как у Бенуа или Арно Ле Блана, для получения снимка памяти, очень важно продумать наиболее подходящий момент для получения снимка, чтобы поймать утечку памяти, за которой вы, возможно, охотитесь.
Конечно, для этого можно использовать MemoryUsageProcessor от Monolog, но я подумал, что было бы полезнее получить что-то более ✨ наглядное✨.
В наших средах мы используем Datadog, но в моей системе разработки его нет.
Есть несколько показателей, которые можно отследить для устранения проблем с памятью, некоторые из них предоставляются ОС и обычно сообщаются Datadog, другие — PHP через memory_get_usage()
(в настоящее время у меня нет возможности отслеживать их в производстве).
Измерения из PHP
PHP предоставляет несколько методов для понимания того, что происходит с памятью. Во-первых, у вас есть memory_get_usage()
, который принимает аргумент boolean. В зависимости от этого аргумента, метод возвращает память, используемую PHP, или память, выделенную PHP. При освобождении памяти, как правило, первый показатель уменьшается, а второй остается стабильным.
Далее, есть memory_get_peak_usage()
, который сообщает наибольшее значение использованной или выделенной памяти с начала работы скрипта. Это полезно, поскольку может помочь разработчику понять, что он вызывает memory_get_usage()
не там, где использование памяти максимально.
Получение метрик
В случае со сценарием, который я проверял, у меня был главный цикл, который выполнялся часто (но не равномерно). Это все еще хороший кандидат для сбора метрик, как мы увидим.
Вот что я поместил в этот цикл:
<?php
file_put_contents(
'memory.tsv',
time() . "t" .
memory_get_usage(true) / 1024 / 1024 . "t" .
memory_get_usage(false) / 1024 / 1024 . "t".
memory_get_peak_usage(true) / 1024 / 1024 . "t" .
memory_get_peak_usage(false) / 1024 / 1024 . "n",
FILE_APPEND
);
В результате получается TSV-файл, который выглядит следующим образом:
1660831187 20 8.1252593994141 20.359375 19.608978271484
1660831187 20 8.1281814575195 20.359375 19.608978271484
1660831187 20 8.131103515625 20.359375 19.608978271484
1660831187 20 8.134033203125 20.359375 19.608978271484
1660831187 20 8.1369552612305 20.359375 19.608978271484
1660831187 20 8.1398773193359 20.359375 19.608978271484
1660831190 22 8.2328033447266 24.42578125 22.869613647461
1660831190 22 8.2357330322266 24.42578125 22.869613647461
1660831190 22 8.2386627197266 24.42578125 22.869613647461
1660831190 22 8.2415924072266 24.42578125 22.869613647461
Если посмотреть на первый столбец, то можно заметить, что там есть группы строк, которые могут находиться на расстоянии нескольких секунд друг от друга, так что производство метрик действительно, действительно не является регулярным.
Построение графиков
Затем, чтобы создать график, я обратился к gnuplot, который кажется целой вселенной, а также очень надежным программным обеспечением. Я начал с создания следующего конфигурационного файла:
# config.plt
set term png small size 800,600
set output "/tmp/memory_get_usage-graph.png"
set ylabel "memory in MB"
set yrange [0:*]
set xdata time # x is not just a random number
set timefmt "%s" # we use UNIX timestamps
plot "memory.tsv" using 1:2 with lines axes x1y1 title "memory_get_usage(true) in MB",
"memory.tsv" using 1:3 with lines axes x1y1 title "memory_get_usage(false) in MB",
"memory.tsv" using 1:4 with lines axes x1y1 title "memory_get_peak_usage(true) in MB",
"memory.tsv" using 1:5 with lines axes x1y1 title "memory_get_peak_usage(false) in MB"
Как вы видите, можно сообщить gnuplot, что ось x представляет время, что гарантирует, что у вас будет хорошо отформатированная ось X.
График создается путем запуска gnuplot config.plt
.
Рендеринг графика
Было бы удобно иметь график, который обновляется с течением времени. Для этого вам понадобятся 2 маленькие программы: watch
и feh
.
Запустите watch gnuplot config.plt
, чтобы убедиться, что png создается каждые 2 секунды (это значение по умолчанию в watch).
Параллельно с этим вы запускаете feh /tmp/memory_get_usage-graph.png
для отображения png-файла. Что замечательно в feh
, так это то, что он обновляется автоматически, поэтому вам не нужно делать ничего особенного, чтобы получить живой график. 🤯 feh
делает очень мало, но делает это хорошо.
Здесь следует отметить 2 интересных момента:
- Только график для
memory_get_usage(false)
падает, но он действительно падает, так что утечки памяти нет. - Умолчания gnuplot немного уродливы, а я не разработчик фронтенда, так что они так и останутся уродливыми.
Измерения в Linux
Производство метрик
Здесь для получения метрик можно использовать ps
.
while true; do
ps --pid $(pgrep -f some_string_that_identifies_your_process)
-o pid=,%mem=,vsz= >> /tmp/mem.log
gnuplot config.plt
sleep 1
done
Обратите внимание, что вы можете использовать это для любого процесса, а не только для PHP.
Построение графиков
На этот раз все немного сложнее, я говорю gnuplot построить 2 метрики, которые
имеют разные единицы измерения на одном графике.
Левая ось Y будет иметь шкалу для первой метрики, а правая ось Y
будет иметь шкалу для второй метрики.
На этот раз я не настраиваю ось X, так как я создаю метрики в
регулярном темпе.
Все это бессовестно украдено из Stack Overflow.
set term png small size 800,600
set output "/tmp/mem-graph.png"
set ylabel "VSZ"
set y2label "%MEM"
set ytics nomirror
set y2tics nomirror in
set yrange [0:*]
set y2range [0:*]
plot "/tmp/mem.log" using 3 with lines axes x1y1 title "VSZ",
"/tmp/mem.log" using 2 with lines axes x1y2 title "%MEM"
Здесь вы можете видеть, что показатели отличаются от показателей из PHP. Я не буду углубляться в эту тему, потому что это не по теме, но при устранении проблем с памятью может быть важно сравнить оба аспекта.
Вынос
Эти графики помогли мне понять разницу между memory_get_usage(true)
и memory_get_usage(false)
, и дали мне лучшее понимание моего приложения. В частности, я понял, что пакетная обработка, которую я выполнял, полагалась на партии объектов, которые не все были одинакового размера, и что обеспечение того, что все они были примерно одинакового размера, поможет избежать ситуаций, когда серия больших объектов вызывает ошибку нехватки памяти.