1

システム

(ビジネス)クライアント向けに、PHP と Apache に基づいた単純なメッセージ配信システムを作成しました。データ フローの単純化されたバージョンは次のようになります。JSON メッセージは、HTTP を介して、Apache サーバー上で実行されている PHP スクリプトに送信され、そこで何らかの処理が行われ、HTTP を使用して多数のクライアントに配信されます。 . 10-20。これらのメッセージは、1 秒あたり最大 5 件のメッセージでさまざまな間隔で発生しますが、1 分あたり 1 件程度の頻度で発生することもあります。

使用するサーバーは、8 GB のメモリと高速ネットワーク カードを備えた強力な Ubuntu マシンで、接続の良好なデータ ウェアハウスに配置されています。

クライアントには応答時間に関して非常に厳しい要件があるため (応答を期待するメッセージの場合、応答時間は 200 ミリ秒未満でなければなりません)、さまざまなタイムアウトと実行時間を非常にきめ細かく制御できるように、独自の HttpRequest クラスを実装しました。サーバーはこのクラスを使用して、メッセージをクライアントに転送します。興味深い部分は次のようになります。

class HttpRequest {
    private $host;
    private $port;
    private $path;

    private $connection = null;
    private $headers = array();

    [...]

    const CONTENT_TYPE = 'application/json';
    const ENCODING = 'utf-8';
    const TIMEOUT = 0.2;

    const MAX_RESPONSE_SIZE = 0xFFFF; // 64 kB

    public function __construct($url, $callback = null) {
        if (empty($url)) {
            throw new HttpException("url cannot be empty");
        }

        $url_parts = parse_url($url);

        $this->host = $url_parts['host'];
        $this->port = (empty($url_parts['port']) ? 80 : $url_parts['port']);
        $this->path = $url_parts['path'];

        $this->headers['Host'] = $this->host;
        $this->headers['Connection'] = 'close';
        $this->headers['Cache-Control'] = 'no-cache';
    }

    public function __destruct() {
        try {
            $this->disconnect();
        } catch (Exception $e) {
        }
    }

    private function connect() {
        if ($this->connection != null) {
            // already connected, simply return
            return;
        }

        $errno = '';
        $errstr = '';
        $_timeout = self::TIMEOUT;

        $this->connection = @fsockopen($this->host, $this->port, $errno, $errstr, $_timeout);

        if ($this->connection === false) {
            throw new HttpException("error during connect: $errstr", ($errno == SOCKET_ETIMEDOUT));
        }

        stream_set_timeout($this->connection, (int)(floor($_timeout)), (int)(($_timeout - floor($_timeout)) * 1000000));

        [variable assignments]
    }

    private function disconnect() {
        if ($this->connection == null) {
            // already disconnected, simply return
            return;
        }

        @fclose($this->connection);
        $this->connection = null;
    }

    public function post($data, $fetch_response = true, $path = null) {
        if (empty($data)) {
            throw new HttpException("no data given", false);
        }

        $contenttype = 'application/x-www-form-urlencoded';

        if (is_string($data)) {
            $data = urlencode($data);
        } else if (is_array($data)) {
            $data = http_build_query($data);
        } else if ($data instanceof Message) {
            $data = json_encode($data);

            $contenttype = 'application/json';
        }

        if (!is_string($data)) {
            throw new HttpException("wrong datatype", false);
        }

        $encoding = mb_detect_encoding($data);

        if ($encoding != self::ENCODING) {
            $data = mb_convert_encoding($data, self::ENCODING, $encoding);
        }

        if (empty($path)) {
            $path = $this->path;
        }

        // set header values
        $this->headers['Content-Type'] = $contenttype . '; charset=' . self::ENCODING;
        $this->headers['Content-Length'] = mb_strlen($data);

        // build request
        $request = "POST $path HTTP/1.1\r\n";

        foreach ($this->headers as $header => $value) {
            $request .= "$header: $value\r\n";
        }

        $request .= "\r\n$data";

        // and send it
        $this->sendRequest($request, $fetch_response);

        if ($fetch_response) {
            // fetch and parse response
            $resp = $this->receiveResponse();

            return $resp;
        }
    }

    public function get($path = null) {
        [build and execute http query]
    }

    private function sendRequest($request, $keep_connected = true) {
        // connect the socket
        $this->connect();

        [timer1]

        // write out data
        $result = @fwrite($this->connection, $request);

        [timer2]

        if ($result === false || $result != mb_strlen($request)) {
            $this->disconnect();
            throw new HttpException("write to socket failed", false);
        }

        if (!$keep_connected) {
            $this->disconnect();
        }
    }

    private function receiveResponse() {
        [fetch response using stream_select() and fgets() while strictly observing the timeout]
    }

    private function parseLine($msg) {
        [process http response, used in receiveResponse()]
    }
}

このクラスは通常、次のように使用されます。

$request = new HttpRequest($url);
$request->post($data);

問題

時々、一部のメッセージが期限切れになり、5 秒を超えるタイムアウトが記録されます。IO 関連の関数へのすべての呼び出しは、この時間のかなり前にタイムアウトする必要があるため、コードから見ると、これは不可能なはずです。

プロファイリング ステートメント (コードでは [timer1] および [timer2] として示されています) により、HttpRequest->connect() の呼び出しでこの遅延が発生していることが明らかになりました。私の最善の推測は、何らかの理由で fsockopen() が渡されたタイムアウトを無視することです。

興味深いことに、指定された制限を超えるタイムアウトが発生するたびに、通常は 5 秒強であり、ネットワーク コードの下位層のどこかに 5 秒の遅延があると考えられます (ソケット リソースの枯渇など)。この動作はホストが IP アドレスを使用して指定されている場合にも発生するため、DNS 関連の問題はおそらく除外できます。

この問題は通常、メッセージが多くのクライアントに配信される場合に発生します。つまり、多くのリクエストが立て続けに送信されますが、1 つのメッセージを 1 つのクライアントにのみ送信する場合にも発生します。Apache への複数の同時リクエストがある場合、一般的に発生するようですが、必ずしもそうである必要はありません。

誰かが同様の問題を経験しましたか? インターウェブはあまり網羅的ではなく、PHP ソース コードの作業も行っていません。この問題にアプローチする方法についての指針はありますか?

4

0 に答える 0