Issue #29 open
imbolc
created an issue

Для встроенных вариантов бд тормозит ощутимо. Два-три раза для обвязки нормально, но двадцать :) Тебе вообще интересно ускорить? Или ты только серверные варианты юзаешь, где разница не так ощутима? Я мог бы потыкать, если примешь правки (иначе слеплю что-то быстренько под tct исключительно).

{{{ --- put 10000 rows doqu: 1.80661892891 tct: 0.0881068706512 delta: 20.5048586513

--- get 10000 random rows doqu: 0.998152971268 tct: 0.0350489616394 delta: 28.4788171911 }}}

Код теста: {{{

!python

!/usr/bin/python

-- coding: utf-8 --

import time from random import randint, shuffle

import doqu import tokyo.cabinet as tc

ROW_COUNT = 10000

keys = [str(i) for i in xrange(ROW_COUNT)]

db = doqu.get_db(backend='docu.ext.tokyo_cabinet', path='var/doqu.test.tct') db.clear() start = time.time() for key in keys: doqu.Document(val=key).save(db, key) db.connection.close() doqu_time = time.time() - start

db = tc.TDB() db.open('var/test.tct', tc.TDBOWRITER | tc.TDBOCREAT | tc.TDBOTRUNC) start = time.time() for key in keys: db.put(key, {'val': key}) db.close() tct_time = time.time() - start

print '--- put %i rows' % ROW_COUNT print 'doqu:', doqu_time print 'tct:', tct_time print 'delta:', doqu_time / tct_time

db = doqu.get_db(backend='docu.ext.tokyo_cabinet', path='var/doqu.test.tct') doqu_keys = [str(i+1) for i in xrange(len(keys))] shuffle(doqu_keys) start = time.time() for key in doqu_keys: obj = db.get(doqu.Document, key) db.connection.close() doqu_time = time.time() - start

db = tc.TDB() db.open('var/test.tct', tc.TDBOREADER) shuffle(keys) start = time.time() for key in keys: db.get(key) db.close() tct_time = time.time() - start

print '--- get %i random rows' % ROW_COUNT print 'doqu:', doqu_time print 'tct:', tct_time print 'delta:', doqu_time / tct_time }}}

Comments (16)

  1. Andy Mikhailenko repo owner
    • changed status to open
    • marked as bug

    Мда... :)

    А если выкинуть Document, получается:

    а) разница примерно в 4-5 раз против 20 на запись:

    for key in keys:
        #doqu.Document(val=key).save(db, key)
        db.save(key, {'val': key})
    

    ...и б) разница в 17 раз против 55 (омг!) на чтение:

    for key in keys:
        #db.get(key, doqu.Document)
        db.get(key)  # т.е. db.get(key, dict)
    

    В общем, даже адаптер БД показывает неприемлемые результаты, надо оптимизировать. Я пока этим не занимаюсь, потому что использую Doqu там, где приоритетом является удобство API (личный органайзер с веб- и CLI-интерфейсом). Серьезная оптимизация сейчас может быть преждевременной. С другой стороны, переделывать можно бесконечно, а библиотеку пора обкатывать на разных кейсах. Ну и тормоза адаптера БД наверняка сводятся к паре глупых ляпов, потому что _там_ тормозить почти нечему. :)

    Ситуацию вижу так:

    1. В целом система довольно проста и должна хорошо поддаваться оптимизации;
    2. Интерфейс пока не совсем устоялся и внутренности надо выпрямлять (особенно это касается препроцессоров полей и связей между документами);
    3. Непреодолимые архитектурные тормоза могут быть (только) у концепта document-query. Т.е., валидаторы могут генерить много поисковых условий, на которые индексов не напасешься. На практике пока проблем не замечено. В крайнем случае можно разводить схемы по хранилищам/коллекциям (правда, пока ссылки между хранилищами не поддерживаются).
    4. Недавно я отделил document-query от всего остального, чтобы с адаптером БД можно было спокойно использовать как обычные словарики, так и более "умные" классы (напр., doqu.Document, но не обязательно). Так что можно даже прицепить совершенно другую реализацию как расширение.

    Если тебе интересно заняться оптимизацией, есть смысл отложить всякий вялотекущий рефакторинг и допилить библиотеку в текущем состоянии. Разумеется, правки приму. Документация пользователя пока в запустении, но в коде ее достаточно, вроде. Если что, отвечу на вопросы (тут или в группе).

    P.S.: в скрипте s/docu/doqu/g

  2. imbolc reporter

    Наверное, тебе запушиться пора :) В текущей версии db.save(key, {'val': key}) = db.save({'val': key}, key) и у мну получается не в 4 раза совсем:

    --- put 10000 rows
    doqu: 0.123748064041
    tct: 0.0881669521332
    delta: 1.40356518
    

    db.get(dict, key) - не смог запустить

    Вообще, мне система кажется несколько противоречивой идеологически. Вроде как она делается как замена rdbms. Т.е. безсхемность пользователей не интересует. Что остаётся? Только производительность? Но она достигается по большей части фишками каждой субд. Например, монго без его хитрых обновлений объектов без загрузки как бы и смысла не имеет. В плане производительности.

    А тут получаются базовые функции, без этих самых фишек. Я бы предложил выкинуть всякие монги вообще. И даже hash-варианты. Под последние проще написать отдельную либу (у мну есть уже, могу показать). Оставить только tokyo :)

  3. Andy Mikhailenko repo owner

    Насчет концепции. Есть два уровня:

    1. адаптеры хранилищ и запросов (унифицированный api для in-memory, tc/tt, mongo и тд);
    2. "схема-запрос" (документ с двусторонней валидацией).

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

    Второй уровень вырос из обвязки для TC. Может пригодиться для более сложного анализа дампов, моделирования данных и отработки архитектуры _до_ окончательного выбора бэкенда. Мы здесь _не_ пытаемся переизобрести RDBMS на бессхемном хранилище (в отличие даже от MongoKit, где схема привязывается к коллекции). Здесь один документ может спокойно быть представлен несколькими схемами. На РБД такое невозможно без EAV. Это основная причина, по которой Doqu не поддерживает РБД (только через shove). Хотя шутки ради можно прикрутить EAV-бэкенд вроде eav-django))

    Насчет фишек бэкендов: я сразу отказался от поддержки CouchDB, потому что она навязывает особенную архитектуру приложения. У остальных бэкендов не так много внешних различий. Пока единственная серьезная возможность MongoDB, которую упускает Doqu -- это вложенные структуры (в тч поиск по ним). Остальное, вроде, можно либо вкрутить в адаптер, либо делать напрямую (адаптеры не прячут соединение с БД и тд). Тогда более-менее простые вещи будут реализовываться в клиентском коде абсолютно одинаково для разных бэкендов, а для остального можно на разных уровнях добавить ветвление.

    Почему не только TC? Потому что у меня для разных целей было несколько библиотек с пересекающимися возможностями (напр., dark и pyrant) и стала очевидной необходимость их унифицировать. (А тот же Dark обязательно должен работать с простым списком словарей.) И еще: когда я начал писать органайзер, была мысль для начала работать с BDB. Но пошли грабли с потерей данных. Заменил на TT, но он сильно тормозил при limit/offset, когда кол-во элементов приближалось к 10К (важно в веб-админке; возможно, проблема в питоньей обвязке). Также был бэкенд для монги уровня пруф-оф-консепт, так вот он оказался и надежным, и реактивным. Поскольку API унифицирован, все миграции сводились к правке конфига и простому циклу по элементам. Мне кажется, во многих случаях возможность позднего выбора бэкенда полезна.

  4. imbolc reporter

    с BDB. Но пошли грабли с потерей данных

    а что такое BDB?

    Выложил обёртку бдм: http://pypi.python.org/pypi/odbm/

    Вот тест скорости в сравнении с дампом словаря cPickle перед запихиванием в tc:

    seconds to write/read 10000 rows
    pickle (write, read): (1.2187790870666504, 0.25383901596069336)
    odbm   (write, read): (0.95468688011169434, 0.14065408706665039)
    pickle file size: 2768704
    odbm   file size: 1348752
    

    Размер файла уменшился за счёт уменьшения длины ключей. Мне этой фичи в doqu не хватает. Код примера: https://bitbucket.org/imbolc/odbm/src/50cb54ca00f1/speed_test.py

  5. Andy Mikhailenko repo owner

    а что такое BDB?

    Беркли, но я имел в виду shelve.

    Обертка понравилась, изящная и компактная. Правда, не очень ясно, насколько корректно использовать marshal (напр., см. SO). Еще бы сравнить хэш+маршал с TC TDB (без доп. сериализации). Подозреваю, что встроенные средства поиска TDB дадут при фильтрации лучшие результаты, чем проворачивание данных через питон даже с маршалом. Правда, к этому добавятся накладные расходы на приведение типов полезных документах (но это и в odbm просходит с частью данных).

    Про ключи не понял: речь о том, что они тоже сериализуются?

    Еще примечание в связи с odbm: в doqu я сознательно отвязал соединение с БД от модели, чтобы не навязывать её базовый класс.

  6. imbolc reporter

    Давно тестил Беркли, она оказалась сильно медленней GDBM. Потом нашёл TC, он ещё быстрее и файлики компактнее. А компактность файлика принципиальна - как только он перестаёт влазить в кэш фс, скорость рандомных выборок падает на два порядка:

    Данные в dbm никогда не терялись. Может, это связано с тредовым веб-сервером? Одновременные попытки записи разными тредами? Сам использую gevent, он однопоточный и этой проблемы не стоит.

    На практике marshal использую уже давно в критичных местах, с проблемами не сталкивался. Но при смене версии питона, надо быть готовым конвертить бд, конечно :)

    встроенные средства поиска TDB дадут при фильтрации лучшие результаты

    Только при наличии крупных полей. И если они используются в поиске. Ведь, приведение типов происходит при обращении к полю. Ещё маршал позволяет использовать лямбды при поиске, мне очень нравится :)

    Про ключи не понял: речь о том, что они тоже сериализуются?

    birthday = odbm.DateProperty(key='b') в базе сохранится {'b': данные}. Т.е. длинный ключ birthday станет коротким b.

  7. Andy Mikhailenko repo owner

    Насчет влияния компактности на скорость — интересный аргумент. Но как быть, если БД постепенно растёт и в какой-то момент объем превышает пороговое значение? Получается, что конкретную БД нужно оптимизировать под конкретное железо, это странно.

    > Одновременные попытки записи разными тредами?
    

    Именно. Werkzeug настраивается, но я решил не рисковать и для веба запускал TT.

    >> встроенные средства поиска TDB дадут при фильтрации лучшие результаты
    > Только при наличии крупных полей. И если они используются в поиске.
    

    Насколько крупных? :) Мой юзкейс: в органайзере 18К элементов, их постоянно нужно просеивать по ряду полей (включая, например, какой-нибудь FK, а это UUID) и сортировать по дате.

    Лямбды — удобно, некоторые даже транслируют их в SQL =)

    В целом, думаю, в слое абстракции надо по возможности использовать максимально стандартные, простые и прозрачные типы/структуры, а их уже посыпать синтаксическим сахаром. И давать прямой доступ к нижележащему интерфейсу (т.е. при желании использовать сразу лямбды, TC'шные триплеты, монговские "документы запросов" и т.д.). Это в меньшей степени касается специализированных библиотек (pyrant, qdbm), но важно для doqu.

    > Ведь, приведение типов происходит при обращении к полю
    

    Кстати, да, так и надо, наверно. В докью по разным (преодолимым?) причинам конвертация не ленивая.

    > birthday = odbm.DateProperty(key='b')
    

    В раздумьи. Следуя вышеозначенной логике, я старался сделать подобный синтаксис факультативным синтаксахаром для словариков. Сейчас вот такая штука:

    class Foo(Document):
        birthday = Field(datetime.date)
    

    есть синтаксический сахар для вот этого:

    class Foo(Document):
        structure = {
            'birthday': datetime.date,
        }
    

    Думаю, что нетрудно сделать и так:

    class Foo(Document):
        birthday = Field(datetime.date, key='b')
    
    class Foo(Document):
        structure = {
            'b': datetime.date,
        }
        aliases = {
            'birthday': 'b',
        }
    

    Ну и при __getitem__/__setitem__ смотреть сначала в aliases. Это может пригодиться не только для компактности, но и для переформулировки неудачных полей без правки самой базы. Удобно маппить всякие CSV-дампы... и в органайзере у меня куча ETL. Так что фича полезная)

  8. imbolc reporter

    если БД постепенно растёт и в какой-то момент объем превышает пороговое значение

    На практике всё приятнее. Странички сайта редко дёргаются рандомно. Допустим на блоге процентов 80 трафика примет морда. Тогда, даже если весь файл бд не входит в кэш, в нём закешируется часть с данными мордочки и она будет отдаваться быстро.

    Но я предпочитаю разносить "таблицы" в разные файлики физически. Поскольку точных алгоритмов фс не знаю, да и меняются они. В таком случае наиболее используемые таблицы будут в кеше.

    Кстати, в doqu поле _type_ в некоторых случаях может увеличить размер файлика в разы.

    В целом, думаю, в слое абстракции надо по возможности использовать максимально стандартные, простые и прозрачные типы/структуры, а их уже посыпать синтаксическим сахаром. И давать прямой доступ к нижележащему интерфейсу (т.е. при желании использовать сразу лямбды, TC'шные триплеты, монговские "документы запросов" и т.д.)

    Вот в этом мне и видится некоторая противоречивость doqu. Если юзать нижележащий интерфейс, то сменить бэкенд уже сложновато будет. А если его не юзать то из возможных бд сразу можно выбрать максимально производительную. Остаётся редкий случай, когда толком не представляешь, что понадобится и начинаешь писать. Но на этот случай уже есть всякие джанги :)

    aliases

    Вообще, заметил в doqu тенденцию делать одно и то же разными способами. В odbm, а точнее в её предшественнице тоже было подобное (всякие обращение в dict и dot синтаксисе, property были вообще дополнительными). Но на практике всё равно приходилось описывать структуру в каментах, иначе через пару месяцев править код было сложно. И она разделилась на две либы с однозначным синтаксисом: odbm с жёсткой структурой и другую безсхемную обёртку. И использовать в проекте их обе оказалось удобнее, чем мудрить с одной универсальной :)

  9. imbolc reporter

    Насколько крупных? :)

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

  10. Andy Mikhailenko repo owner
    > Тогда, даже если весь файл бд не входит в кэш, в нём закешируется
    > часть с данными мордочки и она будет отдаваться быстро.
    

    В таком случае имеет ли значение общий размер файла?

    А зачем поле __type__?

    Если разносить "таблицы" по файлам, получается уже нечто, близкое к РБД. Наверно, тогда лучше взять обычный ORM. А если данные разных классов переплетаются (что в том же органайзере имеет место), нужна как раз одна "гибкая таблица", т.е. DODB. И doqu решает проблему сопоставления документов схемам через "двусторонние валидаторы" (т.е. схему, которая и валидирует входящие данные, и формулирует базовый запрос для отбора уже существующих). При формулировке запроса используется именно схема (структура и валидаторы).

    > Если юзать нижележащий интерфейс, то сменить бэкенд уже сложновато будет.
    > А если его не юзать то из возможных бд сразу можно выбрать максимально
    > производительную. Остаётся редкий случай, когда толком не представляешь, 
    > что понадобится и начинаешь писать. Но на этот случай уже есть всякие джанги
    

    Django ORM, как и любой ORM, работает только с РБД, в этом и проблема. Doqu оптимальна для быстрого прототипирования (сначала рабочий прототип, затем сравниваем производительность бэкендов на реальном приложении и смотрим, что можно оптимизировать, а в каком случае проще сменить БД) и для написания приложений-плагинов, не зависящих от бэкенда. Использование нижележащего интерфейса может быть либо временным хаком, либо следствием окончательного выбора бэкенда, либо вообще завернуто в проверку типа бэкенда (и, возможно, позднее перенесено из клиентского кода в саму doqu).

    Насчет getitem/getattr: думаю, что надо оставить интерфейс словарика, а остальное привинчивать по вкусу. Ядро Doqu должно предоставлять простой, ясный, прозрачный и однозначный интерфейс. Поверх могут быть любые дополнительные абстракции. Чем выше уровень абстракции, тем большую роль играют эстетические предпочтения и тем больше случаев, когда абстракция просто не подходит. Поэтому и желательна слоистость, модульность. Одноуровневая схема (с объявлением атрибутов в духе Property) годится для многих случаев, но иногда нужны вложенные структуры. И тут "колоночный" синтаксис пролетает, а объявление в духе MongoKit (словариками) работает как прежде. "Колонки" -- частный случай стуктуры документа.

    (Энивей, думаю, не совсем понял абзац про две либы;)

  11. imbolc reporter

    Ух всёж сложно мне это всё понять. Сложилось ощущение, что ты решаешь проблемы, которых пока не стоит. Какие-то абстрактные проблемы, так чтобы каким-то абстрактным всем было удобно :) А тебе, наверное, будут приятны лавры всеобщего любимца ;)

    Т.е. я совсем даже не против. Это, наверное интересно, написать такую штуку. И будущее, наверное, за такими штуками. Джанга, например, одна из таких вещей достигшая популярности.

    Но у мну конкретная проблема согнавшая с rdbms - производительность. Если бы была простенькая rdbm, но очень производительная - выбрал бы её. Но таковой не знаю. Даже объектные базы недостаточно производительны. Mongo, например, лишь раза в 3 опережает Postgres на простых выборках. Видимо, буду писать с нуля обёртку над tct :)

  12. Andy Mikhailenko repo owner
    > Сложилось ощущение, что ты решаешь проблемы, которых пока не стоит
    > [...] Это, наверное интересно, написать такую штуку.
    

    Мм... сначала это был не более чем временный эксперимент. Затем выяснилось, что он решает вполне реальную проблему, которая согнала меня с Django ORM (и РБД вообще): в органайзере много сущностей, которые не ложатся на жесткие таблицы, плохо поддаются разделению. И даже для компромиссного варианта чрезвычайно трудно заранее спроектировать наименее граблегенную структуру. Поэтому я начал проектировать модели на Doqu, а затем не увидел смысла их переносить обратно на РБД и вновь огребать возню с South. Ну и отрадно, что накопившиеся решения формируют пространство для маневра: вместо SQLite — TC, вместо MySQL — MongoDB, у всех свои ниши и один апи.

    Короче, у нас разные приоритеты: мне важна гибкость и переносимость, тебе — производительность. Интересно, можно ли найти хороший баланс. Например, если ты сделаешь обертку вокруг tct, а я попробую ее слить с doqu и минимизировать разрыв в производительности, выиграют все. Кстати, надо тесты производительности использовать наряду с юнит-тестами. Каюсь: часто даже не догадываюсь о влиянии очередного коммита на эту часть требований.

    Видимо, буду писать с нуля обёртку над tct :)

    Она чем-то будет принципиально отличаться от odbm?

  13. imbolc reporter

    Она чем-то будет принципиально отличаться от odbm?

    Синтаксис? Да они все ни чем особо не отличаются от джанга-орм по синтаксису :)

    Вообще, меня сильно подмывает дописать индексы к odbm :) Причём не b-tree, а обычные жёсткие 1, 2, ... Чтобы они пересчитывались при первом запросе. А при изменении затронутых полей объекта - протухали. Это будет оочень производительно при приоритете чтения. А b-tree и должны тормозить на лимитах как ты и говорил, они же пробегаются с начала.

  14. Andy Mikhailenko repo owner

    Симпатично. Какой оверхед получился?

    Не представляю пока, как это можно подключить в Doqu, тк в tclite всё сильно завязано на Model. Но думаю, что можно.

    Синтаксис фильтров неплох. Правда, в уже существующих данных имена полей могут содержать пробелы. В фильтрах это можно нормально обработать через partition вместо split (и тогда синтаксис будет полезнее, чем в Django и Doqu), но вот модель/поля уже не прогнуть ни под пробелы, ни под non-ascii; только dict-like API покатит как для декларирования такой схемы, так и для работы с документом.

    Кстати, по сабжу: в Doqu get/put/search со словариком медленнее чистого TC в 3-4 раза, а вот смена dict на Document очень сильно замедляет всё, поэтому его надо тестировать отдельно.

  15. imbolc reporter
    --- put 10000 rows
    doqu: 1.86304998398
    tclite: 0.808727025986
    tct: 0.132590055466
    --- get 10000 random rows
    doqu: 0.977886915207
    tclite: 0.165719985962
    tct: 0.0403220653534
    

    код: http://dumpz.org/37557/

    Ещё есть в сравнении с sqlite:

    seconds to write/read 10000 rows
    sqlite (write, read): (1.6685819625854492, 1.2025470733642578)
    tclite by index (write, read): (1.1604280471801758, 0.79429101943969727)
    tclite by keyname (write, read): (0.98326301574707031, 0.17406105995178223)
    

    код: https://bitbucket.org/imbolc/tclite/src/0e87207ff8fc/test_speed.py

    в уже существующих данных имена полей могут содержать пробелы

    есть же key в fields

  16. Log in to comment