Bug 50032 - PHP выделяет реально ОЗУ в 2 раза меньше указанного в настройке memory_limit
Summary: PHP выделяет реально ОЗУ в 2 раза меньше указанного в настройке memory_limit
Status: NEW
Alias: None
Product: Sisyphus
Classification: Development
Component: php8.2-fpm-fcgi (show other bugs)
Version: unstable
Hardware: x86_64 Linux
: P5 normal
Assignee: Anton Farygin
QA Contact: qa-sisyphus
URL:
Keywords:
Depends on:
Blocks:
 
Reported: 2024-04-15 10:46 MSK by Анатолий Кирсанов
Modified: 2024-04-28 10:10 MSK (History)
3 users (show)

See Also:


Attachments
Настройки nginx, Apache, тестовый PHP скрипт (4.32 KB, application/gzip)
2024-04-15 10:46 MSK, Анатолий Кирсанов
no flags Details
Настроки PHP по лимиту памяти (41.82 KB, image/png)
2024-04-16 17:07 MSK, Анатолий Кирсанов
no flags Details

Note You need to log in before you can comment on or make changes to this bug.
Description Анатолий Кирсанов 2024-04-15 10:46:44 MSK
Created attachment 15874 [details]
Настройки nginx, Apache, тестовый PHP скрипт

Для i586 есть только sysv. Без вариантов, ибо пакетов systemd для этой платформы нет в принципе.
Я пользовался образом alt-p10-jeos-sysv-20240309-i586.iso.

Для x86_64 пользовался образом alt-p10-jeos-systemd-20240309-x86_64.iso

После установки вы получите локальную сеть на dhcp.
Если нужен доступ извне, то потребуется настройка сети.
Она сводится к созданию трех файлов в папке интерфейса (ищите в /etc/net/ifaces).
Также, для работы DNS нужен /etc/resolv.conf
Я забил туда серверы имен Яндекс.

Увы, здесь поможет только vim.

Опционально, можно настроить и hostname. Делается это редактированием /etc/sysconfig/network и последующей перезагрузкой.
Для systemd ничего редактировать не надо, есть команда 
  
  hostnamectl set-hostname example.com

После, при необходимости, можно настроить sshd для доступа по паролям, ключам и т.п. А до этого момента все делается, если на хостинге, по VNC и подобному.

Для настройки sshd предстоит лезть в файл /etc/openssh/sshd_config. Там есть параметр PermitRootLogin. Временно ему надо поставить значение yes. После копирования ключей и их добавления в authorized_keys параметр нужно вернуть в исходное значение.

Если привыкли пользоваться mc, то очень полезно установить пакет glibc-locales. Локали там уже настроены, не хватает их описаний. В варианте с systemd это не требуется (все уже есть).

Для последующей настройки WEB потребуются пакеты nginx apache2-httpd-event apache2-mod_fcgid php8.2-cgi
Необходимые зависимости подтянуться.

Далее уже мои персональные заморочки. Я использую именно этот MPM:

alternatives-manual /usr/sbin/httpd2 /usr/sbin/httpd2.event
alternatives-update

Сделав это, вы увидите:

# httpd2 -l
Compiled in modules:
  core.c
  mod_so.c
  http_core.c
  event.c
  mod_unixd.c
  
Определенно, на главный вопрос по PHP это не влияет. Но суеверия ... 
Этот шаг можно и пропустить.

Далее рассовываете предзаготовленные настроечные файлы nginx, Apache и wrapper. Все это, и описанное выше есть в архиве.
Там нужно поменять на свои IP и домены.

Команды, которые могут оказаться полезны (папка WEB содержит распакованный архив, приложенный к этому тикету):

$ cd WEB/etc/httpd2
$ scp -r conf/ root@vds.example.com:/etc/httpd2

$ cd WEB/etc/nginx
$ scp -r sites-available.d/ root@vds.example.com:/etc/nginx

$ cd WEB/var
$ scp -r www/ root@vds.example.com:/var

При настройке Apache у вас должно получиться:

# a2chkconfig_list
ports http no
ports http-localhost-8088 yes
extra httpd-addon.d no
extra httpd-autoindex yes
extra httpd-default yes
extra httpd-icons yes
extra httpd-languages yes
extra httpd-mime yes
extra httpd-mpm yes
extra httpd-multilang-errordoc yes
sites default no
sites php yes
sites ports_all no
sites vhosts no
mods access_compat yes
mods alias yes
mods authz_core yes
mods authz_host yes
mods autoindex yes
mods dir yes
mods fcgid yes
mods include yes
mods log_config yes
mods logio yes
mods mime yes
mods negotiation yes
mods systemd no

Применение настроек делается командой a2chkconfig.

nginx поставляется без таких удобств. Ссылку на /etc/nginx/sites-available.d/php.conf придется сделать самому.

Далее запускаете службы:

service httpd2 start
service nginx start

Для образа systemd, можно и без оберток:

# systemctl start httpd2
# systemctl start nginx

А далее то, для чего все затевалось:

http://vds.example.com/mem.php?max=64

    128M total
    64M requested
    SUCCESS

А хотим мы увидеть на http://vds.example.com/mem.php?max=128 вместо статуса 500

    128M total
    128M requested
    SUCCESS

PS: если страница недоступна, то требуется открыть порт. Я делал это командой 

    alterator-net-iptables write -s  +www
    
Для ее работы нужно установить пакет alterator-net-iptables (он потянет за собой и iptables)
Я не вполне уверен, что это требуется (может порты и открыты), просто так делаю всегда.

PS2: проблема легко обнаруживается и в "фирменном" образе docker php:8.1-apache (просто для моей задачи нужен именно он)
А там уже модуль Apache. Почему я столкнулся с проблемой только сейчас - вопрос.
Comment 1 Osmolovskaya Anastasia 2024-04-16 16:22:22 MSK
Добрый день, Анатолий!

Не могли бы предоставить дополнительную информацию о вашей системе, приложив вывод следующих команд: 
uname -a
cat /etc/os-release
apt-repo
rpm -qa | grep php

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

Хотелось ещё уточнить, что выводит phpinfo() при ваших настройках? Соответствует ли его вывод ожидаемому?
Comment 2 Анатолий Кирсанов 2024-04-16 17:06:52 MSK
(Ответ для Osmolovskaya Anastasia на комментарий #1)
> Не могли бы предоставить дополнительную информацию о вашей системе, приложив
> вывод следующих команд: 
> uname -a
> cat /etc/os-release
> apt-repo
> rpm -qa | grep php

Площадку для x86 я уже удалил. Осталась только 64-х битная (та, что из образа alt-p10-jeos-systemd-20240309-x86_64.iso).

# uname -a
Linux vds.example.com 6.1.79-un-def-alt1 #1 SMP PREEMPT_DYNAMIC Fri Feb 23 18:56:17 UTC 2024 x86_64 GNU/Linux

# cat /etc/os-release
NAME="starter kit"
VERSION="10"
ID=altlinux
VERSION_ID=10
PRETTY_NAME="ALT Starterkit 10 (Hypericum)"
ANSI_COLOR="1;33"
CPE_NAME="cpe:/o:alt:starterkit:10"
BUILD_ID="starter kit 10"
ALT_BRANCH_ID="p10"
HOME_URL="http://en.altlinux.org/starterkits"
BUG_REPORT_URL="https://bugs.altlinux.org/"
LOGO=altlinux

# apt-repo
-bash: apt-repo: команда не найдена

# rpm -qa | grep php
php8.2-libs-8.2.17-alt1.x86_64
php-base-2.7-alt3.x86_64
php8.2-8.2.17-alt1.x86_64
php8.2-cgi-8.2.17-alt1.x86_64

> И детальнее описать последовательность действий, приводящих к
> воспроизведению ошибки (можно только команды, которые используете именно вы,
> чтобы не возникало путаницы). 
Не вполне понял что еще требуется. Я описал, как из двух образов стартер китов (x86 и x86_64) сделать тестовую площадку. Выложил архив с необходимыми настройками. И предложил набрать в браузере определенный адрес (тестовый скрипт тоже в архиве есть). Написал что получилось и что надо.

> Хотелось ещё уточнить, что выводит phpinfo() при ваших настройках?
> Соответствует ли его вывод ожидаемому?
Там установлена memory_limit на 128 МБ. Это стандартные значения для пакета. Ничего не менял.

apt-repo, как видите, не установлена в образе.

# apt-get update
Получено: 1 http://ftp.altlinux.org p10/branch/x86_64 release [4223B]
Получено: 2 http://ftp.altlinux.org p10/branch/x86_64-i586 release [1665B]
Получено: 3 http://ftp.altlinux.org p10/branch/noarch release [2844B]
Получено 8732B за 0s (43,6kB/s).
Получено: 1 http://ftp.altlinux.org p10/branch/x86_64/classic pkglist [23,9MB]
Получено: 2 http://ftp.altlinux.org p10/branch/x86_64/classic release [137B]
Получено: 3 http://ftp.altlinux.org p10/branch/x86_64-i586/classic pkglist [17,5MB]
Получено: 4 http://ftp.altlinux.org p10/branch/x86_64-i586/classic release [142B]
Получено: 5 http://ftp.altlinux.org p10/branch/noarch/classic pkglist [7223kB]
Получено: 6 http://ftp.altlinux.org p10/branch/noarch/classic release [137B]
Получено 48,7MB за 9s (5078kB/s).                                                                                                                                                                                                                           
Чтение списков пакетов... Завершено
Построение дерева зависимостей... Завершено
Comment 3 Анатолий Кирсанов 2024-04-16 17:07:55 MSK
Created attachment 15882 [details]
Настроки PHP по лимиту памяти
Comment 4 Osmolovskaya Anastasia 2024-04-23 11:32:00 MSK
Версия пакета: php8.2-fpm-fcgi-8.2.18-alt1.x86_64

Дополнительно: в sisyphus на данный момент нет возможности проверить 

Удалось воспроизвести ошибку по следующим шагам: 
1. # systemctl stop httpd2
2. # apt-get install nginx php8.2-fpm-fcgi webserver-common
3. # cat > /etc/nginx/sites-enabled.d/default.conf <<EOF
#load_module modules/ngx_http_geoip_module.so;
#load_module modules/ngx_http_perl_module.so;
#load_module modules/ngx_mail_module.so;
#load_module modules/ngx_stream_module.so;

server {
        listen  localhost:80;
        server_name localhost localhost.localdomain;

        location / {
            root /var/www/html;
        }


location ~* ^.+\.(ogv|iso|html)\$ {
root /var/www/html;
}

location ~ \.php\$ {
root /var/www/html;
try_files \$uri =404;
include /etc/nginx/fastcgi_params;
fastcgi_pass unix:/var/run/php8.2-fpm/php8.2-fpm.sock;
fastcgi_param SCRIPT_FILENAME /var/www/html/\$fastcgi_script_name;
}

location ~ /\.ht {
deny all;
}

    access_log  /var/log/nginx/access.log;
}
EOF

4. # cat > /var/www/html/index.php <<EOF
<?php
header('Content-Type: text/plain');

print ini_get('memory_limit') . ' total' .  PHP_EOL;

    $max=intval($_GET['max']);
    if (!$max) $max = 127;

print $max . 'M requested' . PHP_EOL;

    for($i=1;$i<=$max;$i++)
           $a[]=str_repeat(chr($i),1024*1024); // 1 Mb
    die("SUCCESS");
?>
EOF

5. # systemctl restart nginx php8.2-fpm

6. Проверить вывод:
# curl -s 'http://localhost/index.php?max=64'
# curl -s 'http://localhost/index.php?max=128'

Ожидаемый результат:
успешный вывод команд
 128M total
    64M requested
    SUCCESS

    128M total
    128M requested
    SUCCESS

Реальный результат: 
При выполнении # curl -s 'http://localhost/index.php?max=64' успешный вывод

При выполнении # curl -s 'http://localhost/index.php?max=128' вывод отсутствует. В логах /var/log/nginx/error.log будет ошибка:

FastCGI sent in stderr: "PHP message: PHP Fatal error:  Allowed memory size of 134217728 bytes exhausted (tried to allocate 1052672 bytes)
in /var/www/html/index.php on line 12" while reading response header from upstream, client: 127.0.0.1, server: localhost,
request: "GET /index.php?max=128 HTTP/1.1", upstream: "fastcgi://unix:/var/run/php8.2-fpm/php8.2-fpm.sock:", host: "localhost"
Comment 5 Osmolovskaya Anastasia 2024-04-23 12:07:35 MSK
Воспроизводится в sisyphus
Comment 6 keleth 2024-04-24 10:07:43 MSK
Позволю себе ворваться, но здесь нет ошибки.
Все правильно, этот скрипт поедает заведомо больше.
Достаточно добавить в цикле, где заполняется строка вывод реально выделяемой памяти:
print(memory_get_usage() / 1048576 . "Mb /". memory_get_usage(true) / 1048576 . " Mb Real\n");

Будет видно, что когда автор ожидает, что у него 64, на самом деле он уже скушал 128.
64.624717712402Mb / 128Mb Real
PHP Fatal error:  Allowed memory size of 134217728 bytes exhausted (tried to allocate 1052672 bytes)
Comment 7 Анатолий Кирсанов 2024-04-24 10:40:38 MSK
(Ответ для keleth на комментарий #6)
> Позволю себе ворваться, но здесь нет ошибки.
> Все правильно, этот скрипт поедает заведомо больше.
> Достаточно добавить в цикле, где заполняется строка вывод реально выделяемой
> памяти:
> print(memory_get_usage() / 1048576 . "Mb /". memory_get_usage(true) /
> 1048576 . " Mb Real\n");
> 
> Будет видно, что когда автор ожидает, что у него 64, на самом деле он уже
> скушал 128.
> 64.624717712402Mb / 128Mb Real
> PHP Fatal error:  Allowed memory size of 134217728 bytes exhausted (tried to
> allocate 1052672 bytes)

На этом скрипте так быть не должно. Там в цикле запрашивается 1МБ за один проход. Если PHP при этом занимает не один, а два мегабайта - в этом и проблема.

Для интересующихся - этот скрипт основан на стандартном тесте Битрикс. Мои дополнения там незначительны и имеют другой отступ. Если выяснится, каким-то чудом, что тест написан неверно, будет повод обратиться в поддержку Битрикс.

А пока ситуаций так себе ...
Comment 8 keleth 2024-04-24 11:24:39 MSK
Ну как не должно? это суровая действительность.
Проверно на доступных мне: Debian c php 8.2/8.3, Альте 10 и Сизиф.
Comment 9 Анатолий Кирсанов 2024-04-24 14:40:52 MSK
(Ответ для keleth на комментарий #8)
> Ну как не должно? это суровая действительность.
> Проверно на доступных мне: Debian c php 8.2/8.3, Альте 10 и Сизиф.

Да причем тут это? Я проверял скриптом, который в Битрикс используется для проверки РЕКАЛЬНО ДОСТУПНОГО
Comment 10 Анатолий Кирсанов 2024-04-24 14:43:26 MSK
млин, что-то не то нажал. И не исправить ничего ...

Это тест реально доступного объема памяти. И реально доступный объем меньше в два раза выделенного по настройке memory_limit. Какая разница, что это везде так?

Да я и знаю, что это так. О чем написал в самом первом сообщении. Даже в фирменном docker конетейнере эта болячка есть. А там другой SAPI. Я проверял сам в FCGI. Команда убедилась в реальности проблемы на FPM. А docker сделан на mod_apache.
Comment 11 keleth 2024-04-24 15:23:51 MSK
Так ошибка в тесте, вот пришлось пойти и вспоминать основы PHP:
"PHP активно использует механизм copy-on-write. Это означает, что при попытке внутри функции что-то записать в переданные ей параметры, вначале будет сделана копия этой переменной, а уж затем в неё что-то запишется. "

Таким образом, когда мы добираемся в цикле до 64 итерации, то, чтобы добавить еще 1 мегабайт в массив, нам нужно уже 128 - все, финал.
Вопросы к авторам теста.
Comment 12 Анатолий Кирсанов 2024-04-24 15:49:38 MSK
(Ответ для keleth на комментарий #11)
> Так ошибка в тесте, вот пришлось пойти и вспоминать основы PHP:
> "PHP активно использует механизм copy-on-write. Это означает, что при
> попытке внутри функции что-то записать в переданные ей параметры, вначале
> будет сделана копия этой переменной, а уж затем в неё что-то запишется. "
> 
> Таким образом, когда мы добираемся в цикле до 64 итерации, то, чтобы
> добавить еще 1 мегабайт в массив, нам нужно уже 128 - все, финал.
> Вопросы к авторам теста.

Тогда логично было бы при запросе 64 МБ получить 65 использованных. Разве нет?

Вот оригинальный тест:

elseif (@$_GET['memory_test'])
{
	$max=intval($_GET['max']);
	if (!$max) $max = 255;
	for($i=1;$i<=$max;$i++)
	       $a[]=str_repeat(chr($i),1024*1024); // 1 Mb
	die("SUCCESS");
}

Там, как раз, учтен этот пресловутый мегабайт.
Comment 13 keleth 2024-04-28 07:40:49 MSK
Я честно не совсем понял какой мегабайт был учтен, здесь просто создается строка размером примеррно с мегабайт и добавляется как новый элемент в массив $a.

Ранее я тоже не совсем был прав, но топтался около.
Когда существующий массив достигает пределов выделенной под него памяти, создается новый массив с бОльшим объемом зарезервированной под него памяти, в новый массив копируется содержимого старого, добавляется новый элемент и подменяется в переменной, по указателю или как там оно (здесь нам не важно), вот пока весь процесс переноса данных не быдет завершен - нельзя освободить старые данные, отсюда и удвоение размера памяти в пиковых случаях.

Поэтому и говорю, что тест не совсем корректный.



(Ответ для Анатолий Кирсанов на комментарий #12)
> (Ответ для keleth на комментарий #11)
> > Так ошибка в тесте, вот пришлось пойти и вспоминать основы PHP:
> > "PHP активно использует механизм copy-on-write. Это означает, что при
> > попытке внутри функции что-то записать в переданные ей параметры, вначале
> > будет сделана копия этой переменной, а уж затем в неё что-то запишется. "
> > 
> > Таким образом, когда мы добираемся в цикле до 64 итерации, то, чтобы
> > добавить еще 1 мегабайт в массив, нам нужно уже 128 - все, финал.
> > Вопросы к авторам теста.
> 
> Тогда логично было бы при запросе 64 МБ получить 65 использованных. Разве
> нет?
> 
> Вот оригинальный тест:
> 
> elseif (@$_GET['memory_test'])
> {
> 	$max=intval($_GET['max']);
> 	if (!$max) $max = 255;
> 	for($i=1;$i<=$max;$i++)
> 	       $a[]=str_repeat(chr($i),1024*1024); // 1 Mb
> 	die("SUCCESS");
> }
> 
> Там, как раз, учтен этот пресловутый мегабайт.
Comment 14 Анатолий Кирсанов 2024-04-28 10:10:21 MSK
(Ответ для keleth на комментарий #13)
> Я честно не совсем понял какой мегабайт был учтен
Лимитом в цикле. Стандартный тест Битрикс доволен хостингом, если найдет 256 МБ ОЗУ.

> Когда существующий массив достигает пределов выделенной под него памяти,
> создается новый массив с бОльшим объемом зарезервированной под него памяти,
> в новый массив копируется содержимого старого, добавляется новый элемент и
> подменяется в переменной, по указателю или как там оно (здесь нам не важно),
> вот пока весь процесс переноса данных не будет завершен - нельзя освободить
> старые данные, отсюда и удвоение размера памяти в пиковых случаях.
Выглядит занимательно, но так не должно быть. Таких экстремальных ситуаций в реальных скриптах полно.

> Поэтому и говорю, что тест не совсем корректный.
Чтобы долго не ходить по кругу (я не такой гуру в таких тонких вопросах), я обратился с вопросом в Битрикс (обращение 3501810).
Посмотрим, что они скажут.

Что касается самого тикета в Багзилле, то он появился не просто так. Я не припомню за 15 лет работы с Битрикс такой ситуации.
Толи я был слеп все это время и детально не проверял это ограничение (я начинал с PHP 4), толи что-то изменилось повсеместно во всех сборках PHP.
Я пока открыт для любого варианта.