Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
PHP で PHP のメモリプロファイラをつくろう
Search
sji
October 08, 2023
Programming
0
1.6k
PHP で PHP のメモリプロファイラをつくろう
PHP カンファレンス 2023
10:50 〜
Track2
sji
October 08, 2023
Tweet
Share
More Decks by sji
See All by sji
PHP で読む楽しいコアダンプ
sji
0
530
PHP で作られたゲテモノを色々紹介する(自作他作含めて)
sji
0
720
時間を気にせず普通にカンニングもしつつ ISUCON12 本選問題を PHP でやってみる
sji
0
4.5k
導⼊から 10 年、PHP の trait は滅びるべきなのか その適切な使いどころと弱点、将来について
sji
0
630
PHP で PHP のプロファイラをつくろう
sji
1
1.1k
モダン PHP テクニック 12 選 ―PsalmとPHP 8.1で今はこんなこともできる!―
sji
0
440
PHP 8 と V8 (JavaScript) で速さを見比べてみよう!
sji
0
140
PHP でファイルシステムを作ろう
sji
0
100
PHP 8 で WEB 以外の世界の扉 を叩く
sji
0
78
Other Decks in Programming
See All in Programming
PHP でアセンブリ言語のように書く技術
memory1994
PRO
1
170
リアーキテクチャxDDD 1年間の取り組みと進化
hsawaji
1
220
ActiveSupport::Notifications supporting instrumentation of Rails apps with OpenTelemetry
ymtdzzz
1
230
LLM生成文章の精度評価自動化とプロンプトチューニングの効率化について
layerx
PRO
2
190
TypeScriptでライブラリとの依存を限定的にする方法
tutinoko
2
660
Hotwire or React? ~アフタートーク・本編に含めなかった話~ / Hotwire or React? after talk
harunatsujita
1
120
Kaigi on Rails 2024 〜運営の裏側〜
krpk1900
1
190
Pinia Colada が実現するスマートな非同期処理
naokihaba
4
220
最新TCAキャッチアップ
0si43
0
140
Creating a Free Video Ad Network on the Edge
mizoguchicoji
0
110
C++でシェーダを書く
fadis
6
4.1k
Content Security Policy入門 セキュリティ設定と 違反レポートのはじめ方 / Introduction to Content Security Policy Getting Started with Security Configuration and Violation Reporting
uskey512
1
520
Featured
See All Featured
How GitHub (no longer) Works
holman
310
140k
The Invisible Side of Design
smashingmag
298
50k
Cheating the UX When There Is Nothing More to Optimize - PixelPioneers
stephaniewalter
280
13k
Save Time (by Creating Custom Rails Generators)
garrettdimon
PRO
27
840
Navigating Team Friction
lara
183
14k
Practical Tips for Bootstrapping Information Extraction Pipelines
honnibal
PRO
10
720
Responsive Adventures: Dirty Tricks From The Dark Corners of Front-End
smashingmag
250
21k
Fireside Chat
paigeccino
34
3k
Code Reviewing Like a Champion
maltzj
520
39k
Why Our Code Smells
bkeepers
PRO
334
57k
Easily Structure & Communicate Ideas using Wireframe
afnizarnur
191
16k
Side Projects
sachag
452
42k
Transcript
PHP で PHP のメモリプロファイラを作ろう 五十嵐 進士 / sji / sj-i
/ @sji_ch
自己紹介 @sji_ch SNS 上のアイコンは GitHub が自動生成した奴
生まれも育ちも仙台
PHP カンファレンス仙台とかやった
ふつうのサラリーマン 株式会社インフィニットループ仙台支社所属 スマホゲーのサーバサイドプログラマ 地元が仙台や札幌の人とかはぜひ一緒に働きま しょう
Agenda メモリとはなんぞや(約 10 分) PHP のメモリ管理機構について(約 10 分) PHP のメモリ消費量の計測方法(約
10 分) PHP のメモリプロファイラを自作する取り組み(約 15 分)
メモリの概要
メモリはコンピュータの部品 コンピュータは様々な部品から構成 メモリはその中の一つ
コンピュータは数値で動く 2 つの状態を持つものの並び = 信号 = 数値 電気の強弱 磁性体の向き 0
と 1 この信号パターンならこう動く、というのが機械への命令 命令を動作の種類と内容を分けて構成すれば様々な情報が数値に
CPU は鳥頭 CPU が命令を処理 CPU 自体では多くの命令を覚えられない CPU が命令を読み取るための装置が必要
メモリが数値の並び(= 情報)を大量に記憶 メモリが CPU のために情報を記憶 CPU はメモリから情報を読んでメモリに情報を書き込む
扱うデータの単位 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 メガバイト
メモリはバイト単位のデータの並び メモリはバイトの情報をおさめた箱の並び 一つ一つの箱に背番号 = アドレス、番地 ある番地や番地の範囲をメモリ領域と呼ぶ
メモリは限られた資源 メモリは CPU に大量に読み書きされる メモリが遅いほど CPU が待たされることに メモリは比較的高速に動作するが、高価で大容量化が大変 ついでに通電しなくなると記録していた情報が消える ストレージと組み合わせてやりくり
遅くても安く大容量化でき、通電しなくても情報が消えない
メモリは OS が管理 コンピュータ上では複数プログラムが同時に動く マルチプロセス OS がプロセス間でハードウェア資源の利用を仲介 メモリも OS が管理
プロセスは OS にメモリを要求 OS はプロセス間を仮想メモリ空間で隔離しながらメモリを割り当て プロセスは自分だけのメモリ領域を使っているように見える ストレージとも組み合わせてやりくり(スワップ)
それでも無い袖は振れない ストレージはメモリと比べて圧倒的に遅い スワップが発生すると CPU が全然パワーを発揮できない 設定でスワップを切ったり制限した状態で動かすこともよくある メモリが本当に足りなくなると OOM Killer にプロセスを殺されたりする
各プログラムでなるべく無断のないメモリの使い方が必要
PHP のメモリ管理機構: memory_limit あたり
Allowed memory size of 134217728 bytes exhausted (tried to allocate
4096 bytes)
memory_limit とは リクエストごとのメモリ上限を指定する ini 項目 安全装置 PHP はマルチプロセスでリクエストを処理 マシンリソースを食いつぶさないよう制限できる リクエストの処理が
memory_limit を越えたらプロセスが自刃する 最近のデフォルト値は 128MB プロジェクトや利用箇所によって違う設定にしてるはず
ZendMemoryManager PHP 処理系のメモリ管理部品 2 種類のメモリ領域 リクエストごとのメモリ領域 処理系や C 拡張は libc
の malloc や free を パクった emalloc や efree などの API で操作 永続メモリ領域 通常 libc の malloc とか free が使われる どちらも最終的には OS からもらう 仮想メモリ領域を使う 名前のZend はZeev とAndi の名前から
基本はリクエストごとのメモリ領域に入る PHP スクリプトのメモリ状態は通常リクエストごとにリセット 大部分のデータをリクエストごとのメモリ領域で管理 リークの心配があまりなくなる memory_limit での制限対象はこっちのメモリ領域 memory_get_usage() などでとる情報もこの部分
memory_limit の制限の対象外 永続メモリ領域や Zend Memory Manager 管理外の領域 は memory_limit の対象外
たとえば Xdebug が動作に使うメモリ phpdbg だと Zend Memory Manager 経由で似た情報を持ったり どちらでカバレッジをとるかで PHPUnit でのメモリ使用量報告が大きく変わる opcache が SHM として共有メモリ上に管理するのも別 今回のトークではこの辺の話はしない
PHP のメモリ管理機構: リクエスト内でのメモリの確保と解放
メモリの確保は使うときに処理系がこっそり スクリプト内で「メモリ領域を確保」のような関数は(基本的に)ない PHP スクリプトでメモリが必要になったら裏で処理系が ZMM で確保 変数を使うとか 配列の要素を追加するとか オブジェクトを new
するとか スクリプト内のどの部分がどのくらいメモリを必要とするかは意識せず使える 逆に言うとふつうには意識することができない memory_get_usage() を至るところに差し込めば近づけはする
リクエスト内で不要になったメモリ領域は? 単純な値を持つローカル変数は VM スタックで管理 整数値や浮動小数点数、bool 値など 関数が終了し次第そのまま破棄してよい オブジェクトや配列、文字列などは参照カウントで順次解放 スクリプト内で使われなくなった領域から解放される 循環参照は参照カウントでは破棄できない
M&S っぽく可能性ある奴を一通りなめる循環参照 GC がある https://www.php.net/manual/ja/features.gc.collecting-cycles.php
前提: zval について PHP の値は zval という 128bit (= 16byte)
の構造体で表現 zval は値の種類を表す型情報と、値そのものを持つ 値そのものは union で表現される 64bit (= 8byte )分 long, double, true, false, string, array, object, null などなんでも入る string 、array 、object 、resource の zval は詳細データ構造へのポインタが値 処理系で内部的に使う一部の特別な値でも利用 クラスの定義情報を名前から引くための辞書など
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'; /* 今ここを実行中とする *
参照カウント オブジェクトや配列、文字列などの実体は参照カウントを持つ 今 N 箇所で使われています、の N 関数へ渡したり関数から返したり異なる変数へ代入したりするたび 1 増える 参照が消えるたび
1 減る ローカル変数が unset() されたり スコープを抜けてスタックから破棄されたり カウント 0 になったら解放
循環参照 GC 配列とオブジェクトでは循環参照が発生し得る 参照カウントでは循環参照を解放できない 参照カウントを減らした際 0 にならない配列やオブジェクトがあれば容疑者入り 処理系は疑わしいものを root buffer
と呼ばれるバッファへすべて記録 参照カウントが 0 になるなどして破棄される際は root buffer から除去 root buffer がある程度たまって閾値を超えると循環参照 GC を実行 細かい話は y-uti 先生の『PHP の GC の話』を見よう https://www.slideshare.net/y-uti/php-gc
循環参照 GC の利用上の注意点 循環参照 GC のトリガは root buffer の埋まり具合 埋まり具合が閾値を超えないと循環参照
GC は実行されない つまり memory_limit を超えても循環参照 GC は実行されない 手動で良いタイミングに gc_collect_cycle() を呼ぶだけで memory_limit 越えを避けられる場合も
リクエスト終了まで解放されない領域 リクエスト終了まで解放されない領域もある グローバル変数 関数内静的変数 クラス static ・クラス定数 定数 コンパイルされたファイルや関数・クラスの情報 コンパイラが使うための作業用領域
不意にスクリプトが終了した場合の処理 リクエストの処理中に不意にスクリプトが終了した場合は? exit() とか リクエストごとのメモリ領域そのものが解放されるので基本は大丈夫 オブジェクトがデストラクタで永続領域の何かを解放したがっているかも EG(objects_store) に全オブジェクトの参照があり、シャットダウン処理で順次破棄 PHP マニュアルのデストラクタのページにも少し書いてある
https://www.php.net/manual/ja/language.oop5.decon.php#language.oop5.decon.destructor あるいは、スクリプトの終了時にも順不同でコールされます
何がリクエストの中でメモリを使っているか、 駆け足でまとめる スクリプトコンパイル時の作業用領域 コンパイルされた VM 命令列 グローバル変数用の zval 領域 文字列、配列、オブジェクトの実体
定数用の zval 領域 クラスや関数の静的領域 VM スタック 処理系内の各種管理データ 定数や関数・クラスの各種定義情報とか EG(objects_store ) とか などなど
PHP のメモリ使用量の計測事情
計測の必要性: メモリ使用量はサーバ性能につながり得る Web システムの多くは I/O バウンドなので CPU が遊びがち サーバ資源を有効活用するのに PHP
ワーカを増やしたい CPU が余っていても各ワーカプロセスがメモリを食いすぎると並列度を上げづらい 実質的に可処分メモリ / ワーカの消費メモリが並列度の限界になる
計測の必要性: PHP ツールはメモリを食いがち Web 以外のシーンでも最近の PHP は汎用言語としてわりと使える GB 単位でメモリを食う静的解析ツールの改善などに糸口が見つけられると嬉しい
計測の必要性: long running への安心感 リクエストごとに状態をリセットしない AltFPM が成熟してきた roadrunner / swoole
など PHP ワーカがリクエストをまたいで状態を保持し続ける利用シーンも今後増えていくかも 何がどこでメモリを使っているか分からないのは可観測性に問題
計測の必要性: 当てずっぽうでは当たらない 誰が言ったか「推測するな、計測せよ」 処理時間の場合と同様、メモリ使用量もボトル ネックが生まれがち ごく一部の原因が多くのメモリを消費する ある程度以上の規模のシステムで改善点を突き 止めるのは当てずっぽうでは難しい 砂漠でゴマ粒を、haystack で
needle を探すよ うなもの
計測の必要性はあるが既存の方法は限られている memory_get_usage() / memory_get_peak_usage() xdebug / tideways_xhprof php-memprof php-meminfo
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
memory_get_usage() / memory_get_peak_usage() の問題点1 問題になるケースは何らかの理由でメモリが解放されていない いつ確保されたメモリがいつ解放されているか 何が解放されるべき時に解放されていないのか 使用量の集計だけでは分からない ごく一部のケースにアテがつけられるだけ ある処理で一気に確保されたメモリが、その後の処理で解放されている場合もある
どこで参照カウントが 0 になるかは簡単には分からない場合が多い 適切に解放されていれば別に何も問題がなかったりする
memory_get_usage() / memory_get_peak_usage() の問題点2 そもそも仕込むのが面倒くさい 面倒くさい上に問題点 1 のためにリターンが大したことない 計測対象のソースコードへ大きな改変が必要な手段は選びづらい
xdebug / xhprof 処理時間を計測するプロファイラ機能を持つ おまけ機能で関数の出入りの際にメモリ使用量・ピーク使用量を自動で記録できる PECL の xhprof は最近メンテナが変わって復活 tideways_xhprof
は役割を終えたとして今月(10/6 )アーカイブされた 本質的には memory_get_usage() / memory_get_peak_usage() 方式と同じ 減らない理由が分からない、何が減っていないのかが分からない https://xdebug.org/ https://github.com/longxinH/xhprof
php-memprof 現状での良い選択肢の一つ PCEL の C 拡張 スクリプトの全関数実行をフックして今どこを 実行しているか、を追跡 zend_mm_set_custom_handlers() で
ZendMM の メモリ確保・解放処理をフック emalloc() や efree() の実装を差し替え どこで確保されたメモリがどこで解放された か、解放されていないのはどこか、を関数実 行単位で追跡可能 memory_limit 超過時に自動出力する機能も https://github.com/arnaud-lb/php-memory- profiler
php-memprof の弱点 全関数実行と全メモリ確保・解放処理のフックにはオーバーヘッドがある 計測用に本来とは異なる処理が行われ、厳密には挙動が変わる 本番環境での問題特定には不向き 「どの関数の」確保が解放されていないか、までは分かるが、何の領域かまでは不明 処理系への C 拡張のインストールが必要
php-meminfo PECL にはないが C 拡張 meminfo_dump() を提供 呼び出し時点のスクリプト内のコールスタッ クの変数情報を JSON
としてダンプする ダンプ内容は PHP スクリプトで解析・集計可能 「何が」メモリ食いかをかなり絞り込める https://github.com/BitOne/php-meminfo
php-meminfo の弱点 2022 年 3 月の CI 設定の修正を最後に更新されていない 循環参照には対応していない 定数や関数の静的変数には対応していない
処理系への C 拡張のインストールが必要 スクリプト側を修正してのダンプ出力が必要
PHP で PHP のメモリプロファイラを作ろう
前提: 旧作・reli PHP の PHP による PHP のためのプロファイラ 前から PHP
スクリプトの処理時間を計測するプロファイラ reli を PHP で作ってる 処理系の ELF バイナリと procfs のメモリマップを解析 外部プロセスの処理系の重要構造体の仮想アドレスを特定 FFI でシステムコールを呼び別プロセスの処理系内のメモリを読む gdb などのデバッガと同じようなことをやる 処理系内部のメモリレイアウトの知識を持って内部情報を解釈 実行中の関数・VM 命令のコールトレースをサンプリングで取得 よく取れるものがボトルネック https://github.com/reliforp/reli-prof
処理時間以外の情報も取れるのでは? 元ネタの phpspy ではその時点のメモリ使用量・最大使用量を取得できる これは結局 php-memprof 以外と同じ問題を持つ 元ネタの phpspy は指定したファイル・行の
ローカル・グローバル変数の値の監視などもできる 似たことをもっと大規模にやったら?
EG から読めば読めそうな情報 グローバル変数全部入りテーブル 定義済み関数全部入りテーブル 定義済みクラス全部入りテーブル 定義済み定数全部入りテーブル コールスタック内ローカル変数 わりと全部取れるのでは?
チャレンジ: 稼働中システムへの利用可能性 データの取得中はさすがに対象プログラムを停止させたい 実行中の VM 状態変化の影響は単なるコールトレース以上の筈、読む箇所も多い 止めなければ対象の状態を取得している間に状態が変わってしまう FFI 経由で ptrace
を呼べば止められる しかし長く止めてしまうと本番環境などで使い辛くなる 解析処理には時間がかかる
想定する解法: メモリプールのまるごとコピー 処理系のメモリプールをほぼ解析なしで一気にコピーして即座に停止解除 リクエストごとのプールと永続領域と両方 コピー後のデータでじっくり解析 力こそパワー
プロセス外からのコピー速度 いまどきの DDR4 や DDR5 の速度ならわりといける? process_vm_readv で別プロセスから 1GB 分のメモリをコピってくるのに家のマシンで
234 ミリ秒 システムコールの処理としてのオーバーヘッドがまあまあある 素朴な memcpy などとは雲泥 とはいえよくある memory_limit はこの 1/4 や 1/8 以下 遅いことは遅いが許容できないほどでもない 本当に困ったら対象へ C 拡張でも突っ込めば memcpy の速度で共有メモリ域へコピー可能
チャレンジ: メモリプールの見つけ方問題 外部プロセスのメモリプールのアドレスは自明でない AG (Allocator Globals) は公開シンボルではないので普通にはたどれない デバッグシンボル付の処理系なら取れるが避けたい、使う側がめんどくさいから
リクエストごとのメモリプールの構造 ZendMM の管理メモリは 2MB ごとのチャンク 各チャンク内部で更に 4KB ごとのページへ分割 emalloc は各チャンクを更に小分けにしたり複数
ページまとめたりした領域を返す 各チャンク領域先頭は zend_mm_chunk 構造体 zend_mm_chunk は双方向リンクリストで各チャ ンクをつなげている 最初に確保されるチャンクはメインチャンクと いう特別なチャンク プール全体の管理情報 zend_mm_heap を持つ 処理系内アロケータからは AG(mm_heap) とし てデータ領域のポインタからアクセス
想定する解法: めちゃくちゃ強引な手で見つける 一旦てきとうなリクエスト内確保の要素のメモ リアドレスを EG 経由で解決 /proc/<pid>/maps を見て当該 mmap 領域を特定
2MB のチャンクサイズにアラインしたアドレ スを総当たりしつつ、メインチャンクに特有 の構造を持つかで同定 メインチャンクに固定オフセットにヒープ 全体を管理する zend_mm_heap 全チャンクは先頭要素にこれへのポインタ 全チャンクが通し番号を持ち、メインは 0 全チャンクは双方向リストで接続 力こそパワー
チャレンジ: 循環参照 GC の対象取得 循環参照 GC の root buffer は普通にはプロセス外から見えない
デバッグシンボル付きの処理系を使えばいけるが避けたい root buffer が取れないと特定できない領域がある 循環参照のために回収されていないどこからも参照されていない領域 メインチャンクのようなズルができない
想定する解法: 半分諦める root buffer が取れなくても EG(objects_store) に全オブジェクトの参照がある この中にある他から参照をたどれない奴らが循環参照 GC の対象かもしれない候補
実際は拡張が持つ固有のデータ構造などが有効な参照を握っている可能性も とにかくオブジェクトの情報取得は漏れなく確実にできる 循環参照を起こし得るのはオブジェクトと配列のみ オブジェクトをカバーできるだけでかなり有効な筈
チャレンジ: どのタイミングで取得するか php-memprof ならスクリプト終了時にプロファイル結果を吐ける memory_limit 越え時の自動出力も可能 外部プロセスからのサンプリング方式ではプロセスを止めるタイミングを選びきれない 止めた瞬間が対象プロセスにとってどういうタイミングかが分からない リクエストの開始直後でメモリ状態に何の問題もないタイミングかもしれない メモリ使用量が一旦膨れ上がった後ある程度回収されて平穏なタイミングかもしれない
想定する解法: 色々できるようにしてみる memory_get_usage() 相当の情報は zend_mm_heap.size 経由で取れる サンプリングで閾値を越えた場合にメモリ領域のダンプを取得、といった対応もできる 対象プロセスに手を入れる解法を許容し、自らメモリダンプの取得を依頼できるようにする手も
チャレンジ: どのように結果を出力するか php-memprof なら実質的に関数実行の性能計測と同様の可視化が可能 KCachegrind などで関数呼び出しのツリーを表示 こちらもプロセス全体を根として各種情報をぶら下げたツリーとして扱うことは可能だが…… 複数の箇所から参照されるデータをどう扱えばよいか
想定する解法: PHPer なら SQL ではないか RDB のテーブルをいくつか定義し SQLite などに各種情報を突っ込む 視点を変えて集計できると使い勝手よさそう
クラスごとのメモリ使用量や参照元を絞った解析とか FUSE でファイルシステムとしてサイズ情報をマウント、とかも考えはした 参照をハードリンク扱い du や nautilus などのファイル容量集計などで集計を見れるみたいな わりと真面目に検討したが、SQL の方が PHPer らしいかなと思い直した
実際にある程度作ってみた PoC について まだ見栄えのする出力がない ←のでしょうがなくJSON ダンプ (たぶん会場でよく見えない) https://github.com/reliforp/reli-prof/pull/294
できている部分 PHP 8.2 ターゲット前提の対応 zval のダンプ 対象プロセスの各種データのダンプ グローバル変数テーブル 定義済み関数テーブル 定義済みクラステーブル
定義済み定数テーブル コールスタック内ローカル変数 デバッグシンボルなしでのメインチャンクの特定 サイズ情報のある程度の集計
まだの部分 丸っとコピーしたメモリダンプからのゆっくり解析 EG(objects_store) の対応 取得タイミングのがんばり RDB への処理結果の突っ込み Fiber 対応 参照カウントの取得・循環参照の検知
FFI など各拡張固有のデータ構造の対応 複数バージョンの PHP 対応 ZTS 対応
うまいこといくと手に入りそうなツール 対象プログラム無修正で使える プロセス外からスクリプト内のほぼ全ての状態を取得し SQL でクエリ可能とできる 集計によりメモリリークやメモリボトルネックを特定できる ちょっとしたデバッガのかわりにも使える 方向性は php-meminfo に近いが、PHP
製で PHPer が修正可能
まとめ
メモリは限られた大事な資源
PHP の実行時情報は基本リクエストごとのメモリ領域に
php-memprof は 現在あるメモリ使用量計測の選択肢では有力
根性があれば PHP でも力業により メモリプロファイラは作れる(たぶん)
おしまい