На выходных попробовал RavenDB на небольшой задаче обработки массива документов. Документов было не очень много — порядка 50К, их обработка — задача разовая, но её длительность получалась однозначно порядка 10 часов, плюс всё это отлично параллелилось.
Поэтому возникла мысль загнать все эти документы в БД, чтобы без проблем сохранять промежуточные результаты и не волноваться за exception’ы, безвозвратно прерывающие обработку 10-часового процесса в самом конце :)
Raven-Embedded видился неплохим кандидатом для такого использования, поскольку позволял не париться с маппингами, быстро «установить» БД, просто добавив nuget пакет, позволял динамически добавлять в документы новые структуры данных (результаты обработок) ну и по идее должен был быстро работать :)
Что же из всего этого получилось?
Сразу оговорюсь, что использовал последнюю Prerelease-версию.
Мой код, в общих чертах, представлял из себя выборку и обработку блоков по 100 документов, предварительно внесенных в БД.
Выглядело это как-то так (опуская обработку ошибок и некоторые мелкие детали):
lock (_lockObject) { //получаем список необработанных домов. houses = session.Query<HouseInfo>() .Where(x => x.State == HouseInfo.DownloadState.NotStarted) .Customize(x => x.WaitForNonStaleResultsAsOfNow()) // необходимо, чтобы дома "недавно" помеченные как ParseStarted не попали на повторную обработку .Take(100).ToList(); //помечаем дома, для которых начата обработка houses.ForEach(x => { x.State = HouseInfo.DownloadState.ParseStarted; }); session.SaveChanges(); } foreach (var house in houses) { //производим обработку houses.ParsedData = ParseData(house); //переводим в завершенное состояние houses.State = HouseInfo.DownloadState.Finished; } session.SaveChanges();
Первой неожиданностью стала проблема с .Customize(x => x.WaitForNonStaleResultsAsOfNow()). Точнее, с её изначальным отсутствием :) Из-за этого два соседних потока регулярно принимали в обработку одни и те же документы (потому что измененный State не успевал попасть в индекс). Проблема «невалидных» индексов вообще стала достаточно серьезной в контексте использования Рейвена в небольших «одноразовых» приложениях.
В моем случае все индексы создавались «динамически» (то есть я не писал MapReduce-классов), поэтому индексы получали статус «временных», и по выходу из приложения, насколько я понял, не сохранялись. Соответственно при новом запуске и попытке выполнить запрос типа session.Query
Второй большой проблемой стала производительность БД в контексте использования WaitForNonStaleResults. С учетом того, что обработка блоков в 100 документов занимала не слишком много времени (порядка 3-4 секунд на 100 документов), основным «тормозом» стала как раз выборка данных. Пауза на запросе достигала 15 секунд, что меня весьма печалило.
К концу обработки (где-то на 35К документе) Рейвен раскочегарился и сократил «зависания» до 1-2 секунд, что, в общем-то, вполне приемлемо, однако возможно, что это продлилось бы недолго :)
В сухом итоге впечатления от RavenDB Embedded:
+ очень быстрый старт (установка nuget-пакета), отсутствие маппингов;
+ удобный и понятный синтаксис запросов (знакомый всем linq)
— производительность при последовательных операциях записи/чтения, при строгой необходимости актуальности данных
— несколько непривычный синтаксис запросов в Raven Studio (lucene, a не SQL или linq)
В принципе поставленную задачу Рейвен решил.
В общем-то, задачу оптимизации WaitForNonStaleResults также можно было решить, если в запросах проверять не меняющийся State, а, допустим, id и обрабатывать документы в строго возрастающем порядке. Да, использование nosql даже в относительно простых случаях заставляет думать слегка «по-другому» в сравнении с миром sql :)
P.S. Ну и слегка непонятно, насколько легально такое «одноразовое» использование с учетом отсутствия у меня каких бы то ни было лицензий на Рейвен :)
Хм.. Считаю приведенный подход неправильным, даже для прототипа или одноразового решения
Следовало делать выборку в одном потоке и пихать в какую нить очередь, а в других потоках — обрабатывать эту очередь.
Размер очереди ограничить, например — количеством потоков обрабатывающих сообщения
Выбирать сразу сто и помечать их тут же как обрабатываемые.. странно, а что если памяти не хватит, свет выключат, или астероид упадет, а у вас пока 99-е сообщение не обработается — все предыдущие не пометятся как обрабатываемые =)
По сабжу: RavenDB в целом вещь прикольная
Спасибо за наводку на очереди, можно ссылку на какой-нибудь пример обработки с их помощью?
В данной ситуации я не до конца представляю как реализовать «динамическое пополнение» очереди, если только не пихать в неё все 50К документов сразу..
Я догадывался, что моё решение скорее всего неидеальное, шел просто по быстрейшему пути — вся утилитка вместе с более чем 10-часовой обработкой уложились в двое суток :)
> Выбирать сразу сто и помечать их тут же как
> обрабатываемые.. странно, а что если памяти не хватит,
> свет выключат, или астероид упадет, а у вас пока 99-е ?
> сообщение не обработается — все предыдущие не
> пометятся как обрабатываемые =)
Я там сохранял еще дату последней операции, и «зависшие» в обработке можно было просто сбрасывать через некоторое время. Ну и потерянное время на обработке сотни доков — пара секунд, так что это были мелочи :)
Ну а вынос SaveChanges за пределы цикла был в качестве небольшой оптимизации, возможно, лишней :)
Рейвен удобен, но я не ожадал такой засады в производительности :) Казалось бы, 100 изменившихся документов переиндексировать.. а задержки секунд на 10.
Пример с очередью…
Лучше ознакомьтесь с Concurrent-классами, например ConcurrentQueue http://msdn.microsoft.com/ru-ru/library/dd267265.aspx
Там и пример, кстати, есть )
Почитайте вот тут небольшие заметки http://www.sansys.net/search/label/RavenDb
+ рекомендую книги:
1. Pro .NET 4 Parallel Programming in C# http://goo.gl/DROUb (хотя, признаюсь, сам читал лишь пару глав из нее)
2. CLR via C#, но уверен, что она у вас есть )
Спасибо, с очередями как таковыми я знаком, конечно, но книжку обязательно пролистаю :)
Интересен вопрос конкретного применения для параллелизации обработки большого массива документов, может есть какие-то примеры?
Решение «в лоб» — засунуть все документы в очередь и обрабатывать её в двух потоках — как-то не очень хорошо смотрится. Интересны другие возможные варианты :)