請不要用php進行base64編碼文件上傳

1、場景

領導:小A同窗,咱們要作一個樣本上傳進行分析的功能,你看下是否使用base64編碼加進去,這樣客戶端的同窗就不須要用form-data方式來上傳了,直接使用json格式就能夠上報,可讓格式上報統一。php

小A:好的,領導,立刻搞定!nginx

咋看上面的對話沒啥問題,不少公司團隊內部爲了一些標準化的問題,都會進行一些技術選型問題,可是噩夢也就從這個對話開始,功能實現固然都是很簡單的,先來看簡單流程圖:docker


自己的流程是一個很簡單的文件轉換成base64上傳,再服務端decode保存,在開發聯調過程當中沒有問題,很是完美的走下去了。json

2、問題來了

忽然有一天終端同窗誤操做將一個37M文件上傳,nginx與php-fpm文件上傳限制均爲(60M),可是在界面出現500錯誤,進入docker 日誌查看有一條數據:bash

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

玩php的基本都知道這是啥意思,就是代碼運行過程當中使用內存超過 咱們php.ini設置的memory_limit 的值,而後就屁顛屁顛進入php.ini找參數配置,很快找到:php-fpm

memory_limit=128M
源碼分析

而後就轉念一想,不該該出現這個問題,咱們知道,php的內部變量使用cow(寫時複製)機制來實現,那麼內存申請只有在變量賦值變動纔會進行測試

3、測驗

接下來咱們單獨寫一個程序來進行測試,將一個4.89M文件進行base64_encode 編碼 與base64_decode解碼,查看各自佔用內存以及過程當中佔用峯值內存編碼

<?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.25M,高出總體文件大小很少,這在文件加載過程有一些臨時申請內存的問題
  2. base64_encode佔用內存,這個在使用的時候,就已經將內存差很少進行一個double,而這基本上也是在內核解析過程當中,進行了內存申請,能夠理解,文件自己佔用內存+base64_encode 解析後的內存,兩分內存同時存在的
  3. base64_decode操做,這個操做就是解密了,解密過程當中,這裏直接就佔用了3倍多的內存操做,問題就出在這裏,在場景中出現的問題是一個37M的文件,爲何就把單個fpm的128M內存佔滿了呢


4、源碼解析

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)

在wiki上解釋是:

void *safe_emalloc(size_t nmemb, size_t size, size_t offset) 分配緩衝區來存放每塊大小爲 size 字節的 nmemb 塊,並附加 offset 字節。相似於 emalloc(nmemb * size + offset),但增長了針對溢出的特殊保護。

那麼我能夠簡單的認爲,就是在encode過程當中,從新申請了內存,申請的內存大小是文件自己的 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函數,來看下wiki的解釋

void *emalloc(size_t size) 分配 size 字節的內存。

這個就比較好理解了,傳入參數內存再進行一個double拷貝就能夠,

那麼咱們進行一個decode的內存峯值的計算:

峯值內存=(4/3+4/3) *4.89 =13.04

基本與咱們測試的結果相差很少,由於精度關係,咱們進行四捨五入的計算,測試代碼是精準計算,因此會有小數點誤差。


5、總結

那這就能夠理解爲何一個爲何在咱們一個37M的文件,不能再128M內存進行base64_encode與base64_decode操做,固然這裏有一些臨時變量沒有及時釋放內存的狀況,可是經過源碼分析能夠知道,要作一次這樣場景來進行文件上傳,單純文件的內存損耗是2.6倍左右,因此爲了節省內存,咱們不要再用這個方式來進行操做了,很費內存滴

相關文章
相關標籤/搜索