Pull to refresh

Как оптимизировал работу с MongoDB с помощью устаревшего api или о чем молчит её спецификация…

Reading time4 min
Views19K
image

Однажды столкнулся с задачей: mongoDb использовался как кэш/буфер между backend на Java и frontend на node.js. Все было хорошо, пока не появилось бизнес требование перебрасывать большие объемы за короткое время через mongoDb (до 200 тыс. записей не более чем за пару минут). Для чего не так важно, важна что задача такая появилась. И вот тут уж пришлось разбираться во внутренностях монги…


Раунд 0:
Просто пишем в монгу с Write Concern = Acknowledged. Самый банальный и простой способ в лоб. В этом случае монго гарантирует что все записалось без ошибок и вообще все будет хорошо. Все отлично пишется, но… при 200 тыс. умирает на двадцать и более минут. Не подходит. Путь в лоб вычеркиваем.

image
Раунд 1:
Пробуем Bulk write operations с тем же Write Concern = Acknowledged. Стало лучше, но не сильно. Пишет минут за десять-пятнадцать. Странно, вообще-то ожидалось большее ускорение. Ладно, идем дальше.

image
Раунд 2:
Пробуем поменять Write Concern на Unacknowledged и до кучи использовать Bulk write operations. Вообще, не лучшее решение, так как если что-то в монге пойдет что не так, мы никогда об этом не узнаем, так как она сообщит только что данные до неё дошли, а вот записались ли они в базу или нет неизвестно. С другой стороны, по бизнес требованиям данные это не банковские транзакции, единичная потеря не так критична, а если в монге все будет плохо, мы и так узнаем из мониторинга. Пробуем. С одной стороны записалась за всего минуту это хорошо (без Bulk write operations полторы минуты тоже неплохо), с другой стороны возникла проблема: сразу после записи java дает отмашку node.js и когда она начинает читать, данные приходят то целиком, то вообще не приходят, то половина читается, половина нет. Виной тут асинхронность — при таком Write Concern, монга ещё пишет, а node.js уже читает, соответственно клиент успевает прочитать раньше, чем запись гарантировано закончится. Плохо.


Раунд 3:
Начали думать, идея писать Thread.sleep(60 секунд) или записывать какой-нибудь контрольный объект в монгу, который показывал что все данные загрузились, выглядит очень криво. Решили посмотреть почему Bulk write operations ускоряют так плохо, ведь по идее Write Concern должен замедлять последнею запись при Bulk write operations, а не вообще все. Как-то нелогично получается, что ожидание записи последней порции требует столько времени. Смотрим код драйвера монги на Java, натыкаемся на то пакеты bulk операций ограничены неким параметром maxBatchWriteSize. Debug показывает что этот параметр у нас всего 500, то есть на самом деле весь bulk режется запросами только по 500 записей, поэтому и такие результаты, Acknowledged каждый раз ждет полной записи этих 500 записей, прежде чем послать новый запрос и так четыре тысячи раз при максимальном объеме, а это дико тормозит.


Раунд 4
Пытаемся понять откуда берется этот параметр maxBatchWriteSize, находим что драйвер монги делает запрос getMaxWriteBatchSize() к серверу монги. Возникла мысль увеличить этот параметр в конфиге монги и обойти это ограничение. Попытки найти этот параметр или запрос в спецификации дали нулевой результат. Ладно, ищем в инете, находим исходные коды на C++. Этот параметр — банальная константа, зашитая жестко в коде исходников, то есть увеличить его никак не возможно. Тупик.


Раунд 5
Ищем в инете ещё варианты. Вариант заливать через сотню параллельных потоков решили не пробовать, банально можно за DDos'ить собственный сервер с монгой (тем более что монга и сама умеет параллелить приходящие запросы). И тут нашли такую команду как getLastError, суть его ждать пока все операции сохраняться в базу и вернуть код ошибки или успешного окончания. Спецификация усилено пытается убедить, что метод устарел и его использовать не нужно, в драйвере монги он отмечен как depricated. Но пробуем отправляем запросы с Write Concern = Unacknowledged и Bulk write в ordered режиме, а потом вызываем getLastError() и да, за полторы минуты записали все записи синхронно, теперь клиент начинает чтение именно после полной записи всех объектов, так как getLastError() ждет окончания последней записи, при этом пакеты не тормозят друг друга. Ко всему прочему, если произойдет ошибка, мы об этом узнаем с помощью getLastError(). То есть мы получили именно быстрый Bulk write с Acknowledged, но ожиданием только последнего пакета (ну или почти, обработка ошибок вероятно будет хуже, чем у настоящего режима Acknowledged, вероятно эта команда не покажет ошибку произошедшую только в первых пакетах, с другой стороны вероятность что первый пакет упадет с ошибкой, а последний пройдет успешно — не так велика).

image
Итак о чем молчит спецификация монги:
1. Bulk write операция не очень-то bulk и жестко ограничена потолком 500-1000 запросов в пакете. Update: на самом деле, как я сейчас обнаружил, все-таки упоминание о потолке в 1000 операций появилось, не было упоминания о магической константе больше года назад в версии 2.4, когда и проводился анализ,

2. Увы, но механизм с getLastError был в чем-то более успешным и новый механизм Write Concern пока не полностью его заменяет, точнее можно использовать устаревшую команду для ускорения работы, так логичное поведение «ждать успешную запись только последнего пакет из большого ordered bulk запроса» в монге так и не реализовано,

3. Проблема Write Concern=Unacknowledged даже не в том что данные могут потеряться и ошибка не возвращается, а в том что данные записываются совсем асинхронно и попытка клиента сразу обратиться к данным легко может привести к тому что он не получит данных или получит лишь часть их (важно, если команду на чтение отдавать сразу после записи).

4. У монги производительность запросов сильно страдает от такого ограниченного bulk'а и Acknowledged Write Concern реализован не совсем правильно, правильно ждать окончания записи именно последнего из пакетов.

P.S. В целом, получился интересный опыт оптимизации нестандартными методами, когда в спеках нет всей информации.

P.P.S. Так же советую посмотреть мой opensource проект [useful-java-links](https://github.com/Vedenin/useful-java-links/tree/master/link-rus) — возможно, наиболее полная коллекция полезных Java библиотек, фреймворков и русскоязычного обучающего видео. Так же есть аналогичная [английская версия](https://github.com/Vedenin/useful-java-links/) этого проекта и начинаю opensource подпроект [Hello world](https://github.com/Vedenin/useful-java-links/tree/master/helloworlds) по подготовке коллекции простых примеров для разных Java библиотек в одном maven проекте (буду благодарен за любую помощь).
Tags:
Hubs:
If this publication inspired you and you want to support the author, do not hesitate to click on the button
Total votes 41: ↑30 and ↓11+19
Comments14

Articles