If a C programmer asks "do you want to see something cool?", run away.
--John Van Enk

Friday, August 31, 2012

MongoDB MapReduce & Sharding Gotchas

tl;dr: это б....дь п........ц

Ну а те кто хочет прочесть историю печальную сию - ниже ее изложение.

Итак, "посчастливилось" мне попробовать MongoDB в реальном проекте (больше такого счастья не хочется почему-то). Основа проекта - бэкенд который делает интенсивный data crunching и расчет различных метрик основываясь на данных, которые хранятся в монго. Ну а поскольку в версии 2.0.x монго единственный способ делать агрегацию данных (sum, avg, etc) - MapReduce то именно при помощи его все это и считается. Одна незадача - статистика роста количества входных данных показывает что если все оставить как есть и ничего не менять то в ближайшем будущем упремся в потолок - для подсчета статистики и метрик будет уходить более суток, что не приемлемо. Соответственно возникла задача все это оптимизировать/отмасштабировать чтобы "считалось быстрее" и "все были счастливы" (product owner и customer'ы). С этого и началась моя "эпопея", которой могут позавидовать герои "Иллиады" Гомера.



Надо сказать сразу - оптимизировать там было особо нечего, оставалось только паралелить на несколько ядер/машин. Хорошо что MapReduce - модель распределенная, следовательно должна неплохо паралелиться. "Щас мы это все распаралелим", - подумал я и  полез на сайт mongodb.org  искать кнопку "сделай мне зае....сь" опцию которая включала "паралельность". И тут меня поджидал первый облом в виде тикета SERVER-4258 который говорил о том, что V8 JS (на котором пишутся Map & Reduce функции) в версии 2.0.х монго работает в один поток и вообще пошли все нафиг. Такого жестокого разочарования я, признаться честно не ожидал, и предложил заказчику перейти на Release Candidate где V8 должен был уметь "делать многопоточность", на что получил четкое и однозначное "NO. That's Not an option. Environment stability is our top priority" (в этом месте я подумал "Какого ж Х.... вы тогда взяли монго вместо проверенных годами mysql или постгреса?"). Ну да ладно - деньги ваши (да, я честно предупредил, что любой другой вариант решения займет в разы больше времени и соответственно денег), врубил ирку и полез на #mongodb на FreeNode приставать с распросами к людям. В результате общения в ирке, гугления и чтения stackoverflow выяснилось что второй вариант распаралеливания (process paralelism) может сработать. Т.е. буквально MapReduce будет работать параллельно на нескольких шардах. Да, надо сказать что была еще попытка использовать Hadoop + hadoop mongodb connector, но предварительный evaluation этого решения показал что он займет "ваще как много времени и денех" и после пары дней бодания со слоном я занялся шардингом коллекций которые служили в качестве input для MapReduce (далее MR).

Одной из центральных тем шардинга в mongodb является тема правильного выбора sharding key (проще говоря ключа, по которому монго будет определять на какой шарде хранится документ). Тема эта не простая и часто возникают такие затруднения, что "хочется взять в руки АКМ". Врубив в наушниках по этому случаю КиШ начал я настраивать шардинг. Поднял кластер из двух шардов, одного конфиг сервера и 1 mongos. Первому шарду указал использовать ту же базу что и single instance mongo. Replica sets отложил на потом. В качестве sharding key выбрал _id (единственное поле с индексом которое присутствовало во всех документах ), указал chunkSize 20 и скрестив пальцы начал наблюдать в tail -f /var/log/mongodb/mongos.log как монго перераспределяет чанки между шардами. На следующий день (данных было много :) ) посмотрел распределение в mongo console при помощи db.printShardingStatus() - чанки были распределены поровну между шардами и зарядил MR jobs в очередь а сам, заметив время, приконнектился к обеим шардам через mongostat  и стал смотреть статистику. Поначалу сообщения mongostat о том что второй шард до 90% времени проводит в локах меня не насторожили почему-то. Однако когда MR продолжил работать и после того времени когда он обычно завершался на single instance я понял что "шото нє так". К этому моменту я наткнулся на "Note that because the most significant part of an ObjectId is time-based, using ObjectId as the shard key has the same issue as using time directly." (http://www.mongodb.org/display/DOCS/Choosing+a+Shard+Key#ChoosingaShardKey-Writescaling), выматерился, и решил, ну ок - добавлю в каждый документ поле в котором будет хранится md5sum(_id). Сказано - сделано, а вот хрен там. Поскольку единственным способом поменять sharding key в монго - дропнуть коллекцию, потом заново импортировать данные и дальше по накатанной - shard key, chunk redistribution, etc. Поскольку посчитать md5sum для ~10 миллионов  документов, экспортировать в BSON (mongodump) а потом импортировать (mongorestore) коллекцию в 15 Гб процесс не сильно быстрый то тестирование работы MR проходило уже на следующий день. Тестирование показало что все сильно хуже/медленнее. Вчитываясь в документ по ссылке приведенной выше - понял что при использовании md5sum(_id) Query Isolation - вообщем с ним вообще все плохо, т.к. один запрос (find()) к базе в данном случае должен будет работать с обеими шардами а не с одним. Подумав немного решил что обеим условиям (Cardinality, Qery Isolation) хорошо удовлетворил бы ключ вроде md5sum(FOREIGN KEY) и нечто похожее на FOREIGN KEY в документах было, но не во всех. Выручила банальная конструкция shard_key = doc.f_key.nil? ? md5sum("0") : md5sum(doc.f_key). Проще говоря у всех документов у которых "внешнего ключа" не было - sharding key считался как md5sum("0"). Далее - снова дроп коллекции, импорт, расчет ключа, шардинг, запуск теста. Второй шард опять ~90% locked. В этом месте я начинаю изъясняться исключительно матом, на community надежд никаких увы нет. Включаю профилирование и пытаюсь выудить оттуда что-то полезное (https://gist.github.com/3414360). И тут совершенно случайно замечаю в db.stats() что на втором шарде 7k индексов (СЕМЬ ТЫСЯЧ ШТУК ИНДЕКСОВ А НЕ ЗАПИСЕЙ В ИНДЕКСЕ). Как оказалось некоторые MR из предыдущих тест ранов свалились в экспешн и временные коллекции в которые записываются промежуточные результаты не удаляются и переиндексируются при последующих MR (вроде как - точно не уверен). Удалив временные коллекции (https://gist.github.com/3413526) перезапустил тест и - вуаля locked % на втором шарде ~20% и прирост в производительности на MR около 30%. Неплохо, но какого ... 30% only? Оказалось mergesort (финальная стадия MR) выполняется на mongos соотв имея 2 шарда в 2 раза быстрее работать не будет.

В общем - это только кролики быстро ..., а весь тот траблшутинг, которые описан выше занял у меня очень много времени. И все бы ничего, да вот буквально недавно на днях вышел MongoDB 2.2.x stable (тот самый в котором заимплементен пресловутый тикет SERVER-4258) ("А Я ЖЕ ГОВОРИЛ!"). В свете этого релиза мне предстоит протестировать производительность того же MR на mongo 2.2.x single instance & sharded instance. Так что будет follow up :) - stay tuned.

P.S. да к стати chunksize имеет значение. в моем случае 20 Мб оказалось мало - mongos не получалось их сплитить. т.к. результат в 20 Мб не влезал и процесс зависал на одном документе который был большего размера чем чанк (mongos пытался сплитнуть чанк, но результат превосходил 20Мб). В общем я увеличил его до 100Мб. на распределении чанков (данных всего ~ 15 Гб) по шардам это практически никак не сказалось.