Пожалуйста, не используйте php для загрузки файла в кодировке base64

PHP

1. Сценарий

Лидер: Сяо А, мы сделаем функцию загрузки образцов для анализа.Вы можете увидеть, используете ли вы кодировку base64 для их добавления, чтобы учащимся на стороне клиента не нужно было использовать метод данных формы для загрузки, и они могут отчитываться напрямую в формате json., что может сделать формат отчёта унифицированным.

Маленький А: Хорошо, лидер, сделай это прямо сейчас!

На первый взгляд, приведенный выше диалог не является проблемой. Многие команды компаний будут выполнять некоторые технические задачи по выбору некоторых проблем стандартизации, но с этого диалога начинается кошмар. Конечно, реализация функции очень проста. Давайте посмотрим на простой процесс первый.


Сам процесс представляет собой очень простое преобразование файла в загрузку base64, а затем сервер декодирует и сохраняет, нет никаких проблем в процессе разработки и совместной отладки, и он проходит отлично.

Во-вторых, проблема

Вдруг в один прекрасный день терминальный одноклассник загрузил по ошибке файл 37M.Ограничение загрузки файлов nginx и php-fpm у обоих (60M), но на интерфейсе появилась ошибка 500, а в логе докера есть кусок данных :

Allowed memory size of 8388608 bytes exhausted (tried to allocate 1298358 bytes)

Любой, кто играет в php, в основном знает, что это значит, то есть память, используемая во время выполнения кода, превышает значение memory_limit, установленное нашим php.ini, а затем входит в php.ini, чтобы найти параметр конфигурации, и быстро находит:

memory_limit=128M

Потом подумал, такой проблемы быть не должно, мы знаем, что внутренние переменные php реализованы с помощью механизма cow (copy-on-write), поэтому приложение памяти будет выполняться только при изменении назначения переменных.

3. Тест

Далее пишем отдельную программу для тестирования, кодирования base64_encode и декодирования base64_decode файла 4.89M, и проверяем занимаемую каждым из них память и пиковую занятую память в процессе

<?php
$mid = memory_get_usage();
$apk_content = file_get_contents(__DIR__ . '/4bc1c8a05b8505662be778b6dad23b55.apk');
var_dump('文件加载到内存:' . round((memory_get_usage() - $mid) / 1024 / 1024, 2) . 'M');
var_dump('过程中峰值使用的内存:' . round(memory_get_peak_usage() / 1024 / 1024, 2) . 'M');

unset($mid);
$mid = memory_get_usage();
$base64_encode = base64_encode($apk_content);unset($apk_content);
var_dump('base64_encode占用内存:' . round((memory_get_usage() - $mid) / 1024 / 1024, 2) . 'M');
var_dump('过程中峰值使用的内存:' . round(memory_get_peak_usage() / 1024 / 1024, 2) . 'M');

unset($mid);
$mid = memory_get_usage();
base64_decode($base64_encode);
var_dump('base64_decode占用内存:' . round((memory_get_usage() - $mid) / 1024 / 1024, 2) . 'M');
var_dump('过程中峰值使用的内存:' . round(memory_get_peak_usage() / 1024 / 1024, 2) . 'M');
unset($mid);



执行结果:

string(29) "文件加载到内存:4.89M"
string(38) "过程中峰值使用的内存:5.25M"
string(33) "base64_encode占用内存:1.63M"
string(39) "过程中峰值使用的内存:11.76M"
string(30) "base64_decode占用内存:0M"
string(38) "过程中峰值使用的内存:13.4M"


Из приведенных выше результатов видно, что

  1. Нет большой проблемы с памятью, используемой для загрузки файлов.Пиковое значение, используемое в процессе загрузки, составляет 5,25 МБ, что ненамного превышает общий размер файла.Есть некоторые временные проблемы с памятью в процессе загрузки файла.
  2. base64_encode занимает память.При таком использовании память почти удваивается,и это в основном приложение памяти в процессе парсинга ядра.Понятно,что сам файл занимает память + base64_encode разобранная память,две копии памяти существуют одновременно время
  3. Операция base64_decode,эта операция расшифровка.В процессе расшифровки более чем в 3 раза напрямую занята операция памяти.Проблема кроется здесь.Проблема в сцене 37M файл,зачем использовать один fpm 128M Память заполнена ?


Четыре, анализ исходного кода

анализ исходного кода base64_encode

Сначала найдите соответствующий c файл base64.c, найдите внутрифункция php_base64_encode

PHPAPI zend_string *php_base64_encode(const unsigned char *str, size_t length) /* {{{ */
{
	const unsigned char *current = str;
	unsigned char *p;
	zend_string *result;

	result = zend_string_safe_alloc(((length + 2) / 3), 4 * sizeof(char), 0, 0);
	p = (unsigned char *)ZSTR_VAL(result);
        ...
}

Давайте сначала проанализируем этот код, потому что он связан с проблемами памяти, а затем мы рассмотрим

result = zend_string_safe_alloc(((length + 2) / 3), 4 * sizeof(char), 0, 0);

Что это значит?

Чтобы применить для памяти, вызывается последняя функция:

safe_emalloc(size_t nmemb, size_t size, size_t offset)

Объяснение на вики такое:

void *safe_emalloc(size_t nmemb, size_t size, size_t offset) Выделите буферы для хранения блоков размераsizeбайтnmembзаблокировать и добавитьoffsetбайт. похожий наemalloc(nmemb * size + offset), но добавляет специальную защиту от переполнения.

Тогда я могу просто подумать, что память повторно применяется в процессе кодирования, а размер применяемой памяти составляет 4/3 от размера самого файла, плюс размер самого исходного файла, тогда можно понять размер пика так как

Пиковая память = 7/3 * 4,89 = 11,41

Тогда он в основном согласуется с размером пика во время нашего эксперимента.


операция base64_decode

Аналогично проводим анализ исходного кода

PHPAPI zend_string *php_base64_decode_ex(const unsigned char *str, size_t length, zend_bool strict) /* {{{ */
{
	const unsigned char *current = str;
	int ch, i = 0, j = 0, padding = 0;
	zend_string *result;

	result = zend_string_alloc(length, 0);
	...

}

Используемый здесь zend_string_alloc используется для обращения к памяти, тогда базовой функцией является функция emalloc, см. объяснение в вики.

void *emalloc(size_t size) распределятьsizeбайт памяти.

Это проще понять, просто передайте в память параметров и сделайте двойную копию,

Затем выполняем расчет декодированного пика памяти:

Пиковая память = (4/3+4/3) * 4,89 = 13,04

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


V. Резюме

Тогда понятно, почему мы не можем выполнять операции base64_encode и base64_decode с памятью 128M в файле 37M.Конечно, есть некоторые временные переменные, которые не освобождают память вовремя, но через анализ исходного кода мы можем знать, что нам нужно сделать этот сценарий один раз.Для загрузки файлов потребление памяти простым файлом составляет примерно 2,6 раза, поэтому для экономии памяти мы не должны использовать этот метод для работы, он очень потребляет память