Upgrade to Pro — share decks privately, control downloads, hide ads and more …

俺の PHP プロファイラの話 PHP スクリプトで PHP 処理系のメモリをのぞき込む

俺の PHP プロファイラの話 PHP スクリプトで PHP 処理系のメモリをのぞき込む

Infiniteloop

June 06, 2024
Tweet

More Decks by Infiniteloop

Other Decks in Programming

Transcript

  1. Reli 2020 年頃から個人的に作っている PHP 製ツール 趣味でも仕事でも使ってる PHP の PHP による

    PHP のためのプロファイラ 実行中のプログラムの処理内容やメモリ内容を確認できる CLI ツール 無修正の解析対象プログラムをプロセス外から解析 https://github.com/reliforp/reli-prof
  2. というかずっと同じネタをこすり続けてる ここ数年の技術トークのけっこうな割合がこれについての話な気がする PHP Conference Fukuoka 2019: PHPの関数実行とその計測 PHPerKaigi 2022: PHP

    で PHP のプロファイラをつくろう PHPerKaigi 2023: 時間を気にせず普通にカンニングもしつつ ISUCON12 本選問題を PHP でやってみる PHP Conference Tokyo 2023: PHP で PHP のメモリプロファイラをつくろう PHPerKaigi 2024: PHP で読む楽しいコアダンプ
  3. strrev('php-profiler') → reliforp-php → Reli for P(PHP) 元は php-profiler という名前

    ライセンス問題から名前の変更が必要に 最適化のために取った処理系由来のコードが PHP ライセンス PHP ライセンスは「PHP hogehoge」のような名前を許可しない テキトーに選んだ PHP の文字列操作関数を元の名前へ適用した上で変形
  4. トレース取得 実行中のプロセスのコールトレースを取得 プロセス ID で対象を指定 今どの関数やメソッド・ファイル・行の処理を実行中か どこから呼ばれているか さらにそれがどこから呼ばれているか さらにそれがどこから呼ばれているか 「このスクリプト本当に動いてる?処理進んでそう?」を確認できる

    composer install が遅いな、ちゃんと進んでるかな、などもわかる $ sudo php ./reli i:trace -p 2182685 0 time_nanosleep <internal>:-1 1 Reli\Lib\Loop\LoopMiddleware\NanoSleepMiddleware::invoke /home/sji/work/reli/src/Lib/Loop/LoopMiddlew 2 Reli\Lib\Loop\LoopMiddleware\KeyboardCancelMiddleware::invoke /home/sji/work/reli/src/Lib/Loop/LoopMi 3 Reli\Lib\Loop\LoopMiddleware\RetryOnExceptionMiddleware::invoke /home/sji/work/reli/src/Lib/Loop/Loop 4 Reli\Lib\Loop\Loop::invoke /home/sji/work/reli/src/Lib/Loop/Loop.php:26 5 Reli\Command\Inspector\GetTraceCommand::execute /home/sji/work/reli/src/Command/Inspector/GetTraceCom 6 Symfony\Component\Console\Command\Command::run /home/sji/work/reli/vendor/symfony/console/Command/Com 7 Symfony\Component\Console\Application::doRunCommand /home/sji/work/reli/vendor/symfony/console/Applic 8 Symfony\Component\Console\Application::doRun /home/sji/work/reli/vendor/symfony/console/Application.p 9 Symfony\Component\Console\Application::run /home/sji/work/reli/vendor/symfony/console/Application.php 10 <main> /home/sji/work/reli/reli:45
  5. daemon モードでのトレース取得 プロセス ID でなくコマンドラインの正規表現パターンで対象を指定 毎秒プロセスリストを監視し、条件にマッチするプロセスを見つけるたびアタッチ 対象の プロセス ID を決め打てる場合ばかりではない、ので必要

    apache や fpm などのワーカプロセスは数リクエスト処理したら生え変わり、とかもある 「とにかく fpm のこの名前の pool のワーカ」といった指定が可能 Reli は複数のワーカプロセスを生やす 複数ターゲットから同時にトレースを取得可能 デフォルトでは 8 並列で待ち受け、設定で増やせる $ sudo php ./reli i:daemon -P "^/usr/sbin/httpd"
  6. speedscope でのコールトレースの集計・可視化 集めたコールトレースを speedscope で可視化 reli の出力を標準出力リダイレクトで集める reli の speedscope

    変換機能を使って speedscope 用の JSON に変換 フレームグラフで遅い部分が視覚的に分かる ある処理の caller と callee それぞれを集計表示 URL の振り分け処理から呼ばれてる処理を見れば遅い URL が分かる DB アクセスのような末端処理を呼ぶ処理を見れば遅いクエリが分かる
  7. リクエストごとのメモリ領域 PHP 処理系のメモリは ZendMM が管理 Zend Memory Manager ZendMM には

    2 種類のメモリ領域 リクエストごとリセットされるメモリ領域 リクエストをまたいで残る永続メモリ領域 どちらも最終的には OS からもらう仮想メモリ領 域を使う 名前の Zend は Zeev と Andi の名前から memory_get_usage() などで取れるのはリクエス トごとの領域
  8. 長時間実行プロセスでは話が別 mod_php や fpm 以外の環境で PHP を動かすシーンも増えてきた RoadRunner や Swoole

    などの AltFPM バッチ処理やジョブキューのワーカー 静的解析器など開発用の PHP ツール これらのプロセスは長時間生きたり、大量のデータを処理することが多い メモリリークが問題になることも どこにどれだけメモリを使っているか把握が必要
  9. 種類別にサイズの大きな領域ランキングを集計する例 cat memory_analyzed.json | jq .location_types_summary | jq -r '(["location_type",

    "count", "memory_usag location_type | count | memory_usage ============= | ===== | ============ ZendStringMemoryLocation | 38045 | 1992633 ZendArrayTableMemoryLocation | 538 | 991352 ZendOpArrayBodyMemoryLocation | 1323 | 921840 ZendArrayTableOverheadMemoryLocation | 416 | 370896 ZendOpArrayHeaderMemoryLocation | 1356 | 325440 ZendClassEntryMemoryLocation | 179 | 88784 ZendArgInfosMemoryLocation | 1228 | 82080 RuntimeCacheMemoryLocation | 354 | 42976 ZendObjectMemoryLocation | 273 | 40744 ZendPropertyInfoMemoryLocation | 371 | 20776
  10. インスタンスがメモリを多く食っている クラスのランキングを集計する例 cat memory_analyzed.json | jq .class_objects_summary | jq -r

    '(["class_name", "count", "memory_usage"] class_name | count | memory_usage ========== | ===== | ============ Closure | 62 | 20336 DI\\Definition\\ObjectDefinition | 12 | 2016 Fiber | 5 | 1480 DI\\Definition\\Helper\\AutowireDefinitionHelper | 10 | 1200 Revolt\\EventLoop\\Internal\\SignalCallback | 7 | 952 DI\\Definition\\Reference | 12 | 864 Amp\\Internal\\FutureState | 6 | 816 Amp\\ByteStream\\ReadableResourceStream | 3 | 696 DI\\Definition\\ObjectDefinition\\MethodInjection | 9 | 648 Monolog\\Level | 8 | 576
  11. サイズの大きな文字列を 参照元付きでランキング表示する例 cat $memory_analyzed_reli | jq '. as $root |

    path(..|objects|select(."#type"=="StringContext"))| . as $ size | node_id | path ==== | ======= | ==== 93450252 | 14396 | context.call_frames.2.local_variables.serialized_messages.referenced.array_element 45285439 | 14377 | context.call_frames.2.local_variables.content.array_elements.0.value 168289 | 59466 | context.class_table.psalm\\internal\\provider\\fileprovider.static_properties.open 72415 | 59264 | context.class_table.psalm\\internal\\provider\\fileprovider.static_properties.open 37639 | 59200 | context.class_table.psalm\\internal\\provider\\fileprovider.static_properties.open 34919 | 59160 | context.class_table.psalm\\internal\\provider\\fileprovider.static_properties.open 32192 | 58802 | context.class_table.psalm\\internal\\provider\\fileprovider.static_properties.open 26572 | 59044 | context.class_table.psalm\\internal\\provider\\fileprovider.static_properties.open 26459 | 59154 | context.class_table.psalm\\internal\\provider\\fileprovider.static_properties.open 26336 | 58754 | context.class_table.psalm\\internal\\provider\\fileprovider.static_properties.open
  12. サイズの大きな配列を参照元付きでランキング表示する例 cat memory_analyzed.json | jq '. as $root | path(..|objects|select(."#type"=="ArrayHeaderContext"))|

    . size | count | node_id | path ==== | ===== | ======= | ==== 319520 | 7937 | 42380 | context.class_table.jetbrains\\phpstormstub\\phpstormstubsmap.constants.CONS 222944 | 4919 | 27618 | context.class_table.jetbrains\\phpstormstub\\phpstormstubsmap.constants.FUNC 203616 | 3917 | 82513 | context.interned_strings 59424 | 1345 | 23577 | context.class_table.jetbrains\\phpstormstub\\phpstormstubsmap.constants.CLAS 54944 | 1205 | 19586 | context.class_table.composerautoloaderinitfa3d2d2c0bbb9c2389347803a32247de.s 1984 | 46 | 19254 | context.class_table.composerautoloaderinitfa3d2d2c0bbb9c2389347803a32247de.s 1248 | 31 | 23472 | context.class_table.composer\\autoload\\composerstaticinitfa3d2d2c0bbb9c2389 1248 | 31 | 1845 | context.call_frames.25.symbol_table.array_elements.__composer_autoload_files 1216 | 30 | 1754 | context.call_frames.25.symbol_table.array_elements._SERVER.value 800 | 17 | 620 | context.call_frames.7.local_variables.container.object_properties.fetchedDef
  13. コールトレースの取得 cat memory_analyzed.json | jq -r '(["frame_no", "function", "line"] |

    (., map(length*"="))),(path(.cont frame_no function line ======== ======== ==== 0 system 4 1 {closure}(/home/sji/work/oss/tmp/pdfparser_test/test.php:11-21) 20 2 Smalot\\PdfParser\\Font::uchr 150 3 Smalot\\PdfParser\\Font::loadTranslateTable 230 4 Smalot\\PdfParser\\Font::init 78 5 Smalot\\PdfParser\\Document::init 90 6 Smalot\\PdfParser\\Document::setObjects 316 7 Smalot\\PdfParser\\Parser::parseContent 122 8 Smalot\\PdfParser\\Parser::parseFile 90 9 <main>
  14. 特定のコールフレームの 特定のローカル変数の値を出力する例 cat memory_analyzed.json | jq '.context.call_frames."1".local_variables|{abc, def}' { "abc":

    { "#node_id": 142, "#type": "ScalarValueContext", "#value": 506895433343375 }, "def": { "#node_id": 143, "#type": "ScalarValueContext", "#value": 9958150 } }
  15. コアダンプからの読み取り Reli のメモリ解析機能はコアダンプの読み取りにも対応 プロセス死亡時の PHP レベルでの状況を詳しく調べられる gdb も処理系のデバッグシンボルもいらない 生きてるプロセスも gcore

    でごく短い間止めるだけで解析できる コアダンプ取得の時間しか要らない 生きてるプロセスの直接のメモリ解析は時間がかかり、プロダクションで使い辛い コアだけ吐いておけば後でじっくり根本原因を(現実的な手間の範囲で)追える トラブル対応でプロセスの再起動など急場しのぎの対応が要る場合も状況を保存できる その場にいない詳しい人が後で調べる、も可能
  16. ざっくりの概観 対象プロセスの重要構造体のアドレスを特定 処理系の ELF バイナリと procfs のメモリマップを解析 FFI でシステムコールを呼び別プロセスの処理系内のメモリを読む gdb

    などのデバッガと同じようなことをやる 処理系内部のメモリレイアウトの知識を持って内部情報を解釈 FFI で処理系内の C 構造体定義を読み込む ポインタをたどり実行中の関数・VM命令のトレースやメモリ内容を収集