PHPを使用してソケットベースのサーバーアプリケーションを構築する場合、 socket_accept()は、リスニングソケットからの接続を受け入れる非常にコア関数です。ただし、高い並行性環境では、開発者はsocket_accept()ブロッキング、応答の遅延、さらにはサーバーのクラッシュに遭遇することがよくあります。この記事は、基礎となるメカニズムから始まり、 Socket_accept()が高い並行性シナリオでボトルネックになる理由を詳細に分析し、PHPの実装に基づいて最適化の提案を提供します。
PHPでTCPサーバーを作成する一般的なプロセスは次のとおりです。
$socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_bind($socket, '0.0.0.0', 8080);
socket_listen($socket);
while (true) {
$client = socket_accept($socket);
// フォローアップ $client 処理の読み取りと書き込み
}
socket_accept()の目的は、リスニングソケットから握手を完了したクライアント接続を抽出し、データ通信のために新しいソケットを返すことです。
デフォルトでは、 socket_accept()がブロックしています。クライアントの接続がない場合、接続が来るまで待ちます。この方法は低い並行性シナリオでうまく動作する可能性がありますが、以下の問題は高い並行性の下で公開されます。
socket_accept()はブロックされているため、接続を待っている間に他のロジックを実行することはできません。高い並行性シナリオでは、数千の接続要求が同時に到着し、メインループで一度に1つの接続のみを処理できると仮定すると、残りの接続はキューにしかできません。この「シングルスレッド +ブロッキング」治療法は、高い並行性の下でボトルネックになる可能性があります。
TCPプロトコルは、サーバー側にバックログを維持し、3つのハンドシェイクを完了したがアプリケーションレイヤーに受け入れられていない接続を保存します。キューがいっぱいの場合、新しい接続はオペレーティングシステムによって直接拒否されます。 PHP socket_listen()の3番目のパラメーターは、バックログキューサイズであり、デフォルトでは128に過ぎない場合があります。
socket_listen($socket, 1024); // 増加 backlog サイズ
バックログが大きく設定されていても、 socket_accept()が時間内に接続を処理できない場合、キューはすぐに入力されます。
PHPのCLIモードは、本質的に同期、ブロッキング、シングルスレッド、および非同期I/Oおよび並行性機能を欠いています。これは、nginxやnode.jsなどのイベント駆動型モデルを介して複数の接続を同時に処理できないことを意味します。 PCNTL_FORK()を使用して子プロセスを作成できますが、これは多くのリソースを消費し、管理が複雑です。
さらに、PHPには組み込みの高性能イベントループメカニズム(ePoll、kqueueなど)はありません。
改善方法は、Socket_Select()と組み合わせて非ブロッキングI/O多重化を実装することです。
$clients = [];
while (true) {
$read = array_merge([$socket], $clients);
$write = $except = null;
if (socket_select($read, $write, $except, null) > 0) {
if (in_array($socket, $read)) {
$newClient = socket_accept($socket);
if ($newClient) {
$clients[] = $newClient;
socket_getpeername($newClient, $ip, $port);
echo "からの新しい接続 $ip:$port\n";
}
}
foreach ($clients as $key => $client) {
$data = socket_read($client, 1024, PHP_NORMAL_READ);
if ($data === false || trim($data) == '') {
unset($clients[$key]);
socket_close($client);
continue;
}
socket_write($client, "You said: $data");
}
}
}
この方法では、 Socket_Accept()のパフォーマンスボトルネックをある程度緩和できますが、単一の読み取りSelect()モデルに基づいています。接続が多すぎると、 Select()自体が非効率になります。 Epollなどのより効率的なモデルは、PHPによってネイティブにサポートされていません。
コードレベルの最適化に加えて、展開レイヤーでいくつかの改善を行うことができます。
リバースプロキシの使用:NginxまたはCaddyを逆プロキシサーバーとして使用して、複数のPHPインスタンスにロードバランスをクライアント接続を配布します。
PHP拡張機能またはスウールの使用:ビジネスが高い並行性のために非常に高い要件を持っている場合、コルーチンベースの非同期サービスモデルを実装するSwooleなどのPHP拡張機能を使用することを検討できます。
マルチプロセスまたはデーモンモデル: PCNTL_FORK()を使用してシンプルなプロセスプールモデルを構築し、各チャイルドプロセスは独立してSocket_Accept()を呼び出して接続を受け入れます。
Socket_accept()が高い並行性の下でボトルネックになる基本的な理由は、そのブロッキング呼び出しメカニズムとPHPシングルスレッドランニングモデルの制限です。 Socket_Select()を使用して、長期的にはSocket_Select()を使用して、ある程度の同時処理を実現できますが、非同期I/O(Swooleなど)、プロセスモデル、または外部サービスを使用してストレスを緩和する拡張機能を使用して、より実現可能なソリューションです。
並行性の高いシナリオでは、ソケットの使用はコードの最適化の問題であるだけでなく、アーキテクチャの設計とランニングモデルの包括的な課題でもあります。 PHPを選択して高い並行性長い接続サービスを構築する場合、その自然な制限とスケーラビリティを比較検討することが重要です。