php-fpmを動かしていて以下のようなエラーが出た時には、memory_limit 以上にメモリを割り当てようとしているためだと思う。
Allowed memory size of xxxx bytes exhausted (tried to allocate xxxx bytes) ...
ここでpsでプロセスのメモリ使用状態を見た時に、php.ini
のmemory_limit
で設定した値以上にphp-fpmプロセスがメモリを確保しているように見えるときがある。
例えば、memory_limit
を16M
にしていた時のプロセスの状態を見ると、メモリは36860(36M)
割り当てられているように見えるけど、php-fpmは普通に動いている。
psで見える物理メモリは実際に書き込まれたときに増えるはずなので実メモリに36M程度割り当てられていることになる...はず。
hoge 28849 0.0 3.4 427360 36860 ? S Dec22 2:37 \_ php-fpm: pool example.com
そもそもphp-fpmはどのようにメモリを確保しているのか知らない...とか思い始めたので調べてみた。
PHPのメモリ管理の仕組みを見る
該当のエラーが発生した時のエラーメッセージを元に調べると、zend_alloc.c のzend_mm_alloc_pages()
, zend_mm_alloc_huge()
, zend_mm_realloc_huge()
の中で定義されている模様。
#if ZEND_MM_LIMIT if (UNEXPECTED(ZEND_MM_CHUNK_SIZE > heap->limit - heap->real_size)) { if (zend_mm_gc(heap)) { goto get_chunk; } else if (heap->overflow == 0) { #if ZEND_DEBUG zend_mm_safe_error(heap, "Allowed memory size of %zu bytes exhausted at %s:%d (tried to allocate %zu bytes)", heap->limit, __zend_filename, __zend_lineno, size); #else zend_mm_safe_error(heap, "Allowed memory size of %zu bytes exhausted (tried to allocate %zu bytes)", heap->limit, ZEND_MM_PAGE_SIZE * pages_count); #endif return NULL; } }
#if ZEND_MM_LIMIT if (UNEXPECTED(new_size - old_size > heap->limit - heap->real_size)) { if (zend_mm_gc(heap) && new_size - old_size <= heap->limit - heap->real_size) { /* pass */ } else if (heap->overflow == 0) { #if ZEND_DEBUG zend_mm_safe_error(heap, "Allowed memory size of %zu bytes exhausted at %s:%d (tried to allocate %zu bytes)", heap->limit, __zend_filename, __zend_lineno, size); #else zend_mm_safe_error(heap, "Allowed memory size of %zu bytes exhausted (tried to allocate %zu bytes)", heap->limit, size); #endif return NULL; } }
#if ZEND_MM_LIMIT if (UNEXPECTED(new_size > heap->limit - heap->real_size)) { if (zend_mm_gc(heap) && new_size <= heap->limit - heap->real_size) { /* pass */ } else if (heap->overflow == 0) { #if ZEND_DEBUG zend_mm_safe_error(heap, "Allowed memory size of %zu bytes exhausted at %s:%d (tried to allocate %zu bytes)", heap->limit, __zend_filename, __zend_lineno, size); #else zend_mm_safe_error(heap, "Allowed memory size of %zu bytes exhausted (tried to allocate %zu bytes)", heap->limit, size); #endif return NULL; } } #endif
エラーが発生したらzend_mm_safe_error()
が呼び出されるようだった。
いずれの関数も limit(memory_limit)
と今割り当てられているページサイズ(real_size)
の差を計算し、その差より割り当てようとしているメモリが多い場合はGC(zend_mm_gc()
)を実行し、それでもあふれる場合はエラーとなる...ように見える。
zend_mm_gc()
の中身は見ていない。
static ZEND_COLD ZEND_NORETURN void zend_mm_safe_error(zend_mm_heap *heap, const char *format, size_t limit, #if ZEND_DEBUG const char *filename, uint32_t lineno, #endif size_t size) { heap->overflow = 1; zend_try { zend_error_noreturn(E_ERROR, format, limit, #if ZEND_DEBUG filename, lineno, #endif size); } zend_catch { } zend_end_try(); heap->overflow = 0; zend_bailout(); exit(1); }
で、これらのエラーの時のメモリはどの値を見ているのかと思い、各関数を見ていると、zend_mm_heap
の値を参照しているようだった。
struct _zend_mm_heap { #if ZEND_MM_CUSTOM int use_custom_heap; #endif #if ZEND_MM_STORAGE zend_mm_storage *storage; #endif #if ZEND_MM_STAT size_t size; /* current memory usage */ size_t peak; /* peak memory usage */ #endif zend_mm_free_slot *free_slot[ZEND_MM_BINS]; /* free lists for small sizes */ #if ZEND_MM_STAT || ZEND_MM_LIMIT size_t real_size; /* current size of allocated pages */ #endif #if ZEND_MM_STAT size_t real_peak; /* peak size of allocated pages */ #endif #if ZEND_MM_LIMIT size_t limit; /* memory limit */ int overflow; /* memory overflow flag */ #endif
ここで確保したメモリを管理しているように見える。
zend_mm
って何の略なんだと思って調べたら Zend Memory Manager のことらしい。
Zend Memory Manager はemalloc()
という関数によってメモリを確保している。
USE_ZEND_ALLOC=0
を指定するとemalloc()
を使わず、システムのデフォルトのメモリアロケーターを使うらしい。(Linuxだとmalloc()
ってことか?)
ということで、PHPのメモリ管理には独自のもの(Zend Memory Manager)が使われている、ということが分かった。
_zend_mm_heap
構造体を見ると、memory_limit
の値はZend Memory Managerの中で管理されているメモリ量から管理されていることも分かった。
まとめ
この記事ではPHPのメモリ管理の仕組みについて調べてみて自分なりにわかったことをまとめた。
PHPはデフォルトではZend Memory Managerという独自のメモリ管理機能を使っており、emalloc()
という関数でメモリを確保していることがわかった。
この場合、memory_limit
はZend Memory Managerの中のzend_alloc.c
の中の_zend_mm_heap
構造体の中で管理されているメモリの状況に応じてエラーハンドリングを行っているらしいことがわかった。