icicleio / http

HTTP component for Icicle
MIT License
61 stars 5 forks source link

Memory leak? #9

Open yangxikun opened 8 years ago

yangxikun commented 8 years ago

I use apache jemeter to make benchmark on the example server(with litter modify):

public function onRequest(Request $request, Socket $socket)
    {
        $data = sprintf(
            'Hello to %s:%d from %s:%d!',
            $socket->getRemoteAddress(),
            $socket->getRemotePort(),
            $socket->getLocalAddress(),
            $socket->getLocalPort()
        );

        $body = $request->getBody();

        if ($body->isReadable()) {
            $data .= "\n\n";
            do {
                $data .= (yield $body->read());
            } while ($body->isReadable());
        }

        $coroutine = Coroutine\create(function () {
            $client = new Client();
            $encoder = new Http1Encoder();

            // Connect to a google.com IP.
            // Use Icicle\Dns\connect() in icicleio/dns package to resolve and connect using domain names.
            $data = '';
            //Make a http request to another site
            try {
                $socket = (yield Icicle\Socket\connect('10.123.5.34', 80));

                /** @var \Icicle\Http\Message\Response $response */
                $response = (yield $client->request($socket, 'GET', 'an url', ['Cookie' => 'cookie=xxx']));

                $stream = $response->getBody();
                while ($stream->isReadable()) {
                    $data .= (yield $stream->read());
                }
            } catch (\Exception $e) {
                echo "Catch Exception by me : " . $e->getMessage() . "\n";
            }

            yield $data;
        });

        $coroutine->done(null, function (Exception $exception) {
            printf("Exception: %s\n", $exception);
        });
        $data .= (yield $coroutine);
        $sink = new MemorySink();
        yield $sink->end($data);

        $response = new BasicResponse(200, [
            'Content-Type' => 'text/plain',
            'Content-Length' => $sink->getLength(),
        ], $sink);

        yield $response;
    }

I found the memory use by the process is increase, and not decrease:

~/dev/icicle » ps -e -o 'pid,ppid,rsz,vsz,args' | grep php
114552   7223 125376 411320 php server.php

And there have many error output like:

Error when handling request from 192.168.103.1:51302: Failed to write to stream. Errno: 2; fwrite(): 2588 is not a valid stream resource

If I continue benchmark, will suffer a fatal error:

PHP Fatal error:  Allowed memory size of 134217728 bytes exhausted at /home/rokety/software/php-5.6.15/Zend/zend_execute.h:187 (tried to allocate 130968 bytes) in /home/rokety/dev/icicle/vendor/icicleio/http/src/Driver/Encoder/Http1Encoder.php on line 70

Another, If I ctrl+c the server script, it print a memory leak message:

/home/rokety/software/php-5.6.15/Zend/zend_vm_execute.h(944) :  Freeing 0x7F8E06527168 (32 bytes), script=/home/rokety/dev/icicle/server.php
=== Total 1 memory leaks detected ===
yangxikun commented 8 years ago

Maybe I have found the increase memory use reason: in the such EvIoManager class has properties like private $events = []; or private $timers = [];

The framework stop some events or timers : $this->events[$id]->stop(); and $this->timers[$id]->stop(); But don't unset them(unset($this->events[$id]), unset($this->timers[$id])

trowski commented 8 years ago

I believe the memory leak was coming from the socket created by $socket = (yield Icicle\Socket\connect('10.123.5.34', 80)); not being closed (not calling $socket->close()). This leaves resources in the loop associated with that object. However, I see that being a common problem, so I updated the class extended by NetworkSocket so that the loop resources would be freed from the loop without calling close. Update to the latest version of icicleio/socket (should be able to just run composer update) and see if your code works without changes. Please let me know if you still are seeing memory leaks after updating.

yangxikun commented 8 years ago

After update icicleio/socket, my code works! However, there still something wrong:

  1. Under a high concurrent request, the allowed memory still exhausted(once this happen, the server is deny of service, and has to be kill -9). Is there a good practices to limit the concurrent connection number on the server script?
  2. zend engine still complain the memory leak:
~/dev/icicle » php server.php
^C[Fri Jan  1 07:41:41 2016]  Script:  '/home/rokety/dev/icicle/server.php'
/home/rokety/software/php-5.6.15/Zend/zend_vm_execute.h(944) :  Freeing 0x071F6418 (32 bytes), script=/home/rokety/dev/icicle/server.php
=== Total 1 memory leaks detected ===

If I pass false in Icicle\Loop\loop():

function loop(Loop $loop = null)
    {
        static $instance;

        if (null !== $loop) {
            $instance = $loop;
        } elseif (null === $instance) {
            $instance = create(false);
        }

        return $instance;
    }

zend engine doesn't complain the memory leak. So there is something wrong with the signal handling?

bwoebi commented 8 years ago

Eventually try compiling php-src master branch which fixes a bunch of memory leaks. There are leaks involving cleanup of Generators and Exceptions in older versions.

As long as these leaks aren't numerous, there's nothing you need to bother about. (After all, it's just 32 bytes ...)

trowski commented 8 years ago

@yangxikun What sort of high concurrency are you talking about? I've successfully tested a couple thousand concurrent requests with no issues. Does the server just hang (even with no load) after this happens? I'm going to need a little more information to track down what might be happening.

yangxikun commented 8 years ago

Under high concurrency, the server script will hold many connections at the same time. In my case, each request will make a remote request to another web services. It meant each request will cost a lot of time. So, Increasingly connections that hold by server, will lead to more memory use, until the amount of memory use exceed php limit.