some image

IIS течет? Утечка памяти в модуле sqlsrv 2.0

Работа

То, о чем пойдет речь в данном посте, не что иное как огромная, наглая, ужасная подстава от Microsoft. Выпустить драйвер который безвозвратно забирает n байт памяти при каждом SQL запросе — это надо умудриться.

Итак, обо всем по-порядку.

Довольно таки продолжительное время продакшн сервер грешил непонятными скачками памяти. Сразу было понятно что это w3wp.exe ( рабочий процесс IIS для application пула ).

При этом все работало стабильно и никакие ошибки с сервера не шли, поэтому эта проблема просто была в подвешенном состоянии и за проблему вообщем то и не воспринималась. Ну течет и течет, с кем не бывает, не падает же.

Продолжалось это до момента, когда сброс памяти (абсолютно не прогнозируемый, к слову) не совпал с моими действиями на сервере. И тут обнаружилось, что в этот момент все не просто падает, а падает так капитально, что не может даже репортить о том что упало. Примерно минут на 5, потом все ок.

Естественно сразу начались поиски причин утечки. На мой взгляд их не так много:

1) То, что связано с постоянными подключениями (бд, memcached) и тп. Они могут не закрываться и висеть в памяти, например. Очень много может быть нюансов, связанных с ними, тема отдельной статьи.

2) Багнутые dll-ки. Но этот момент как-то был опущен, т.к. к PHP подключены самые распространенные библиотеки и как-то не верилось что проблема может быть в них.

Был установлен пакет Diagnostic Tools, официально рекомендованный к дебагингу для IIS.

http://support.microsoft.com/kb/919790

Был снят дамп процесса во время падения:

Detected possible blocking or leaked critical section at ntdll!LdrpLoaderLock owned by thread 23 in w3wp.exe__DefaultAppPool__PID__2376__Date__11_26_2012__Time_12_55_07AM__485__Manual Dump.dmp
Impact of this lock
54.84% of threads blocked
(Threads 8 11 12 13 14 15 16 17 18 19 20 22 25 26 27 28 30)
The following functions are trying to enter this critical section
ntdll!LdrShutdownThread+4c
ntdll!LdrUnloadDll+27
ntdll!LdrpInitializeThread+6a


The following module(s) are involved with this critical section
C:\Windows\SysWOW64\ntdll.dll from Microsoft Corporation
The entry-point function for a dynamic link library (DLL) should perform only simple initialization or termination tasks, however this thread (23) is not fully resolved and may or may not be a problem. Further analysis of this thread may be required. Follow the guidance in the MSDN documentation for DllMain to avoid access violations and deadlocks while loading and unloading libraries.
Please follow up with the vendor Microsoft Corp. for C:\Program Files (x86)\PHP\ext\php_sqlsrv_52_ts_vc6.

Стало понятно что проблема с sqlsrv драйвером. Но и в голову не могло прийти что утечка была не по нашей вине. Начался дебаг всей обертки этого драйвера в приложении.

Но работа не давала результата и наконец была найдена эта ветка: http://social.msdn.microsoft.com/Forums/en-US/sqldriverforphp/thread/7e419559-0d8f-4cea-a2ae-28b71a800f63  в которой работники microsoft честно признали — да, бага есть, фиксить не будем, ждите 3.0. К счастью версия 1.1 оказалась не подвержена этому багу, и был произведен успешный откат на нее.

Вот код для проверки уязвимости вашего приложения на этот баг, на случай, если в исходнике затеряется. В нормальной ситуации от запуска к запуску процесс вообще не должен прибавлять в памяти. У нас, с учетом нагрузки в 3.4 миллиона sql запросов в сутки, IIS растекался больше чем на 3гб.

<?php

$db_conn = get_db_conn_sqlsrv();
#$db_conn = get_db_conn_mysqli();

$query = "SELECT 1 AS dummy"; // FROM DUAL
$seconds = 10; //60 * 60;

echo '<br>php version: [' . phpversion() . ']';
echo '<br>sapi name: [' . php_sapi_name() . ']';
echo '<br>runtime: [' . $seconds . 's]';
echo '<br>memory_get_usage start: [' . memory_get_usage() . ']';
echo '<br>memory usage by tasklist start: [' . memory_get_usage_by_tasklist() . ']';

$start_time = time();
while (time() - $start_time < $seconds) {
    db_query_sqlsrv($db_conn, $query);
    #db_query_mysqli($db_conn, $query);
}

echo '<br>memory_get_usage end: [' . memory_get_usage() . ']';
echo '<br>memory_get_peak_usage: [' . memory_get_peak_usage() . ']';
echo '<br>memory_get_peak_usage real: [' . memory_get_peak_usage(true) . ']';
echo '<br>memory usage by tasklist end: [' . memory_get_usage_by_tasklist() . ']';

function db_query_sqlsrv($conn, $query)
{
    $res = sqlsrv_query($conn, $query);
    if (!$res) {
        throw new Exception('query failed');
    }

    #sqlsrv_free_stmt($res);
    #sqlsrv_close($conn);
}

function db_query_mysqli($conn, $query)
{
    $query_id = mysqli_query($conn, $query);
    if (!$query_id) {
        throw new Exception('Invalid SQL: ' . $query);
    }
}

function get_db_conn_sqlsrv()
{
    $host = "xxx,1234";
    $conn_info = array(
        'Database'                  => 'db_name',
        'MultipleActiveResultSets'  => true,
        'CharacterSet'              => 'UTF-8',
        'UID'                       => 'uid',
        'PWD'                       => 'pwd'
        );

    sqlsrv_configure('WarningsReturnAsErrors', 0);
    $conn = sqlsrv_connect($host, $conn_info);
    if (!$conn) {
        throw new Exception('no link');
    }

    return $conn;
}

function get_db_conn_mysqli()
{
    $host = 'xxx';
    $user = 'user';
    $pass = 'pwd';
    $port = '1234';
    $database = 'db';

    $link_id = mysqli_connect($host, $user, $pass, '', $port);

    if (!$link_id) {
        throw new Exception('no connection');
    }

    if (!mysqli_select_db($link_id, $database)) {
        throw new Exception('cannot use database');
    }

    return $link_id;
}

// = "working set memory" in taskmgr (windows server 2008r2)
function memory_get_usage_by_tasklist()
{
    if (!is_windows()) {
        return 'n/a';
    }

    $output = array();
    exec('tasklist ', $output);
    foreach ($output as $value) {
        $ex = explode(" ", $value);
        $count_ex = count($ex);
        if (preg_match('/ ' . getmypid() . ' Services/', $value)) {
            $memory_size = $ex[$count_ex - 2] . ' Kb';
            return $memory_size;
        }
    }
}

function is_windows()
{
    return (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN');
}