0Pricing
PHP Academy · レッスン

Swooleによる高性能サーバー

コルーチンベースのSwooleで数千の接続に対応します。

「Swooleによる高性能サーバー」はCoddyKit上の無料PHP Academyレッスンです。 これはレッスン4/4です。 下記で完全なレッスンを無料で読むことができます。その後、ブラウザ内の組み込みコードエディタと24時間対応のAIチューターでハンズオン演習できます。 これはPHP Academy学習パスの一部であり、ウェブとCoddyKitアプリ全体で進捗が同期されます。 PHP Academyコースには全4レッスンが含まれています。

Swooleとは

Swoole(およびそのフォークであるOpenSwoole)は、PHPを高性能なコルーチンベースのイベント駆動ランタイムに変えるC拡張機能です。PHP-FPMがリクエストごとにプロセスを生成する代わりに、Swooleサーバーはアプリケーションを一度だけ起動し、組み込みコルーチンを備えた長時間稼働するワーカーのプールをメモリ上に保持して、そこからリクエストを処理します。その結果、スループットが桁違いに向上し、1回のリクエスト内でネイティブな並行処理を実現できます。

永続HTTPサーバー

典型的な例は、スクリプト内に完全なHTTPサーバーを構築することです。サーバーは接続を待ち受け、各リクエストをワーカーに振り分け、フレームワークを再度ブートストラップすることなく動作し続けます。なお、これはext-swooleが必要であり、標準のPHP CLIでは実行できません。

<?php
$server = new Swoole\Http\Server('0.0.0.0', 9501);
$server->set(['worker_num' => 4]);

$server->on('request', function ($request, $response) {
    $response->header('Content-Type', 'application/json');
    $response->end(json_encode(['hello' => $request->server['request_uri']]));
});

echo "listening on :9501\n";
$server->start();

コールバックではなくコルーチン

Swooleの最大の特徴はコルーチンです。ブロッキング処理のように順番に実行するコードを記述し、ランタイムが協調的にスケジュールします。Coroutine\run()はコルーチンコンテキストを開始し、go()は新しいコルーチンを起動します。各コルーチンはFiberのように独自のスタックを持ちますが、SwooleはI/Oの発生箇所で自動的にスケジュールします。

<?php
use Swoole\Coroutine as Co;

Co\run(function () {
    go(function () { Co::sleep(1); echo "A done\n"; });
    go(function () { Co::sleep(1); echo "B done\n"; });
    // Both sleep concurrently -> total ~1s, not 2s
});

ブロッキング関数のフック

コルーチンを使いやすくする仕組みがランタイムフックです。Swooleはブロッキング呼び出しであるPDO、Redis、curl、sleep、file_get_contentsを、コルーチンに対応したノンブロッキング版へ透過的に書き換えます。通常の$pdo->query()を実行しても、ワーカーを停止させる代わりにコルーチンが一時停止します。

<?php
use Swoole\Runtime;
use Swoole\Coroutine as Co;

Runtime::enableCoroutine(SWOOLE_HOOK_ALL); // hook PDO, curl, sleep, etc.

Co\run(function () {
    go(function () { sleep(1); echo "task 1\n"; }); // sleep() is now async!
    go(function () { sleep(1); echo "task 2\n"; });
}); // finishes in ~1s because sleep was hooked

WaitGroupによる並行I/O

処理を分散して合流させるために、SwooleはGoのsync.WaitGroupに似たWaitGroupを提供します。コルーチンを起動し、タスクごとにadd()を呼び出し、完了時にdone()を呼び出してから、すべてが並行して完了するまでwait()で待機します。

<?php
use Swoole\Coroutine as Co;
use Swoole\Coroutine\WaitGroup;

Co\run(function () {
    $wg = new WaitGroup();
    $results = [];
    foreach (['eu', 'us', 'asia'] as $region) {
        $wg->add();
        go(function () use ($wg, $region, &$results) {
            Co::sleep(0.5);              // simulated API call
            $results[$region] = "ok-$region";
            $wg->done();
        });
    }
    $wg->wait();                         // all three ran in parallel (~0.5s)
    var_dump($results);
});

通信用チャネル

コルーチンはチャネルを介して連携します。チャネルは型付きで容量制限のあるキューで、満杯になるとプロデューサーを、空になるとコンシューマーを一時停止します。これはCSPスタイルのメッセージングであり、共有状態のロックを完全に避けられます。

<?php
use Swoole\Coroutine as Co;
use Swoole\Coroutine\Channel;

Co\run(function () {
    $chan = new Channel(2);
    go(function () use ($chan) {
        foreach (range(1, 3) as $n) $chan->push($n);
        $chan->push(null); // sentinel
    });
    go(function () use ($chan) {
        while (($v = $chan->pop()) !== null) echo "got $v\n";
    });
});

コネクションプーリング

永続サーバーでは、リクエストごとにDB接続を開くべきではありません。Swooleはコルーチンから安全に利用できるコネクションプールを提供します。これにより、ワーカーは固定数の接続を数千の並行コルーチン間で再利用でき、接続の頻繁な生成と破棄をなくせます。

<?php
use Swoole\Database\PDOConfig;
use Swoole\Database\PDOPool;
use Swoole\Coroutine as Co;

Co\run(function () {
    $pool = new PDOPool((new PDOConfig())
        ->withHost('127.0.0.1')->withDbname('app')
        ->withUsername('u')->withPassword('p'), 8);

    go(function () use ($pool) {
        $pdo = $pool->get();           // borrow
        $row = $pdo->query('SELECT NOW()')->fetch();
        $pool->put($pdo);              // return to pool
        var_dump($row);
    });
});

状態リーク:永続メモリの落とし穴

プロセスがリクエストをまたいで存続するため、グローバル状態とstatic状態が保持されます。無制限に増え続けるstaticキャッシュ、リクエスト固有のデータを保持するシングルトン、または解放されないDBトランザクションは、後続のリクエストを壊し、メモリを使い果たす可能性があります。ルールは次のとおりです。staticやシングルトンにリクエストスコープのデータを置かないこと、リクエストごとのコンテキストをリセットすること、そしてmax_requestを設定してワーカーを定期的に再起動することです。

<?php
$server = new Swoole\Http\Server('0.0.0.0', 9501);
$server->set([
    'worker_num'  => 8,
    'max_request' => 10000, // recycle a worker after N requests to bound leaks
]);
// Never store $request data in a static property between requests!

コルーチンローカルコンテキスト

1つのワーカーが多数のコルーチンを交互に実行するため、リクエストごとのデータに通常のstaticを使うことはできません。Swooleは各コルーチンにコンテキスト(Coroutine::getContext())を提供します。これはコルーチンの存続期間だけ存在するオブジェクトで、現在のユーザー、リクエストID、トランザクションハンドルなどを保持するのに最適です。

<?php
use Swoole\Coroutine as Co;

Co\run(function () {
    go(function () {
        $ctx = Co::getContext();
        $ctx['request_id'] = 'req-123';
        // ... deep call chain can read Co::getContext()['request_id']
        echo Co::getContext()['request_id'], PHP_EOL;
    });
});

プロセスモデルとタスクワーカー

Swooleは、masterプロセス(イベントループスレッドを管理)、manager(ワーカーを生成・監視)、workerプロセス(リクエストハンドラーとコルーチンを実行)、そして必要に応じてtask worker($server->task()で委譲された負荷の高い処理、ブロッキング処理、CPUバウンドのジョブを実行)を動かします。画像処理や長時間のCPU処理にはタスクワーカーを使い、リクエスト用コルーチンを停止させないようにします。これにより、並行性と並列性の両方を実現できます。

Deferとタイマー

Swooleでは、Coroutine::defer()を使って現在のコルーチンの終了後に処理をスケジュールできます。例外が発生した場合でもプールされたリソースを確実に解放できるため、最適です。サーバー全体のタイマー(Timer::tick)を使えば、外部のcronなしで、キャッシュ更新などの定期処理をワーカー内で実行できます。

<?php
use Swoole\Coroutine as Co;

Co\run(function () {
    go(function () {
        $conn = 'borrowed-connection';
        Co::defer(function () use ($conn) {
            echo "released $conn\n"; // runs when coroutine exits, even on error
        });
        echo "using $conn\n";
    });
});

確認問題

Swooleサーバーでリクエストデータをstaticプロパティに保存してはいけないのはなぜですか。

まとめ

SwooleはPHPを常駐型のコルーチン駆動サーバーに変えます。

  • アプリケーションは一度だけ起動し、ワーカープールが組み込みのコルーチン(Co\run/go())でリクエストを処理します。
  • ランタイムフックにより、通常のPDO、curl、sleepがノンブロッキングになります。WaitGroupとチャネルで並行処理を連携できます。
  • コネクションプールにより、コルーチン間でDB接続を再利用できます。
  • メモリが永続するため、状態リークを避ける必要があります。リクエストごとのデータにはCoroutine::getContext()を使い、max_requestを設定し、負荷の高い処理はタスクワーカーに委譲します。

よくある質問

「Swooleによる高性能サーバー」レッスンは無料ですか?

はい。「Swooleによる高性能サーバー」の完全なテキストはこのウェブで無料で読めます。インタラクティブに演習し(組み込みコードエディタと24時間対応のAIチューター)、PHP Academyコースの残りをアンロックするには、CoddyKit PROにアップグレードしてください。 PHP Academyコースには全4レッスンが含まれています。

「Swooleによる高性能サーバー」で何を学びますか?

コルーチンベースのSwooleで数千の接続に対応します。 ブラウザで直接実行するハンズオンコードでPHP Academyを演習し、24時間対応のAIチューターがレッスンを進める中での質問に答えます。

PHP Academyを始めるのに経験は必要ですか?

事前経験は必要ありません。CoddyKitのPHP Academyは初級者から上級者向けに構成されているため、ここから始めるか最初から始めて、自分のペースで進むことができます。 これはレッスン4/4です。

「Swooleによる高性能サーバー」レッスンにはどのくらい時間がかかりますか?

ほとんどのCoddyKitレッスンは約5~10分かかります。各レッスンはコンパクトでインタラクティブなので、着実に進歩し、ウェブとアプリ全体で正確に前回の場所から再開できます。

このPHP Academyレッスンでコードを書いて実行できますか?

はい。すべてのPHP Academyレッスンに組み込みコードエディタが含まれているため、ブラウザでリアルコードを書いて実行し、即座のAIフィードバックを取得できます。ローカル設定は不要です。

このコースのすべてのレッスン

  1. PHPの並行実行モデル
  2. PHP 8.1 Fibers
  3. ReactPHPによるイベントループ
  4. Swooleによる高性能サーバー
← PHP Academyに戻る