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構造体の中で管理されているメモリの状況に応じてエラーハンドリングを行っているらしいことがわかった。