Pull to refresh

Comments 21

А на кой черт вам все сто тыщ записей в памяти? Запрашивайте кусочками по тысяче, всё равно же обрабатываете по одной.
Вот именно. Если вы зачем-то выбираете 100000 записей в приложении разом — вы что-то делаете не так. И уж тем более вам не нужно столько записей на одной странице сайта, так что заявка про «никто не будет ждать 52 секунды» тоже ни о чем.

Это, что характерно, не отменяет рассуждений о применимости тех или иных инструментов для тех или иных задач, но вот задачи у вас какие-то странные.
Согласен.
Но! В данном посте не алгоритм работы моих функций описывался, а результаты тестирования двух подходов в получении данных.
Благодаря тесту можно видеть, что даже при выборке пачками в 1 000 штук, DAO жрет в 6,37 раз меньше ресурсов.
Не подходов, всё-таки, а конкретных реализаций двух паттернов.
Не в 6.37 раз, а на 6МБ. То есть по текущим ценам где-то несколько центов.
а как вы проверяете, что между запросами данные не поменялись и вы выберите всё, без пропусков и дублирований?
Что-то не припомню ничего подобного в исходной постановке задачи. Хотите менять условия — распишите целиком, что вам хочется получить и насколько критично чего-то из перечисленного не достичь.
То есть в задаче «сделать рассылку по 100 000 адресам» вам надо отдельно указывать, что «не разослать по десятку из них или разослать дважды — плохо»?
Конечно надо. Хотите предсказуемый результат — учитесь ставить задачи.

Вообще, в данном случае, первое, что приходит на ум — нарезать диапазоны по id, завернуть все select-ы в одну транзакцию и дальше не париться. Разослать дважды не получится (неуникальность почтового адреса не рассматриваем), проблемы с неактуальностью могут возникнуть только для записей, которые появились или отредактировались во время рассылки (первым ничего не придёт, вторым придёт на старый адрес). Насколько вероятны такие ситуации и насколько это плохо — решать бизнесу.
> Хотите предсказуемый результат — учитесь ставить задачи.

Вообще то когда я хочу предсказуемый результат, то другое просто не оплачу. И здесь не важно умею ли я ставить задачи.
У меня на похожей задаче (выгрузка статистики пользовательских DNS-запросов в CSV), когда она возникла, была очень жесткая позиция: только потоковая обработка. Никаких переменных, хранящих весь результат запроса (потенциально сотни тысяч строк). Любая попытка сложить все в память (в том числе любой вызов C-функции mysql_fetch_result()) автоматически является багом. Вот рекомендации, как этого бага избежать.

C: используйте mysql_use_result()

PHP: выполняйте небуферизованные запросы с флагом MYSQLI_USE_RESULT, см. www.php.net/manual/en/mysqlinfo.concepts.buffering.php

Python: есть класс MySQLdb.cursors.SSCursor.

Во всех случаях перед формированием очередного запроса необходимо полностью прочитать результат предыдущего.

К сожалению, при использовании с Django, SSCursor немедленно делает приложение совершенно не поддающимся отладке, и вот почему. Если при обработке очередной (не последней) строки view выбросит исключение, то вызовется стандартный обработчик исключений. Он постарается откатить транзакцию и отправить traceback куда надо. Только вот попытка откатить транзакцию проваливается: выбрасывается исключение ProgrammingError, так как SQL-запрос ROLLBACK пришел до того, как все строки прочитаны. В итоге первоначальное исключение теряется, а в лог и в почту попадает «безволосая черная дыра» в виде ProgrammingError. Собственно, я считаю это багом в Django: code.djangoproject.com/ticket/21777

Вот костыль для тех случаев, когда патчить Django запрещено.

# Workaround against https://code.djangoproject.com/ticket/21777
# In short: gets meaningful tracebacks in apache log even if
# SSCursor is not completely consumed by the application when
# an exception occurs

from django.core.signals import got_request_exception, request_finished
from MySQLdb import DatabaseError
from MySQLdb.cursors import SSCursor
from threading import local

store = local()


class SafeSSCursor(SSCursor):
    def execute(self, *args, **kwargs):
        if not hasattr(store, 'cursors'):
            store.cursors = []
        if not self in store.cursors:
            store.cursors.append(self)
        return super(SafeSSCursor, self).execute(*args, **kwargs)


def expunge_cursors(**kwargs):
    store.cursors = []


def consume_cursors(**kwargs):
    if not hasattr(store, 'cursors'):
        return
    for cursor in store.cursors:
        try:
            for item in cursor:
                pass
        except DatabaseError:
            pass
    store.cursors = []


request_finished.connect(expunge_cursors)
got_request_exception.connect(consume_cursors)

Прошу прощения за опечатку. Ошибкой является вызов mysql_store_result()
Да, в качестве фреймворка используется Yii
мне кажется, что в вашей ситуации нужно не active record на dao (или еще чтото) менять, а сам алгоритм
зачем грузить сразу все записи в память? — чтобы «быстрее работало» что ли?

у меня в системе порядка 1млн подписок, которые каждый день обрабатываются, обработка идет пачками по 1000 штук, германовские скрипты месяцами висят запущенными и не жрут память
В данном посте не алгоритм работы моих функций описывался, а результаты тестирования двух подходов в получении данных.
Благодаря тесту можно видеть, что даже при выборке пачками в 1 000 штук, DAO жрет в 6,37 раз меньше ресурсов.
Пожалуй стоит в статье пометить (где-нибудь в начале) что тестировалась имплементация ar в yii.

Вообще все это всегда было понятно. А для уменьшения потребления памяти стоит применять штуки типа dataProviderIterator. И да, active record никогда не рассчитывалась на то, что придется работать одновременно с большим количеством объектов.
Как-то не очень корректно тестирование проведено, по-моему. Протестированы по одной неназванной реализации двух паттернов (если я вообще правильно понял о чём статья) на неизвестной выборке из неизвестной схемы. По личному опыту, универсальные ActiveRecord библиотеки с каким-то декларативным объявлением очень чутко реагируют на вид запросов, в частности на количество объединений — потребление памяти и процессора растёт на порядок с каждым объединением: скажем, из недавнего, на маппинге 30 строк Доктрина 1.2 вылетала с аут оф мемори — гигабайта не хватало. Убрал из выборки только одно объединение — порядка ста метров максимальное потребление. Если же маппинг захардкожен ручками, то всё намного лучше — рост линейный.
Специально для этого в Yii придумали CDataProviderIterator он загружает CActiveRecord объекты из базы страницами (так же, как если бы вы выводили их постранично, используя CDataProvider), а не все сразу. А так, понятно, что DAO в любом случае будет быстрее, особенно на больших объёмах.
Sign up to leave a comment.