Есть ли решение этой утечки памяти cfqueryparam?

Обновления:

  • Я отправил сообщение об ошибке в Adobe и сослался на этот вопрос SO

  • В моем реальном коде, где возникла проблема, я решил просто удалить использование cfqueryparam. Теперь я использую пользовательскую функцию для форматирования параметра на основе типа. Есть проблемы с безопасностью и скоростью, с которыми мне придется иметь дело, но это заставляет конкретный процесс работать приемлемо при текущей нагрузке.

  • В будущем я планирую перейти к процессу, который извлекает файлы данных во временные таблицы в базе данных. Затем я буду выполнять операции с данными и передавать данные в живые таблицы, используя SQL, насколько это возможно, вместо того, чтобы полагаться на ColdFusion.


У меня возникла проблема с зацикливанием запросов с использованием тегов cfqueryparam при вставке данных. (Я не тестировал запросы на выбор или обновление). Зацикливание постепенно занимает больше памяти, которая не освобождается до тех пор, пока запрос не будет выполнен. Однако проблема возникает только при циклическом выполнении запроса в функции.

Похоже, он очень чувствителен к количеству используемых тегов cfqueryparam. В этом примере вставляется 15 значений, однако в моем коде, который действительно нуждается в этом, я вставляю неизвестное количество значений, которые могут усугубить проблему.

Ниже приведен код, который показывает проблему. Дайте ему имя источника данных (проверено на MSSQL), и он создаст таблицу tmp и вставит записи, например, с функцией и без нее. Использование памяти отображается до, после нефункционального цикла, а затем после функционального цикла. Он также запрашивает сборку мусора и ждет 10 секунд, прежде чем выводить информацию о памяти, чтобы обеспечить максимально точное отображение информации.

По моему опыту с этим конкретным тестом, цикл в работе привел к использованию более 200 МБ памяти. В моем реальном мире это приводит к сбою ColdFusion :-(

<cfsetting enablecfoutputonly="true">
<cfsetting requesttimeout="600">

<cfset insertCount = 100000>
<cfset dsn = "TmpDB">

<cfset dropTmpTable()>
<cfset createTmpTable()>

<cfset showMemory("Before")>
<cfflush interval="1">

<cfloop from="1" to="#insertCount#" index="i">
    <cfquery name="testq" datasource="#dsn#">
        INSERT INTO tmp ( [col1],[col2],[col3],[col4],[col5],[col6],[col7],[col8],[col9],[col10],[col11],[col12],[col13],[col14],[col15] )
        VALUES ( <cfqueryparam value="TestValue" cfsqltype="CF_SQL_CHAR">, <cfqueryparam value="TestValue" cfsqltype="CF_SQL_CHAR">, <cfqueryparam value="TestValue" cfsqltype="CF_SQL_CHAR">, <cfqueryparam value="TestValue" cfsqltype="CF_SQL_CHAR">, <cfqueryparam value="TestValue" cfsqltype="CF_SQL_CHAR">, <cfqueryparam value="TestValue" cfsqltype="CF_SQL_CHAR">, <cfqueryparam value="TestValue" cfsqltype="CF_SQL_CHAR">, <cfqueryparam value="TestValue" cfsqltype="CF_SQL_CHAR">, <cfqueryparam value="TestValue" cfsqltype="CF_SQL_CHAR">, <cfqueryparam value="TestValue" cfsqltype="CF_SQL_CHAR">, <cfqueryparam value="TestValue" cfsqltype="CF_SQL_CHAR">, <cfqueryparam value="TestValue" cfsqltype="CF_SQL_CHAR">, <cfqueryparam value="TestValue" cfsqltype="CF_SQL_CHAR">, <cfqueryparam value="TestValue" cfsqltype="CF_SQL_CHAR">, <cfqueryparam value="TestValue" cfsqltype="CF_SQL_CHAR"> )
    </cfquery>
</cfloop>

<cfset showMemory("After Non-Function INSERTS")>
<cfflush interval="1">

<cfset funcTest()>

<cfset showMemory("After Function based INSERTS")>

<cfset dropTmpTable()>

<cffunction name="funcTest" output="false">
    <cfset var i = 0>
    <cfset var testq = "">
    <cfloop from="1" to="#insertCount#" index="i">
        <cfquery name="testq" datasource="#dsn#">
            INSERT INTO tmp ( [col1],[col2],[col3],[col4],[col5],[col6],[col7],[col8],[col9],[col10],[col11],[col12],[col13],[col14],[col15] )
            VALUES ( <cfqueryparam value="TestValue" cfsqltype="CF_SQL_CHAR">, <cfqueryparam value="TestValue" cfsqltype="CF_SQL_CHAR">, <cfqueryparam value="TestValue" cfsqltype="CF_SQL_CHAR">, <cfqueryparam value="TestValue" cfsqltype="CF_SQL_CHAR">, <cfqueryparam value="TestValue" cfsqltype="CF_SQL_CHAR">, <cfqueryparam value="TestValue" cfsqltype="CF_SQL_CHAR">, <cfqueryparam value="TestValue" cfsqltype="CF_SQL_CHAR">, <cfqueryparam value="TestValue" cfsqltype="CF_SQL_CHAR">, <cfqueryparam value="TestValue" cfsqltype="CF_SQL_CHAR">, <cfqueryparam value="TestValue" cfsqltype="CF_SQL_CHAR">, <cfqueryparam value="TestValue" cfsqltype="CF_SQL_CHAR">, <cfqueryparam value="TestValue" cfsqltype="CF_SQL_CHAR">, <cfqueryparam value="TestValue" cfsqltype="CF_SQL_CHAR">, <cfqueryparam value="TestValue" cfsqltype="CF_SQL_CHAR">, <cfqueryparam value="TestValue" cfsqltype="CF_SQL_CHAR"> )
        </cfquery>
    </cfloop>
</cffunction>

<cffunction name="showMemory" output="true">
    <cfargument name="label" required="true">

    <cfset var runtime = "">
    <cfset var memoryUsed = "">
    <cfset requestGC("10")>
    <cfset runtime = CreateObject("java","java.lang.Runtime").getRuntime()>
    <cfset memoryUsed = (runtime.totalMemory() - runtime.freeMemory()) / 1024 / 1024>
    <cfoutput>
        <h2>#arguments.label#</h2>
        Memory Used: #Round(memoryUsed)#mb
    </cfoutput>
</cffunction>

<cffunction name="requestGC">
    <cfargument name="waitSeconds" required="false" default="0" type="numeric">
    <cfscript>
        createObject("java","java.lang.Runtime").getRuntime().gc();
        createObject("java", "java.lang.Thread").sleep(arguments.waitSeconds*1000);
    </cfscript>
</cffunction>

<cffunction name="dropTmpTable" output="false">
    <cftry>
        <cfquery datasource="#dsn#">
            DROP TABLE tmp
        </cfquery>
        <cfcatch type="database"></cfcatch>
    </cftry>
</cffunction>

<cffunction name="createTmpTable" output="false">
    <cfquery datasource="#dsn#">
        CREATE TABLE tmp(
            col1 nchar(10) NULL, col2 nchar(10) NULL, col3 nchar(10) NULL, col4 nchar(10) NULL, col5 nchar(10) NULL, col6 nchar(10) NULL, col7 nchar(10) NULL, col8 nchar(10) NULL, col9 nchar(10) NULL, col10 nchar(10) NULL, col11 nchar(10) NULL, col12 nchar(10) NULL, col13 nchar(10) NULL, col14 nchar(10) NULL, col15 nchar(10) NULL
        )  ON [PRIMARY]
    </cfquery>
</cffunction>

Просто чтобы показать, что память может быть освобождена во время операции, вот пример кода, который создает большую структуру и показывает память, используемую до и после перезаписи переменной и сборки мусора. В моем прогоне эта память использовалась после заполнения 118 МБ, а после перезаписи и сборки мусора - 31 МБ.

<cfset showMemory("Before struct creation")>
<cfflush interval="1">

<cfset tmpStruct = {}>
<cfloop from="1" to="1000000" index="i">
    <cfset tmpStruct["index:#i#"] = "testvalue testvalue testvalue testvalue testvalue testvalue testvalue testvalue testvalue testvalue">
</cfloop>

<cfset showMemory("After struct population")>
<cfflush interval="1">

<cfset tmpStruct = {}>
<cfset showMemory("After struct overwritten")>

person Dan Roberts    schedule 12.05.2009    source источник
comment
Хммм, у вас разные источники данных в двух тегах cfquery — возможно, это просто ошибка в примере?   -  person Peter Boughton    schedule 13.05.2009
comment
Вы уверены, что виноват cfqueryparam? Что произойдет, если вы не используете cfqueryparam?   -  person ale    schedule 13.05.2009
comment
Спасибо, Питер Эл, я только что попробовал запрос с «TestValue» вместо всех тегов cfqueryparam, и в примере с функцией не было создано памяти. Поскольку cfqueryparam - единственное, что я меняю между двумя независимыми тестами, я считаю, что это как-то связано.   -  person Dan Roberts    schedule 13.05.2009


Ответы (10)


У вас включена отладка в Администраторе?

Если да, то даже если у вас есть showdebugoutput="false", CF будет хранить отладочную информацию обо всех этих запросах, а при таком количестве запросов отладочная информация может быстро накопиться.


Кроме того, если у вас действительно есть 80 000 строк для вставки, вы, вероятно, захотите сделать это по-другому, например. создание сценария импорта, который работает непосредственно с БД (без вмешательства CF/JDBC).

person Peter Boughton    schedule 12.05.2009
comment
Отладка не включена (я устанавливал enablecfoutputonly, а не showdebugoutput). Я не согласен с вами в том, что сценарий исключительно на стороне базы данных, вероятно, был бы хорошей идеей. Я предполагаю, что если мне нужно будет сделать что-то из нескольких сотен тысяч, мне, вероятно, понадобится это сделать. Однако прямо сейчас меня укусила какая-то ошибка в законном коде, и я пытаюсь ее обойти. - person Dan Roberts; 13.05.2009
comment
Я начинаю думать, что подход db может быть самой безопасной ставкой - person Dan Roberts; 13.05.2009
comment
Пит считал, что даже если вы подавляете вывод отладки, его включение может занимать память. Убедитесь, что флажок Включить вывод отладки запроса не установлен. - person ale; 13.05.2009
comment
Только что проверил на абсолютную уверенность и это НЕ проверка. Я столкнулся с проблемами при запуске отладки, так что это тоже было одной из моих первых мыслей. - person Dan Roberts; 13.05.2009

Возможно, многократная вставка может помочь? Этот метод сам по себе обычно работает быстрее, экономия времени может помочь вам сэкономить память.

Да, я видел вашу заметку «вставка неизвестного количества значений», но это должно работать, если у вас есть постоянное количество полей/значений в одном пакете вставки.

person Sergey Galashyn    schedule 13.05.2009
comment
Похоже, что эта техника приносит значительное улучшение скорости. Я ожидаю, что все еще придется избавиться от cfqueryparam, но, по крайней мере, с этим я сомневаюсь, что будет потеря производительности. - person Dan Roberts; 13.05.2009

Не знаю, будет ли это иметь значение, но что-то попробовать - уменьшить цикл внутри функции и несколько раз выполнить цикл вокруг функции.

То, что это делает с памятью, может помочь сузить область ее использования.

<cffunction name="funcTest" output="false">
    <cfargument name="from" />
    <cfargument name="to" />
    <cfset var i = 0>
    <cfset var testq = "">
    <cfloop from="#arguments.from#" to="#arguments.to#" index="i">
        <cfquery name="testq" datasource="#dsn#">
            ...
        </cfquery>
    </cfloop>
</cffunction>


<cfset BlockSize = 100 />
<cfloop index="CurBlock" from="1" to="#(InsertCount/BlockSize)#">

    <cfset funcTest
        ( from : CurBlock*(BlockSize-1) + 1
        , to   : CurBlock*BlockSize
        )/>

</cfloop>
person Peter Boughton    schedule 12.05.2009
comment
Это очень хорошая идея. Завтра я проведу тест, чтобы увидеть, уменьшает ли вызов функции для подмножества вставки накопление памяти. Я надеюсь, что это сработает, хотя теперь, когда я оглядываюсь назад на свой пример, память не освобождается после выполнения вызова функции и вызова gc. - person Dan Roberts; 13.05.2009
comment
К сожалению, это не помогло. Я сделал 50 блоков по 3000, и потребляемая память была такой же, как один блок из 150 000. - person Dan Roberts; 13.05.2009

Я столкнулся с похожей проблемой.

http://misterdai.wordpress.com/2009/06/24/when-not-to-use-cfqueryparam/

Подход зависит от нескольких вещей. Если вы можете доверять данным, не используйте cfqueryparam, это значительно уменьшит использование памяти. Оттуда минимизируйте SQL, насколько это возможно. Я выполнял довольно много работы с БД для каждой строки, поэтому вместо этого создал хранимую процедуру. Большим преимуществом в борьбе с использованием памяти была буферизация вызовов SQL к базе данных. Создайте массив, добавьте к нему свой SQL, затем каждые 50 строк (личный выбор после тестирования) сделайте ArrayToList в массиве внутри тега CfQuery. Это ограничивает трафик базы данных до меньшего, но большего, вместо множества меньших.

После всего этого у меня все заработало. Но я по-прежнему думаю, что ColdFusion действительно не подходит для такого типа задач, а скорее для домена самого сервера базы данных, если это возможно.

person Mister Dai    schedule 26.01.2011

Моим первым предположением было бы ввести значения в ваш cfqueryparam - как в type="CF_SQL_CHAR". Почему это поможет? Я не уверен, но могу предположить, что с нетипизированной переменной возникнут дополнительные накладные расходы.

person Community    schedule 13.05.2009
comment
Хорошая мысль, но я внес изменение, и память все еще накапливается и не освобождается во время цикла работы. - person Dan Roberts; 13.05.2009

Предполагая, что вы используете CF8... не уверен, что это происходит в CF7...

Попробуйте отключить "Max Pooled Statements" (установите его на ноль) в "расширенных настройках" вашего источника данных... Держу пари, что утечка памяти исчезнет...

Вот где я обнаружил ошибку... это вызывало всевозможные сбои на некоторых серверах CF, пока мы не обнаружили это... теперь мы на 100% более стабильны из-за этого...

Патрик Стайл

person Community    schedule 14.05.2009
comment
На самом деле я установил сводные статистические данные на ноль, а затем полностью отключил поддержку соединения после прочтения в различных блогах о том, что это возможное решение. Что заставляет меня теперь исключать различные настройки, так это тот факт, что я сузил утечку памяти только до того, находится ли код в функции. - person Dan Roberts; 15.05.2009
comment
Это в CF8. Получаете ли вы аналогичные результаты при выполнении теста? - person Dan Roberts; 15.05.2009
comment
Я сам больше не тестировал это ... так вы говорите, что с включенными операторами Max Pooled и cfqueryparam НЕ внутри CFFUNCTION утечка памяти НЕ происходит? И имеет ли значение, находится ли эта CFFUNCTION внутри CFC или это происходит только в обычной UDF на странице CFM? - person ; 16.05.2009

попробуйте добавить "переменные". перед каждым запросом внутри ваших cffunctions. У меня была похожая проблема, и это исправило ее.

Итак, измените:

<cfquery name="testq" datasource="CongressPlus">

to

<cfquery name="variables.testq" datasource="CongressPlus">

Ваше здоровье,

Томас

person Community    schedule 13.05.2009
comment
На самом деле я не хочу, чтобы запрос находился в области переменных. Мой фактический код находится в CFC, и это может вызвать проблемы. На самом деле в моем реальном коде я полностью удалил имя. - person Dan Roberts; 13.05.2009
comment
Для таких тегов, как cfquery, которые создают переменные, для вещей, которые должны быть закрытыми, вам нужно заранее изменить переменную. ‹cfset var qName= /› ‹cfquery name=qName ...› Конечно, если вы не используете имя в своих вставках, это, вероятно, не имеет значения. - person ale; 13.05.2009
comment
да, в некоторых случаях переменные с именами по умолчанию (например: cfhttp), но это не относится к cfquery и так или иначе не влияет на тест, когда запрос является вставкой/обновлением - person Dan Roberts; 13.05.2009

В сообществе хорошо задокументировано, что CF не будет освобождать память до тех пор, пока запрос не будет завершен. Даже прямой вызов GC не влияет на освобождение памяти во время выполнения запроса. Не знаю, это так задумано или баг.

Я понятия не имею, почему вы вообще хотите сделать что-то подобное в CF. У вас нет причин вставлять 80 000 строк в базу данных с помощью CF, независимо от того, какой механизм базы данных вы используете.

Теперь, если есть причина, по которой вам нужно это сделать, например, вы получаете данные, скажем, из загруженного файла CSV или XML; У MSSQL есть ТОННА лучших способов сделать это и обходных путей.

Один из подходов, который я использовал на протяжении многих лет, заключается в создании хранимой процедуры в MSSQL, которая вызывает BCP или МАССИВНАЯ ВСТАВКА для чтения файла, который содержит данные для вставки.

Лучшее в этом подходе то, что единственное, что делает CF, — это обрабатывает загрузку файла, а MMSQL выполняет всю работу по обработке файла. MSSQL без проблем вставляет миллионы строк с помощью BCP или BULK INSERT и будет БЕСКОНЕЧНО быстрее, чем все, что может обработать CF.

person rip747    schedule 13.05.2009
comment
Я согласен, что это, вероятно, лучше всего использовать в базе данных. Я делаю манипуляции и поиск в CF, но я считаю, что их можно перенести в операции запросов на основе наборов, хотя я еще не все просмотрел. Я не согласен с вашим утверждением, что CF не будет освобождать память до тех пор, пока не будет получен запрос. Я добавлю приведенный выше пример кода, показывающий освобождение памяти при перезаписи переменной. - person Dan Roberts; 13.05.2009

Способ предотвратить утечку памяти из cfqueryparam в большом цикле запросов состоял в том, чтобы не использовать cfqueryparam. Однако более широкий ответ заключается в том, чтобы избежать неэффективности CF и утечек памяти, чтобы не использовать CF в этих ситуациях. Я довел конкретный процесс до приемлемого уровня нагрузки на тот момент, но в долгосрочной перспективе буду переписывать его на другом языке, возможно C#, прямо в движке базы данных.

person Dan Roberts    schedule 06.11.2010

Я понятия не имею, решит ли это вашу проблему, но что я обычно делаю, когда у меня есть несколько подобных вставок, так это цикл самого оператора SQL вместо всего cfquery.

Итак, вместо того, чтобы:

<cfloop from="1" to="#insertCount#" index="i">
    <cfquery name="testq" datasource="#dsn#">
        ...
    </cfquery>
</cfloop>

I do :

<cfquery name="testq" datasource="#dsn#">
    <cfloop from="1" to="#insertCount#" index="i">
        ...
    </cfloop>
</cfquery>

Таким образом, вместо нескольких обращений к базе данных у вас есть только один большой.

Я понятия не имею, как это повлияет на вашу проблему с утечкой памяти, но я никогда не сталкивался с утечками памяти таким образом.

person FreddyF    schedule 02.12.2011
comment
Я делаю это в разных местах по разным причинам, но это не решение проблемы с утечкой памяти. Существует ограничение на количество запросов, которые могут быть отправлены в один вызов базы данных (я думаю, что общий размер sql/params составляет 64 КБ), поэтому требуется разбивка по записям. При использовании cfqueryparam по-прежнему возникают проблемы с памятью. - person Dan Roberts; 08.12.2011