Hi,
Unsafe "max_execution_time" and "Out of Memory" handling is a huge problem,
that often lead to crashes and SHM corruption.
The PoC solves the first problem.
https://github.com/php/php-src/pull/1173
Instead of throwing zend_error() from signal handler, now we just set
EG(vm_interrupt) and EG(timed_out) flags. PHP VM checks EG(vm_interrupt)
flag on each JMPx instruction (potential loop iteration) and then throws
the same zend_error() from VM context. This is safe, and we don't need to
wrap some critical code sections with
HANDLE_BLOCK_INTERRUPTIONS/HANDLE_UNBLOCK_INTERRUPTIONS anymore (we will
need them only in opcache). A small overhead of checking EG(vm_interrupt)
on jumps is counterbalanced by improvement from
HANDLE_BLOCK_INTERRUPTIONS/HANDLE_UNBLOCK_INTERRUPTIONS removal.
Unfortunately this approach doesn't support interruption of long-running
internal functions yet. It should be extended in some way. May be
additional timeout.
Improvement ideas are welcome...
Also, do we need "safe" handling for other signals?
I think no, but may be I miss something.
Thanks. Dmitry.
Hi!
Instead of throwing zend_error() from signal handler, now we just set
EG(vm_interrupt) and EG(timed_out) flags. PHP VM checks EG(vm_interrupt)
flag on each JMPx instruction (potential loop iteration) and then throws
That looks very nice but makes timeouts much less powerful. I think
without some ability to interrupt internal functions it won't be good.
Unfortunately this approach doesn't support interruption of long-running
internal functions yet. It should be extended in some way. May be
additional timeout.
Doing additional setjmp when entering internal function probably would
be too expensive. So the question is how to get out of the bad function
without incurring per-function overhead... Not sure how to do it.
--
Stas Malyshev
smalyshev@gmail.com
On Thu, Mar 12, 2015 at 1:23 AM, Stanislav Malyshev smalyshev@gmail.com
wrote:
Hi!
Instead of throwing zend_error() from signal handler, now we just set
EG(vm_interrupt) and EG(timed_out) flags. PHP VM checks EG(vm_interrupt)
flag on each JMPx instruction (potential loop iteration) and then throwsThat looks very nice but makes timeouts much less powerful. I think
without some ability to interrupt internal functions it won't be good.
agree. This is why I ask for ideas.
Unfortunately this approach doesn't support interruption of long-running
internal functions yet. It should be extended in some way. May be
additional timeout.Doing additional setjmp when entering internal function probably would
be too expensive. So the question is how to get out of the bad function
without incurring per-function overhead... Not sure how to do it.
The problem is not in overhead. If we longjmp() from signal handler we
always may have something partially uninitialized and this leads may lead
to crash on request shutdown and SHM pollution.
I think, after "max_execution_time" is exceeded, we may start another
"hard_timeout", and in case the EG(vm_interrupt) wasn't handled "safely",
kill the process.
Thanks. Dmitry.
--
Stas Malyshev
smalyshev@gmail.com
Hi!
I think, after "max_execution_time" is exceeded, we may start another
"hard_timeout", and in case the EG(vm_interrupt) wasn't handled
"safely", kill the process.
I remember such proposal floating on the list recently, with two-stage
timeouts. But wouldn't killing the process run the same risk that it
left something messed up in SHM?
Also, in threaded environments that's probably not an option, and PHP
may not even own the thread.
--
Stas Malyshev
smalyshev@gmail.com
On Thu, Mar 12, 2015 at 1:46 AM, Stanislav Malyshev smalyshev@gmail.com
wrote:
Hi!
I think, after "max_execution_time" is exceeded, we may start another
"hard_timeout", and in case the EG(vm_interrupt) wasn't handled
"safely", kill the process.I remember such proposal floating on the list recently, with two-stage
timeouts. But wouldn't killing the process run the same risk that it
left something messed up in SHM?
yes :(
if we try to recovery from some inconsistent state we most probably crash.
I even don't talk about memory and resource leaks.
Also, in threaded environments that's probably not an option, and PHP
may not even own the thread.
I'm not sure how the existing setjmp/longjmp based timeout handling works
in ZTS environment now, because except of SHM it may also easily corrupt
the process memory shared with other threads.
Thanks. Dmitry.
--
Stas Malyshev
smalyshev@gmail.com
Hi!
Instead of throwing zend_error() from signal handler, now we just set
EG(vm_interrupt) and EG(timed_out) flags. PHP VM checks EG(vm_interrupt)
flag on each JMPx instruction (potential loop iteration) and then throws
JMPs are not the only operations that can transfer control and thus
potentially form a loop.
Unfortunately this approach doesn't support interruption of long-running
internal functions yet. It should be extended in some way. May be
additional timeout.
Doing additional setjmp when entering internal function probably would
be too expensive. So the question is how to get out of the bad function
without incurring per-function overhead... Not sure how to do it.
--
Stas Malyshev
smalyshev@gmail.com
Hi!
Instead of throwing zend_error() from signal handler, now we just set
EG(vm_interrupt) and EG(timed_out) flags. PHP VM checks EG(vm_interrupt)
flag on each JMPx instruction (potential loop iteration) and then throwsJMPs are not the only operations that can transfer control and thus
potentially form a loop.
Oops, please ignore this, it was a draft sent accidentally.
Hi,
Improvement ideas are welcome...
Hi Dmitry,
The idea was raised before of having both soft and hard limits for the
memory consumption and time limits, and to trigger a user defined
callback when the soft limit was reached.
This would be beneficial in a couple of ways.
i) It allows people to detect that their application is consuming more
memory/time than they would like, but not enough to cause stability
issues. This would be useful in production where you don't want to
make a request fail unless you absolutely have to.
ii) It allows them to decide exactly what action to take. Sometimes
it's fine to terminate requests straight away, other times people
would want to do some cleanup before terminating.
Additionally being able to call gc_collect_cycles()
in the callback
would sometimes release enough memory to bring the memory used to back
under the soft limit and so allow processing to continue. Ironically,
this seems to be an issue when writing efficient PHP code. Due to the
way the garbage collector only collects cycles rarely, if you have
very large variables compared to typical code, you can easily
encounter a situation where the total memory that is still being
referenced is small, but there is still a huge amount being held in
cycles.
The code below shows this happening:
With gc_collect_cycles called: peak memory = 786kB
Without gc_collect_cycles called: Allowed memory size of 67108864
bytes exhausted
i.e. 95% of memory allocated isn't being used and could be freed, but
hasn't because the number of GC objects didn't reach 10,000 and so the
gc_collect_cycles didn't kick in automatically.
cheers
Dan
<?php
$performGC = false;
if ($performGC) {
echo "GC collection will be done - app should not crash.\n";
}
else {
echo "GC collection won't be done - app should crash.\n";
}
$dataSizeInKB = 128;
//Change this line if you tweak the parameters above.
ini_set('memory_limit', "64M");
$memData = '';
for ($y=0 ; $y<$dataSizeInKB ; $y++) {
for ($x=0 ; $x<32 ; $x++) { //1kB
$memData .= md5(time() + (($y * 32) + $x));
}
}
file_put_contents("memdata.txt", $memData);
// This function creates a cyclic variable loop
function useSomeMemory($x) {
$data = [];
$data[$x] = file_get_contents("memdata.txt");
$data[$x + 1] = &$data;
};
for($x=0 ; $x<1000 ; $x++) {
useSomeMemory($x);
if ($performGC == true) {
gc_collect_cycles()
;
}
}
printf(
"\nused: %10d | allocated: %10d | peak: %10d\n",
memory_get_usage()
,
memory_get_usage(true),
memory_get_peak_usage(true)
);