PHP 8.6開発版では、非同期I/Oに関係する次の機能が追加されています。
- Polling API
stream_socket_get_crypto_status()- Stream Error Handling Improvements
これらは、アプリケーション開発者が日常的に直接呼び出す機能とは限りません。WebSocketや非同期HTTP通信では、RevoltやAMPHPなどのライブラリを使うほうが現実的です。
それでも、中級者以上が概要を知る意味はあります。
今回の変更は、単に新しい関数が増えたという話ではありません。これまでPHP本体や各拡張モジュールの内部に分散していたI/O処理を、共通の基盤へ整理していく動きだからです。
想定読者
この記事は、次のようなPHP開発者を想定しています。
- PHPのストリームやソケットを使ったことがある
- WebSocketや非同期HTTP通信に関心がある
- RevoltやAMPHPを使う側として、PHP 8.6の変更を評価したい
- イベントループを自作する予定はないが、何をしているかは理解したい
- PHP拡張や通信ライブラリの今後に関心がある
epollの詳細な実装、イベントループの自作、PHP拡張のCコードまでは扱いません。
この記事の目的は、PHP 8.6のI/O改善が何を解決し、利用するライブラリについて何を確認できるようになるのかを整理することです。
1. PHP 8.6の非同期I/O改善とは何か
今回扱う3つの機能は、それぞれ異なる判断を支えます。
| 必要な判断 | 対応する機能 |
|---|---|
| どのI/Oを待つか | Polling API |
| TLSが読み書きのどちらを待っているか | stream_socket_get_crypto_status() |
| 待てばよいのか、失敗したのか | Stream Error Handling Improvements |
非ブロッキングI/Oでは、処理を一度試して終わりではありません。
処理が完了しなかった場合、次に何を待つべきかを決め、I/Oが可能になった時点で再試行する必要があります。それでも処理できなければ、一時的な未完了なのか、回復できない失敗なのかを判定します。
PHP 8.6の3つの改善は、この判断に必要な情報をPHP本体から提供するものです。
ここでいう「ストリーム」はHTTPストリームではない
PHPのストリームは、ファイル、ネットワーク、データ圧縮など、共通の読み書き操作を持つ対象を一般化した仕組みです。PHPマニュアルでは、線形に読み書きできるresourceとして説明されています。
これは、次のような「ストリーム」とは意味が異なります。
- HTTPレスポンスを少しずつ送信するストリーミング
- SSEによるイベント配信
- AIの生成結果の逐次配信
- HTTP/2やHTTP/3の論理ストリーム
今回改善されるのは、主にHTTPより下にあるI/O層です。
HTTP・WebSocket
↓
TLS
↓
TCPソケット
↓
PHPストリーム
↓
OSのI/O待機機構
PHP 8.6にHTTPストリーミング機能が直接追加されたわけではありません。HTTPクライアント、WebSocketライブラリ、イベントループなどが利用する下位のI/O基盤が改善されています。
2. epollは以前からあったが、PHP全体では共有できなかった
Linuxで多数のソケットを監視する仕組みとして、epollがあります。
PHPがPHP 8.6で初めてepollを利用するようになったわけではありません。php-fpmには以前からepoll用のイベントモジュールがあり、epoll_create()、epoll_wait()、epoll_ctl()を使ってイベントを管理しています。
しかし、この仕組みはFPM内部のためのものでした。
従来のphp-src
FPM
└─ FPM専用のイベント機構
└─ epoll
PHPストリーム
└─ stream_select()
PHP拡張
└─ 独自実装または外部ライブラリ
イベントループ
└─ ext-event・ext-uvなど
つまり問題は、epollが存在しなかったことではありません。
高性能なI/O待機機構へ到達する共通APIが、php-src全体に存在しなかったことが問題でした。
Polling APIは、Linuxのepoll、BSDやmacOSのkqueue、Solaris系のevent ports、WindowsのWSAPoll、汎用的なpollを共通化します。従来のPHPには、PHP Coreと拡張モジュールが共有できる標準のpolling APIがなく、その解消がRFCの主要な目的として挙げられています。
PHP Core・PHP拡張・ユーザー空間
↓
共通Polling API
↓
epoll / kqueue / event ports / WSAPoll / poll
PHP拡張からも利用できる
Polling APIは、ユーザー空間のIo\Pollだけではありません。
PHP CoreとPHP拡張から利用するための内部APIもphp_poll.hとして用意されています。拡張モジュールは、ファイルディスクリプタの取得、有効性の判定、後始末などを実装した独自のpoll可能なHandleを提供できます。
これは、PHPコードからepollを選べること以上に重要です。
これまで拡張モジュールが独自に抱えていたI/O監視を、同じpolling基盤へ参加させるための契約が用意されたからです。
ただし、PHP 8.6の時点ですべての分断が解消されたわけではありません。Socketやcurl用のHandle、FPMの新しい内部APIへの移行、php-src内に存在する個別のpolling処理の統合は、RFCでも将来の範囲として挙げられています。
PHP 8.6は、統合が完了したバージョンではありません。
今後統合するための共通境界が導入されたバージョンと評価するのが適切です。
PHP版libuvではない
Polling APIを、PHP版libuvと説明するのも正確ではありません。
libuvは、OSごとのI/O監視だけでなく、タイマー、TCP・UDP、DNS、ファイル操作、子プロセス、シグナル、スレッドプールなどを含むイベントループ基盤です。ネットワークI/OではLinuxのepollなどを利用し、一部のLinuxファイル操作では明示的な設定によりio_uringも利用できます。
一方、PHP 8.6のPolling APIが対象とするのは、主にreadinessの通知です。
- 読み込み可能
- 書き込み可能
- 切断
- エラー
現在のバックエンドにはio_uringは含まれていません。io_uringのようにI/O操作そのものを投入して完了を受け取る仕組みを本格的に扱うには、バッファの寿命、キャンセル、完了結果などを含む別のAPI設計が必要です。Polling APIは、複数OSで共有しやすいI/O監視へ責務を絞っています。
3. 3つの改善はどうつながるのか
Polling APIだけでは、非ブロッキング通信は完成しません。
処理全体は、概念的には次のようになります。
I/O操作を試す
│
├─ 完了
│ └─ 結果を処理する
│
├─ まだ完了できない
│ ├─ 読み込みを待つ
│ └─ 書き込みを待つ
│
└─ 失敗
└─ エラーを上位層へ渡す
Polling APIは、処理可能なI/Oを待つ
従来のstream_select()は、OSのselect()を基礎として複数のストリームを監視します。
select()は監視対象が増えるほど処理コストが増え、一般的な環境ではファイルディスクリプタ数の制約もあります。Polling APIのRFCでは、従来方式の制約として、監視対象数に比例する処理、一般に1024程度となるFD上限、epollやkqueueへアクセスできないことが挙げられています。
epollは監視対象を継続的に登録し、発生したイベントを中心に取得します。
そのため、次のような条件で違いが大きくなります。
- 接続数が多い
- 接続を長時間維持する
- 実際に動く接続は一部だけ
- 同じ接続群を繰り返し監視する
epollを利用すれば常に高速になるという話ではありません。
通常のphp-fpmリクエストで数本の外部接続を扱うだけなら、差は限定的です。多数のアイドル接続を長時間監視する用途で、スケーラビリティの設計差が表れます。
crypto statusは、TLSが待つ方向を示す
非ブロッキングTLSでは、アプリケーションが読み込みを試したからといって、次も読み込み可能になるのを待てばよいとは限りません。
TLSプロトコルの処理中には、読み込み操作が書き込みを必要としたり、書き込み操作が読み込みを必要としたりします。
従来は、stream_socket_enable_crypto()が処理途中を示した場合や、fread()、fwrite()がデータを返せなかった場合に、OpenSSLがWANT_READとWANT_WRITEのどちらを要求しているかをPHPから判定できませんでした。
stream_socket_get_crypto_status()は、この状態を次の定数として公開します。
STREAM_CRYPTO_STATUS_NONESTREAM_CRYPTO_STATUS_WANT_READSTREAM_CRYPTO_STATUS_WANT_WRITE
これにより、イベントループや通信ライブラリは次に監視すべき方向を判断できます。
Stream Error Handlingは、失敗の意味を構造化する
非ブロッキングI/Oでは、「今は処理できない」と「処理そのものが失敗した」を区別する必要があります。
従来のPHPストリームでは、エラーの報告方法がwrapperや操作によって異なり、warning、notice、戻り値、文字列メッセージなどに分散していました。
Stream Error Handling Improvementsでは、warning、例外、silentという報告方法を選択できるほか、エラーコード、wrapper名、重大度、処理を終了させるエラーかどうかなどをStreamErrorとして取得できます。既存コードとの互換性を維持するため、デフォルトの報告方法は従来どおりです。
本質は、warningを例外へ変えられることだけではありません。
- エラーの意味
- 人間向けのメッセージ
- エラーの報告方法
この3つを分離し、ライブラリが文字列解析へ依存せず判断できることが重要です。
まとめると、それぞれの責務は次のようになります。
- Polling APIは、I/Oイベントを待つ
- crypto statusは、TLSが待つ方向を決める
- Stream Error Handlingは、失敗した理由を示す
4. WebSocketでイベントループの必要性を理解する
イベントループが必要になる代表例がWebSocketです。
通常のphp-fpmでは、リクエストを受け取り、レスポンスを返せばPHPの処理は終了します。
WebSocketでは、HTTP Upgrade後も接続を長時間維持します。
接続A:メッセージ受信待ち
接続B:送信可能になるのを待つ
接続C:TLS処理中
接続D:Pingの実行待ち
接続E:タイムアウト待ち
接続数が多くても、大部分は何も起きていない待機状態です。
たとえば1万本の接続があっても、ある瞬間にメッセージが届くのは数本だけかもしれません。このような「多数のアイドル接続と少数の活動中接続」は、epollなどのpolling機構が効果を発揮しやすい条件です。
ただし、Polling APIだけでWebSocketサーバーが完成するわけではありません。
| 処理 | 主な担当 |
|---|---|
| 読み書き可能な接続を待つ | Polling API |
| 接続と処理を対応づける | イベントループ |
| Pingやタイムアウト | タイマー機構 |
| WebSocketフレームを解析する | WebSocketライブラリ |
| 送信待ちデータを管理する | WebSocketライブラリ |
| TLSの待機方向を判定する | crypto status |
| 失敗理由を取得する | Stream Error Handling |
Polling APIは、どの接続を処理できるかを知らせます。
イベントループは、その結果を受けて、どの処理を実行するかを管理します。
多くのPHP開発者がイベントループを自作する必要はありません。それでも基本的な仕組みを学ぶ意味はあります。
問題が起きたときに、原因を次の責務へ分解できるからです。
- I/Oを正しく監視しているか
- TLSが必要とする方向を待っているか
- watcherやタイマーを解除しているか
- ブロッキング処理がループ全体を止めていないか
- 一時的な未完了と切断を区別しているか
- 送信側の処理速度を超えてデータを積んでいないか
イベントループを学ぶ目的は、自分で非同期フレームワークを作ることではありません。
WebSocketや非同期HTTPクライアントが何を代行しており、問題が起きたときにどの責務を調べるべきか判断できるようになることです。
5. PHP開発者は何を確認すればよいのか
PHP 8.6へ更新しただけで、既存のアプリケーションが自動的に高速化するわけではありません。
性能や安定性の改善につながるには、イベントループ、HTTPクライアント、WebSocketライブラリ、PHP拡張が新しいAPIを採用する必要があります。
アプリケーション開発者は、新しい低レベルAPIを直接使うより、利用しているライブラリについて次を確認することになります。
- PHP 8.6のPolling APIをバックエンドとして採用するか
- Linuxでepollを利用する条件は何か
- ext-eventやext-uvなど既存バックエンドをどう扱うか
- TLSの
WANT_READとWANT_WRITEを正しく処理するか - 構造化されたストリームエラーを上位APIへ伝えるか
- curlやSocketなど、拡張固有のHandleへ対応するか
ライブラリやPHP拡張の作者にとって重要なのは、低レベルな判断を呼び出し側へ漏らさないことです。
- どのI/Oイベントを監視するか
- TLSが次に必要とする方向は何か
- 再試行可能な状態か、終了すべき失敗か
- poll可能なリソースをどのHandleとして公開するか
これらは、アプリケーションコードではなく、イベントループや通信ライブラリが引き受けるべき判断です。
まとめ
PHP 8.6で重要なのは、epollが初めてPHPへ導入されたことではありません。
php-fpmなど個別の実装に閉じていたI/O監視を、PHP Core、PHP拡張、ユーザー空間のライブラリが共有できる境界へ引き上げたことです。
Polling API、stream_socket_get_crypto_status()、Stream Error Handling Improvementsは、それぞれ次の判断を支えます。
- 何を待つか
- 読み書きのどちらを待つか
- 待つべき状態か、失敗した状態か
PHP 8.6はイベントループを完成させたわけではありません。
イベントループとPHP拡張が共有できるI/O監視と状態判定の基盤を用意したと評価するのが適切です。