您好,登錄后才能下訂單哦!
一、場景
領導:小A同學,我們要做一個樣本上傳進行分析的功能,你看下是否使用base64編碼加進去,這樣客戶端的同學就不需要用form-data方式來上傳了,直接使用json格式就可以上報,可以讓格式上報統一。
小A:好的,領導,馬上搞定!
咋看上面的對話沒啥問題,很多公司團隊內部為了一些標準化的問題,都會進行一些技術選型問題,但是噩夢也就從這個對話開始,功能實現當然都是很簡單的,先來看簡單流程圖:
本身的流程是一個很簡單的文件轉換成base64上傳,再服務端decode保存,在開發聯調過程中沒有問題,非常完美的走下去了。
二、問題來了
突然有一天終端同學誤操作將一個37M文件上傳,nginx與php-fpm文件上傳限制均為(60M),但是在界面出現500錯誤,進入docker 日志查看有一條數據:
Allowed memory size of 8388608 bytes exhausted (tried to allocate 1298358 bytes)
玩php的基本都知道這是啥意思,就是代碼運行過程中使用內存超過 我們php.ini設置的memory_limit 的值,然后就屁顛屁顛進入php.ini找參數配置,很快找到:
memory_limit=128M
然后就轉念一想,不應該出現這個問題,我們知道,php的內部變量使用cow(寫時復制)機制來實現,那么內存申請只有在變量賦值變更才會進行
三、測驗
接下來我們單獨寫一個程序來進行測試,將一個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"
通過上面結果可以看出
四、源碼解析
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
基本與我們測試的結果相差不多,因為精度關系,我們進行四舍五入的計算,測試代碼是精準計算,所以會有小數點偏差。
五、總結
那這就可以理解為什么一個為什么在我們一個37M的文件,不能再128M內存進行base64_encode與base64_decode操作,當然這里有一些臨時變量沒有及時釋放內存的情況,但是通過源碼分析可以知道,要做一次這樣場景來進行文件上傳,單純文件的內存損耗是2.6倍左右,所以為了節省內存,我們不要再用這個方式來進行操作了,很費內存的
以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持億速云。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。