自由帳

とりとめのない学習メモです。主に Web サービスのシステム基盤や運用に関することを書いています

Allowed memory size of xxxx bytes exhausted (tried to allocate xxxx bytes)が発生したので、PHPのメモリはどこで管理されているものなのか調べた

php-fpmを動かしていて以下のようなエラーが出た時には、memory_limit 以上にメモリを割り当てようとしているためだと思う。

Allowed memory size of xxxx bytes exhausted (tried to allocate xxxx bytes) ...

ここでpsでプロセスのメモリ使用状態を見た時に、php.inimemory_limitで設定した値以上にphp-fpmプロセスがメモリを確保しているように見えるときがある。 例えば、memory_limit16Mにしていた時のプロセスの状態を見ると、メモリは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.czend_mm_alloc_pages(), zend_mm_alloc_huge(), zend_mm_realloc_huge() の中で定義されている模様。

  • zend_alloc.czend_mm_alloc_pages()の中のエラー処理
  • #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;
                        }
                    }
    
  • zend_alloc.czend_mm_realloc_huge()の中のエラー処理
  • #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;
                    }
                }
    
  • zend_alloc.czend_mm_alloc_huge()の中のエラー処理
  • #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()の中身は見ていない。

  • zend_alloc.czend_mm_safe_error()
  • 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の値を参照しているようだった。

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

参考