Lock in $30 Savings on PRO—Offer Ends Soon! ⏳

PHP で PHP のメモリプロファイラをつくろう

sji
October 08, 2023

PHP で PHP のメモリプロファイラをつくろう

PHP カンファレンス 2023
10:50 〜
Track2

sji

October 08, 2023
Tweet

More Decks by sji

Other Decks in Programming

Transcript

  1. コンピュータは数値で動く 2 つの状態を持つものの並び = 信号 = 数値 電気の強弱 磁性体の向き 0

    と 1 この信号パターンならこう動く、というのが機械への命令 命令を動作の種類と内容を分けて構成すれば様々な情報が数値に
  2. 扱うデータの単位 1 ビット 0 か 1 のどちらかの値を持つ単位 1 バイト 8

    ビットの並び 2 の 8 乗で 0 から 255 までの 256 種類の値を持つ 1 キロバイト 1,000 バイト or 1,024 バイト 1 メガバイト 1,000 キロバイト or 1,024 キロバイト 1 ギガバイト 1,000 メガバイト or 1,024 メガバイト
  3. メモリは OS が管理 コンピュータ上では複数プログラムが同時に動く マルチプロセス OS がプロセス間でハードウェア資源の利用を仲介 メモリも OS が管理

    プロセスは OS にメモリを要求 OS はプロセス間を仮想メモリ空間で隔離しながらメモリを割り当て プロセスは自分だけのメモリ領域を使っているように見える ストレージとも組み合わせてやりくり(スワップ)
  4. memory_limit とは リクエストごとのメモリ上限を指定する ini 項目 安全装置 PHP はマルチプロセスでリクエストを処理 マシンリソースを食いつぶさないよう制限できる リクエストの処理が

    memory_limit を越えたらプロセスが自刃する 最近のデフォルト値は 128MB プロジェクトや利用箇所によって違う設定にしてるはず
  5. ZendMemoryManager PHP 処理系のメモリ管理部品 2 種類のメモリ領域 リクエストごとのメモリ領域 処理系や C 拡張は libc

    の malloc や free を パクった emalloc や efree などの API で操作 永続メモリ領域 通常 libc の malloc とか free が使われる どちらも最終的には OS からもらう 仮想メモリ領域を使う 名前のZend はZeev とAndi の名前から
  6. memory_limit の制限の対象外 永続メモリ領域や Zend Memory Manager 管理外の領域 は memory_limit の対象外

    たとえば Xdebug が動作に使うメモリ phpdbg だと Zend Memory Manager 経由で似た情報を持ったり どちらでカバレッジをとるかで PHPUnit でのメモリ使用量報告が大きく変わる opcache が SHM として共有メモリ上に管理するのも別 今回のトークではこの辺の話はしない
  7. メモリの確保は使うときに処理系がこっそり スクリプト内で「メモリ領域を確保」のような関数は(基本的に)ない PHP スクリプトでメモリが必要になったら裏で処理系が ZMM で確保 変数を使うとか 配列の要素を追加するとか オブジェクトを new

    するとか スクリプト内のどの部分がどのくらいメモリを必要とするかは意識せず使える 逆に言うとふつうには意識することができない memory_get_usage() を至るところに差し込めば近づけはする
  8. 前提: zval について PHP の値は zval という 128bit (= 16byte)

    の構造体で表現 zval は値の種類を表す型情報と、値そのものを持つ 値そのものは union で表現される 64bit (= 8byte )分 long, double, true, false, string, array, object, null などなんでも入る string 、array 、object 、resource の zval は詳細データ構造へのポインタが値 処理系で内部的に使う一部の特別な値でも利用 クラスの定義情報を名前から引くための辞書など
  9. VM スタック ローカル変数領域は VM スタック内の zval 配列 関数呼び出しのたびに VM スタックへ必要なロ

    ーカル変数や引数用の領域を確保 関数が return するたびにローカル変数の zval を 含むコールフレーム領域を取り除く 整数値や浮動小数点数、bool 値のような単純な 値はこれで十分 zval が詳細データへのポインタを持つ系なら? オブジェクトや配列、文字列など 実体は別の領域、zval 削除だけではダメ function f1() {$a = 123; f2();} function f2() {$b = 456; $c = 789; f3();} function f3() {$e = 'abc'; /* 今ここを実行中とする *
  10. 循環参照 GC 配列とオブジェクトでは循環参照が発生し得る 参照カウントでは循環参照を解放できない 参照カウントを減らした際 0 にならない配列やオブジェクトがあれば容疑者入り 処理系は疑わしいものを root buffer

    と呼ばれるバッファへすべて記録 参照カウントが 0 になるなどして破棄される際は root buffer から除去 root buffer がある程度たまって閾値を超えると循環参照 GC を実行 細かい話は y-uti 先生の『PHP の GC の話』を見よう https://www.slideshare.net/y-uti/php-gc
  11. 循環参照 GC の利用上の注意点 循環参照 GC のトリガは root buffer の埋まり具合 埋まり具合が閾値を超えないと循環参照

    GC は実行されない つまり memory_limit を超えても循環参照 GC は実行されない 手動で良いタイミングに gc_collect_cycle() を呼ぶだけで memory_limit 越えを避けられる場合も
  12. 何がリクエストの中でメモリを使っているか、 駆け足でまとめる スクリプトコンパイル時の作業用領域 コンパイルされた VM 命令列 グローバル変数用の zval 領域 文字列、配列、オブジェクトの実体

    定数用の zval 領域 クラスや関数の静的領域 VM スタック 処理系内の各種管理データ 定数や関数・クラスの各種定義情報とか EG(objects_store ) とか などなど
  13. 計測の必要性: メモリ使用量はサーバ性能につながり得る Web システムの多くは I/O バウンドなので CPU が遊びがち サーバ資源を有効活用するのに PHP

    ワーカを増やしたい CPU が余っていても各ワーカプロセスがメモリを食いすぎると並列度を上げづらい 実質的に可処分メモリ / ワーカの消費メモリが並列度の限界になる
  14. 計測の必要性: long running への安心感 リクエストごとに状態をリセットしない AltFPM が成熟してきた roadrunner / swoole

    など PHP ワーカがリクエストをまたいで状態を保持し続ける利用シーンも今後増えていくかも 何がどこでメモリを使っているか分からないのは可観測性に問題
  15. memory_get_usage() / memory_get_peak_usage() memory_get_usage() で現在のメモリ使用量取 得 memory_get_peak_usage() でリクエスト内での 最大使用量取得 PHP

    8.2 以降は memory_reset_peak_usage() で 最大使用量記録をリセットできる ある処理を行う前後での差分を取ることで、 その処理でのメモリ増加量や減少量を計測で きる しかし…… $before = memory_get_usage(); $result = なにかの処理 (); $diff = memory_get_usage() - $befo
  16. xdebug / xhprof 処理時間を計測するプロファイラ機能を持つ おまけ機能で関数の出入りの際にメモリ使用量・ピーク使用量を自動で記録できる PECL の xhprof は最近メンテナが変わって復活 tideways_xhprof

    は役割を終えたとして今月(10/6 )アーカイブされた 本質的には memory_get_usage() / memory_get_peak_usage() 方式と同じ 減らない理由が分からない、何が減っていないのかが分からない https://xdebug.org/ https://github.com/longxinH/xhprof
  17. php-memprof 現状での良い選択肢の一つ PCEL の C 拡張 スクリプトの全関数実行をフックして今どこを 実行しているか、を追跡 zend_mm_set_custom_handlers() で

    ZendMM の メモリ確保・解放処理をフック emalloc() や efree() の実装を差し替え どこで確保されたメモリがどこで解放された か、解放されていないのはどこか、を関数実 行単位で追跡可能 memory_limit 超過時に自動出力する機能も https://github.com/arnaud-lb/php-memory- profiler
  18. php-meminfo PECL にはないが C 拡張 meminfo_dump() を提供 呼び出し時点のスクリプト内のコールスタッ クの変数情報を JSON

    としてダンプする ダンプ内容は PHP スクリプトで解析・集計可能 「何が」メモリ食いかをかなり絞り込める https://github.com/BitOne/php-meminfo
  19. 前提: 旧作・reli PHP の PHP による PHP のためのプロファイラ 前から PHP

    スクリプトの処理時間を計測するプロファイラ reli を PHP で作ってる 処理系の ELF バイナリと procfs のメモリマップを解析 外部プロセスの処理系の重要構造体の仮想アドレスを特定 FFI でシステムコールを呼び別プロセスの処理系内のメモリを読む gdb などのデバッガと同じようなことをやる 処理系内部のメモリレイアウトの知識を持って内部情報を解釈 実行中の関数・VM 命令のコールトレースをサンプリングで取得 よく取れるものがボトルネック https://github.com/reliforp/reli-prof
  20. プロセス外からのコピー速度 いまどきの DDR4 や DDR5 の速度ならわりといける? process_vm_readv で別プロセスから 1GB 分のメモリをコピってくるのに家のマシンで

    234 ミリ秒 システムコールの処理としてのオーバーヘッドがまあまあある 素朴な memcpy などとは雲泥 とはいえよくある memory_limit はこの 1/4 や 1/8 以下 遅いことは遅いが許容できないほどでもない 本当に困ったら対象へ C 拡張でも突っ込めば memcpy の速度で共有メモリ域へコピー可能
  21. リクエストごとのメモリプールの構造 ZendMM の管理メモリは 2MB ごとのチャンク 各チャンク内部で更に 4KB ごとのページへ分割 emalloc は各チャンクを更に小分けにしたり複数

    ページまとめたりした領域を返す 各チャンク領域先頭は zend_mm_chunk 構造体 zend_mm_chunk は双方向リンクリストで各チャ ンクをつなげている 最初に確保されるチャンクはメインチャンクと いう特別なチャンク プール全体の管理情報 zend_mm_heap を持つ 処理系内アロケータからは AG(mm_heap) とし てデータ領域のポインタからアクセス
  22. 想定する解法: めちゃくちゃ強引な手で見つける 一旦てきとうなリクエスト内確保の要素のメモ リアドレスを EG 経由で解決 /proc/<pid>/maps を見て当該 mmap 領域を特定

    2MB のチャンクサイズにアラインしたアドレ スを総当たりしつつ、メインチャンクに特有 の構造を持つかで同定 メインチャンクに固定オフセットにヒープ 全体を管理する zend_mm_heap 全チャンクは先頭要素にこれへのポインタ 全チャンクが通し番号を持ち、メインは 0 全チャンクは双方向リストで接続 力こそパワー
  23. チャレンジ: 循環参照 GC の対象取得 循環参照 GC の root buffer は普通にはプロセス外から見えない

    デバッグシンボル付きの処理系を使えばいけるが避けたい root buffer が取れないと特定できない領域がある 循環参照のために回収されていないどこからも参照されていない領域 メインチャンクのようなズルができない
  24. 想定する解法: 半分諦める root buffer が取れなくても EG(objects_store) に全オブジェクトの参照がある この中にある他から参照をたどれない奴らが循環参照 GC の対象かもしれない候補

    実際は拡張が持つ固有のデータ構造などが有効な参照を握っている可能性も とにかくオブジェクトの情報取得は漏れなく確実にできる 循環参照を起こし得るのはオブジェクトと配列のみ オブジェクトをカバーできるだけでかなり有効な筈
  25. 想定する解法: PHPer なら SQL ではないか RDB のテーブルをいくつか定義し SQLite などに各種情報を突っ込む 視点を変えて集計できると使い勝手よさそう

    クラスごとのメモリ使用量や参照元を絞った解析とか FUSE でファイルシステムとしてサイズ情報をマウント、とかも考えはした 参照をハードリンク扱い du や nautilus などのファイル容量集計などで集計を見れるみたいな わりと真面目に検討したが、SQL の方が PHPer らしいかなと思い直した
  26. できている部分 PHP 8.2 ターゲット前提の対応 zval のダンプ 対象プロセスの各種データのダンプ グローバル変数テーブル 定義済み関数テーブル 定義済みクラステーブル

    定義済み定数テーブル コールスタック内ローカル変数 デバッグシンボルなしでのメインチャンクの特定 サイズ情報のある程度の集計